2189 lines
72 KiB
Objective-C
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
|