#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 )storage { return [self initWithCapabilitiesStorage:storage dispatchQueue:NULL]; } - (id)initWithCapabilitiesStorage:(id )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 { 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 "<" 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 "<" string. return [str stringByReplacingOccurrencesOfString:@"<" withString:@"<"]; } 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 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 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 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 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 element. // 3. For each 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 // // // // // 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. // // 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 { // // // // // 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); // // // 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"]; // // // // // // 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: // // // // 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: // // // // // // Disco Response: // // // // // // // 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