// // 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 "XMPPIncomingFileTransfer.h" #import "XMPPConstants.h" #import "XMPPLogging.h" #import "idn-int.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 /** * Tags for _asyncSocket handling. */ #define SOCKS_TAG_WRITE_METHOD 101 #define SOCKS_TAG_READ_METHOD 102 #define SOCKS_TAG_WRITE_CONNECT 103 #define SOCKS_TAG_READ_REPLY 104 #define SOCKS_TAG_READ_ADDRESS 105 #define SOCKS_TAG_READ_DATA 106 #define TIMEOUT_WRITE -1 #define TIMEOUT_READ 5.0 // XMPP Incoming File Transfer State typedef NS_ENUM(int, XMPPIFTState) { XMPPIFTStateNone, XMPPIFTStateWaitingForSIOffer, XMPPIFTStateWaitingForStreamhosts, XMPPIFTStateConnectingToStreamhosts, XMPPIFTStateConnected, XMPPIFTStateWaitingForIBBOpen, XMPPIFTStateWaitingForIBBData }; NSString *const XMPPIncomingFileTransferErrorDomain = @"XMPPIncomingFileTransferErrorDomain"; @interface XMPPIncomingFileTransfer () { XMPPIFTState _transferState; XMPPJID *_senderJID; NSString *_streamhostsQueryId; NSString *_streamhostUsed; NSMutableData *_receivedData; NSString *_receivedFileName; NSUInteger _totalDataSize; NSUInteger _receivedDataSize; dispatch_source_t _ibbTimer; } @end @implementation XMPPIncomingFileTransfer #pragma mark - Lifecycle - (instancetype)initWithDispatchQueue:(dispatch_queue_t)queue { self = [super initWithDispatchQueue:queue]; if (self) { _transferState = XMPPIFTStateNone; } return self; } /** * Standard deconstructor. */ - (void)dealloc { XMPPLogTrace(); if (_transferState != XMPPIFTStateNone) { XMPPLogWarn(@"%@: Deallocating prior to completion or cancellation.", THIS_FILE); } if (_ibbTimer) dispatch_source_cancel(_ibbTimer); #if !OS_OBJECT_USE_OBJC dispatch_release(_ibbTimer); #endif _ibbTimer = NULL; if (_asyncSocket.delegate == self) { [_asyncSocket setDelegate:nil delegateQueue:NULL]; [_asyncSocket disconnect]; } } #pragma mark - Public Methods /** * Public facing method for accepting a SI offer. If autoAcceptFileTransfers is * set to YES, this method will do nothing, since the internal method is invoked * automatically. * * @see sendSIOfferAcceptance: */ - (void)acceptSIOffer:(XMPPIQ *)offer { XMPPLogTrace(); if (!_autoAcceptFileTransfers) { [self sendSIOfferAcceptance:offer]; } } #pragma mark - Private Methods /** * This method will send the device's identity in response to a `disco#info` * query. In our case, we will send something close the following: * * * * * * * * * * * * This tells the requester who they're dealing with and which transfer types * we support. If there's a better way than hard-coding these values, I'm open * to suggestions. */ - (void)sendIdentity:(XMPPIQ *)request { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { XMPPIQ *iq = [XMPPIQ iqWithType:@"result" to:request.from elementID:request.elementID]; [iq addAttributeWithName:@"from" stringValue:xmppStream.myJID.full]; NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:XMPPDiscoInfoNamespace]; NSXMLElement *identity = [NSXMLElement elementWithName:@"identity"]; [identity addAttributeWithName:@"category" stringValue:@"client"]; [identity addAttributeWithName:@"type" stringValue:@"ios-osx"]; [query addChild:identity]; NSXMLElement *feature = [NSXMLElement elementWithName:@"feature"]; [feature addAttributeWithName:@"var" stringValue:XMPPSINamespace]; [query addChild:feature]; NSXMLElement *feature1 = [NSXMLElement elementWithName:@"feature"]; [feature1 addAttributeWithName:@"var" stringValue:XMPPSIProfileFileTransferNamespace]; [query addChild:feature1]; if (!self.disableSOCKS5) { NSXMLElement *feature2 = [NSXMLElement elementWithName:@"feature"]; [feature2 addAttributeWithName:@"var" stringValue:XMPPBytestreamsNamespace]; [query addChild:feature2]; } if (!self.disableIBB) { NSXMLElement *feature3 = [NSXMLElement elementWithName:@"feature"]; [feature3 addAttributeWithName:@"var" stringValue:XMPPIBBNamespace]; [query addChild:feature3]; } [iq addChild:query]; [xmppStream sendElement:iq]; } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } /** * This method will send an IQ stanza accepting the SI offer. We need to choose * which 'stream-method' we prefer to use. For now, we will be using IBB as the * 'stream-method', but SOCKS5 is preferable. * * Take a look at XEP-0096 Examples 2 and 4 for more details. */ - (void)sendSIOfferAcceptance:(XMPPIQ *)offer { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { // Store the sender's JID _senderJID = offer.from; // Store the sid for later use NSXMLElement *inSi = offer.childElement; self.sid = [inSi attributeStringValueForName:@"id"]; // Store the size of the incoming data for later use NSXMLElement *inFile = [inSi elementForName:@"file"]; _totalDataSize = [inFile attributeUnsignedIntegerValueForName:@"size"]; // Store the name of the file for later use _receivedFileName = [inFile attributeStringValueForName:@"name"]; // Outgoing XMPPIQ *iq = [XMPPIQ iqWithType:@"result" to:offer.from elementID:offer.elementID]; NSXMLElement *si = [NSXMLElement elementWithName:@"si" xmlns:XMPPSINamespace]; NSXMLElement *feature = [NSXMLElement elementWithName:@"feature" xmlns:XMPPFeatureNegNamespace]; NSXMLElement *x = [NSXMLElement elementWithName:@"x" xmlns:@"jabber:x:data"]; [x addAttributeWithName:@"type" stringValue:@"submit"]; NSXMLElement *field = [NSXMLElement elementWithName:@"field"]; [field addAttributeWithName:@"var" stringValue:@"stream-method"]; NSXMLElement *value = [NSXMLElement elementWithName:@"value"]; // Prefer SOCKS5 if it's not disabled. if (!self.disableSOCKS5) { [value setStringValue:XMPPBytestreamsNamespace]; _transferState = XMPPIFTStateWaitingForStreamhosts; } else { [value setStringValue:XMPPIBBNamespace]; _transferState = XMPPIFTStateWaitingForIBBOpen; } [field addChild:value]; [x addChild:field]; [feature addChild:x]; [si addChild:feature]; [iq addChild:si]; [xmppStream sendElement:iq]; } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } #pragma mark - IBB Methods /** * This method will send an IQ stanza accepting the IBB request. See XEP-0047 * Example 2 for more details. */ - (void)sendIBBAcceptance:(XMPPIQ *)request { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { XMPPIQ *iq = [XMPPIQ iqWithType:@"result" to:request.from elementID:request.elementID]; [xmppStream sendElement:iq]; // Prepare to receive data _receivedData = [NSMutableData new]; } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } /** * This method is responsible for reading the incoming data from the IQ stanza * and writing it to the member variable '_receivedData'. After successfully * reading the data, a response (XEP-0047 Example 7) will be sent back to the * sender. */ - (void)processReceivedIBBDataIQ:(XMPPIQ *)received { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { // Handle the scenario that the transfer is cancelled. [self resetIBBTimer:20]; // Handle incoming data NSXMLElement *dataElem = received.childElement; NSData *temp = [[NSData alloc] initWithBase64EncodedString:dataElem.stringValue options:0]; [_receivedData appendData:temp]; // According the base64 encoding, it takes up 4/3 n bytes of space, so // we need to find the size of the data before base64. _receivedDataSize += (3 * dataElem.stringValue.length) / 4; XMPPLogVerbose(@"Downloaded %lu/%lu bytes in IBB transfer.", (unsigned long) _receivedDataSize, (unsigned long) _totalDataSize); if (_receivedDataSize < _totalDataSize) { // Send ack response XMPPIQ *iq = [XMPPIQ iqWithType:@"result" to:received.from elementID:received.elementID]; [xmppStream sendElement:iq]; } else { // We're finished! XMPPLogInfo(@"Finished downloading IBB data."); [self transferSuccess]; } } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } #pragma mark - Util Methods /** * This method determines whether or not the IQ stanza is a `disco#info` * request. Should be in the following form: * * * * */ - (BOOL)isDiscoInfoIQ:(XMPPIQ *)iq { if (!iq) return NO; NSXMLElement *query = iq.childElement; return query != nil && [query.xmlns isEqualToString:XMPPDiscoInfoNamespace]; } /** * This method determines whether or not the the IQ stanza is a Stream * Initiation Offer (XEP-0096 Examples 1 and 3). */ - (BOOL)isSIOfferIQ:(XMPPIQ *)iq { if (!iq) return NO; if (![iq.type isEqualToString:@"set"]) return NO; NSXMLElement *si = iq.childElement; if (!si || ![si.xmlns isEqualToString:XMPPSINamespace]) return NO; NSXMLElement *file = (DDXMLElement *) [si childAtIndex:0]; if (!file || ![file.xmlns isEqualToString:XMPPSIProfileFileTransferNamespace]) return NO; NSXMLElement *feature = (DDXMLElement *) [si childAtIndex:1]; return !(!feature || ![feature.xmlns isEqualToString:XMPPFeatureNegNamespace]); // Maybe there should be further verification, but I think this should be // plenty... } /** * This method determines whether or not the IQ stanza is an IBB session request * (XEP-0047 Example 1). */ - (BOOL)isIBBOpenRequestIQ:(XMPPIQ *)iq { if (!iq) return NO; if (![iq.type isEqualToString:@"set"]) return NO; NSXMLElement *open = iq.childElement; return !(!open || ![open.xmlns isEqualToString:XMPPIBBNamespace]); } /** * This method determines whether or not the IQ stanza is an IBB data stanza * (XEP-0047 Example 6). */ - (BOOL)isIBBDataIQ:(XMPPIQ *)iq { if (!iq) return NO; if (![iq.type isEqualToString:@"set"]) return NO; NSXMLElement *data = iq.childElement; return !(!data || ![data.xmlns isEqualToString:XMPPIBBNamespace]); } /** * This method determines whether or not the IQ stanza contains a list of * streamhosts as shown in XEP-0065 Example 12. */ - (BOOL)isStreamhostsListIQ:(XMPPIQ *)iq { if (!iq) return NO; if (![iq.type isEqualToString:@"set"]) return NO; NSXMLElement *query = iq.childElement; if (!query || ![[query attributeStringValueForName:@"sid"] isEqualToString:self.sid]) return NO; return [query elementsForName:@"streamhost"].count > 0; } /** * 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 incoming file transfer, we are always the target. */ - (NSData *)sha1Hash { NSString *hashMe = [NSString stringWithFormat:@"%@%@%@", self.sid, _senderJID.full, xmppStream.myJID.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 when the transfer fails. */ - (void)failWithReason:(NSString *)causeOfFailure error: (NSError *)error { XMPPLogTrace(); XMPPLogInfo(@"Incoming file transfer failed because: %@", causeOfFailure); if (!error && causeOfFailure) { NSDictionary *errInfo = @{NSLocalizedDescriptionKey : causeOfFailure}; error = [NSError errorWithDomain:XMPPIncomingFileTransferErrorDomain code:-1 userInfo:errInfo]; } _transferState = XMPPIFTStateNone; [multicastDelegate xmppIncomingFileTransfer:self didFailWithError:error]; } /** * This method is called when the transfer is successfully completed. It * handles resetting variables for another transfer and alerts the delegate of * the transfer completion. */ - (void)transferSuccess { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { [self cancelIBBTimer]; [multicastDelegate xmppIncomingFileTransfer:self didSucceedWithData:_receivedData named:_receivedFileName]; [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; } _streamMethods &= 0; _transferState = XMPPIFTStateNone; _senderJID = nil; _streamhostsQueryId = nil; _streamhostUsed = nil; _receivedData = nil; _receivedFileName = nil; _totalDataSize = 0; _receivedDataSize = 0; } #pragma mark - Timeouts /** * Resets the IBB timer that will cause the transfer to formally fail if an IBB * data IQ stanza isn't received within the timeout. */ - (void)resetIBBTimer:(NSTimeInterval)timeout { NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue."); if (_ibbTimer == NULL) { _ibbTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, moduleQueue); dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, timeout * NSEC_PER_SEC); dispatch_source_set_timer(_ibbTimer, tt, DISPATCH_TIME_FOREVER, 1); dispatch_resume(_ibbTimer); } else { dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, timeout * NSEC_PER_SEC); dispatch_source_set_timer(_ibbTimer, tt, DISPATCH_TIME_FOREVER, 1); } dispatch_source_set_event_handler(_ibbTimer, ^{ @autoreleasepool { NSString *errMsg = @"The IBB transfer timed out. It's likely that the sender canceled the" @" transfer or has gone offline."; [self failWithReason:errMsg error:nil]; } }); } - (void)cancelIBBTimer { NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue."); if (_ibbTimer) { dispatch_source_cancel(_ibbTimer); #if !OS_OBJECT_USE_OBJC dispatch_release(_ibbTimer); #endif _ibbTimer = NULL; } } #pragma mark - XMPPStreamDelegate - (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq { if (_transferState == XMPPIFTStateNone && [self isDiscoInfoIQ:iq]) { [self sendIdentity:iq]; _transferState = XMPPIFTStateWaitingForSIOffer; return YES; } if ((_transferState == XMPPIFTStateNone || _transferState == XMPPIFTStateWaitingForSIOffer) && [self isSIOfferIQ:iq]) { // Alert the delegate that we've received a stream initiation offer [multicastDelegate xmppIncomingFileTransfer:self didReceiveSIOffer:iq]; if (_autoAcceptFileTransfers) { [self sendSIOfferAcceptance:iq]; } return YES; } if (_transferState == XMPPIFTStateWaitingForStreamhosts && [self isStreamhostsListIQ:iq]) { [self attemptStreamhostsConnection:iq]; return YES; } if (_transferState == XMPPIFTStateWaitingForIBBOpen && [self isIBBOpenRequestIQ:iq]) { [self sendIBBAcceptance:iq]; _transferState = XMPPIFTStateWaitingForIBBData; // Handle the scenario that the transfer is cancelled. [self resetIBBTimer:20]; return YES; } if (_transferState == XMPPIFTStateWaitingForIBBData && [self isIBBDataIQ:iq]) { [self processReceivedIBBDataIQ:iq]; } return iq != nil; } #pragma mark - GCDAsyncSocketDelegate - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port { XMPPLogVerbose(@"%@: didConnectToHost:%@ port:%d", THIS_FILE, host, port); [self socks5WriteMethod]; _transferState = XMPPIFTStateConnected; } - (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_REPLY: [self socks5ReadReply:data]; case SOCKS_TAG_READ_ADDRESS: [_asyncSocket readDataToLength:_totalDataSize withTimeout:TIMEOUT_READ tag:SOCKS_TAG_READ_DATA]; break; case SOCKS_TAG_READ_DATA: // Success! _receivedData = [data mutableCopy]; [self transferSuccess]; default: break; } } - (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag { XMPPLogVerbose(@"%@: didWriteDataWithTag:%ld", THIS_FILE, tag); switch (tag) { case SOCKS_TAG_WRITE_METHOD: [_asyncSocket readDataToLength:2 withTimeout:TIMEOUT_READ tag:SOCKS_TAG_READ_METHOD]; break; case SOCKS_TAG_WRITE_CONNECT: [_asyncSocket readDataToLength:5 withTimeout:TIMEOUT_READ tag:SOCKS_TAG_READ_REPLY]; default: break; } } - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err { XMPPLogTrace(); if (_transferState == XMPPIFTStateConnected) { [self failWithReason:@"Socket disconnected before transfer complete." error:nil]; } } #pragma mark - SOCKS5 /** * This method attempts a connection to each of the streamhosts provided until * either a connection is established or there are no more streamhosts. In the * latter case, an error stanza is sent to the sender. * * @see socket:didConnectToHost:port: */ - (void)attemptStreamhostsConnection:(XMPPIQ *)iq { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { _streamhostsQueryId = iq.elementID; _transferState = XMPPIFTStateConnectingToStreamhosts; _asyncSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:moduleQueue]; // Since we've already validated our IQ stanza, we can just pull the data NSArray *streamhosts = [iq.childElement elementsForName:@"streamhost"]; for (NSXMLElement *streamhost in streamhosts) { NSString *host = [streamhost attributeStringValueForName:@"host"]; uint16_t port = (gl_uint16_t) [streamhost attributeUInt32ValueForName:@"port"]; NSError *err; if (![_asyncSocket connectToHost:host onPort:port error:&err]) { XMPPLogVerbose(@"%@: Unable to host:%@ port:%d error:%@", THIS_FILE, host, port, err); continue; } // If we make it this far, we've successfully connected to one of the hosts. _streamhostUsed = [streamhost attributeStringValueForName:@"jid"]; return; } // If we reach this, we weren't able to connect to any of the streamhosts. // We'll send an error to the sender to let them know, and then we'll alert // the delegate of the failure. // // XEP-0065 Example 13. // // // // // // XMPPIQ *errorIq = [XMPPIQ iqWithType:@"error" to:iq.from elementID:iq.elementID]; NSXMLElement *errorElem = [NSXMLElement elementWithName:@"error"]; [errorElem addAttributeWithName:@"type" stringValue:@"modify"]; NSXMLElement *notAcceptable = [NSXMLElement elementWithName:@"not-acceptable" xmlns:@"urn:ietf:params:xml:ns:xmpp-stanzas"]; [errorElem addChild:notAcceptable]; [errorIq addChild:errorElem]; [xmppStream sendElement:errorIq]; NSString *errMsg = @"Unable to connect to any of the provided streamhosts."; [self failWithReason:errMsg error:nil]; } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } - (void)socks5WriteMethod { 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_METHOD]; } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } - (void)socks5ReadMethod:(NSData *)incomingData { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { // We've sent a request to connect with no authentication. This is the // response: // // +----+--------+ // |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_CONNECT]; XMPPLogVerbose(@"%@: writing connect request: %@", THIS_FILE, data); } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } - (void)socks5ReadReply:(NSData *)incomingData { XMPPLogTrace(); dispatch_block_t block = ^{ @autoreleasepool { // The server/sender 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; } // 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 // just read the next 42 bytes (hostlen + portlen) so there's no // conflict when reading the data and then send to // the file transfer initiator. Note that the sid must be included. // // XEP-0065 Example 17: // // // // // // XMPPIQ *iq = [XMPPIQ iqWithType:@"result" to:_senderJID elementID:_streamhostsQueryId]; NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:XMPPBytestreamsNamespace]; [query addAttributeWithName:@"sid" stringValue:self.sid]; NSXMLElement *streamhostUsed = [NSXMLElement elementWithName:@"streamhost-used"]; [streamhostUsed addAttributeWithName:@"jid" stringValue:_streamhostUsed]; [query addChild:streamhostUsed]; [iq addChild:query]; [xmppStream sendElement:iq]; // We're basically piping these to dev/null because we don't care. // However, we need to tag this read so we can start to read the actual // data once this read is finished. [_asyncSocket readDataToLength:hostlen + 2 withTimeout:TIMEOUT_READ tag:SOCKS_TAG_READ_ADDRESS]; } }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } @end