// // Created by Jonathon Staff on 10/21/14. // Copyright (c) 2014 nplexity, LLC. All rights reserved. // #if !__has_feature(objc_arc) #warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). #endif #import #import #import #import #import "XMPPLogging.h" #import "XMPPOutgoingFileTransfer.h" #import "XMPPIDTracker.h" #import "idn-int.h" #import "XMPPConstants.h" #import "NSNumber+XMPP.h" #import "NSData+XMPP.h" #if DEBUG static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; // XMPP_LOG_LEVEL_VERBOSE | XMPP_LOG_FLAG_TRACE; #else static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; #endif #define IOS_CELLULAR @"pdp_ip0" #define IOS_WIFI @"en0" #define IP_ADDR_IPv4 @"ipv4" #define IP_ADDR_IPv6 @"ipv6" /** * Seeing a return statements within an inner block * can sometimes be mistaken for a return point of the enclosing method. * This makes inline blocks a bit easier to read. **/ #define return_from_block return /** * Tags for _asyncSocket handling. */ #define SOCKS_TAG_READ_METHOD 101 #define SOCKS_TAG_WRITE_METHOD 102 #define SOCKS_TAG_READ_REQUEST 103 #define SOCKS_TAG_READ_DOMAIN 104 #define SOCKS_TAG_WRITE_REPLY 105 #define SOCKS_TAG_WRITE_DATA 106 #define SOCKS_TAG_WRITE_PROXY_METHOD 107 #define SOCKS_TAG_READ_PROXY_METHOD 108 #define SOCKS_TAG_WRITE_PROXY_CONNECT 109 #define SOCKS_TAG_READ_PROXY_REPLY 110 #define TIMEOUT_WRITE -1 #define TIMEOUT_READ 5.0 /** * Set the default timeout for requests to be 60 seconds. */ #define OUTGOING_DEFAULT_TIMEOUT 60 // XMPP Outgoing File Transfer State typedef NS_ENUM(int, XMPPOFTState) { XMPPOFTStateNone, XMPPOFTStateStarted, XMPPOFTStateSOCKSLive, XMPPOFTStateConnectingToProxy, XMPPOFTStateFinished }; NSString *const XMPPOutgoingFileTransferErrorDomain = @"XMPPOutgoingFileTransferErrorDomain"; @interface XMPPOutgoingFileTransfer () { dispatch_queue_t _outgoingQueue; void *_outgoingQueueTag; NSString *_localIPAddress; uint16_t _localPort; GCDAsyncSocket *_outgoingSocket; int32_t _outgoingDataBlockSeq; NSUInteger _sentDataSize; NSUInteger _totalDataSize; NSString *_outgoingDataBase64; XMPPOFTState _transferState; XMPPJID *_proxyJID; NSMutableDictionary *_pastRecipients; } @end @implementation XMPPOutgoingFileTransfer - (instancetype)initWithDispatchQueue:(dispatch_queue_t)queue { self = [super initWithDispatchQueue:queue]; if (self) { // Create separate dispatch queue. _outgoingQueue = dispatch_queue_create("XMPPOutgoingFileTransfer", NULL); _outgoingQueueTag = &_outgoingQueueTag; dispatch_queue_set_specific(_outgoingQueue, _outgoingQueueTag, _outgoingQueueTag, NULL); // define the default block-size in case we use IBB _blockSize = 4096; _transferState = XMPPOFTStateNone; _pastRecipients = [NSMutableDictionary new]; } return self; } #pragma mark - XMPPModule Methods - (void)didActivate { XMPPLogTrace(); _idTracker = [[XMPPIDTracker alloc] initWithStream:xmppStream dispatchQueue:moduleQueue]; } - (void)willDeactivate { XMPPLogTrace(); [_idTracker removeAllIDs]; _idTracker = nil; } #pragma mark - Public Methods - (BOOL)startFileTransfer:(NSError **)errPtr { XMPPLogTrace(); if (!xmppStream.isConnected) { if (errPtr) { NSString *errMsg = @"You must be connected to send a file"; *errPtr = [self localErrorWithMessage:errMsg code:-1]; } return NO; } if (!_outgoingData) { if (errPtr) { NSString *errMsg = @"You must provide data to be sent."; *errPtr = [self localErrorWithMessage:errMsg code:-1]; } return NO; } if (!_recipientJID || ![_recipientJID isFull]) { if (errPtr) { NSString *errMsg = @"You must provide a recipient (including a resource)."; *errPtr = [self localErrorWithMessage:errMsg code:-1]; } return NO; } if (self.disableSOCKS5 && self.disableIBB) { if (errPtr) { NSString *errMsg = @"Both SOCKS5 and IBB transfers are disabled."; *errPtr = [self localErrorWithMessage:errMsg code:-1]; } return NO; } if (_transferState != XMPPOFTStateNone) { if (errPtr) { NSString *errMsg = @"Transfer already in progress."; *errPtr = [self localErrorWithMessage:errMsg code:-1]; } return NO; } dispatch_block_t block = ^{ @autoreleasepool { _transferState = XMPPOFTStateStarted; if (_pastRecipients[_recipientJID.full]) { uint8_t methods = (gl_uint8_t) [_pastRecipients[_recipientJID.full] unsignedIntValue]; if (methods & XMPPFileTransferStreamMethodBytestreams) { _streamMethods |= XMPPFileTransferStreamMethodBytestreams; } if (methods & XMPPFileTransferStreamMethodIBB) { _streamMethods |= XMPPFileTransferStreamMethodIBB; } if (_streamMethods) { [self querySIOffer]; return_from_block; } } [self queryRecipientDiscoInfo]; } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); return YES; } - (BOOL)sendData:(NSData *)data toRecipient:(XMPPJID *)recipient { return _transferState != XMPPOFTStateNone ? NO : [self sendData:data named:nil toRecipient:recipient description:nil error:nil]; } - (BOOL)sendData:(NSData *)data named:(NSString *)name toRecipient:(XMPPJID *)recipient description:(NSString *)description error:(NSError **)errPtr { if (_transferState != XMPPOFTStateNone) { if (errPtr) { NSString *errMsg = @"Transfer already in progress."; *errPtr = [self localErrorWithMessage:errMsg code:-1]; } return NO; } self.outgoingData = data; self.outgoingFileName = name; self.recipientJID = recipient; self.outgoingFileDescription = description; return [self startFileTransfer:errPtr]; } #pragma mark - Private Methods /** * This method sends a `disco#info` query to the recipient. This is done to * ensure they support file transfer, SOCKS5, and IBB. * * The request will look like the following: * * * * * * @see handleRecipientDiscoInfoQueryIQ:withInfo: */ - (void)queryRecipientDiscoInfo { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:_recipientJID elementID:[xmppStream generateUUID]]; NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:XMPPDiscoInfoNamespace]; [iq addChild:query]; [_idTracker addElement:iq target:self selector:@selector(handleRecipientDiscoInfoQueryIQ:withInfo:) timeout:OUTGOING_DEFAULT_TIMEOUT]; [xmppStream sendElement:iq]; } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } /** * This method is responsible for sending the Stream Initiation Offer as * described in Examples 1 and 3 of XEP-0096. Both SOCKS5 bytestreams (XEP-0065) * and IBB (XEP-0047) are sent as options. The default, per XEP-0096 3.1, is * SOCKS5, with IBB as the fallback. * * The outgoing IQ will be similar to the one below: * * * * * We should destroy this, right? * * * * * * * * * * * * * @see handleSIOfferQueryIQ:withInfo: */ - (void)querySIOffer { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:_recipientJID elementID:[xmppStream generateUUID]]; [iq addAttributeWithName:@"from" stringValue:xmppStream.myJID.full]; // Store the sid; we'll need this later self.sid = [xmppStream generateUUID]; NSXMLElement *si = [NSXMLElement elementWithName:@"si" xmlns:XMPPSINamespace]; [si addAttributeWithName:@"id" stringValue:self.sid]; [si addAttributeWithName:@"profile" stringValue:XMPPSIProfileFileTransferNamespace]; [iq addChild:si]; // Generate a random filename if one isn't provided NSString *fileName; if (_outgoingFileName) { // If there is a name provided, but a random one should be created, we'll keep the file ext. if (_shouldGenerateRandomName) { NSString *ext = [[_outgoingFileName componentsSeparatedByString:@"."] lastObject]; fileName = [NSString stringWithFormat:@"%@.%@", [xmppStream generateUUID], ext]; } else { fileName = _outgoingFileName; } } else { fileName = [xmppStream generateUUID]; } NSXMLElement *file = [NSXMLElement elementWithName:@"file" xmlns:XMPPSIProfileFileTransferNamespace]; [file addAttributeWithName:@"name" stringValue:fileName]; [file addAttributeWithName:@"size" stringValue:[[NSString alloc] initWithFormat:@"%lu", (unsigned long) [_outgoingData length]]];//TODO [si addChild:file]; // Only include description if it's provided if (_outgoingFileDescription) { NSXMLElement *desc = [NSXMLElement elementWithName:@"desc" stringValue:_outgoingFileDescription]; [file addChild:desc]; } NSXMLElement *feature = [NSXMLElement elementWithName:@"feature" xmlns:XMPPFeatureNegNamespace]; [si addChild:feature]; NSXMLElement *x = [NSXMLElement elementWithName:@"x" xmlns:@"jabber:x:data"]; [x addAttributeWithName:@"type" stringValue:@"form"]; [feature addChild:x]; NSXMLElement *field = [NSXMLElement elementWithName:@"field"]; [field addAttributeWithName:@"var" stringValue:@"stream-method"]; [field addAttributeWithName:@"type" stringValue:@"list-single"]; [x addChild:field]; // We support SOCKS5 if (!self.disableSOCKS5) { NSXMLElement *option = [NSXMLElement elementWithName:@"option"]; [field addChild:option]; NSXMLElement *value = [NSXMLElement elementWithName:@"value" stringValue:XMPPBytestreamsNamespace]; [option addChild:value]; } // We support IBB if (!self.disableIBB) { NSXMLElement *option2 = [NSXMLElement elementWithName:@"option"]; [field addChild:option2]; NSXMLElement *value2 = [NSXMLElement elementWithName:@"value" stringValue:XMPPIBBNamespace]; [option2 addChild:value2]; } [_idTracker addElement:iq target:self selector:@selector(handleSIOfferQueryIQ:withInfo:) timeout:OUTGOING_DEFAULT_TIMEOUT]; [xmppStream sendElement:iq]; } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } /** * This method begins the process of collecting streamhosts to send to the * recipient. The first (and preferred) streamhost is the sender's local * IP Address and a random local port. If the recipient is able to connect * using this streamhost, the bytestream should be directly between clients and * not require the use of a proxy. */ - (void)collectStreamHosts { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { _localIPAddress = [self getIPAddress:YES]; if (!_localPort) { _localPort = [XMPPOutgoingFileTransfer getRandomPort]; } _streamhosts = [NSMutableArray new]; // Don't send direct streamhost details if disabled. if (!self.disableDirectTransfers) { NSXMLElement *streamHost = [NSXMLElement elementWithName:@"streamhost"]; [streamHost addAttributeWithName:@"jid" stringValue:xmppStream.myJID.full]; [streamHost addAttributeWithName:@"host" stringValue:_localIPAddress]; [streamHost addAttributeWithName:@"port" intValue:_localPort]; [_streamhosts addObject:streamHost]; } [self queryProxyDiscoItems]; } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } /** * This method queries the server to determine what its services are, hopefully * finding that one of them is a proxy. A `disco#items` query is sent to the * domain of the file transfer initiator. * * @see handleProxyDiscoItemsQueryIQ:withInfo: */ - (void)queryProxyDiscoItems { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { NSString *toStr = xmppStream.myJID.domain; NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:XMPPDiscoItemsNamespace]; XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:[XMPPJID jidWithString:toStr] elementID:[xmppStream generateUUID] child:query]; [_idTracker addElement:iq target:self selector:@selector(handleProxyDiscoItemsQueryIQ:withInfo:) timeout:OUTGOING_DEFAULT_TIMEOUT]; [xmppStream sendElement:iq]; } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } /** * This method queries a JID directly to determine whether or not it is a proxy * service. The provided JID will be in the form `subdomain.domain.com`, likely * `proxy.domain.com`. This method will be called for each service found at the * initiator's server until a proxy service is found or all services have been * exhausted. * * @see handleProxyDiscoInfoQueryIQ:withInfo: */ - (void)queryProxyDiscoInfoWithJID:(XMPPJID *)jid { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:jid elementID:[xmppStream generateUUID]]; NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:XMPPDiscoInfoNamespace]; [iq addChild:query]; [_idTracker addElement:iq target:self selector:@selector(handleProxyDiscoInfoQueryIQ:withInfo:) timeout:OUTGOING_DEFAULT_TIMEOUT]; [xmppStream sendElement:iq]; } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } /** * This method queries a JID directly to determine its address and port. It has * already been established that the provided JID is indeed a proxy. We merely * need to know how to connect. * * @see handleProxyAddressQueryIQ:withInfo: */ - (void)queryProxyAddressWithJID:(XMPPJID *)jid { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:jid elementID:[xmppStream generateUUID]]; NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:XMPPBytestreamsNamespace]; [iq addChild:query]; [_idTracker addElement:iq target:self selector:@selector(handleProxyAddressQueryIQ:withInfo:) timeout:OUTGOING_DEFAULT_TIMEOUT]; [xmppStream sendElement:iq]; } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } /** * This method sends the list of streamhosts to the recipient and waits for a * connection on one of them. * * One of these streamhosts will be a local IP address of the sender. If either * or both of the parties are behind a Network Address Translation (NAT) device, * this will not work (provided that they aren't on the same Local Area Network * (LAN). In theory, this means that if both devices are on cellular data, they * should be able to establish a direct connection. If one (or both) are on wifi, * either a proxy streamhost will have to be used or IBB will have to be used. */ - (void)sendStreamHostsAndWaitForConnection { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { if (_streamhosts.count < 1) { NSString *errMsg = [NSString stringWithFormat:@"Unable to send streamhosts to %@", _recipientJID.full]; [self failWithReason:errMsg error:nil]; return; } XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:_recipientJID elementID:[xmppStream generateUUID]]; [iq addAttributeWithName:@"xmlns" stringValue:@"jabber:client"]; [iq addAttributeWithName:@"from" stringValue:xmppStream.myJID.full]; NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:XMPPBytestreamsNamespace]; [query addAttributeWithName:@"sid" stringValue:self.sid]; for (NSXMLElement *streamhost in _streamhosts) { [streamhost detach]; [query addChild:streamhost]; } [iq addChild:query]; [_idTracker addElement:iq target:self selector:@selector(handleSentStreamhostsQueryIQ:withInfo:) timeout:OUTGOING_DEFAULT_TIMEOUT]; // Send the list of streamhosts to the recipient [xmppStream sendElement:iq]; // Create a socket to listen for a direct connection if (!_asyncSocket) { _asyncSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:moduleQueue]; } NSError *error; if (![_asyncSocket acceptOnPort:_localPort error:&error]) { NSString *errMsg = [NSString stringWithFormat:@"Failed to open port %d", _localPort]; [self failWithReason:errMsg error:error]; } } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } #pragma mark - IBB Transfer /** * This method is responsible for opening a new In-Band Bytestream, as shown in * XEP-0047 Example 1. We *MUST* send the same sid inside the stanza * that was used in the SI offer. * * The outgoing IQ will be similar to the following: * * * * * * @see handleInitialIBBQueryIQ:withInfo: */ - (void)beginIBBTransfer { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:_recipientJID elementID:[xmppStream generateUUID]]; NSXMLElement *open = [NSXMLElement elementWithName:@"open" xmlns:XMPPIBBNamespace]; [open addAttributeWithName:@"block-size" intValue:_blockSize]; [open addAttributeWithName:@"sid" stringValue:self.sid]; [open addAttributeWithName:@"stanza" stringValue:@"iq"]; [iq addChild:open]; [_idTracker addElement:iq target:self selector:@selector(handleInitialIBBQueryIQ:withInfo:) timeout:OUTGOING_DEFAULT_TIMEOUT]; [xmppStream sendElement:iq]; // Convert our data to base64 for the IBB transmission _outgoingDataBase64 = [_outgoingData base64EncodedStringWithOptions:0]; _totalDataSize = _outgoingDataBase64.length; } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } /** * This method handles the response to a query matching Example 1. Initiator * requests session (XEP-0047). * * @see beginIBBTransfer * * The possible responses are described in Examples 2-5 of XEP-0047. */ - (void)handleInitialIBBQueryIQ:(XMPPIQ *)iq withInfo:(XMPPBasicTrackingInfo *)info { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { if (!iq) { // If we're inside this block, it means that the timeout has been // fired and we need to force a failure NSString *errMsg = @"Timeout waiting for response to IBB intiation."; [self failWithReason:errMsg error:nil]; } NSXMLElement *errorElem = [iq elementForName:@"error"]; if (errorElem) { NSString *errType = [errorElem attributeStringValueForName:@"type"]; // Handle Example 3 and 5 if ([errType isEqualToString:@"cancel"]) { NSString *errMsg = [NSString stringWithFormat:@"Error initiating IBB: %@", [errorElem childAtIndex:0].name]; [self failWithReason:errMsg error:nil]; return_from_block; } // Handle Example 4. We'll divide the block-size by 4 and try again. if ([errType isEqualToString:@"modify"] && [[errorElem childAtIndex:0].name isEqualToString:@"resource-constraint"]) { XMPPLogInfo(@"Responder prefers smaller IBB chunks. Shrinking block-size and retrying"); _blockSize /= 2; [self beginIBBTransfer]; return_from_block; } } // Handle Example 2. Responder accepts session if (iq.childCount == 0) { XMPPLogVerbose(@"Responder has accepted IBB session. Begin sending data"); [self sendIBBData]; } } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } /** * Sends the data to the recipient via IBB. This method will continue until it * has verified that all the data has been sent, it fails, or receives an error * from the recipient. It will close the IBB stream upon completion. * * Example 6. Sending data in an IQ stanza (XEP-0047) * * * * qANQR1DBwU4DX7jmYZnncmUQB/9KuKBddzQH+tZ1ZywKK0yHKnq57kWq+RFtQdCJ * WpdWpR0uQsuJe7+vh3NWn59/gTc5MDlX8dS9p0ovStmNcyLhxVgmqS8ZKhsblVeu * IpQ0JgavABqibJolc3BKrVtVV1igKiX/N7Pi8RtY1K18toaMDhdEfhBRzO/XB0+P * AQhYlRjNacGcslkhXqNjK5Va4tuOAPy2n1Q8UUrHbUd0g+xJ9Bm0G0LZXyvCWyKH * kuNEHFQiLuCY6Iv0myq6iX6tjuHehZlFSh80b5BVV9tNLwNR5Eqz1klxMhoghJOA * * * * @see handleIBBTransferQueryIQ:withInfo: */ - (void)sendIBBData { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { if (_sentDataSize < _totalDataSize) { XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:_recipientJID elementID:[xmppStream generateUUID]]; NSXMLElement *data = [NSXMLElement elementWithName:@"data" xmlns:XMPPIBBNamespace]; [data addAttributeWithName:@"sid" stringValue:self.sid]; [data addAttributeWithName:@"seq" intValue:_outgoingDataBlockSeq++]; // Get the base64 data for our block NSUInteger length = _sentDataSize + _blockSize > _totalDataSize ? _totalDataSize - _sentDataSize : _blockSize; NSRange range = NSMakeRange(_sentDataSize, length); NSString *dataString = [_outgoingDataBase64 substringWithRange:range]; XMPPLogVerbose(@"Uploading %lu/%lu bytes in IBB transfer.", (unsigned long) _sentDataSize, (unsigned long) _totalDataSize); [data setStringValue:dataString]; [iq addChild:data]; [_idTracker addElement:iq target:self selector:@selector(handleIBBTransferQueryIQ:withInfo:) timeout:OUTGOING_DEFAULT_TIMEOUT]; [xmppStream sendElement:iq]; } else { XMPPLogInfo(@"IBB file transfer complete. Closing stream..."); // All the data has been sent. Alert the delegate that the transfer // was successful and close the stream. [multicastDelegate xmppOutgoingFileTransferDidSucceed:self]; [self closeIBB]; } } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } /** * Handles the response from the data recipient during an IBB file transfer. The * recipient should be sending back a childless result IQ confirming that they * received the data we sent. We will wait until receiving this IQ before * sending the next block of data. * * Example 7. Acknowledging data received via IQ (XEP-0047) * * * * @see sendIBBData */ - (void)handleIBBTransferQueryIQ:(XMPPIQ *)iq withInfo:(XMPPBasicTrackingInfo *)info { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { if (!iq) { // If we're inside this block, it means that the timeout has been // fired and we need to force a failure NSString *errMsg = @"Timeout waiting for response to IBB sent data."; [self failWithReason:errMsg error:nil]; } NSXMLElement *errorElem = [iq elementForName:@"error"]; // Handle dropped connection or recipient offline. if (errorElem) { NSString *errMsg = [NSString stringWithFormat:@"Error transferring with IBB: %@", [errorElem childAtIndex:0]]; NSError *err = [self localErrorWithMessage:errMsg code:-1]; NSString *reason = @"The recipient might be offline, the connection was interrupted, or the transfer was canceled."; [self failWithReason:reason error:err]; return; } // Handle the scenario when the recipient closes the bytestream. NSXMLElement *close = [iq elementForName:@"close"]; if (close) { if (_sentDataSize >= _totalDataSize) { // We can assume the transfer was successful. [multicastDelegate xmppOutgoingFileTransferDidSucceed:self]; [multicastDelegate xmppOutgoingFileTransferIBBClosed:self]; // As per Examples 8-9 (XEP-0047), we SHOULD send the following // response to let the other party know it's alright to close the // bytestream. There's no reason to track it, however. // // XMPPIQ *resultIq = [XMPPIQ iqWithType:@"result" to:_recipientJID elementID:iq.elementID]; [xmppStream sendElement:resultIq]; } else { // There must have been a reason to close, but we don't know it. // Therefore, the transfer might not have been successful. [self failWithReason:@"Recipient closed IBB stream." error:nil]; [multicastDelegate xmppOutgoingFileTransferIBBClosed:self]; } } // At this point, we're assuming that we've received the stanza shown // above and the recipient has successfully received the data we sent, // so we should now send them the next block of data. _sentDataSize += _blockSize; [self sendIBBData]; XMPPLogVerbose( @"Received response signifying successful IBB stanza. Sending the next block"); } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } /** * XEP-0047 Example 8. Closing the bytestream * * Sends an IQ to the recipient stating that the bytestream will be closed. As * per the protocol, we SHOULD wait for an IQ response before we can consider * the bytestream to be closed. * * Note that the 'sid' must be included. * * @see handleCloseIBBQueryIQ:withInfo: */ - (void)closeIBB { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:_recipientJID elementID:[xmppStream generateUUID]]; NSXMLElement *close = [NSXMLElement elementWithName:@"close" xmlns:XMPPIBBNamespace]; [close addAttributeWithName:@"sid" stringValue:self.sid]; [iq addChild:close]; [_idTracker addElement:iq target:self selector:@selector(handleCloseIBBQueryIQ:withInfo:) timeout:OUTGOING_DEFAULT_TIMEOUT]; [xmppStream sendElement:iq]; } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } /** * Handles the response of our query to close the IBB. When it gets a response, * it merely changes the state and logs that the stream is closed. a successful * response will look like XEP-0047 Example 9. */ - (void)handleCloseIBBQueryIQ:(XMPPIQ *)iq withInfo:(XMPPBasicTrackingInfo *)info { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { if (!iq) { // If we're inside this block, it means that the timeout has been // fired and we need to force a failure NSString *errMsg = @"Timeout waiting for close IBB response."; [self failWithReason:errMsg error:nil]; } // The protocol states that we might receive an // response, so we'll just ignore that here NSXMLElement *errorElem = [iq elementForName:@"error"]; if (errorElem && ![errorElem.name isEqualToString:@"item-not-found"]) { NSString *errMsg = [errorElem.children componentsJoinedByString:@", "]; NSError *err = [self localErrorWithMessage:errMsg code:[errorElem attributeIntValueForName:@"code"]]; [self failWithReason:errMsg error:err]; return_from_block; } // We're assuming that if it makes it this far, it's the response we want [multicastDelegate xmppOutgoingFileTransferIBBClosed:self]; } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } #pragma mark - Response Handling /** * This method handles the response of our `disco#info` query sent to the file * recipient. We ensure that the recipient has the capabilities for our transfer * before sending an SI offer. * * * * * * * * * * */ - (void)handleRecipientDiscoInfoQueryIQ:(XMPPIQ *)iq withInfo:(XMPPBasicTrackingInfo *)info { XMPPLogTrace(); XMPPLogInfo(@"iq: %@, info: %@", iq, info); dispatch_block_t block = ^{ @autoreleasepool { if (!iq) { // If we're inside this block, it means that the timeout has been // fired and we need to force a failure NSString *errMsg = @"Timeout waiting for recipient `disco#info` response."; [self failWithReason:errMsg error:nil]; } NSXMLElement *errorElem = [iq elementForName:@"error"]; if (errorElem) { NSString *errMsg = [errorElem.children componentsJoinedByString:@", "]; NSError *err = [self localErrorWithMessage:errMsg code:[errorElem attributeIntValueForName:@"code"]]; [self failWithReason:errMsg error:err]; return_from_block; } NSXMLElement *query = [iq elementForName:@"query"]; // We're checking to see if the recipient has the features we need BOOL hasSI = NO; BOOL hasFT = NO; BOOL hasSOCKS5 = NO; BOOL hasIBB = NO; NSArray *features = [query elementsForName:@"feature"]; for (NSXMLElement *feature in features) { NSString *var = [feature attributeStringValueForName:@"var"]; if ([var isEqualToString:XMPPSINamespace]) hasSI = YES; if ([var isEqualToString:XMPPSIProfileFileTransferNamespace]) hasFT = YES; if ([var isEqualToString:XMPPBytestreamsNamespace]) hasSOCKS5 = YES; if ([var isEqualToString:XMPPIBBNamespace]) hasIBB = YES; } hasSOCKS5 = hasSI && hasFT && hasSOCKS5; hasIBB = hasSI && hasFT && hasIBB; if (!hasSOCKS5 || !hasIBB) { NSString *errMsg = @"Unable to send SI offer; the recipient doesn't have the required features."; XMPPLogInfo(@"%@: %@", THIS_FILE, errMsg); NSError *err = [self localErrorWithMessage:errMsg code:-1]; [multicastDelegate xmppOutgoingFileTransfer:self didFailWithError:err]; return_from_block; } [self querySIOffer]; // TODO: // The following lines are currently useless. Maybe at some point I'll // add the ability to restart the transfer using IBB if bytestreams // fail, but only if the stream-method is available. if (hasSOCKS5) { _streamMethods |= XMPPFileTransferStreamMethodBytestreams; } if (hasIBB) { _streamMethods |= XMPPFileTransferStreamMethodIBB; } _pastRecipients[_recipientJID.full] = @(_streamMethods); } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } /** * This method is responsible for handling the response to the Stream Initiation * Offer that will be in the form described in Examples 2 and 4 of XEP-0096. * Depending on the response, this method will trigger an error or begin the * transfer process using either SOCKS5 or IBB (whichever is sent back first). * * The response should be in a similar format to that which is shown below: * * * * * * * http://jabber.org/protocol/bytestreams * * * * * */ - (void)handleSIOfferQueryIQ:(XMPPIQ *)iq withInfo:(XMPPBasicTrackingInfo *)info { XMPPLogTrace(); dispatch_block_t block = ^void { @autoreleasepool { if (!iq) { // If we're inside this block, it means that the timeout has been // fired and we need to force a failure NSString *errMsg = @"Timeout waiting for SI offer response."; [self failWithReason:errMsg error:nil]; } NSXMLElement *errorElem = [iq elementForName:@"error"]; if (errorElem) { NSString *errMsg = [errorElem.children componentsJoinedByString:@", "]; NSError *err = [self localErrorWithMessage:errMsg code:[errorElem attributeIntValueForName:@"code"]]; [self failWithReason:@"There was an issue with the SI offer." error:err]; return_from_block; } NSXMLElement *si = iq.childElement; NSXMLElement *feature = (DDXMLElement *) [si childAtIndex:0]; NSXMLElement *x = (DDXMLElement *) [feature childAtIndex:0]; NSXMLElement *field = (DDXMLElement *) [x childAtIndex:0]; NSXMLElement *value = (DDXMLElement *) [field childAtIndex:0]; if ([[value stringValue] isEqualToString:XMPPBytestreamsNamespace]) { XMPPLogVerbose(@"The recipient has confirmed the use of SOCKS5. Starting transfer..."); [self collectStreamHosts]; } else if ([[value stringValue] isEqualToString:XMPPIBBNamespace]) { XMPPLogVerbose(@"The recipient has confirmed the use of IBB. Beginning IBB transfer"); [self beginIBBTransfer]; } } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } /** * This method handles the server's response to the `disco#items` query sent * before. It iterates through the results and queries each JID to determine * whether or not it is a proxy service. * * @see queryProxyDiscoInfoWithJID: */ - (void)handleProxyDiscoItemsQueryIQ:(XMPPIQ *)iq withInfo:(XMPPBasicTrackingInfo *)info { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { if (!iq) { // If we're inside this block, it means that the timeout has been // fired and we need to force a failure NSString *errMsg = @"Timeout waiting for proxy `disco#items` response."; [self failWithReason:errMsg error:nil]; } NSXMLElement *errorElem = [iq elementForName:@"error"]; if (errorElem) { NSString *errMsg = [errorElem.children componentsJoinedByString:@", "]; NSError *err = [self localErrorWithMessage:errMsg code:[errorElem attributeIntValueForName:@"code"]]; [self failWithReason:@"There was an error with the disco#items request." error:err]; return_from_block; } NSXMLElement *query = [iq elementForName:@"query" xmlns:XMPPDiscoItemsNamespace]; if (!query) return; NSArray *items = [query elementsForName:@"item"]; for (NSXMLElement *item in items) { XMPPJID *itemJid = [XMPPJID jidWithString:[item attributeStringValueForName:@"jid"]]; if (itemJid) { XMPPLogVerbose(@"Found service %@. Querying to see if it's a proxy.", itemJid.full); [self queryProxyDiscoInfoWithJID:itemJid]; } } } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } /** * This method handles the server's response to the `disco#info` query sent * before. It determines whether or not the service is indeed a proxy. If it is, * the service is queried for its address and port. * * @see queryProxyAddressWithJID */ - (void)handleProxyDiscoInfoQueryIQ:(XMPPIQ *)iq withInfo:(XMPPBasicTrackingInfo *)info { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { if (!iq) { // If we're inside this block, it means that the timeout has been // fired and we need to force a failure NSString *errMsg = @"Timeout waiting for proxy `disco#info` response."; [self failWithReason:errMsg error:nil]; } NSXMLElement *errorElem = [iq elementForName:@"error"]; if (errorElem) { NSString *errMsg = [errorElem.children componentsJoinedByString:@", "]; NSError *err = [self localErrorWithMessage:errMsg code:[errorElem attributeIntValueForName:@"code"]]; [self failWithReason:@"There was an error with the disco#info request." error:err]; return_from_block; } NSXMLElement *query = [iq elementForName:@"query" xmlns:XMPPDiscoInfoNamespace]; NSArray *identities = [query elementsForName:@"identity"]; for (NSXMLElement *identity in identities) { NSString *category = [identity attributeStringValueForName:@"category"]; NSString *type = [identity attributeStringValueForName:@"type"]; if ([category isEqualToString:@"proxy"] && [type isEqualToString:@"bytestreams"]) { [self queryProxyAddressWithJID:iq.from]; } } } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } /** * This method handles the server's response to the address query sent before. * If there is no error, we assume that we were sent an address and a port in * the form of a streamhost and send the streamhosts to the recipient to begin * the actual connection process. * * @see sendStreamHostsAndWaitForConnection */ - (void)handleProxyAddressQueryIQ:(XMPPIQ *)iq withInfo:(XMPPBasicTrackingInfo *)info { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { if (!iq) { // If we're inside this block, it means that the timeout has been // fired and we need to force a failure NSString *errMsg = @"Timeout waiting for proxy address discovery response."; [self failWithReason:errMsg error:nil]; } NSXMLElement *errorElem = [iq elementForName:@"error"]; if (errorElem) { NSString *errMsg = [errorElem.children componentsJoinedByString:@", "]; NSError *err = [self localErrorWithMessage:errMsg code:[errorElem attributeIntValueForName:@"code"]]; [self failWithReason:@"There was an issue with the proxy address query." error:err]; return_from_block; } NSXMLElement *query = [iq elementForName:@"query" xmlns:XMPPBytestreamsNamespace]; NSXMLElement *streamHost = [query elementForName:@"streamhost"]; if (!streamHost) { [self failWithReason:@"There must be at least one streamhost." error:nil]; return_from_block; } // Detach the streamHost object so it can later be added to a query [streamHost detach]; [_streamhosts addObject:streamHost]; [self sendStreamHostsAndWaitForConnection]; } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } /** * This method handles the server's response after sending the query of * streamhosts. If there is an error, it alerts the delegate and causes the * transfer to fail. Otherwise, the connection will proceed and the data will be * written to the bytestream. */ - (void)handleSentStreamhostsQueryIQ:(XMPPIQ *)iq withInfo:(XMPPBasicTrackingInfo *)info { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { NSXMLElement *errorElem = [iq elementForName:@"error"]; if (errorElem) { NSString *errMsg = [errorElem.children componentsJoinedByString:@", "]; NSError *err = [self localErrorWithMessage:errMsg code:[errorElem attributeIntValueForName:@"code"]]; [self failWithReason:@"There was an issue with sending the streamhosts." error:err]; return_from_block; } // Check for // // We're expecting something like: // // // // // // NSXMLElement *query = iq.childElement; NSXMLElement *streamhostUsed = [query elementForName:@"streamhost-used"]; NSString *jid = [streamhostUsed attributeStringValueForName:@"jid"]; XMPPLogVerbose(@"%@: streamhost-used received with jid: %@", THIS_FILE, jid); if ([jid isEqualToString:xmppStream.myJID.full]) { XMPPLogVerbose(@"%@: writing data via direct connection.", THIS_FILE); [_outgoingSocket writeData:_outgoingData withTimeout:TIMEOUT_WRITE tag:SOCKS_TAG_WRITE_DATA]; return; } XMPPLogVerbose(@"%@: unable use a direct connection; trying the provided streamhost.", THIS_FILE); if (_outgoingSocket) { if (_outgoingSocket.isConnected) { [_outgoingSocket disconnect]; } _outgoingSocket = nil; } // We need to get the streamhost which we discovered earlier as a proxy. NSXMLElement *proxy; for (NSXMLElement *streamhost in _streamhosts) { if ([jid isEqualToString:[streamhost attributeStringValueForName:@"jid"]]) { proxy = streamhost; _proxyJID = [XMPPJID jidWithString:jid]; break; } } if (_asyncSocket) { [_asyncSocket setDelegate:nil]; [_asyncSocket disconnect]; } if (!_asyncSocket) { _asyncSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:_outgoingQueue]; } else { [_asyncSocket setDelegate:self]; } NSError *err; NSString *proxyHost = [proxy attributeStringValueForName:@"host"]; uint16_t proxyPort = (gl_uint16_t) [proxy attributeUnsignedIntegerValueForName:@"port"]; if (![_asyncSocket connectToHost:proxyHost onPort:proxyPort error:&err]) { [self failWithReason:@"Unable to connect to proxy." error:err]; return_from_block; } _transferState = XMPPOFTStateConnectingToProxy; // See the GCDAsyncSocket Delegate for the next steps. } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } /** * This method handles the server's response after sending the IQ * query as described in XEP-0065 Example 24. * * If the response is valid (Example 25), the actual transfer of data begins. */ - (void)handleSentActivateQueryIQ:(XMPPIQ *)iq withInfo:(XMPPBasicTrackingInfo *)info { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { if (!iq) { // If we're inside this block, it means that the timeout has been // fired and we need to force a failure NSString *errMsg = @"Timeout waiting for sent activate response."; [self failWithReason:errMsg error:nil]; } XMPPLogVerbose(@"Receive response to activate. Starting the actual data transfer now..."); [_asyncSocket writeData:_outgoingData withTimeout:TIMEOUT_WRITE tag:SOCKS_TAG_WRITE_DATA]; } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } #pragma mark - Util Methods - (NSError *)localErrorWithMessage:(NSString *)msg code:(NSInteger)code { NSDictionary *errInfo = @{NSLocalizedDescriptionKey : [msg copy]}; return [NSError errorWithDomain:XMPPOutgoingFileTransferErrorDomain code:code userInfo:errInfo]; } - (NSString *)getIPAddress:(BOOL)preferIPv4 { NSArray *searchArray; if (preferIPv4) { searchArray = @[IOS_WIFI @"/" IP_ADDR_IPv4, IOS_WIFI @"/" IP_ADDR_IPv6, IOS_CELLULAR @"/" IP_ADDR_IPv4, IOS_CELLULAR @"/" IP_ADDR_IPv6]; } else { searchArray = @[IOS_WIFI @"/" IP_ADDR_IPv6, IOS_WIFI @"/" IP_ADDR_IPv4, IOS_CELLULAR @"/" IP_ADDR_IPv6, IOS_CELLULAR @"/" IP_ADDR_IPv4]; } NSDictionary *addresses = [self getIPAddresses]; __block NSString *address; [searchArray enumerateObjectsUsingBlock:^(NSString *key, NSUInteger idx, BOOL *stop) { address = addresses[key]; if (address) *stop = YES; }]; return address; } - (NSDictionary *)getIPAddresses { NSMutableDictionary *addresses = [NSMutableDictionary dictionaryWithCapacity:8]; // Retrieve the current interfaces — returns 0 on success struct ifaddrs *interfaces; if (!getifaddrs(&interfaces)) { // Loop through linked list of interfaces struct ifaddrs *curr; for (curr = interfaces; curr; curr = curr->ifa_next) { if (!curr->ifa_flags & IFF_UP) { continue; } const struct sockaddr_in *addr = (const struct sockaddr_in *) curr->ifa_addr; char addr_buf[MAX(INET_ADDRSTRLEN, INET6_ADDRSTRLEN)]; if (addr && (addr->sin_family == AF_INET || addr->sin_family == AF_INET6)) { NSString *name = [NSString stringWithUTF8String:curr->ifa_name]; NSString *type; if (addr->sin_family == AF_INET) { if (inet_ntop(AF_INET, &addr->sin_addr, addr_buf, INET_ADDRSTRLEN)) { type = IP_ADDR_IPv4; } } else { const struct sockaddr_in6 *addr6 = (const struct sockaddr_in6 *) curr->ifa_addr; if (inet_ntop(AF_INET6, &addr6->sin6_addr, addr_buf, INET6_ADDRSTRLEN)) { type = IP_ADDR_IPv6; } } if (type) { NSString *key = [NSString stringWithFormat:@"%@/%@", name, type]; addresses[key] = [NSString stringWithUTF8String:addr_buf]; } } } freeifaddrs(interfaces); } return addresses.count ? addresses : nil; } /** * Returns a random port number between 1024 and 49151, since these are the * values available to use as ports. */ + (uint16_t)getRandomPort { int port = arc4random_uniform(49151); return (uint16_t) (port < 1024 ? port + 1024 : port); } /** * This method returns the SHA1 hash as per XEP-0065. * * The [address] MUST be SHA1(SID + Initiator JID + Target JID) and the output * is hexadecimal encoded (not binary). * * Because this is an outgoing file transfer, we are always the initiator. */ - (NSData *)sha1Hash { NSString *hashMe = [NSString stringWithFormat:@"%@%@%@", self.sid, xmppStream.myJID.full, _recipientJID.full]; NSData *hashRaw = [[hashMe dataUsingEncoding:NSUTF8StringEncoding] xmpp_sha1Digest]; NSData *hash = [[hashRaw xmpp_hexStringValue] dataUsingEncoding:NSUTF8StringEncoding]; XMPPLogVerbose(@"%@: hashMe : %@", THIS_FILE, hashMe); XMPPLogVerbose(@"%@: hashRaw: %@", THIS_FILE, hashRaw); XMPPLogVerbose(@"%@: hash : %@", THIS_FILE, hash); return hash; } /** * This method is called to clean up everything if the transfer fails. */ - (void)failWithReason:(NSString *)causeOfFailure error:(NSError *)error { XMPPLogTrace(); XMPPLogInfo(@"Outgoing file transfer failed because: %@", causeOfFailure); if (!error && causeOfFailure) { NSDictionary *errInfo = @{NSLocalizedDescriptionKey : causeOfFailure}; error = [NSError errorWithDomain:XMPPOutgoingFileTransferErrorDomain code:-1 userInfo:errInfo]; } [self cleanUp]; [multicastDelegate xmppOutgoingFileTransfer:self didFailWithError:error]; } /** * This method is called to clean up everything if the transfer succeeds. */ - (void)transferSuccess { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { _transferState = XMPPOFTStateFinished; [multicastDelegate xmppOutgoingFileTransferDidSucceed:self]; [self cleanUp]; } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } /** * This method is used to reset the system for receiving new files. */ - (void)cleanUp { XMPPLogTrace(); if (_asyncSocket) { [_asyncSocket setDelegate:nil]; [_asyncSocket disconnect]; _asyncSocket = nil; } if (_outgoingSocket) { [_outgoingSocket setDelegate:nil]; [_outgoingSocket disconnect]; _outgoingSocket = nil; } _streamMethods &= 0; _transferState = XMPPOFTStateNone; _totalDataSize = 0; _outgoingDataBlockSeq = 0; _sentDataSize = 0; _outgoingDataBase64 = nil; } #pragma mark - XMPPStreamDelegate /** * Default XMPPStreamDelegate method. We need this to handle the IQ responses. */ - (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq { NSString *type = iq.type; if ([type isEqualToString:@"result"] || [type isEqualToString:@"error"]) { return [_idTracker invokeForElement:iq withObject:iq]; } return NO; } #pragma mark - GCDAsyncSocketDelegate - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket { XMPPLogVerbose(@"Did accept new socket"); XMPPLogVerbose(@"connected host: %@", newSocket.connectedHost); XMPPLogVerbose(@"connected port: %hu", newSocket.connectedPort); _outgoingSocket = newSocket; [_outgoingSocket readDataToLength:3 withTimeout:20 tag:SOCKS_TAG_READ_METHOD]; } - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port { XMPPLogVerbose(@"%@: didConnectToHost:%@ port:%d", THIS_FILE, host, port); [self socks5WriteProxyMethod]; } - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag { XMPPLogVerbose(@"%@: didReadData:%@ withTag:%ld", THIS_FILE, data, tag); switch (tag) { case SOCKS_TAG_READ_METHOD: [self socks5ReadMethod:data]; break; case SOCKS_TAG_READ_REQUEST: [self socks5ReadRequest:data]; break; case SOCKS_TAG_READ_DOMAIN: [self socks5ReadDomain:data]; break; case SOCKS_TAG_READ_PROXY_METHOD: [self socks5ReadProxyMethod:data]; break; case SOCKS_TAG_READ_PROXY_REPLY: [self socks5ReadProxyReply:data]; break; default: break; } } - (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag { XMPPLogVerbose(@"%@: didWriteDataWithTag:%ld", THIS_FILE, tag); switch (tag) { case SOCKS_TAG_WRITE_METHOD: [_outgoingSocket readDataToLength:4 withTimeout:TIMEOUT_READ tag:SOCKS_TAG_READ_REQUEST]; break; case SOCKS_TAG_WRITE_PROXY_METHOD: [_asyncSocket readDataToLength:2 withTimeout:TIMEOUT_READ tag:SOCKS_TAG_READ_PROXY_METHOD]; break; case SOCKS_TAG_WRITE_PROXY_CONNECT: [_asyncSocket readDataToLength:5 withTimeout:TIMEOUT_READ tag:SOCKS_TAG_READ_PROXY_REPLY]; break; case SOCKS_TAG_WRITE_DATA: [self transferSuccess]; break; default: break; } } - (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutReadWithTag:(long)tag elapsed:(NSTimeInterval)elapsed bytesDone:(NSUInteger)length { XMPPLogVerbose(@"%@: socket shouldTimeoutReadWithTag:%ld elapsed:%f bytesDone:%lu", THIS_FILE, tag, elapsed, (unsigned long)length); NSString *reason = [NSString stringWithFormat:@"Read timeout. %lu bytes read.", (unsigned long)length]; [self failWithReason:reason error:nil]; return 0; } - (NSTimeInterval) socket:(GCDAsyncSocket *)sock shouldTimeoutWriteWithTag:(long)tag elapsed:(NSTimeInterval)elapsed bytesDone:(NSUInteger)length { XMPPLogVerbose(@"%@: socket shouldTimeoutWriteWithTag:%ld elapsed:%f bytesDone:%lu", THIS_FILE, tag, elapsed, (unsigned long) length); NSString *reason = [NSString stringWithFormat:@"Write timeout. %lu bytes written.", (unsigned long) length]; [self failWithReason:reason error:nil]; return 0; } - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err { XMPPLogVerbose(@"socket did disconnect with error: %@", err); if (_transferState != XMPPOFTStateFinished && _transferState != XMPPOFTStateNone) { [self failWithReason:@"Socket disconnected before transfer completion." error:err]; } } #pragma mark - SOCKS5 - (void)socks5ReadMethod:(NSData *)incomingData { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { // According to the SOCKS5 protocol (http://tools.ietf.org/html/rfc1928), // we facilitate the role of the 'server' in this scenario, meaning that // the 'client' has connected to us and written data in the following form: // // +----+----------+----------+ // |VER | NMETHODS | METHODS | // +----+----------+----------+ // | 1 | 1 | 1 to 255 | // +----+----------+----------+ // // The VER field should always be set to 5 (for SOCKS v5). // NMETHODS will always be a single byte and since we really only want this // to be 1, we're free to ignore it. // METHODS can have various values, but if it's set to anything other than // 0 (no authentication), we're going to abort the process. // // We're thus expecting: // // VER = 5 // NMETHODS = 1 // METHODS = 0 UInt8 version = [NSNumber xmpp_extractUInt8FromData:incomingData atOffset:0]; UInt8 method = [NSNumber xmpp_extractUInt8FromData:incomingData atOffset:2]; if (version == 5 && method == 0) { // At this point, we've determined that no authentication is required and // are able to proceed. In order to do so, we need to write data in the // following form: // // +----+--------+ // |VER | METHOD | // +----+--------+ // | 1 | 1 | // +----+--------+ // // The VER will once again be set to 5, and METHOD will be set to 0. // // We're sending: // // VER = 5 // METHOD = 0 void *byteBuf = malloc(2); UInt8 ver = 5; memcpy(byteBuf, &ver, sizeof(ver)); UInt8 mtd = 0; memcpy(byteBuf + 1, &mtd, sizeof(mtd)); NSData *responseData = [NSData dataWithBytesNoCopy:byteBuf length:2 freeWhenDone:YES]; XMPPLogVerbose(@"%@: writing SOCKS5 auth response: %@", THIS_FILE, responseData); [_outgoingSocket writeData:responseData withTimeout:TIMEOUT_WRITE tag:SOCKS_TAG_WRITE_METHOD]; } } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } - (void)socks5ReadRequest:(NSData *)incomingData { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { // The SOCKS request is formed as follows: // // +----+-----+-------+------+----------+----------+ // |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | // +----+-----+-------+------+----------+----------+ // | 1 | 1 | X'00' | 1 | Variable | 2 | // +----+-----+-------+------+----------+----------+ // // We're expecting: // // VER = 5 // CMD = 1 (connect) // RSV = 0 (reserved; this will always be 0) // ATYP = 1 (IPv4), 3 (domain name), or 4 (IPv6) // DST.ADDR (varies based on ATYP) // DST.PORT = 0 (according to XEP-0065) // // At this stage, we've only actually read 4 bytes from the stream, those // being VER, CMD, RSV, and ATYP. We need to read ATYP to determine how many // more bytes we should read. Scenarios listed below: // // ATYP = 3 (domain name): Read the next byte which will contain the number // bytes in the address. Then read that many bytes + // 2 for the port. Since this is the only type of // ATYP we want to support, any other fails. We'll go // ahead and read the whole address and port. It // should always be 40 bytes long (SHA1). UInt8 ver = [NSNumber xmpp_extractUInt8FromData:incomingData atOffset:0]; UInt8 cmd = [NSNumber xmpp_extractUInt8FromData:incomingData atOffset:1]; UInt8 atyp = [NSNumber xmpp_extractUInt8FromData:incomingData atOffset:3]; if (ver != 5 || cmd != 1) { [self failWithReason:@"Incorrect SOCKS version or command is not 'CONNECT'." error:nil]; return; } // Read the length byte + the 40-byte SHA1 + 2-byte address NSUInteger length = 43; if (atyp == 3) { [_outgoingSocket readDataToLength:length withTimeout:TIMEOUT_READ tag:SOCKS_TAG_READ_DOMAIN]; } else { [self failWithReason:@"ATYP value is invalid." error:nil]; } } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } - (void)socks5ReadDomain:(NSData *)incomingData { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { NSData *hash = [self sha1Hash]; // We need to pull the address data out, which starts after the first byte // and goes for 40 bytes. NSRange addrRange = NSMakeRange(1, 40); if (![hash isEqualToData:[incomingData subdataWithRange:addrRange]]) { XMPPLogVerbose(@"Addresses don't match. Canceling the SOCKS5 transfer."); [self failWithReason:@"Addresses don't match." error:nil]; return; } // We need to next pull the port out and verify that it's 0x00, 0x00. UInt8 addrPort0 = [NSNumber xmpp_extractUInt8FromData:incomingData atOffset:41]; UInt8 addrPort1 = [NSNumber xmpp_extractUInt8FromData:incomingData atOffset:41]; if (addrPort0 || addrPort1) { XMPPLogVerbose(@"Port should always be 0x00. Canceling the SOCKS5 transfer."); [self failWithReason:@"Port isn't 0x00." error:nil]; return; } // If the DST.ADDR and DST.PORT are valid, then we proceed with the process. // We send our reply which is described below. // // +----+-----+-------+------+----------+----------+ // |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | // +----+-----+-------+------+----------+----------+ // | 1 | 1 | X'00' | 1 | Variable | 2 | // +----+-----+-------+------+----------+----------+ // // VER = 5 (SOCKS5) // REP = 0 (Success) // RSV = 0 // ATYP = 3 (Domain) - NOTE: Since we're using ATYP = 3, we must send the // length of our host in the very next byte. // BND.ADDR = local IP address // BND.PORT = 0x00 // 0x00 const char *host = [_localIPAddress UTF8String]; NSUInteger numBytes = 5 + strlen(host) + 2; void *byteBuf = malloc(numBytes); UInt8 ver = 5; memcpy(byteBuf, &ver, sizeof(ver)); UInt8 rep = 0; memcpy(byteBuf + 1, &rep, sizeof(rep)); UInt8 rsv = 0; memcpy(byteBuf + 2, &rsv, sizeof(rsv)); UInt8 atyp = 3; memcpy(byteBuf + 3, &atyp, sizeof(atyp)); UInt8 hostlen = (UInt8) strlen(host); memcpy(byteBuf + 4, &hostlen, sizeof(hostlen)); memcpy(byteBuf + 5, host, hostlen); UInt8 port = 0; memcpy(byteBuf + 5 + hostlen, &port, sizeof(port)); memcpy(byteBuf + 6 + hostlen, &port, sizeof(port)); NSData *responseData = [NSData dataWithBytesNoCopy:byteBuf length:numBytes freeWhenDone:YES]; XMPPLogVerbose(@"%@: writing SOCKS5 auth response: %@", THIS_FILE, responseData); [_outgoingSocket writeData:responseData withTimeout:TIMEOUT_WRITE tag:SOCKS_TAG_WRITE_REPLY]; _transferState = XMPPOFTStateSOCKSLive; // Now we wait for a IQ stanza before sending the data. } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } - (void)socks5WriteProxyMethod { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { // We will attempt anonymous authentication with the proxy. The request is // the same that we would read if this were a direct connection. The only // difference is this time we initiate the request as a client rather than // being a the 'server.' // // +----+----------+----------+ // |VER | NMETHODS | METHODS | // +----+----------+----------+ // | 1 | 1 | 1 to 255 | // +----+----------+----------+ // // We're sending: // // VER = 5 (SOCKS5) // NMETHODS = 1 (number of methods) // METHODS = 0 (no authentication) void *byteBuf = malloc(3); UInt8 ver = 5; memcpy(byteBuf, &ver, sizeof(ver)); UInt8 nmethods = 1; memcpy(byteBuf + 1, &nmethods, sizeof(nmethods)); UInt8 methods = 0; memcpy(byteBuf + 2, &methods, sizeof(methods)); NSData *data = [NSData dataWithBytesNoCopy:byteBuf length:3 freeWhenDone:YES]; [_asyncSocket writeData:data withTimeout:TIMEOUT_WRITE tag:SOCKS_TAG_WRITE_PROXY_METHOD]; } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } - (void)socks5ReadProxyMethod:(NSData *)incomingData { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { // We've sent a request to connect with no authentication. This data contains // the proxy server's response to our request. // // +----+--------+ // |VER | METHOD | // +----+--------+ // | 1 | 1 | // +----+--------+ // // We're expecting: // // VER = 5 (SOCKS5) // METHOD = 0 (no authentication) UInt8 version = [NSNumber xmpp_extractUInt8FromData:incomingData atOffset:0]; UInt8 method = [NSNumber xmpp_extractUInt8FromData:incomingData atOffset:1]; if (version != 5 || method) { [self failWithReason:@"Proxy doesn't allow anonymous authentication." error:nil]; return; } NSData *hash = [self sha1Hash]; // The SOCKS request is formed as follows: // // +----+-----+-------+------+----------+----------+ // |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | // +----+-----+-------+------+----------+----------+ // | 1 | 1 | X'00' | 1 | Variable | 2 | // +----+-----+-------+------+----------+----------+ // // We're sending: // // VER = 5 // CMD = 1 (connect) // RSV = 0 (reserved; this will always be 0) // ATYP = 3 (domain name) // DST.ADDR (varies based on ATYP) // DST.PORT = 0 (according to XEP-0065) // // Immediately after ATYP, we need to send the length of our address. Because // SHA1 is always 40 bytes, we simply send this value. After it, we append // the actual hash and then the port. void *byteBuf = malloc(5 + 40 + 2); UInt8 ver = 5; memcpy(byteBuf, &ver, sizeof(ver)); UInt8 cmd = 1; memcpy(byteBuf + 1, &cmd, sizeof(cmd)); UInt8 rsv = 0; memcpy(byteBuf + 2, &rsv, sizeof(rsv)); UInt8 atyp = 3; memcpy(byteBuf + 3, &atyp, sizeof(atyp)); UInt8 hashlen = (UInt8) hash.length; memcpy(byteBuf + 4, &hashlen, sizeof(hashlen)); memcpy(byteBuf + 5, hash.bytes, hashlen); UInt8 port = 0; memcpy(byteBuf + 5 + hashlen, &port, sizeof(port)); memcpy(byteBuf + 6 + hashlen, &port, sizeof(port)); NSData *data = [NSData dataWithBytesNoCopy:byteBuf length:47 freeWhenDone:YES]; [_asyncSocket writeData:data withTimeout:TIMEOUT_WRITE tag:SOCKS_TAG_WRITE_PROXY_CONNECT]; XMPPLogVerbose(@"%@: writing connect request: %@", THIS_FILE, data); } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } - (void)socks5ReadProxyReply:(NSData *)incomingData { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { // The server will reply to our connect command with the following: // // +----+-----+-------+------+----------+----------+ // |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | // +----+-----+-------+------+----------+----------+ // | 1 | 1 | X'00' | 1 | Variable | 2 | // +----+-----+-------+------+----------+----------+ // // VER = 5 (SOCKS5) // REP = 0 (Success) // RSV = 0 // ATYP = 3 (Domain) - NOTE: Since we're using ATYP = 3, we must check the // length of the server's host in the next byte. UInt8 ver = [NSNumber xmpp_extractUInt8FromData:incomingData atOffset:0]; UInt8 rep = [NSNumber xmpp_extractUInt8FromData:incomingData atOffset:1]; UInt8 atyp = [NSNumber xmpp_extractUInt8FromData:incomingData atOffset:3]; UInt8 hostlen = [NSNumber xmpp_extractUInt8FromData:incomingData atOffset:4]; if (ver != 5 || rep || atyp != 3) { [self failWithReason:@"Invalid VER, REP, or ATYP." error:nil]; return; } // Read those bytes off into oblivion... [_asyncSocket readDataToLength:hostlen + 2 withTimeout:TIMEOUT_READ tag:-1]; // According to XEP-0065 Example 23, we don't need to validate the // address we were sent (at least that is how I interpret it), so we // can just go ahead and send the IQ query and start // sending the data once we receive our response. NSXMLElement *activate = [NSXMLElement elementWithName:@"activate" stringValue:_recipientJID.full]; NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"http://jabber.org/protocol/bytestreams"]; [query addAttributeWithName:@"sid" stringValue:self.sid]; [query addChild:activate]; XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:_proxyJID elementID:[xmppStream generateUUID] child:query]; [_idTracker addElement:iq target:self selector:@selector(handleSentActivateQueryIQ:withInfo:) timeout:OUTGOING_DEFAULT_TIMEOUT]; [xmppStream sendElement:iq]; } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } @end