Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
tree: f533eb8d63
Fetching contributors…

Cannot retrieve contributors at this time

707 lines (535 sloc) 18.823 kb
//
// XMPPSRVResolver.m
//
// Originally created by Eric Chamberlain on 6/15/10.
// Based on SRVResolver by Apple, Inc.
//
#import "XMPPSRVResolver.h"
#import "XMPPLogging.h"
#include <dns_util.h>
#include <stdlib.h>
#if ! __has_feature(objc_arc)
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
#endif
/**
* Does ARC support support GCD objects?
* It does if the minimum deployment target is iOS 6+ or Mac OS X 10.8+
**/
#if TARGET_OS_IPHONE
// Compiling for iOS
#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 60000 // iOS 6.0 or later
#define NEEDS_DISPATCH_RETAIN_RELEASE 0
#else // iOS 5.X or earlier
#define NEEDS_DISPATCH_RETAIN_RELEASE 1
#endif
#else
// Compiling for Mac OS X
#if MAC_OS_X_VERSION_MIN_REQUIRED >= 1080 // Mac OS X 10.8 or later
#define NEEDS_DISPATCH_RETAIN_RELEASE 0
#else
#define NEEDS_DISPATCH_RETAIN_RELEASE 1 // Mac OS X 10.7 or earlier
#endif
#endif
NSString *const XMPPSRVResolverErrorDomain = @"XMPPSRVResolverErrorDomain";
// 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
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@interface XMPPSRVRecord ()
@property(nonatomic, assign) NSUInteger srvResultsIndex;
@property(nonatomic, assign) NSUInteger sum;
- (NSComparisonResult)compareByPriority:(XMPPSRVRecord *)aRecord;
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@implementation XMPPSRVResolver
- (id)initWithdDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq resolverQueue:(dispatch_queue_t)rq
{
NSParameterAssert(aDelegate != nil);
NSParameterAssert(dq != NULL);
if ((self = [super init]))
{
XMPPLogTrace();
delegate = aDelegate;
delegateQueue = dq;
#if NEEDS_DISPATCH_RETAIN_RELEASE
dispatch_retain(delegateQueue);
#endif
if (rq)
{
resolverQueue = rq;
#if NEEDS_DISPATCH_RETAIN_RELEASE
dispatch_retain(resolverQueue);
#endif
}
else
{
resolverQueue = dispatch_queue_create("XMPPSRVResolver", NULL);
}
results = [[NSMutableArray alloc] initWithCapacity:2];
}
return self;
}
- (void)dealloc
{
XMPPLogTrace();
[self stop];
#if NEEDS_DISPATCH_RETAIN_RELEASE
if (resolverQueue)
dispatch_release(resolverQueue);
#endif
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Properties
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@dynamic srvName;
@dynamic timeout;
- (NSString *)srvName
{
__block NSString *result = nil;
dispatch_block_t block = ^{
result = [srvName copy];
};
if (dispatch_get_current_queue() == resolverQueue)
block();
else
dispatch_sync(resolverQueue, block);
return result;
}
- (NSTimeInterval)timeout
{
__block NSTimeInterval result = 0.0;
dispatch_block_t block = ^{
result = timeout;
};
if (dispatch_get_current_queue() == resolverQueue)
block();
else
dispatch_sync(resolverQueue, block);
return result;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Private Methods
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)sortResults
{
NSAssert(dispatch_get_current_queue() == resolverQueue, @"Invoked on incorrect queue");
XMPPLogTrace();
// Sort results
NSMutableArray *sortedResults = [NSMutableArray arrayWithCapacity:[results count]];
// Sort the list by priority (lowest number first)
[results sortUsingSelector:@selector(compareByPriority:)];
/* From RFC 2782
*
* For each distinct priority level
* While there are still elements left at this priority level
*
* Select an element as specified above, in the
* description of Weight in "The format of the SRV
* RR" Section, and move it to the tail of the new
* list.
*
* The following algorithm SHOULD be used to order
* the SRV RRs of the same priority:
*/
NSUInteger srvResultsCount;
while ([results count] > 0)
{
srvResultsCount = [results count];
if (srvResultsCount == 1)
{
XMPPSRVRecord *srvRecord = [results objectAtIndex:0];
[sortedResults addObject:srvRecord];
[results removeObjectAtIndex:0];
}
else // (srvResultsCount > 1)
{
// more than two records so we need to sort
/* To select a target to be contacted next, arrange all SRV RRs
* (that have not been ordered yet) in any order, except that all
* those with weight 0 are placed at the beginning of the list.
*
* Compute the sum of the weights of those RRs, and with each RR
* associate the running sum in the selected order.
*/
NSUInteger runningSum = 0;
NSMutableArray *samePriorityRecords = [NSMutableArray arrayWithCapacity:srvResultsCount];
XMPPSRVRecord *srvRecord = [results objectAtIndex:0];
NSUInteger initialPriority = srvRecord.priority;
NSUInteger index = 0;
do
{
if (srvRecord.weight == 0)
{
// add to front of array
[samePriorityRecords insertObject:srvRecord atIndex:0];
srvRecord.srvResultsIndex = index;
srvRecord.sum = 0;
}
else
{
// add to end of array and update the running sum
[samePriorityRecords addObject:srvRecord];
runningSum += srvRecord.weight;
srvRecord.srvResultsIndex = index;
srvRecord.sum = runningSum;
}
if (++index < srvResultsCount)
{
srvRecord = [results objectAtIndex:index];
}
else
{
srvRecord = nil;
}
} while(srvRecord && (srvRecord.priority == initialPriority));
/* Then choose a uniform random number between 0 and the sum computed
* (inclusive), and select the RR whose running sum value is the
* first in the selected order which is greater than or equal to
* the random number selected.
*/
NSUInteger randomIndex = arc4random() % (runningSum + 1);
for (srvRecord in samePriorityRecords)
{
if (srvRecord.sum >= randomIndex)
{
/* The target host specified in the
* selected SRV RR is the next one to be contacted by the client.
* Remove this SRV RR from the set of the unordered SRV RRs and
* apply the described algorithm to the unordered SRV RRs to select
* the next target host. Continue the ordering process until there
* are no unordered SRV RRs. This process is repeated for each
* Priority.
*/
[sortedResults addObject:srvRecord];
[results removeObjectAtIndex:srvRecord.srvResultsIndex];
break;
}
}
}
}
results = sortedResults;
XMPPLogVerbose(@"%@: Sorted results:\n%@", THIS_FILE, results);
}
- (void)succeed
{
NSAssert(dispatch_get_current_queue() == resolverQueue, @"Invoked on incorrect queue");
XMPPLogTrace();
[self sortResults];
id theDelegate = delegate;
NSArray *records = [results copy];
dispatch_async(delegateQueue, ^{ @autoreleasepool {
SEL selector = @selector(srvResolver:didResolveRecords:);
if ([theDelegate respondsToSelector:selector])
{
[theDelegate srvResolver:self didResolveRecords:records];
}
else
{
XMPPLogWarn(@"%@: delegate doesn't implement %@", THIS_FILE, NSStringFromSelector(selector));
}
}});
[self stop];
}
- (void)failWithError:(NSError *)error
{
NSAssert(dispatch_get_current_queue() == resolverQueue, @"Invoked on incorrect queue");
XMPPLogTrace2(@"%@: %@ %@", THIS_FILE, THIS_METHOD, error);
id theDelegate = delegate;
if (delegateQueue != NULL)
{
dispatch_async(delegateQueue, ^{ @autoreleasepool {
SEL selector = @selector(srvResolver:didNotResolveDueToError:);
if ([theDelegate respondsToSelector:selector])
{
[theDelegate srvResolver:self didNotResolveDueToError:error];
}
else
{
XMPPLogWarn(@"%@: delegate doesn't implement %@", THIS_FILE, NSStringFromSelector(selector));
}
}});
}
[self stop];
}
- (void)failWithDNSError:(DNSServiceErrorType)sdErr
{
XMPPLogTrace2(@"%@: %@ %i", THIS_FILE, THIS_METHOD, (int)sdErr);
[self failWithError:[NSError errorWithDomain:XMPPSRVResolverErrorDomain code:sdErr userInfo:nil]];
}
- (XMPPSRVRecord *)processRecord:(const void *)rdata length:(uint16_t)rdlen
{
XMPPLogTrace();
// Note: This method is almost entirely from Apple's sample code.
//
// Otherwise there would be a lot more comments and explanation...
if (rdata == NULL)
{
XMPPLogWarn(@"%@: %@ - rdata == NULL", THIS_FILE, THIS_METHOD);
return nil;
}
// Rather than write a whole bunch of icky parsing code, I just synthesise
// a resource record and use <dns_util.h>.
XMPPSRVRecord *result = nil;
NSMutableData * rrData;
dns_resource_record_t * rr;
uint8_t u8; // 1 byte
uint16_t u16; // 2 bytes
uint32_t u32; // 4 bytes
rrData = [NSMutableData dataWithCapacity:(1 + 2 + 2 + 4 + 2 + rdlen)];
u8 = 0;
[rrData appendBytes:&u8 length:sizeof(u8)];
u16 = htons(kDNSServiceType_SRV);
[rrData appendBytes:&u16 length:sizeof(u16)];
u16 = htons(kDNSServiceClass_IN);
[rrData appendBytes:&u16 length:sizeof(u16)];
u32 = htonl(666);
[rrData appendBytes:&u32 length:sizeof(u32)];
u16 = htons(rdlen);
[rrData appendBytes:&u16 length:sizeof(u16)];
[rrData appendBytes:rdata length:rdlen];
// Parse the record.
rr = dns_parse_resource_record([rrData bytes], (uint32_t) [rrData length]);
if (rr != NULL)
{
NSString *target;
target = [NSString stringWithCString:rr->data.SRV->target encoding:NSASCIIStringEncoding];
if (target != nil)
{
UInt16 priority = rr->data.SRV->priority;
UInt16 weight = rr->data.SRV->weight;
UInt16 port = rr->data.SRV->port;
result = [XMPPSRVRecord recordWithPriority:priority weight:weight port:port target:target];
}
dns_free_resource_record(rr);
}
return result;
}
static void QueryRecordCallback(DNSServiceRef sdRef,
DNSServiceFlags flags,
uint32_t interfaceIndex,
DNSServiceErrorType errorCode,
const char * fullname,
uint16_t rrtype,
uint16_t rrclass,
uint16_t rdlen,
const void * rdata,
uint32_t ttl,
void * context)
{
// Called when we get a response to our query.
// It does some preliminary work, but the bulk of the interesting stuff
// is done in the processRecord:length: method.
XMPPSRVResolver *resolver = (__bridge XMPPSRVResolver *)context;
NSCAssert(dispatch_get_current_queue() == resolver->resolverQueue, @"Invoked on incorrect queue");
XMPPLogCTrace();
if (!(flags & kDNSServiceFlagsAdd))
{
// If the kDNSServiceFlagsAdd flag is not set, the domain information is not valid.
return;
}
if (errorCode == kDNSServiceErr_NoError &&
rrtype == kDNSServiceType_SRV)
{
XMPPSRVRecord *record = [resolver processRecord:rdata length:rdlen];
if (record)
{
[resolver->results addObject:record];
}
if ( ! (flags & kDNSServiceFlagsMoreComing) )
{
[resolver succeed];
}
}
else
{
[resolver failWithDNSError:errorCode];
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Public Methods
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)startWithSRVName:(NSString *)aSRVName timeout:(NSTimeInterval)aTimeout
{
dispatch_block_t block = ^{ @autoreleasepool {
if (resolveInProgress)
{
return;
}
XMPPLogTrace2(@"%@: startWithSRVName:%@ timeout:%f", THIS_FILE, aSRVName, aTimeout);
// Save parameters
srvName = [aSRVName copy];
timeout = aTimeout;
// Check parameters
const char *srvNameCStr = [srvName cStringUsingEncoding:NSASCIIStringEncoding];
if (srvNameCStr == NULL)
{
[self failWithDNSError:kDNSServiceErr_BadParam];
return;
}
// Create DNS Service
DNSServiceErrorType sdErr;
sdErr = DNSServiceQueryRecord(&sdRef, // Pointer to unitialized DNSServiceRef
kDNSServiceFlagsReturnIntermediates, // Flags
kDNSServiceInterfaceIndexAny, // Interface index
srvNameCStr, // Full domain name
kDNSServiceType_SRV, // rrtype
kDNSServiceClass_IN, // rrclass
QueryRecordCallback, // Callback method
(__bridge void *)self); // Context pointer
if (sdErr != kDNSServiceErr_NoError)
{
[self failWithDNSError:sdErr];
return;
}
// Extract unix socket (so we can poll for events)
sdFd = DNSServiceRefSockFD(sdRef);
if (sdFd < 0)
{
// Todo...
}
// Create GCD read source for sd file descriptor
sdReadSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, sdFd, 0, resolverQueue);
dispatch_source_set_event_handler(sdReadSource, ^{ @autoreleasepool {
XMPPLogVerbose(@"%@: sdReadSource_eventHandler", THIS_FILE);
// There is data to be read on the socket (or an error occurred).
//
// Invoking DNSServiceProcessResult will invoke our QueryRecordCallback,
// the callback we set when we created the sdRef.
DNSServiceErrorType dnsErr = DNSServiceProcessResult(sdRef);
if (dnsErr != kDNSServiceErr_NoError)
{
[self failWithDNSError:dnsErr];
}
}});
#if NEEDS_DISPATCH_RETAIN_RELEASE
dispatch_source_t theSdReadSource = sdReadSource;
#endif
DNSServiceRef theSdRef = sdRef;
dispatch_source_set_cancel_handler(sdReadSource, ^{ @autoreleasepool {
XMPPLogVerbose(@"%@: sdReadSource_cancelHandler", THIS_FILE);
#if NEEDS_DISPATCH_RETAIN_RELEASE
dispatch_release(theSdReadSource);
#endif
DNSServiceRefDeallocate(theSdRef);
}});
dispatch_resume(sdReadSource);
// Create timer (if requested timeout > 0)
if (timeout > 0.0)
{
timeoutTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, resolverQueue);
dispatch_source_set_event_handler(timeoutTimer, ^{ @autoreleasepool {
NSString *errMsg = @"Operation timed out";
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
NSError *err = [NSError errorWithDomain:XMPPSRVResolverErrorDomain code:0 userInfo:userInfo];
[self failWithError:err];
}});
dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (timeout * NSEC_PER_SEC));
dispatch_source_set_timer(timeoutTimer, tt, DISPATCH_TIME_FOREVER, 0);
dispatch_resume(timeoutTimer);
}
resolveInProgress = YES;
}};
if (dispatch_get_current_queue() == resolverQueue)
block();
else
dispatch_async(resolverQueue, block);
}
- (void)stop
{
dispatch_block_t block = ^{ @autoreleasepool {
XMPPLogTrace();
delegate = nil;
if (delegateQueue)
{
#if NEEDS_DISPATCH_RETAIN_RELEASE
dispatch_release(delegateQueue);
#endif
delegateQueue = NULL;
}
[results removeAllObjects];
if (sdReadSource)
{
// Cancel the readSource.
// It will be released from within the cancel handler.
dispatch_source_cancel(sdReadSource);
sdReadSource = NULL;
sdFd = -1;
// The sdRef will be deallocated from within the cancel handler too.
sdRef = NULL;
}
if (timeoutTimer)
{
dispatch_source_cancel(timeoutTimer);
#if NEEDS_DISPATCH_RETAIN_RELEASE
dispatch_release(timeoutTimer);
#endif
timeoutTimer = NULL;
}
resolveInProgress = NO;
}};
if (dispatch_get_current_queue() == resolverQueue)
block();
else
dispatch_sync(resolverQueue, block);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Utility Methods
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ (NSString *)srvNameFromXMPPDomain:(NSString *)xmppDomain
{
if (xmppDomain == nil)
return nil;
else
return [NSString stringWithFormat:@"_xmpp-client._tcp.%@", xmppDomain];
}
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@implementation XMPPSRVRecord
@synthesize priority;
@synthesize weight;
@synthesize port;
@synthesize target;
@synthesize sum;
@synthesize srvResultsIndex;
+ (XMPPSRVRecord *)recordWithPriority:(UInt16)p1 weight:(UInt16)w port:(UInt16)p2 target:(NSString *)t
{
return [[XMPPSRVRecord alloc] initWithPriority:p1 weight:w port:p2 target:t];
}
- (id)initWithPriority:(UInt16)p1 weight:(UInt16)w port:(UInt16)p2 target:(NSString *)t
{
if ((self = [super init]))
{
priority = p1;
weight = w;
port = p2;
target = [t copy];
sum = 0;
srvResultsIndex = 0;
}
return self;
}
- (NSString *)description
{
return [NSString stringWithFormat:@"<%@:%p target(%@) port(%hu) priority(%hu) weight(%hu)>",
NSStringFromClass([self class]), self, target, port, priority, weight];
}
- (NSComparisonResult)compareByPriority:(XMPPSRVRecord *)aRecord
{
UInt16 mPriority = self.priority;
UInt16 aPriority = aRecord.priority;
if (mPriority < aPriority)
return NSOrderedAscending;
if (mPriority > aPriority)
return NSOrderedDescending;
return NSOrderedSame;
}
@end
Jump to Line
Something went wrong with that request. Please try again.