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

1782 lines
52 KiB
Objective-C

#import "XMPP.h"
#import "XMPPLogging.h"
#import "XMPPCapabilities.h"
#import "NSData+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
// Log flags: trace
#if DEBUG
static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN;
#else
static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN;
#endif
/**
* Defines the timeout for a capabilities request.
*
* There are two reasons to have a timeout:
* - To prevent the discoRequest variables from growing indefinitely if responses are not received.
* - If a request is sent to a jid broadcasting a capabilities hash, and it does not respond within the timeout,
* we can then send a request to a different jid broadcasting the same capabilities hash.
*
* Remember, if multiple jids all broadcast the same capabilities hash,
* we only (initially) send a disco request to the first jid.
* This is an obvious optimization to remove unnecessary traffic and cpu load.
*
* However, if that jid doesn't respond within a sensible time period,
* we should move on to the next jid in the list.
**/
#define CAPABILITIES_REQUEST_TIMEOUT 30.0 // seconds
/**
* Define various xmlns values.
**/
#define XMLNS_DISCO_INFO @"http://jabber.org/protocol/disco#info"
#define XMLNS_CAPS @"http://jabber.org/protocol/caps"
/**
* Application identifier.
* According to the XEP it is RECOMMENDED for the value of the 'node' attribute to be an HTTP URL.
**/
#ifndef DISCO_NODE
#define DISCO_NODE @"https://github.com/robbiehanson/XMPPFramework"
#endif
@interface GCDTimerWrapper : NSObject
{
dispatch_source_t timer;
}
- (id)initWithDispatchTimer:(dispatch_source_t)aTimer;
- (void)cancel;
@end
@interface XMPPCapabilities (PrivateAPI)
- (void)continueCollectMyCapabilities:(NSXMLElement *)query;
- (void)maybeQueryNextJidWithHashKey:(NSString *)key dueToHashMismatch:(BOOL)hashMismatch;
- (void)setupTimeoutForDiscoRequestFromJID:(XMPPJID *)jid;
- (void)setupTimeoutForDiscoRequestFromJID:(XMPPJID *)jid withHashKey:(NSString *)key;
- (void)cancelTimeoutForDiscoRequestFromJID:(XMPPJID *)jid;
- (void)processTimeoutWithHashKey:(NSString *)key;
- (void)processTimeoutWithJID:(XMPPJID *)jid;
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@implementation XMPPCapabilities
@dynamic xmppCapabilitiesStorage;
@dynamic autoFetchHashedCapabilities;
@dynamic autoFetchNonHashedCapabilities;
@dynamic autoFetchMyServerCapabilities;
- (id)init
{
// This will cause a crash - it's designed to.
// Only the init methods listed in XMPPCapabilities.h are supported.
return [self initWithCapabilitiesStorage:nil dispatchQueue:NULL];
}
- (id)initWithDispatchQueue:(dispatch_queue_t)queue
{
// This will cause a crash - it's designed to.
// Only the init methods listed in XMPPCapabilities.h are supported.
return [self initWithCapabilitiesStorage:nil dispatchQueue:queue];
}
- (id)initWithCapabilitiesStorage:(id <XMPPCapabilitiesStorage>)storage
{
return [self initWithCapabilitiesStorage:storage dispatchQueue:NULL];
}
- (id)initWithCapabilitiesStorage:(id <XMPPCapabilitiesStorage>)storage dispatchQueue:(dispatch_queue_t)queue
{
NSParameterAssert(storage != nil);
if ((self = [super initWithDispatchQueue:queue]))
{
if ([storage configureWithParent:self queue:moduleQueue])
{
xmppCapabilitiesStorage = storage;
}
else
{
XMPPLogError(@"%@: %@ - Unable to configure storage!", THIS_FILE, THIS_METHOD);
}
myCapabilitiesNode = DISCO_NODE;
// discoRequestJidSet:
//
// A set which contains every JID for which a current disco request applies.
// Note that one disco request may satisfy multiple jids in this set.
// This is the case if multiple jids broadcast the same capabilities hash.
// When this happens we send a single disco request to one of the jids,
// but every single jid with that hash is included in this set.
// This allows us to quickly and easily see if there is an outstanding disco request for a jid.
//
// discoRequestHashDict:
//
// A dictionary which tells us about disco requests that have been sent concerning hashed capabilities.
// It maps from hash (key=hash+hashAlgorithm) to an array of jids that use this hash.
//
// discoTimerJidDict:
//
// A dictionary that contains all the timers for timing out disco requests.
// It maps from jid to associated timer.
discoRequestJidSet = [[NSMutableSet alloc] init];
discoRequestHashDict = [[NSMutableDictionary alloc] init];
discoTimerJidDict = [[NSMutableDictionary alloc] init];
autoFetchHashedCapabilities = YES;
autoFetchNonHashedCapabilities = NO;
autoFetchMyServerCapabilities = NO;
}
return self;
}
- (void)dealloc
{
for (GCDTimerWrapper *timerWrapper in discoTimerJidDict)
{
[timerWrapper cancel];
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Configuration
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (id <XMPPCapabilitiesStorage>)xmppCapabilitiesStorage
{
return xmppCapabilitiesStorage;
}
- (NSString *)myCapabilitiesNode
{
if (dispatch_get_specific(moduleQueueTag))
{
return myCapabilitiesNode;
}
else
{
__block NSString *result;
dispatch_sync(moduleQueue, ^{
result = myCapabilitiesNode;
});
return result;
}
}
- (void)setMyCapabilitiesNode:(NSString *)flag
{
NSAssert([flag length], @"myCapabilitiesNode MUST NOT be nil");
dispatch_block_t block = ^{
myCapabilitiesNode = flag;
};
if (dispatch_get_specific(moduleQueueTag))
block();
else
dispatch_async(moduleQueue, block);
}
- (BOOL)autoFetchHashedCapabilities
{
__block BOOL result = NO;
dispatch_block_t block = ^{
result = autoFetchHashedCapabilities;
};
if (dispatch_get_specific(moduleQueueTag))
block();
else
dispatch_sync(moduleQueue, block);
return result;
}
- (void)setAutoFetchHashedCapabilities:(BOOL)flag
{
dispatch_block_t block = ^{
autoFetchHashedCapabilities = flag;
};
if (dispatch_get_specific(moduleQueueTag))
block();
else
dispatch_async(moduleQueue, block);
}
- (BOOL)autoFetchNonHashedCapabilities
{
__block BOOL result = NO;
dispatch_block_t block = ^{
result = autoFetchNonHashedCapabilities;
};
if (dispatch_get_specific(moduleQueueTag))
block();
else
dispatch_sync(moduleQueue, block);
return result;
}
- (void)setAutoFetchNonHashedCapabilities:(BOOL)flag
{
dispatch_block_t block = ^{
autoFetchNonHashedCapabilities = flag;
};
if (dispatch_get_specific(moduleQueueTag))
block();
else
dispatch_async(moduleQueue, block);
}
- (BOOL)autoFetchMyServerCapabilities
{
__block BOOL result = NO;
dispatch_block_t block = ^{
result = autoFetchMyServerCapabilities;
};
if (dispatch_get_specific(moduleQueueTag))
block();
else
dispatch_sync(moduleQueue, block);
return result;
}
- (void)setAutoFetchMyServerCapabilities:(BOOL)flag
{
dispatch_block_t block = ^{
autoFetchMyServerCapabilities = flag;
};
if (dispatch_get_specific(moduleQueueTag))
block();
else
dispatch_async(moduleQueue, block);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Hashing
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static NSString* encodeLt(NSString *str)
{
// From the RFC:
//
// If the string "&lt;" appears in any of the hash values,
// then that value MUST NOT convert it to "<" because
// completing such a conversion would open the protocol to trivial attacks.
//
// All of the XML libraries perform this conversion for us automatically (which makes sense).
// Furthermore, it is illegal for an attribute or namespace value to have a raw "<" character (as per XML).
// So the solution is very simple:
// Just convert any '<' characters to the escaped "&lt;" string.
return [str stringByReplacingOccurrencesOfString:@"<" withString:@"&lt;"];
}
static NSInteger sortIdentities(NSXMLElement *identity1, NSXMLElement *identity2, void *context)
{
// Sort the service discovery identities by category and then by type and then by xml:lang (if it exists).
//
// All sort operations MUST be performed using "i;octet" collation as specified in Section 9.3 of RFC 4790.
NSComparisonResult result;
NSString *category1 = [identity1 attributeStringValueForName:@"category" withDefaultValue:@""];
NSString *category2 = [identity2 attributeStringValueForName:@"category" withDefaultValue:@""];
category1 = encodeLt(category1);
category2 = encodeLt(category2);
result = [category1 compare:category2 options:NSLiteralSearch];
if (result != NSOrderedSame)
{
return result;
}
NSString *type1 = [identity1 attributeStringValueForName:@"type" withDefaultValue:@""];
NSString *type2 = [identity2 attributeStringValueForName:@"type" withDefaultValue:@""];
type1 = encodeLt(type1);
type2 = encodeLt(type2);
result = [type1 compare:type2 options:NSLiteralSearch];
if (result != NSOrderedSame)
{
return result;
}
NSString *lang1 = [identity1 attributeStringValueForName:@"xml:lang" withDefaultValue:@""];
NSString *lang2 = [identity2 attributeStringValueForName:@"xml:lang" withDefaultValue:@""];
lang1 = encodeLt(lang1);
lang2 = encodeLt(lang2);
result = [lang1 compare:lang2 options:NSLiteralSearch];
if (result != NSOrderedSame)
{
return result;
}
NSString *name1 = [identity1 attributeStringValueForName:@"name" withDefaultValue:@""];
NSString *name2 = [identity2 attributeStringValueForName:@"name" withDefaultValue:@""];
name1 = encodeLt(name1);
name2 = encodeLt(name2);
return [name1 compare:name2 options:NSLiteralSearch];
}
static NSInteger sortFeatures(NSXMLElement *feature1, NSXMLElement *feature2, void *context)
{
// All sort operations MUST be performed using "i;octet" collation as specified in Section 9.3 of RFC 4790.
NSString *var1 = [feature1 attributeStringValueForName:@"var" withDefaultValue:@""];
NSString *var2 = [feature2 attributeStringValueForName:@"var" withDefaultValue:@""];
var1 = encodeLt(var1);
var2 = encodeLt(var2);
return [var1 compare:var2 options:NSLiteralSearch];
}
static NSString* extractFormTypeValue(NSXMLElement *form)
{
// From the RFC:
//
// If the FORM_TYPE field is not of type "hidden" or the form does not
// include a FORM_TYPE field, ignore the form but continue processing.
//
// If the FORM_TYPE field contains more than one <value/> element with different XML character data,
// consider the entire response to be ill-formed.
// This method will return:
//
// - The form type's value if it exists
// - An empty string if it does not contain a form type field (or the form type is not of type hidden)
// - Nil if the form type is invalid (contains more than one <value/> element which are different)
//
// In other words
//
// - Non-empty string -> proper form
// - Empty string -> ignore form
// - Nil -> Entire response is to be considered ill-formed
//
// The returned value is properly encoded via encodeLt() and contains the trailing '<' character.
NSArray *fields = [form elementsForName:@"field"];
for (NSXMLElement *field in fields)
{
NSString *var = [field attributeStringValueForName:@"var"];
NSString *type = [field attributeStringValueForName:@"type"];
if ([var isEqualToString:@"FORM_TYPE"] && [type isEqualToString:@"hidden"])
{
NSArray *values = [field elementsForName:@"value"];
if ([values count] > 0)
{
if ([values count] > 1)
{
NSString *baseValue = [values[0] stringValue];
NSUInteger i;
for (i = 1; i < [values count]; i++)
{
NSString *value = [values[i] stringValue];
if (![value isEqualToString:baseValue])
{
// Multiple <value/> elements with differing XML character data
return nil;
}
}
}
NSString *result = [[values lastObject] stringValue];
if (result == nil)
{
// This is why the result contains the trailing '<' character.
result = @"";
}
return [NSString stringWithFormat:@"%@<", encodeLt(result)];
}
}
}
return @"";
}
static NSInteger sortForms(NSXMLElement *form1, NSXMLElement *form2, void *context)
{
// Sort the forms by the FORM_TYPE (i.e., by the XML character data of the <value/> element.
//
// All sort operations MUST be performed using "i;octet" collation as specified in Section 9.3 of RFC 4790.
NSString *formTypeValue1 = extractFormTypeValue(form1);
NSString *formTypeValue2 = extractFormTypeValue(form2);
// The formTypeValue variable is guaranteed to be properly encoded.
if (formTypeValue1)
{
if (formTypeValue2)
return [formTypeValue1 compare:formTypeValue2 options:NSLiteralSearch];
else
return NSOrderedAscending;
}
else if (formTypeValue2)
{
return NSOrderedDescending;
}
else
{
return NSOrderedSame;
}
}
static NSInteger sortFormFields(NSXMLElement *field1, NSXMLElement *field2, void *context)
{
// Sort the fields by the "var" attribute.
//
// All sort operations MUST be performed using "i;octet" collation as specified in Section 9.3 of RFC 4790.
NSString *var1 = [field1 attributeStringValueForName:@"var" withDefaultValue:@""];
NSString *var2 = [field2 attributeStringValueForName:@"var" withDefaultValue:@""];
var1 = encodeLt(var1);
var2 = encodeLt(var2);
return [var1 compare:var2 options:NSLiteralSearch];
}
static NSInteger sortFieldValues(NSXMLElement *value1, NSXMLElement *value2, void *context)
{
NSString *str1 = [value1 stringValue];
NSString *str2 = [value2 stringValue];
if (str1 == nil) str1 = @"";
if (str2 == nil) str2 = @"";
str1 = encodeLt(str1);
str2 = encodeLt(str2);
return [str1 compare:str2 options:NSLiteralSearch];
}
+ (NSString *)hashCapabilitiesFromQuery:(NSXMLElement *)query
{
if (query == nil) return nil;
NSMutableSet *set = [NSMutableSet set];
NSMutableString *s = [NSMutableString string];
NSArray *identities = [[query elementsForName:@"identity"] sortedArrayUsingFunction:sortIdentities context:NULL];
for (NSXMLElement *identity in identities)
{
// Format as: category / type / lang / name
NSString *category = [identity attributeStringValueForName:@"category" withDefaultValue:@""];
NSString *type = [identity attributeStringValueForName:@"type" withDefaultValue:@""];
NSString *lang = [identity attributeStringValueForName:@"xml:lang" withDefaultValue:@""];
NSString *name = [identity attributeStringValueForName:@"name" withDefaultValue:@""];
category = encodeLt(category);
type = encodeLt(type);
lang = encodeLt(lang);
name = encodeLt(name);
NSString *mash = [NSString stringWithFormat:@"%@/%@/%@/%@<", category, type, lang, name];
// Section 5.4, rule 3.3:
//
// If the response includes more than one service discovery identity with
// the same category/type/lang/name, consider the entire response to be ill-formed.
if ([set containsObject:mash])
{
return nil;
}
else
{
[set addObject:mash];
}
[s appendString:mash];
}
[set removeAllObjects];
NSArray *features = [[query elementsForName:@"feature"] sortedArrayUsingFunction:sortFeatures context:NULL];
for (NSXMLElement *feature in features)
{
NSString *var = [feature attributeStringValueForName:@"var" withDefaultValue:@""];
var = encodeLt(var);
NSString *mash = [NSString stringWithFormat:@"%@<", var];
// Section 5.4, rule 3.4:
//
// If the response includes more than one service discovery feature with the
// same XML character data, consider the entire response to be ill-formed.
if ([set containsObject:mash])
{
return nil;
}
else
{
[set addObject:mash];
}
[s appendString:mash];
}
[set removeAllObjects];
NSArray *unsortedForms = [query elementsForLocalName:@"x" URI:@"jabber:x:data"];
NSArray *forms = [unsortedForms sortedArrayUsingFunction:sortForms context:NULL];
for (NSXMLElement *form in forms)
{
NSString *formTypeValue = extractFormTypeValue(form);
if (formTypeValue == nil)
{
// Invalid according to section 5.4, rule 3.5
return nil;
}
if ([formTypeValue length] == 0)
{
// Ignore according to section 5.4, rule 3.6
continue;
}
// Note: The formTypeValue is properly encoded and contains the trailing '<' character.
[s appendString:formTypeValue];
NSArray *fields = [[form elementsForName:@"field"] sortedArrayUsingFunction:sortFormFields context:NULL];
for (NSXMLElement *field in fields)
{
// For each field other than FORM_TYPE:
//
// 1. Append the value of the var attribute, followed by the '<' character.
// 2. Sort values by the XML character data of the <value/> element.
// 3. For each <value/> element, append the XML character data, followed by the '<' character.
NSString *var = [field attributeStringValueForName:@"var" withDefaultValue:@""];
var = encodeLt(var);
if ([var isEqualToString:@"FORM_TYPE"])
{
continue;
}
[s appendFormat:@"%@<", var];
NSArray *values = [[field elementsForName:@"value"] sortedArrayUsingFunction:sortFieldValues context:NULL];
for (NSXMLElement *value in values)
{
NSString *str = [value stringValue];
if (str == nil)
{
str = @"";
}
str = encodeLt(str);
[s appendFormat:@"%@<", str];
}
}
}
NSData *data = [s dataUsingEncoding:NSUTF8StringEncoding];
NSData *hash = [data xmpp_sha1Digest];
return [hash xmpp_base64Encoded];
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Key Conversions
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (NSString *)keyFromHash:(NSString *)hash algorithm:(NSString *)hashAlg
{
return [NSString stringWithFormat:@"%@-%@", hash, hashAlg];
}
- (BOOL)getHash:(NSString **)hashPtr algorithm:(NSString **)hashAlgPtr fromKey:(NSString *)key
{
if (key == nil) return NO;
NSRange range = [key rangeOfString:@"-"];
if (range.location == NSNotFound)
{
return NO;
}
if (hashPtr)
{
*hashPtr = [key substringToIndex:range.location];
}
if (hashAlgPtr)
{
*hashAlgPtr = [key substringFromIndex:(range.location + range.length)];
}
return YES;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Logic
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)collectMyCapabilities
{
// This method must be invoked on the moduleQueue
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue");
XMPPLogTrace();
if (collectingMyCapabilities)
{
XMPPLogInfo(@"%@: %@ - Existing collection already in progress", [self class], THIS_METHOD);
return;
}
myCapabilitiesQuery = nil;
myCapabilitiesC = nil;
collectingMyCapabilities = YES;
// Create new query and add standard features
//
// <query xmlns="http://jabber.org/protocol/disco#info">
// <feature var='http://jabber.org/protocol/disco#info'/>
// <feature var="http://jabber.org/protocol/caps"/>
// </query>
NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:XMLNS_DISCO_INFO];
NSXMLElement *feature1 = [NSXMLElement elementWithName:@"feature"];
[feature1 addAttributeWithName:@"var" stringValue:XMLNS_DISCO_INFO];
NSXMLElement *feature2 = [NSXMLElement elementWithName:@"feature"];
[feature2 addAttributeWithName:@"var" stringValue:XMLNS_CAPS];
[query addChild:feature1];
[query addChild:feature2];
// Now prompt the delegates to add any additional features.
SEL collectingMyCapabilitiesSelector = @selector(xmppCapabilities:collectingMyCapabilities:);
SEL myFeaturesForXMPPCapabilitiesSelector = @selector(myFeaturesForXMPPCapabilities:);
if (![multicastDelegate hasDelegateThatRespondsToSelector:collectingMyCapabilitiesSelector]
&& ![multicastDelegate hasDelegateThatRespondsToSelector:myFeaturesForXMPPCapabilitiesSelector])
{
// None of the delegates implement the method.
// Use a shortcut.
[self continueCollectMyCapabilities:query];
}
else
{
// Query all interested delegates.
// This must be done serially to allow them to alter the element in a thread-safe manner.
GCDMulticastDelegateEnumerator *collectingMyCapabilitiesDelegateEnumerator = [multicastDelegate delegateEnumerator];
GCDMulticastDelegateEnumerator *myFeaturesForXMPPCapabilitiesDelegateEnumerator = [multicastDelegate delegateEnumerator];
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(concurrentQueue, ^{ @autoreleasepool {
id del;
dispatch_queue_t dq;
while ([collectingMyCapabilitiesDelegateEnumerator getNextDelegate:&del delegateQueue:&dq forSelector:collectingMyCapabilitiesSelector])
{
dispatch_sync(dq, ^{ @autoreleasepool {
[del xmppCapabilities:self collectingMyCapabilities:query];
}});
}
while ([myFeaturesForXMPPCapabilitiesDelegateEnumerator getNextDelegate:&del delegateQueue:&dq forSelector:myFeaturesForXMPPCapabilitiesSelector])
{
dispatch_sync(dq, ^{ @autoreleasepool {
NSArray *features = [del myFeaturesForXMPPCapabilities:self];
for(NSString *feature in features){
BOOL found = NO;
//Check to see if the feature is already in my capabilities
for (NSXMLElement *childElement in query.children) {
if([[childElement attributeStringValueForName:@"var"] isEqualToString:feature])
{
found = YES;
break;
}
}
//The feature is not already in our capabilities so add it
if(!found)
{
NSXMLElement *featureElement = [NSXMLElement elementWithName:@"feature"];
[featureElement addAttributeWithName:@"var" stringValue:feature];
[query addChild:featureElement];
}
}
}});
}
dispatch_async(moduleQueue, ^{ @autoreleasepool {
[self continueCollectMyCapabilities:query];
}});
}});
}
}
- (void)continueCollectMyCapabilities:(NSXMLElement *)query
{
// This method must be invoked on the moduleQueue
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue");
XMPPLogTrace();
collectingMyCapabilities = NO;
myCapabilitiesQuery = query;
XMPPLogVerbose(@"%@: My capabilities:\n%@", THIS_FILE,
[query XMLStringWithOptions:(NSXMLNodeCompactEmptyElement | NSXMLNodePrettyPrint)]);
NSString *hash = [self.class hashCapabilitiesFromQuery:query];
if (hash == nil)
{
XMPPLogWarn(@"%@: Unable to hash capabilites (in order to send in presense element)\n"
"Perhaps there are duplicate advertised features...\n%@", THIS_FILE,
[query XMLStringWithOptions:(NSXMLNodeCompactEmptyElement | NSXMLNodePrettyPrint)]);
return;
}
NSString *hashAlg = @"sha-1";
// Cache the hash
[xmppCapabilitiesStorage setCapabilities:query forHash:hash algorithm:hashAlg];
// Create the c element, which will be added to normal outgoing presence elements.
//
// <c xmlns="http://jabber.org/protocol/caps"
// hash="sha-1"
// node="https://github.com/robbiehanson/XMPPFramework"
// ver="QgayPKawpkPSDYmwT/WM94uA1u0="/>
myCapabilitiesC = [[NSXMLElement alloc] initWithName:@"c" xmlns:XMLNS_CAPS];
[myCapabilitiesC addAttributeWithName:@"hash" stringValue:hashAlg];
[myCapabilitiesC addAttributeWithName:@"node" stringValue:myCapabilitiesNode];
[myCapabilitiesC addAttributeWithName:@"ver" stringValue:hash];
// If the collection process started when the stream was connected,
// and ended up taking so long as to not be available when the presence was sent,
// we should re-broadcast our presence now that we know what our capabilities are.
[xmppStream resendMyPresence];
}
- (void)recollectMyCapabilities
{
// This is a public method.
// It may be invoked on any thread/queue.
dispatch_block_t block = ^{ @autoreleasepool {
[self collectMyCapabilities];
}};
if (dispatch_get_specific(moduleQueueTag))
block();
else
dispatch_async(moduleQueue, block);
}
- (void)sendDiscoInfoQueryTo:(XMPPJID *)jid withNode:(NSString *)node ver:(NSString *)ver
{
// <iq to="romeo@montague.lit/orchard" id="uuid" type="get">
// <query xmlns="http://jabber.org/protocol/disco#info" node="[node]#[ver]"/>
// </iq>
//
// Note:
// Some xmpp clients will return an error if we don't specify the proper query node.
// Some xmpp clients will return an error if we don't include an id attribute in the iq.
NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:XMLNS_DISCO_INFO];
if (node && ver)
{
NSString *nodeValue = [NSString stringWithFormat:@"%@#%@", node, ver];
[query addAttributeWithName:@"node" stringValue:nodeValue];
}
XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:jid elementID:[xmppStream generateUUID] child:query];
[xmppStream sendElement:iq];
}
- (void)fetchCapabilitiesForJID:(XMPPJID *)jid
{
// This is a public method.
// It may be invoked on any thread/queue.
dispatch_block_t block = ^{ @autoreleasepool {
if ([discoRequestJidSet containsObject:jid])
{
// We're already requesting capabilities concerning this JID
return;
}
BOOL areCapabilitiesKnown;
BOOL haveFailedFetchingBefore;
NSString *node = nil;
NSString *ver = nil;
NSString *hash = nil;
NSString *hashAlg = nil;
[xmppCapabilitiesStorage getCapabilitiesKnown:&areCapabilitiesKnown
failed:&haveFailedFetchingBefore
node:&node
ver:&ver
ext:nil
hash:&hash
algorithm:&hashAlg
forJID:jid
xmppStream:xmppStream];
if (areCapabilitiesKnown)
{
// We already know the capabilities for this JID
return;
}
if (haveFailedFetchingBefore)
{
// We've already sent a fetch request to the JID in the past, which failed.
return;
}
NSString *key = nil;
if (hash && hashAlg)
{
// This jid is associated with a capabilities hash.
//
// Now, we've verified that the jid is not in the discoRequestJidSet.
// But consider the following scenario.
//
// - autoFetchCapabilities is false.
// - We receive 2 presence elements from 2 different jids, both advertising the same capabilities hash.
// - This method is called for the first jid.
// - This method is then immediately called for the second jid.
//
// Now since autoFetchCapabilities is false, the second jid will not be in the discoRequestJidSet.
// However, there is still a disco request that concerns the jid.
key = [self keyFromHash:hash algorithm:hashAlg];
NSMutableArray *jids = discoRequestHashDict[key];
if (jids)
{
// We're already requesting capabilities concerning this JID.
// That is, there is another JID with the same hash, and we've already sent a disco request to it.
[jids addObject:jid];
[discoRequestJidSet addObject:jid];
return;
}
// The first object in the jids array is the index of the last jid that we've sent a disco request to.
// This is used in case the jid does not respond.
NSNumber *requestIndexNum = @1;
jids = [@[requestIndexNum, jid] mutableCopy];
discoRequestHashDict[key] = jids;
[discoRequestJidSet addObject:jid];
}
else
{
[discoRequestJidSet addObject:jid];
}
// Send disco#info query
[self sendDiscoInfoQueryTo:jid withNode:node ver:ver];
// Setup request timeout
if (key)
{
[self setupTimeoutForDiscoRequestFromJID:jid withHashKey:key];
}
else
{
[self setupTimeoutForDiscoRequestFromJID:jid];
}
}};
if (dispatch_get_specific(moduleQueueTag))
block();
else
dispatch_async(moduleQueue, block);
}
/**
* Invoked when an available presence element is received with
* a capabilities child element that conforms to the XEP-0115 standard.
**/
- (void)handlePresenceCapabilities:(NSXMLElement *)c fromJID:(XMPPJID *)jid
{
// This method must be invoked on the moduleQueue
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue");
XMPPLogTrace2(@"%@: %@ %@", THIS_FILE, THIS_METHOD, jid);
// <presence from="romeo@montague.lit/orchard">
// <c xmlns="http://jabber.org/protocol/caps"
// hash="sha-1"
// node="http://code.google.com/p/exodus"
// ver="QgayPKawpkPSDYmwT/WM94uA1u0="/>
// </presence>
NSString *node = [c attributeStringValueForName:@"node"];
NSString *ver = [c attributeStringValueForName:@"ver"];
NSString *hash = [c attributeStringValueForName:@"hash"];
if ((node == nil) || (ver == nil))
{
// Invalid capabilities node!
if (autoFetchNonHashedCapabilities)
{
[self fetchCapabilitiesForJID:jid];
}
return;
}
// Note: We already checked the hash variable in the xmppStream:didReceivePresence: method below.
// Remember: hash="sha-1" ver="ABC-Actual-Hash-DEF".
// It's a bit confusing as it was designed this way for backwards compatibility with v 1.4 and below.
NSXMLElement *newCapabilities = nil;
BOOL areCapabilitiesKnown = [xmppCapabilitiesStorage setCapabilitiesNode:node
ver:ver
ext:nil
hash:ver // Yes, this is correct (see above)
algorithm:hash // Ditto
forJID:jid
xmppStream:xmppStream
andGetNewCapabilities:&newCapabilities];
if (areCapabilitiesKnown)
{
XMPPLogVerbose(@"%@: Capabilities already known for jid(%@) with hash(%@)", THIS_FILE, jid, ver);
if (newCapabilities)
{
// This is the first time we've linked the jid with the set of capabilities.
// We didn't need to do any lookups due to hashing and caching.
// Notify the delegate(s)
[multicastDelegate xmppCapabilities:self didDiscoverCapabilities:newCapabilities forJID:jid];
}
// The capabilities for this hash are already known
return;
}
// Should we automatically fetch the capabilities?
if (!autoFetchHashedCapabilities)
{
return;
}
// Are we already fetching the capabilities?
NSString *key = [self keyFromHash:ver algorithm:hash];
NSMutableArray *jids = discoRequestHashDict[key];
if (jids)
{
XMPPLogVerbose(@"%@: We're already fetching capabilities for hash(%@)", THIS_FILE, ver);
// Is the jid already included in this list?
//
// There are actually two ways we can answer this question.
// - Invoke containsObject on the array (jids)
// - Invoke containsObject on the set (discoRequestJidSet)
//
// This is much faster to do on a set.
if (![discoRequestJidSet containsObject:jid])
{
[discoRequestJidSet addObject:jid];
[jids addObject:jid];
}
// We've already sent a disco request concerning this hash.
return;
}
// We've never sent a request for this hash.
// Add the jid to the discoRequest variables.
// Note: The first object in the jids array is the index of the last jid that we've sent a disco request to.
// This is used in case the jid does not respond.
//
// Here's the scenario:
// We receive 5 presence elements from 5 different jids,
// all advertising the same capabilities via the same hash.
// We don't want to waste bandwidth and cpu by sending a disco request to all 5 jids.
// So we send a disco request to the first jid.
// But then what happens if that jid never responds?
// Perhaps it went offline before it could get the message.
// After a period of time ellapses, we should send a request to the next jid in the list.
// So how do we know what the next jid in the list is?
// Via the requestIndexNum of course.
NSNumber *requestIndexNum = @1;
jids = [@[requestIndexNum, jid] mutableCopy];
discoRequestHashDict[key] = jids;
[discoRequestJidSet addObject:jid];
// Send disco#info query
[self sendDiscoInfoQueryTo:jid withNode:node ver:ver];
// Setup request timeout
[self setupTimeoutForDiscoRequestFromJID:jid withHashKey:key];
}
/**
* Invoked when an available presence element is received with
* a capabilities child element that implements the legacy version of XEP-0115.
**/
- (void)handleLegacyPresenceCapabilities:(NSXMLElement *)c fromJID:(XMPPJID *)jid
{
// This method must be invoked on the moduleQueue
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue");
XMPPLogTrace2(@"%@: %@ %@", THIS_FILE, THIS_METHOD, jid);
NSString *node = [c attributeStringValueForName:@"node"];
NSString *ver = [c attributeStringValueForName:@"ver"];
NSString *ext = [c attributeStringValueForName:@"ext"];
if ((node == nil) || (ver == nil))
{
// Invalid capabilities node!
if (autoFetchNonHashedCapabilities)
{
[self fetchCapabilitiesForJID:jid];
}
return;
}
BOOL areCapabilitiesKnown = [xmppCapabilitiesStorage setCapabilitiesNode:node
ver:ver
ext:ext
hash:nil
algorithm:nil
forJID:jid
xmppStream:xmppStream
andGetNewCapabilities:nil];
if (areCapabilitiesKnown)
{
XMPPLogVerbose(@"%@: Capabilities already known for jid(%@)", THIS_FILE, jid);
// The capabilities for this jid are already known
return;
}
// Should we automatically fetch the capabilities?
if (!autoFetchNonHashedCapabilities)
{
return;
}
// Are we already fetching the capabilities?
if ([discoRequestJidSet containsObject:jid])
{
XMPPLogVerbose(@"%@: We're already fetching capabilities for jid(%@)", THIS_FILE, jid);
// We've already sent a disco request to this jid.
return;
}
[discoRequestJidSet addObject:jid];
// Send disco#info query
[self sendDiscoInfoQueryTo:jid withNode:node ver:ver];
// Setup request timeout
[self setupTimeoutForDiscoRequestFromJID:jid];
}
/**
* Invoked when we receive a disco request (request for our capabilities).
* We should response with the proper disco response.
**/
- (void)handleDiscoRequest:(XMPPIQ *)iqRequest
{
// This method must be invoked on the moduleQueue
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue");
XMPPLogTrace();
if (myCapabilitiesQuery == nil)
{
// It appears we haven't collected our list of capabilites yet.
// This will need to be done before we can add the hash to the outgoing presence element.
[self collectMyCapabilities];
}
else if (myCapabilitiesC)
{
NSXMLElement *queryRequest = [iqRequest childElement];
NSString *node = [queryRequest attributeStringValueForName:@"node"];
// <iq to="jid" id="id" type="result">
// <query xmlns="http://jabber.org/protocol/disco#info">
// <feature var="feature1"/>
// <feature var="feature2"/>
// </query>
// </iq>
NSXMLElement *query = [myCapabilitiesQuery copy];
if (node)
{
[query addAttributeWithName:@"node" stringValue:node];
}
XMPPIQ *iqResponse = [XMPPIQ iqWithType:@"result"
to:[iqRequest from]
elementID:[iqRequest elementID]
child:query];
[xmppStream sendElement:iqResponse];
}
}
/**
* Invoked when we receive a response to one of our previously sent disco requests.
**/
- (void)handleDiscoResponse:(NSXMLElement *)querySubElement fromJID:(XMPPJID *)jid
{
// This method must be invoked on the moduleQueue
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue");
XMPPLogTrace();
// Remember XML hiearchy memory management rules.
// The passed parameter is a subnode of the IQ, and we need to pass it asynchronously to storge / delegate(s).
NSXMLElement *query = [querySubElement copy];
NSString *hash = nil;
NSString *hashAlg = nil;
BOOL hashResponse = [xmppCapabilitiesStorage getCapabilitiesHash:&hash
algorithm:&hashAlg
forJID:jid
xmppStream:xmppStream];
if (hashResponse)
{
XMPPLogVerbose(@"%@: %@ - Hash response...", THIS_FILE, THIS_METHOD);
// Standard version 1.5+
NSString *key = [self keyFromHash:hash algorithm:hashAlg];
NSString *calculatedHash = [self.class hashCapabilitiesFromQuery:query];
if ([calculatedHash isEqualToString:hash])
{
XMPPLogVerbose(@"%@: %@ - Hash matches!", THIS_FILE, THIS_METHOD);
// Store the capabilities (associated with the hash)
[xmppCapabilitiesStorage setCapabilities:query forHash:hash algorithm:hashAlg];
// Remove the jid(s) from the discoRequest variables
NSArray *jids = discoRequestHashDict[key];
NSUInteger i;
for (i = 1; i < [jids count]; i++)
{
XMPPJID *currentJid = jids[i];
[discoRequestJidSet removeObject:currentJid];
// Notify the delegate(s)
[multicastDelegate xmppCapabilities:self didDiscoverCapabilities:query forJID:currentJid];
}
[discoRequestHashDict removeObjectForKey:key];
// Cancel the request timeout
[self cancelTimeoutForDiscoRequestFromJID:jid];
}
else
{
XMPPLogWarn(@"%@: Hash mismatch! hash(%@) != calculatedHash(%@)", THIS_FILE, hash, calculatedHash);
// Revoke the associated hash from the jid
[xmppCapabilitiesStorage clearCapabilitiesHashAndAlgorithmForJID:jid xmppStream:xmppStream];
// Now set the capabilities for the jid
[xmppCapabilitiesStorage setCapabilities:query forJID:jid xmppStream:xmppStream];
// Notify the delegate(s)
[multicastDelegate xmppCapabilities:self didDiscoverCapabilities:query forJID:jid];
// We'd still like to know what the capabilities are for this hash.
// Move onto the next one in the list (if there are more, otherwise stop).
[self maybeQueryNextJidWithHashKey:key dueToHashMismatch:YES];
}
}
else
{
XMPPLogVerbose(@"%@: %@ - Non-Hash response", THIS_FILE, THIS_METHOD);
// Store the capabilities (associated with the jid)
[xmppCapabilitiesStorage setCapabilities:query forJID:jid xmppStream:xmppStream];
// Remove the jid from the discoRequest variable
[discoRequestJidSet removeObject:jid];
// Cancel the request timeout
[self cancelTimeoutForDiscoRequestFromJID:jid];
// Notify the delegate(s)
[multicastDelegate xmppCapabilities:self didDiscoverCapabilities:query forJID:jid];
}
}
- (void)handleDiscoErrorResponse:(NSXMLElement *)querySubElement fromJID:(XMPPJID *)jid
{
// This method must be invoked on the moduleQueue
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue");
XMPPLogTrace();
NSString *hash = nil;
NSString *hashAlg = nil;
BOOL hashResponse = [xmppCapabilitiesStorage getCapabilitiesHash:&hash
algorithm:&hashAlg
forJID:jid
xmppStream:xmppStream];
if (hashResponse)
{
NSString *key = [self keyFromHash:hash algorithm:hashAlg];
// We'd still like to know what the capabilities are for this hash.
// Move onto the next one in the list (if there are more, otherwise stop).
[self maybeQueryNextJidWithHashKey:key dueToHashMismatch:NO];
}
else
{
// Make a note of the failure
[xmppCapabilitiesStorage setCapabilitiesFetchFailedForJID:jid xmppStream:xmppStream];
// Remove the jid from the discoRequest variable
[discoRequestJidSet removeObject:jid];
// Cancel the request timeout
[self cancelTimeoutForDiscoRequestFromJID:jid];
}
}
- (void)maybeQueryNextJidWithHashKey:(NSString *)key dueToHashMismatch:(BOOL)hashMismatch
{
// This method must be invoked on the moduleQueue
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue");
XMPPLogTrace();
// Get the list of jids that have the same capabilities hash
NSMutableArray *jids = discoRequestHashDict[key];
if (jids == nil)
{
XMPPLogWarn(@"%@: %@ - Key doesn't exist in discoRequestHashDict", THIS_FILE, THIS_METHOD);
return;
}
// Get the index and jid of the fetch that just failed
NSUInteger requestIndex = [jids[0] unsignedIntegerValue];
XMPPJID *jid = jids[requestIndex];
// Release the associated timer
[self cancelTimeoutForDiscoRequestFromJID:jid];
if (hashMismatch)
{
// We need to remove the naughty jid from the lists.
[discoRequestJidSet removeObject:jid];
[jids removeObjectAtIndex:requestIndex];
}
else
{
// We want to move onto the next jid in the list.
// Increment request index (and update object in jids array),
requestIndex++;
jids[0] = @(requestIndex);
}
// Do we have another jid that we can query?
// That is, another jid that was broadcasting the same capabilities hash.
if (requestIndex < [jids count])
{
jid = jids[requestIndex];
NSString *node = nil;
NSString *ver = nil;
[xmppCapabilitiesStorage getCapabilitiesKnown:nil
failed:nil
node:&node
ver:&ver
ext:nil
hash:nil
algorithm:nil
forJID:jid
xmppStream:xmppStream];
// Send disco#info query
[self sendDiscoInfoQueryTo:jid withNode:node ver:ver];
// Setup request timeout
[self setupTimeoutForDiscoRequestFromJID:jid withHashKey:key];
}
else
{
// We've queried every single jid that was broadcasting this capabilities hash.
// Nothing left to do now but wait.
//
// If one of the jids happens to eventually respond,
// then we'll still be able to link the capabilities to every jid with the same capabilities hash.
//
// This would be handled by the xmppCapabilitiesStorage class,
// via the setCapabilitiesForJID method.
NSUInteger i;
for (i = 1; i < [jids count]; i++)
{
jid = jids[i];
[discoRequestJidSet removeObject:jid];
[xmppCapabilitiesStorage setCapabilitiesFetchFailedForJID:jid xmppStream:xmppStream];
}
[discoRequestHashDict removeObjectForKey:key];
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark XMPPStream Delegate
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)xmppStreamDidConnect:(XMPPStream *)sender
{
// If this is the first time we've connected, start collecting our list of capabilities.
// We do this now so that the process is likely ready by the time we need to send a presence element.
if (myCapabilitiesQuery == nil)
{
[self collectMyCapabilities];
}
}
- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender
{
if (autoFetchMyServerCapabilities)
{
XMPPJID *myJID = [xmppStream myJID];
XMPPJID *myServerJID = [XMPPJID jidWithUser:nil domain:[myJID domain] resource:nil];
[self fetchCapabilitiesForJID:myServerJID];
}
}
- (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence
{
// This method is invoked on the moduleQueue.
// XEP-0115 presence:
//
// <presence from="romeo@montague.lit/orchard">
// <c xmlns="http://jabber.org/protocol/caps"
// hash="sha-1"
// node="http://code.google.com/p/exodus"
// ver="QgayPKawpkPSDYmwT/WM94uA1u0="/>
// </presence>
NSString *type = [presence type];
XMPPJID *myJID = xmppStream.myJID;
if ([myJID isEqual:[presence from]])
{
// Our own presence is being reflected back to us.
return;
}
if ([type isEqualToString:@"unavailable"])
{
[xmppCapabilitiesStorage clearNonPersistentCapabilitiesForJID:[presence from] xmppStream:xmppStream];
}
else if ([type isEqualToString:@"available"])
{
NSXMLElement *c = [presence elementForName:@"c" xmlns:XMLNS_CAPS];
if (c == nil)
{
if (autoFetchNonHashedCapabilities)
{
[self fetchCapabilitiesForJID:[presence from]];
}
}
else
{
NSString *hash = [c attributeStringValueForName:@"hash"];
if (hash)
{
[self handlePresenceCapabilities:c fromJID:[presence from]];
}
else
{
[self handleLegacyPresenceCapabilities:c fromJID:[presence from]];
}
}
}
}
- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq
{
// This method is invoked on the moduleQueue.
// Disco Request:
//
// <iq from="juliet@capulet.lit/chamber" type="get">
// <query xmlns="http://jabber.org/protocol/disco#info"/>
// </iq>
//
// Disco Response:
//
// <iq from="romeo@montague.lit/orchard" type="result">
// <query xmlns="http://jabber.org/protocol/disco#info">
// <feature var="feature1"/>
// <feature var="feature2"/>
// </query>
// </iq>
NSXMLElement *query = [iq elementForName:@"query" xmlns:XMLNS_DISCO_INFO];
if (query == nil)
{
return NO;
}
NSString *type = [[iq attributeStringValueForName:@"type"] lowercaseString];
if ([type isEqualToString:@"get"])
{
NSString *node = [query attributeStringValueForName:@"node"];
if (node == nil || [node hasPrefix:myCapabilitiesNode])
{
[self handleDiscoRequest:iq];
}
else
{
return NO;
}
}
else if ([type isEqualToString:@"result"])
{
[self handleDiscoResponse:query fromJID:[iq from]];
}
else if ([type isEqualToString:@"error"])
{
[self handleDiscoErrorResponse:query fromJID:[iq from]];
}
else
{
return NO;
}
return YES;
}
- (XMPPPresence *)xmppStream:(XMPPStream *)sender willSendPresence:(XMPPPresence *)presence
{
// This method is invoked on the moduleQueue.
NSString *type = [presence type];
if ([type isEqualToString:@"unavailable"])
{
[xmppCapabilitiesStorage clearAllNonPersistentCapabilitiesForXMPPStream:xmppStream];
}
else if ([type isEqualToString:@"available"])
{
if (myCapabilitiesQuery == nil)
{
// It appears we haven't collected our list of capabilites yet.
// This will need to be done before we can add the hash to the outgoing presence element.
[self collectMyCapabilities];
}
else if (myCapabilitiesC)
{
NSXMLElement *c = [myCapabilitiesC copy];
NSXMLElement *oldC = [presence elementForName:c.name xmlns:c.xmlns];
if (oldC)
{
[presence removeChildAtIndex:[presence.children indexOfObject:oldC]];
[presence addChild:c];
}
else
{
[presence addChild:c];
}
}
}
return presence;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Timers
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)setupTimeoutForDiscoRequestFromJID:(XMPPJID *)jid
{
// This method must be invoked on the moduleQueue
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue");
XMPPLogTrace();
// If the timeout occurs, we will remove the jid from the discoRequestJidSet.
// If we eventually get a response (after the timeout) we will still be able to process it.
// The timeout simply prevents the set from growing infinitely.
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, moduleQueue);
dispatch_source_set_event_handler(timer, ^{ @autoreleasepool {
[self processTimeoutWithJID:jid];
dispatch_source_cancel(timer);
#if !OS_OBJECT_USE_OBJC
dispatch_release(timer);
#endif
}});
dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (CAPABILITIES_REQUEST_TIMEOUT * NSEC_PER_SEC));
dispatch_source_set_timer(timer, tt, DISPATCH_TIME_FOREVER, 0);
dispatch_resume(timer);
// We also keep a reference to the timer in the discoTimerJidDict.
// This allows us to cancel the timer when we get a response to the disco request.
GCDTimerWrapper *timerWrapper = [[GCDTimerWrapper alloc] initWithDispatchTimer:timer];
discoTimerJidDict[jid] = timerWrapper;
}
- (void)setupTimeoutForDiscoRequestFromJID:(XMPPJID *)jid withHashKey:(NSString *)key
{
// This method must be invoked on the moduleQueue
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue");
XMPPLogTrace();
// If the timeout occurs, we want to send a request to the next jid with the same capabilities hash.
// This list of jids is stored in the discoRequestHashDict.
// The key will allow us to fetch the jid list.
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, moduleQueue);
dispatch_source_set_event_handler(timer, ^{ @autoreleasepool {
[self processTimeoutWithHashKey:key];
dispatch_source_cancel(timer);
#if !OS_OBJECT_USE_OBJC
dispatch_release(timer);
#endif
}});
dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (CAPABILITIES_REQUEST_TIMEOUT * NSEC_PER_SEC));
dispatch_source_set_timer(timer, tt, DISPATCH_TIME_FOREVER, 0);
dispatch_resume(timer);
// We also keep a reference to the timer in the discoTimerJidDict.
// This allows us to cancel the timer when we get a response to the disco request.
GCDTimerWrapper *timerWrapper = [[GCDTimerWrapper alloc] initWithDispatchTimer:timer];
discoTimerJidDict[jid] = timerWrapper;
}
- (void)cancelTimeoutForDiscoRequestFromJID:(XMPPJID *)jid
{
// This method must be invoked on the moduleQueue
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue");
XMPPLogTrace();
GCDTimerWrapper *timerWrapper = discoTimerJidDict[jid];
if (timerWrapper)
{
[timerWrapper cancel];
[discoTimerJidDict removeObjectForKey:jid];
}
}
- (void)processTimeoutWithHashKey:(NSString *)key
{
// This method must be invoked on the moduleQueue
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue");
XMPPLogTrace();
[self maybeQueryNextJidWithHashKey:key dueToHashMismatch:NO];
}
- (void)processTimeoutWithJID:(XMPPJID *)jid
{
// This method must be invoked on the moduleQueue
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue");
XMPPLogTrace();
// We queried the jid for its capabilities, but it didn't answer us.
// Nothing left to do now but wait.
//
// If it happens to eventually respond,
// then we'll still be able to process the capabilities properly.
//
// But at this point we're going to consider the query to be done.
// This prevents our discoRequestJidSet from growing infinitely,
// and also opens up the possibility of sending it another query in the future.
[discoRequestJidSet removeObject:jid];
[xmppCapabilitiesStorage setCapabilitiesFetchFailedForJID:jid xmppStream:xmppStream];
}
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@implementation GCDTimerWrapper
- (id)initWithDispatchTimer:(dispatch_source_t)aTimer
{
if ((self = [super init]))
{
timer = aTimer;
#if !OS_OBJECT_USE_OBJC
dispatch_retain(timer);
#endif
}
return self;
}
- (void)cancel
{
if (timer)
{
dispatch_source_cancel(timer);
#if !OS_OBJECT_USE_OBJC
dispatch_release(timer);
#endif
timer = NULL;
}
}
- (void)dealloc
{
[self cancel];
}
@end