#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 HTTPNSURLContext; class HTTPRequest : public RequestBase { public: HTTPRequest(HTTPNSURLContext*, const Resource&, Callback, uv_loop_t*, std::shared_ptr); ~HTTPRequest(); void cancel() override; void retry() override; private: void start(); void handleResult(NSData *data, NSURLResponse *res, NSError *error); void handleResponse(); void retry(uint64_t timeout); HTTPNSURLContext *context = nullptr; bool cancelled = false; NSURLSessionDataTask *task = nullptr; std::unique_ptr response; const std::shared_ptr existingResponse; ResponseStatus status = ResponseStatus::PermanentError; uv::async async; uv::timer timer; int attempts = 0; enum : bool { PreemptImmediately, ExponentialBackoff } strategy = PreemptImmediately; static const int maxAttempts = 4; }; // ------------------------------------------------------------------------------------------------- class HTTPNSURLContext : public HTTPContext { public: HTTPNSURLContext(uv_loop_t *loop); ~HTTPNSURLContext(); RequestBase* createRequest(const Resource&, RequestBase::Callback, uv_loop_t*, std::shared_ptr) override; NSURLSession *session = nil; NSString *userAgent = nil; }; HTTPNSURLContext::HTTPNSURLContext(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"; } } HTTPNSURLContext::~HTTPNSURLContext() { [session release]; session = nullptr; [userAgent release]; userAgent = nullptr; } RequestBase* HTTPNSURLContext::createRequest(const Resource& resource, RequestBase::Callback callback, uv_loop_t* loop, std::shared_ptr response) { return new HTTPRequest(this, resource, callback, loop, response); } // ------------------------------------------------------------------------------------------------- HTTPRequest::HTTPRequest(HTTPNSURLContext* context_, const Resource& resource_, Callback callback_, uv_loop_t *loop, std::shared_ptr existingResponse_) : RequestBase(resource_, callback_), context(context_), existingResponse(existingResponse_), async(loop, [this] { handleResponse(); }), timer(loop) { context->addRequest(this); start(); } HTTPRequest::~HTTPRequest() { assert(!task); // Stop the backoff timer to avoid re-triggering this request. timer.stop(); context->removeRequest(this); } void HTTPRequest::start() { assert(!task); attempts++; @autoreleasepool { NSURL *url = [NSURL URLWithString:@(resource.url.c_str())]; if ([url.host isEqualToString:@"mapbox.com"] || [url.host hasSuffix:@".mapbox.com"]) { if ([[NSUserDefaults standardUserDefaults] integerForKey:@"MGLMapboxAccountType"] == 0) { NSString *absoluteString = [url.absoluteString stringByAppendingFormat: (url.query ? @"&%@" : @"?%@"), @"events=true"]; url = [NSURL URLWithString:absoluteString]; } } NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:url]; 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); }]; [task retain]; [task resume]; } } void HTTPRequest::handleResponse() { if (task) { [task release]; task = nullptr; } if (!cancelled) { 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) { notify(std::move(response), FileCache::Hint::Refresh); } else { notify(std::move(response), FileCache::Hint::Full); } } delete this; } void HTTPRequest::cancel() { context->removeRequest(this); cancelled = true; // Stop the backoff timer to avoid re-triggering this request. timer.stop(); if (task) { [task cancel]; [task release]; task = nullptr; } else { delete this; } } 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 HTTPRequest::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 copy over the existing response's data. response->status = existingResponse->status; response->message = existingResponse->message; response->modified = existingResponse->modified; response->etag = existingResponse->etag; response->data = existingResponse->data; 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"; } async.send(); } void HTTPRequest::retry(uint64_t timeout) { response.reset(); timer.stop(); timer.start(timeout, 0, [this] { start(); }); } void HTTPRequest::retry() { // 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 (strategy == PreemptImmediately) { // Triggers the timer upon the next event loop iteration. timer.stop(); timer.start(0, 0, [this] { start(); }); } } std::unique_ptr HTTPContext::createContext(uv_loop_t* loop) { return util::make_unique(loop); } }