// // Copyright (c) 2016-present, Facebook, Inc. // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. An additional grant // of patent rights can be found in the PATENTS file in the same directory. // #import "SRProxyConnect.h" #import "NSRunLoop+SRWebSocket.h" #import "SRConstants.h" #import "SRError.h" #import "SRLog.h" #import "SRURLUtilities.h" @interface SRProxyConnect() @property (nonatomic, strong) NSURL *url; @property (nonatomic, strong) NSInputStream *inputStream; @property (nonatomic, strong) NSOutputStream *outputStream; @end @implementation SRProxyConnect { SRProxyConnectCompletion _completion; NSString *_httpProxyHost; uint32_t _httpProxyPort; CFHTTPMessageRef _receivedHTTPHeaders; NSString *_socksProxyHost; uint32_t _socksProxyPort; NSString *_socksProxyUsername; NSString *_socksProxyPassword; BOOL _connectionRequiresSSL; NSMutableArray *_inputQueue; dispatch_queue_t _writeQueue; } ///-------------------------------------- #pragma mark - Init ///-------------------------------------- -(instancetype)initWithURL:(NSURL *)url { self = [super init]; if (!self) return self; _url = url; _connectionRequiresSSL = SRURLRequiresSSL(url); _writeQueue = dispatch_queue_create("com.facebook.socketrocket.proxyconnect.write", DISPATCH_QUEUE_SERIAL); _inputQueue = [NSMutableArray arrayWithCapacity:2]; return self; } - (void)dealloc { // If we get deallocated before the socket open finishes - we need to cleanup everything. [self.inputStream removeFromRunLoop:[NSRunLoop SR_networkRunLoop] forMode:NSDefaultRunLoopMode]; self.inputStream.delegate = nil; [self.inputStream close]; self.inputStream = nil; self.outputStream.delegate = nil; [self.outputStream close]; self.outputStream = nil; } ///-------------------------------------- #pragma mark - Open ///-------------------------------------- - (void)openNetworkStreamWithCompletion:(SRProxyConnectCompletion)completion { _completion = completion; [self _configureProxy]; } ///-------------------------------------- #pragma mark - Flow ///-------------------------------------- - (void)_didConnect { SRDebugLog(@"_didConnect, return streams"); if (_connectionRequiresSSL) { if (_httpProxyHost) { // Must set the real peer name before turning on SSL SRDebugLog(@"proxy set peer name to real host %@", self.url.host); [self.outputStream setProperty:self.url.host forKey:@"_kCFStreamPropertySocketPeerName"]; } } if (_receivedHTTPHeaders) { CFRelease(_receivedHTTPHeaders); _receivedHTTPHeaders = NULL; } NSInputStream *inputStream = self.inputStream; NSOutputStream *outputStream = self.outputStream; self.inputStream = nil; self.outputStream = nil; [inputStream removeFromRunLoop:[NSRunLoop SR_networkRunLoop] forMode:NSDefaultRunLoopMode]; inputStream.delegate = nil; outputStream.delegate = nil; _completion(nil, inputStream, outputStream); } - (void)_failWithError:(NSError *)error { SRDebugLog(@"_failWithError, return error"); if (!error) { error = SRHTTPErrorWithCodeDescription(500, 2132,@"Proxy Error"); } if (_receivedHTTPHeaders) { CFRelease(_receivedHTTPHeaders); _receivedHTTPHeaders = NULL; } self.inputStream.delegate = nil; self.outputStream.delegate = nil; [self.inputStream removeFromRunLoop:[NSRunLoop SR_networkRunLoop] forMode:NSDefaultRunLoopMode]; [self.inputStream close]; [self.outputStream close]; self.inputStream = nil; self.outputStream = nil; _completion(error, nil, nil); } // get proxy setting from device setting - (void)_configureProxy { SRDebugLog(@"configureProxy"); NSDictionary *proxySettings = CFBridgingRelease(CFNetworkCopySystemProxySettings()); // CFNetworkCopyProxiesForURL doesn't understand ws:// or wss:// NSURL *httpURL; if (_connectionRequiresSSL) { httpURL = [NSURL URLWithString:[NSString stringWithFormat:@"https://%@", _url.host]]; } else { httpURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@", _url.host]]; } NSArray *proxies = CFBridgingRelease(CFNetworkCopyProxiesForURL((__bridge CFURLRef)httpURL, (__bridge CFDictionaryRef)proxySettings)); if (proxies.count == 0) { SRDebugLog(@"configureProxy no proxies"); [self _openConnection]; return; // no proxy } NSDictionary *settings = [proxies objectAtIndex:0]; NSString *proxyType = settings[(NSString *)kCFProxyTypeKey]; if ([proxyType isEqualToString:(NSString *)kCFProxyTypeAutoConfigurationURL]) { NSURL *pacURL = settings[(NSString *)kCFProxyAutoConfigurationURLKey]; if (pacURL) { [self _fetchPAC:pacURL withProxySettings:proxySettings]; return; } } if ([proxyType isEqualToString:(__bridge NSString *)kCFProxyTypeAutoConfigurationJavaScript]) { NSString *script = settings[(__bridge NSString *)kCFProxyAutoConfigurationJavaScriptKey]; if (script) { [self _runPACScript:script withProxySettings:proxySettings]; return; } } [self _readProxySettingWithType:proxyType settings:settings]; [self _openConnection]; } - (void)_readProxySettingWithType:(NSString *)proxyType settings:(NSDictionary *)settings { if ([proxyType isEqualToString:(NSString *)kCFProxyTypeHTTP] || [proxyType isEqualToString:(NSString *)kCFProxyTypeHTTPS]) { _httpProxyHost = settings[(NSString *)kCFProxyHostNameKey]; NSNumber *portValue = settings[(NSString *)kCFProxyPortNumberKey]; if (portValue) { _httpProxyPort = [portValue intValue]; } } if ([proxyType isEqualToString:(NSString *)kCFProxyTypeSOCKS]) { _socksProxyHost = settings[(NSString *)kCFProxyHostNameKey]; NSNumber *portValue = settings[(NSString *)kCFProxyPortNumberKey]; if (portValue) _socksProxyPort = [portValue intValue]; _socksProxyUsername = settings[(NSString *)kCFProxyUsernameKey]; _socksProxyPassword = settings[(NSString *)kCFProxyPasswordKey]; } if (_httpProxyHost) { SRDebugLog(@"Using http proxy %@:%u", _httpProxyHost, _httpProxyPort); } else if (_socksProxyHost) { SRDebugLog(@"Using socks proxy %@:%u", _socksProxyHost, _socksProxyPort); } else { SRDebugLog(@"configureProxy no proxies"); } } - (void)_fetchPAC:(NSURL *)PACurl withProxySettings:(NSDictionary *)proxySettings { SRDebugLog(@"SRWebSocket fetchPAC:%@", PACurl); if ([PACurl isFileURL]) { NSError *error = nil; NSString *script = [NSString stringWithContentsOfURL:PACurl usedEncoding:NULL error:&error]; if (error) { [self _openConnection]; } else { [self _runPACScript:script withProxySettings:proxySettings]; } return; } NSString *scheme = [PACurl.scheme lowercaseString]; if (![scheme isEqualToString:@"http"] && ![scheme isEqualToString:@"https"]) { // Don't know how to read data from this URL, we'll have to give up // We'll simply assume no proxies, and start the request as normal [self _openConnection]; return; } __weak __typeof(self) wself = self; NSURLRequest *request = [NSURLRequest requestWithURL:PACurl]; NSURLSession *session = [NSURLSession sharedSession]; [[session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { __strong __typeof(wself) sself = wself; if (!error) { NSString *script = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; [sself _runPACScript:script withProxySettings:proxySettings]; } else { [sself _openConnection]; } }] resume]; } - (void)_runPACScript:(NSString *)script withProxySettings:(NSDictionary *)proxySettings { if (!script) { [self _openConnection]; return; } SRDebugLog(@"runPACScript"); // From: http://developer.apple.com/samplecode/CFProxySupportTool/listing1.html // Work around . This dummy call to // CFNetworkCopyProxiesForURL initialise some state within CFNetwork // that is required by CFNetworkCopyProxiesForAutoConfigurationScript. CFBridgingRelease(CFNetworkCopyProxiesForURL((__bridge CFURLRef)_url, (__bridge CFDictionaryRef)proxySettings)); // Obtain the list of proxies by running the autoconfiguration script CFErrorRef err = NULL; // CFNetworkCopyProxiesForAutoConfigurationScript doesn't understand ws:// or wss:// NSURL *httpURL; if (_connectionRequiresSSL) httpURL = [NSURL URLWithString:[NSString stringWithFormat:@"https://%@", _url.host]]; else httpURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@", _url.host]]; NSArray *proxies = CFBridgingRelease(CFNetworkCopyProxiesForAutoConfigurationScript((__bridge CFStringRef)script,(__bridge CFURLRef)httpURL, &err)); if (!err && [proxies count] > 0) { NSDictionary *settings = [proxies objectAtIndex:0]; NSString *proxyType = settings[(NSString *)kCFProxyTypeKey]; [self _readProxySettingWithType:proxyType settings:settings]; } [self _openConnection]; } - (void)_openConnection { [self _initializeStreams]; [self.inputStream scheduleInRunLoop:[NSRunLoop SR_networkRunLoop] forMode:NSDefaultRunLoopMode]; //[self.outputStream scheduleInRunLoop:[NSRunLoop SR_networkRunLoop] // forMode:NSDefaultRunLoopMode]; [self.outputStream open]; [self.inputStream open]; } - (void)_initializeStreams { assert(_url.port.unsignedIntValue <= UINT32_MAX); uint32_t port = _url.port.unsignedIntValue; if (port == 0) { port = (_connectionRequiresSSL ? 443 : 80); } NSString *host = _url.host; if (_httpProxyHost) { host = _httpProxyHost; port = (_httpProxyPort ?: 80); } CFReadStreamRef readStream = NULL; CFWriteStreamRef writeStream = NULL; SRDebugLog(@"ProxyConnect connect stream to %@:%u", host, port); CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)host, port, &readStream, &writeStream); self.outputStream = CFBridgingRelease(writeStream); self.inputStream = CFBridgingRelease(readStream); if (_socksProxyHost) { SRDebugLog(@"ProxyConnect set sock property stream to %@:%u user %@ password %@", _socksProxyHost, _socksProxyPort, _socksProxyUsername, _socksProxyPassword); NSMutableDictionary *settings = [NSMutableDictionary dictionaryWithCapacity:4]; settings[NSStreamSOCKSProxyHostKey] = _socksProxyHost; if (_socksProxyPort) { settings[NSStreamSOCKSProxyPortKey] = @(_socksProxyPort); } if (_socksProxyUsername) { settings[NSStreamSOCKSProxyUserKey] = _socksProxyUsername; } if (_socksProxyPassword) { settings[NSStreamSOCKSProxyPasswordKey] = _socksProxyPassword; } [self.inputStream setProperty:settings forKey:NSStreamSOCKSProxyConfigurationKey]; [self.outputStream setProperty:settings forKey:NSStreamSOCKSProxyConfigurationKey]; } self.inputStream.delegate = self; self.outputStream.delegate = self; } - (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode; { SRDebugLog(@"stream handleEvent %u", eventCode); switch (eventCode) { case NSStreamEventOpenCompleted: { if (aStream == self.inputStream) { if (_httpProxyHost) { [self _proxyDidConnect]; } else { [self _didConnect]; } } } break; case NSStreamEventErrorOccurred: { [self _failWithError:aStream.streamError]; } break; case NSStreamEventEndEncountered: { [self _failWithError:aStream.streamError]; } break; case NSStreamEventHasBytesAvailable: { if (aStream == _inputStream) { [self _processInputStream]; } } break; case NSStreamEventHasSpaceAvailable: case NSStreamEventNone: SRDebugLog(@"(default) %@", aStream); break; } } - (void)_proxyDidConnect { SRDebugLog(@"Proxy Connected"); uint32_t port = _url.port.unsignedIntValue; if (port == 0) { port = (_connectionRequiresSSL ? 443 : 80); } // Send HTTP CONNECT Request NSString *connectRequestStr = [NSString stringWithFormat:@"CONNECT %@:%u HTTP/1.1\r\nHost: %@\r\nConnection: keep-alive\r\nProxy-Connection: keep-alive\r\n\r\n", _url.host, port, _url.host]; NSData *message = [connectRequestStr dataUsingEncoding:NSUTF8StringEncoding]; SRDebugLog(@"Proxy sending %@", connectRequestStr); [self _writeData:message]; } ///handles the incoming bytes and sending them to the proper processing method - (void)_processInputStream { NSMutableData *buf = [NSMutableData dataWithCapacity:SRDefaultBufferSize()]; uint8_t *buffer = buf.mutableBytes; NSInteger length = [_inputStream read:buffer maxLength:SRDefaultBufferSize()]; if (length <= 0) { return; } BOOL process = (_inputQueue.count == 0); [_inputQueue addObject:[NSData dataWithBytes:buffer length:length]]; if (process) { [self _dequeueInput]; } } // dequeue the incoming input so it is processed in order - (void)_dequeueInput { while (_inputQueue.count > 0) { NSData *data = _inputQueue.firstObject; [_inputQueue removeObjectAtIndex:0]; // No need to process any data further, we got the full header data. if ([self _proxyProcessHTTPResponseWithData:data]) { break; } } } //handle checking the proxy connection status - (BOOL)_proxyProcessHTTPResponseWithData:(NSData *)data { if (_receivedHTTPHeaders == NULL) { _receivedHTTPHeaders = CFHTTPMessageCreateEmpty(NULL, NO); } CFHTTPMessageAppendBytes(_receivedHTTPHeaders, (const UInt8 *)data.bytes, data.length); if (CFHTTPMessageIsHeaderComplete(_receivedHTTPHeaders)) { SRDebugLog(@"Finished reading headers %@", CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(_receivedHTTPHeaders))); [self _proxyHTTPHeadersDidFinish]; return YES; } return NO; } - (void)_proxyHTTPHeadersDidFinish { NSInteger responseCode = CFHTTPMessageGetResponseStatusCode(_receivedHTTPHeaders); if (responseCode >= 299) { SRDebugLog(@"Connect to Proxy Request failed with response code %d", responseCode); NSError *error = SRHTTPErrorWithCodeDescription(responseCode, 2132, [NSString stringWithFormat:@"Received bad response code from proxy server: %d.", (int)responseCode]); [self _failWithError:error]; return; } SRDebugLog(@"proxy connect return %d, call socket connect", responseCode); [self _didConnect]; } static NSTimeInterval const SRProxyConnectWriteTimeout = 5.0; - (void)_writeData:(NSData *)data { const uint8_t * bytes = data.bytes; __block NSInteger timeout = (NSInteger)(SRProxyConnectWriteTimeout * 1000000); // wait timeout before giving up __weak __typeof(self) wself = self; dispatch_async(_writeQueue, ^{ __strong __typeof(wself) sself = self; if (!sself) { return; } NSOutputStream *outStream = sself.outputStream; if (!outStream) { return; } while (![outStream hasSpaceAvailable]) { usleep(100); //wait until the socket is ready timeout -= 100; if (timeout < 0) { NSError *error = SRHTTPErrorWithCodeDescription(408, 2132, @"Proxy timeout"); [sself _failWithError:error]; } else if (outStream.streamError != nil) { [sself _failWithError:outStream.streamError]; } } [outStream write:bytes maxLength:data.length]; }); } @end