summaryrefslogtreecommitdiff
path: root/platform/darwin/src/http_file_source.mm
diff options
context:
space:
mode:
Diffstat (limited to 'platform/darwin/src/http_file_source.mm')
-rw-r--r--platform/darwin/src/http_file_source.mm320
1 files changed, 320 insertions, 0 deletions
diff --git a/platform/darwin/src/http_file_source.mm b/platform/darwin/src/http_file_source.mm
new file mode 100644
index 0000000000..eb751258c8
--- /dev/null
+++ b/platform/darwin/src/http_file_source.mm
@@ -0,0 +1,320 @@
+#include <mbgl/storage/http_file_source.hpp>
+#include <mbgl/storage/resource.hpp>
+#include <mbgl/storage/response.hpp>
+
+#include <mbgl/util/http_header.hpp>
+#include <mbgl/util/async_task.hpp>
+
+#include <mbgl/util/version.hpp>
+
+#import <Foundation/Foundation.h>
+
+#include <mutex>
+
+@interface MBGLBundleCanary : NSObject
+@end
+
+@implementation MBGLBundleCanary
+@end
+
+namespace mbgl {
+
+// Data that is shared between the requesting thread and the thread running the completion handler.
+class HTTPRequestShared {
+public:
+ HTTPRequestShared(Response& response_, util::AsyncTask& async_)
+ : response(response_),
+ async(async_) {
+ }
+
+ void notify(const Response& response_) {
+ std::lock_guard<std::mutex> lock(mutex);
+ if (!cancelled) {
+ response = response_;
+ async.send();
+ }
+ }
+
+ void cancel() {
+ std::lock_guard<std::mutex> lock(mutex);
+ cancelled = true;
+ }
+
+private:
+ std::mutex mutex;
+ bool cancelled = false;
+
+ Response& response;
+ util::AsyncTask& async;
+};
+
+class HTTPRequest : public AsyncRequest {
+public:
+ HTTPRequest(FileSource::Callback callback_)
+ : shared(std::make_shared<HTTPRequestShared>(response, async)),
+ callback(callback_) {
+ }
+
+ ~HTTPRequest() override {
+ shared->cancel();
+ if (task) {
+ [task cancel];
+ }
+ }
+
+ std::shared_ptr<HTTPRequestShared> shared;
+ NSURLSessionDataTask* task = nil;
+
+private:
+ FileSource::Callback callback;
+ Response response;
+
+ util::AsyncTask async { [this] {
+ // Calling `callback` may result in deleting `this`. Copy data to temporaries first.
+ auto callback_ = callback;
+ auto response_ = response;
+ callback_(response_);
+ } };
+};
+
+class HTTPFileSource::Impl {
+public:
+ Impl() {
+ @autoreleasepool {
+ NSURLSessionConfiguration* sessionConfig =
+ [NSURLSessionConfiguration defaultSessionConfiguration];
+ sessionConfig.timeoutIntervalForResource = 30;
+ sessionConfig.HTTPMaximumConnectionsPerHost = 8;
+ sessionConfig.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
+ sessionConfig.URLCache = nil;
+
+ session = [NSURLSession sessionWithConfiguration:sessionConfig];
+
+ userAgent = getUserAgent();
+
+ accountType = [[NSUserDefaults standardUserDefaults] integerForKey:@"MGLMapboxAccountType"];
+ }
+ }
+
+ NSURLSession* session = nil;
+ NSString* userAgent = nil;
+ NSInteger accountType = 0;
+
+private:
+ NSString* getUserAgent() const;
+ NSBundle* getSDKBundle() const;
+};
+
+NSString *HTTPFileSource::Impl::getUserAgent() const {
+ NSMutableArray *userAgentComponents = [NSMutableArray array];
+
+ NSBundle *appBundle = [NSBundle mainBundle];
+ if (appBundle) {
+ NSString *appName = appBundle.infoDictionary[@"CFBundleName"];
+ [userAgentComponents addObject:[NSString stringWithFormat:@"%@/%@",
+ appName.length ? appName : appBundle.infoDictionary[@"CFBundleIdentifier"],
+ appBundle.infoDictionary[@"CFBundleShortVersionString"]]];
+ } else {
+ [userAgentComponents addObject:[NSProcessInfo processInfo].processName];
+ }
+
+ NSBundle *sdkBundle = HTTPFileSource::Impl::getSDKBundle();
+ if (sdkBundle) {
+ NSString *versionString = sdkBundle.infoDictionary[@"MGLSemanticVersionString"];
+ if (!versionString) {
+ versionString = sdkBundle.infoDictionary[@"CFBundleShortVersionString"];
+ }
+ if (versionString) {
+ [userAgentComponents addObject:[NSString stringWithFormat:@"%@/%@",
+ sdkBundle.infoDictionary[@"CFBundleName"], versionString]];
+ }
+ }
+
+ // Avoid %s here because it inserts hidden bidirectional markers on OS X when the system
+ // language is set to a right-to-left language.
+ [userAgentComponents addObject:[NSString stringWithFormat:@"MapboxGL/%@ (%@)",
+ CFSTR(MBGL_VERSION_STRING), CFSTR(MBGL_VERSION_REV)]];
+
+ NSString *systemName = @"Darwin";
+#if TARGET_OS_IPHONE
+ systemName = @"iOS";
+#elif TARGET_OS_MAC
+ systemName = @"OS X";
+#elif TARGET_OS_WATCH
+ systemName = @"watchOS";
+#elif TARGET_OS_TV
+ systemName = @"tvOS";
+#endif
+#if TARGET_OS_SIMULATOR
+ systemName = [systemName stringByAppendingString:@" Simulator"];
+#endif
+ NSString *systemVersion = nil;
+ if ([NSProcessInfo instancesRespondToSelector:@selector(operatingSystemVersion)]) {
+ NSOperatingSystemVersion osVersion = [NSProcessInfo processInfo].operatingSystemVersion;
+ systemVersion = [NSString stringWithFormat:@"%ld.%ld.%ld",
+ (long)osVersion.majorVersion, (long)osVersion.minorVersion, (long)osVersion.patchVersion];
+ }
+ if (systemVersion) {
+ [userAgentComponents addObject:[NSString stringWithFormat:@"%@/%@", systemName, systemVersion]];
+ }
+
+ NSString *cpu = nil;
+#if TARGET_CPU_X86
+ cpu = @"x86";
+#elif TARGET_CPU_X86_64
+ cpu = @"x86_64";
+#elif TARGET_CPU_ARM
+ cpu = @"arm";
+#elif TARGET_CPU_ARM64
+ cpu = @"arm64";
+#endif
+ if (cpu) {
+ [userAgentComponents addObject:[NSString stringWithFormat:@"(%@)", cpu]];
+ }
+
+ return [userAgentComponents componentsJoinedByString:@" "];
+}
+
+NSBundle *HTTPFileSource::Impl::getSDKBundle() const {
+ NSBundle *bundle = [NSBundle bundleForClass:[MBGLBundleCanary class]];
+ if (bundle && ![bundle.infoDictionary[@"CFBundlePackageType"] isEqualToString:@"FMWK"]) {
+ // For static frameworks, the class is contained in the application bundle rather than the
+ // framework bundle.
+ bundle = [NSBundle bundleWithPath:[bundle.privateFrameworksPath
+ stringByAppendingPathComponent:@"Mapbox.framework"]];
+ }
+ return bundle;
+}
+
+HTTPFileSource::HTTPFileSource()
+ : impl(std::make_unique<Impl>()) {
+}
+
+HTTPFileSource::~HTTPFileSource() = default;
+
+uint32_t HTTPFileSource::maximumConcurrentRequests() {
+ return 20;
+}
+
+std::unique_ptr<AsyncRequest> HTTPFileSource::request(const Resource& resource, Callback callback) {
+ auto request = std::make_unique<HTTPRequest>(callback);
+ auto shared = request->shared; // Explicit copy so that it also gets copied into the completion handler block below.
+
+ @autoreleasepool {
+ NSURL* url = [NSURL URLWithString:@(resource.url.c_str())];
+ if (impl->accountType == 0 &&
+ ([url.host isEqualToString:@"mapbox.com"] || [url.host hasSuffix:@".mapbox.com"])) {
+ NSString* absoluteString = [url.absoluteString
+ stringByAppendingFormat:(url.query ? @"&%@" : @"?%@"), @"events=true"];
+ url = [NSURL URLWithString:absoluteString];
+ }
+
+ NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL:url];
+ if (resource.priorEtag) {
+ [req addValue:@(resource.priorEtag->c_str())
+ forHTTPHeaderField:@"If-None-Match"];
+ } else if (resource.priorModified) {
+ [req addValue:@(util::rfc1123(*resource.priorModified).c_str())
+ forHTTPHeaderField:@"If-Modified-Since"];
+ }
+
+ [req addValue:impl->userAgent forHTTPHeaderField:@"User-Agent"];
+
+ request->task = [impl->session
+ dataTaskWithRequest:req
+ completionHandler:^(NSData* data, NSURLResponse* res, NSError* error) {
+ if (error && [error code] == NSURLErrorCancelled) {
+ return;
+ }
+
+ Response response;
+ using Error = Response::Error;
+
+ if (error) {
+ if (data) {
+ response.data =
+ std::make_shared<std::string>((const char*)[data bytes], [data length]);
+ }
+
+ switch ([error code]) {
+ case NSURLErrorBadServerResponse: // 5xx errors
+ response.error = std::make_unique<Error>(
+ Error::Reason::Server, [[error localizedDescription] UTF8String]);
+ break;
+
+ case NSURLErrorNetworkConnectionLost:
+ case NSURLErrorCannotFindHost:
+ case NSURLErrorCannotConnectToHost:
+ case NSURLErrorDNSLookupFailed:
+ case NSURLErrorNotConnectedToInternet:
+ case NSURLErrorInternationalRoamingOff:
+ case NSURLErrorCallIsActive:
+ case NSURLErrorDataNotAllowed:
+ case NSURLErrorTimedOut:
+ response.error = std::make_unique<Error>(
+ Error::Reason::Connection, [[error localizedDescription] UTF8String]);
+ break;
+
+ default:
+ response.error = std::make_unique<Error>(
+ Error::Reason::Other, [[error localizedDescription] UTF8String]);
+ break;
+ }
+ } else if ([res isKindOfClass:[NSHTTPURLResponse class]]) {
+ const long responseCode = [(NSHTTPURLResponse *)res statusCode];
+
+ NSDictionary *headers = [(NSHTTPURLResponse *)res allHeaderFields];
+ NSString *cache_control = [headers objectForKey:@"Cache-Control"];
+ if (cache_control) {
+ response.expires = http::CacheControl::parse([cache_control UTF8String]).toTimePoint();
+ }
+
+ NSString *expires = [headers objectForKey:@"Expires"];
+ if (expires) {
+ response.expires = util::parseTimestamp([expires UTF8String]);
+ }
+
+ NSString *last_modified = [headers objectForKey:@"Last-Modified"];
+ if (last_modified) {
+ response.modified = util::parseTimestamp([last_modified UTF8String]);
+ }
+
+ NSString *etag = [headers objectForKey:@"ETag"];
+ if (etag) {
+ response.etag = std::string([etag UTF8String]);
+ }
+
+ if (responseCode == 200) {
+ response.data = std::make_shared<std::string>((const char *)[data bytes], [data length]);
+ } else if (responseCode == 204 || (responseCode == 404 && resource.kind == Resource::Kind::Tile)) {
+ response.noContent = true;
+ } else if (responseCode == 304) {
+ response.notModified = true;
+ } else if (responseCode == 404) {
+ response.error =
+ std::make_unique<Error>(Error::Reason::NotFound, "HTTP status code 404");
+ } else if (responseCode >= 500 && responseCode < 600) {
+ response.error =
+ std::make_unique<Error>(Error::Reason::Server, std::string{ "HTTP status code " } +
+ std::to_string(responseCode));
+ } else {
+ response.error =
+ std::make_unique<Error>(Error::Reason::Other, std::string{ "HTTP status code " } +
+ std::to_string(responseCode));
+ }
+ } else {
+ // This should never happen.
+ response.error = std::make_unique<Error>(Error::Reason::Other,
+ "Response class is not NSHTTPURLResponse");
+ }
+
+ shared->notify(response);
+ }];
+
+ [request->task resume];
+ }
+
+ return std::move(request);
+}
+
+}