2016-02-24 16:56:39 +01:00

1536 lines
42 KiB
Objective-C

#import "TURNSocket.h"
#import "XMPP.h"
#import "XMPPLogging.h"
#import "GCDAsyncSocket.h"
#import "NSData+XMPP.h"
#import "NSNumber+XMPP.h"
#if ! __has_feature(objc_arc)
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
#endif
// Log levels: off, error, warn, info, verbose
#if DEBUG
static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; // | XMPP_LOG_FLAG_TRACE;
#else
static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN;
#endif
// Define various states
#define STATE_INIT 0
#define STATE_PROXY_DISCO_ITEMS 10
#define STATE_PROXY_DISCO_INFO 11
#define STATE_PROXY_DISCO_ADDR 12
#define STATE_REQUEST_SENT 13
#define STATE_INITIATOR_CONNECT 14
#define STATE_ACTIVATE_SENT 15
#define STATE_TARGET_CONNECT 20
#define STATE_DONE 30
#define STATE_FAILURE 31
// Define various socket tags
#define SOCKS_OPEN 101
#define SOCKS_CONNECT 102
#define SOCKS_CONNECT_REPLY_1 103
#define SOCKS_CONNECT_REPLY_2 104
// Define various timeouts (in seconds)
#define TIMEOUT_DISCO_ITEMS 8.00
#define TIMEOUT_DISCO_INFO 8.00
#define TIMEOUT_DISCO_ADDR 5.00
#define TIMEOUT_CONNECT 8.00
#define TIMEOUT_READ 5.00
#define TIMEOUT_TOTAL 80.00
// Declare private methods
@interface TURNSocket (PrivateAPI)
- (void)processDiscoItemsResponse:(XMPPIQ *)iq;
- (void)processDiscoInfoResponse:(XMPPIQ *)iq;
- (void)processDiscoAddressResponse:(XMPPIQ *)iq;
- (void)processRequestResponse:(XMPPIQ *)iq;
- (void)processActivateResponse:(XMPPIQ *)iq;
- (void)performPostInitSetup;
- (void)queryProxyCandidates;
- (void)queryNextProxyCandidate;
- (void)queryCandidateJIDs;
- (void)queryNextCandidateJID;
- (void)queryProxyAddress;
- (void)targetConnect;
- (void)targetNextConnect;
- (void)initiatorConnect;
- (void)setupDiscoTimerForDiscoItems;
- (void)setupDiscoTimerForDiscoInfo;
- (void)setupDiscoTimerForDiscoAddress;
- (void)doDiscoItemsTimeout:(NSString *)uuid;
- (void)doDiscoInfoTimeout:(NSString *)uuid;
- (void)doDiscoAddressTimeout:(NSString *)uuid;
- (void)doTotalTimeout;
- (void)succeed;
- (void)fail;
- (void)cleanup;
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@implementation TURNSocket
static NSMutableDictionary *existingTurnSockets;
static NSMutableArray *proxyCandidates;
/**
* Called automatically (courtesy of Cocoa) before the first method of this class is called.
* It may also be called directly, hence the safety mechanism.
**/
+ (void)initialize
{
static BOOL initialized = NO;
if (!initialized)
{
initialized = YES;
existingTurnSockets = [[NSMutableDictionary alloc] init];
proxyCandidates = [@[@"jabber.org"] mutableCopy];
}
}
/**
* Returns whether or not the given IQ is a new start TURN request.
* That is, the IQ must have a query with the proper namespace,
* and it must not correspond to an existing TURNSocket.
**/
+ (BOOL)isNewStartTURNRequest:(XMPPIQ *)iq
{
XMPPLogTrace();
// An incoming turn request looks like this:
//
// <iq type="set" from="[jid full]" id="uuid">
// <query xmlns="http://jabber.org/protocol/bytestreams" sid="uuid" mode="tcp">
// <streamhosts>
// <streamhost jid="proxy1.domain.tld" host="100.200.30.41" port"6969"/>
// <streamhost jid="proxy2.domain.tld" host="100.200.30.42" port"6969"/>
// </streamhosts>
// </query>
// </iq>
//
// From XEP 65 (9.1):
// The 'mode' attribute specifies the mode to use, either "tcp" or "udp".
// If this attribute is not included, the default value of "tcp" MUST be assumed.
// This attribute is OPTIONAL.
NSXMLElement *query = [iq elementForName:@"query" xmlns:@"http://jabber.org/protocol/bytestreams"];
if (query == nil) {
return NO;
}
NSString *queryMode = [[query attributeForName:@"mode"] stringValue];
BOOL isTcpBytestreamQuery = YES;
if (queryMode)
{
isTcpBytestreamQuery = [queryMode caseInsensitiveCompare:@"tcp"] == NSOrderedSame;
}
if (isTcpBytestreamQuery)
{
NSString *uuid = [iq elementID];
@synchronized(existingTurnSockets)
{
if (existingTurnSockets[uuid])
return NO;
else
return YES;
}
}
return NO;
}
/**
* Returns a list of proxy candidates.
*
* You may want to configure this to include NSUserDefaults stuff, or implement your own static/dynamic list.
**/
+ (NSArray *)proxyCandidates
{
NSArray *result = nil;
@synchronized(proxyCandidates)
{
XMPPLogTrace();
result = [proxyCandidates copy];
}
return result;
}
+ (void)setProxyCandidates:(NSArray *)candidates
{
@synchronized(proxyCandidates)
{
XMPPLogTrace();
[proxyCandidates removeAllObjects];
[proxyCandidates addObjectsFromArray:candidates];
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Init, Dealloc
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Initializes a new TURN socket to create a TCP connection by routing through a proxy.
* This constructor configures the object to be the client connecting to a server.
**/
- (id)initWithStream:(XMPPStream *)stream toJID:(XMPPJID *)aJid
{
if ((self = [super init]))
{
XMPPLogTrace();
// Store references
xmppStream = stream;
jid = aJid;
// Create a uuid to be used as the id for all messages in the stun communication.
// This helps differentiate various turn messages between various turn sockets.
// Relying only on JID's is troublesome, because client A could be initiating a connection to server B,
// while at the same time client B could be initiating a connection to server A.
// So an incoming connection from JID clientB@deusty.com/home would be for which turn socket?
uuid = [xmppStream generateUUID];
// Setup initial state for a client connection
state = STATE_INIT;
isClient = YES;
// Get list of proxy candidates
// Each host in this list will be queried to see if it can be used as a proxy
proxyCandidates = [[self class] proxyCandidates];
// Configure everything else
[self performPostInitSetup];
}
return self;
}
/**
* Initializes a new TURN socket to create a TCP connection by routing through a proxy.
* This constructor configures the object to be the server accepting a connection from a client.
**/
- (id)initWithStream:(XMPPStream *)stream incomingTURNRequest:(XMPPIQ *)iq
{
if ((self = [super init]))
{
XMPPLogTrace();
// Store references
xmppStream = stream;
jid = [iq from];
// Store a copy of the ID (which will be our uuid)
uuid = [[iq elementID] copy];
// Setup initial state for a server connection
state = STATE_INIT;
isClient = NO;
// Extract streamhost information from turn request
NSXMLElement *query = [iq elementForName:@"query" xmlns:@"http://jabber.org/protocol/bytestreams"];
streamhosts = [[query elementsForName:@"streamhost"] mutableCopy];
// Configure everything else
[self performPostInitSetup];
}
return self;
}
/**
* Common initialization tasks shared by all init methods.
**/
- (void)performPostInitSetup
{
// Create dispatch queue.
turnQueue = dispatch_queue_create("TURNSocket", NULL);
turnQueueTag = &turnQueueTag;
dispatch_queue_set_specific(turnQueue, turnQueueTag, turnQueueTag, NULL);
// We want to add this new turn socket to the list of existing sockets.
// This gives us a central repository of turn socket objects that we can easily query.
@synchronized(existingTurnSockets)
{
existingTurnSockets[uuid] = self;
}
}
/**
* Standard deconstructor.
* Release any objects we may have retained.
* These objects should all be defined in the header.
**/
- (void)dealloc
{
XMPPLogTrace();
if ((state > STATE_INIT) && (state < STATE_DONE))
{
XMPPLogWarn(@"%@: Deallocating prior to completion or cancellation. "
@"You should explicitly cancel before releasing.", THIS_FILE);
}
if (turnTimer)
dispatch_source_cancel(turnTimer);
if (discoTimer)
dispatch_source_cancel(discoTimer);
#if !OS_OBJECT_USE_OBJC
if (turnQueue)
dispatch_release(turnQueue);
if (delegateQueue)
dispatch_release(delegateQueue);
if (turnTimer)
dispatch_release(turnTimer);
if (discoTimer)
dispatch_release(discoTimer);
#endif
if ([asyncSocket delegate] == self)
{
[asyncSocket setDelegate:nil delegateQueue:NULL];
[asyncSocket disconnect];
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Correspondence Methods
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Starts the TURNSocket with the given delegate.
* If the TURNSocket has already been started, this method does nothing, and the existing delegate is not changed.
**/
- (void)startWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)aDelegateQueue
{
NSParameterAssert(aDelegate != nil);
NSParameterAssert(aDelegateQueue != NULL);
dispatch_async(turnQueue, ^{ @autoreleasepool {
if (state != STATE_INIT)
{
XMPPLogWarn(@"%@: Ignoring start request. Turn procedure already started.", THIS_FILE);
return;
}
// Set reference to delegate and delegate's queue.
// Note that we do NOT retain the delegate.
delegate = aDelegate;
delegateQueue = aDelegateQueue;
#if !OS_OBJECT_USE_OBJC
dispatch_retain(delegateQueue);
#endif
// Add self as xmpp delegate so we'll get message responses
[xmppStream addDelegate:self delegateQueue:turnQueue];
// Start the timer to calculate how long the procedure takes
startTime = [[NSDate alloc] init];
// Schedule timer to cancel the turn procedure.
// This ensures that, in the event of network error or crash,
// the TURNSocket object won't remain in memory forever, and will eventually fail.
turnTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, turnQueue);
dispatch_source_set_event_handler(turnTimer, ^{ @autoreleasepool {
[self doTotalTimeout];
}});
dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (TIMEOUT_TOTAL * NSEC_PER_SEC));
dispatch_source_set_timer(turnTimer, tt, DISPATCH_TIME_FOREVER, 0.1);
dispatch_resume(turnTimer);
// Start the TURN procedure
if (isClient)
[self queryProxyCandidates];
else
[self targetConnect];
}});
}
/**
* Returns the type of connection
* YES for a client connection to a server, NO for a server connection from a client.
**/
- (BOOL)isClient
{
// Note: The isClient variable is readonly (set in the init method).
return isClient;
}
/**
* Aborts the TURN connection attempt.
* The status will be changed to failure, and no delegate messages will be posted.
**/
- (void)abort
{
dispatch_block_t block = ^{ @autoreleasepool {
if ((state > STATE_INIT) && (state < STATE_DONE))
{
// The only thing we really have to do here is move the state to failure.
// This simple act should prevent any further action from being taken in this TUNRSocket object,
// since every action is dictated based on the current state.
state = STATE_FAILURE;
// And don't forget to cleanup after ourselves
[self cleanup];
}
}};
if (dispatch_get_specific(turnQueueTag))
block();
else
dispatch_async(turnQueue, block);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Communication
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Sends the request, from initiator to target, to start a connection to one of the streamhosts.
* This method automatically updates the state.
**/
- (void)sendRequest
{
NSAssert(isClient, @"Only the Initiator sends the request");
XMPPLogTrace();
// <iq type="set" to="target" id="123">
// <query xmlns="http://jabber.org/protocol/bytestreams" sid="123" mode="tcp">
// <streamhost jid="proxy.domain1.org" host="100.200.300.401" port="7777"/>
// <streamhost jid="proxy.domain2.org" host="100.200.300.402" port="7777"/>
// </query>
// </iq>
NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"http://jabber.org/protocol/bytestreams"];
[query addAttributeWithName:@"sid" stringValue:uuid];
[query addAttributeWithName:@"mode" stringValue:@"tcp"];
NSUInteger i;
for(i = 0; i < [streamhosts count]; i++)
{
[query addChild:streamhosts[i]];
}
XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:jid elementID:uuid child:query];
[xmppStream sendElement:iq];
// Update state
state = STATE_REQUEST_SENT;
}
/**
* Sends the reply, from target to initiator, notifying the initiator of the streamhost we connected to.
**/
- (void)sendReply
{
NSAssert(!isClient, @"Only the Target sends the reply");
XMPPLogTrace();
// <iq type="result" to="initiator" id="123">
// <query xmlns="http://jabber.org/protocol/bytestreams" sid="123">
// <streamhost-used jid="proxy.domain"/>
// </query>
// </iq>
NSXMLElement *streamhostUsed = [NSXMLElement elementWithName:@"streamhost-used"];
[streamhostUsed addAttributeWithName:@"jid" stringValue:[proxyJID full]];
NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"http://jabber.org/protocol/bytestreams"];
[query addAttributeWithName:@"sid" stringValue:uuid];
[query addChild:streamhostUsed];
XMPPIQ *iq = [XMPPIQ iqWithType:@"result" to:jid elementID:uuid child:query];
[xmppStream sendElement:iq];
}
/**
* Sends the activate message to the proxy after the target and initiator are both connected to the proxy.
* This method automatically updates the state.
**/
- (void)sendActivate
{
NSAssert(isClient, @"Only the Initiator activates the proxy");
XMPPLogTrace();
NSXMLElement *activate = [NSXMLElement elementWithName:@"activate" stringValue:[jid full]];
NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"http://jabber.org/protocol/bytestreams"];
[query addAttributeWithName:@"sid" stringValue:uuid];
[query addChild:activate];
XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:proxyJID elementID:uuid child:query];
[xmppStream sendElement:iq];
// Update state
state = STATE_ACTIVATE_SENT;
}
/**
* Sends the error, from target to initiator, notifying the initiator we were unable to connect to any streamhost.
**/
- (void)sendError
{
NSAssert(!isClient, @"Only the Target sends the error");
XMPPLogTrace();
// <iq type="error" to="initiator" id="123">
// <error code="404" type="cancel">
// <item-not-found xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">
// </error>
// </iq>
NSXMLElement *inf = [NSXMLElement elementWithName:@"item-not-found" xmlns:@"urn:ietf:params:xml:ns:xmpp-stanzas"];
NSXMLElement *error = [NSXMLElement elementWithName:@"error"];
[error addAttributeWithName:@"code" stringValue:@"404"];
[error addAttributeWithName:@"type" stringValue:@"cancel"];
[error addChild:inf];
XMPPIQ *iq = [XMPPIQ iqWithType:@"error" to:jid elementID:uuid child:error];
[xmppStream sendElement:iq];
}
/**
* Invoked by XMPPClient when an IQ is received.
* We can determine if the IQ applies to us by checking its element ID.
**/
- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq
{
// Disco queries (sent to jabber server) use id=discoUUID
// P2P queries (sent to other Mojo app) use id=uuid
if (state <= STATE_PROXY_DISCO_ADDR)
{
if (![discoUUID isEqualToString:[iq elementID]])
{
// Doesn't apply to us, or is a delayed response that we've decided to ignore
return NO;
}
}
else
{
if (![uuid isEqualToString:[iq elementID]])
{
// Doesn't apply to us
return NO;
}
}
XMPPLogTrace2(@"%@: %@ - state(%i)", THIS_FILE, THIS_METHOD, state);
if (state == STATE_PROXY_DISCO_ITEMS)
{
[self processDiscoItemsResponse:iq];
}
else if (state == STATE_PROXY_DISCO_INFO)
{
[self processDiscoInfoResponse:iq];
}
else if (state == STATE_PROXY_DISCO_ADDR)
{
[self processDiscoAddressResponse:iq];
}
else if (state == STATE_REQUEST_SENT)
{
[self processRequestResponse:iq];
}
else if (state == STATE_ACTIVATE_SENT)
{
[self processActivateResponse:iq];
}
return YES;
}
- (void)processDiscoItemsResponse:(XMPPIQ *)iq
{
XMPPLogTrace();
// We queried the current proxy candidate for all known JIDs in it's disco list.
//
// <iq from="domain.org" to="initiator" id="123" type="result">
// <query xmlns="http://jabber.org/protocol/disco#items">
// <item jid="conference.domain.org"/>
// <item jid="proxy.domain.org"/>
// </query>
// </iq>
NSXMLElement *query = [iq elementForName:@"query" xmlns:@"http://jabber.org/protocol/disco#items"];
NSArray *items = [query elementsForName:@"item"];
candidateJIDs = [[NSMutableArray alloc] initWithCapacity:[items count]];
NSUInteger i;
for(i = 0; i < [items count]; i++)
{
NSString *itemJidStr = [[items[i] attributeForName:@"jid"] stringValue];
XMPPJID *itemJid = [XMPPJID jidWithString:itemJidStr];
if(itemJid)
{
[candidateJIDs addObject:itemJid];
}
}
[self queryCandidateJIDs];
}
- (void)processDiscoInfoResponse:(XMPPIQ *)iq
{
XMPPLogTrace();
// We queried a potential proxy server to see if it was indeed a proxy.
//
// <iq from="domain.org" to="initiator" id="123" type="result">
// <query xmlns="http://jabber.org/protocol/disco#info">
// <identity category="proxy" type="bytestreams" name="SOCKS5 Bytestreams Service"/>
// <feature var="http://jabber.org/protocol/bytestreams"/>
// </query>
// </iq>
NSXMLElement *query = [iq elementForName:@"query" xmlns:@"http://jabber.org/protocol/disco#info"];
NSArray *identities = [query elementsForName:@"identity"];
BOOL found = NO;
NSUInteger i;
for(i = 0; i < [identities count] && !found; i++)
{
NSXMLElement *identity = identities[i];
NSString *category = [[identity attributeForName:@"category"] stringValue];
NSString *type = [[identity attributeForName:@"type"] stringValue];
if([category isEqualToString:@"proxy"] && [type isEqualToString:@"bytestreams"])
{
found = YES;
}
}
if(found)
{
// We found a proxy service!
// Now we query the proxy for its public IP and port.
[self queryProxyAddress];
}
else
{
// There are many jabber servers out there that advertise a proxy service via JID proxy.domain.tld.
// However, not all of these servers have an entry for proxy.domain.tld in the DNS servers.
// Thus, when we try to query the proxy JID, we end up getting a 404 error because our
// jabber server was unable to connect to the given JID.
//
// We could ignore the 404 error, and try to connect anyways,
// but this would be useless because we'd be unable to activate the stream later.
XMPPJID *candidateJID = candidateJIDs[candidateJIDIndex];
// So the service was not a useable proxy service, or will not allow us to use its proxy.
//
// Now most servers have serveral services such as proxy, conference, pubsub, etc.
// If we queried a JID that started with "proxy", and it said no,
// chances are that none of the other services are proxies either,
// so we might as well not waste our time querying them.
if([[candidateJID domain] hasPrefix:@"proxy"])
{
// Move on to the next server
[self queryNextProxyCandidate];
}
else
{
// Try the next JID in the list from the server
[self queryNextCandidateJID];
}
}
}
- (void)processDiscoAddressResponse:(XMPPIQ *)iq
{
XMPPLogTrace();
// We queried a proxy for its public IP and port.
//
// <iq from="domain.org" to="initiator" id="123" type="result">
// <query xmlns="http://jabber.org/protocol/bytestreams">
// <streamhost jid="proxy.domain.org" host="100.200.300.400" port="7777"/>
// </query>
// </iq>
NSXMLElement *query = [iq elementForName:@"query" xmlns:@"http://jabber.org/protocol/bytestreams"];
NSXMLElement *streamhost = [query elementForName:@"streamhost"];
NSString *jidStr = [[streamhost attributeForName:@"jid"] stringValue];
XMPPJID *streamhostJID = [XMPPJID jidWithString:jidStr];
NSString *host = [[streamhost attributeForName:@"host"] stringValue];
UInt16 port = [[[streamhost attributeForName:@"port"] stringValue] intValue];
if(streamhostJID != nil || host != nil || port > 0)
{
[streamhost detach];
[streamhosts addObject:streamhost];
}
// Finished with the current proxy candidate - move on to the next
[self queryNextProxyCandidate];
}
- (void)processRequestResponse:(XMPPIQ *)iq
{
XMPPLogTrace();
// Target has replied - hopefully they've been able to connect to one of the streamhosts
NSXMLElement *query = [iq elementForName:@"query" xmlns:@"http://jabber.org/protocol/bytestreams"];
NSXMLElement *streamhostUsed = [query elementForName:@"streamhost-used"];
NSString *streamhostUsedJID = [[streamhostUsed attributeForName:@"jid"] stringValue];
BOOL found = NO;
NSUInteger i;
for(i = 0; i < [streamhosts count] && !found; i++)
{
NSXMLElement *streamhost = streamhosts[i];
NSString *streamhostJID = [[streamhost attributeForName:@"jid"] stringValue];
if([streamhostJID isEqualToString:streamhostUsedJID])
{
NSAssert(proxyJID == nil && proxyHost == nil, @"proxy and proxyHost are expected to be nil");
proxyJID = [XMPPJID jidWithString:streamhostJID];
proxyHost = [[streamhost attributeForName:@"host"] stringValue];
if([proxyHost isEqualToString:@"0.0.0.0"])
{
proxyHost = [proxyJID full];
}
proxyPort = [[[streamhost attributeForName:@"port"] stringValue] intValue];
found = YES;
}
}
if(found)
{
// The target is connected to the proxy
// Now it's our turn to connect
[self initiatorConnect];
}
else
{
// Target was unable to connect to any of the streamhosts we sent it
[self fail];
}
}
- (void)processActivateResponse:(XMPPIQ *)iq
{
XMPPLogTrace();
NSString *type = [[iq attributeForName:@"type"] stringValue];
BOOL activated = NO;
if (type)
{
activated = [type caseInsensitiveCompare:@"result"] == NSOrderedSame;
}
if (activated) {
[self succeed];
}
else {
[self fail];
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Proxy Discovery
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Each query we send during the proxy discovery process has a different element id.
* This allows us to easily use timeouts, so we can recover from offline servers, and overly slow servers.
* In other words, changing the discoUUID allows us to easily ignore delayed responses from a server.
**/
- (void)updateDiscoUUID
{
discoUUID = [xmppStream generateUUID];
}
/**
* Initiates the process of querying each item in the proxyCandidates array to determine if it supports XEP-65.
* In order to do this we have to:
* - ask the server for a list of services, which returns a list of JIDs
* - query each service JID to determine if it's a proxy
* - if it is a proxy, we ask the proxy for it's public IP and port
**/
- (void)queryProxyCandidates
{
XMPPLogTrace();
// Prepare the streamhosts array, which will hold all of our results
streamhosts = [[NSMutableArray alloc] initWithCapacity:[proxyCandidates count]];
// Start querying each candidate in order
proxyCandidateIndex = -1;
[self queryNextProxyCandidate];
}
/**
* Queries the next proxy candidate in the list.
* If we've queried every candidate, then sends the request to the target, or fails if no proxies were found.
**/
- (void)queryNextProxyCandidate
{
XMPPLogTrace();
// Update state
state = STATE_PROXY_DISCO_ITEMS;
// We start off with multiple proxy candidates (servers that have been known to be proxy servers in the past).
// We can stop when we've found at least 2 proxies.
XMPPJID *proxyCandidateJID = nil;
if ([streamhosts count] < 2)
{
while ((proxyCandidateJID == nil) && (++proxyCandidateIndex < [proxyCandidates count]))
{
NSString *proxyCandidate = proxyCandidates[proxyCandidateIndex];
proxyCandidateJID = [XMPPJID jidWithString:proxyCandidate];
if (proxyCandidateJID == nil)
{
XMPPLogWarn(@"%@: Invalid proxy candidate '%@', not a valid JID", THIS_FILE, proxyCandidate);
}
}
}
if (proxyCandidateJID)
{
[self updateDiscoUUID];
NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"http://jabber.org/protocol/disco#items"];
XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:proxyCandidateJID elementID:discoUUID child:query];
[xmppStream sendElement:iq];
[self setupDiscoTimerForDiscoItems];
}
else
{
if ([streamhosts count] > 0)
{
// We've got a list of potential proxy servers to send to the initiator
XMPPLogVerbose(@"%@: Streamhosts: \n%@", THIS_FILE, streamhosts);
[self sendRequest];
}
else
{
// We were unable to find a single proxy server from our list
XMPPLogVerbose(@"%@: No proxies found", THIS_FILE);
[self fail];
}
}
}
/**
* Initiates the process of querying each candidate JID to determine if it represents a proxy service.
* This process will be stopped when a proxy service is found, or after each candidate JID has been queried.
**/
- (void)queryCandidateJIDs
{
XMPPLogTrace();
// Most of the time, the proxy will have a domain name that includes the word "proxy".
// We can speed up the process of discovering the proxy by searching for these domains, and querying them first.
NSUInteger i;
for (i = 0; i < [candidateJIDs count]; i++)
{
XMPPJID *candidateJID = candidateJIDs[i];
NSRange proxyRange = [[candidateJID domain] rangeOfString:@"proxy" options:NSCaseInsensitiveSearch];
if (proxyRange.length > 0)
{
[candidateJIDs removeObjectAtIndex:i];
[candidateJIDs insertObject:candidateJID atIndex:0];
}
}
XMPPLogVerbose(@"%@: CandidateJIDs: \n%@", THIS_FILE, candidateJIDs);
// Start querying each candidate in order (we can stop when we find one)
candidateJIDIndex = -1;
[self queryNextCandidateJID];
}
/**
* Queries the next candidate JID in the list.
* If we've queried every item, we move on to the next proxy candidate.
**/
- (void)queryNextCandidateJID
{
XMPPLogTrace();
// Update state
state = STATE_PROXY_DISCO_INFO;
candidateJIDIndex++;
if (candidateJIDIndex < [candidateJIDs count])
{
[self updateDiscoUUID];
XMPPJID *candidateJID = candidateJIDs[candidateJIDIndex];
NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"http://jabber.org/protocol/disco#info"];
XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:candidateJID elementID:discoUUID child:query];
[xmppStream sendElement:iq];
[self setupDiscoTimerForDiscoInfo];
}
else
{
// Ran out of candidate JIDs for the current proxy candidate.
// Time to move on to the next proxy candidate.
[self queryNextProxyCandidate];
}
}
/**
* Once we've discovered a proxy service, we need to query it to obtain its public IP and port.
**/
- (void)queryProxyAddress
{
XMPPLogTrace();
// Update state
state = STATE_PROXY_DISCO_ADDR;
[self updateDiscoUUID];
XMPPJID *candidateJID = candidateJIDs[candidateJIDIndex];
NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"http://jabber.org/protocol/bytestreams"];
XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:candidateJID elementID:discoUUID child:query];
[xmppStream sendElement:iq];
[self setupDiscoTimerForDiscoAddress];
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Proxy Connection
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)targetConnect
{
XMPPLogTrace();
// Update state
state = STATE_TARGET_CONNECT;
// Start trying to connect to each streamhost in order
streamhostIndex = -1;
[self targetNextConnect];
}
- (void)targetNextConnect
{
XMPPLogTrace();
streamhostIndex++;
if(streamhostIndex < [streamhosts count])
{
NSXMLElement *streamhost = streamhosts[streamhostIndex];
proxyJID = [XMPPJID jidWithString:[[streamhost attributeForName:@"jid"] stringValue]];
proxyHost = [[streamhost attributeForName:@"host"] stringValue];
if([proxyHost isEqualToString:@"0.0.0.0"])
{
proxyHost = [proxyJID full];
}
proxyPort = [[[streamhost attributeForName:@"port"] stringValue] intValue];
if (asyncSocket == nil)
{
asyncSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:turnQueue];
}
else
{
NSAssert([asyncSocket isDisconnected], @"Expecting the socket to be disconnected at this point...");
}
XMPPLogVerbose(@"TURNSocket: targetNextConnect: %@(%@:%hu)", [proxyJID full], proxyHost, proxyPort);
NSError *err = nil;
if (![asyncSocket connectToHost:proxyHost onPort:proxyPort withTimeout:TIMEOUT_CONNECT error:&err])
{
XMPPLogError(@"TURNSocket: targetNextConnect: err: %@", err);
[self targetNextConnect];
}
}
else
{
[self sendError];
[self fail];
}
}
- (void)initiatorConnect
{
NSAssert(asyncSocket == nil, @"Expecting asyncSocket to be nil");
asyncSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:turnQueue];
XMPPLogVerbose(@"TURNSocket: initiatorConnect: %@(%@:%hu)", [proxyJID full], proxyHost, proxyPort);
NSError *err = nil;
if (![asyncSocket connectToHost:proxyHost onPort:proxyPort withTimeout:TIMEOUT_CONNECT error:&err])
{
XMPPLogError(@"TURNSocket: initiatorConnect: err: %@", err);
[self fail];
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark SOCKS
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Sends the SOCKS5 open/handshake/authentication data, and starts reading the response.
* We attempt to gain anonymous access (no authentication).
**/
- (void)socksOpen
{
XMPPLogTrace();
// +-----+-----------+---------+
// NAME | VER | NMETHODS | METHODS |
// +-----+-----------+---------+
// SIZE | 1 | 1 | 1 - 255 |
// +-----+-----------+---------+
//
// Note: Size is in bytes
//
// Version = 5 (for SOCKS5)
// NumMethods = 1
// Method = 0 (No authentication, anonymous access)
void *byteBuffer = malloc(3);
UInt8 ver = 5;
memcpy(byteBuffer+0, &ver, sizeof(ver));
UInt8 nMethods = 1;
memcpy(byteBuffer+1, &nMethods, sizeof(nMethods));
UInt8 method = 0;
memcpy(byteBuffer+2, &method, sizeof(method));
NSData *data = [NSData dataWithBytesNoCopy:byteBuffer length:3 freeWhenDone:YES];
XMPPLogVerbose(@"TURNSocket: SOCKS_OPEN: %@", data);
[asyncSocket writeData:data withTimeout:-1 tag:SOCKS_OPEN];
// +-----+--------+
// NAME | VER | METHOD |
// +-----+--------+
// SIZE | 1 | 1 |
// +-----+--------+
//
// Note: Size is in bytes
//
// Version = 5 (for SOCKS5)
// Method = 0 (No authentication, anonymous access)
[asyncSocket readDataToLength:2 withTimeout:TIMEOUT_READ tag:SOCKS_OPEN];
}
/**
* Sends the SOCKS5 connect data (according to XEP-65), and starts reading the response.
**/
- (void)socksConnect
{
XMPPLogTrace();
XMPPJID *myJID = [xmppStream myJID];
// From XEP-0065:
//
// The [address] MUST be SHA1(SID + Initiator JID + Target JID) and
// the output is hexadecimal encoded (not binary).
XMPPJID *initiatorJID = isClient ? myJID : jid;
XMPPJID *targetJID = isClient ? jid : myJID;
NSString *hashMe = [NSString stringWithFormat:@"%@%@%@", uuid, [initiatorJID full], [targetJID full]];
NSData *hashRaw = [[hashMe dataUsingEncoding:NSUTF8StringEncoding] xmpp_sha1Digest];
NSData *hash = [[hashRaw xmpp_hexStringValue] dataUsingEncoding:NSUTF8StringEncoding];
XMPPLogVerbose(@"TURNSocket: hashMe : %@", hashMe);
XMPPLogVerbose(@"TURNSocket: hashRaw: %@", hashRaw);
XMPPLogVerbose(@"TURNSocket: hash : %@", hash);
// +-----+-----+-----+------+------+------+
// NAME | VER | CMD | RSV | ATYP | ADDR | PORT |
// +-----+-----+-----+------+------+------+
// SIZE | 1 | 1 | 1 | 1 | var | 2 |
// +-----+-----+-----+------+------+------+
//
// Note: Size is in bytes
//
// Version = 5 (for SOCKS5)
// Command = 1 (for Connect)
// Reserved = 0
// Address Type = 3 (1=IPv4, 3=DomainName 4=IPv6)
// Address = P:D (P=LengthOfDomain D=DomainWithoutNullTermination)
// Port = 0
uint byteBufferLength = (uint)(4 + 1 + [hash length] + 2);
void *byteBuffer = malloc(byteBufferLength);
UInt8 ver = 5;
memcpy(byteBuffer+0, &ver, sizeof(ver));
UInt8 cmd = 1;
memcpy(byteBuffer+1, &cmd, sizeof(cmd));
UInt8 rsv = 0;
memcpy(byteBuffer+2, &rsv, sizeof(rsv));
UInt8 atyp = 3;
memcpy(byteBuffer+3, &atyp, sizeof(atyp));
UInt8 hashLength = [hash length];
memcpy(byteBuffer+4, &hashLength, sizeof(hashLength));
memcpy(byteBuffer+5, [hash bytes], [hash length]);
UInt16 port = 0;
memcpy(byteBuffer+5+[hash length], &port, sizeof(port));
NSData *data = [NSData dataWithBytesNoCopy:byteBuffer length:byteBufferLength freeWhenDone:YES];
XMPPLogVerbose(@"TURNSocket: SOCKS_CONNECT: %@", data);
[asyncSocket writeData:data withTimeout:-1 tag:SOCKS_CONNECT];
// +-----+-----+-----+------+------+------+
// NAME | VER | REP | RSV | ATYP | ADDR | PORT |
// +-----+-----+-----+------+------+------+
// SIZE | 1 | 1 | 1 | 1 | var | 2 |
// +-----+-----+-----+------+------+------+
//
// Note: Size is in bytes
//
// Version = 5 (for SOCKS5)
// Reply = 0 (0=Succeeded, X=ErrorCode)
// Reserved = 0
// Address Type = 3 (1=IPv4, 3=DomainName 4=IPv6)
// Address = P:D (P=LengthOfDomain D=DomainWithoutNullTermination)
// Port = 0
//
// It is expected that the SOCKS server will return the same address given in the connect request.
// But according to XEP-65 this is only marked as a SHOULD and not a MUST.
// So just in case, we'll read up to the address length now, and then read in the address+port next.
[asyncSocket readDataToLength:5 withTimeout:TIMEOUT_READ tag:SOCKS_CONNECT_REPLY_1];
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark AsyncSocket Delegate Methods
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port
{
XMPPLogTrace();
// Start the SOCKS protocol stuff
[self socksOpen];
}
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
XMPPLogTrace();
if (tag == SOCKS_OPEN)
{
// See socksOpen method for socks reply format
UInt8 ver = [NSNumber xmpp_extractUInt8FromData:data atOffset:0];
UInt8 mtd = [NSNumber xmpp_extractUInt8FromData:data atOffset:1];
XMPPLogVerbose(@"TURNSocket: SOCKS_OPEN: ver(%o) mtd(%o)", ver, mtd);
if(ver == 5 && mtd == 0)
{
[self socksConnect];
}
else
{
// Some kind of error occurred.
// The proxy probably requires some kind of authentication.
[asyncSocket disconnect];
}
}
else if (tag == SOCKS_CONNECT_REPLY_1)
{
// See socksConnect method for socks reply format
XMPPLogVerbose(@"TURNSocket: SOCKS_CONNECT_REPLY_1: %@", data);
UInt8 ver = [NSNumber xmpp_extractUInt8FromData:data atOffset:0];
UInt8 rep = [NSNumber xmpp_extractUInt8FromData:data atOffset:1];
XMPPLogVerbose(@"TURNSocket: SOCKS_CONNECT_REPLY_1: ver(%o) rep(%o)", ver, rep);
if(ver == 5 && rep == 0)
{
// We read in 5 bytes which we expect to be:
// 0: ver = 5
// 1: rep = 0
// 2: rsv = 0
// 3: atyp = 3
// 4: size = size of addr field
//
// However, some servers don't follow the protocol, and send a atyp value of 0.
UInt8 atyp = [NSNumber xmpp_extractUInt8FromData:data atOffset:3];
if (atyp == 3)
{
UInt8 addrLength = [NSNumber xmpp_extractUInt8FromData:data atOffset:4];
UInt8 portLength = 2;
XMPPLogVerbose(@"TURNSocket: addrLength: %o", addrLength);
XMPPLogVerbose(@"TURNSocket: portLength: %o", portLength);
[asyncSocket readDataToLength:(addrLength+portLength)
withTimeout:TIMEOUT_READ
tag:SOCKS_CONNECT_REPLY_2];
}
else if (atyp == 0)
{
// The size field was actually the first byte of the port field
// We just have to read in that last byte
[asyncSocket readDataToLength:1 withTimeout:TIMEOUT_READ tag:SOCKS_CONNECT_REPLY_2];
}
else
{
XMPPLogError(@"TURNSocket: Unknown atyp field in connect reply");
[asyncSocket disconnect];
}
}
else
{
// Some kind of error occurred.
[asyncSocket disconnect];
}
}
else if (tag == SOCKS_CONNECT_REPLY_2)
{
// See socksConnect method for socks reply format
XMPPLogVerbose(@"TURNSocket: SOCKS_CONNECT_REPLY_2: %@", data);
if (isClient)
{
[self sendActivate];
}
else
{
[self sendReply];
[self succeed];
}
}
}
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
{
XMPPLogTrace2(@"%@: %@ %@", THIS_FILE, THIS_METHOD, err);
if (state == STATE_TARGET_CONNECT)
{
[self targetNextConnect];
}
else if (state == STATE_INITIATOR_CONNECT)
{
[self fail];
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Timeouts
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)setupDiscoTimer:(NSTimeInterval)timeout
{
NSAssert(dispatch_get_specific(turnQueueTag), @"Invoked on incorrect queue");
if (discoTimer == NULL)
{
discoTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, turnQueue);
dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (timeout * NSEC_PER_SEC));
dispatch_source_set_timer(discoTimer, tt, DISPATCH_TIME_FOREVER, 0.1);
dispatch_resume(discoTimer);
}
else
{
dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (timeout * NSEC_PER_SEC));
dispatch_source_set_timer(discoTimer, tt, DISPATCH_TIME_FOREVER, 0.1);
}
}
- (void)setupDiscoTimerForDiscoItems
{
XMPPLogTrace();
[self setupDiscoTimer:TIMEOUT_DISCO_ITEMS];
NSString *theUUID = discoUUID;
dispatch_source_set_event_handler(discoTimer, ^{ @autoreleasepool {
[self doDiscoItemsTimeout:theUUID];
}});
}
- (void)setupDiscoTimerForDiscoInfo
{
XMPPLogTrace();
[self setupDiscoTimer:TIMEOUT_DISCO_INFO];
NSString *theUUID = discoUUID;
dispatch_source_set_event_handler(discoTimer, ^{ @autoreleasepool {
[self doDiscoInfoTimeout:theUUID];
}});
}
- (void)setupDiscoTimerForDiscoAddress
{
XMPPLogTrace();
[self setupDiscoTimer:TIMEOUT_DISCO_ADDR];
NSString *theUUID = discoUUID;
dispatch_source_set_event_handler(discoTimer, ^{ @autoreleasepool {
[self doDiscoAddressTimeout:theUUID];
}});
}
- (void)doDiscoItemsTimeout:(NSString *)theUUID
{
NSAssert(dispatch_get_specific(turnQueueTag), @"Invoked on incorrect queue");
if (state == STATE_PROXY_DISCO_ITEMS)
{
if ([theUUID isEqualToString:discoUUID])
{
XMPPLogTrace();
// Server isn't responding - server may be offline
[self queryNextProxyCandidate];
}
}
}
- (void)doDiscoInfoTimeout:(NSString *)theUUID
{
NSAssert(dispatch_get_specific(turnQueueTag), @"Invoked on incorrect queue");
if (state == STATE_PROXY_DISCO_INFO)
{
if ([theUUID isEqualToString:discoUUID])
{
XMPPLogTrace();
// Move on to the next proxy candidate
[self queryNextProxyCandidate];
}
}
}
- (void)doDiscoAddressTimeout:(NSString *)theUUID
{
NSAssert(dispatch_get_specific(turnQueueTag), @"Invoked on incorrect queue");
if (state == STATE_PROXY_DISCO_ADDR)
{
if ([theUUID isEqualToString:discoUUID])
{
XMPPLogTrace();
// Server is taking a long time to respond to a simple query.
// We could jump to the next candidate JID, but we'll take this as a sign of an overloaded server.
[self queryNextProxyCandidate];
}
}
}
- (void)doTotalTimeout
{
NSAssert(dispatch_get_specific(turnQueueTag), @"Invoked on incorrect queue");
if ((state != STATE_DONE) && (state != STATE_FAILURE))
{
XMPPLogTrace();
// A timeout occured to cancel the entire TURN procedure.
// This probably means the other endpoint crashed, or a network error occurred.
// In either case, we can consider this a failure, and recycle the memory associated with this object.
[self fail];
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Finish and Cleanup
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)succeed
{
NSAssert(dispatch_get_specific(turnQueueTag), @"Invoked on incorrect queue");
XMPPLogTrace();
// Record finish time
finishTime = [[NSDate alloc] init];
// Update state
state = STATE_DONE;
dispatch_async(delegateQueue, ^{ @autoreleasepool {
if ([delegate respondsToSelector:@selector(turnSocket:didSucceed:)])
{
[delegate turnSocket:self didSucceed:asyncSocket];
}
}});
[self cleanup];
}
- (void)fail
{
NSAssert(dispatch_get_specific(turnQueueTag), @"Invoked on incorrect queue");
XMPPLogTrace();
// Record finish time
finishTime = [[NSDate alloc] init];
// Update state
state = STATE_FAILURE;
dispatch_async(delegateQueue, ^{ @autoreleasepool {
if ([delegate respondsToSelector:@selector(turnSocketDidFail:)])
{
[delegate turnSocketDidFail:self];
}
}});
[self cleanup];
}
- (void)cleanup
{
// This method must be run on the turnQueue
NSAssert(dispatch_get_specific(turnQueueTag), @"Invoked on incorrect queue.");
XMPPLogTrace();
if (turnTimer)
{
dispatch_source_cancel(turnTimer);
#if !OS_OBJECT_USE_OBJC
dispatch_release(turnTimer);
#endif
turnTimer = NULL;
}
if (discoTimer)
{
dispatch_source_cancel(discoTimer);
#if !OS_OBJECT_USE_OBJC
dispatch_release(discoTimer);
#endif
discoTimer = NULL;
}
// Remove self as xmpp delegate
[xmppStream removeDelegate:self delegateQueue:turnQueue];
// Remove self from existingStuntSockets dictionary so we can be deallocated
@synchronized(existingTurnSockets)
{
[existingTurnSockets removeObjectForKey:uuid];
}
}
@end