| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "nacl_io/httpfs/http_fs_node.h" |
| |
| #include <assert.h> |
| #include <errno.h> |
| #include <stdio.h> |
| #include <string.h> |
| |
| #include <ppapi/c/pp_errors.h> |
| |
| #include "nacl_io/httpfs/http_fs.h" |
| #include "nacl_io/http_status_codes.h" |
| #include "nacl_io/kernel_handle.h" |
| #include "nacl_io/osinttypes.h" |
| |
| #if defined(WIN32) |
| #define snprintf _snprintf |
| #endif |
| |
| namespace nacl_io { |
| |
| namespace { |
| |
| // If we're attempting to read a partial request, but the server returns a full |
| // request, we need to read all of the data up to the start of our partial |
| // request into a dummy buffer. This is the maximum size of that buffer. |
| const int MAX_READ_BUFFER_SIZE = 64 * 1024; |
| |
| StringMap_t ParseHeaders(const char* headers, int32_t headers_length) { |
| enum State { |
| FINDING_KEY, |
| SKIPPING_WHITESPACE, |
| FINDING_VALUE, |
| }; |
| |
| StringMap_t result; |
| std::string key; |
| std::string value; |
| |
| State state = FINDING_KEY; |
| const char* start = headers; |
| for (int i = 0; i < headers_length; ++i) { |
| switch (state) { |
| case FINDING_KEY: |
| if (headers[i] == ':') { |
| // Found key. |
| key.assign(start, &headers[i] - start); |
| key = NormalizeHeaderKey(key); |
| state = SKIPPING_WHITESPACE; |
| } |
| break; |
| |
| case SKIPPING_WHITESPACE: |
| if (headers[i] == ' ') { |
| // Found whitespace, keep going... |
| break; |
| } |
| |
| // Found a non-whitespace, mark this as the start of the value. |
| start = &headers[i]; |
| state = FINDING_VALUE; |
| |
| // NOTE: Avoid fallthrough as it produces a warning on newer compilers, |
| // but can't easily be silenced by the older NaCl compilers. |
| // |
| // Fallthrough to start processing value without incrementing i. |
| goto finding_value; |
| |
| finding_value: |
| case FINDING_VALUE: |
| if (headers[i] == '\n') { |
| // Found value. |
| value.assign(start, &headers[i] - start); |
| result[key] = value; |
| start = &headers[i + 1]; |
| state = FINDING_KEY; |
| } |
| break; |
| } |
| } |
| |
| return result; |
| } |
| |
| bool ParseContentLength(const StringMap_t& headers, off_t* content_length) { |
| StringMap_t::const_iterator iter = headers.find("Content-Length"); |
| if (iter == headers.end()) |
| return false; |
| |
| *content_length = strtoull(iter->second.c_str(), NULL, 10); |
| return true; |
| } |
| |
| bool ParseContentRange(const StringMap_t& headers, |
| off_t* read_start, |
| off_t* read_end, |
| off_t* entity_length) { |
| StringMap_t::const_iterator iter = headers.find("Content-Range"); |
| if (iter == headers.end()) |
| return false; |
| |
| // The key should look like "bytes ##-##/##" or "bytes ##-##/*". The last |
| // value is the entity length, which can potentially be * (i.e. unknown). |
| off_t read_start_int; |
| off_t read_end_int; |
| off_t entity_length_int; |
| int result = sscanf(iter->second.c_str(), |
| "bytes %" SCNi64 "-%" SCNi64 "/%" SCNi64, |
| &read_start_int, |
| &read_end_int, |
| &entity_length_int); |
| |
| // The Content-Range header specifies an inclusive range: e.g. the first ten |
| // bytes is "bytes 0-9/*". Convert it to a half-open range by incrementing |
| // read_end. |
| if (result == 2) { |
| if (read_start) |
| *read_start = read_start_int; |
| if (read_end) |
| *read_end = read_end_int + 1; |
| if (entity_length) |
| *entity_length = 0; |
| return true; |
| } else if (result == 3) { |
| if (read_start) |
| *read_start = read_start_int; |
| if (read_end) |
| *read_end = read_end_int + 1; |
| if (entity_length) |
| *entity_length = entity_length_int; |
| return true; |
| } |
| |
| return false; |
| } |
| |
| // Maps an HTTP |status_code| onto the appropriate errno code. |
| int HTTPStatusCodeToErrno(int status_code) { |
| switch (status_code) { |
| case STATUSCODE_OK: |
| case STATUSCODE_PARTIAL_CONTENT: |
| return 0; |
| case STATUSCODE_FORBIDDEN: |
| return EACCES; |
| case STATUSCODE_NOT_FOUND: |
| return ENOENT; |
| } |
| if (status_code >= 400 && status_code < 500) |
| return EINVAL; |
| return EIO; |
| } |
| |
| } // namespace |
| |
| void HttpFsNode::SetCachedSize(off_t size) { |
| has_cached_size_ = true; |
| stat_.st_size = size; |
| } |
| |
| Error HttpFsNode::FSync() { |
| return EACCES; |
| } |
| |
| Error HttpFsNode::GetDents(size_t offs, |
| struct dirent* pdir, |
| size_t count, |
| int* out_bytes) { |
| *out_bytes = 0; |
| return EACCES; |
| } |
| |
| Error HttpFsNode::GetStat(struct stat* stat) { |
| AUTO_LOCK(node_lock_); |
| return GetStat_Locked(stat); |
| } |
| |
| Error HttpFsNode::Read(const HandleAttr& attr, |
| void* buf, |
| size_t count, |
| int* out_bytes) { |
| *out_bytes = 0; |
| |
| AUTO_LOCK(node_lock_); |
| if (cache_content_) { |
| if (cached_data_.empty()) { |
| Error error = DownloadToCache(); |
| if (error) |
| return error; |
| } |
| |
| return ReadPartialFromCache(attr, buf, count, out_bytes); |
| } |
| |
| return DownloadPartial(attr, buf, count, out_bytes); |
| } |
| |
| Error HttpFsNode::FTruncate(off_t size) { |
| return EACCES; |
| } |
| |
| Error HttpFsNode::Write(const HandleAttr& attr, |
| const void* buf, |
| size_t count, |
| int* out_bytes) { |
| // TODO(binji): support POST? |
| *out_bytes = 0; |
| return EACCES; |
| } |
| |
| Error HttpFsNode::GetSize(off_t* out_size) { |
| *out_size = 0; |
| |
| // TODO(binji): This value should be cached properly; i.e. obey the caching |
| // headers returned by the server. |
| AUTO_LOCK(node_lock_); |
| struct stat statbuf; |
| Error error = GetStat_Locked(&statbuf); |
| if (error) |
| return error; |
| |
| *out_size = stat_.st_size; |
| return 0; |
| } |
| |
| HttpFsNode::HttpFsNode(Filesystem* filesystem, |
| const std::string& url, |
| bool cache_content) |
| : Node(filesystem), |
| url_(url), |
| buffer_(NULL), |
| buffer_len_(0), |
| cache_content_(cache_content), |
| has_cached_size_(false) { |
| // http nodes are read-only by default |
| SetMode(S_IRALL); |
| } |
| |
| HttpFsNode::~HttpFsNode() { |
| free(buffer_); |
| } |
| |
| Error HttpFsNode::GetStat_Locked(struct stat* stat) { |
| // Assume we need to 'HEAD' if we do not know the size, otherwise, assume |
| // that the information is constant. We can add a timeout if needed. |
| HttpFs* filesystem = static_cast<HttpFs*>(filesystem_); |
| if (!has_cached_size_ || !filesystem->cache_stat_) { |
| StringMap_t headers; |
| ScopedResource loader(filesystem_->ppapi()); |
| ScopedResource request(filesystem_->ppapi()); |
| ScopedResource response(filesystem_->ppapi()); |
| int32_t statuscode; |
| StringMap_t response_headers; |
| const char* method = "HEAD"; |
| |
| if (filesystem->is_blob_url_) { |
| // Blob URLs do not support HEAD requests, but do give the content length |
| // in their response headers. We issue a single-byte GET request to |
| // retrieve the content length. |
| method = "GET"; |
| headers["Range"] = "bytes=0-0"; |
| } |
| |
| Error error = OpenUrl(method, |
| &headers, |
| &loader, |
| &request, |
| &response, |
| &statuscode, |
| &response_headers); |
| if (error) |
| return error; |
| |
| off_t entity_length; |
| if (ParseContentRange(response_headers, NULL, NULL, &entity_length)) { |
| SetCachedSize(static_cast<off_t>(entity_length)); |
| } else if (ParseContentLength(response_headers, &entity_length)) { |
| SetCachedSize(static_cast<off_t>(entity_length)); |
| } else if (cache_content_) { |
| // The server didn't give a content length; download the data to memory |
| // via DownloadToCache, which will also set stat_.st_size; |
| error = DownloadToCache(); |
| if (error) |
| return error; |
| } else { |
| // The user doesn't want to cache content, but we didn't get a |
| // "Content-Length" header. Read the entire entity, and throw it away. |
| // Don't use DownloadToCache, as that will still allocate enough memory |
| // for the entire entity. |
| off_t bytes_read; |
| error = DownloadToTemp(&bytes_read); |
| if (error) |
| return error; |
| |
| SetCachedSize(bytes_read); |
| } |
| |
| stat_.st_atime = 0; // TODO(binji): Use "Last-Modified". |
| stat_.st_mtime = 0; |
| stat_.st_ctime = 0; |
| |
| SetType(S_IFREG); |
| } |
| |
| // Fill the stat structure if provided |
| if (stat) |
| *stat = stat_; |
| |
| return 0; |
| } |
| |
| Error HttpFsNode::OpenUrl(const char* method, |
| StringMap_t* request_headers, |
| ScopedResource* out_loader, |
| ScopedResource* out_request, |
| ScopedResource* out_response, |
| int32_t* out_statuscode, |
| StringMap_t* out_response_headers) { |
| // Clear all out parameters. |
| *out_statuscode = 0; |
| out_response_headers->clear(); |
| |
| // Assume lock_ is already held. |
| PepperInterface* ppapi = filesystem_->ppapi(); |
| |
| HttpFs* mount_http = static_cast<HttpFs*>(filesystem_); |
| out_request->Reset( |
| mount_http->MakeUrlRequestInfo(url_, method, request_headers)); |
| if (!out_request->pp_resource()) |
| return EINVAL; |
| |
| URLLoaderInterface* loader_interface = ppapi->GetURLLoaderInterface(); |
| URLResponseInfoInterface* response_interface = |
| ppapi->GetURLResponseInfoInterface(); |
| VarInterface* var_interface = ppapi->GetVarInterface(); |
| |
| out_loader->Reset(loader_interface->Create(ppapi->GetInstance())); |
| if (!out_loader->pp_resource()) |
| return EINVAL; |
| |
| int32_t result = loader_interface->Open(out_loader->pp_resource(), |
| out_request->pp_resource(), |
| PP_BlockUntilComplete()); |
| if (result != PP_OK) |
| return PPERROR_TO_ERRNO(result); |
| |
| out_response->Reset( |
| loader_interface->GetResponseInfo(out_loader->pp_resource())); |
| if (!out_response->pp_resource()) |
| return EINVAL; |
| |
| // Get response statuscode. |
| PP_Var statuscode = response_interface->GetProperty( |
| out_response->pp_resource(), PP_URLRESPONSEPROPERTY_STATUSCODE); |
| |
| if (statuscode.type != PP_VARTYPE_INT32) |
| return EINVAL; |
| |
| *out_statuscode = statuscode.value.as_int; |
| |
| // Only accept OK or Partial Content. |
| Error error = HTTPStatusCodeToErrno(*out_statuscode); |
| if (error) |
| return error; |
| |
| // Get response headers. |
| PP_Var response_headers_var = response_interface->GetProperty( |
| out_response->pp_resource(), PP_URLRESPONSEPROPERTY_HEADERS); |
| |
| uint32_t response_headers_length; |
| const char* response_headers_str = |
| var_interface->VarToUtf8(response_headers_var, &response_headers_length); |
| |
| *out_response_headers = |
| ParseHeaders(response_headers_str, response_headers_length); |
| |
| var_interface->Release(response_headers_var); |
| |
| return 0; |
| } |
| |
| Error HttpFsNode::DownloadToCache() { |
| StringMap_t headers; |
| ScopedResource loader(filesystem_->ppapi()); |
| ScopedResource request(filesystem_->ppapi()); |
| ScopedResource response(filesystem_->ppapi()); |
| int32_t statuscode; |
| StringMap_t response_headers; |
| Error error = OpenUrl("GET", |
| &headers, |
| &loader, |
| &request, |
| &response, |
| &statuscode, |
| &response_headers); |
| if (error) |
| return error; |
| |
| off_t content_length = 0; |
| if (ParseContentLength(response_headers, &content_length)) { |
| cached_data_.resize(content_length); |
| int real_size; |
| error = ReadResponseToBuffer( |
| loader, cached_data_.data(), content_length, &real_size); |
| if (error) |
| return error; |
| |
| SetCachedSize(real_size); |
| cached_data_.resize(real_size); |
| return 0; |
| } |
| |
| int bytes_read; |
| error = ReadEntireResponseToCache(loader, &bytes_read); |
| if (error) |
| return error; |
| |
| SetCachedSize(bytes_read); |
| return 0; |
| } |
| |
| Error HttpFsNode::ReadPartialFromCache(const HandleAttr& attr, |
| void* buf, |
| int count, |
| int* out_bytes) { |
| *out_bytes = 0; |
| off_t size = cached_data_.size(); |
| |
| if (attr.offs + count > size) |
| count = size - attr.offs; |
| |
| if (count <= 0) |
| return 0; |
| |
| memcpy(buf, &cached_data_.data()[attr.offs], count); |
| *out_bytes = count; |
| return 0; |
| } |
| |
| Error HttpFsNode::DownloadPartial(const HandleAttr& attr, |
| void* buf, |
| off_t count, |
| int* out_bytes) { |
| *out_bytes = 0; |
| |
| StringMap_t headers; |
| |
| char buffer[100]; |
| // Range request is inclusive: 0-99 returns 100 bytes. |
| snprintf(&buffer[0], |
| sizeof(buffer), |
| "bytes=%" PRIi64 "-%" PRIi64, |
| attr.offs, |
| attr.offs + count - 1); |
| headers["Range"] = buffer; |
| |
| ScopedResource loader(filesystem_->ppapi()); |
| ScopedResource request(filesystem_->ppapi()); |
| ScopedResource response(filesystem_->ppapi()); |
| int32_t statuscode; |
| StringMap_t response_headers; |
| Error error = OpenUrl("GET", |
| &headers, |
| &loader, |
| &request, |
| &response, |
| &statuscode, |
| &response_headers); |
| if (error) { |
| if (statuscode == STATUSCODE_REQUESTED_RANGE_NOT_SATISFIABLE) { |
| // We're likely trying to read past the end. Return 0 bytes. |
| *out_bytes = 0; |
| return 0; |
| } |
| |
| return error; |
| } |
| |
| off_t read_start = 0; |
| if (statuscode == STATUSCODE_OK) { |
| // No partial result, read everything starting from the part we care about. |
| off_t content_length; |
| if (ParseContentLength(response_headers, &content_length)) { |
| if (attr.offs >= content_length) |
| return EINVAL; |
| |
| // Clamp count, if trying to read past the end of the file. |
| if (attr.offs + count > content_length) { |
| count = content_length - attr.offs; |
| } |
| } |
| } else if (statuscode == STATUSCODE_PARTIAL_CONTENT) { |
| // Determine from the headers where we are reading. |
| off_t read_end; |
| off_t entity_length; |
| if (ParseContentRange( |
| response_headers, &read_start, &read_end, &entity_length)) { |
| if (read_start > attr.offs || read_start > read_end) { |
| // If this error occurs, the server is returning bogus values. |
| return EINVAL; |
| } |
| |
| // Clamp count, if trying to read past the end of the file. |
| count = std::min(read_end - read_start, count); |
| } else { |
| // Partial Content without Content-Range. Assume that the server gave us |
| // exactly what we asked for. This can happen even when the server |
| // returns 200 -- the cache may return 206 in this case, but not modify |
| // the headers. |
| read_start = attr.offs; |
| } |
| } |
| |
| if (read_start < attr.offs) { |
| // We aren't yet at the location where we want to start reading. Read into |
| // our dummy buffer until then. |
| int bytes_to_read = attr.offs - read_start; |
| int bytes_read; |
| error = ReadResponseToTemp(loader, bytes_to_read, &bytes_read); |
| if (error) |
| return error; |
| |
| // Tried to read past the end of the entity. |
| if (bytes_read < bytes_to_read) { |
| *out_bytes = 0; |
| return 0; |
| } |
| } |
| |
| return ReadResponseToBuffer(loader, buf, count, out_bytes); |
| } |
| |
| Error HttpFsNode::DownloadToTemp(off_t* out_bytes) { |
| StringMap_t headers; |
| ScopedResource loader(filesystem_->ppapi()); |
| ScopedResource request(filesystem_->ppapi()); |
| ScopedResource response(filesystem_->ppapi()); |
| int32_t statuscode; |
| StringMap_t response_headers; |
| Error error = OpenUrl("GET", |
| &headers, |
| &loader, |
| &request, |
| &response, |
| &statuscode, |
| &response_headers); |
| if (error) |
| return error; |
| |
| off_t content_length = 0; |
| if (ParseContentLength(response_headers, &content_length)) { |
| *out_bytes = content_length; |
| return 0; |
| } |
| |
| return ReadEntireResponseToTemp(loader, out_bytes); |
| } |
| |
| Error HttpFsNode::ReadEntireResponseToTemp(const ScopedResource& loader, |
| off_t* out_bytes) { |
| *out_bytes = 0; |
| |
| const int kBytesToRead = MAX_READ_BUFFER_SIZE; |
| buffer_ = (char*)realloc(buffer_, kBytesToRead); |
| assert(buffer_); |
| if (!buffer_) { |
| buffer_len_ = 0; |
| return ENOMEM; |
| } |
| buffer_len_ = kBytesToRead; |
| |
| while (true) { |
| int bytes_read; |
| Error error = |
| ReadResponseToBuffer(loader, buffer_, kBytesToRead, &bytes_read); |
| if (error) |
| return error; |
| |
| *out_bytes += bytes_read; |
| |
| if (bytes_read < kBytesToRead) |
| return 0; |
| } |
| } |
| |
| Error HttpFsNode::ReadEntireResponseToCache(const ScopedResource& loader, |
| int* out_bytes) { |
| *out_bytes = 0; |
| const int kBytesToRead = MAX_READ_BUFFER_SIZE; |
| |
| while (true) { |
| // Always recalculate the buf pointer because it may have moved when |
| // cached_data_ was resized. |
| cached_data_.resize(*out_bytes + kBytesToRead); |
| void* buf = cached_data_.data() + *out_bytes; |
| |
| int bytes_read; |
| Error error = ReadResponseToBuffer(loader, buf, kBytesToRead, &bytes_read); |
| if (error) |
| return error; |
| |
| *out_bytes += bytes_read; |
| |
| if (bytes_read < kBytesToRead) { |
| // Shrink the cached data buffer to the correct size. |
| cached_data_.resize(*out_bytes); |
| return 0; |
| } |
| } |
| } |
| |
| Error HttpFsNode::ReadResponseToTemp(const ScopedResource& loader, |
| int count, |
| int* out_bytes) { |
| *out_bytes = 0; |
| |
| if (buffer_len_ < count) { |
| int new_len = std::min(count, MAX_READ_BUFFER_SIZE); |
| buffer_ = (char*)realloc(buffer_, new_len); |
| assert(buffer_); |
| if (!buffer_) { |
| buffer_len_ = 0; |
| return ENOMEM; |
| } |
| buffer_len_ = new_len; |
| } |
| |
| int bytes_left = count; |
| while (bytes_left > 0) { |
| int bytes_to_read = std::min(bytes_left, buffer_len_); |
| int bytes_read; |
| Error error = ReadResponseToBuffer( |
| loader, buffer_, bytes_to_read, &bytes_read); |
| if (error) |
| return error; |
| |
| if (bytes_read == 0) |
| return 0; |
| |
| bytes_left -= bytes_read; |
| *out_bytes += bytes_read; |
| } |
| |
| return 0; |
| } |
| |
| Error HttpFsNode::ReadResponseToBuffer(const ScopedResource& loader, |
| void* buf, |
| int count, |
| int* out_bytes) { |
| *out_bytes = 0; |
| |
| PepperInterface* ppapi = filesystem_->ppapi(); |
| URLLoaderInterface* loader_interface = ppapi->GetURLLoaderInterface(); |
| |
| char* out_buffer = static_cast<char*>(buf); |
| int bytes_to_read = count; |
| while (bytes_to_read > 0) { |
| int bytes_read = |
| loader_interface->ReadResponseBody(loader.pp_resource(), |
| out_buffer, |
| bytes_to_read, |
| PP_BlockUntilComplete()); |
| |
| if (bytes_read == 0) { |
| // This is not an error -- it may just be that we were trying to read |
| // more data than exists. |
| *out_bytes = count - bytes_to_read; |
| return 0; |
| } |
| |
| if (bytes_read < 0) |
| return PPERROR_TO_ERRNO(bytes_read); |
| |
| assert(bytes_read <= bytes_to_read); |
| bytes_to_read -= bytes_read; |
| out_buffer += bytes_read; |
| } |
| |
| *out_bytes = count; |
| return 0; |
| } |
| |
| } // namespace nacl_io |