1782 lines
52 KiB
Objective-C
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 "<" 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 <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
|