#include #include #include #include #include #include #import #include #include 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); ~HTTPRequestImpl(); void cancel(); void cancelTimer(); 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; std::unique_ptr 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 { public: HTTPCocoaContext(uv_loop_t *loop); ~HTTPCocoaContext(); NSURLSession *session = nil; NSString *userAgent = nil; }; template<> pthread_key_t HTTPContext::key{}; template<> pthread_once_t HTTPContext::once = PTHREAD_ONCE_INIT; HTTPCocoaContext::HTTPCocoaContext(uv_loop_t *loop_) : HTTPContext(loop_) { @autoreleasepool { NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; sessionConfig.timeoutIntervalForResource = 30; sessionConfig.HTTPMaximumConnectionsPerHost = 8; sessionConfig.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData; sessionConfig.URLCache = nil; session = [NSURLSession sessionWithConfiguration:sessionConfig]; [session retain]; // Write user agent string userAgent = @"MapboxGL"; } } HTTPCocoaContext::~HTTPCocoaContext() { [session release]; session = nullptr; [userAgent release]; userAgent = nullptr; } // ------------------------------------------------------------------------------------------------- HTTPRequestImpl::HTTPRequestImpl(HTTPRequest *request_, uv_loop_t *loop, std::unique_ptr 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(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:context->userAgent forHTTPHeaderField:@"User-Agent"]; task = [context->session dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *res, NSError *error) { handleResult(data, res, error); }]; [req release]; [task retain]; [task resume]; } } void HTTPRequestImpl::handleResponse() { if (task) { [task release]; 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; // Stop the backoff timer to avoid re-triggering this request. cancelTimer(); if (task) { [task cancel]; [task release]; task = nullptr; } } void HTTPRequestImpl::cancelTimer() { if (timer) { uv_timer_stop(timer); uv::close(timer); timer = nullptr; } } HTTPRequestImpl::~HTTPRequestImpl() { assert(!task); assert(async); // Stop the backoff timer to avoid re-triggering this request. cancelTimer(); 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::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->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->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->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(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) { } HTTPRequest::~HTTPRequest() { MBGL_VERIFY_THREAD(tid); if (ptr) { reinterpret_cast(ptr)->cancel(); } } void HTTPRequest::start(uv_loop_t *loop, std::unique_ptr 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(ptr)->retryImmediately(); } } void HTTPRequest::cancel() { MBGL_VERIFY_THREAD(tid); delete this; } }