399 lines
15 KiB
Objective-C
399 lines
15 KiB
Objective-C
#import "apple_transport_darwin.h"
|
|
|
|
#import <CoreFoundation/CFStream.h>
|
|
#import <Foundation/Foundation.h>
|
|
#import <Security/Security.h>
|
|
#import <dispatch/dispatch.h>
|
|
#import <stdlib.h>
|
|
#import <string.h>
|
|
|
|
typedef struct box_apple_http_session {
|
|
void *handle;
|
|
} box_apple_http_session_t;
|
|
|
|
typedef struct box_apple_http_task {
|
|
void *task;
|
|
void *done_semaphore;
|
|
box_apple_http_response_t *response;
|
|
char *error;
|
|
} box_apple_http_task_t;
|
|
|
|
static NSString *const box_apple_http_verify_time_key = @"sing-box.verify-time";
|
|
|
|
static void box_set_error_string(char **error_out, NSString *message) {
|
|
if (error_out == NULL || *error_out != NULL) {
|
|
return;
|
|
}
|
|
const char *utf8 = [message UTF8String];
|
|
*error_out = strdup(utf8 != NULL ? utf8 : "unknown error");
|
|
}
|
|
|
|
static void box_set_error_from_nserror(char **error_out, NSError *error) {
|
|
if (error == nil) {
|
|
box_set_error_string(error_out, @"unknown error");
|
|
return;
|
|
}
|
|
box_set_error_string(error_out, error.localizedDescription ?: error.description);
|
|
}
|
|
|
|
static NSArray *box_parse_certificates_from_pem(const char *pem, size_t pem_len) {
|
|
if (pem == NULL || pem_len == 0) {
|
|
return @[];
|
|
}
|
|
NSString *content = [[NSString alloc] initWithBytes:pem length:pem_len encoding:NSUTF8StringEncoding];
|
|
if (content == nil) {
|
|
return @[];
|
|
}
|
|
NSString *beginMarker = @"-----BEGIN CERTIFICATE-----";
|
|
NSString *endMarker = @"-----END CERTIFICATE-----";
|
|
NSMutableArray *certificates = [NSMutableArray array];
|
|
NSUInteger searchFrom = 0;
|
|
while (searchFrom < content.length) {
|
|
NSRange beginRange = [content rangeOfString:beginMarker options:0 range:NSMakeRange(searchFrom, content.length - searchFrom)];
|
|
if (beginRange.location == NSNotFound) {
|
|
break;
|
|
}
|
|
NSUInteger bodyStart = beginRange.location + beginRange.length;
|
|
NSRange endRange = [content rangeOfString:endMarker options:0 range:NSMakeRange(bodyStart, content.length - bodyStart)];
|
|
if (endRange.location == NSNotFound) {
|
|
break;
|
|
}
|
|
NSString *base64Section = [content substringWithRange:NSMakeRange(bodyStart, endRange.location - bodyStart)];
|
|
NSArray<NSString *> *components = [base64Section componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
NSString *base64Content = [components componentsJoinedByString:@""];
|
|
NSData *der = [[NSData alloc] initWithBase64EncodedString:base64Content options:0];
|
|
if (der != nil) {
|
|
SecCertificateRef certificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)der);
|
|
if (certificate != NULL) {
|
|
[certificates addObject:(__bridge id)certificate];
|
|
CFRelease(certificate);
|
|
}
|
|
}
|
|
searchFrom = endRange.location + endRange.length;
|
|
}
|
|
return certificates;
|
|
}
|
|
|
|
static bool box_evaluate_trust(SecTrustRef trustRef, NSArray *anchors, bool anchor_only, NSDate *verifyDate) {
|
|
if (trustRef == NULL) {
|
|
return false;
|
|
}
|
|
if (verifyDate != nil && SecTrustSetVerifyDate(trustRef, (__bridge CFDateRef)verifyDate) != errSecSuccess) {
|
|
return false;
|
|
}
|
|
if (anchors.count > 0 || anchor_only) {
|
|
CFMutableArrayRef anchorArray = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);
|
|
for (id certificate in anchors) {
|
|
CFArrayAppendValue(anchorArray, (__bridge const void *)certificate);
|
|
}
|
|
SecTrustSetAnchorCertificates(trustRef, anchorArray);
|
|
SecTrustSetAnchorCertificatesOnly(trustRef, anchor_only);
|
|
CFRelease(anchorArray);
|
|
}
|
|
CFErrorRef error = NULL;
|
|
bool result = SecTrustEvaluateWithError(trustRef, &error);
|
|
if (error != NULL) {
|
|
CFRelease(error);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static NSDate *box_apple_http_verify_date_for_request(NSURLRequest *request) {
|
|
if (request == nil) {
|
|
return nil;
|
|
}
|
|
id value = [NSURLProtocol propertyForKey:box_apple_http_verify_time_key inRequest:request];
|
|
if (![value isKindOfClass:[NSNumber class]]) {
|
|
return nil;
|
|
}
|
|
return [NSDate dateWithTimeIntervalSince1970:[(NSNumber *)value longLongValue] / 1000.0];
|
|
}
|
|
|
|
static box_apple_http_response_t *box_create_response(NSHTTPURLResponse *httpResponse, NSData *data) {
|
|
box_apple_http_response_t *response = calloc(1, sizeof(box_apple_http_response_t));
|
|
response->status_code = (int)httpResponse.statusCode;
|
|
NSDictionary *headers = httpResponse.allHeaderFields;
|
|
response->header_count = headers.count;
|
|
if (response->header_count > 0) {
|
|
response->header_keys = calloc(response->header_count, sizeof(char *));
|
|
response->header_values = calloc(response->header_count, sizeof(char *));
|
|
NSUInteger index = 0;
|
|
for (id key in headers) {
|
|
NSString *keyString = [[key description] copy];
|
|
NSString *valueString = [[headers[key] description] copy];
|
|
response->header_keys[index] = strdup(keyString.UTF8String ?: "");
|
|
response->header_values[index] = strdup(valueString.UTF8String ?: "");
|
|
index++;
|
|
}
|
|
}
|
|
if (data.length > 0) {
|
|
response->body_len = data.length;
|
|
response->body = malloc(data.length);
|
|
memcpy(response->body, data.bytes, data.length);
|
|
}
|
|
return response;
|
|
}
|
|
|
|
@interface BoxAppleHTTPSessionDelegate : NSObject <NSURLSessionTaskDelegate, NSURLSessionDataDelegate>
|
|
@property(nonatomic, assign) BOOL insecure;
|
|
@property(nonatomic, assign) BOOL anchorOnly;
|
|
@property(nonatomic, strong) NSArray *anchors;
|
|
@property(nonatomic, strong) NSData *pinnedPublicKeyHashes;
|
|
@end
|
|
|
|
@implementation BoxAppleHTTPSessionDelegate
|
|
|
|
- (void)URLSession:(NSURLSession *)session
|
|
task:(NSURLSessionTask *)task
|
|
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
|
|
newRequest:(NSURLRequest *)request
|
|
completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {
|
|
completionHandler(nil);
|
|
}
|
|
|
|
- (void)URLSession:(NSURLSession *)session
|
|
task:(NSURLSessionTask *)task
|
|
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
|
|
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler {
|
|
if (![challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
|
|
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
|
|
return;
|
|
}
|
|
SecTrustRef trustRef = challenge.protectionSpace.serverTrust;
|
|
if (trustRef == NULL) {
|
|
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
|
|
return;
|
|
}
|
|
NSDate *verifyDate = box_apple_http_verify_date_for_request(task.currentRequest ?: task.originalRequest);
|
|
BOOL needsCustomHandling = self.insecure || self.anchorOnly || self.anchors.count > 0 || self.pinnedPublicKeyHashes.length > 0 || verifyDate != nil;
|
|
if (!needsCustomHandling) {
|
|
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
|
|
return;
|
|
}
|
|
BOOL ok = YES;
|
|
if (!self.insecure) {
|
|
ok = box_evaluate_trust(trustRef, self.anchors, self.anchorOnly, verifyDate);
|
|
}
|
|
if (ok && self.pinnedPublicKeyHashes.length > 0) {
|
|
CFArrayRef certificateChain = SecTrustCopyCertificateChain(trustRef);
|
|
SecCertificateRef leafCertificate = NULL;
|
|
if (certificateChain != NULL && CFArrayGetCount(certificateChain) > 0) {
|
|
leafCertificate = (SecCertificateRef)CFArrayGetValueAtIndex(certificateChain, 0);
|
|
}
|
|
if (leafCertificate == NULL) {
|
|
ok = NO;
|
|
} else {
|
|
NSData *leafData = CFBridgingRelease(SecCertificateCopyData(leafCertificate));
|
|
char *pinError = box_apple_http_verify_public_key_sha256(
|
|
(uint8_t *)self.pinnedPublicKeyHashes.bytes,
|
|
self.pinnedPublicKeyHashes.length,
|
|
(uint8_t *)leafData.bytes,
|
|
leafData.length
|
|
);
|
|
if (pinError != NULL) {
|
|
free(pinError);
|
|
ok = NO;
|
|
}
|
|
}
|
|
if (certificateChain != NULL) {
|
|
CFRelease(certificateChain);
|
|
}
|
|
}
|
|
if (!ok) {
|
|
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
|
|
return;
|
|
}
|
|
completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:trustRef]);
|
|
}
|
|
|
|
@end
|
|
|
|
@interface BoxAppleHTTPSessionHandle : NSObject
|
|
@property(nonatomic, strong) NSURLSession *session;
|
|
@property(nonatomic, strong) BoxAppleHTTPSessionDelegate *delegate;
|
|
@end
|
|
|
|
@implementation BoxAppleHTTPSessionHandle
|
|
@end
|
|
|
|
box_apple_http_session_t *box_apple_http_session_create(
|
|
const box_apple_http_session_config_t *config,
|
|
char **error_out
|
|
) {
|
|
@autoreleasepool {
|
|
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration ephemeralSessionConfiguration];
|
|
sessionConfig.URLCache = nil;
|
|
sessionConfig.HTTPCookieStorage = nil;
|
|
sessionConfig.URLCredentialStorage = nil;
|
|
sessionConfig.HTTPShouldSetCookies = NO;
|
|
if (config != NULL && config->proxy_host != NULL && config->proxy_port > 0) {
|
|
NSMutableDictionary *proxyDictionary = [NSMutableDictionary dictionary];
|
|
proxyDictionary[(__bridge NSString *)kCFStreamPropertySOCKSProxyHost] = [NSString stringWithUTF8String:config->proxy_host];
|
|
proxyDictionary[(__bridge NSString *)kCFStreamPropertySOCKSProxyPort] = @(config->proxy_port);
|
|
proxyDictionary[(__bridge NSString *)kCFStreamPropertySOCKSVersion] = (__bridge NSString *)kCFStreamSocketSOCKSVersion5;
|
|
if (config->proxy_username != NULL) {
|
|
proxyDictionary[(__bridge NSString *)kCFStreamPropertySOCKSUser] = [NSString stringWithUTF8String:config->proxy_username];
|
|
}
|
|
if (config->proxy_password != NULL) {
|
|
proxyDictionary[(__bridge NSString *)kCFStreamPropertySOCKSPassword] = [NSString stringWithUTF8String:config->proxy_password];
|
|
}
|
|
sessionConfig.connectionProxyDictionary = proxyDictionary;
|
|
}
|
|
if (config != NULL && config->min_tls_version != 0) {
|
|
sessionConfig.TLSMinimumSupportedProtocolVersion = (tls_protocol_version_t)config->min_tls_version;
|
|
}
|
|
if (config != NULL && config->max_tls_version != 0) {
|
|
sessionConfig.TLSMaximumSupportedProtocolVersion = (tls_protocol_version_t)config->max_tls_version;
|
|
}
|
|
BoxAppleHTTPSessionDelegate *delegate = [[BoxAppleHTTPSessionDelegate alloc] init];
|
|
if (config != NULL) {
|
|
delegate.insecure = config->insecure;
|
|
delegate.anchorOnly = config->anchor_only;
|
|
delegate.anchors = box_parse_certificates_from_pem(config->anchor_pem, config->anchor_pem_len);
|
|
if (config->pinned_public_key_sha256 != NULL && config->pinned_public_key_sha256_len > 0) {
|
|
delegate.pinnedPublicKeyHashes = [NSData dataWithBytes:config->pinned_public_key_sha256 length:config->pinned_public_key_sha256_len];
|
|
}
|
|
}
|
|
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:delegate delegateQueue:nil];
|
|
if (session == nil) {
|
|
box_set_error_string(error_out, @"create URLSession");
|
|
return NULL;
|
|
}
|
|
BoxAppleHTTPSessionHandle *handle = [[BoxAppleHTTPSessionHandle alloc] init];
|
|
handle.session = session;
|
|
handle.delegate = delegate;
|
|
box_apple_http_session_t *sessionHandle = calloc(1, sizeof(box_apple_http_session_t));
|
|
sessionHandle->handle = (__bridge_retained void *)handle;
|
|
return sessionHandle;
|
|
}
|
|
}
|
|
|
|
void box_apple_http_session_retire(box_apple_http_session_t *session) {
|
|
if (session == NULL || session->handle == NULL) {
|
|
return;
|
|
}
|
|
BoxAppleHTTPSessionHandle *handle = (__bridge_transfer BoxAppleHTTPSessionHandle *)session->handle;
|
|
[handle.session finishTasksAndInvalidate];
|
|
free(session);
|
|
}
|
|
|
|
void box_apple_http_session_close(box_apple_http_session_t *session) {
|
|
if (session == NULL || session->handle == NULL) {
|
|
return;
|
|
}
|
|
BoxAppleHTTPSessionHandle *handle = (__bridge_transfer BoxAppleHTTPSessionHandle *)session->handle;
|
|
[handle.session invalidateAndCancel];
|
|
free(session);
|
|
}
|
|
|
|
box_apple_http_task_t *box_apple_http_session_send_async(
|
|
box_apple_http_session_t *session,
|
|
const box_apple_http_request_t *request,
|
|
char **error_out
|
|
) {
|
|
@autoreleasepool {
|
|
if (session == NULL || session->handle == NULL || request == NULL || request->method == NULL || request->url == NULL) {
|
|
box_set_error_string(error_out, @"invalid apple HTTP request");
|
|
return NULL;
|
|
}
|
|
BoxAppleHTTPSessionHandle *handle = (__bridge BoxAppleHTTPSessionHandle *)session->handle;
|
|
NSURL *requestURL = [NSURL URLWithString:[NSString stringWithUTF8String:request->url]];
|
|
if (requestURL == nil) {
|
|
box_set_error_string(error_out, @"invalid request URL");
|
|
return NULL;
|
|
}
|
|
NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:requestURL];
|
|
urlRequest.HTTPMethod = [NSString stringWithUTF8String:request->method];
|
|
for (size_t index = 0; index < request->header_count; index++) {
|
|
const char *key = request->header_keys[index];
|
|
const char *value = request->header_values[index];
|
|
if (key == NULL || value == NULL) {
|
|
continue;
|
|
}
|
|
[urlRequest addValue:[NSString stringWithUTF8String:value] forHTTPHeaderField:[NSString stringWithUTF8String:key]];
|
|
}
|
|
if (request->body != NULL && request->body_len > 0) {
|
|
urlRequest.HTTPBody = [NSData dataWithBytes:request->body length:request->body_len];
|
|
}
|
|
if (request->has_verify_time) {
|
|
[NSURLProtocol setProperty:@(request->verify_time_unix_millis) forKey:box_apple_http_verify_time_key inRequest:urlRequest];
|
|
}
|
|
box_apple_http_task_t *task = calloc(1, sizeof(box_apple_http_task_t));
|
|
dispatch_semaphore_t doneSemaphore = dispatch_semaphore_create(0);
|
|
task->done_semaphore = (__bridge_retained void *)doneSemaphore;
|
|
NSURLSessionDataTask *dataTask = [handle.session dataTaskWithRequest:urlRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
|
|
if (error != nil) {
|
|
box_set_error_from_nserror(&task->error, error);
|
|
} else if (![response isKindOfClass:[NSHTTPURLResponse class]]) {
|
|
box_set_error_string(&task->error, @"unexpected HTTP response type");
|
|
} else {
|
|
task->response = box_create_response((NSHTTPURLResponse *)response, data ?: [NSData data]);
|
|
}
|
|
dispatch_semaphore_signal((__bridge dispatch_semaphore_t)task->done_semaphore);
|
|
}];
|
|
if (dataTask == nil) {
|
|
box_set_error_string(error_out, @"create data task");
|
|
box_apple_http_task_close(task);
|
|
return NULL;
|
|
}
|
|
task->task = (__bridge_retained void *)dataTask;
|
|
[dataTask resume];
|
|
return task;
|
|
}
|
|
}
|
|
|
|
box_apple_http_response_t *box_apple_http_task_wait(
|
|
box_apple_http_task_t *task,
|
|
char **error_out
|
|
) {
|
|
if (task == NULL || task->done_semaphore == NULL) {
|
|
box_set_error_string(error_out, @"invalid apple HTTP task");
|
|
return NULL;
|
|
}
|
|
dispatch_semaphore_wait((__bridge dispatch_semaphore_t)task->done_semaphore, DISPATCH_TIME_FOREVER);
|
|
if (task->error != NULL) {
|
|
box_set_error_string(error_out, [NSString stringWithUTF8String:task->error]);
|
|
return NULL;
|
|
}
|
|
return task->response;
|
|
}
|
|
|
|
void box_apple_http_task_cancel(box_apple_http_task_t *task) {
|
|
if (task == NULL || task->task == NULL) {
|
|
return;
|
|
}
|
|
NSURLSessionTask *nsTask = (__bridge NSURLSessionTask *)task->task;
|
|
[nsTask cancel];
|
|
}
|
|
|
|
void box_apple_http_task_close(box_apple_http_task_t *task) {
|
|
if (task == NULL) {
|
|
return;
|
|
}
|
|
if (task->task != NULL) {
|
|
__unused NSURLSessionTask *nsTask = (__bridge_transfer NSURLSessionTask *)task->task;
|
|
task->task = NULL;
|
|
}
|
|
if (task->done_semaphore != NULL) {
|
|
__unused dispatch_semaphore_t doneSemaphore = (__bridge_transfer dispatch_semaphore_t)task->done_semaphore;
|
|
task->done_semaphore = NULL;
|
|
}
|
|
free(task->error);
|
|
free(task);
|
|
}
|
|
|
|
void box_apple_http_response_free(box_apple_http_response_t *response) {
|
|
if (response == NULL) {
|
|
return;
|
|
}
|
|
for (size_t index = 0; index < response->header_count; index++) {
|
|
free(response->header_keys[index]);
|
|
free(response->header_values[index]);
|
|
}
|
|
free(response->header_keys);
|
|
free(response->header_values);
|
|
free(response->body);
|
|
free(response->error);
|
|
free(response);
|
|
}
|