PNXMPPFramework/Extensions/FileTransfer/XMPPOutgoingFileTransfer.m
2016-02-24 16:56:39 +01:00

2189 lines
72 KiB
Objective-C

//
// 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 <ifaddrs.h>
#import <net/if.h>
#import <netinet/in.h>
#import <arpa/inet.h>
#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:
*
* <iq xmlns="jabber:client"
* from="deckardcain@sanctuary.org/tristram"
* to="tyrael@sanctuary.org/talrashastomb"
* type="get"
* id="purplea2da8fc9">
* <query xmlns="http://jabber.org/protocol/disco#info"/>
* </iq>
*
* @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:
*
* <iq xmlns="jabber:client"
* from="deckardcain@sanctuary.org/tristram"
* to="tyrael@sanctuary.org/talrashastomb"
* type="set"
* id="purplea2da8fca">
* <si xmlns="http://jabber.org/protocol/si"
* id="purplea2da8fcb"
* profile="http://jabber.org/protocol/si/profile/file-transfer">
* <file xmlns="http://jabber.org/protocol/si/profile/file-transfer"
* name="Baal's Soulstone.jpg"
* size="433362">
* <desc>We should destroy this, right?</desc>
* </file>
* <feature xmlns="http://jabber.org/protocol/feature-neg">
* <x xmlns="jabber:x:data" type="form">
* <field var="stream-method" type="list-single">
* <option>
* <value>http://jabber.org/protocol/bytestreams</value>
* </option>
* <option>
* <value>http://jabber.org/protocol/ibb</value>
* </option>
* </field>
* </x>
* </feature>
* </si>
* </iq>
*
* @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 <open /> stanza
* that was used in the SI offer.
*
* The outgoing IQ will be similar to the following:
*
* <iq from='deckardcain@sanctuary.org/tristram'
* id='jn3h8g65'
* to='tyrael@sanctuary.org/talrashastomb'
* type='set'>
* <open xmlns='http://jabber.org/protocol/ibb'
* block-size='4096'
* sid='i781hf64'
* stanza='iq'/>
* </iq>
*
* @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)
*
* <iq from='deckardcain@sanctuary.org/tristram'
* id='kr91n475'
* to='tyrael@sanctuary.org/talrashastomb'
* type='set'>
* <data xmlns='http://jabber.org/protocol/ibb' seq='0' sid='i781hf64'>
* qANQR1DBwU4DX7jmYZnncmUQB/9KuKBddzQH+tZ1ZywKK0yHKnq57kWq+RFtQdCJ
* WpdWpR0uQsuJe7+vh3NWn59/gTc5MDlX8dS9p0ovStmNcyLhxVgmqS8ZKhsblVeu
* IpQ0JgavABqibJolc3BKrVtVV1igKiX/N7Pi8RtY1K18toaMDhdEfhBRzO/XB0+P
* AQhYlRjNacGcslkhXqNjK5Va4tuOAPy2n1Q8UUrHbUd0g+xJ9Bm0G0LZXyvCWyKH
* kuNEHFQiLuCY6Iv0myq6iX6tjuHehZlFSh80b5BVV9tNLwNR5Eqz1klxMhoghJOA
* </data>
* </iq>
*
* @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)
*
* <iq from='tyrael@sanctuary.org/talrashastomb'
* id='kr91n475'
* to='deckardcain@sanctuary.org/tristram'
* type='result'/>
*
* @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.
//
// <iq from='tyrael@sanctuary.org/talrashastomb'
// id='us71g45j'
// to='deckardcain@sanctuary.org/tristram'
// type='result'/>
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 <item-not-found />
// 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.
*
* <iq type="result"
* id="purplea2da8fc9"
* from="tyrael@sanctuary.org/talrashastomb"
* to="deckardcain@sanctuary.org/tristram">
* <query xmlns="http://jabber.org/protocol/disco#info">
* <identity category="client" type="phone"/>
* <feature var="http://jabber.org/protocol/si"/>
* <feature var="http://jabber.org/protocol/si/profile/file-transfer"/>
* <feature var="http://jabber.org/protocol/bytestreams"/>
* </query>
* </iq>
*
*/
- (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:
*
* <iq type="result"
* id="purplea2da8fca"
* from="tyrael@sanctuary.org/talrashastomb"
* to="deckardcain@sanctuary.org/tristram">
* <si xmlns="http://jabber.org/protocol/si">
* <feature xmlns="http://jabber.org/protocol/feature-neg">
* <x xmlns="jabber:x:data" type="submit">
* <field var="stream-method">
* <value>http://jabber.org/protocol/bytestreams</value>
* </field>
* </x>
* </feature>
* </si>
* </iq>
*/
- (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 <streamhost-used/>
//
// We're expecting something like:
//
// <iq xmlns="jabber:client"
// from="tyrael@sanctuary.org/talrashastomb"
// to="deckardcain@sanctuary.org/tristram"
// type="result"
// id="A07A356F-DF15-49BC-92D8-EB3C0357A190">
// <query xmlns="http://jabber.org/protocol/bytestreams">
// <streamhost-used jid="deckardcain@sanctuary.org/tristram"/>
// </query>
// </iq>
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 <activate/> 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 <streamhost-used/> 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 <activate/> 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