diff options
Diffstat (limited to 'platform/darwin/http_request_cocoa.mm')
-rw-r--r-- | platform/darwin/http_request_cocoa.mm | 388 |
1 files changed, 388 insertions, 0 deletions
diff --git a/platform/darwin/http_request_cocoa.mm b/platform/darwin/http_request_cocoa.mm new file mode 100644 index 0000000000..163d4cf2db --- /dev/null +++ b/platform/darwin/http_request_cocoa.mm @@ -0,0 +1,388 @@ +#include <mbgl/storage/default/http_request.hpp> +#include <mbgl/storage/default/http_context.hpp> +#include <mbgl/storage/response.hpp> +#include <mbgl/util/uv.hpp> + +#include <mbgl/util/time.hpp> +#include <mbgl/util/parsedate.h> + +#import <Foundation/Foundation.h> + +#include <map> +#include <cassert> + +dispatch_once_t request_initialize = 0; +NSURLSession *session = nullptr; +NSString *userAgent = nil; + +namespace mbgl { + +enum class ResponseStatus : uint8_t { + // This error probably won't be resolved by retrying anytime soon. We are giving up. + PermanentError, + + // This error might be resolved by waiting some time (e.g. server issues). + // We are going to do an exponential back-off and will try again in a few seconds. + TemporaryError, + + // This error was caused by a temporary error and it is likely that it will be resolved + // immediately. We are going to try again right away. This is like the TemporaryError, except + // that we will not perform exponential back-off. + SingularError, + + // This error might be resolved once the network reachability status changes. + // We are going to watch the network status for changes and will retry as soon as the + // operating system notifies us of a network status change. + ConnectionError, + + // The request was canceled mid-way. + Canceled, + + // The request returned data successfully. We retrieved and decoded the data successfully. + Successful, + + // The request confirmed that the data wasn't changed. We already have the data. + NotModified, +}; + +// ------------------------------------------------------------------------------------------------- + +class HTTPCocoaContext; + +class HTTPRequestImpl { +public: + HTTPRequestImpl(HTTPRequest *request, uv_loop_t *loop, std::unique_ptr<Response> response); + ~HTTPRequestImpl(); + + void cancel(); + + void start(); + void handleResult(NSData *data, NSURLResponse *res, NSError *error); + void handleResponse(); + + void retry(uint64_t timeout); + void retryImmediately(); + static void restart(uv_timer_t *timer, int); + +private: + HTTPCocoaContext *context = nullptr; + HTTPRequest *request = nullptr; + NSURLSessionDataTask *task = nullptr; + std::unique_ptr<Response> response; + std::unique_ptr<Response> existingResponse; + ResponseStatus status = ResponseStatus::PermanentError; + uv_async_t *async = nullptr; + int attempts = 0; + uv_timer_t *timer = nullptr; + enum : bool { PreemptImmediately, ExponentialBackoff } strategy = PreemptImmediately; + + static const int maxAttempts = 4; +}; + +// ------------------------------------------------------------------------------------------------- + +class HTTPCocoaContext : public HTTPContext<HTTPCocoaContext> { +public: + HTTPCocoaContext(uv_loop_t *loop); +}; + +template<> pthread_key_t HTTPContext<HTTPCocoaContext>::key{}; +template<> pthread_once_t HTTPContext<HTTPCocoaContext>::once = PTHREAD_ONCE_INIT; + +HTTPCocoaContext::HTTPCocoaContext(uv_loop_t *loop_) : HTTPContext(loop_) {} + +// ------------------------------------------------------------------------------------------------- + +HTTPRequestImpl::HTTPRequestImpl(HTTPRequest *request_, uv_loop_t *loop, + std::unique_ptr<Response> existingResponse_) + : context(HTTPCocoaContext::Get(loop)), + request(request_), + existingResponse(std::move(existingResponse_)), + async(new uv_async_t) { + assert(request); + context->addRequest(request); + + async->data = this; + uv_async_init(loop, async, [](uv_async_t *as, int) { + auto impl = reinterpret_cast<HTTPRequestImpl *>(as->data); + impl->handleResponse(); + }); + + start(); +} + +void HTTPRequestImpl::start() { + assert(!task); + + attempts++; + + @autoreleasepool { + NSMutableURLRequest *req = [[NSMutableURLRequest alloc] + initWithURL:[NSURL URLWithString:@(request->resource.url.c_str())]]; + if (existingResponse) { + if (!existingResponse->etag.empty()) { + [req addValue:@(existingResponse->etag.c_str()) forHTTPHeaderField:@"If-None-Match"]; + } else if (existingResponse->modified) { + const std::string time = util::rfc1123(existingResponse->modified); + [req addValue:@(time.c_str()) forHTTPHeaderField:@"If-Modified-Since"]; + } + } + + [req addValue:userAgent forHTTPHeaderField:@"User-Agent"]; + + task = [session dataTaskWithRequest:req + completionHandler:^(NSData *data, NSURLResponse *res, + NSError *error) { handleResult(data, res, error); }]; + [req release]; + [task resume]; + } +} + +void HTTPRequestImpl::handleResponse() { + task = nullptr; + + if (request) { + if (status == ResponseStatus::TemporaryError && attempts < maxAttempts) { + strategy = ExponentialBackoff; + return retry((1 << (attempts - 1)) * 1000); + } else if (status == ResponseStatus::ConnectionError && attempts < maxAttempts) { + // By default, we will retry every 30 seconds (network change notification will + // preempt the timeout). + strategy = PreemptImmediately; + return retry(30000); + } + + // Actually return the response. + if (status == ResponseStatus::NotModified) { + request->notify(std::move(response), FileCache::Hint::Refresh); + } else { + request->notify(std::move(response), FileCache::Hint::Full); + } + + context->removeRequest(request); + delete request; + request = nullptr; + } + + delete this; +} + +void HTTPRequestImpl::cancel() { + context->removeRequest(request); + request = nullptr; + + [task cancel]; + task = nullptr; +} + +HTTPRequestImpl::~HTTPRequestImpl() { + assert(!task); + assert(async); + + uv::close(async); + + if (request) { + context->removeRequest(request); + request->ptr = nullptr; + } +} + +int64_t parseCacheControl(const char *value) { + if (value) { + unsigned long long seconds = 0; + // TODO: cache-control may contain other information as well: + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 + if (std::sscanf(value, "max-age=%llu", &seconds) == 1) { + return std::chrono::duration_cast<std::chrono::seconds>( + std::chrono::system_clock::now().time_since_epoch()).count() + + seconds; + } + } + + return 0; +} + +void HTTPRequestImpl::handleResult(NSData *data, NSURLResponse *res, NSError *error) { + if (error) { + if ([error code] == NSURLErrorCancelled) { + status = ResponseStatus::Canceled; + } else { + // TODO: Use different codes for host not found, timeout, invalid URL etc. + // These can be categorized in temporary and permanent errors. + response = util::make_unique<Response>(); + response->status = Response::Error; + response->message = [[error localizedDescription] UTF8String]; + + switch ([error code]) { + case NSURLErrorBadServerResponse: // 5xx errors + status = ResponseStatus::TemporaryError; + break; + + case NSURLErrorTimedOut: + case NSURLErrorUserCancelledAuthentication: + status = ResponseStatus::SingularError; // retry immediately + break; + + case NSURLErrorNetworkConnectionLost: + case NSURLErrorCannotFindHost: + case NSURLErrorCannotConnectToHost: + case NSURLErrorDNSLookupFailed: + case NSURLErrorNotConnectedToInternet: + case NSURLErrorInternationalRoamingOff: + case NSURLErrorCallIsActive: + case NSURLErrorDataNotAllowed: + status = ResponseStatus::ConnectionError; + break; + + default: + status = ResponseStatus::PermanentError; + } + } + } else if ([res isKindOfClass:[NSHTTPURLResponse class]]) { + const long responseCode = [(NSHTTPURLResponse *)res statusCode]; + + response = util::make_unique<Response>(); + response->data = {(const char *)[data bytes], [data length]}; + + NSDictionary *headers = [(NSHTTPURLResponse *)res allHeaderFields]; + NSString *cache_control = [headers objectForKey:@"Cache-Control"]; + if (cache_control) { + response->expires = parseCacheControl([cache_control UTF8String]); + } + + NSString *expires = [headers objectForKey:@"Expires"]; + if (expires) { + response->expires = parse_date([expires UTF8String]); + } + + NSString *last_modified = [headers objectForKey:@"Last-Modified"]; + if (last_modified) { + response->modified = parse_date([last_modified UTF8String]); + } + + NSString *etag = [headers objectForKey:@"ETag"]; + if (etag) { + response->etag = [etag UTF8String]; + } + + if (responseCode == 304) { + if (existingResponse) { + // We're going to reuse the old response object, but need to copy over the new + // expires value (if possible). + std::swap(response, existingResponse); + if (existingResponse->expires) { + response->expires = existingResponse->expires; + } + status = ResponseStatus::NotModified; + } else { + // This is an unsolicited 304 response and should only happen on malfunctioning + // HTTP servers. It likely doesn't include any data, but we don't have much options. + response->status = Response::Successful; + status = ResponseStatus::Successful; + } + } else if (responseCode == 200) { + response->status = Response::Successful; + status = ResponseStatus::Successful; + } else if (responseCode >= 500 && responseCode < 600) { + // Server errors may be temporary, so back off exponentially. + response->status = Response::Error; + response->message = "HTTP status code " + std::to_string(responseCode); + status = ResponseStatus::TemporaryError; + } else { + // We don't know how to handle any other errors, so declare them as permanently failing. + response->status = Response::Error; + response->message = "HTTP status code " + std::to_string(responseCode); + status = ResponseStatus::PermanentError; + } + } else { + // This should never happen. + status = ResponseStatus::PermanentError; + response = util::make_unique<Response>(); + response->status = Response::Error; + response->message = "response class is not NSHTTPURLResponse"; + } + + uv_async_send(async); +} + +void HTTPRequestImpl::retry(uint64_t timeout) { + response.reset(); + + assert(!timer); + timer = new uv_timer_t; + timer->data = this; + uv_timer_init(async->loop, timer); + uv_timer_start(timer, restart, timeout, 0); +} + +void HTTPRequestImpl::retryImmediately() { + // All batons get notified when the network status changed, but some of them + // might not actually wait for the network to become available again. + if (timer && strategy == PreemptImmediately) { + // Triggers the timer upon the next event loop iteration. + uv_timer_stop(timer); + uv_timer_start(timer, restart, 0, 0); + } +} + +void HTTPRequestImpl::restart(uv_timer_t *timer, int) { + // Restart the request. + auto impl = reinterpret_cast<HTTPRequestImpl *>(timer->data); + + // Get rid of the timer. + impl->timer = nullptr; + uv::close(timer); + + impl->start(); +} + +// ------------------------------------------------------------------------------------------------- + +HTTPRequest::HTTPRequest(DefaultFileSource *source, const Resource &resource) + : SharedRequestBase(source, resource) { + // Global initialization. + dispatch_once(&request_initialize, ^{ + NSURLSessionConfiguration *sessionConfig = + [NSURLSessionConfiguration defaultSessionConfiguration]; + sessionConfig.timeoutIntervalForResource = 30; + sessionConfig.HTTPMaximumConnectionsPerHost = 8; + sessionConfig.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData; + sessionConfig.URLCache = nil; + + session = [NSURLSession sessionWithConfiguration:sessionConfig]; + + // Write user agent string + userAgent = @"MapboxGL"; + }); +} + +HTTPRequest::~HTTPRequest() { + MBGL_VERIFY_THREAD(tid); + + if (ptr) { + reinterpret_cast<HTTPRequestImpl *>(ptr)->cancel(); + } +} + +void HTTPRequest::start(uv_loop_t *loop, std::unique_ptr<Response> response) { + MBGL_VERIFY_THREAD(tid); + + assert(!ptr); + ptr = new HTTPRequestImpl(this, loop, std::move(response)); +} + +void HTTPRequest::retryImmediately() { + MBGL_VERIFY_THREAD(tid); + + if (ptr) { + reinterpret_cast<HTTPRequestImpl *>(ptr)->retryImmediately(); + } +} + +void HTTPRequest::cancel() { + MBGL_VERIFY_THREAD(tid); + + delete this; +} + +} |