diff --git a/Authentication/Anonymous/XMPPAnonymousAuthentication.h b/Authentication/Anonymous/XMPPAnonymousAuthentication.h new file mode 100644 index 0000000..94b5515 --- /dev/null +++ b/Authentication/Anonymous/XMPPAnonymousAuthentication.h @@ -0,0 +1,45 @@ +#import +#import "XMPPSASLAuthentication.h" +#import "XMPP.h" + + +@interface XMPPAnonymousAuthentication : NSObject + +- (id)initWithStream:(XMPPStream *)stream; + +// This class implements the XMPPSASLAuthentication protocol. +// +// See XMPPSASLAuthentication.h for more information. + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPStream (XMPPAnonymousAuthentication) + +/** + * Returns whether or not the server support anonymous authentication. + * + * This information is available after the stream is connected. + * In other words, after the delegate has received xmppStreamDidConnect: notification. +**/ +- (BOOL)supportsAnonymousAuthentication; + +/** + * This method attempts to start the anonymous authentication process. + * + * This method is asynchronous. + * + * If there is something immediately wrong, + * such as the stream is not connected or doesn't support anonymous authentication, + * the method will return NO and set the error. + * Otherwise the delegate callbacks are used to communicate auth success or failure. + * + * @see xmppStreamDidAuthenticate: + * @see xmppStream:didNotAuthenticate: +**/ +- (BOOL)authenticateAnonymously:(NSError **)errPtr; + +@end diff --git a/Authentication/Anonymous/XMPPAnonymousAuthentication.m b/Authentication/Anonymous/XMPPAnonymousAuthentication.m new file mode 100644 index 0000000..aadc1a7 --- /dev/null +++ b/Authentication/Anonymous/XMPPAnonymousAuthentication.m @@ -0,0 +1,131 @@ +#import "XMPPAnonymousAuthentication.h" +#import "XMPP.h" +#import "XMPPLogging.h" +#import "XMPPInternal.h" +#import "NSXMLElement+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_INFO; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +/** + * Seeing a return statements within an inner block + * can sometimes be mistaken for a return point of the enclosing method. + * This makes inline blocks a bit easier to read. +**/ +#define return_from_block return + + +@implementation XMPPAnonymousAuthentication +{ + #if __has_feature(objc_arc_weak) + __weak XMPPStream *xmppStream; + #else + __unsafe_unretained XMPPStream *xmppStream; + #endif +} + ++ (NSString *)mechanismName +{ + return @"ANONYMOUS"; +} + +- (id)initWithStream:(XMPPStream *)stream +{ + if ((self = [super init])) + { + xmppStream = stream; + } + return self; +} + +- (id)initWithStream:(XMPPStream *)stream password:(NSString *)password +{ + return [self initWithStream:stream]; +} + +- (BOOL)start:(NSError **)errPtr +{ + // + + NSXMLElement *auth = [NSXMLElement elementWithName:@"auth" xmlns:@"urn:ietf:params:xml:ns:xmpp-sasl"]; + [auth addAttributeWithName:@"mechanism" stringValue:@"ANONYMOUS"]; + + [xmppStream sendAuthElement:auth]; + + return YES; +} + +- (XMPPHandleAuthResponse)handleAuth:(NSXMLElement *)authResponse +{ + // We're expecting a success response. + // If we get anything else we can safely assume it's the equivalent of a failure response. + + if ([[authResponse name] isEqualToString:@"success"]) + { + return XMPP_AUTH_SUCCESS; + } + else + { + return XMPP_AUTH_FAIL; + } +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPStream (XMPPAnonymousAuthentication) + +- (BOOL)supportsAnonymousAuthentication +{ + return [self supportsAuthenticationMechanism:[XMPPAnonymousAuthentication mechanismName]]; +} + +- (BOOL)authenticateAnonymously:(NSError **)errPtr +{ + XMPPLogTrace(); + + __block BOOL result = YES; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if ([self supportsAnonymousAuthentication]) + { + XMPPAnonymousAuthentication *anonymousAuth = [[XMPPAnonymousAuthentication alloc] initWithStream:self]; + + result = [self authenticate:anonymousAuth error:&err]; + } + else + { + NSString *errMsg = @"The server does not support anonymous authentication."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamUnsupportedAction userInfo:info]; + + result = NO; + } + }}; + + if (dispatch_get_specific(self.xmppQueueTag)) + block(); + else + dispatch_sync(self.xmppQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +@end diff --git a/Authentication/Deprecated-Digest/XMPPDeprecatedDigestAuthentication.h b/Authentication/Deprecated-Digest/XMPPDeprecatedDigestAuthentication.h new file mode 100644 index 0000000..4993f03 --- /dev/null +++ b/Authentication/Deprecated-Digest/XMPPDeprecatedDigestAuthentication.h @@ -0,0 +1,22 @@ +#import +#import "XMPPSASLAuthentication.h" +#import "XMPPStream.h" + + +@interface XMPPDeprecatedDigestAuthentication : NSObject + +// This class implements the XMPPSASLAuthentication protocol. +// +// See XMPPSASLAuthentication.h for more information. + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPStream (XMPPDeprecatedDigestAuthentication) + +- (BOOL)supportsDeprecatedDigestAuthentication; + +@end diff --git a/Authentication/Deprecated-Digest/XMPPDeprecatedDigestAuthentication.m b/Authentication/Deprecated-Digest/XMPPDeprecatedDigestAuthentication.m new file mode 100644 index 0000000..c39b521 --- /dev/null +++ b/Authentication/Deprecated-Digest/XMPPDeprecatedDigestAuthentication.m @@ -0,0 +1,162 @@ +#import "XMPPDeprecatedDigestAuthentication.h" +#import "XMPP.h" +#import "XMPPInternal.h" +#import "XMPPLogging.h" +#import "NSData+XMPP.h" +#import "NSXMLElement+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_INFO; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + + +@implementation XMPPDeprecatedDigestAuthentication +{ + #if __has_feature(objc_arc_weak) + __weak XMPPStream *xmppStream; + #else + __unsafe_unretained XMPPStream *xmppStream; + #endif + + NSString *password; +} + ++ (NSString *)mechanismName +{ + // This deprecated method isn't listed in the normal mechanisms list + return nil; +} + +- (id)initWithStream:(XMPPStream *)stream password:(NSString *)inPassword +{ + if ((self = [super init])) + { + xmppStream = stream; + password = inPassword; + } + return self; +} + +- (BOOL)start:(NSError **)errPtr +{ + XMPPLogTrace(); + + // The server does not appear to support SASL authentication (at least any type we can use) + // So we'll revert back to the old fashioned jabber:iq:auth mechanism + + XMPPJID *myJID = xmppStream.myJID; + + NSString *username = [myJID user]; + NSString *resource = [myJID resource]; + + if ([resource length] == 0) + { + // If resource is nil or empty, we need to auto-create one + + resource = [XMPPStream generateUUID]; + } + + NSString *rootID = [[[xmppStream rootElement] attributeForName:@"id"] stringValue]; + NSString *digestStr = [NSString stringWithFormat:@"%@%@", rootID, password]; + + NSString *digest = [[[digestStr dataUsingEncoding:NSUTF8StringEncoding] xmpp_sha1Digest] xmpp_hexStringValue]; + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:auth"]; + [query addChild:[NSXMLElement elementWithName:@"username" stringValue:username]]; + [query addChild:[NSXMLElement elementWithName:@"resource" stringValue:resource]]; + [query addChild:[NSXMLElement elementWithName:@"digest" stringValue:digest]]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set"]; + [iq addChild:query]; + + [xmppStream sendAuthElement:iq]; + + return YES; +} + +- (XMPPHandleAuthResponse)handleAuth:(NSXMLElement *)authResponse +{ + XMPPLogTrace(); + + // We used the old fashioned jabber:iq:auth mechanism + + if ([[authResponse attributeStringValueForName:@"type"] isEqualToString:@"error"]) + { + return XMPP_AUTH_FAIL; + } + else + { + return XMPP_AUTH_SUCCESS; + } +} + +- (BOOL)shouldResendOpeningNegotiationAfterSuccessfulAuthentication +{ + return NO; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPStream (XMPPDeprecatedDigestAuthentication) + +/** + * This method only applies to servers that don't support XMPP version 1.0, as defined in RFC 3920. + * With these servers, we attempt to discover supported authentication modes via the jabber:iq:auth namespace. +**/ +- (BOOL)supportsDeprecatedDigestAuthentication +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ @autoreleasepool { + + // The root element can be properly queried for authentication mechanisms anytime after the + // stream:features are received, and TLS has been setup (if required) + if (self.state >= STATE_XMPP_POST_NEGOTIATION) + { + // Search for an iq element within the rootElement. + // Recall that some servers might stupidly add a "jabber:client" namespace which might cause problems + // if we simply used the elementForName method. + + NSXMLElement *iq = nil; + + NSUInteger i, count = [self.rootElement childCount]; + for (i = 0; i < count; i++) + { + NSXMLNode *childNode = [self.rootElement childAtIndex:i]; + + if ([childNode kind] == NSXMLElementKind) + { + if ([[childNode name] isEqualToString:@"iq"]) + { + iq = (NSXMLElement *)childNode; + } + } + } + + NSXMLElement *query = [iq elementForName:@"query" xmlns:@"jabber:iq:auth"]; + NSXMLElement *digest = [query elementForName:@"digest"]; + + result = (digest != nil); + } + }}; + + if (dispatch_get_specific(self.xmppQueueTag)) + block(); + else + dispatch_sync(self.xmppQueue, block); + + return result; +} + +@end diff --git a/Authentication/Deprecated-Plain/XMPPDeprecatedPlainAuthentication.h b/Authentication/Deprecated-Plain/XMPPDeprecatedPlainAuthentication.h new file mode 100644 index 0000000..b86efa3 --- /dev/null +++ b/Authentication/Deprecated-Plain/XMPPDeprecatedPlainAuthentication.h @@ -0,0 +1,22 @@ +#import +#import "XMPPSASLAuthentication.h" +#import "XMPPStream.h" + + +@interface XMPPDeprecatedPlainAuthentication : NSObject + +// This class implements the XMPPSASLAuthentication protocol. +// +// See XMPPSASLAuthentication.h for more information. + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPStream (XMPPDeprecatedPlainAuthentication) + +- (BOOL)supportsDeprecatedPlainAuthentication; + +@end diff --git a/Authentication/Deprecated-Plain/XMPPDeprecatedPlainAuthentication.m b/Authentication/Deprecated-Plain/XMPPDeprecatedPlainAuthentication.m new file mode 100644 index 0000000..f6c36b4 --- /dev/null +++ b/Authentication/Deprecated-Plain/XMPPDeprecatedPlainAuthentication.m @@ -0,0 +1,156 @@ +#import "XMPPDeprecatedPlainAuthentication.h" +#import "XMPP.h" +#import "XMPPInternal.h" +#import "XMPPLogging.h" +#import "NSXMLElement+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_INFO; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + + +@implementation XMPPDeprecatedPlainAuthentication +{ + #if __has_feature(objc_arc_weak) + __weak XMPPStream *xmppStream; + #else + __unsafe_unretained XMPPStream *xmppStream; + #endif + + NSString *password; +} + ++ (NSString *)mechanismName +{ + // This deprecated method isn't listed in the normal mechanisms list + return nil; +} + +- (id)initWithStream:(XMPPStream *)stream password:(NSString *)inPassword +{ + if ((self = [super init])) + { + xmppStream = stream; + password = inPassword; + } + return self; +} + +- (BOOL)start:(NSError **)errPtr +{ + XMPPLogTrace(); + + // The server does not appear to support SASL authentication (at least any type we can use) + // So we'll revert back to the old fashioned jabber:iq:auth mechanism + + XMPPJID *myJID = xmppStream.myJID; + + NSString *username = [myJID user]; + NSString *resource = [myJID resource]; + + if ([resource length] == 0) + { + // If resource is nil or empty, we need to auto-create one + + resource = [XMPPStream generateUUID]; + } + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:auth"]; + [query addChild:[NSXMLElement elementWithName:@"username" stringValue:username]]; + [query addChild:[NSXMLElement elementWithName:@"resource" stringValue:resource]]; + [query addChild:[NSXMLElement elementWithName:@"password" stringValue:password]]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set"]; + [iq addChild:query]; + + [xmppStream sendAuthElement:iq]; + + return YES; +} + +- (XMPPHandleAuthResponse)handleAuth:(NSXMLElement *)authResponse +{ + XMPPLogTrace(); + + // We used the old fashioned jabber:iq:auth mechanism + + if ([[authResponse attributeStringValueForName:@"type"] isEqualToString:@"error"]) + { + return XMPP_AUTH_FAIL; + } + else + { + return XMPP_AUTH_SUCCESS; + } +} + +- (BOOL)shouldResendOpeningNegotiationAfterSuccessfulAuthentication +{ + return NO; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPStream (XMPPDeprecatedPlainAuthentication) + +/** + * This method only applies to servers that don't support XMPP version 1.0, as defined in RFC 3920. + * With these servers, we attempt to discover supported authentication modes via the jabber:iq:auth namespace. +**/ +- (BOOL)supportsDeprecatedPlainAuthentication +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ @autoreleasepool { + + // The root element can be properly queried for authentication mechanisms anytime after the + // stream:features are received, and TLS has been setup (if required) + if (self.state >= STATE_XMPP_POST_NEGOTIATION) + { + // Search for an iq element within the rootElement. + // Recall that some servers might stupidly add a "jabber:client" namespace which might cause problems + // if we simply used the elementForName method. + + NSXMLElement *iq = nil; + + NSUInteger i, count = [self.rootElement childCount]; + for (i = 0; i < count; i++) + { + NSXMLNode *childNode = [self.rootElement childAtIndex:i]; + + if ([childNode kind] == NSXMLElementKind) + { + if ([[childNode name] isEqualToString:@"iq"]) + { + iq = (NSXMLElement *)childNode; + } + } + } + + NSXMLElement *query = [iq elementForName:@"query" xmlns:@"jabber:iq:auth"]; + NSXMLElement *plain = [query elementForName:@"password"]; + + result = (plain != nil); + } + }}; + + if (dispatch_get_specific(self.xmppQueueTag)) + block(); + else + dispatch_sync(self.xmppQueue, block); + + return result; +} + +@end diff --git a/Authentication/Digest-MD5/XMPPDigestMD5Authentication.h b/Authentication/Digest-MD5/XMPPDigestMD5Authentication.h new file mode 100644 index 0000000..b228781 --- /dev/null +++ b/Authentication/Digest-MD5/XMPPDigestMD5Authentication.h @@ -0,0 +1,22 @@ +#import +#import "XMPPSASLAuthentication.h" +#import "XMPPStream.h" + + +@interface XMPPDigestMD5Authentication : NSObject + +// This class implements the XMPPSASLAuthentication protocol. +// +// See XMPPSASLAuthentication.h for more information. + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPStream (XMPPDigestMD5Authentication) + +- (BOOL)supportsDigestMD5Authentication; + +@end diff --git a/Authentication/Digest-MD5/XMPPDigestMD5Authentication.m b/Authentication/Digest-MD5/XMPPDigestMD5Authentication.m new file mode 100644 index 0000000..28d4697 --- /dev/null +++ b/Authentication/Digest-MD5/XMPPDigestMD5Authentication.m @@ -0,0 +1,336 @@ +#import "XMPPDigestMD5Authentication.h" +#import "XMPP.h" +#import "XMPPLogging.h" +#import "XMPPInternal.h" +#import "NSData+XMPP.h" +#import "NSXMLElement+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_INFO; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +@interface XMPPDigestMD5Authentication () +{ + #if __has_feature(objc_arc_weak) + __weak XMPPStream *xmppStream; + #else + __unsafe_unretained XMPPStream *xmppStream; + #endif + + BOOL awaitingChallenge; + + NSString *realm; + NSString *nonce; + NSString *qop; + NSString *cnonce; + NSString *digestURI; + NSString *username; + NSString *password; +} + +// The properties are hooks (primarily for testing) + +@property (nonatomic, strong) NSString *realm; +@property (nonatomic, strong) NSString *nonce; +@property (nonatomic, strong) NSString *qop; +@property (nonatomic, strong) NSString *cnonce; +@property (nonatomic, strong) NSString *digestURI; +@property (nonatomic, strong) NSString *username; +@property (nonatomic, strong) NSString *password; + +- (NSDictionary *)dictionaryFromChallenge:(NSXMLElement *)challenge; +- (NSString *)base64EncodedFullResponse; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPDigestMD5Authentication + ++ (NSString *)mechanismName +{ + return @"DIGEST-MD5"; +} + +@synthesize realm; +@synthesize nonce; +@synthesize qop; +@synthesize cnonce; +@synthesize digestURI; +@synthesize username; +@synthesize password; + +- (id)initWithStream:(XMPPStream *)stream password:(NSString *)inPassword +{ + return [self initWithStream:stream username:nil password:inPassword]; +} + +- (id)initWithStream:(XMPPStream *)stream username:(NSString *)inUsername password:(NSString *)inPassword +{ + if ((self = [super init])) + { + xmppStream = stream; + username = inUsername; + password = inPassword; + } + return self; +} + +- (BOOL)start:(NSError **)errPtr +{ + XMPPLogTrace(); + + // + + NSXMLElement *auth = [NSXMLElement elementWithName:@"auth" xmlns:@"urn:ietf:params:xml:ns:xmpp-sasl"]; + [auth addAttributeWithName:@"mechanism" stringValue:@"DIGEST-MD5"]; + + [xmppStream sendAuthElement:auth]; + awaitingChallenge = YES; + + return YES; +} + +- (XMPPHandleAuthResponse)handleAuth1:(NSXMLElement *)authResponse +{ + XMPPLogTrace(); + + // We're expecting a challenge response. + // If we get anything else we're going to assume it's some kind of failure response. + + if (![[authResponse name] isEqualToString:@"challenge"]) + { + return XMPP_AUTH_FAIL; + } + + // Extract components from incoming challenge + + NSDictionary *auth = [self dictionaryFromChallenge:authResponse]; + + realm = auth[@"realm"]; + nonce = auth[@"nonce"]; + qop = auth[@"qop"]; + + // Fill out all the other variables + // + // Sometimes the realm isn't specified. + // In this case I believe the realm is implied as the virtual host name. + + XMPPJID *myJID = xmppStream.myJID; + + NSString *virtualHostName = [myJID domain]; + NSString *serverHostName = xmppStream.hostName; + + if (realm == nil) + { + if ([virtualHostName length] > 0) + realm = virtualHostName; + else + realm = serverHostName; + } + + if ([virtualHostName length] > 0) + digestURI = [NSString stringWithFormat:@"xmpp/%@", virtualHostName]; + else + digestURI = [NSString stringWithFormat:@"xmpp/%@", serverHostName]; + + if (cnonce == nil) + cnonce = [XMPPStream generateUUID]; + + if (username == nil) + { + username = [myJID user]; + } + + // Create and send challenge response element + + NSXMLElement *response = [NSXMLElement elementWithName:@"response" xmlns:@"urn:ietf:params:xml:ns:xmpp-sasl"]; + [response setStringValue:[self base64EncodedFullResponse]]; + + [xmppStream sendAuthElement:response]; + awaitingChallenge = NO; + + return XMPP_AUTH_CONTINUE; +} + +- (XMPPHandleAuthResponse)handleAuth2:(NSXMLElement *)authResponse +{ + XMPPLogTrace(); + + if ([[authResponse name] isEqualToString:@"challenge"]) + { + NSDictionary *auth = [self dictionaryFromChallenge:authResponse]; + NSString *rspauth = auth[@"rspauth"]; + + if (rspauth == nil) + { + // We're getting another challenge? + // Not sure what this could possibly be, so for now we'll assume it's a failure. + + return XMPP_AUTH_FAIL; + } + else + { + // We received another challenge, but it's really just an rspauth + // This is supposed to be included in the success element (according to the updated RFC) + // but many implementations incorrectly send it inside a second challenge request. + // + // Create and send empty challenge response element. + + NSXMLElement *response = + [NSXMLElement elementWithName:@"response" xmlns:@"urn:ietf:params:xml:ns:xmpp-sasl"]; + + [xmppStream sendAuthElement:response]; + + return XMPP_AUTH_CONTINUE; + } + } + else if ([[authResponse name] isEqualToString:@"success"]) + { + return XMPP_AUTH_SUCCESS; + } + else + { + return XMPP_AUTH_FAIL; + } +} + +- (XMPPHandleAuthResponse)handleAuth:(NSXMLElement *)auth +{ + XMPPLogTrace(); + + if (awaitingChallenge) + { + return [self handleAuth1:auth]; + } + else + { + return [self handleAuth2:auth]; + } +} + +- (NSDictionary *)dictionaryFromChallenge:(NSXMLElement *)challenge +{ + // The value of the challenge stanza is base 64 encoded. + // Once "decoded", it's just a string of key=value pairs separated by commas. + + NSData *base64Data = [[challenge stringValue] dataUsingEncoding:NSASCIIStringEncoding]; + NSData *decodedData = [base64Data xmpp_base64Decoded]; + + NSString *authStr = [[NSString alloc] initWithData:decodedData encoding:NSUTF8StringEncoding]; + + XMPPLogVerbose(@"%@: Decoded challenge: %@", THIS_FILE, authStr); + + NSArray *components = [authStr componentsSeparatedByString:@","]; + NSMutableDictionary *auth = [NSMutableDictionary dictionaryWithCapacity:5]; + + for (NSString *component in components) + { + NSRange separator = [component rangeOfString:@"="]; + if (separator.location != NSNotFound) + { + NSMutableString *key = [[component substringToIndex:separator.location] mutableCopy]; + NSMutableString *value = [[component substringFromIndex:separator.location+1] mutableCopy]; + + if(key) CFStringTrimWhitespace((__bridge CFMutableStringRef)key); + if(value) CFStringTrimWhitespace((__bridge CFMutableStringRef)value); + + if ([value hasPrefix:@"\""] && [value hasSuffix:@"\""] && [value length] > 2) + { + // Strip quotes from value + [value deleteCharactersInRange:NSMakeRange(0, 1)]; + [value deleteCharactersInRange:NSMakeRange([value length]-1, 1)]; + } + + if(key && value) + { + auth[key] = value; + } + } + } + + return auth; +} + +- (NSString *)response +{ + NSString *HA1str = [NSString stringWithFormat:@"%@:%@:%@", username, realm, password]; + NSString *HA2str = [NSString stringWithFormat:@"AUTHENTICATE:%@", digestURI]; + + XMPPLogVerbose(@"HA1str: %@", HA1str); + XMPPLogVerbose(@"HA2str: %@", HA2str); + + NSData *HA1dataA = [[HA1str dataUsingEncoding:NSUTF8StringEncoding] xmpp_md5Digest]; + NSData *HA1dataB = [[NSString stringWithFormat:@":%@:%@", nonce, cnonce] dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogVerbose(@"HA1dataA: %@", HA1dataA); + XMPPLogVerbose(@"HA1dataB: %@", HA1dataB); + + NSMutableData *HA1data = [NSMutableData dataWithCapacity:([HA1dataA length] + [HA1dataB length])]; + [HA1data appendData:HA1dataA]; + [HA1data appendData:HA1dataB]; + + XMPPLogVerbose(@"HA1data: %@", HA1data); + + NSString *HA1 = [[HA1data xmpp_md5Digest] xmpp_hexStringValue]; + + NSString *HA2 = [[[HA2str dataUsingEncoding:NSUTF8StringEncoding] xmpp_md5Digest] xmpp_hexStringValue]; + + XMPPLogVerbose(@"HA1: %@", HA1); + XMPPLogVerbose(@"HA2: %@", HA2); + + NSString *responseStr = [NSString stringWithFormat:@"%@:%@:00000001:%@:auth:%@", + HA1, nonce, cnonce, HA2]; + + XMPPLogVerbose(@"responseStr: %@", responseStr); + + NSString *response = [[[responseStr dataUsingEncoding:NSUTF8StringEncoding] xmpp_md5Digest] xmpp_hexStringValue]; + + XMPPLogVerbose(@"response: %@", response); + + return response; +} + +- (NSString *)base64EncodedFullResponse +{ + NSMutableString *buffer = [NSMutableString stringWithCapacity:100]; + [buffer appendFormat:@"username=\"%@\",", username]; + [buffer appendFormat:@"realm=\"%@\",", realm]; + [buffer appendFormat:@"nonce=\"%@\",", nonce]; + [buffer appendFormat:@"cnonce=\"%@\",", cnonce]; + [buffer appendFormat:@"nc=00000001,"]; + [buffer appendFormat:@"qop=auth,"]; + [buffer appendFormat:@"digest-uri=\"%@\",", digestURI]; + [buffer appendFormat:@"response=%@,", [self response]]; + [buffer appendFormat:@"charset=utf-8"]; + + XMPPLogVerbose(@"%@: Decoded response: %@", THIS_FILE, buffer); + + NSData *utf8data = [buffer dataUsingEncoding:NSUTF8StringEncoding]; + + return [utf8data xmpp_base64Encoded]; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPStream (XMPPDigestMD5Authentication) + +- (BOOL)supportsDigestMD5Authentication +{ + return [self supportsAuthenticationMechanism:[XMPPDigestMD5Authentication mechanismName]]; +} + +@end diff --git a/Authentication/Plain/XMPPPlainAuthentication.h b/Authentication/Plain/XMPPPlainAuthentication.h new file mode 100644 index 0000000..a1f79f1 --- /dev/null +++ b/Authentication/Plain/XMPPPlainAuthentication.h @@ -0,0 +1,22 @@ +#import +#import "XMPPSASLAuthentication.h" +#import "XMPPStream.h" + + +@interface XMPPPlainAuthentication : NSObject + +// This class implements the XMPPSASLAuthentication protocol. +// +// See XMPPSASLAuthentication.h for more information. + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPStream (XMPPPlainAuthentication) + +- (BOOL)supportsPlainAuthentication; + +@end diff --git a/Authentication/Plain/XMPPPlainAuthentication.m b/Authentication/Plain/XMPPPlainAuthentication.m new file mode 100644 index 0000000..7b74551 --- /dev/null +++ b/Authentication/Plain/XMPPPlainAuthentication.m @@ -0,0 +1,114 @@ +#import "XMPPPlainAuthentication.h" +#import "XMPP.h" +#import "XMPPLogging.h" +#import "XMPPInternal.h" +#import "NSData+XMPP.h" +#import "NSXMLElement+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_INFO; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + + +@implementation XMPPPlainAuthentication +{ + #if __has_feature(objc_arc_weak) + __weak XMPPStream *xmppStream; + #else + __unsafe_unretained XMPPStream *xmppStream; + #endif + + NSString *username; + NSString *password; +} + ++ (NSString *)mechanismName +{ + return @"PLAIN"; +} + +- (id)initWithStream:(XMPPStream *)stream password:(NSString *)inPassword +{ + return [self initWithStream:stream username:nil password:inPassword]; +} + +- (id)initWithStream:(XMPPStream *)stream username:(NSString *)inUsername password:(NSString *)inPassword +{ + if ((self = [super init])) + { + xmppStream = stream; + username = inUsername; + password = inPassword; + } + return self; +} + +- (BOOL)start:(NSError **)errPtr +{ + XMPPLogTrace(); + + // From RFC 4616 - PLAIN SASL Mechanism: + // [authzid] UTF8NUL authcid UTF8NUL passwd + // + // authzid: authorization identity + // authcid: authentication identity (username) + // passwd : password for authcid + + NSString *authUsername = username; + if (!authUsername) + { + authUsername = [xmppStream.myJID user]; + } + + NSString *payload = [NSString stringWithFormat:@"\0%@\0%@", authUsername, password]; + NSString *base64 = [[payload dataUsingEncoding:NSUTF8StringEncoding] xmpp_base64Encoded]; + + // Base-64-Info + + NSXMLElement *auth = [NSXMLElement elementWithName:@"auth" xmlns:@"urn:ietf:params:xml:ns:xmpp-sasl"]; + [auth addAttributeWithName:@"mechanism" stringValue:@"PLAIN"]; + [auth setStringValue:base64]; + + [xmppStream sendAuthElement:auth]; + + return YES; +} + +- (XMPPHandleAuthResponse)handleAuth:(NSXMLElement *)authResponse +{ + XMPPLogTrace(); + + // We're expecting a success response. + // If we get anything else we can safely assume it's the equivalent of a failure response. + + if ([[authResponse name] isEqualToString:@"success"]) + { + return XMPP_AUTH_SUCCESS; + } + else + { + return XMPP_AUTH_FAIL; + } +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPStream (XMPPPlainAuthentication) + +- (BOOL)supportsPlainAuthentication +{ + return [self supportsAuthenticationMechanism:[XMPPPlainAuthentication mechanismName]]; +} + +@end diff --git a/Authentication/SCRAM-SHA-1/XMPPSCRAMSHA1Authentication.h b/Authentication/SCRAM-SHA-1/XMPPSCRAMSHA1Authentication.h new file mode 100644 index 0000000..cf741e7 --- /dev/null +++ b/Authentication/SCRAM-SHA-1/XMPPSCRAMSHA1Authentication.h @@ -0,0 +1,21 @@ +// +// XMPPSCRAMSHA1Authentication.h +// iPhoneXMPP +// +// Created by David Chiles on 3/21/14. +// +// + +#import +#import "XMPPSASLAuthentication.h" +#import "XMPPStream.h" + +@interface XMPPSCRAMSHA1Authentication : NSObject + +@end + +@interface XMPPStream (XMPPSCRAMSHA1Authentication) + +- (BOOL)supportsSCRAMSHA1Authentication; + +@end diff --git a/Authentication/SCRAM-SHA-1/XMPPSCRAMSHA1Authentication.m b/Authentication/SCRAM-SHA-1/XMPPSCRAMSHA1Authentication.m new file mode 100644 index 0000000..d0d795b --- /dev/null +++ b/Authentication/SCRAM-SHA-1/XMPPSCRAMSHA1Authentication.m @@ -0,0 +1,342 @@ +// +// XMPPSCRAMSHA1Authentication.m +// iPhoneXMPP +// +// Created by David Chiles on 3/21/14. +// +// + +#import "XMPPSCRAMSHA1Authentication.h" +#import "XMPP.h" +#import "XMPPLogging.h" +#import "XMPPStream.h" +#import "XMPPInternal.h" +#import "NSData+XMPP.h" +#import "XMPPStringPrep.h" + +#import + + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +#if DEBUG +static const int xmppLogLevel = XMPP_LOG_LEVEL_INFO; // | XMPP_LOG_FLAG_TRACE; +#else +static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +@interface XMPPSCRAMSHA1Authentication () +{ + #if __has_feature(objc_arc_weak) + __weak XMPPStream *xmppStream; + #else + __unsafe_unretained XMPPStream *xmppStream; + #endif +} + +@property (nonatomic) BOOL awaitingChallenge; +@property (nonatomic, strong) NSString *username; +@property (nonatomic, strong) NSString *password; +@property (nonatomic, strong) NSString *clientNonce; +@property (nonatomic, strong) NSString *combinedNonce; +@property (nonatomic, strong) NSString *salt; +@property (nonatomic, strong) NSNumber *count; +@property (nonatomic, strong) NSString *serverMessage1; +@property (nonatomic, strong) NSString *clientFirstMessageBare; +@property (nonatomic, strong) NSData *serverSignatureData; +@property (nonatomic, strong) NSData *clientProofData; +@property (nonatomic) CCHmacAlgorithm hashAlgorithm; + +@end + +///////////RFC5802 http://tools.ietf.org/html/rfc5802 ////////////// + +//Channel binding not yet supported + +@implementation XMPPSCRAMSHA1Authentication + ++ (NSString *)mechanismName +{ + return @"SCRAM-SHA-1"; +} + +- (id)initWithStream:(XMPPStream *)stream password:(NSString *)password +{ + return [self initWithStream:stream username:nil password:password]; +} + +- (id)initWithStream:(XMPPStream *)stream username:(NSString *)username password:(NSString *)password +{ + if ((self = [super init])) { + xmppStream = stream; + if (username) + { + _username = username; + } + else + { + _username = [XMPPStringPrep prepNode:[xmppStream.myJID user]]; + } + _password = [XMPPStringPrep prepPassword:password]; + _hashAlgorithm = kCCHmacAlgSHA1; + } + return self; +} + +- (BOOL)start:(NSError **)errPtr +{ + XMPPLogTrace(); + + if(self.username.length || self.password.length) { + + NSXMLElement *auth = [NSXMLElement elementWithName:@"auth" xmlns:@"urn:ietf:params:xml:ns:xmpp-sasl"]; + [auth addAttributeWithName:@"mechanism" stringValue:@"SCRAM-SHA-1"]; + [auth setStringValue:[self clientMessage1]]; + + [xmppStream sendAuthElement:auth]; + self.awaitingChallenge = YES; + + return YES; + } + else { + return NO; + } +} + +- (XMPPHandleAuthResponse)handleAuth1:(NSXMLElement *)authResponse +{ + XMPPLogTrace(); + + // We're expecting a challenge response. + // If we get anything else we're going to assume it's some kind of failure response. + + if (![[authResponse name] isEqualToString:@"challenge"]) + { + return XMPP_AUTH_FAIL; + } + + NSDictionary *auth = [self dictionaryFromChallenge:authResponse]; + + NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init]; + [numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle]; + + self.combinedNonce = auth[@"r"]; + self.salt = auth[@"s"]; + self.count = [numberFormatter numberFromString:auth[@"i"]]; + + //We have all the necessary information to calculate client proof and server signature + if ([self calculateProofs]) { + NSXMLElement *response = [NSXMLElement elementWithName:@"response" xmlns:@"urn:ietf:params:xml:ns:xmpp-sasl"]; + [response setStringValue:[self clientMessage2]]; + + [xmppStream sendAuthElement:response]; + self.awaitingChallenge = NO; + + return XMPP_AUTH_CONTINUE; + } + else { + return XMPP_AUTH_FAIL; + } +} + +- (XMPPHandleAuthResponse)handleAuth2:(NSXMLElement *)authResponse +{ + XMPPLogTrace(); + + NSDictionary *auth = [self dictionaryFromChallenge:authResponse]; + + if ([[authResponse name] isEqual:@"success"]) { + NSString *receivedServerSignature = auth[@"v"]; + + if([self.serverSignatureData isEqualToData:[[receivedServerSignature dataUsingEncoding:NSUTF8StringEncoding] xmpp_base64Decoded]]){ + return XMPP_AUTH_SUCCESS; + } + else { + return XMPP_AUTH_FAIL; + } + } + else { + return XMPP_AUTH_FAIL; + } +} + +- (XMPPHandleAuthResponse)handleAuth:(NSXMLElement *)auth +{ + XMPPLogTrace(); + + if (self.awaitingChallenge) { + return [self handleAuth1:auth]; + } + else { + return [self handleAuth2:auth]; + } +} + +- (NSString *)clientMessage1 +{ + self.clientNonce = [XMPPStream generateUUID]; + + self.clientFirstMessageBare = [NSString stringWithFormat:@"n=%@,r=%@",self.username,self.clientNonce]; + + NSData *message1Data = [[NSString stringWithFormat:@"n,,%@",self.clientFirstMessageBare] dataUsingEncoding:NSUTF8StringEncoding]; + + return [message1Data xmpp_base64Encoded]; +} + +- (NSString *)clientMessage2 +{ + NSString *clientProofString = [self.clientProofData xmpp_base64Encoded]; + NSData *message2Data = [[NSString stringWithFormat:@"c=biws,r=%@,p=%@",self.combinedNonce,clientProofString] dataUsingEncoding:NSUTF8StringEncoding]; + + return [message2Data xmpp_base64Encoded]; +} + +- (BOOL)calculateProofs +{ + //Check to see that we have a password, salt and iteration count above 4096 (from RFC5802) + if (!self.password.length || !self.salt.length || self.count.unsignedIntegerValue < 4096) { + return NO; + } + + NSData *passwordData = [self.password dataUsingEncoding:NSUTF8StringEncoding]; + NSData *saltData = [[self.salt dataUsingEncoding:NSUTF8StringEncoding] xmpp_base64Decoded]; + + NSData *saltedPasswordData = [self HashWithAlgorithm:self.hashAlgorithm password:passwordData salt:saltData iterations:[self.count unsignedIntValue]]; + + NSData *clientKeyData = [self HashWithAlgorithm:self.hashAlgorithm data:[@"Client Key" dataUsingEncoding:NSUTF8StringEncoding] key:saltedPasswordData]; + NSData *serverKeyData = [self HashWithAlgorithm:self.hashAlgorithm data:[@"Server Key" dataUsingEncoding:NSUTF8StringEncoding] key:saltedPasswordData]; + + NSData *storedKeyData = [clientKeyData xmpp_sha1Digest]; + + NSData *authMessageData = [[NSString stringWithFormat:@"%@,%@,c=biws,r=%@",self.clientFirstMessageBare,self.serverMessage1,self.combinedNonce] dataUsingEncoding:NSUTF8StringEncoding]; + + NSData *clientSignatureData = [self HashWithAlgorithm:self.hashAlgorithm data:authMessageData key:storedKeyData]; + + self.serverSignatureData = [self HashWithAlgorithm:self.hashAlgorithm data:authMessageData key:serverKeyData]; + self.clientProofData = [self xorData:clientKeyData withData:clientSignatureData]; + + //check to see that we caclulated some client proof and server signature + if (self.clientProofData && self.serverSignatureData) { + return YES; + } + else { + return NO; + } +} + +- (NSData *)HashWithAlgorithm:(CCHmacAlgorithm) algorithm password:(NSData *)passwordData salt:(NSData *)saltData iterations:(NSUInteger)rounds +{ + NSMutableData *mutableSaltData = [saltData mutableCopy]; + UInt8 zeroHex= 0x00; + UInt8 oneHex= 0x01; + NSData *zeroData = [[NSData alloc] initWithBytes:&zeroHex length:sizeof(zeroHex)]; + NSData *oneData = [[NSData alloc] initWithBytes:&oneHex length:sizeof(oneHex)]; + + [mutableSaltData appendData:zeroData]; + [mutableSaltData appendData:zeroData]; + [mutableSaltData appendData:zeroData]; + [mutableSaltData appendData:oneData]; + + NSData *result = [self HashWithAlgorithm:algorithm data:mutableSaltData key:passwordData]; + NSData *previous = [result copy]; + + for (int i = 1; i < rounds; i++) { + previous = [self HashWithAlgorithm:algorithm data:previous key:passwordData]; + result = [self xorData:result withData:previous]; + } + + return result; +} + +- (NSData *)HashWithAlgorithm:(CCHmacAlgorithm) algorithm data:(NSData *)data key:(NSData *)key +{ + unsigned char cHMAC[CC_SHA1_DIGEST_LENGTH]; + + CCHmac(algorithm, [key bytes], [key length], [data bytes], [data length], cHMAC); + + return [[NSData alloc] initWithBytes:cHMAC length:sizeof(cHMAC)]; +} + +- (NSData *)xorData:(NSData *)data1 withData:(NSData *)data2 +{ + NSMutableData *result = data1.mutableCopy; + + char *dataPtr = (char *)result.mutableBytes; + + char *keyData = (char *)data2.bytes; + + char *keyPtr = keyData; + int keyIndex = 0; + + for (int x = 0; x < data1.length; x++) { + *dataPtr = *dataPtr ^ *keyPtr; + dataPtr++; + keyPtr++; + + if (++keyIndex == data2.length) { + keyIndex = 0; + keyPtr = keyData; + } + } + return result; +} + +- (NSDictionary *)dictionaryFromChallenge:(NSXMLElement *)challenge +{ + // The value of the challenge stanza is base 64 encoded. + // Once "decoded", it's just a string of key=value pairs separated by commas. + + NSData *base64Data = [[challenge stringValue] dataUsingEncoding:NSASCIIStringEncoding]; + NSData *decodedData = [base64Data xmpp_base64Decoded]; + + self.serverMessage1 = [[NSString alloc] initWithData:decodedData encoding:NSUTF8StringEncoding]; + + XMPPLogVerbose(@"%@: Decoded challenge: %@", THIS_FILE, self.serverMessage1); + + NSArray *components = [self.serverMessage1 componentsSeparatedByString:@","]; + NSMutableDictionary *auth = [NSMutableDictionary dictionaryWithCapacity:5]; + + for (NSString *component in components) + { + NSRange separator = [component rangeOfString:@"="]; + if (separator.location != NSNotFound) + { + NSMutableString *key = [[component substringToIndex:separator.location] mutableCopy]; + NSMutableString *value = [[component substringFromIndex:separator.location+1] mutableCopy]; + + if(key) CFStringTrimWhitespace((__bridge CFMutableStringRef)key); + if(value) CFStringTrimWhitespace((__bridge CFMutableStringRef)value); + + if ([value hasPrefix:@"\""] && [value hasSuffix:@"\""] && [value length] > 2) + { + // Strip quotes from value + [value deleteCharactersInRange:NSMakeRange(0, 1)]; + [value deleteCharactersInRange:NSMakeRange([value length]-1, 1)]; + } + + if(key && value) + { + auth[key] = value; + } + } + } + + return auth; +} +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPStream (XMPPSCRAMSHA1Authentication) + +- (BOOL)supportsSCRAMSHA1Authentication +{ + return [self supportsAuthenticationMechanism:[XMPPSCRAMSHA1Authentication mechanismName]]; +} + +@end diff --git a/Authentication/X-Facebook-Platform/XMPPXFacebookPlatformAuthentication.h b/Authentication/X-Facebook-Platform/XMPPXFacebookPlatformAuthentication.h new file mode 100644 index 0000000..ef0a97a --- /dev/null +++ b/Authentication/X-Facebook-Platform/XMPPXFacebookPlatformAuthentication.h @@ -0,0 +1,56 @@ +#import +#import "XMPPSASLAuthentication.h" +#import "XMPPStream.h" + + +@interface XMPPXFacebookPlatformAuthentication : NSObject + +/** + * You should use this init method (as opposed the one defined in the XMPPSASLAuthentication protocol). +**/ +- (id)initWithStream:(XMPPStream *)stream appId:(NSString *)appId accessToken:(NSString *)accessToken; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPStream (XMPPXFacebookPlatformAuthentication) + +/** + * Facebook Chat X-FACEBOOK-PLATFORM SASL authentication initialization. + * This is a convienence init method to help configure Facebook Chat. +**/ +- (id)initWithFacebookAppId:(NSString *)fbAppId; + +/** + * The appId can be passed to custom authentication classes. + * For example, the appId is used for Facebook Chat X-FACEBOOK-PLATFORM SASL authentication. +**/ +@property (readwrite, copy) NSString *facebookAppId; + +/** + * Returns whether or not the server supports X-FACEBOOK-PLATFORM authentication. + * + * This information is available after the stream is connected. + * In other words, after the delegate has received xmppStreamDidConnect: notification. +**/ +- (BOOL)supportsXFacebookPlatformAuthentication; + +/** + * This method attempts to start the facebook oauth authentication process. + * + * This method is asynchronous. + * + * If there is something immediately wrong, + * such as the stream is not connected or doesn't have a set appId or accessToken, + * the method will return NO and set the error. + * Otherwise the delegate callbacks are used to communicate auth success or failure. + * + * @see xmppStreamDidAuthenticate: + * @see xmppStream:didNotAuthenticate: + **/ +- (BOOL)authenticateWithFacebookAccessToken:(NSString *)accessToken error:(NSError **)errPtr; + +@end diff --git a/Authentication/X-Facebook-Platform/XMPPXFacebookPlatformAuthentication.m b/Authentication/X-Facebook-Platform/XMPPXFacebookPlatformAuthentication.m new file mode 100644 index 0000000..dccc80d --- /dev/null +++ b/Authentication/X-Facebook-Platform/XMPPXFacebookPlatformAuthentication.m @@ -0,0 +1,327 @@ +#import "XMPPXFacebookPlatformAuthentication.h" +#import "XMPP.h" +#import "XMPPLogging.h" +#import "XMPPInternal.h" +#import "NSData+XMPP.h" + +#import + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_INFO; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +static NSString *const XMPPFacebookChatHostName = @"chat.facebook.com"; + +static char facebookAppIdKey; + +@interface XMPPXFacebookPlatformAuthentication () +{ + #if __has_feature(objc_arc_weak) + __weak XMPPStream *xmppStream; + #else + __unsafe_unretained XMPPStream *xmppStream; + #endif + + BOOL awaitingChallenge; + + NSString *appId; + NSString *accessToken; + NSString *nonce; + NSString *method; +} + +- (NSDictionary *)dictionaryFromChallenge:(NSXMLElement *)challenge; +- (NSString *)base64EncodedFullResponse; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPXFacebookPlatformAuthentication + ++ (NSString *)mechanismName +{ + return @"X-FACEBOOK-PLATFORM"; +} + +- (id)initWithStream:(XMPPStream *)stream password:(NSString *)password +{ + if ((self = [super init])) + { + xmppStream = stream; + } + return self; +} + +- (id)initWithStream:(XMPPStream *)stream appId:(NSString *)inAppId accessToken:(NSString *)inAccessToken +{ + if ((self = [super init])) + { + xmppStream = stream; + appId = inAppId; + accessToken = inAccessToken; + } + return self; +} + +- (BOOL)start:(NSError **)errPtr +{ + if (!appId || !accessToken) + { + NSString *errMsg = @"Missing facebook appId and/or accessToken."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + NSError *err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidState userInfo:info]; + + if (errPtr) *errPtr = err; + return NO; + } + + // + + NSXMLElement *auth = [NSXMLElement elementWithName:@"auth" xmlns:@"urn:ietf:params:xml:ns:xmpp-sasl"]; + [auth addAttributeWithName:@"mechanism" stringValue:@"X-FACEBOOK-PLATFORM"]; + + [xmppStream sendAuthElement:auth]; + awaitingChallenge = YES; + + return YES; +} + +- (XMPPHandleAuthResponse)handleAuth1:(NSXMLElement *)authResponse +{ + XMPPLogTrace(); + + // We're expecting a challenge response. + // If we get anything else we're going to assume it's some kind of failure response. + + if (![[authResponse name] isEqualToString:@"challenge"]) + { + return XMPP_AUTH_FAIL; + } + + // Extract components from incoming challenge + + NSDictionary *auth = [self dictionaryFromChallenge:authResponse]; + + nonce = auth[@"nonce"]; + method = auth[@"method"]; + + // Create and send challenge response element + + NSXMLElement *response = [NSXMLElement elementWithName:@"response" xmlns:@"urn:ietf:params:xml:ns:xmpp-sasl"]; + [response setStringValue:[self base64EncodedFullResponse]]; + + [xmppStream sendAuthElement:response]; + awaitingChallenge = NO; + + return XMPP_AUTH_CONTINUE; +} + +- (XMPPHandleAuthResponse)handleAuth2:(NSXMLElement *)authResponse +{ + XMPPLogTrace(); + + // We're expecting a success response. + // If we get anything else we can safely assume it's the equivalent of a failure response. + + if ([[authResponse name] isEqualToString:@"success"]) + { + return XMPP_AUTH_SUCCESS; + } + else + { + return XMPP_AUTH_FAIL; + } +} + +- (XMPPHandleAuthResponse)handleAuth:(NSXMLElement *)authResponse +{ + if (awaitingChallenge) + { + return [self handleAuth1:authResponse]; + } + else + { + return [self handleAuth2:authResponse]; + } +} + +- (NSDictionary *)dictionaryFromChallenge:(NSXMLElement *)challenge +{ + // The value of the challenge stanza is base 64 encoded. + // Once "decoded", it's just a string of key=value pairs separated by ampersands. + + NSData *base64Data = [[challenge stringValue] dataUsingEncoding:NSASCIIStringEncoding]; + NSData *decodedData = [base64Data xmpp_base64Decoded]; + + NSString *authStr = [[NSString alloc] initWithData:decodedData encoding:NSUTF8StringEncoding]; + + XMPPLogVerbose(@"%@: decoded challenge: %@", THIS_FILE, authStr); + + NSArray *components = [authStr componentsSeparatedByString:@"&"]; + NSMutableDictionary *auth = [NSMutableDictionary dictionaryWithCapacity:3]; + + for (NSString *component in components) + { + NSRange separator = [component rangeOfString:@"="]; + if (separator.location != NSNotFound) + { + NSString *key = [[component substringToIndex:separator.location] + stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + + NSString *value = [[component substringFromIndex:separator.location+1] + stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + + if ([value hasPrefix:@"\""] && [value hasSuffix:@"\""] && [value length] > 2) + { + // Strip quotes from value + value = [value substringWithRange:NSMakeRange(1,([value length]-2))]; + } + + auth[key] = value; + } + } + + return auth; +} + +- (NSString *)base64EncodedFullResponse +{ + if (!appId || !accessToken || !method || !nonce) + { + return nil; + } + + srand([[NSDate date] timeIntervalSince1970]); + + NSMutableString *buffer = [NSMutableString stringWithCapacity:250]; + [buffer appendFormat:@"method=%@&", method]; + [buffer appendFormat:@"nonce=%@&", nonce]; + [buffer appendFormat:@"access_token=%@&", accessToken]; + [buffer appendFormat:@"api_key=%@&", appId]; + [buffer appendFormat:@"call_id=%d&", rand()]; + [buffer appendFormat:@"v=%@",@"1.0"]; + + XMPPLogVerbose(@"XMPPXFacebookPlatformAuthentication: response for facebook: %@", buffer); + + NSData *utf8data = [buffer dataUsingEncoding:NSUTF8StringEncoding]; + + return [utf8data xmpp_base64Encoded]; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPStream (XMPPXFacebookPlatformAuthentication) + +- (id)initWithFacebookAppId:(NSString *)fbAppId +{ + if ((self = [self init])) // Note: Using [self init], NOT [super init] + { + self.facebookAppId = fbAppId; + self.myJID = [XMPPJID jidWithString:XMPPFacebookChatHostName]; + + // As of October 8, 2011, Facebook doesn't have their XMPP SRV records set. + // And, as per the XMPP specification, we MUST check the XMPP SRV records for an IP address, + // before falling back to a traditional A record lookup. + // + // So we're setting the hostname as a minor optimization to avoid the SRV timeout delay. + + self.hostName = XMPPFacebookChatHostName; + } + return self; +} + +- (NSString *)facebookAppId +{ + __block NSString *result = nil; + + dispatch_block_t block = ^{ + result = objc_getAssociatedObject(self, &facebookAppIdKey); + }; + + if (dispatch_get_specific(self.xmppQueueTag)) + block(); + else + dispatch_sync(self.xmppQueue, block); + + return result; +} + +- (void)setFacebookAppId:(NSString *)inFacebookAppId +{ + NSString *newFacebookAppId = [inFacebookAppId copy]; + + dispatch_block_t block = ^{ + objc_setAssociatedObject(self, &facebookAppIdKey, newFacebookAppId, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + }; + + if (dispatch_get_specific(self.xmppQueueTag)) + block(); + else + dispatch_async(self.xmppQueue, block); +} + +- (BOOL)supportsXFacebookPlatformAuthentication +{ + return [self supportsAuthenticationMechanism:[XMPPXFacebookPlatformAuthentication mechanismName]]; +} + +/** + * This method attempts to connect to the Facebook Chat servers + * using the Facebook OAuth token returned by the Facebook OAuth 2.0 authentication process. +**/ +- (BOOL)authenticateWithFacebookAccessToken:(NSString *)accessToken error:(NSError **)errPtr +{ + XMPPLogTrace(); + + __block BOOL result = YES; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if ([self supportsXFacebookPlatformAuthentication]) + { + XMPPXFacebookPlatformAuthentication *facebookAuth = + [[XMPPXFacebookPlatformAuthentication alloc] initWithStream:self + appId:self.facebookAppId + accessToken:accessToken]; + + result = [self authenticate:facebookAuth error:&err]; + } + else + { + NSString *errMsg = @"The server does not support X-FACEBOOK-PLATFORM authentication."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamUnsupportedAction userInfo:info]; + + result = NO; + } + }}; + + if (dispatch_get_specific(self.xmppQueueTag)) + block(); + else + dispatch_sync(self.xmppQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +@end diff --git a/Authentication/X-OAuth2-Google/XMPPXOAuth2Google.h b/Authentication/X-OAuth2-Google/XMPPXOAuth2Google.h new file mode 100644 index 0000000..4720206 --- /dev/null +++ b/Authentication/X-OAuth2-Google/XMPPXOAuth2Google.h @@ -0,0 +1,28 @@ +// +// XMPPXOAuth2Google.h +// Off the Record +// +// Created by David Chiles on 9/13/13. +// Copyright (c) 2013 Chris Ballinger. All rights reserved. +// + +#import +#import "XMPPSASLAuthentication.h" +#import "XMPPStream.h" + +@interface XMPPXOAuth2Google : NSObject + +-(id)initWithStream:(XMPPStream *)stream accessToken:(NSString *)accessToken; + +@end + + + +@interface XMPPStream (XMPPXOAuth2Google) + + +- (BOOL)supportsXOAuth2GoogleAuthentication; + +- (BOOL)authenticateWithGoogleAccessToken:(NSString *)accessToken error:(NSError **)errPtr; + +@end diff --git a/Authentication/X-OAuth2-Google/XMPPXOAuth2Google.m b/Authentication/X-OAuth2-Google/XMPPXOAuth2Google.m new file mode 100644 index 0000000..e07c361 --- /dev/null +++ b/Authentication/X-OAuth2-Google/XMPPXOAuth2Google.m @@ -0,0 +1,180 @@ +// +// XMPPXOAuth2Google.m +// Off the Record +// +// Created by David Chiles on 9/13/13. +// Copyright (c) 2013 Chris Ballinger. All rights reserved. +// + +#import "XMPPXOAuth2Google.h" +#import "XMPP.h" +#import "XMPPLogging.h" +#import "XMPPInternal.h" +#import "NSData+XMPP.h" + +#import + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +#if DEBUG +static const int xmppLogLevel = XMPP_LOG_LEVEL_INFO; // | XMPP_LOG_FLAG_TRACE; +#else +static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +static NSString *const XMPPGoogleTalkHostName = @"talk.google.com"; + +@interface XMPPXOAuth2Google () +{ +#if __has_feature(objc_arc_weak) + __weak XMPPStream *xmppStream; +#else + __unsafe_unretained XMPPStream *xmppStream; +#endif + + //BOOL awaitingChallenge; + + //NSString *appId; + NSString *accessToken; + //NSString *nonce; + //NSString *method; +} + + + +@end + +@implementation XMPPXOAuth2Google + ++ (NSString *)mechanismName +{ + return @"X-OAUTH2"; +} + +- (id)initWithStream:(XMPPStream *)stream password:(NSString *)password +{ + if ((self = [super init])) + { + xmppStream = stream; + xmppStream.hostName = XMPPGoogleTalkHostName; + } + return self; +} + +-(id)initWithStream:(XMPPStream *)stream accessToken:(NSString *)inAccessToken +{ + if (self = [super init]) { + xmppStream = stream; + accessToken = inAccessToken; + } + return self; +} + +- (BOOL)start:(NSError **)errPtr +{ + if (!accessToken) + { + NSString *errMsg = @"Missing facebook accessToken."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + NSError *err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidState userInfo:info]; + + if (errPtr) *errPtr = err; + return NO; + } + XMPPLogTrace(); + + // From RFC 4616 - PLAIN SASL Mechanism: + // [authzid] UTF8NUL authcid UTF8NUL passwd + // + // authzid: authorization identity + // authcid: authentication identity (username) + // passwd : password for authcid + + NSString *username = [xmppStream.myJID user]; + + NSString *payload = [NSString stringWithFormat:@"\0%@\0%@", username, accessToken]; + NSString *base64 = [[payload dataUsingEncoding:NSUTF8StringEncoding] xmpp_base64Encoded]; + + // Base-64-Info + + NSXMLElement *auth = [NSXMLElement elementWithName:@"auth" xmlns:@"urn:ietf:params:xml:ns:xmpp-sasl"]; + [auth addAttributeWithName:@"mechanism" stringValue:@"X-OAUTH2"]; + [auth addAttributeWithName:@"auth:service" stringValue:@"oauth2"]; + [auth addAttributeWithName:@"xmlns:auth" stringValue:@"http://www.google.com/talk/protocol/auth"]; + [auth setStringValue:base64]; + + [xmppStream sendAuthElement:auth]; + + return YES; +} + +- (XMPPHandleAuthResponse)handleAuth:(NSXMLElement *)authResponse +{ + XMPPLogTrace(); + + // We're expecting a success response. + // If we get anything else we can safely assume it's the equivalent of a failure response. + + if ([[authResponse name] isEqualToString:@"success"]) + { + return XMPP_AUTH_SUCCESS; + } + else + { + return XMPP_AUTH_FAIL; + } +} +@end + +@implementation XMPPStream (XMPPXOAuth2Google) + + + +- (BOOL)supportsXOAuth2GoogleAuthentication +{ + return [self supportsAuthenticationMechanism:[XMPPXOAuth2Google mechanismName]]; +} + +- (BOOL)authenticateWithGoogleAccessToken:(NSString *)accessToken error:(NSError **)errPtr +{ + XMPPLogTrace(); + + __block BOOL result = YES; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if ([self supportsXOAuth2GoogleAuthentication]) + { + XMPPXOAuth2Google * googleAuth = [[XMPPXOAuth2Google alloc] initWithStream:self + accessToken:accessToken]; + + result = [self authenticate:googleAuth error:&err]; + } + else + { + NSString *errMsg = @"The server does not support X-OATH2-GOOGLE authentication."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamUnsupportedAction userInfo:info]; + + result = NO; + } + }}; + + if (dispatch_get_specific(self.xmppQueueTag)) + block(); + else + dispatch_sync(self.xmppQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +@end diff --git a/Authentication/XMPPCustomBinding.h b/Authentication/XMPPCustomBinding.h new file mode 100644 index 0000000..fa83485 --- /dev/null +++ b/Authentication/XMPPCustomBinding.h @@ -0,0 +1,93 @@ +#import +#if TARGET_OS_IPHONE + #import "DDXML.h" +#endif + +typedef NS_ENUM(NSInteger, XMPPBindResult) { + + XMPP_BIND_CONTINUE, // The custom binding process is still ongoing. + + XMPP_BIND_SUCCESS, // Custom binding succeeded. + // The stream should continue normal post-binding operation. + + XMPP_BIND_FAIL_FALLBACK, // Custom binding failed. + // The stream should fallback to the standard binding protocol. + + XMPP_BIND_FAIL_ABORT // Custom binding failed. + // The stream must abort the binding process. + // Further, because the stream is in a bad state (authenticated, but + // unable to complete the full handshake) it must immediately disconnect. + // The given NSError will be reported via xmppStreamDidDisconnect:withError: +}; + +/** + * Binding a JID resource is a standard part of the authentication process, + * and occurs after SASL authentication completes (which generally authenticates the JID username). + * + * This protocol may be used if there is a need to customize the binding process. + * For example: + * + * - Custom SASL authentication scheme required both username & resource + * - Custom SASL authentication scheme provided required resource in server response + * - Stream Management (XEP-0198) replaces binding with resumption from previously bound session + * + * A custom binding procedure may be plugged into an XMPPStream instance via the delegate method: + * - (id )xmppStreamWillBind; +**/ +@protocol XMPPCustomBinding +@required + +/** + * Attempts to start the custom binding process. + * + * If it isn't possible to start the process (perhaps due to missing information), + * this method should return XMPP_BIND_FAIL_FALLBACK or XMPP_BIND_FAIL_ABORT. + * + * (The error message is only used by xmppStream if this method returns XMPP_BIND_FAIL_ABORT.) + * + * If binding isn't needed (for example, because custom SASL authentication already handled it), + * this method should return XMPP_BIND_SUCCESS. + * In this case, xmppStream will immediately move to its post-binding operations. + * + * Otherwise this method should send whatever stanzas are needed to begin the binding process. + * And then return XMPP_BIND_CONTINUE. + * + * This method is called by automatically XMPPStream. + * You MUST NOT invoke this method manually. +**/ +- (XMPPBindResult)start:(NSError **)errPtr; + +/** + * After the custom binding process has started, all incoming xmpp stanzas are routed to this method. + * The method should process the stanza as appropriate, and return the coresponding result. + * If the process is not yet complete, it should return XMPP_BIND_CONTINUE, + * meaning the xmpp stream will continue to forward all incoming xmpp stanzas to this method. + * + * This method is called automatically by XMPPStream. + * You MUST NOT invoke this method manually. +**/ +- (XMPPBindResult)handleBind:(NSXMLElement *)auth withError:(NSError **)errPtr; + +@optional + +/** + * Optionally implement this method to override the default behavior. + * By default behavior, we mean the behavior normally taken by xmppStream, which is: + * + * - IF the server includes in its stream:features + * - AND xmppStream.skipStartSession property is NOT set + * - THEN xmppStream will send the session start request, and await the response before transitioning to authenticated + * + * Thus if you implement this method and return YES, then xmppStream will skip starting a session, + * regardless of the stream:features and the current xmppStream.skipStartSession property value. + * + * If you implement this method and return NO, then xmppStream will follow the default behavior detailed above. + * This means that, even if this method returns NO, the xmppStream may still skip starting a session if + * the server doesn't require it via its stream:features, + * or if the user has explicitly forbidden it via the xmppStream.skipStartSession property. + * + * The default value is NO. +**/ +- (BOOL)shouldSkipStartSessionAfterSuccessfulBinding; + +@end diff --git a/Authentication/XMPPSASLAuthentication.h b/Authentication/XMPPSASLAuthentication.h new file mode 100644 index 0000000..fb78872 --- /dev/null +++ b/Authentication/XMPPSASLAuthentication.h @@ -0,0 +1,102 @@ +#import +#if TARGET_OS_IPHONE + #import "DDXML.h" +#endif + +@class XMPPStream; + + +typedef NS_ENUM(NSInteger, XMPPHandleAuthResponse) { + + XMPP_AUTH_FAIL, // Authentication failed. + // The delegate will be informed via xmppStream:didNotAuthenticate: + + XMPP_AUTH_SUCCESS, // Authentication succeeded. + // The delegate will be informed via xmppStreamDidAuthenticate: + + XMPP_AUTH_CONTINUE, // The authentication process is still ongoing. +}; + + +@protocol XMPPSASLAuthentication +@required + +/** + * Returns the associated mechanism name. + * + * An xmpp server sends a list of supported authentication mechanisms during the xmpp handshake. + * The list looks something like this: + * + * + * + * DIGEST-MD5 + * X-FACEBOOK-PLATFORM + * X-YOUR-CUSTOM-AUTH-SCHEME + * + * + * + * The mechanismName returned should match the value inside the HERE. +**/ ++ (NSString *)mechanismName; + +/** + * Standard init method. + * + * The XMPPStream class natively supports the standard authentication scheme (auth with password). + * If that method is used, then xmppStream will automatically create an authentication instance via this method. + * Which authentication class it chooses is based on the configured authentication priorities, + * and the auth mechanisms supported by the server. + * + * Not all authentication mechanisms will use this init method. + * For example: + * - they require an appId and authToken + * - they require a userName (not related to JID), privilegeLevel, and password + * - they require an eyeScan and voiceFingerprint + * + * In this case, the authentication mechanism class should provide it's own custom init method. + * However it should still implement this method, and then use the start method to notify of errors. +**/ +- (id)initWithStream:(XMPPStream *)stream password:(NSString *)password; + + +/** + * Attempts to start the authentication process. + * The auth mechanism should send whatever stanzas are needed to begin the authentication process. + * + * If it isn't possible to start the authentication process (perhaps due to missing information), + * this method should return NO and set an appropriate error message. + * For example: "X-Custom-Platform authentication requires authToken" + * Otherwise this method should return YES. + * + * This method is called by automatically XMPPStream (via the authenticate: method). + * You should NOT invoke this method manually. +**/ +- (BOOL)start:(NSError **)errPtr; + +/** + * After the authentication process has started, all incoming xmpp stanzas are routed to this method. + * The authentication mechanism should process the stanza as appropriate, and return the coresponding result. + * If the authentication is not yet complete, it should return XMPP_AUTH_CONTINUE, + * meaning the xmpp stream will continue to forward all incoming xmpp stanzas to this method. + * + * This method is called automatically by XMPPStream (via the authenticate: method). + * You should NOT invoke this method manually. +**/ +- (XMPPHandleAuthResponse)handleAuth:(NSXMLElement *)auth; + +@optional + +/** + * Use this init method if the username used for authentication does not match the user part of the JID. + * If username is nil, the user part of the JID will be used. + * The standard init method uses this init method, passing nil for the username. + **/ +- (id)initWithStream:(XMPPStream *)stream username:(NSString *)username password:(NSString *)password; + +/** + * Optionally implement this method to override the default behavior. + * The default value is YES. +**/ +- (BOOL)shouldResendOpeningNegotiationAfterSuccessfulAuthentication; + +@end diff --git a/Categories/NSData+XMPP.h b/Categories/NSData+XMPP.h new file mode 100644 index 0000000..593abdc --- /dev/null +++ b/Categories/NSData+XMPP.h @@ -0,0 +1,34 @@ +#import + +@interface NSData (XMPP) + +- (NSData *)xmpp_md5Digest; + +- (NSData *)xmpp_sha1Digest; + +- (NSString *)xmpp_hexStringValue; + +- (NSString *)xmpp_base64Encoded; +- (NSData *)xmpp_base64Decoded; + +- (BOOL)xmpp_isJPEG; +- (BOOL)xmpp_isPNG; +- (NSString *)xmpp_imageType; + +@end + +#ifndef XMPP_EXCLUDE_DEPRECATED + +#define XMPP_DEPRECATED($message) __attribute__((deprecated($message))) + +@interface NSData (XMPPDeprecated) +- (NSData *)md5Digest XMPP_DEPRECATED("Use -xmpp_md5Digest"); +- (NSData *)sha1Digest XMPP_DEPRECATED("Use -xmpp_sha1Digest"); +- (NSString *)hexStringValue XMPP_DEPRECATED("Use -xmpp_hexStringValue"); +- (NSString *)base64Encoded XMPP_DEPRECATED("Use -xmpp_base64Encoded"); +- (NSData *)base64Decoded XMPP_DEPRECATED("Use -xmpp_base64Decoded"); +@end + +#undef XMPP_DEPRECATED + +#endif diff --git a/Categories/NSData+XMPP.m b/Categories/NSData+XMPP.m new file mode 100644 index 0000000..18a3eb9 --- /dev/null +++ b/Categories/NSData+XMPP.m @@ -0,0 +1,240 @@ +#import "NSData+XMPP.h" +#import + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@implementation NSData (XMPP) + +static char encodingTable[64] = { + 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P', + 'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f', + 'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v', + 'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/' }; + + +- (NSData *)xmpp_md5Digest +{ + unsigned char result[CC_MD5_DIGEST_LENGTH]; + + CC_MD5([self bytes], (CC_LONG)[self length], result); + return [NSData dataWithBytes:result length:CC_MD5_DIGEST_LENGTH]; +} + +- (NSData *)xmpp_sha1Digest +{ + unsigned char result[CC_SHA1_DIGEST_LENGTH]; + + CC_SHA1([self bytes], (CC_LONG)[self length], result); + return [NSData dataWithBytes:result length:CC_SHA1_DIGEST_LENGTH]; +} + +- (NSString *)xmpp_hexStringValue +{ + NSMutableString *stringBuffer = [NSMutableString stringWithCapacity:([self length] * 2)]; + + const unsigned char *dataBuffer = [self bytes]; + int i; + + for (i = 0; i < [self length]; ++i) + { + [stringBuffer appendFormat:@"%02x", (unsigned int)dataBuffer[i]]; + } + + return [stringBuffer copy]; +} + +- (NSString *)xmpp_base64Encoded +{ + const unsigned char *bytes = [self bytes]; + NSMutableString *result = [NSMutableString stringWithCapacity:[self length]]; + unsigned long ixtext = 0; + unsigned long lentext = [self length]; + long ctremaining = 0; + unsigned char inbuf[3], outbuf[4]; + unsigned short i = 0; + unsigned short charsonline = 0, ctcopy = 0; + unsigned long ix = 0; + + while( YES ) + { + ctremaining = lentext - ixtext; + if( ctremaining <= 0 ) break; + + for( i = 0; i < 3; i++ ) { + ix = ixtext + i; + if( ix < lentext ) inbuf[i] = bytes[ix]; + else inbuf [i] = 0; + } + + outbuf [0] = (inbuf [0] & 0xFC) >> 2; + outbuf [1] = ((inbuf [0] & 0x03) << 4) | ((inbuf [1] & 0xF0) >> 4); + outbuf [2] = ((inbuf [1] & 0x0F) << 2) | ((inbuf [2] & 0xC0) >> 6); + outbuf [3] = inbuf [2] & 0x3F; + ctcopy = 4; + + switch( ctremaining ) + { + case 1: + ctcopy = 2; + break; + case 2: + ctcopy = 3; + break; + } + + for( i = 0; i < ctcopy; i++ ) + [result appendFormat:@"%c", encodingTable[outbuf[i]]]; + + for( i = ctcopy; i < 4; i++ ) + [result appendString:@"="]; + + ixtext += 3; + charsonline += 4; + } + + return [NSString stringWithString:result]; +} + +- (NSData *)xmpp_base64Decoded +{ + const unsigned char *bytes = [self bytes]; + NSMutableData *result = [NSMutableData dataWithCapacity:[self length]]; + + unsigned long ixtext = 0; + unsigned long lentext = [self length]; + unsigned char ch = 0; + unsigned char inbuf[4] = {0, 0, 0, 0}; + unsigned char outbuf[3] = {0, 0, 0}; + short i = 0, ixinbuf = 0; + BOOL flignore = NO; + BOOL flendtext = NO; + + while( YES ) + { + if( ixtext >= lentext ) break; + ch = bytes[ixtext++]; + flignore = NO; + + if( ( ch >= 'A' ) && ( ch <= 'Z' ) ) ch = ch - 'A'; + else if( ( ch >= 'a' ) && ( ch <= 'z' ) ) ch = ch - 'a' + 26; + else if( ( ch >= '0' ) && ( ch <= '9' ) ) ch = ch - '0' + 52; + else if( ch == '+' ) ch = 62; + else if( ch == '=' ) flendtext = YES; + else if( ch == '/' ) ch = 63; + else flignore = YES; + + if( ! flignore ) + { + short ctcharsinbuf = 3; + BOOL flbreak = NO; + + if( flendtext ) + { + if( ! ixinbuf ) break; + if( ( ixinbuf == 1 ) || ( ixinbuf == 2 ) ) ctcharsinbuf = 1; + else ctcharsinbuf = 2; + ixinbuf = 3; + flbreak = YES; + } + + inbuf [ixinbuf++] = ch; + + if( ixinbuf == 4 ) + { + ixinbuf = 0; + outbuf [0] = ( inbuf[0] << 2 ) | ( ( inbuf[1] & 0x30) >> 4 ); + outbuf [1] = ( ( inbuf[1] & 0x0F ) << 4 ) | ( ( inbuf[2] & 0x3C ) >> 2 ); + outbuf [2] = ( ( inbuf[2] & 0x03 ) << 6 ) | ( inbuf[3] & 0x3F ); + + for( i = 0; i < ctcharsinbuf; i++ ) + [result appendBytes:&outbuf[i] length:1]; + } + + if( flbreak ) break; + } + } + + return [NSData dataWithData:result]; +} + + +- (BOOL)xmpp_isJPEG +{ + if (self.length > 4) + { + unsigned char buffer[4]; + [self getBytes:&buffer length:4]; + + return buffer[0]==0xff && + buffer[1]==0xd8 && + buffer[2]==0xff && + buffer[3]==0xe0; + } + + return NO; +} + +- (BOOL)xmpp_isPNG +{ + if (self.length > 4) + { + unsigned char buffer[4]; + [self getBytes:&buffer length:4]; + + return buffer[0]==0x89 && + buffer[1]==0x50 && + buffer[2]==0x4e && + buffer[3]==0x47; + } + + return NO; +} + +- (NSString *)xmpp_imageType +{ + NSString *result = nil; + + if([self xmpp_isPNG]) + { + result = @"image/png"; + } + else if([self xmpp_isJPEG]) + { + result = @"image/jpeg"; + } + + return result; +} + +@end + +#ifndef XMPP_EXCLUDE_DEPRECATED + +@implementation NSData (XMPPDeprecated) + +- (NSData *)md5Digest { + return [self xmpp_md5Digest]; +} + +- (NSData *)sha1Digest { + return [self xmpp_sha1Digest]; +} + +- (NSString *)hexStringValue { + return [self xmpp_hexStringValue]; +} + +- (NSString *)base64Encoded { + return [self xmpp_base64Encoded]; +} + +- (NSData *)base64Decoded { + return [self xmpp_base64Decoded]; +} + +@end + +#endif diff --git a/Categories/NSNumber+XMPP.h b/Categories/NSNumber+XMPP.h new file mode 100644 index 0000000..2c4cff6 --- /dev/null +++ b/Categories/NSNumber+XMPP.h @@ -0,0 +1,46 @@ +#import + + +@interface NSNumber (XMPP) + ++ (NSNumber *)xmpp_numberWithPtr:(const void *)ptr; +- (id)xmpp_initWithPtr:(const void *)ptr __attribute__((objc_method_family(init))); + ++ (BOOL)xmpp_parseString:(NSString *)str intoInt32:(int32_t *)pNum; ++ (BOOL)xmpp_parseString:(NSString *)str intoUInt32:(uint32_t *)pNum; + ++ (BOOL)xmpp_parseString:(NSString *)str intoInt64:(int64_t *)pNum; ++ (BOOL)xmpp_parseString:(NSString *)str intoUInt64:(uint64_t *)pNum; + ++ (BOOL)xmpp_parseString:(NSString *)str intoNSInteger:(NSInteger *)pNum; ++ (BOOL)xmpp_parseString:(NSString *)str intoNSUInteger:(NSUInteger *)pNum; + ++ (UInt8)xmpp_extractUInt8FromData:(NSData *)data atOffset:(unsigned int)offset; + ++ (UInt16)xmpp_extractUInt16FromData:(NSData *)data atOffset:(unsigned int)offset andConvertFromNetworkOrder:(BOOL)flag; + ++ (UInt32)xmpp_extractUInt32FromData:(NSData *)data atOffset:(unsigned int)offset andConvertFromNetworkOrder:(BOOL)flag; + +@end + +#ifndef XMPP_EXCLUDE_DEPRECATED + +#define XMPP_DEPRECATED($message) __attribute__((deprecated($message))) + +@interface NSNumber (XMPPDeprecated) ++ (NSNumber *)numberWithPtr:(const void *)ptr XMPP_DEPRECATED("Use +xmpp_numberWithPtr:"); +- (id)initWithPtr:(const void *)ptr XMPP_DEPRECATED("Use -xmpp_initWithPtr:"); ++ (BOOL)parseString:(NSString *)str intoInt32:(int32_t *)pNum XMPP_DEPRECATED("Use +xmpp_parseString:intoInt32:"); ++ (BOOL)parseString:(NSString *)str intoUInt32:(uint32_t *)pNum XMPP_DEPRECATED("Use +xmpp_parseString:intoUInt32:"); ++ (BOOL)parseString:(NSString *)str intoInt64:(int64_t *)pNum XMPP_DEPRECATED("Use +xmpp_parseString:intoInt64:"); ++ (BOOL)parseString:(NSString *)str intoUInt64:(uint64_t *)pNum XMPP_DEPRECATED("Use +xmpp_parseString:intoUInt64:"); ++ (BOOL)parseString:(NSString *)str intoNSInteger:(NSInteger *)pNum XMPP_DEPRECATED("Use +xmpp_parseString:intoNSInteger:"); ++ (BOOL)parseString:(NSString *)str intoNSUInteger:(NSUInteger *)pNum XMPP_DEPRECATED("Use +xmpp_parseString:intoNSUInteger:"); ++ (UInt8)extractUInt8FromData:(NSData *)data atOffset:(unsigned int)offset XMPP_DEPRECATED("Use +xmpp_extractUInt8FromData:atOffset:"); ++ (UInt16)extractUInt16FromData:(NSData *)data atOffset:(unsigned int)offset andConvertFromNetworkOrder:(BOOL)flag XMPP_DEPRECATED("Use +xmpp_extractUInt16FromData:atOffset:andConvertFromNetworkOrder:"); ++ (UInt32)extractUInt32FromData:(NSData *)data atOffset:(unsigned int)offset andConvertFromNetworkOrder:(BOOL)flag XMPP_DEPRECATED("Use +xmpp_extractUInt32FromData:atOffset:andConvertFromNetworkOrder:"); +@end + +#undef XMPP_DEPRECATED + +#endif diff --git a/Categories/NSNumber+XMPP.m b/Categories/NSNumber+XMPP.m new file mode 100644 index 0000000..01bdb91 --- /dev/null +++ b/Categories/NSNumber+XMPP.m @@ -0,0 +1,265 @@ +#import "NSNumber+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@implementation NSNumber (XMPP) + ++ (NSNumber *)xmpp_numberWithPtr:(const void *)ptr +{ + return [[NSNumber alloc] xmpp_initWithPtr:ptr]; +} + +- (id)xmpp_initWithPtr:(const void *)ptr +{ + return [self initWithLong:(long)ptr]; +} + ++ (BOOL)xmpp_parseString:(NSString *)str intoInt32:(int32_t *)pNum +{ + if (str == nil) + { + *pNum = (int32_t)0; + return NO; + } + + errno = 0; + + long result = strtol([str UTF8String], NULL, 10); + + if (LONG_BIT != 32) + { + if (result > INT32_MAX) + { + *pNum = INT32_MAX; + return NO; + } + if (result < INT32_MIN) + { + *pNum = INT32_MIN; + return NO; + } + } + + // From the manpage: + // + // If no conversion could be performed, 0 is returned and the global variable errno is set to EINVAL. + // If an overflow or underflow occurs, errno is set to ERANGE and the function return value is clamped. + // + // Clamped means it will be TYPE_MAX or TYPE_MIN. + // If overflow/underflow occurs, returning a clamped value is more accurate then returning zero. + + *pNum = (int32_t)result; + + if (errno != 0) + return NO; + else + return YES; +} + ++ (BOOL)xmpp_parseString:(NSString *)str intoUInt32:(uint32_t *)pNum +{ + if (str == nil) + { + *pNum = (uint32_t)0; + return NO; + } + + errno = 0; + + unsigned long result = strtoul([str UTF8String], NULL, 10); + + if (LONG_BIT != 32) + { + if (result > UINT32_MAX) + { + *pNum = UINT32_MAX; + return NO; + } + } + + // From the manpage: + // + // If no conversion could be performed, 0 is returned and the global variable errno is set to EINVAL. + // If an overflow or underflow occurs, errno is set to ERANGE and the function return value is clamped. + // + // Clamped means it will be TYPE_MAX or TYPE_MIN. + // If overflow/underflow occurs, returning a clamped value is more accurate then returning zero. + + *pNum = (uint32_t)result; + + if (errno != 0) + return NO; + else + return YES; +} + ++ (BOOL)xmpp_parseString:(NSString *)str intoInt64:(int64_t *)pNum +{ + if (str == nil) + { + *pNum = (int64_t)0; + return NO; + } + + errno = 0; + + // On both 32-bit and 64-bit machines, long long = 64 bit + + *pNum = strtoll([str UTF8String], NULL, 10); + + // From the manpage: + // + // If no conversion could be performed, 0 is returned and the global variable errno is set to EINVAL. + // If an overflow or underflow occurs, errno is set to ERANGE and the function return value is clamped. + // + // Clamped means it will be TYPE_MAX or TYPE_MIN. + // If overflow/underflow occurs, returning a clamped value is more accurate then returning zero. + + if (errno != 0) + return NO; + else + return YES; +} + ++ (BOOL)xmpp_parseString:(NSString *)str intoUInt64:(uint64_t *)pNum +{ + if (str == nil) + { + *pNum = (uint64_t)0; + return NO; + } + + errno = 0; + + // On both 32-bit and 64-bit machines, unsigned long long = 64 bit + + *pNum = strtoull([str UTF8String], NULL, 10); + + // From the manpage: + // + // If no conversion could be performed, 0 is returned and the global variable errno is set to EINVAL. + // If an overflow or underflow occurs, errno is set to ERANGE and the function return value is clamped. + // + // Clamped means it will be TYPE_MAX or TYPE_MIN. + // If overflow/underflow occurs, returning a clamped value is more accurate then returning zero. + + if (errno != 0) + return NO; + else + return YES; +} + ++ (BOOL)xmpp_parseString:(NSString *)str intoNSInteger:(NSInteger *)pNum +{ + if (NSIntegerMax == INT32_MAX) + return [self xmpp_parseString:str intoInt32:(int32_t *)pNum]; + else + return [self xmpp_parseString:str intoInt64:(int64_t *)pNum]; +} + ++ (BOOL)xmpp_parseString:(NSString *)str intoNSUInteger:(NSUInteger *)pNum +{ + if (NSUIntegerMax == UINT32_MAX) + return [self xmpp_parseString:str intoUInt32:(uint32_t *)pNum]; + else + return [self xmpp_parseString:str intoUInt64:(uint64_t *)pNum]; +} + ++ (UInt8)xmpp_extractUInt8FromData:(NSData *)data atOffset:(unsigned int)offset +{ + // 8 bits = 1 byte + + if([data length] < offset + 1) return 0; + + UInt8 *pResult = (UInt8 *)([data bytes] + offset); + UInt8 result = *pResult; + + return result; +} + ++ (UInt16)xmpp_extractUInt16FromData:(NSData *)data atOffset:(unsigned int)offset andConvertFromNetworkOrder:(BOOL)flag +{ + // 16 bits = 2 bytes + + if([data length] < offset + 2) return 0; + + UInt16 *pResult = (UInt16 *)([data bytes] + offset); + UInt16 result = *pResult; + + if(flag) + return ntohs(result); + else + return result; +} + ++ (UInt32)xmpp_extractUInt32FromData:(NSData *)data atOffset:(unsigned int)offset andConvertFromNetworkOrder:(BOOL)flag +{ + // 32 bits = 4 bytes + + if([data length] < offset + 4) return 0; + + UInt32 *pResult = (UInt32 *)([data bytes] + offset); + UInt32 result = *pResult; + + if(flag) + return ntohl(result); + else + return result; +} + +@end + +#ifndef XMPP_EXCLUDE_DEPRECATED + +@implementation NSNumber (XMPPDeprecated) + ++ (NSNumber *)numberWithPtr:(const void *)ptr { + return [self xmpp_numberWithPtr:ptr]; +} + +- (id)initWithPtr:(const void *)ptr { + return [self xmpp_initWithPtr:ptr]; +} + ++ (BOOL)parseString:(NSString *)str intoInt32:(int32_t *)pNum { + return [self xmpp_parseString:str intoInt32:pNum]; +} + ++ (BOOL)parseString:(NSString *)str intoUInt32:(uint32_t *)pNum { + return [self xmpp_parseString:str intoUInt32:pNum]; +} + ++ (BOOL)parseString:(NSString *)str intoInt64:(int64_t *)pNum { + return [self xmpp_parseString:str intoInt64:pNum]; +} + ++ (BOOL)parseString:(NSString *)str intoUInt64:(uint64_t *)pNum { + return [self xmpp_parseString:str intoUInt64:pNum]; +} + ++ (BOOL)parseString:(NSString *)str intoNSInteger:(NSInteger *)pNum { + return [self xmpp_parseString:str intoNSInteger:pNum]; +} + ++ (BOOL)parseString:(NSString *)str intoNSUInteger:(NSUInteger *)pNum { + return [self xmpp_parseString:str intoNSUInteger:pNum]; +} + ++ (UInt8)extractUInt8FromData:(NSData *)data atOffset:(unsigned int)offset { + return [self xmpp_extractUInt8FromData:data atOffset:offset]; +} + ++ (UInt16)extractUInt16FromData:(NSData *)data atOffset:(unsigned int)offset andConvertFromNetworkOrder:(BOOL)flag { + return [self xmpp_extractUInt16FromData:data atOffset:offset andConvertFromNetworkOrder:flag]; +} + ++ (UInt32)extractUInt32FromData:(NSData *)data atOffset:(unsigned int)offset andConvertFromNetworkOrder:(BOOL)flag { + return [self xmpp_extractUInt32FromData:data atOffset:offset andConvertFromNetworkOrder:flag]; +} + +@end + +#endif diff --git a/Categories/NSXMLElement+XMPP.h b/Categories/NSXMLElement+XMPP.h new file mode 100644 index 0000000..addf89a --- /dev/null +++ b/Categories/NSXMLElement+XMPP.h @@ -0,0 +1,157 @@ +#import + +#if TARGET_OS_IPHONE + #import "DDXML.h" +#endif + + +@interface NSXMLElement (XMPP) + +/** + * Convenience methods for Creating elements. +**/ + ++ (NSXMLElement *)elementWithName:(NSString *)name numberValue:(NSNumber *)number; +- (id)initWithName:(NSString *)name numberValue:(NSNumber *)number; + ++ (NSXMLElement *)elementWithName:(NSString *)name objectValue:(id)objectValue; +- (id)initWithName:(NSString *)name objectValue:(id)objectValue; + +/** + * Creating elements with explicit xmlns values. + * + * Use these instead of [NSXMLElement initWithName:URI:]. + * The category methods below are more readable, and they actually work. +**/ + ++ (NSXMLElement *)elementWithName:(NSString *)name xmlns:(NSString *)ns; +- (id)initWithName:(NSString *)name xmlns:(NSString *)ns; + +/** + * Extracting multiple elements. +**/ + +- (NSArray *)elementsForXmlns:(NSString *)ns; +- (NSArray *)elementsForXmlnsPrefix:(NSString *)nsPrefix; + +/** + * Extracting a single element. +**/ + +- (NSXMLElement *)elementForName:(NSString *)name; +- (NSXMLElement *)elementForName:(NSString *)name xmlns:(NSString *)xmlns; +- (NSXMLElement *)elementForName:(NSString *)name xmlnsPrefix:(NSString *)xmlnsPrefix; + +/** + * Convenience methods for removing child elements. + * + * If the element doesn't exist, these methods do nothing. +**/ + +- (void)removeElementForName:(NSString *)name; +- (void)removeElementsForName:(NSString *)name; +- (void)removeElementForName:(NSString *)name xmlns:(NSString *)xmlns; +- (void)removeElementForName:(NSString *)name xmlnsPrefix:(NSString *)xmlnsPrefix; + +/** + * Working with the common xmpp xmlns value. + * + * Use these instead of getting/setting the URI. + * The category methods below are more readable, and they actually work. +**/ + +- (NSString *)xmlns; +- (void)setXmlns:(NSString *)ns; + +/** + * Convenience methods for printing xml elements with different styles. +**/ + +- (NSString *)prettyXMLString; +- (NSString *)compactXMLString; + +/** + * Convenience methods for adding attributes. +**/ + +- (void)addAttributeWithName:(NSString *)name intValue:(int)intValue; +- (void)addAttributeWithName:(NSString *)name boolValue:(BOOL)boolValue; +- (void)addAttributeWithName:(NSString *)name floatValue:(float)floatValue; +- (void)addAttributeWithName:(NSString *)name doubleValue:(double)doubleValue; +- (void)addAttributeWithName:(NSString *)name integerValue:(NSInteger)integerValue; +- (void)addAttributeWithName:(NSString *)name unsignedIntegerValue:(NSUInteger)unsignedIntegerValue; +- (void)addAttributeWithName:(NSString *)name stringValue:(NSString *)string; +- (void)addAttributeWithName:(NSString *)name numberValue:(NSNumber *)number; +- (void)addAttributeWithName:(NSString *)name objectValue:(id)objectValue; + +/** + * Convenience methods for extracting attribute values in different formats. + * + * E.g. // float price = [beer attributeFloatValueForName:@"price"]; +**/ + +- (int)attributeIntValueForName:(NSString *)name; +- (BOOL)attributeBoolValueForName:(NSString *)name; +- (float)attributeFloatValueForName:(NSString *)name; +- (double)attributeDoubleValueForName:(NSString *)name; +- (int32_t)attributeInt32ValueForName:(NSString *)name; +- (uint32_t)attributeUInt32ValueForName:(NSString *)name; +- (int64_t)attributeInt64ValueForName:(NSString *)name; +- (uint64_t)attributeUInt64ValueForName:(NSString *)name; +- (NSInteger)attributeIntegerValueForName:(NSString *)name; +- (NSUInteger)attributeUnsignedIntegerValueForName:(NSString *)name; +- (NSString *)attributeStringValueForName:(NSString *)name; +- (NSNumber *)attributeNumberIntValueForName:(NSString *)name; +- (NSNumber *)attributeNumberBoolValueForName:(NSString *)name; +- (NSNumber *)attributeNumberFloatValueForName:(NSString *)name; +- (NSNumber *)attributeNumberDoubleValueForName:(NSString *)name; +- (NSNumber *)attributeNumberInt32ValueForName:(NSString *)name; +- (NSNumber *)attributeNumberUInt32ValueForName:(NSString *)name; +- (NSNumber *)attributeNumberInt64ValueForName:(NSString *)name; +- (NSNumber *)attributeNumberUInt64ValueForName:(NSString *)name; +- (NSNumber *)attributeNumberIntegerValueForName:(NSString *)name; +- (NSNumber *)attributeNumberUnsignedIntegerValueForName:(NSString *)name; + +- (int)attributeIntValueForName:(NSString *)name withDefaultValue:(int)defaultValue; +- (BOOL)attributeBoolValueForName:(NSString *)name withDefaultValue:(BOOL)defaultValue; +- (float)attributeFloatValueForName:(NSString *)name withDefaultValue:(float)defaultValue; +- (double)attributeDoubleValueForName:(NSString *)name withDefaultValue:(double)defaultValue; +- (int32_t)attributeInt32ValueForName:(NSString *)name withDefaultValue:(int32_t)defaultValue; +- (uint32_t)attributeUInt32ValueForName:(NSString *)name withDefaultValue:(uint32_t)defaultValue; +- (int64_t)attributeInt64ValueForName:(NSString *)name withDefaultValue:(int64_t)defaultValue; +- (uint64_t)attributeUInt64ValueForName:(NSString *)name withDefaultValue:(uint64_t)defaultValue; +- (NSInteger)attributeIntegerValueForName:(NSString *)name withDefaultValue:(NSInteger)defaultValue; +- (NSUInteger)attributeUnsignedIntegerValueForName:(NSString *)name withDefaultValue:(NSUInteger)defaultValue; +- (NSString *)attributeStringValueForName:(NSString *)name withDefaultValue:(NSString *)defaultValue; +- (NSNumber *)attributeNumberIntValueForName:(NSString *)name withDefaultValue:(int)defaultValue; +- (NSNumber *)attributeNumberBoolValueForName:(NSString *)name withDefaultValue:(BOOL)defaultValue; + +- (NSMutableDictionary *)attributesAsDictionary; + +/** + * Convenience methods for extracting element values in different formats. + * + * E.g. 9.99 // float price = [priceElement stringValueAsFloat]; +**/ + +- (int)stringValueAsInt; +- (BOOL)stringValueAsBool; +- (float)stringValueAsFloat; +- (double)stringValueAsDouble; +- (int32_t)stringValueAsInt32; +- (uint32_t)stringValueAsUInt32; +- (int64_t)stringValueAsInt64; +- (uint64_t)stringValueAsUInt64; +- (NSInteger)stringValueAsNSInteger; +- (NSUInteger)stringValueAsNSUInteger; + +/** + * Working with namespaces. +**/ + +- (void)addNamespaceWithPrefix:(NSString *)prefix stringValue:(NSString *)string; + +- (NSString *)namespaceStringValueForPrefix:(NSString *)prefix; +- (NSString *)namespaceStringValueForPrefix:(NSString *)prefix withDefaultValue:(NSString *)defaultValue; + +@end diff --git a/Categories/NSXMLElement+XMPP.m b/Categories/NSXMLElement+XMPP.m new file mode 100644 index 0000000..1daaf2e --- /dev/null +++ b/Categories/NSXMLElement+XMPP.m @@ -0,0 +1,663 @@ +#import "NSXMLElement+XMPP.h" +#import "NSNumber+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +@implementation NSXMLElement (XMPP) + +/** + * Convenience methods for Creating elements. +**/ + ++ (NSXMLElement *)elementWithName:(NSString *)name numberValue:(NSNumber *)number +{ + return [self elementWithName:name stringValue:[number stringValue]]; +} + +- (id)initWithName:(NSString *)name numberValue:(NSNumber *)number +{ + return [self initWithName:name stringValue:[number stringValue]]; +} + ++ (NSXMLElement *)elementWithName:(NSString *)name objectValue:(id)objectValue +{ + if([objectValue isKindOfClass:[NSString class]]) + { + return [self elementWithName:name stringValue:objectValue]; + } + else if([objectValue isKindOfClass:[NSNumber class]]) + { + return [self elementWithName:name numberValue:objectValue]; + } + else if([objectValue respondsToSelector:@selector(stringValue)]) + { + return [self elementWithName:name stringValue:[objectValue stringValue]]; + } + else + { + return [self elementWithName:name]; + } +} + +- (id)initWithName:(NSString *)name objectValue:(id)objectValue +{ + if([objectValue isKindOfClass:[NSString class]]) + { + return [self initWithName:name stringValue:objectValue]; + } + else if([objectValue isKindOfClass:[NSNumber class]]) + { + return [self initWithName:name numberValue:objectValue]; + } + else if([objectValue respondsToSelector:@selector(stringValue)]) + { + return [self initWithName:name stringValue:[objectValue stringValue]]; + } + else + { + return [self initWithName:name]; + } +} + +/** + * Quick method to create an element +**/ ++ (NSXMLElement *)elementWithName:(NSString *)name xmlns:(NSString *)ns +{ + NSXMLElement *element = [NSXMLElement elementWithName:name]; + [element setXmlns:ns]; + return element; +} + +- (id)initWithName:(NSString *)name xmlns:(NSString *)ns +{ + if ((self = [self initWithName:name])) + { + [self setXmlns:ns]; + } + return self; +} + +- (NSArray *)elementsForXmlns:(NSString *)ns +{ + NSMutableArray *elements = [NSMutableArray array]; + + for (NSXMLNode *node in [self children]) + { + if ([node isKindOfClass:[NSXMLElement class]]) + { + NSXMLElement *element = (NSXMLElement *)node; + + if ([[element xmlns] isEqual:ns]) + { + [elements addObject:element]; + } + } + } + + return elements; +} + +- (NSArray *)elementsForXmlnsPrefix:(NSString *)nsPrefix +{ + NSMutableArray *elements = [NSMutableArray array]; + + for (NSXMLNode *node in [self children]) + { + if ([node isKindOfClass:[NSXMLElement class]]) + { + NSXMLElement *element = (NSXMLElement *)node; + + if ([[element xmlns] hasPrefix:nsPrefix]) + { + [elements addObject:element]; + } + } + } + + return elements; +} + +/** + * This method returns the first child element for the given name (as an NSXMLElement). + * If no child elements exist for the given name, nil is returned. +**/ +- (NSXMLElement *)elementForName:(NSString *)name +{ + NSArray *elements = [self elementsForName:name]; + if ([elements count] > 0) + { + return elements[0]; + } + else + { + // There is a bug in the NSXMLElement elementsForName: method. + // Consider the following XML fragment: + // + // + // + // + // + // Calling [query elementsForName:@"x"] results in an empty array! + // + // However, it will work properly if you use the following: + // [query elementsForLocalName:@"x" URI:@"some:other:namespace"] + // + // The trouble with this is that we may not always know the xmlns in advance, + // so in this particular case there is no way to access the element without looping through the children. + // + // This bug was submitted to apple on June 1st, 2007 and was classified as "serious". + // + // --!!-- This bug does NOT exist in DDXML --!!-- + + return nil; + } +} + +/** + * This method returns the first child element for the given name and given xmlns (as an NSXMLElement). + * If no child elements exist for the given name and given xmlns, nil is returned. +**/ +- (NSXMLElement *)elementForName:(NSString *)name xmlns:(NSString *)xmlns +{ + NSArray *elements = [self elementsForLocalName:name URI:xmlns]; + if ([elements count] > 0) + { + return elements[0]; + } + else + { + return nil; + } +} + +- (NSXMLElement *)elementForName:(NSString *)name xmlnsPrefix:(NSString *)xmlnsPrefix{ + + NSXMLElement *result = nil; + + for (NSXMLNode *node in [self children]) + { + if ([node isKindOfClass:[NSXMLElement class]]) + { + NSXMLElement *element = (NSXMLElement *)node; + + if ([[element name] isEqualToString:name] && [[element xmlns] hasPrefix:xmlnsPrefix]) + { + result = element; + break; + } + } + } + + return result; +} + +/** + * This method removes the first child element for the given name. + * If no child elements exist for the given name, this method does nothing. +**/ +- (void)removeElementForName:(NSString *)name +{ + NSXMLElement *element = [self elementForName:name]; + + if(element) + { + [self removeChildAtIndex:[[self children] indexOfObject:element]]; + } +} + +/** + * This method removes the all child elements for the given name. + * If no child elements exist for the given name, this method does nothing. +**/ +- (void)removeElementsForName:(NSString *)name +{ + NSArray *elements = [self elementsForName:name]; + + for(NSXMLElement *element in elements) + { + [self removeChildAtIndex:[[self children] indexOfObject:element]]; + } +} + +/** + * This method removes the first child element for the given name and given xmlns. + * If no child elements exist for the given name and given xmlns, this method does nothing. +**/ +- (void)removeElementForName:(NSString *)name xmlns:(NSString *)xmlns +{ + NSXMLElement *element = [self elementForName:name xmlns:xmlns]; + + if(element) + { + [self removeChildAtIndex:[[self children] indexOfObject:element]]; + } +} + +/** + * This method removes the first child element for the given name and given xmlns prefix. + * If no child elements exist for the given name and given xmlns prefix, this method does nothing. +**/ +- (void)removeElementForName:(NSString *)name xmlnsPrefix:(NSString *)xmlnsPrefix +{ + NSXMLElement *element = [self elementForName:name xmlnsPrefix:xmlnsPrefix]; + + if(element) + { + [self removeChildAtIndex:[[self children] indexOfObject:element]]; + } +} + +/** + * Returns the common xmlns "attribute", which is only accessible via the namespace methods. + * The xmlns value is often used in jabber elements. +**/ +- (NSString *)xmlns +{ + return [[self namespaceForPrefix:@""] stringValue]; +} + +- (void)setXmlns:(NSString *)ns +{ + // If we use setURI: then the xmlns won't be displayed in the XMLString. + // Adding the namespace this way works properly. + + [self addNamespace:[NSXMLNode namespaceWithName:@"" stringValue:ns]]; +} + +/** + * Shortcut to get a pretty (formatted) string representation of the element. +**/ +- (NSString *)prettyXMLString +{ + return [self XMLStringWithOptions:(NSXMLNodePrettyPrint | NSXMLNodeCompactEmptyElement)]; +} + +/** + * Shortcut to get a compact string representation of the element. +**/ +- (NSString *)compactXMLString +{ + return [self XMLStringWithOptions:NSXMLNodeCompactEmptyElement]; +} + +/** + * Shortcut to avoid having to use NSXMLNode everytime +**/ + +- (void)addAttributeWithName:(NSString *)name intValue:(int)intValue +{ + [self addAttributeWithName:name numberValue:@(intValue)]; +} + +- (void)addAttributeWithName:(NSString *)name boolValue:(BOOL)boolValue +{ + [self addAttributeWithName:name numberValue:@(boolValue)]; +} + +- (void)addAttributeWithName:(NSString *)name floatValue:(float)floatValue +{ + [self addAttributeWithName:name numberValue:@(floatValue)]; +} + +- (void)addAttributeWithName:(NSString *)name doubleValue:(double)doubleValue +{ + [self addAttributeWithName:name numberValue:@(doubleValue)]; +} + +- (void)addAttributeWithName:(NSString *)name integerValue:(NSInteger)integerValue +{ + [self addAttributeWithName:name numberValue:@(integerValue)]; +} + +- (void)addAttributeWithName:(NSString *)name unsignedIntegerValue:(NSUInteger)unsignedIntegerValue +{ + [self addAttributeWithName:name numberValue:@(unsignedIntegerValue)]; +} + +- (void)addAttributeWithName:(NSString *)name stringValue:(NSString *)string +{ + [self addAttribute:[NSXMLNode attributeWithName:name stringValue:string]]; +} + +- (void)addAttributeWithName:(NSString *)name numberValue:(NSNumber *)number +{ + [self addAttributeWithName:name stringValue:[number stringValue]]; +} + +- (void)addAttributeWithName:(NSString *)name objectValue:(id)objectValue +{ + if([objectValue isKindOfClass:[NSString class]]) + { + [self addAttributeWithName:name stringValue:objectValue]; + } + else if([objectValue isKindOfClass:[NSNumber class]]) + { + [self addAttributeWithName:name numberValue:objectValue]; + } + else if([objectValue respondsToSelector:@selector(stringValue)]) + { + [self addAttributeWithName:name stringValue:[objectValue stringValue]]; + } +} + +/** + * The following methods return the corresponding value of the attribute with the given name. +**/ + +- (int)attributeIntValueForName:(NSString *)name +{ + return [[self attributeStringValueForName:name] intValue]; +} +- (BOOL)attributeBoolValueForName:(NSString *)name +{ + NSString *attributeStringValueForName = [self attributeStringValueForName:name]; + + BOOL result = NO; + + // An XML boolean datatype can have the following legal literals: true, false, 1, 0 + + if ([attributeStringValueForName isEqualToString:@"true"] || [attributeStringValueForName isEqualToString:@"1"]) + { + result = YES; + } + else if([attributeStringValueForName isEqualToString:@"false"] || [attributeStringValueForName isEqualToString:@"0"]) + { + result = NO; + } + else + { + result = [attributeStringValueForName boolValue]; + } + + return result; +} +- (float)attributeFloatValueForName:(NSString *)name +{ + return [[self attributeStringValueForName:name] floatValue]; +} +- (double)attributeDoubleValueForName:(NSString *)name +{ + return [[self attributeStringValueForName:name] doubleValue]; +} +- (int32_t)attributeInt32ValueForName:(NSString *)name +{ + int32_t result = 0; + [NSNumber xmpp_parseString:[self attributeStringValueForName:name] intoInt32:&result]; + return result; +} +- (uint32_t)attributeUInt32ValueForName:(NSString *)name +{ + uint32_t result = 0; + [NSNumber xmpp_parseString:[self attributeStringValueForName:name] intoUInt32:&result]; + return result; +} +- (int64_t)attributeInt64ValueForName:(NSString *)name +{ + int64_t result; + [NSNumber xmpp_parseString:[self attributeStringValueForName:name] intoInt64:&result]; + return result; +} +- (uint64_t)attributeUInt64ValueForName:(NSString *)name +{ + uint64_t result; + [NSNumber xmpp_parseString:[self attributeStringValueForName:name] intoUInt64:&result]; + return result; +} +- (NSInteger)attributeIntegerValueForName:(NSString *)name +{ + NSInteger result; + [NSNumber xmpp_parseString:[self attributeStringValueForName:name] intoNSInteger:&result]; + return result; +} +- (NSUInteger)attributeUnsignedIntegerValueForName:(NSString *)name +{ + NSUInteger result = 0; + [NSNumber xmpp_parseString:[self attributeStringValueForName:name] intoNSUInteger:&result]; + return result; +} +- (NSString *)attributeStringValueForName:(NSString *)name +{ + return [[self attributeForName:name] stringValue]; +} +- (NSNumber *)attributeNumberIntValueForName:(NSString *)name +{ + return @([self attributeIntValueForName:name]); +} +- (NSNumber *)attributeNumberBoolValueForName:(NSString *)name +{ + return @([self attributeBoolValueForName:name]); +} +- (NSNumber *)attributeNumberFloatValueForName:(NSString *)name +{ + return @([self attributeFloatValueForName:name]); +} +- (NSNumber *)attributeNumberDoubleValueForName:(NSString *)name +{ + return @([self attributeDoubleValueForName:name]); +} +- (NSNumber *)attributeNumberInt32ValueForName:(NSString *)name +{ + return @([self attributeInt32ValueForName:name]); +} +- (NSNumber *)attributeNumberUInt32ValueForName:(NSString *)name +{ + return @([self attributeUInt32ValueForName:name]); +} +- (NSNumber *)attributeNumberInt64ValueForName:(NSString *)name +{ + return @([self attributeInt64ValueForName:name]); +} +- (NSNumber *)attributeNumberUInt64ValueForName:(NSString *)name +{ + return @([self attributeUInt64ValueForName:name]); +} +- (NSNumber *)attributeNumberIntegerValueForName:(NSString *)name +{ + return @([self attributeIntegerValueForName:name]); +} +- (NSNumber *)attributeNumberUnsignedIntegerValueForName:(NSString *)name +{ + return @([self attributeUnsignedIntegerValueForName:name]); +} + +/** + * The following methods return the corresponding value of the attribute with the given name. + * If the attribute does not exist, the given defaultValue is returned. +**/ + +- (int)attributeIntValueForName:(NSString *)name withDefaultValue:(int)defaultValue +{ + NSXMLNode *attr = [self attributeForName:name]; + return (attr) ? [[attr stringValue] intValue] : defaultValue; +} +- (BOOL)attributeBoolValueForName:(NSString *)name withDefaultValue:(BOOL)defaultValue +{ + NSXMLNode *attr = [self attributeForName:name]; + return (attr) ? [[attr stringValue] boolValue] : defaultValue; +} +- (float)attributeFloatValueForName:(NSString *)name withDefaultValue:(float)defaultValue +{ + NSXMLNode *attr = [self attributeForName:name]; + return (attr) ? [[attr stringValue] floatValue] : defaultValue; +} +- (double)attributeDoubleValueForName:(NSString *)name withDefaultValue:(double)defaultValue +{ + NSXMLNode *attr = [self attributeForName:name]; + return (attr) ? [[attr stringValue] doubleValue] : defaultValue; +} +- (int32_t)attributeInt32ValueForName:(NSString *)name withDefaultValue:(int32_t)defaultValue +{ + int32_t result = 0; + if ([NSNumber xmpp_parseString:[self attributeStringValueForName:name] intoInt32:&result]) + return result; + else + return defaultValue; +} +- (uint32_t)attributeUInt32ValueForName:(NSString *)name withDefaultValue:(uint32_t)defaultValue +{ + uint32_t result = 0; + if ([NSNumber xmpp_parseString:[self attributeStringValueForName:name] intoUInt32:&result]) + return result; + else + return defaultValue; +} +- (int64_t)attributeInt64ValueForName:(NSString *)name withDefaultValue:(int64_t)defaultValue +{ + int64_t result = 0; + if ([NSNumber xmpp_parseString:[self attributeStringValueForName:name] intoInt64:&result]) + return result; + else + return defaultValue; +} +- (uint64_t)attributeUInt64ValueForName:(NSString *)name withDefaultValue:(uint64_t)defaultValue +{ + uint64_t result = 0; + if ([NSNumber xmpp_parseString:[self attributeStringValueForName:name] intoUInt64:&result]) + return result; + else + return defaultValue; +} +- (NSInteger)attributeIntegerValueForName:(NSString *)name withDefaultValue:(NSInteger)defaultValue +{ + NSInteger result = 0; + if ([NSNumber xmpp_parseString:[self attributeStringValueForName:name] intoNSInteger:&result]) + return result; + else + return defaultValue; +} +- (NSUInteger)attributeUnsignedIntegerValueForName:(NSString *)name withDefaultValue:(NSUInteger)defaultValue +{ + NSUInteger result = 0; + if ([NSNumber xmpp_parseString:[self attributeStringValueForName:name] intoNSUInteger:&result]) + return result; + else + return defaultValue; +} +- (NSString *)attributeStringValueForName:(NSString *)name withDefaultValue:(NSString *)defaultValue +{ + NSXMLNode *attr = [self attributeForName:name]; + return (attr) ? [attr stringValue] : defaultValue; +} +- (NSNumber *)attributeNumberIntValueForName:(NSString *)name withDefaultValue:(int)defaultValue +{ + return @([self attributeIntValueForName:name withDefaultValue:defaultValue]); +} +- (NSNumber *)attributeNumberBoolValueForName:(NSString *)name withDefaultValue:(BOOL)defaultValue +{ + return @([self attributeBoolValueForName:name withDefaultValue:defaultValue]); +} + +/** + * Returns all the attributes in a dictionary. +**/ +- (NSMutableDictionary *)attributesAsDictionary +{ + NSArray *attributes = [self attributes]; + NSMutableDictionary *result = [NSMutableDictionary dictionaryWithCapacity:[attributes count]]; + + NSUInteger i; + for(i = 0; i < [attributes count]; i++) + { + NSXMLNode *node = attributes[i]; + + result[[node name]] = [node stringValue]; + } + return result; +} + +/** + * The following methods return the corresponding value of the node. +**/ + +- (int)stringValueAsInt +{ + return [[self stringValue] intValue]; +} +- (BOOL)stringValueAsBool +{ + return [[self stringValue] boolValue]; +} +- (float)stringValueAsFloat +{ + return [[self stringValue] floatValue]; +} +- (double)stringValueAsDouble +{ + return [[self stringValue] doubleValue]; +} +- (int32_t)stringValueAsInt32 +{ + int32_t result; + if ([NSNumber xmpp_parseString:[self stringValue] intoInt32:&result]) + return result; + else + return 0; +} +- (uint32_t)stringValueAsUInt32 +{ + uint32_t result; + if ([NSNumber xmpp_parseString:[self stringValue] intoUInt32:&result]) + return result; + else + return 0; +} +- (int64_t)stringValueAsInt64 +{ + int64_t result = 0; + if ([NSNumber xmpp_parseString:[self stringValue] intoInt64:&result]) + return result; + else + return 0; +} +- (uint64_t)stringValueAsUInt64 +{ + uint64_t result = 0; + if ([NSNumber xmpp_parseString:[self stringValue] intoUInt64:&result]) + return result; + else + return 0; +} +- (NSInteger)stringValueAsNSInteger +{ + NSInteger result = 0; + if ([NSNumber xmpp_parseString:[self stringValue] intoNSInteger:&result]) + return result; + else + return 0; +} +- (NSUInteger)stringValueAsNSUInteger +{ + NSUInteger result = 0; + if ([NSNumber xmpp_parseString:[self stringValue] intoNSUInteger:&result]) + return result; + else + return 0; +} + +/** + * Shortcut to avoid having to use NSXMLNode everytime +**/ +- (void)addNamespaceWithPrefix:(NSString *)prefix stringValue:(NSString *)string +{ + [self addNamespace:[NSXMLNode namespaceWithName:prefix stringValue:string]]; +} + +/** + * Just to make your code look a little bit cleaner. +**/ + +- (NSString *)namespaceStringValueForPrefix:(NSString *)prefix +{ + return [[self namespaceForPrefix:prefix] stringValue]; +} + +- (NSString *)namespaceStringValueForPrefix:(NSString *)prefix withDefaultValue:(NSString *)defaultValue +{ + NSXMLNode *namespace = [self namespaceForPrefix:prefix]; + return (namespace) ? [namespace stringValue] : defaultValue; +} + +@end diff --git a/Core/XMPP.h b/Core/XMPP.h new file mode 100644 index 0000000..eae4465 --- /dev/null +++ b/Core/XMPP.h @@ -0,0 +1,31 @@ +// +// Core classes +// + +#import "XMPPJID.h" +#import "XMPPStream.h" +#import "XMPPElement.h" +#import "XMPPIQ.h" +#import "XMPPMessage.h" +#import "XMPPPresence.h" +#import "XMPPModule.h" + +// +// Authentication +// + +#import "XMPPSASLAuthentication.h" +#import "XMPPCustomBinding.h" +#import "XMPPDigestMD5Authentication.h" +#import "XMPPSCRAMSHA1Authentication.h" +#import "XMPPPlainAuthentication.h" +#import "XMPPXFacebookPlatformAuthentication.h" +#import "XMPPAnonymousAuthentication.h" +#import "XMPPDeprecatedPlainAuthentication.h" +#import "XMPPDeprecatedDigestAuthentication.h" + +// +// Categories +// + +#import "NSXMLElement+XMPP.h" diff --git a/Core/XMPPConstants.h b/Core/XMPPConstants.h new file mode 100644 index 0000000..63e94e0 --- /dev/null +++ b/Core/XMPPConstants.h @@ -0,0 +1,16 @@ +#import + +/** +* This class is provided to house various namespaces that are reused throughout +* the project. Feel free to add to the constants as you see necessary. If a +* particular namespace is only applicable to a particular extension, then it +* should be inside that extension rather than here. +*/ + +extern NSString *const XMPPSINamespace; +extern NSString *const XMPPSIProfileFileTransferNamespace; +extern NSString *const XMPPFeatureNegNamespace; +extern NSString *const XMPPBytestreamsNamespace; +extern NSString *const XMPPIBBNamespace; +extern NSString *const XMPPDiscoItemsNamespace; +extern NSString *const XMPPDiscoInfoNamespace; diff --git a/Core/XMPPConstants.m b/Core/XMPPConstants.m new file mode 100644 index 0000000..d341b56 --- /dev/null +++ b/Core/XMPPConstants.m @@ -0,0 +1,10 @@ +#import "XMPPConstants.h" + +NSString *const XMPPSINamespace = @"http://jabber.org/protocol/si"; +NSString *const XMPPSIProfileFileTransferNamespace = + @"http://jabber.org/protocol/si/profile/file-transfer"; +NSString *const XMPPFeatureNegNamespace = @"http://jabber.org/protocol/feature-neg"; +NSString *const XMPPBytestreamsNamespace = @"http://jabber.org/protocol/bytestreams"; +NSString *const XMPPIBBNamespace = @"http://jabber.org/protocol/ibb"; +NSString *const XMPPDiscoItemsNamespace = @"http://jabber.org/protocol/disco#items"; +NSString *const XMPPDiscoInfoNamespace = @"http://jabber.org/protocol/disco#info"; diff --git a/Core/XMPPElement.h b/Core/XMPPElement.h new file mode 100644 index 0000000..4c02e49 --- /dev/null +++ b/Core/XMPPElement.h @@ -0,0 +1,44 @@ +#import +#import "XMPPJID.h" + +#if TARGET_OS_IPHONE + #import "DDXML.h" +#endif + + +/** + * The XMPPElement provides the base class for XMPPIQ, XMPPMessage & XMPPPresence. + * + * This class extends NSXMLElement. + * The NSXML classes (NSXMLElement & NSXMLNode) provide a full-featured library for working with XML elements. + * + * On the iPhone, the KissXML library provides a drop-in replacement for Apple's NSXML classes. +**/ + +@interface XMPPElement : NSXMLElement + +#pragma mark Common Jabber Methods + +- (NSString *)elementID; + +- (XMPPJID *)to; +- (XMPPJID *)from; + +- (NSString *)toStr; +- (NSString *)fromStr; + +#pragma mark To and From Methods + +- (BOOL)isTo:(XMPPJID *)to; +- (BOOL)isTo:(XMPPJID *)to options:(XMPPJIDCompareOptions)mask; + +- (BOOL)isFrom:(XMPPJID *)from; +- (BOOL)isFrom:(XMPPJID *)from options:(XMPPJIDCompareOptions)mask; + +- (BOOL)isToOrFrom:(XMPPJID *)toOrFrom; +- (BOOL)isToOrFrom:(XMPPJID *)toOrFrom options:(XMPPJIDCompareOptions)mask; + +- (BOOL)isTo:(XMPPJID *)to from:(XMPPJID *)from; +- (BOOL)isTo:(XMPPJID *)to from:(XMPPJID *)from options:(XMPPJIDCompareOptions)mask; + +@end diff --git a/Core/XMPPElement.m b/Core/XMPPElement.m new file mode 100644 index 0000000..55a1113 --- /dev/null +++ b/Core/XMPPElement.m @@ -0,0 +1,192 @@ +#import "XMPPElement.h" +#import "XMPPJID.h" +#import "NSXMLElement+XMPP.h" + +#import + + +@implementation XMPPElement + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Encoding, Decoding +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if ! TARGET_OS_IPHONE +- (id)replacementObjectForPortCoder:(NSPortCoder *)encoder +{ + if([encoder isBycopy]) + return self; + else + return [super replacementObjectForPortCoder:encoder]; + // return [NSDistantObject proxyWithLocal:self connection:[encoder connection]]; +} +#endif + +- (id)initWithCoder:(NSCoder *)coder +{ + NSString *xmlString; + if([coder allowsKeyedCoding]) + { + if([coder respondsToSelector:@selector(requiresSecureCoding)] && + [coder requiresSecureCoding]) + { + xmlString = [coder decodeObjectOfClass:[NSString class] forKey:@"xmlString"]; + } + else + { + xmlString = [coder decodeObjectForKey:@"xmlString"]; + } + } + else + { + xmlString = [coder decodeObject]; + } + + // The method [super initWithXMLString:error:] may return a different self. + // In other words, it may [self release], and alloc/init/return a new self. + // + // So to maintain the proper class (XMPPIQ, XMPPMessage, XMPPPresence, etc) + // we need to get a reference to the class before invoking super. + + Class selfClass = [self class]; + + if ((self = [super initWithXMLString:xmlString error:nil])) + { + object_setClass(self, selfClass); + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + NSString *xmlString = [self compactXMLString]; + + if([coder allowsKeyedCoding]) + { + [coder encodeObject:xmlString forKey:@"xmlString"]; + } + else + { + [coder encodeObject:xmlString]; + } +} + ++ (BOOL) supportsSecureCoding +{ + return YES; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Copying +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (id)copyWithZone:(NSZone *)zone +{ + NSXMLElement *elementCopy = [super copyWithZone:zone]; + object_setClass(elementCopy, [self class]); + + return elementCopy; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Common Jabber Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSString *)elementID +{ + return [[self attributeForName:@"id"] stringValue]; +} + +- (NSString *)toStr +{ + return [[self attributeForName:@"to"] stringValue]; +} + +- (NSString *)fromStr +{ + return [[self attributeForName:@"from"] stringValue]; +} + +- (XMPPJID *)to +{ + return [XMPPJID jidWithString:[self toStr]]; +} + +- (XMPPJID *)from +{ + return [XMPPJID jidWithString:[self fromStr]]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark To and From Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)isTo:(XMPPJID *)to +{ + return [self.to isEqualToJID:to]; +} + +- (BOOL)isTo:(XMPPJID *)to options:(XMPPJIDCompareOptions)mask +{ + return [self.to isEqualToJID:to options:mask]; +} + +- (BOOL)isFrom:(XMPPJID *)from +{ + return [self.from isEqualToJID:from]; +} + +- (BOOL)isFrom:(XMPPJID *)from options:(XMPPJIDCompareOptions)mask +{ + return [self.from isEqualToJID:from options:mask]; +} + +- (BOOL)isToOrFrom:(XMPPJID *)toOrFrom +{ + if([self isTo:toOrFrom] || [self isFrom:toOrFrom]) + { + return YES; + } + else + { + return NO; + } +} + +- (BOOL)isToOrFrom:(XMPPJID *)toOrFrom options:(XMPPJIDCompareOptions)mask +{ + if([self isTo:toOrFrom options:mask] || [self isFrom:toOrFrom options:mask]) + { + return YES; + } + else + { + return NO; + } +} + +- (BOOL)isTo:(XMPPJID *)to from:(XMPPJID *)from +{ + if([self isTo:to] && [self isFrom:from]) + { + return YES; + } + else + { + return NO; + } +} + +- (BOOL)isTo:(XMPPJID *)to from:(XMPPJID *)from options:(XMPPJIDCompareOptions)mask +{ + if([self isTo:to options:mask] && [self isFrom:from options:mask]) + { + return YES; + } + else + { + return NO; + } +} + +@end diff --git a/Core/XMPPIQ.h b/Core/XMPPIQ.h new file mode 100644 index 0000000..4c63801 --- /dev/null +++ b/Core/XMPPIQ.h @@ -0,0 +1,83 @@ +#import +#import "XMPPElement.h" + +/** + * The XMPPIQ class represents an element. + * It extends XMPPElement, which in turn extends NSXMLElement. + * All elements that go in and out of the + * xmpp stream will automatically be converted to XMPPIQ objects. + * + * This class exists to provide developers an easy way to add functionality to IQ processing. + * Simply add your own category to XMPPIQ to extend it with your own custom methods. +**/ + +@interface XMPPIQ : XMPPElement + +/** + * Converts an NSXMLElement to an XMPPIQ element in place (no memory allocations or copying) +**/ ++ (XMPPIQ *)iqFromElement:(NSXMLElement *)element; + +/** + * Creates and returns a new autoreleased XMPPIQ element. + * If the type or elementID parameters are nil, those attributes will not be added. +**/ ++ (XMPPIQ *)iq; ++ (XMPPIQ *)iqWithType:(NSString *)type; ++ (XMPPIQ *)iqWithType:(NSString *)type to:(XMPPJID *)jid; ++ (XMPPIQ *)iqWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid; ++ (XMPPIQ *)iqWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid child:(NSXMLElement *)childElement; ++ (XMPPIQ *)iqWithType:(NSString *)type elementID:(NSString *)eid; ++ (XMPPIQ *)iqWithType:(NSString *)type elementID:(NSString *)eid child:(NSXMLElement *)childElement; ++ (XMPPIQ *)iqWithType:(NSString *)type child:(NSXMLElement *)childElement; + +/** + * Creates and returns a new XMPPIQ element. + * If the type or elementID parameters are nil, those attributes will not be added. +**/ +- (id)init; +- (id)initWithType:(NSString *)type; +- (id)initWithType:(NSString *)type to:(XMPPJID *)jid; +- (id)initWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid; +- (id)initWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid child:(NSXMLElement *)childElement; +- (id)initWithType:(NSString *)type elementID:(NSString *)eid; +- (id)initWithType:(NSString *)type elementID:(NSString *)eid child:(NSXMLElement *)childElement; +- (id)initWithType:(NSString *)type child:(NSXMLElement *)childElement; + +/** + * Returns the type attribute of the IQ. + * According to the XMPP protocol, the type should be one of 'get', 'set', 'result' or 'error'. + * + * This method converts the attribute to lowercase so + * case-sensitive string comparisons are safe (regardless of server treatment). +**/ +- (NSString *)type; + +/** + * Convenience methods for determining the IQ type. +**/ +- (BOOL)isGetIQ; +- (BOOL)isSetIQ; +- (BOOL)isResultIQ; +- (BOOL)isErrorIQ; + +/** + * Convenience method for determining if the IQ is of type 'get' or 'set'. +**/ +- (BOOL)requiresResponse; + +/** + * The XMPP RFC has various rules for the number of child elements an IQ is allowed to have: + * + * - An IQ stanza of type "get" or "set" MUST contain one and only one child element. + * - An IQ stanza of type "result" MUST include zero or one child elements. + * - An IQ stanza of type "error" SHOULD include the child element contained in the + * associated "get" or "set" and MUST include an child. + * + * The childElement returns the single non-error element, if one exists, or nil. + * The childErrorElement returns the error element, if one exists, or nil. +**/ +- (NSXMLElement *)childElement; +- (NSXMLElement *)childErrorElement; + +@end diff --git a/Core/XMPPIQ.m b/Core/XMPPIQ.m new file mode 100644 index 0000000..2edba24 --- /dev/null +++ b/Core/XMPPIQ.m @@ -0,0 +1,222 @@ +#import "XMPPIQ.h" +#import "XMPPJID.h" +#import "NSXMLElement+XMPP.h" + +#import + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@implementation XMPPIQ + +#if DEBUG + ++ (void)initialize +{ + // We use the object_setClass method below to dynamically change the class from a standard NSXMLElement. + // The size of the two classes is expected to be the same. + // + // If a developer adds instance methods to this class, bad things happen at runtime that are very hard to debug. + // This check is here to aid future developers who may make this mistake. + // + // For Fearless And Experienced Objective-C Developers: + // It may be possible to support adding instance variables to this class if you seriously need it. + // To do so, try realloc'ing self after altering the class, and then initialize your variables. + + size_t superSize = class_getInstanceSize([NSXMLElement class]); + size_t ourSize = class_getInstanceSize([XMPPIQ class]); + + if (superSize != ourSize) + { + NSLog(@"Adding instance variables to XMPPIQ is not currently supported!"); + exit(15); + } +} + +#endif + ++ (XMPPIQ *)iqFromElement:(NSXMLElement *)element +{ + object_setClass(element, [XMPPIQ class]); + + return (XMPPIQ *)element; +} + ++ (XMPPIQ *)iq +{ + return [[XMPPIQ alloc] initWithType:nil to:nil elementID:nil child:nil]; +} + ++ (XMPPIQ *)iqWithType:(NSString *)type +{ + return [[XMPPIQ alloc] initWithType:type to:nil elementID:nil child:nil]; +} + ++ (XMPPIQ *)iqWithType:(NSString *)type to:(XMPPJID *)jid +{ + return [[XMPPIQ alloc] initWithType:type to:jid elementID:nil child:nil]; +} + ++ (XMPPIQ *)iqWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid +{ + return [[XMPPIQ alloc] initWithType:type to:jid elementID:eid child:nil]; +} + ++ (XMPPIQ *)iqWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid child:(NSXMLElement *)childElement +{ + return [[XMPPIQ alloc] initWithType:type to:jid elementID:eid child:childElement]; +} + ++ (XMPPIQ *)iqWithType:(NSString *)type elementID:(NSString *)eid +{ + return [[XMPPIQ alloc] initWithType:type to:nil elementID:eid child:nil]; +} + ++ (XMPPIQ *)iqWithType:(NSString *)type elementID:(NSString *)eid child:(NSXMLElement *)childElement +{ + return [[XMPPIQ alloc] initWithType:type to:nil elementID:eid child:childElement]; +} + ++ (XMPPIQ *)iqWithType:(NSString *)type child:(NSXMLElement *)childElement +{ + return [[XMPPIQ alloc] initWithType:type to:nil elementID:nil child:childElement]; +} + +- (id)init +{ + return [self initWithType:nil to:nil elementID:nil child:nil]; +} + +- (id)initWithType:(NSString *)type +{ + return [self initWithType:type to:nil elementID:nil child:nil]; +} + +- (id)initWithType:(NSString *)type to:(XMPPJID *)jid +{ + return [self initWithType:type to:jid elementID:nil child:nil]; +} + +- (id)initWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid +{ + return [self initWithType:type to:jid elementID:eid child:nil]; +} + +- (id)initWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid child:(NSXMLElement *)childElement +{ + if ((self = [super initWithName:@"iq"])) + { + if (type) + [self addAttributeWithName:@"type" stringValue:type]; + + if (jid) + [self addAttributeWithName:@"to" stringValue:[jid full]]; + + if (eid) + [self addAttributeWithName:@"id" stringValue:eid]; + + if (childElement) + [self addChild:childElement]; + } + return self; +} + +- (id)initWithType:(NSString *)type elementID:(NSString *)eid +{ + return [self initWithType:type to:nil elementID:eid child:nil]; +} + +- (id)initWithType:(NSString *)type elementID:(NSString *)eid child:(NSXMLElement *)childElement +{ + return [self initWithType:type to:nil elementID:eid child:childElement]; +} + +- (id)initWithType:(NSString *)type child:(NSXMLElement *)childElement +{ + return [self initWithType:type to:nil elementID:nil child:childElement]; +} + +- (id)initWithXMLString:(NSString *)string error:(NSError *__autoreleasing *)error +{ + if((self = [super initWithXMLString:string error:error])){ + self = [XMPPIQ iqFromElement:self]; + } + return self; +} + +- (id)copyWithZone:(NSZone *)zone +{ + NSXMLElement *element = [super copyWithZone:zone]; + return [XMPPIQ iqFromElement:element]; +} + +- (NSString *)type +{ + return [[self attributeStringValueForName:@"type"] lowercaseString]; +} + +- (BOOL)isGetIQ +{ + return [[self type] isEqualToString:@"get"]; +} + +- (BOOL)isSetIQ +{ + return [[self type] isEqualToString:@"set"]; +} + +- (BOOL)isResultIQ +{ + return [[self type] isEqualToString:@"result"]; +} + +- (BOOL)isErrorIQ +{ + return [[self type] isEqualToString:@"error"]; +} + +- (BOOL)requiresResponse +{ + // An entity that receives an IQ request of type "get" or "set" MUST reply with an IQ response + // of type "result" or "error" (the response MUST preserve the 'id' attribute of the request). + + return [self isGetIQ] || [self isSetIQ]; +} + +- (NSXMLElement *)childElement +{ + NSArray *children = [self children]; + for (NSXMLElement *child in children) + { + // Careful: NSOrderedSame == 0 + + NSString *childName = [child name]; + if (childName && ([childName caseInsensitiveCompare:@"error"] != NSOrderedSame)) + { + return child; + } + } + + return nil; +} + +- (NSXMLElement *)childErrorElement +{ + NSArray *children = [self children]; + for (NSXMLElement *child in children) + { + // Careful: NSOrderedSame == 0 + + NSString *childName = [child name]; + if (childName && ([childName caseInsensitiveCompare:@"error"] == NSOrderedSame)) + { + return child; + } + } + + return nil; +} + +@end diff --git a/Core/XMPPInternal.h b/Core/XMPPInternal.h new file mode 100644 index 0000000..9b58ae2 --- /dev/null +++ b/Core/XMPPInternal.h @@ -0,0 +1,118 @@ +// +// This file is for XMPPStream and various internal components. +// + +#import "XMPPStream.h" +#import "XMPPModule.h" + +// Define the various states we'll use to track our progress +typedef NS_ENUM(NSInteger, XMPPStreamState) { + STATE_XMPP_DISCONNECTED, + STATE_XMPP_RESOLVING_SRV, + STATE_XMPP_CONNECTING, + STATE_XMPP_OPENING, + STATE_XMPP_NEGOTIATING, + STATE_XMPP_STARTTLS_1, + STATE_XMPP_STARTTLS_2, + STATE_XMPP_POST_NEGOTIATION, + STATE_XMPP_REGISTERING, + STATE_XMPP_AUTH, + STATE_XMPP_BINDING, + STATE_XMPP_START_SESSION, + STATE_XMPP_CONNECTED, +}; + +/** + * It is recommended that storage classes cache a stream's myJID. + * This prevents them from constantly querying the property from the xmppStream instance, + * as doing so goes through xmppStream's dispatch queue. + * Caching the stream's myJID frees the dispatch queue to handle xmpp processing tasks. + * + * The object of the notification will be the XMPPStream instance. + * + * Note: We're not using the typical MulticastDelegate paradigm for this task as + * storage classes are not typically added as a delegate of the xmppStream. +**/ +extern NSString *const XMPPStreamDidChangeMyJIDNotification; + +@interface XMPPStream (/* Internal */) + +/** + * XMPPStream maintains thread safety by dispatching through the internal serial xmppQueue. + * Subclasses of XMPPStream MUST follow the same technique: + * + * dispatch_block_t block = ^{ + * // Code goes here + * }; + * + * if (dispatch_get_specific(xmppQueueTag)) + * block(); + * else + * dispatch_sync(xmppQueue, block); + * + * Category methods may or may not need to dispatch through the xmppQueue. + * It depends entirely on what properties of xmppStream the category method needs to access. + * For example, if a category only accesses a single property, such as the rootElement, + * then it can simply fetch the atomic property, inspect it, and complete its job. + * However, if the category needs to fetch multiple properties, then it likely needs to fetch all such + * properties in an atomic fashion. In this case, the category should likely go through the xmppQueue, + * to ensure that it gets an atomic state of the xmppStream in order to complete its job. +**/ +@property (nonatomic, readonly) dispatch_queue_t xmppQueue; +@property (nonatomic, readonly) void *xmppQueueTag; + +/** + * Returns the current state of the xmppStream. +**/ +@property (atomic, readonly) XMPPStreamState state; + +/** + * This method is for use by xmpp authentication mechanism classes. + * They should send elements using this method instead of the public sendElement methods, + * as those methods don't send the elements while authentication is in progress. + * + * @see XMPPSASLAuthentication +**/ +- (void)sendAuthElement:(NSXMLElement *)element; + +/** + * This method is for use by xmpp custom binding classes. + * They should send elements using this method instead of the public sendElement methods, + * as those methods don't send the elements while authentication/binding is in progress. + * + * @see XMPPCustomBinding +**/ +- (void)sendBindElement:(NSXMLElement *)element; + +/** + * This method allows you to inject an element into the stream as if it was received on the socket. + * This is an advanced technique, but makes for some interesting possibilities. +**/ +- (void)injectElement:(NSXMLElement *)element; + +/** + * The XMPP standard only supports , and stanzas (excluding session setup stuff). + * But some extensions use non-standard element types. + * The standard example is XEP-0198, which uses & elements. + * + * XMPPStream will assume that any non-standard element types are errors, unless you register them. + * Once registered the stream can recognize them, and will use the following delegate methods: + * + * xmppStream:didSendCustomElement: + * xmppStream:didReceiveCustomElement: +**/ +- (void)registerCustomElementNames:(NSSet *)names; +- (void)unregisterCustomElementNames:(NSSet *)names; + +@end + +@interface XMPPModule (/* Internal */) + +/** + * Used internally by methods like XMPPStream's unregisterModule:. + * Normally removing a delegate is a synchronous operation, but due to multiple dispatch_sync operations, + * it must occasionally be done asynchronously to avoid deadlock. +**/ +- (void)removeDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue synchronously:(BOOL)synchronously; + +@end diff --git a/Core/XMPPJID.h b/Core/XMPPJID.h new file mode 100644 index 0000000..cfe51eb --- /dev/null +++ b/Core/XMPPJID.h @@ -0,0 +1,74 @@ +#import + +enum XMPPJIDCompareOptions +{ + XMPPJIDCompareUser = 1, // 001 + XMPPJIDCompareDomain = 2, // 010 + XMPPJIDCompareResource = 4, // 100 + + XMPPJIDCompareBare = 3, // 011 + XMPPJIDCompareFull = 7, // 111 +}; +typedef enum XMPPJIDCompareOptions XMPPJIDCompareOptions; + + +@interface XMPPJID : NSObject +{ + __strong NSString *user; + __strong NSString *domain; + __strong NSString *resource; +} + ++ (XMPPJID *)jidWithString:(NSString *)jidStr; ++ (XMPPJID *)jidWithString:(NSString *)jidStr resource:(NSString *)resource; ++ (XMPPJID *)jidWithUser:(NSString *)user domain:(NSString *)domain resource:(NSString *)resource; + +@property (strong, readonly) NSString *user; +@property (strong, readonly) NSString *domain; +@property (strong, readonly) NSString *resource; + +/** + * Terminology (from RFC 6120): + * + * The term "bare JID" refers to an XMPP address of the form (for an account at a server) + * or of the form (for a server). + * + * The term "full JID" refers to an XMPP address of the form + * (for a particular authorized client or device associated with an account) + * or of the form (for a particular resource or script associated with a server). + * + * Thus a bareJID is one that does not have a resource. + * And a fullJID is one that does have a resource. + * + * For convenience, there are also methods that that check for a user component as well. +**/ + +- (XMPPJID *)bareJID; +- (XMPPJID *)domainJID; + +- (NSString *)bare; +- (NSString *)full; + +- (BOOL)isBare; +- (BOOL)isBareWithUser; + +- (BOOL)isFull; +- (BOOL)isFullWithUser; + +/** + * A server JID does not have a user component. +**/ +- (BOOL)isServer; + +/** + * Returns a new jid with the given resource. +**/ +- (XMPPJID *)jidWithNewResource:(NSString *)resource; + +/** + * When you know both objects are JIDs, this method is a faster way to check equality than isEqual:. +**/ +- (BOOL)isEqualToJID:(XMPPJID *)aJID; +- (BOOL)isEqualToJID:(XMPPJID *)aJID options:(XMPPJIDCompareOptions)mask; + +@end diff --git a/Core/XMPPJID.m b/Core/XMPPJID.m new file mode 100644 index 0000000..fac24e0 --- /dev/null +++ b/Core/XMPPJID.m @@ -0,0 +1,603 @@ +#import "XMPPJID.h" +#import "XMPPStringPrep.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@implementation XMPPJID + ++ (BOOL)validateDomain:(NSString *)domain +{ + // Domain is the only required part of a JID + if ((domain == nil) || ([domain length] == 0)) + return NO; + + // If there's an @ symbol in the domain it probably means user put @ in their username + NSRange invalidAtRange = [domain rangeOfString:@"@"]; + if (invalidAtRange.location != NSNotFound) + return NO; + + return YES; +} + ++ (BOOL)validateResource:(NSString *)resource +{ + // Can't use an empty string resource name + if ((resource != nil) && ([resource length] == 0)) + return NO; + + return YES; +} + ++ (BOOL)validateUser:(NSString *)user domain:(NSString *)domain resource:(NSString *)resource +{ + if (![self validateDomain:domain]) + return NO; + + if (![self validateResource:resource]) + return NO; + + return YES; +} + ++ (BOOL)parse:(NSString *)jidStr + outUser:(NSString **)user + outDomain:(NSString **)domain + outResource:(NSString **)resource +{ + if(user) *user = nil; + if(domain) *domain = nil; + if(resource) *resource = nil; + + if(jidStr == nil) return NO; + + NSString *rawUser = nil; + NSString *rawDomain = nil; + NSString *rawResource = nil; + + NSRange atRange = [jidStr rangeOfString:@"@"]; + + if (atRange.location != NSNotFound) + { + rawUser = [jidStr substringToIndex:atRange.location]; + + NSString *minusUser = [jidStr substringFromIndex:atRange.location+1]; + + NSRange slashRange = [minusUser rangeOfString:@"/"]; + + if (slashRange.location != NSNotFound) + { + rawDomain = [minusUser substringToIndex:slashRange.location]; + rawResource = [minusUser substringFromIndex:slashRange.location+1]; + } + else + { + rawDomain = minusUser; + } + } + else + { + NSRange slashRange = [jidStr rangeOfString:@"/"]; + + if (slashRange.location != NSNotFound) + { + rawDomain = [jidStr substringToIndex:slashRange.location]; + rawResource = [jidStr substringFromIndex:slashRange.location+1]; + } + else + { + rawDomain = jidStr; + } + } + + NSString *prepUser = [XMPPStringPrep prepNode:rawUser]; + NSString *prepDomain = [XMPPStringPrep prepDomain:rawDomain]; + NSString *prepResource = [XMPPStringPrep prepResource:rawResource]; + + if ([XMPPJID validateUser:prepUser domain:prepDomain resource:prepResource]) + { + if(user) *user = prepUser; + if(domain) *domain = prepDomain; + if(resource) *resource = prepResource; + + return YES; + } + + return NO; +} + ++ (XMPPJID *)jidWithString:(NSString *)jidStr +{ + NSString *user; + NSString *domain; + NSString *resource; + + if ([XMPPJID parse:jidStr outUser:&user outDomain:&domain outResource:&resource]) + { + XMPPJID *jid = [[XMPPJID alloc] init]; + jid->user = [user copy]; + jid->domain = [domain copy]; + jid->resource = [resource copy]; + + return jid; + } + + return nil; +} + ++ (XMPPJID *)jidWithString:(NSString *)jidStr resource:(NSString *)resource +{ + NSString *prepResource = [XMPPStringPrep prepResource:resource]; + if (![self validateResource:prepResource]) return nil; + + NSString *user; + NSString *domain; + + if ([XMPPJID parse:jidStr outUser:&user outDomain:&domain outResource:nil]) + { + XMPPJID *jid = [[XMPPJID alloc] init]; + jid->user = [user copy]; + jid->domain = [domain copy]; + jid->resource = [prepResource copy]; + + return jid; + } + + return nil; +} + ++ (XMPPJID *)jidWithUser:(NSString *)user domain:(NSString *)domain resource:(NSString *)resource +{ + NSString *prepUser = [XMPPStringPrep prepNode:user]; + NSString *prepDomain = [XMPPStringPrep prepDomain:domain]; + NSString *prepResource = [XMPPStringPrep prepResource:resource]; + + if ([XMPPJID validateUser:prepUser domain:prepDomain resource:prepResource]) + { + XMPPJID *jid = [[XMPPJID alloc] init]; + jid->user = [prepUser copy]; + jid->domain = [prepDomain copy]; + jid->resource = [prepResource copy]; + + return jid; + } + + return nil; +} + ++ (XMPPJID *)jidWithPrevalidatedUser:(NSString *)user + prevalidatedDomain:(NSString *)domain + prevalidatedResource:(NSString *)resource +{ + XMPPJID *jid = [[XMPPJID alloc] init]; + jid->user = [user copy]; + jid->domain = [domain copy]; + jid->resource = [resource copy]; + + return jid; +} + ++ (XMPPJID *)jidWithPrevalidatedUser:(NSString *)user + prevalidatedDomain:(NSString *)domain + resource:(NSString *)resource +{ + NSString *prepResource = [XMPPStringPrep prepResource:resource]; + if (![self validateResource:prepResource]) return nil; + + XMPPJID *jid = [[XMPPJID alloc] init]; + jid->user = [user copy]; + jid->domain = [domain copy]; + jid->resource = [prepResource copy]; + + return jid; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Encoding, Decoding: +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if ! TARGET_OS_IPHONE +- (id)replacementObjectForPortCoder:(NSPortCoder *)encoder +{ + if([encoder isBycopy]) + return self; + else + return [super replacementObjectForPortCoder:encoder]; + // return [NSDistantObject proxyWithLocal:self connection:[encoder connection]]; +} +#endif + +- (id)initWithCoder:(NSCoder *)coder +{ + if ((self = [super init])) + { + if ([coder allowsKeyedCoding]) + { + if([coder respondsToSelector:@selector(requiresSecureCoding)] && + [coder requiresSecureCoding]) + { + user = [[coder decodeObjectOfClass:[NSString class] forKey:@"user"] copy]; + domain = [[coder decodeObjectOfClass:[NSString class] forKey:@"domain"] copy]; + resource = [[coder decodeObjectOfClass:[NSString class] forKey:@"resource"] copy]; + } + else + { + user = [[coder decodeObjectForKey:@"user"] copy]; + domain = [[coder decodeObjectForKey:@"domain"] copy]; + resource = [[coder decodeObjectForKey:@"resource"] copy]; + } + } + else + { + user = [[coder decodeObject] copy]; + domain = [[coder decodeObject] copy]; + resource = [[coder decodeObject] copy]; + } + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + if ([coder allowsKeyedCoding]) + { + [coder encodeObject:user forKey:@"user"]; + [coder encodeObject:domain forKey:@"domain"]; + [coder encodeObject:resource forKey:@"resource"]; + } + else + { + [coder encodeObject:user]; + [coder encodeObject:domain]; + [coder encodeObject:resource]; + } +} + ++ (BOOL) supportsSecureCoding +{ + return YES; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Copying: +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (id)copyWithZone:(NSZone *)zone +{ + // This class is immutable + return self; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Normal Methods: +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// Why didn't we just synthesize these properties? +// +// Since these variables are readonly within the class, +// we want the synthesized methods to work like a nonatomic property. +// In order to do this, we have to mark the properties as nonatomic in the header. +// However we don't like marking the property as nonatomic in the header because +// then people might think it's not thread-safe when in fact it is. + +- (NSString *)user +{ + return user; // Why didn't we just synthesize this? See comment above. +} + +- (NSString *)domain +{ + return domain; // Why didn't we just synthesize this? See comment above. +} + +- (NSString *)resource +{ + return resource; // Why didn't we just synthesize this? See comment above. +} + +- (XMPPJID *)bareJID +{ + if (resource == nil) + { + return self; + } + else + { + return [XMPPJID jidWithPrevalidatedUser:user prevalidatedDomain:domain prevalidatedResource:nil]; + } +} + +- (XMPPJID *)domainJID +{ + if (user == nil && resource == nil) + { + return self; + } + else + { + return [XMPPJID jidWithPrevalidatedUser:nil prevalidatedDomain:domain prevalidatedResource:nil]; + } +} + +- (NSString *)bare +{ + if (user) + return [NSString stringWithFormat:@"%@@%@", user, domain]; + else + return domain; +} + +- (NSString *)full +{ + if (user) + { + if (resource) + return [NSString stringWithFormat:@"%@@%@/%@", user, domain, resource]; + else + return [NSString stringWithFormat:@"%@@%@", user, domain]; + } + else + { + if (resource) + return [NSString stringWithFormat:@"%@/%@", domain, resource]; + else + return domain; + } +} + +- (BOOL)isBare +{ + // From RFC 6120 Terminology: + // + // The term "bare JID" refers to an XMPP address of the form (for an account at a server) + // or of the form (for a server). + + return (resource == nil); +} + +- (BOOL)isBareWithUser +{ + return (user != nil && resource == nil); +} + +- (BOOL)isFull +{ + // From RFC 6120 Terminology: + // + // The term "full JID" refers to an XMPP address of the form + // (for a particular authorized client or device associated with an account) + // or of the form (for a particular resource or script associated with a server). + + return (resource != nil); +} + +- (BOOL)isFullWithUser +{ + return (user != nil && resource != nil); +} + +- (BOOL)isServer +{ + return (user == nil); +} + +- (XMPPJID *)jidWithNewResource:(NSString *)newResource +{ + return [XMPPJID jidWithPrevalidatedUser:user prevalidatedDomain:domain resource:newResource]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark NSObject Methods: +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSUInteger)hash +{ + // We used to do this: + // return [[self full] hash]; + // + // It was functional but less than optimal because it required the creation of a new NSString everytime. + // Now the hashing of a string itself is extremely fast, + // so combining 3 hashes is much faster than creating a new string. + // To accomplish this we use the murmur hashing algorithm. + // + // MurmurHash2 was written by Austin Appleby, and is placed in the public domain. + // http://code.google.com/p/smhasher + + NSUInteger uhash = [user hash]; + NSUInteger dhash = [domain hash]; + NSUInteger rhash = [resource hash]; + + if (NSUIntegerMax == UINT32_MAX) // Should be optimized out via compiler since these are constants + { + // MurmurHash2 (32-bit) + // + // uint32_t MurmurHash2 ( const void * key, int len, uint32_t seed ) + // + // Normally one would pass a chunk of data ('key') and associated data chunk length ('len'). + // Instead we're going to use our 3 hashes. + // And we're going to randomly make up a 'seed'. + + const uint32_t seed = 0xa2f1b6f; // Some random value I made up + const uint32_t len = 12; // 3 hashes, each 4 bytes = 12 bytes + + // 'm' and 'r' are mixing constants generated offline. + // They're not really 'magic', they just happen to work well. + + const uint32_t m = 0x5bd1e995; + const int r = 24; + + // Initialize the hash to a 'random' value + + uint32_t h = seed ^ len; + uint32_t k; + + // Mix uhash + + k = uhash; + + k *= m; + k ^= k >> r; + k *= m; + + h *= m; + h ^= k; + + // Mix dhash + + k = dhash; + + k *= m; + k ^= k >> r; + k *= m; + + h *= m; + h ^= k; + + // Mix rhash + + k = rhash; + + k *= m; + k ^= k >> r; + k *= m; + + h *= m; + h ^= k; + + // Do a few final mixes of the hash to ensure the last few + // bytes are well-incorporated. + + h ^= h >> 13; + h *= m; + h ^= h >> 15; + + return (NSUInteger)h; + } + else + { + // MurmurHash2 (64-bit) + // + // uint64_t MurmurHash64A ( const void * key, int len, uint64_t seed ) + // + // Normally one would pass a chunk of data ('key') and associated data chunk length ('len'). + // Instead we're going to use our 3 hashes. + // And we're going to randomly make up a 'seed'. + + const uint32_t seed = 0xa2f1b6f; // Some random value I made up + const uint32_t len = 24; // 3 hashes, each 8 bytes = 24 bytes + + // 'm' and 'r' are mixing constants generated offline. + // They're not really 'magic', they just happen to work well. + + const uint64_t m = 0xc6a4a7935bd1e995LLU; + const int r = 47; + + // Initialize the hash to a 'random' value + + uint64_t h = seed ^ (len * m); + uint64_t k; + + // Mix uhash + + k = uhash; + + k *= m; + k ^= k >> r; + k *= m; + + h ^= k; + h *= m; + + // Mix dhash + + k = dhash; + + k *= m; + k ^= k >> r; + k *= m; + + h ^= k; + h *= m; + + // Mix rhash + + k = rhash; + + k *= m; + k ^= k >> r; + k *= m; + + h ^= k; + h *= m; + + // Do a few final mixes of the hash to ensure the last few + // bytes are well-incorporated. + + h ^= h >> r; + h *= m; + h ^= h >> r; + + return (NSUInteger)h; + } +} + +- (BOOL)isEqual:(id)anObject +{ + if ([anObject isMemberOfClass:[self class]]) + { + return [self isEqualToJID:(XMPPJID *)anObject options:XMPPJIDCompareFull]; + } + return NO; +} + +- (BOOL)isEqualToJID:(XMPPJID *)aJID +{ + return [self isEqualToJID:aJID options:XMPPJIDCompareFull]; +} + +- (BOOL)isEqualToJID:(XMPPJID *)aJID options:(XMPPJIDCompareOptions)mask +{ + if (aJID == nil) return NO; + + if (mask & XMPPJIDCompareUser) + { + if (user) { + if (![user isEqualToString:aJID->user]) return NO; + } + else { + if (aJID->user) return NO; + } + } + + if (mask & XMPPJIDCompareDomain) + { + if (domain) { + if (![domain isEqualToString:aJID->domain]) return NO; + } + else { + if (aJID->domain) return NO; + } + } + + if (mask & XMPPJIDCompareResource) + { + if (resource) { + if (![resource isEqualToString:aJID->resource]) return NO; + } + else { + if (aJID->resource) return NO; + } + } + + return YES; +} + +- (NSString *)description +{ + return [self full]; +} + + +@end diff --git a/Core/XMPPLogging.h b/Core/XMPPLogging.h new file mode 100644 index 0000000..975ee67 --- /dev/null +++ b/Core/XMPPLogging.h @@ -0,0 +1,191 @@ +/** + * In order to provide fast and flexible logging, this project uses Cocoa Lumberjack. + * + * The GitHub project page has a wealth of documentation if you have any questions. + * https://github.com/robbiehanson/CocoaLumberjack + * + * Here's what you need to know concerning how logging is setup for XMPPFramework: + * + * There are 4 log levels: + * - Error + * - Warning + * - Info + * - Verbose + * + * In addition to this, there is a Trace flag that can be enabled. + * When tracing is enabled, it spits out the methods that are being called. + * + * Please note that tracing is separate from the log levels. + * For example, one could set the log level to warning, and enable tracing. + * + * All logging is asynchronous, except errors. + * To use logging within your own custom files, follow the steps below. + * + * Step 1: + * Import this header in your implementation file: + * + * #import "XMPPLogging.h" + * + * Step 2: + * Define your logging level in your implementation file: + * + * // Log levels: off, error, warn, info, verbose + * static const int xmppLogLevel = XMPP_LOG_LEVEL_VERBOSE; + * + * If you wish to enable tracing, you could do something like this: + * + * // Log levels: off, error, warn, info, verbose + * static const int xmppLogLevel = XMPP_LOG_LEVEL_INFO | XMPP_LOG_FLAG_TRACE; + * + * Step 3: + * Replace your NSLog statements with XMPPLog statements according to the severity of the message. + * + * NSLog(@"Fatal error, no dohickey found!"); -> XMPPLogError(@"Fatal error, no dohickey found!"); + * + * XMPPLog has the same syntax as NSLog. + * This means you can pass it multiple variables just like NSLog. + * + * You may optionally choose to define different log levels for debug and release builds. + * You can do so like this: + * + * // Log levels: off, error, warn, info, verbose + * #if DEBUG + * static const int xmppLogLevel = XMPP_LOG_LEVEL_VERBOSE; + * #else + * static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; + * #endif + * + * Xcode projects created with Xcode 4 automatically define DEBUG via the project's preprocessor macros. + * If you created your project with a previous version of Xcode, you may need to add the DEBUG macro manually. +**/ + +#import "CocoaLumberJack/DDLog.h" + +// Global flag to enable/disable logging throughout the entire xmpp framework. + +#ifndef XMPP_LOGGING_ENABLED +#define XMPP_LOGGING_ENABLED 1 +#endif + +// Define logging context for every log message coming from the XMPP framework. +// The logging context can be extracted from the DDLogMessage from within the logging framework. +// This gives loggers, formatters, and filters the ability to optionally process them differently. + +#define XMPP_LOG_CONTEXT 5222 + +// Configure log levels. + +#define XMPP_LOG_FLAG_ERROR (1 << 0) // 0...00001 +#define XMPP_LOG_FLAG_WARN (1 << 1) // 0...00010 +#define XMPP_LOG_FLAG_INFO (1 << 2) // 0...00100 +#define XMPP_LOG_FLAG_VERBOSE (1 << 3) // 0...01000 + +#define XMPP_LOG_LEVEL_OFF 0 // 0...00000 +#define XMPP_LOG_LEVEL_ERROR (XMPP_LOG_LEVEL_OFF | XMPP_LOG_FLAG_ERROR) // 0...00001 +#define XMPP_LOG_LEVEL_WARN (XMPP_LOG_LEVEL_ERROR | XMPP_LOG_FLAG_WARN) // 0...00011 +#define XMPP_LOG_LEVEL_INFO (XMPP_LOG_LEVEL_WARN | XMPP_LOG_FLAG_INFO) // 0...00111 +#define XMPP_LOG_LEVEL_VERBOSE (XMPP_LOG_LEVEL_INFO | XMPP_LOG_FLAG_VERBOSE) // 0...01111 + +// Setup fine grained logging. +// The first 4 bits are being used by the standard log levels (0 - 3) +// +// We're going to add tracing, but NOT as a log level. +// Tracing can be turned on and off independently of log level. + +#define XMPP_LOG_FLAG_TRACE (1 << 4) // 0...10000 + +// Setup the usual boolean macros. + +#define XMPP_LOG_ERROR (xmppLogLevel & XMPP_LOG_FLAG_ERROR) +#define XMPP_LOG_WARN (xmppLogLevel & XMPP_LOG_FLAG_WARN) +#define XMPP_LOG_INFO (xmppLogLevel & XMPP_LOG_FLAG_INFO) +#define XMPP_LOG_VERBOSE (xmppLogLevel & XMPP_LOG_FLAG_VERBOSE) +#define XMPP_LOG_TRACE (xmppLogLevel & XMPP_LOG_FLAG_TRACE) + +// Configure asynchronous logging. +// We follow the default configuration, +// but we reserve a special macro to easily disable asynchronous logging for debugging purposes. + +#if DEBUG +#define XMPP_LOG_ASYNC_ENABLED NO +#else +#define XMPP_LOG_ASYNC_ENABLED YES +#endif + +#define XMPP_LOG_ASYNC_ERROR ( NO && XMPP_LOG_ASYNC_ENABLED) +#define XMPP_LOG_ASYNC_WARN (YES && XMPP_LOG_ASYNC_ENABLED) +#define XMPP_LOG_ASYNC_INFO (YES && XMPP_LOG_ASYNC_ENABLED) +#define XMPP_LOG_ASYNC_VERBOSE (YES && XMPP_LOG_ASYNC_ENABLED) +#define XMPP_LOG_ASYNC_TRACE (YES && XMPP_LOG_ASYNC_ENABLED) + +// Define logging primitives. +// These are primarily wrappers around the macros defined in Lumberjack's DDLog.h header file. + +#define XMPP_LOG_OBJC_MAYBE(async, lvl, flg, ctx, frmt, ...) \ + do{ if(XMPP_LOGGING_ENABLED) LOG_MAYBE(async, lvl, flg, ctx, sel_getName(_cmd), frmt, ##__VA_ARGS__); } while(0) + +#define XMPP_LOG_C_MAYBE(async, lvl, flg, ctx, frmt, ...) \ + do{ if(XMPP_LOGGING_ENABLED) LOG_MAYBE(async, lvl, flg, ctx, __FUNCTION__, frmt, ##__VA_ARGS__); } while(0) + + +#define XMPPLogError(frmt, ...) XMPP_LOG_OBJC_MAYBE(XMPP_LOG_ASYNC_ERROR, xmppLogLevel, XMPP_LOG_FLAG_ERROR, \ + XMPP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + +#define XMPPLogWarn(frmt, ...) XMPP_LOG_OBJC_MAYBE(XMPP_LOG_ASYNC_WARN, xmppLogLevel, XMPP_LOG_FLAG_WARN, \ + XMPP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + +#define XMPPLogInfo(frmt, ...) XMPP_LOG_OBJC_MAYBE(XMPP_LOG_ASYNC_INFO, xmppLogLevel, XMPP_LOG_FLAG_INFO, \ + XMPP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + +#define XMPPLogVerbose(frmt, ...) XMPP_LOG_OBJC_MAYBE(XMPP_LOG_ASYNC_VERBOSE, xmppLogLevel, XMPP_LOG_FLAG_VERBOSE, \ + XMPP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + +#define XMPPLogTrace() XMPP_LOG_OBJC_MAYBE(XMPP_LOG_ASYNC_TRACE, xmppLogLevel, XMPP_LOG_FLAG_TRACE, \ + XMPP_LOG_CONTEXT, @"%@: %@", THIS_FILE, THIS_METHOD) + +#define XMPPLogTrace2(frmt, ...) XMPP_LOG_OBJC_MAYBE(XMPP_LOG_ASYNC_TRACE, xmppLogLevel, XMPP_LOG_FLAG_TRACE, \ + XMPP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + + +#define XMPPLogCError(frmt, ...) XMPP_LOG_C_MAYBE(XMPP_LOG_ASYNC_ERROR, xmppLogLevel, XMPP_LOG_FLAG_ERROR, \ + XMPP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + +#define XMPPLogCWarn(frmt, ...) XMPP_LOG_C_MAYBE(XMPP_LOG_ASYNC_WARN, xmppLogLevel, XMPP_LOG_FLAG_WARN, \ + XMPP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + +#define XMPPLogCInfo(frmt, ...) XMPP_LOG_C_MAYBE(XMPP_LOG_ASYNC_INFO, xmppLogLevel, XMPP_LOG_FLAG_INFO, \ + XMPP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + +#define XMPPLogCVerbose(frmt, ...) XMPP_LOG_C_MAYBE(XMPP_LOG_ASYNC_VERBOSE, xmppLogLevel, XMPP_LOG_FLAG_VERBOSE, \ + XMPP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + +#define XMPPLogCTrace() XMPP_LOG_C_MAYBE(XMPP_LOG_ASYNC_TRACE, xmppLogLevel, XMPP_LOG_FLAG_TRACE, \ + XMPP_LOG_CONTEXT, @"%@: %s", THIS_FILE, __FUNCTION__) + +#define XMPPLogCTrace2(frmt, ...) XMPP_LOG_C_MAYBE(XMPP_LOG_ASYNC_TRACE, xmppLogLevel, XMPP_LOG_FLAG_TRACE, \ + XMPP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + +// Setup logging for XMPPStream (and subclasses such as XMPPStreamFacebook) + +#define XMPP_LOG_FLAG_SEND (1 << 5) +#define XMPP_LOG_FLAG_RECV_PRE (1 << 6) // Prints data before it goes to the parser +#define XMPP_LOG_FLAG_RECV_POST (1 << 7) // Prints data as it comes out of the parser + +#define XMPP_LOG_FLAG_SEND_RECV (XMPP_LOG_FLAG_SEND | XMPP_LOG_FLAG_RECV_POST) + +#define XMPP_LOG_SEND (xmppLogLevel & XMPP_LOG_FLAG_SEND) +#define XMPP_LOG_RECV_PRE (xmppLogLevel & XMPP_LOG_FLAG_RECV_PRE) +#define XMPP_LOG_RECV_POST (xmppLogLevel & XMPP_LOG_FLAG_RECV_POST) + +#define XMPP_LOG_ASYNC_SEND (YES && XMPP_LOG_ASYNC_ENABLED) +#define XMPP_LOG_ASYNC_RECV_PRE (YES && XMPP_LOG_ASYNC_ENABLED) +#define XMPP_LOG_ASYNC_RECV_POST (YES && XMPP_LOG_ASYNC_ENABLED) + +#define XMPPLogSend(format, ...) XMPP_LOG_OBJC_MAYBE(XMPP_LOG_ASYNC_SEND, xmppLogLevel, \ + XMPP_LOG_FLAG_SEND, XMPP_LOG_CONTEXT, format, ##__VA_ARGS__) + +#define XMPPLogRecvPre(format, ...) XMPP_LOG_OBJC_MAYBE(XMPP_LOG_ASYNC_RECV_PRE, xmppLogLevel, \ + XMPP_LOG_FLAG_RECV_PRE, XMPP_LOG_CONTEXT, format, ##__VA_ARGS__) + +#define XMPPLogRecvPost(format, ...) XMPP_LOG_OBJC_MAYBE(XMPP_LOG_ASYNC_RECV_POST, xmppLogLevel, \ + XMPP_LOG_FLAG_RECV_POST, XMPP_LOG_CONTEXT, format, ##__VA_ARGS__) diff --git a/Core/XMPPMessage.h b/Core/XMPPMessage.h new file mode 100644 index 0000000..f34e6d3 --- /dev/null +++ b/Core/XMPPMessage.h @@ -0,0 +1,55 @@ +#import +#import "XMPPElement.h" + +/** + * The XMPPMessage class represents a element. + * It extends XMPPElement, which in turn extends NSXMLElement. + * All elements that go in and out of the + * xmpp stream will automatically be converted to XMPPMessage objects. + * + * This class exists to provide developers an easy way to add functionality to message processing. + * Simply add your own category to XMPPMessage to extend it with your own custom methods. +**/ + +@interface XMPPMessage : XMPPElement + +// Converts an NSXMLElement to an XMPPMessage element in place (no memory allocations or copying) ++ (XMPPMessage *)messageFromElement:(NSXMLElement *)element; + ++ (XMPPMessage *)message; ++ (XMPPMessage *)messageWithType:(NSString *)type; ++ (XMPPMessage *)messageWithType:(NSString *)type to:(XMPPJID *)to; ++ (XMPPMessage *)messageWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid; ++ (XMPPMessage *)messageWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid child:(NSXMLElement *)childElement; ++ (XMPPMessage *)messageWithType:(NSString *)type elementID:(NSString *)eid; ++ (XMPPMessage *)messageWithType:(NSString *)type elementID:(NSString *)eid child:(NSXMLElement *)childElement; ++ (XMPPMessage *)messageWithType:(NSString *)type child:(NSXMLElement *)childElement; + +- (id)init; +- (id)initWithType:(NSString *)type; +- (id)initWithType:(NSString *)type to:(XMPPJID *)to; +- (id)initWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid; +- (id)initWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid child:(NSXMLElement *)childElement; +- (id)initWithType:(NSString *)type elementID:(NSString *)eid; +- (id)initWithType:(NSString *)type elementID:(NSString *)eid child:(NSXMLElement *)childElement; +- (id)initWithType:(NSString *)type child:(NSXMLElement *)childElement; + +- (NSString *)type; +- (NSString *)subject; +- (NSString *)body; +- (NSString *)bodyForLanguage:(NSString *)language; +- (NSString *)thread; + +- (void)addSubject:(NSString *)subject; +- (void)addBody:(NSString *)body; +- (void)addBody:(NSString *)body withLanguage:(NSString *)language; +- (void)addThread:(NSString *)thread; + +- (BOOL)isChatMessage; +- (BOOL)isChatMessageWithBody; +- (BOOL)isErrorMessage; +- (BOOL)isMessageWithBody; + +- (NSError *)errorMessage; + +@end diff --git a/Core/XMPPMessage.m b/Core/XMPPMessage.m new file mode 100644 index 0000000..2a44aea --- /dev/null +++ b/Core/XMPPMessage.m @@ -0,0 +1,267 @@ +#import "XMPPMessage.h" +#import "XMPPJID.h" +#import "NSXMLElement+XMPP.h" + +#import + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@implementation XMPPMessage + +#if DEBUG + ++ (void)initialize +{ + // We use the object_setClass method below to dynamically change the class from a standard NSXMLElement. + // The size of the two classes is expected to be the same. + // + // If a developer adds instance methods to this class, bad things happen at runtime that are very hard to debug. + // This check is here to aid future developers who may make this mistake. + // + // For Fearless And Experienced Objective-C Developers: + // It may be possible to support adding instance variables to this class if you seriously need it. + // To do so, try realloc'ing self after altering the class, and then initialize your variables. + + size_t superSize = class_getInstanceSize([NSXMLElement class]); + size_t ourSize = class_getInstanceSize([XMPPMessage class]); + + if (superSize != ourSize) + { + NSLog(@"Adding instance variables to XMPPMessage is not currently supported!"); + exit(15); + } +} + +#endif + ++ (XMPPMessage *)messageFromElement:(NSXMLElement *)element +{ + object_setClass(element, [XMPPMessage class]); + + return (XMPPMessage *)element; +} + ++ (XMPPMessage *)message +{ + return [[XMPPMessage alloc] init]; +} + ++ (XMPPMessage *)messageWithType:(NSString *)type +{ + return [[XMPPMessage alloc] initWithType:type to:nil]; +} + ++ (XMPPMessage *)messageWithType:(NSString *)type to:(XMPPJID *)to +{ + return [[XMPPMessage alloc] initWithType:type to:to]; +} + ++ (XMPPMessage *)messageWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid +{ + return [[XMPPMessage alloc] initWithType:type to:jid elementID:eid]; +} + ++ (XMPPMessage *)messageWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid child:(NSXMLElement *)childElement +{ + return [[XMPPMessage alloc] initWithType:type to:jid elementID:eid child:childElement]; +} + ++ (XMPPMessage *)messageWithType:(NSString *)type elementID:(NSString *)eid +{ + return [[XMPPMessage alloc] initWithType:type elementID:eid]; +} + ++ (XMPPMessage *)messageWithType:(NSString *)type elementID:(NSString *)eid child:(NSXMLElement *)childElement +{ + return [[XMPPMessage alloc] initWithType:type elementID:eid child:childElement]; +} + ++ (XMPPMessage *)messageWithType:(NSString *)type child:(NSXMLElement *)childElement +{ + return [[XMPPMessage alloc] initWithType:type child:childElement]; +} + +- (id)init +{ + return [self initWithType:nil to:nil elementID:nil child:nil]; +} + +- (id)initWithType:(NSString *)type +{ + return [self initWithType:type to:nil elementID:nil child:nil]; +} + +- (id)initWithType:(NSString *)type to:(XMPPJID *)jid +{ + return [self initWithType:type to:jid elementID:nil child:nil]; +} + +- (id)initWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid +{ + return [self initWithType:type to:jid elementID:eid child:nil]; +} + +- (id)initWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid child:(NSXMLElement *)childElement +{ + if ((self = [super initWithName:@"message"])) + { + if (type) + [self addAttributeWithName:@"type" stringValue:type]; + + if (jid) + [self addAttributeWithName:@"to" stringValue:[jid full]]; + + if (eid) + [self addAttributeWithName:@"id" stringValue:eid]; + + if (childElement) + [self addChild:childElement]; + } + return self; +} + +- (id)initWithType:(NSString *)type elementID:(NSString *)eid +{ + return [self initWithType:type to:nil elementID:eid child:nil]; +} + +- (id)initWithType:(NSString *)type elementID:(NSString *)eid child:(NSXMLElement *)childElement +{ + return [self initWithType:type to:nil elementID:eid child:childElement]; +} + +- (id)initWithType:(NSString *)type child:(NSXMLElement *)childElement +{ + return [self initWithType:type to:nil elementID:nil child:childElement]; +} + +- (id)initWithXMLString:(NSString *)string error:(NSError *__autoreleasing *)error +{ + if((self = [super initWithXMLString:string error:error])){ + self = [XMPPMessage messageFromElement:self]; + } + return self; +} + +- (id)copyWithZone:(NSZone *)zone +{ + NSXMLElement *element = [super copyWithZone:zone]; + return [XMPPMessage messageFromElement:element]; +} + +- (NSString *)type +{ + return [[self attributeForName:@"type"] stringValue]; +} + +- (NSString *)subject +{ + return [[self elementForName:@"subject"] stringValue]; +} + +- (NSString *)body +{ + return [[self elementForName:@"body"] stringValue]; +} + +- (NSString *)bodyForLanguage:(NSString *)language +{ + NSString *bodyForLanguage = nil; + + for (NSXMLElement *bodyElement in [self elementsForName:@"body"]) + { + NSString *lang = [[bodyElement attributeForName:@"xml:lang"] stringValue]; + + // Openfire strips off the xml prefix + if (lang == nil) + { + lang = [[bodyElement attributeForName:@"lang"] stringValue]; + } + + if ([language isEqualToString:lang] || ([language length] == 0 && [lang length] == 0)) + { + bodyForLanguage = [bodyElement stringValue]; + break; + } + } + + return bodyForLanguage; +} + +- (NSString *)thread +{ + return [[self elementForName:@"thread"] stringValue]; +} + +- (void)addSubject:(NSString *)subject +{ + NSXMLElement *subjectElement = [NSXMLElement elementWithName:@"subject" stringValue:subject]; + [self addChild:subjectElement]; +} + +- (void)addBody:(NSString *)body +{ + NSXMLElement *bodyElement = [NSXMLElement elementWithName:@"body" stringValue:body]; + [self addChild:bodyElement]; +} + +- (void)addBody:(NSString *)body withLanguage:(NSString *)language +{ + NSXMLElement *bodyElement = [NSXMLElement elementWithName:@"body" stringValue:body]; + + if ([language length]) + { + [bodyElement addAttributeWithName:@"xml:lang" stringValue:language]; + } + + [self addChild:bodyElement]; +} + +- (void)addThread:(NSString *)thread +{ + NSXMLElement *threadElement = [NSXMLElement elementWithName:@"thread" stringValue:thread]; + [self addChild:threadElement]; +} + +- (BOOL)isChatMessage +{ + return [[[self attributeForName:@"type"] stringValue] isEqualToString:@"chat"]; +} + +- (BOOL)isChatMessageWithBody +{ + if ([self isChatMessage]) + { + return [self isMessageWithBody]; + } + + return NO; +} + +- (BOOL)isErrorMessage +{ + return [[[self attributeForName:@"type"] stringValue] isEqualToString:@"error"]; +} + +- (NSError *)errorMessage +{ + if (![self isErrorMessage]) { + return nil; + } + + NSXMLElement *error = [self elementForName:@"error"]; + return [NSError errorWithDomain:@"urn:ietf:params:xml:ns:xmpp-stanzas" + code:[error attributeIntValueForName:@"code"] + userInfo:@{NSLocalizedDescriptionKey : [error compactXMLString]}]; + +} + +- (BOOL)isMessageWithBody +{ + return ([self elementForName:@"body"] != nil); +} + +@end diff --git a/Core/XMPPModule.h b/Core/XMPPModule.h new file mode 100644 index 0000000..73af16c --- /dev/null +++ b/Core/XMPPModule.h @@ -0,0 +1,43 @@ +#import +#import "GCDMulticastDelegate.h" + +@class XMPPStream; + +/** + * XMPPModule is the base class that all extensions/modules inherit. + * They automatically get: + * + * - A dispatch queue. + * - A multicast delegate that automatically invokes added delegates. + * + * The module also automatically registers/unregisters itself with the + * xmpp stream during the activate/deactive methods. +**/ +@interface XMPPModule : NSObject +{ + XMPPStream *xmppStream; + + dispatch_queue_t moduleQueue; + void *moduleQueueTag; + + id multicastDelegate; +} + +@property (readonly) dispatch_queue_t moduleQueue; +@property (readonly) void *moduleQueueTag; + +@property (strong, readonly) XMPPStream *xmppStream; + +- (id)init; +- (id)initWithDispatchQueue:(dispatch_queue_t)queue; + +- (BOOL)activate:(XMPPStream *)aXmppStream; +- (void)deactivate; + +- (void)addDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; +- (void)removeDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; +- (void)removeDelegate:(id)delegate; + +- (NSString *)moduleName; + +@end diff --git a/Core/XMPPModule.m b/Core/XMPPModule.m new file mode 100644 index 0000000..24ba9d3 --- /dev/null +++ b/Core/XMPPModule.m @@ -0,0 +1,224 @@ +#import "XMPPModule.h" +#import "XMPPStream.h" +#import "XMPPLogging.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@implementation XMPPModule + +/** + * Standard init method. +**/ +- (id)init +{ + return [self initWithDispatchQueue:NULL]; +} + +/** + * Designated initializer. +**/ +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + if ((self = [super init])) + { + if (queue) + { + moduleQueue = queue; + #if !OS_OBJECT_USE_OBJC + dispatch_retain(moduleQueue); + #endif + } + else + { + const char *moduleQueueName = [[self moduleName] UTF8String]; + moduleQueue = dispatch_queue_create(moduleQueueName, NULL); + } + + moduleQueueTag = &moduleQueueTag; + dispatch_queue_set_specific(moduleQueue, moduleQueueTag, moduleQueueTag, NULL); + + multicastDelegate = [[GCDMulticastDelegate alloc] init]; + } + return self; +} + +- (void)dealloc +{ + #if !OS_OBJECT_USE_OBJC + dispatch_release(moduleQueue); + #endif +} + +/** + * The activate method is the point at which the module gets plugged into the xmpp stream. + * + * It is recommended that subclasses override didActivate, instead of this method, + * to perform any custom actions upon activation. +**/ +- (BOOL)activate:(XMPPStream *)aXmppStream +{ + __block BOOL result = YES; + + dispatch_block_t block = ^{ + + if (xmppStream != nil) + { + result = NO; + } + else + { + xmppStream = aXmppStream; + + [xmppStream addDelegate:self delegateQueue:moduleQueue]; + [xmppStream registerModule:self]; + + [self didActivate]; + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +/** + * It is recommended that subclasses override this method (instead of activate:) + * to perform tasks after the module has been activated. + * + * This method is only invoked if the module is successfully activated. + * This method is always invoked on the moduleQueue. +**/ +- (void)didActivate +{ + // Override me to do custom work after the module is activated +} + +/** + * The deactivate method unplugs a module from the xmpp stream. + * When this method returns, no further delegate methods on this module will be dispatched. + * However, there may be delegate methods that have already been dispatched. + * If this is the case, the module will be properly retained until the delegate methods have completed. + * If your custom module requires that delegate methods are not run after the deactivate method has been run, + * then simply check the xmppStream variable in your delegate methods. + * + * It is recommended that subclasses override didDeactivate, instead of this method, + * to perform any custom actions upon deactivation. +**/ +- (void)deactivate +{ + dispatch_block_t block = ^{ + + if (xmppStream) + { + [self willDeactivate]; + + [xmppStream removeDelegate:self delegateQueue:moduleQueue]; + [xmppStream unregisterModule:self]; + + xmppStream = nil; + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); +} + +/** + * It is recommended that subclasses override this method (instead of deactivate:) + * to perform tasks after the module has been deactivated. + * + * This method is only invoked if the module is transitioning from activated to deactivated. + * This method is always invoked on the moduleQueue. +**/ +- (void)willDeactivate +{ + // Override me to do custom work after the module is deactivated +} + +- (dispatch_queue_t)moduleQueue +{ + return moduleQueue; +} + +- (void *)moduleQueueTag +{ + return moduleQueueTag; +} + +- (XMPPStream *)xmppStream +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return xmppStream; + } + else + { + __block XMPPStream *result; + + dispatch_sync(moduleQueue, ^{ + result = xmppStream; + }); + + return result; + } +} + +- (void)addDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue +{ + // Asynchronous operation (if outside xmppQueue) + + dispatch_block_t block = ^{ + [multicastDelegate addDelegate:delegate delegateQueue:delegateQueue]; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (void)removeDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue synchronously:(BOOL)synchronously +{ + dispatch_block_t block = ^{ + [multicastDelegate removeDelegate:delegate delegateQueue:delegateQueue]; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else if (synchronously) + dispatch_sync(moduleQueue, block); + else + dispatch_async(moduleQueue, block); + +} +- (void)removeDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue +{ + // Synchronous operation (common-case default) + + [self removeDelegate:delegate delegateQueue:delegateQueue synchronously:YES]; +} + +- (void)removeDelegate:(id)delegate +{ + // Synchronous operation (common-case default) + + [self removeDelegate:delegate delegateQueue:NULL synchronously:YES]; +} + +- (NSString *)moduleName +{ + // Override me (if needed) to provide a customized module name. + // This name is used as the name of the dispatch_queue which could aid in debugging. + + return NSStringFromClass([self class]); +} + +@end diff --git a/Core/XMPPParser.h b/Core/XMPPParser.h new file mode 100644 index 0000000..cff2587 --- /dev/null +++ b/Core/XMPPParser.h @@ -0,0 +1,40 @@ +#import + +#if TARGET_OS_IPHONE + #import "DDXML.h" +#endif + + +@interface XMPPParser : NSObject + +- (id)initWithDelegate:(id)delegate delegateQueue:(dispatch_queue_t)dq; +- (id)initWithDelegate:(id)delegate delegateQueue:(dispatch_queue_t)dq parserQueue:(dispatch_queue_t)pq; + +- (void)setDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; + +/** + * Asynchronously parses the given data. + * The delegate methods will be dispatch_async'd as events occur. +**/ +- (void)parseData:(NSData *)data; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPParserDelegate +@optional + +- (void)xmppParser:(XMPPParser *)sender didReadRoot:(NSXMLElement *)root; + +- (void)xmppParser:(XMPPParser *)sender didReadElement:(NSXMLElement *)element; + +- (void)xmppParserDidEnd:(XMPPParser *)sender; + +- (void)xmppParser:(XMPPParser *)sender didFail:(NSError *)error; + +- (void)xmppParserDidParseData:(XMPPParser *)sender; + +@end diff --git a/Core/XMPPParser.m b/Core/XMPPParser.m new file mode 100644 index 0000000..692da86 --- /dev/null +++ b/Core/XMPPParser.m @@ -0,0 +1,850 @@ +#import "XMPPParser.h" +#import "XMPPLogging.h" +#import +#import + +#if TARGET_OS_IPHONE + #import "DDXMLPrivate.h" +#endif + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +#define CHECK_FOR_NULL(value) \ + do { \ + if (value == NULL) { \ + xmpp_xmlAbortDueToMemoryShortage(ctxt); \ + return; \ + } \ + } while(false) + +#if !TARGET_OS_IPHONE + static void xmpp_recursiveAddChild(NSXMLElement *parent, xmlNodePtr childNode); +#endif + +@implementation XMPPParser +{ + #if __has_feature(objc_arc_weak) + __weak id delegate; + #else + __unsafe_unretained id delegate; + #endif + dispatch_queue_t delegateQueue; + + dispatch_queue_t parserQueue; + void *xmppParserQueueTag; + + BOOL hasReportedRoot; + unsigned depth; + + xmlParserCtxt *parserCtxt; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark iPhone +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if TARGET_OS_IPHONE + +static void xmpp_onDidReadRoot(XMPPParser *parser, xmlNodePtr root) +{ + if (parser->delegateQueue && [parser->delegate respondsToSelector:@selector(xmppParser:didReadRoot:)]) + { + // We first copy the root node. + // We do this to allow the delegate to retain and make changes to the reported root + // without affecting the underlying xmpp parser. + + // xmlCopyNode(const xmlNodePtr node, int extended) + // + // node: + // the node to copy + // extended: + // if 1 do a recursive copy (properties, namespaces and children when applicable) + // if 2 copy properties and namespaces (when applicable) + + xmlNodePtr rootCopy = xmlCopyNode(root, 2); + DDXMLElement *rootCopyWrapper = [DDXMLElement nodeWithElementPrimitive:rootCopy owner:nil]; + + __strong id theDelegate = parser->delegate; + + dispatch_async(parser->delegateQueue, ^{ @autoreleasepool { + + [theDelegate xmppParser:parser didReadRoot:rootCopyWrapper]; + }}); + + // Note: DDXMLElement will properly free the rootCopy when it's deallocated. + } +} + +static void xmpp_onDidReadElement(XMPPParser *parser, xmlNodePtr child) +{ + // Detach the child from the xml tree. + // + // clean: Nullify next, prev, parent and doc pointers of child. + // fixNamespaces: Recurse through subtree, and ensure no namespaces are pointing to xmlNs nodes outside the tree. + // E.G. in a parent node that will no longer be available after the child is detached. + // + // We don't need to fix namespaces since we used xmpp_xmlSearchNs() to ensure we never created any + // namespaces outside the subtree of the child in the first place. + + [DDXMLNode detachChild:child andClean:YES andFixNamespaces:NO]; + + DDXMLElement *childWrapper = [DDXMLElement nodeWithElementPrimitive:child owner:nil]; + + // Note: We want to detach the child from the root even if the delegate method isn't setup. + // This prevents the doc from growing infinitely large. + + if (parser->delegateQueue && [parser->delegate respondsToSelector:@selector(xmppParser:didReadElement:)]) + { + __strong id theDelegate = parser->delegate; + + dispatch_async(parser->delegateQueue, ^{ @autoreleasepool { + + [theDelegate xmppParser:parser didReadElement:childWrapper]; + }}); + } + + // Note: DDXMLElement will properly free the child when it's deallocated. +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Mac +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#else + +static void xmpp_setName(NSXMLElement *element, xmlNodePtr node) +{ + // Remember: The NSString initWithUTF8String raises an exception if passed NULL + + if (node->name == NULL) + { + [element setName:@""]; + return; + } + + if ((node->ns != NULL) && (node->ns->prefix != NULL)) + { + // E.g: + + NSString *prefix = [[NSString alloc] initWithUTF8String:(const char *)node->ns->prefix]; + NSString *name = [[NSString alloc] initWithUTF8String:(const char *)node->name]; + + NSString *elementName = [[NSString alloc] initWithFormat:@"%@:%@", prefix, name]; + [element setName:elementName]; + + } + else + { + NSString *elementName = [[NSString alloc] initWithUTF8String:(const char *)node->name]; + [element setName:elementName]; + } +} + +static void xmpp_addNamespaces(NSXMLElement *element, xmlNodePtr node) +{ + // Remember: The NSString initWithUTF8String raises an exception if passed NULL + + xmlNsPtr nsNode = node->nsDef; + while (nsNode != NULL) + { + if (nsNode->href == NULL) + { + // Namespace doesn't have a value! + } + else + { + NSXMLNode *ns = [[NSXMLNode alloc] initWithKind:NSXMLNamespaceKind]; + + if (nsNode->prefix != NULL) + { + NSString *nsName = [[NSString alloc] initWithUTF8String:(const char *)nsNode->prefix]; + [ns setName:nsName]; + } + else + { + // Default namespace. + // E.g: xmlns="deusty.com" + + [ns setName:@""]; + } + + NSString *nsValue = [[NSString alloc] initWithUTF8String:(const char *)nsNode->href]; + [ns setStringValue:nsValue]; + + [element addNamespace:ns]; + } + + nsNode = nsNode->next; + } +} + +static void xmpp_addChildren(NSXMLElement *element, xmlNodePtr node) +{ + // Remember: The NSString initWithUTF8String raises an exception if passed NULL + + xmlNodePtr childNode = node->children; + while (childNode != NULL) + { + if (childNode->type == XML_ELEMENT_NODE) + { + xmpp_recursiveAddChild(element, childNode); + } + else if (childNode->type == XML_TEXT_NODE) + { + if (childNode->content != NULL) + { + NSString *value = [[NSString alloc] initWithUTF8String:(const char *)childNode->content]; + [element setStringValue:value]; + } + } + + childNode = childNode->next; + } +} + +static void xmpp_addAttributes(NSXMLElement *element, xmlNodePtr node) +{ + // Remember: The NSString initWithUTF8String raises an exception if passed NULL + + xmlAttrPtr attrNode = node->properties; + while (attrNode != NULL) + { + if (attrNode->name == NULL) + { + // Attribute doesn't have a name! + } + else if (attrNode->children == NULL) + { + // Attribute doesn't have a value node! + } + else if (attrNode->children->content == NULL) + { + // Attribute doesn't have a value! + } + else + { + NSXMLNode *attr = [[NSXMLNode alloc] initWithKind:NSXMLAttributeKind]; + + if ((attrNode->ns != NULL) && (attrNode->ns->prefix != NULL)) + { + // E.g: + + NSString *prefix = [[NSString alloc] initWithUTF8String:(const char *)attrNode->ns->prefix]; + NSString *name = [[NSString alloc] initWithUTF8String:(const char *)attrNode->name]; + + NSString *attrName = [[NSString alloc] initWithFormat:@"%@:%@", prefix, name]; + [attr setName:attrName]; + + } + else + { + NSString *attrName = [[NSString alloc] initWithUTF8String:(const char *)attrNode->name]; + [attr setName:attrName]; + } + + NSString *attrValue = [[NSString alloc] initWithUTF8String:(const char *)attrNode->children->content]; + [attr setStringValue:attrValue]; + + [element addAttribute:attr]; + } + + attrNode = attrNode->next; + } +} + +/** + * Recursively adds all the child elements to the given parent. + * + * Note: This method is almost the same as xmpp_nsxmlFromLibxml, with one important difference. + * It doen't add any objects to the autorelease pool (xmpp_nsxmlFromLibXml has return value). +**/ +static void xmpp_recursiveAddChild(NSXMLElement *parent, xmlNodePtr childNode) +{ + // Remember: The NSString initWithUTF8String raises an exception if passed NULL + + NSXMLElement *child = [[NSXMLElement alloc] initWithKind:NSXMLElementKind]; + + xmpp_setName(child, childNode); + + xmpp_addNamespaces(child, childNode); + + xmpp_addChildren(child, childNode); + xmpp_addAttributes(child, childNode); + + [parent addChild:child]; +} + +/** + * Creates and returns an NSXMLElement from the given node. + * Use this method after finding the root element, or root.child element. +**/ +static NSXMLElement* xmpp_nsxmlFromLibxml(xmlNodePtr rootNode) +{ + // Remember: The NSString initWithUTF8String raises an exception if passed NULL + + NSXMLElement *root = [[NSXMLElement alloc] initWithKind:NSXMLElementKind]; + + xmpp_setName(root, rootNode); + + xmpp_addNamespaces(root, rootNode); + + xmpp_addChildren(root, rootNode); + xmpp_addAttributes(root, rootNode); + + return root; +} + +static void xmpp_onDidReadRoot(XMPPParser *parser, xmlNodePtr root) +{ + if (parser->delegateQueue && [parser->delegate respondsToSelector:@selector(xmppParser:didReadRoot:)]) + { + NSXMLElement *nsRoot = xmpp_nsxmlFromLibxml(root); + + __strong id theDelegate = parser->delegate; + + dispatch_async(parser->delegateQueue, ^{ @autoreleasepool { + + [theDelegate xmppParser:parser didReadRoot:nsRoot]; + }}); + } +} + +static void xmpp_onDidReadElement(XMPPParser *parser, xmlNodePtr child) +{ + if (parser->delegateQueue && [parser->delegate respondsToSelector:@selector(xmppParser:didReadElement:)]) + { + NSXMLElement *nsChild = xmpp_nsxmlFromLibxml(child); + + __strong id theDelegate = parser->delegate; + + dispatch_async(parser->delegateQueue, ^{ @autoreleasepool { + + [theDelegate xmppParser:parser didReadElement:nsChild]; + }}); + } + + // Note: We want to detach the child from the root even if the delegate method isn't setup. + // This prevents the doc from growing infinitely large. + + // Detach and free child to keep memory footprint small + xmlUnlinkNode(child); + xmlFreeNode(child); +} + +#endif + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Common +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method is called at the end of the xmlStartElement method. + * This allows us to inspect the parser and xml tree, and determine if we need to invoke any delegate methods. +**/ +static void xmpp_postStartElement(xmlParserCtxt *ctxt) +{ + XMPPParser *parser = (__bridge XMPPParser *)ctxt->_private; + parser->depth++; + + if (!(parser->hasReportedRoot) && (parser->depth == 1)) + { + // We've received the full root - report it to the delegate + + if (ctxt->myDoc) + { + xmlNodePtr root = xmlDocGetRootElement(ctxt->myDoc); + if (root) + { + xmpp_onDidReadRoot(parser, root); + + parser->hasReportedRoot = YES; + } + } + } +} + +/** + * This method is called at the end of the xmlEndElement method. + * This allows us to inspect the parser and xml tree, and determine if we need to invoke any delegate methods. +**/ +static void xmpp_postEndElement(xmlParserCtxt *ctxt) +{ + XMPPParser *parser = (__bridge XMPPParser *)ctxt->_private; + parser->depth--; + + if (parser->depth == 1) + { + // End of full xmpp element. + // That is, a child of the root element. + // Extract the child, and pass it to the delegate. + + xmlDocPtr doc = ctxt->myDoc; + xmlNodePtr root = xmlDocGetRootElement(doc); + + xmlNodePtr child = root->children; + while (child != NULL) + { + if (child->type == XML_ELEMENT_NODE) + { + xmpp_onDidReadElement(parser, child); + + // Exit while loop + break; + } + + child = child->next; + } + } + else if (parser->depth == 0) + { + // End of the root element + + if (parser->delegateQueue && [parser->delegate respondsToSelector:@selector(xmppParserDidEnd:)]) + { + __strong id theDelegate = parser->delegate; + + dispatch_async(parser->delegateQueue, ^{ @autoreleasepool { + + [theDelegate xmppParserDidEnd:parser]; + }}); + } + } +} + +/** + * We're screwed... +**/ +static void xmpp_xmlAbortDueToMemoryShortage(xmlParserCtxt *ctxt) +{ + XMPPParser *parser = (__bridge XMPPParser *)ctxt->_private; + + xmlStopParser(ctxt); + + if (parser->delegateQueue && [parser->delegate respondsToSelector:@selector(xmppParser:didFail:)]) + { + NSString *errMsg = @"Unable to allocate memory in xmpp parser"; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + NSError *error = [NSError errorWithDomain:@"libxmlErrorDomain" code:1001 userInfo:info]; + + __strong id theDelegate = parser->delegate; + + dispatch_async(parser->delegateQueue, ^{ @autoreleasepool { + + [theDelegate xmppParser:parser didFail:error]; + }}); + } +} + +/** + * (Similar to the libxml "xmlSearchNs" method, with one very important difference.) + * + * This method searches for an existing xmlNsPtr in the given node, + * recursing on the parents but stopping before it reaches the root node of the document. + * + * Why do we skip the root node? + * Because all nodes are going to be detached from the root node. + * So it makes no sense to allow them to reference namespaces stored in the root node, + * since the detach algorithm will be forced to copy the namespaces later anyway. +**/ +static xmlNsPtr xmpp_xmlSearchNs(xmlDocPtr doc, xmlNodePtr node, const xmlChar *nameSpace) +{ + xmlNodePtr rootNode = xmlDocGetRootElement(doc); + + xmlNodePtr currentNode = node; + while (currentNode && currentNode != rootNode) + { + xmlNsPtr currentNs = currentNode->nsDef; + while (currentNs) + { + if (currentNs->href != NULL) + { + if ((currentNs->prefix == NULL) && (nameSpace == NULL)) + { + return currentNs; + } + if ((currentNs->prefix != NULL) && (nameSpace != NULL)) + { + if (xmlStrEqual(currentNs->prefix, nameSpace)) + return currentNs; + } + } + + currentNs = currentNs->next; + } + + currentNode = currentNode->parent; + } + + return NULL; +} + +/** + * SAX parser C-style callback. + * Invoked when a new node element is started. +**/ +static void xmpp_xmlStartElement(void *ctx, const xmlChar *nodeName, + const xmlChar *nodePrefix, + const xmlChar *nodeUri, + int nb_namespaces, + const xmlChar **namespaces, + int nb_attributes, + int nb_defaulted, + const xmlChar **attributes) +{ + int i, j; + xmlNsPtr lastAddedNs = NULL; + + xmlParserCtxt *ctxt = (xmlParserCtxt *)ctx; + + // We store the parent node in the context's node pointer. + // We keep this updated by "pushing" the node in the startElement method, + // and "popping" the node in the endElement method. + xmlNodePtr parent = ctxt->node; + + // Create the node + xmlNodePtr newNode = xmlNewDocNode(ctxt->myDoc, NULL, nodeName, NULL); + CHECK_FOR_NULL(newNode); + + // Add the node to the tree + if (parent == NULL) + { + // Root node + xmlAddChild((xmlNodePtr)ctxt->myDoc, newNode); + } + else + { + xmlAddChild(parent, newNode); + } + + // Process the namespaces + for (i = 0, j = 0; j < nb_namespaces; j++) + { + // Extract namespace prefix and uri + const xmlChar *nsPrefix = namespaces[i++]; + const xmlChar *nsUri = namespaces[i++]; + + // Create the namespace + xmlNsPtr newNs = xmlNewNs(NULL, nsUri, nsPrefix); + CHECK_FOR_NULL(newNs); + + // Add namespace to node. + // Each node has a linked list of nodes (in the nsDef variable). + // The linked list is forward only. + // In other words, each ns has a next, but not a prev pointer. + + if (newNode->nsDef == NULL) + { + newNode->nsDef = newNs; + lastAddedNs = newNs; + } + else + { + if(lastAddedNs != NULL) + { + lastAddedNs->next = newNs; + } + + lastAddedNs = newNs; + } + + // Is this the namespace for the node? + + if (nodeUri && (nodePrefix == nsPrefix)) + { + // Ex 1: node == && newNs == stream:url + // Ex 2: node == && newNs == null:url + + newNode->ns = newNs; + } + } + + // Search for the node's namespace if it wasn't already found + if ((nodeUri) && (newNode->ns == NULL)) + { + newNode->ns = xmpp_xmlSearchNs(ctxt->myDoc, newNode, nodePrefix); + + if (newNode->ns == NULL) + { + // We use href==NULL in the case of an element creation where the namespace was not defined. + // + // We do NOT use xmlNewNs(newNode, nodeUri, nodePrefix) because that method doesn't properly add + // the namespace to BOTH nsDef and ns. + + xmlNsPtr newNs = xmlNewNs(NULL, nodeUri, nodePrefix); + CHECK_FOR_NULL(newNs); + + if (newNode->nsDef == NULL) + { + newNode->nsDef = newNs; + } + else if(lastAddedNs != NULL) + { + lastAddedNs->next = newNs; + } + + newNode->ns = newNs; + } + } + + // Process all the attributes + for (i = 0, j = 0; j < nb_attributes; j++) + { + const xmlChar *attrName = attributes[i++]; + const xmlChar *attrPrefix = attributes[i++]; + const xmlChar *attrUri = attributes[i++]; + const xmlChar *valueBegin = attributes[i++]; + const xmlChar *valueEnd = attributes[i++]; + + // The attribute value might contain character references which need to be decoded. + // + // "Franks & Beans" -> "Franks & Beans" + + xmlChar *value = xmlStringLenDecodeEntities(ctxt, // the parser context + valueBegin, // the input string + (int)(valueEnd - valueBegin), // the input string length + (XML_SUBSTITUTE_REF), // what to substitue + 0, 0, 0); // end markers, 0 if none + CHECK_FOR_NULL(value); + + if ((attrPrefix == NULL) && (attrUri == NULL)) + { + // Normal attribute - no associated namespace + xmlAttrPtr newAttr = xmlNewProp(newNode, attrName, value); + CHECK_FOR_NULL(newAttr); + } + else + { + // Find the namespace for the attribute + xmlNsPtr attrNs = xmpp_xmlSearchNs(ctxt->myDoc, newNode, attrPrefix); + + if (attrNs != NULL) + { + xmlAttrPtr newAttr = xmlNewNsProp(newNode, attrNs, attrName, value); + CHECK_FOR_NULL(newAttr); + } + else + { + attrNs = xmlNewNs(NULL, NULL, nodePrefix); + CHECK_FOR_NULL(attrNs); + + xmlAttrPtr newAttr = xmlNewNsProp(newNode, attrNs, attrName, value); + CHECK_FOR_NULL(newAttr); + } + } + + xmlFree(value); + } + + // Update our parent node pointer + ctxt->node = newNode; + + // Invoke delegate methods if needed + xmpp_postStartElement(ctxt); +} + +/** + * SAX parser C-style callback. + * Invoked when characters are found within a node. +**/ +static void xmpp_xmlCharacters(void *ctx, const xmlChar *ch, int len) +{ + xmlParserCtxt *ctxt = (xmlParserCtxt *)ctx; + + if (ctxt->node != NULL) + { + xmlNodePtr textNode = xmlNewTextLen(ch, len); + + // xmlAddChild(xmlNodePtr parent, xmlNodePtr cur) + // + // Add a new node to @parent, at the end of the child list + // merging adjacent TEXT nodes (in which case @cur is freed). + + xmlAddChild(ctxt->node, textNode); + } +} + +/** + * SAX parser C-style callback. + * Invoked when a new node element is ended. +**/ +static void xmpp_xmlEndElement(void *ctx, const xmlChar *localname, + const xmlChar *prefix, + const xmlChar *URI) +{ + xmlParserCtxt *ctxt = (xmlParserCtxt *)ctx; + + // Update our parent node pointer + if (ctxt->node != NULL) + ctxt->node = ctxt->node->parent; + + // Invoke delegate methods if needed + xmpp_postEndElement(ctxt); +} + +- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq +{ + return [self initWithDelegate:aDelegate delegateQueue:dq parserQueue:NULL]; +} + +- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq parserQueue:(dispatch_queue_t)pq +{ + if ((self = [super init])) + { + delegate = aDelegate; + delegateQueue = dq; + + #if !OS_OBJECT_USE_OBJC + if (delegateQueue) + dispatch_retain(delegateQueue); + #endif + + if (pq) { + parserQueue = pq; + + #if !OS_OBJECT_USE_OBJC + dispatch_retain(parserQueue); + #endif + } + else { + parserQueue = dispatch_queue_create("xmpp.parser", NULL); + } + + xmppParserQueueTag = &xmppParserQueueTag; + dispatch_queue_set_specific(parserQueue, xmppParserQueueTag, xmppParserQueueTag, NULL); + + hasReportedRoot = NO; + depth = 0; + + // Create SAX handler + xmlSAXHandler saxHandler; + memset(&saxHandler, 0, sizeof(xmlSAXHandler)); + + saxHandler.initialized = XML_SAX2_MAGIC; + saxHandler.startElementNs = xmpp_xmlStartElement; + saxHandler.characters = xmpp_xmlCharacters; + saxHandler.endElementNs = xmpp_xmlEndElement; + + // Create the push parser context + parserCtxt = xmlCreatePushParserCtxt(&saxHandler, NULL, NULL, 0, NULL); + + // Note: This method copies the saxHandler, so we don't have to keep it around. + + // Create the document to hold the parsed elements + parserCtxt->myDoc = xmlNewDoc(parserCtxt->version); + + // Store reference to ourself + parserCtxt->_private = (__bridge void *)(self); + + // Note: The parserCtxt also has a userData variable, but it is used by the DOM building functions. + // If we put a value there, it actually causes a crash! + // We need to be sure to use the _private variable which libxml won't touch. + } + return self; +} + +- (void)dealloc +{ + if (parserCtxt) + { + // The xmlFreeParserCtxt method will not free the created document in parserCtxt->myDoc. + if (parserCtxt->myDoc) + { + // Free the created xmlDoc + xmlFreeDoc(parserCtxt->myDoc); + } + + xmlFreeParserCtxt(parserCtxt); + } + + #if !OS_OBJECT_USE_OBJC + if (delegateQueue) + dispatch_release(delegateQueue); + if (parserQueue) + dispatch_release(parserQueue); + #endif +} + +- (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue +{ + #if !OS_OBJECT_USE_OBJC + if (newDelegateQueue) + dispatch_retain(newDelegateQueue); + #endif + + dispatch_block_t block = ^{ + + delegate = newDelegate; + + #if !OS_OBJECT_USE_OBJC + if (delegateQueue) + dispatch_release(delegateQueue); + #endif + + delegateQueue = newDelegateQueue; + }; + + if (dispatch_get_specific(xmppParserQueueTag)) + block(); + else + dispatch_async(parserQueue, block); +} + +- (void)parseData:(NSData *)data +{ + dispatch_block_t block = ^{ @autoreleasepool { + + int result = xmlParseChunk(parserCtxt, (const char *)[data bytes], (int)[data length], 0); + + if (result == 0) + { + if (delegateQueue && [delegate respondsToSelector:@selector(xmppParserDidParseData:)]) + { + __strong id theDelegate = delegate; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate xmppParserDidParseData:self]; + }}); + } + } + else + { + if (delegateQueue && [delegate respondsToSelector:@selector(xmppParser:didFail:)]) + { + NSError *error; + + xmlError *xmlErr = xmlCtxtGetLastError(parserCtxt); + + if (xmlErr->message) + { + NSString *errMsg = [NSString stringWithFormat:@"%s", xmlErr->message]; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + error = [NSError errorWithDomain:@"libxmlErrorDomain" code:xmlErr->code userInfo:info]; + } + else + { + error = [NSError errorWithDomain:@"libxmlErrorDomain" code:xmlErr->code userInfo:nil]; + } + + __strong id theDelegate = delegate; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate xmppParser:self didFail:error]; + }}); + } + } + }}; + + if (dispatch_get_specific(xmppParserQueueTag)) + block(); + else + dispatch_async(parserQueue, block); +} + +@end diff --git a/Core/XMPPPresence.h b/Core/XMPPPresence.h new file mode 100644 index 0000000..d77554b --- /dev/null +++ b/Core/XMPPPresence.h @@ -0,0 +1,38 @@ +#import +#import "XMPPElement.h" + +/** + * The XMPPPresence class represents a element. + * It extends XMPPElement, which in turn extends NSXMLElement. + * All elements that go in and out of the + * xmpp stream will automatically be converted to XMPPPresence objects. + * + * This class exists to provide developers an easy way to add functionality to presence processing. + * Simply add your own category to XMPPPresence to extend it with your own custom methods. +**/ + +@interface XMPPPresence : XMPPElement + +// Converts an NSXMLElement to an XMPPPresence element in place (no memory allocations or copying) ++ (XMPPPresence *)presenceFromElement:(NSXMLElement *)element; + ++ (XMPPPresence *)presence; ++ (XMPPPresence *)presenceWithType:(NSString *)type; ++ (XMPPPresence *)presenceWithType:(NSString *)type to:(XMPPJID *)to; + +- (id)init; +- (id)initWithType:(NSString *)type; +- (id)initWithType:(NSString *)type to:(XMPPJID *)to; + +- (NSString *)type; + +- (NSString *)show; +- (NSString *)status; + +- (int)priority; + +- (int)intShow; + +- (BOOL)isErrorPresence; + +@end diff --git a/Core/XMPPPresence.m b/Core/XMPPPresence.m new file mode 100644 index 0000000..a01e46e --- /dev/null +++ b/Core/XMPPPresence.m @@ -0,0 +1,145 @@ +#import "XMPPPresence.h" +#import "XMPPJID.h" +#import "NSXMLElement+XMPP.h" + +#import + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@implementation XMPPPresence + +#if DEBUG + ++ (void)initialize +{ + // We use the object_setClass method below to dynamically change the class from a standard NSXMLElement. + // The size of the two classes is expected to be the same. + // + // If a developer adds instance methods to this class, bad things happen at runtime that are very hard to debug. + // This check is here to aid future developers who may make this mistake. + // + // For Fearless And Experienced Objective-C Developers: + // It may be possible to support adding instance variables to this class if you seriously need it. + // To do so, try realloc'ing self after altering the class, and then initialize your variables. + + size_t superSize = class_getInstanceSize([NSXMLElement class]); + size_t ourSize = class_getInstanceSize([XMPPPresence class]); + + if (superSize != ourSize) + { + NSLog(@"Adding instance variables to XMPPPresence is not currently supported!"); + exit(15); + } +} + +#endif + ++ (XMPPPresence *)presenceFromElement:(NSXMLElement *)element +{ + object_setClass(element, [XMPPPresence class]); + + return (XMPPPresence *)element; +} + ++ (XMPPPresence *)presence +{ + return [[XMPPPresence alloc] init]; +} + ++ (XMPPPresence *)presenceWithType:(NSString *)type +{ + return [[XMPPPresence alloc] initWithType:type to:nil]; +} + ++ (XMPPPresence *)presenceWithType:(NSString *)type to:(XMPPJID *)to +{ + return [[XMPPPresence alloc] initWithType:type to:to]; +} + +- (id)init +{ + self = [super initWithName:@"presence"]; + return self; +} + +- (id)initWithType:(NSString *)type +{ + return [self initWithType:type to:nil]; +} + +- (id)initWithType:(NSString *)type to:(XMPPJID *)to +{ + if ((self = [super initWithName:@"presence"])) + { + if (type) + [self addAttributeWithName:@"type" stringValue:type]; + + if (to) + [self addAttributeWithName:@"to" stringValue:[to full]]; + } + return self; +} + +- (id)initWithXMLString:(NSString *)string error:(NSError *__autoreleasing *)error +{ + if((self = [super initWithXMLString:string error:error])){ + self = [XMPPPresence presenceFromElement:self]; + } + return self; +} + +- (id)copyWithZone:(NSZone *)zone +{ + NSXMLElement *element = [super copyWithZone:zone]; + return [XMPPPresence presenceFromElement:element]; +} + +- (NSString *)type +{ + NSString *type = [self attributeStringValueForName:@"type"]; + if(type) + return [type lowercaseString]; + else + return @"available"; +} + +- (NSString *)show +{ + return [[self elementForName:@"show"] stringValue]; +} + +- (NSString *)status +{ + return [[self elementForName:@"status"] stringValue]; +} + +- (int)priority +{ + return [[[self elementForName:@"priority"] stringValue] intValue]; +} + +- (int)intShow +{ + NSString *show = [self show]; + + if([show isEqualToString:@"dnd"]) + return 0; + if([show isEqualToString:@"xa"]) + return 1; + if([show isEqualToString:@"away"]) + return 2; + if([show isEqualToString:@"chat"]) + return 4; + + return 3; +} + +- (BOOL)isErrorPresence +{ + return [[self type] isEqualToString:@"error"]; +} + +@end diff --git a/Core/XMPPStream.h b/Core/XMPPStream.h new file mode 100644 index 0000000..39d4c0d --- /dev/null +++ b/Core/XMPPStream.h @@ -0,0 +1,1111 @@ +#import +#import "XMPPSASLAuthentication.h" +#import "XMPPCustomBinding.h" +#import "GCDMulticastDelegate.h" +#import "CocoaAsyncSocket/GCDAsyncSocket.h" + +#if TARGET_OS_IPHONE + #import "DDXML.h" +#endif + +@class XMPPSRVResolver; +@class XMPPParser; +@class XMPPJID; +@class XMPPIQ; +@class XMPPMessage; +@class XMPPPresence; +@class XMPPModule; +@class XMPPElement; +@class XMPPElementReceipt; +@protocol XMPPStreamDelegate; + +#if TARGET_OS_IPHONE + #define MIN_KEEPALIVE_INTERVAL 20.0 // 20 Seconds + #define DEFAULT_KEEPALIVE_INTERVAL 120.0 // 2 Minutes +#else + #define MIN_KEEPALIVE_INTERVAL 10.0 // 10 Seconds + #define DEFAULT_KEEPALIVE_INTERVAL 300.0 // 5 Minutes +#endif + +extern NSString *const XMPPStreamErrorDomain; + +typedef NS_ENUM(NSUInteger, XMPPStreamErrorCode) { + XMPPStreamInvalidType, // Attempting to access P2P methods in a non-P2P stream, or vice-versa + XMPPStreamInvalidState, // Invalid state for requested action, such as connect when already connected + XMPPStreamInvalidProperty, // Missing a required property, such as myJID + XMPPStreamInvalidParameter, // Invalid parameter, such as a nil JID + XMPPStreamUnsupportedAction, // The server doesn't support the requested action +}; + +typedef NS_ENUM(NSUInteger, XMPPStreamStartTLSPolicy) { + XMPPStreamStartTLSPolicyAllowed, // TLS will be used if the server requires it + XMPPStreamStartTLSPolicyPreferred, // TLS will be used if the server offers it + XMPPStreamStartTLSPolicyRequired // TLS will be used if the server offers it, else the stream won't connect +}; + +extern const NSTimeInterval XMPPStreamTimeoutNone; + +@interface XMPPStream : NSObject + +/** + * Standard XMPP initialization. + * The stream is a standard client to server connection. + * + * P2P streams using XEP-0174 are also supported. + * See the P2P section below. +**/ +- (id)init; + +/** + * Peer to Peer XMPP initialization. + * The stream is a direct client to client connection as outlined in XEP-0174. +**/ +- (id)initP2PFrom:(XMPPJID *)myJID; + +/** + * XMPPStream uses a multicast delegate. + * This allows one to add multiple delegates to a single XMPPStream instance, + * which makes it easier to separate various components and extensions. + * + * For example, if you were implementing two different custom extensions on top of XMPP, + * you could put them in separate classes, and simply add each as a delegate. +**/ +- (void)addDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; +- (void)removeDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; +- (void)removeDelegate:(id)delegate; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Properties +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The server's hostname that should be used to make the TCP connection. + * This may be a domain name (e.g. "deusty.com") or an IP address (e.g. "70.85.193.226"). + * + * Note that this may be different from the virtual xmpp hostname. + * Just as HTTP servers can support mulitple virtual hosts from a single server, so too can xmpp servers. + * A prime example is google via google apps. + * + * For example, say you own the domain "mydomain.com". + * If you go to mydomain.com in a web browser, + * you are directed to your apache server running on your webserver somewhere in the cloud. + * But you use google apps for your email and xmpp needs. + * So if somebody sends you an email, it actually goes to google's servers, where you later access it from. + * Similarly, you connect to google's servers to sign into xmpp. + * + * In the example above, your hostname is "talk.google.com" and your JID is "me@mydomain.com". + * + * This hostName property is optional. + * If you do not set the hostName, then the framework will follow the xmpp specification using jid's domain. + * That is, it first do an SRV lookup (as specified in the xmpp RFC). + * If that fails, it will fall back to simply attempting to connect to the jid's domain. +**/ +@property (readwrite, copy) NSString *hostName; + +/** + * The port the xmpp server is running on. + * If you do not explicitly set the port, the default port will be used. + * If you set the port to zero, the default port will be used. + * + * The default port is 5222. +**/ +@property (readwrite, assign) UInt16 hostPort; + +/** + * The stream's policy on when to Start TLS. + * + * The default is XMPPStreamStartTLSPolicyAllowed. + * + * @see XMPPStreamStartTLSPolicy +**/ +@property (readwrite, assign) XMPPStreamStartTLSPolicy startTLSPolicy; + +/** + * The JID of the user. + * + * This value is required, and is used in many parts of the underlying implementation. + * When connecting, the domain of the JID is used to properly specify the correct xmpp virtual host. + * It is used during registration to supply the username of the user to create an account for. + * It is used during authentication to supply the username of the user to authenticate with. + * And the resource may be used post-authentication during the required xmpp resource binding step. + * + * A proper JID is of the form user@domain/resource. + * For example: robbiehanson@deusty.com/work + * + * The resource is optional, in the sense that if one is not supplied, + * one will be automatically generated for you (either by us or by the server). + * + * Please note: + * Resource collisions are handled in different ways depending on server configuration. + * + * For example: + * You are signed in with user1@domain.com/home on your desktop. + * Then you attempt to sign in with user1@domain.com/home on your laptop. + * + * The server could possibly: + * - Reject the resource request for the laptop. + * - Accept the resource request for the laptop, and immediately disconnect the desktop. + * - Automatically assign the laptop another resource without a conflict. + * + * For this reason, you may wish to check the myJID variable after the stream has been connected, + * just in case the resource was changed by the server. +**/ +@property (readwrite, copy) XMPPJID *myJID; + +/** + * Only used in P2P streams. +**/ +@property (strong, readonly) XMPPJID *remoteJID; + +/** + * Many routers will teardown a socket mapping if there is no activity on the socket. + * For this reason, the xmpp stream supports sending keep-alive data. + * This is simply whitespace, which is ignored by the xmpp protocol. + * + * Keep-alive data is only sent in the absence of any other data being sent/received. + * + * The default value is defined in DEFAULT_KEEPALIVE_INTERVAL. + * The minimum value is defined in MIN_KEEPALIVE_INTERVAL. + * + * To disable keep-alive, set the interval to zero (or any non-positive number). + * + * The keep-alive timer (if enabled) fires every (keepAliveInterval / 4) seconds. + * Upon firing it checks when data was last sent/received, + * and sends keep-alive data if the elapsed time has exceeded the keepAliveInterval. + * Thus the effective resolution of the keepalive timer is based on the interval. + * + * @see keepAliveWhitespaceCharacter +**/ +@property (readwrite, assign) NSTimeInterval keepAliveInterval; + +/** + * The keep-alive mechanism sends whitespace which is ignored by the xmpp protocol. + * The default whitespace character is a space (' '). + * + * This can be changed, for whatever reason, to another whitespace character. + * Valid whitespace characters are space(' '), tab('\t') and newline('\n'). + * + * If you attempt to set the character to any non-whitespace character, the attempt is ignored. + * + * @see keepAliveInterval +**/ +@property (readwrite, assign) char keepAliveWhitespaceCharacter; + +/** + * Represents the last sent presence element concerning the presence of myJID on the server. + * In other words, it represents the presence as others see us. + * + * This excludes presence elements sent concerning subscriptions, MUC rooms, etc. + * + * @see resendMyPresence +**/ +@property (strong, readonly) XMPPPresence *myPresence; + +/** + * Returns the total number of bytes bytes sent/received by the xmpp stream. + * + * By default this is the byte count since the xmpp stream object has been created. + * If the stream has connected/disconnected/reconnected multiple times, + * the count will be the summation of all connections. + * + * The functionality may optionaly be changed to count only the current socket connection. + * @see resetByteCountPerConnection +**/ +@property (readonly) uint64_t numberOfBytesSent; +@property (readonly) uint64_t numberOfBytesReceived; + +/** + * Same as the individual properties, + * but provides a way to fetch them in one atomic operation. +**/ +- (void)getNumberOfBytesSent:(uint64_t *)bytesSentPtr numberOfBytesReceived:(uint64_t *)bytesReceivedPtr; + +/** + * Affects the funtionality of the byte counter. + * + * The default value is NO. + * + * If set to YES, the byte count will be reset just prior to a new connection (in the connect methods). +**/ +@property (readwrite, assign) BOOL resetByteCountPerConnection; + +/** + * The tag property allows you to associate user defined information with the stream. + * Tag values are not used internally, and should not be used by xmpp modules. +**/ +@property (readwrite, strong) id tag; + +/** + * RFC 6121 states that starting a session is no longer required. + * To skip this step set skipStartSession to YES. + * + * [RFC3921] specified one additional + * precondition: formal establishment of an instant messaging and + * presence session. Implementation and deployment experience has + * shown that this additional step is unnecessary. However, for + * backward compatibility an implementation MAY still offer that + * feature. This enables older software to connect while letting + * newer software save a round trip. + * + * The default value is NO. +**/ +@property (readwrite, assign) BOOL skipStartSession; + +/** + * Validates that a response element is FROM the jid that the request element was sent TO. + * Supports validating responses when request didn't specify a TO. + * + * @see isValidResponseElementFrom:forRequestElementTo: + * @see isValidResponseElement:forRequestElement: + * + * The default value is NO. +**/ +@property (readwrite, assign) BOOL validatesResponses; + +#if TARGET_OS_IPHONE + +/** + * If set, the kCFStreamNetworkServiceTypeVoIP flags will be set on the underlying CFRead/Write streams. + * + * The default value is NO. +**/ +@property (readwrite, assign) BOOL enableBackgroundingOnSocket; + +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark State +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Returns YES if the connection is closed, and thus no stream is open. + * If the stream is neither disconnected, nor connected, then a connection is currently being established. +**/ +- (BOOL)isDisconnected; + +/** + * Returns YES is the connection is currently connecting +**/ +- (BOOL)isConnecting; + +/** + * Returns YES if the connection is open, and the stream has been properly established. + * If the stream is neither disconnected, nor connected, then a connection is currently being established. + * + * If this method returns YES, then it is ready for you to start sending and receiving elements. +**/ +- (BOOL)isConnected; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Connect & Disconnect +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Connects to the configured hostName on the configured hostPort. + * The timeout is optional. To not time out use XMPPStreamTimeoutNone. + * If the hostName or myJID are not set, this method will return NO and set the error parameter. +**/ +- (BOOL)connectWithTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; + +/** + * THIS IS DEPRECATED BY THE XMPP SPECIFICATION. + * + * The xmpp specification outlines the proper use of SSL/TLS by negotiating + * the startTLS upgrade within the stream negotiation. + * This method exists for those ancient servers that still require the connection to be secured prematurely. + * The timeout is optional. To not time out use XMPPStreamTimeoutNone. + * + * Note: Such servers generally use port 5223 for this, which you will need to set. +**/ +- (BOOL)oldSchoolSecureConnectWithTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; + +/** + * Starts a P2P connection to the given user and given address. + * The timeout is optional. To not time out use XMPPStreamTimeoutNone. + * This method only works with XMPPStream objects created using the initP2P method. + * + * The given address is specified as a sockaddr structure wrapped in a NSData object. + * For example, a NSData object returned from NSNetservice's addresses method. +**/ +- (BOOL)connectTo:(XMPPJID *)remoteJID + withAddress:(NSData *)remoteAddr + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; + +/** + * Starts a P2P connection with the given accepted socket. + * This method only works with XMPPStream objects created using the initP2P method. + * + * The given socket should be a socket that has already been accepted. + * The remoteJID will be extracted from the opening stream negotiation. +**/ +- (BOOL)connectP2PWithSocket:(GCDAsyncSocket *)acceptedSocket error:(NSError **)errPtr; + +/** + * Disconnects from the remote host by closing the underlying TCP socket connection. + * The terminating element is not sent to the server. + * + * This method is synchronous. + * Meaning that the disconnect will happen immediately, even if there are pending elements yet to be sent. + * + * The xmppStreamDidDisconnect:withError: delegate method will immediately be dispatched onto the delegate queue. +**/ +- (void)disconnect; + +/** + * Disconnects from the remote host by sending the terminating element, + * and then closing the underlying TCP socket connection. + * + * This method is asynchronous. + * The disconnect will happen after all pending elements have been sent. + * Attempting to send elements after this method has been called will not work (the elements won't get sent). +**/ +- (void)disconnectAfterSending; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Security +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Returns YES if SSL/TLS was used to establish a connection to the server. + * + * Some servers may require an "upgrade to TLS" in order to start communication, + * so even if the connection was not explicitly secured, an ugrade to TLS may have occured. + * + * See also the xmppStream:willSecureWithSettings: delegate method. +**/ +- (BOOL)isSecure; + +/** + * Returns whether or not the server supports securing the connection via SSL/TLS. + * + * Some servers will actually require a secure connection, + * in which case the stream will attempt to secure the connection during the opening process. + * + * If the connection has already been secured, this method may return NO. +**/ +- (BOOL)supportsStartTLS; + +/** + * Attempts to secure the connection via SSL/TLS. + * + * This method is asynchronous. + * The SSL/TLS handshake will occur in the background, and + * the xmppStreamDidSecure: delegate method will be called after the TLS process has completed. + * + * This method returns immediately. + * If the secure process was started, it will return YES. + * If there was an issue while starting the security process, + * this method will return NO and set the error parameter. + * + * The errPtr parameter is optional - you may pass nil. + * + * You may wish to configure the security settings via the xmppStream:willSecureWithSettings: delegate method. + * + * If the SSL/TLS handshake fails, the connection will be closed. + * The reason for the error will be reported via the xmppStreamDidDisconnect:withError: delegate method. + * The error parameter will be an NSError object, and may have an error domain of kCFStreamErrorDomainSSL. + * The corresponding error code is documented in Apple's Security framework, in SecureTransport.h +**/ +- (BOOL)secureConnection:(NSError **)errPtr; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Registration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * In Band Registration. + * Creating a user account on the xmpp server within the xmpp protocol. + * + * The registerWithElements:error: method is asynchronous. + * It will return immediately, and the delegate methods are used to determine success. + * See the xmppStreamDidRegister: and xmppStream:didNotRegister: methods. + * + * If there is something immediately wrong, such as the stream is not connected, + * this method will return NO and set the error. + * + * The errPtr parameter is optional - you may pass nil. + * + * registerWithPassword:error: is a convience method for creating an account using the given username and password. + * + * Security Note: + * The password will be sent in the clear unless the stream has been secured. +**/ +- (BOOL)supportsInBandRegistration; +- (BOOL)registerWithElements:(NSArray *)elements error:(NSError **)errPtr; +- (BOOL)registerWithPassword:(NSString *)password error:(NSError **)errPtr; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Authentication +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Returns the server's list of supported authentication mechanisms. + * Each item in the array will be of type NSString. + * + * For example, if the server supplied this stanza within it's reported stream:features: + * + * + * DIGEST-MD5 + * PLAIN + * + * + * Then this method would return [@"DIGEST-MD5", @"PLAIN"]. +**/ +- (NSArray *)supportedAuthenticationMechanisms; + +/** + * Returns whether or not the given authentication mechanism name was specified in the + * server's list of supported authentication mechanisms. + * + * Note: The authentication classes often provide a category on XMPPStream, adding useful methods. + * + * @see XMPPPlainAuthentication - supportsPlainAuthentication + * @see XMPPDigestMD5Authentication - supportsDigestMD5Authentication + * @see XMPPXFacebookPlatformAuthentication - supportsXFacebookPlatformAuthentication + * @see XMPPDeprecatedPlainAuthentication - supportsDeprecatedPlainAuthentication + * @see XMPPDeprecatedDigestAuthentication - supportsDeprecatedDigestAuthentication +**/ +- (BOOL)supportsAuthenticationMechanism:(NSString *)mechanism; + +/** + * This is the root authentication method. + * All other authentication methods go through this one. + * + * This method attempts to start the authentication process given the auth instance. + * That is, this method will invoke start: on the given auth instance. + * If it returns YES, then the stream will enter into authentication mode. + * It will then continually invoke the handleAuth: method on the given instance until authentication is complete. + * + * This method is asynchronous. + * + * If there is something immediately wrong, such as the stream is not connected, + * the method will return NO and set the error. + * Otherwise the delegate callbacks are used to communicate auth success or failure. + * + * @see xmppStreamDidAuthenticate: + * @see xmppStream:didNotAuthenticate: + * + * @see authenticateWithPassword:error: + * + * Note: The security process is abstracted in order to provide flexibility, + * and allow developers to easily implement their own custom authentication protocols. + * The authentication classes often provide a category on XMPPStream, adding useful methods. + * + * @see XMPPXFacebookPlatformAuthentication - authenticateWithFacebookAccessToken:error: +**/ +- (BOOL)authenticate:(id )auth error:(NSError **)errPtr; + +/** + * This method applies to standard password authentication schemes only. + * This is NOT the primary authentication method. + * + * @see authenticate:error: + * + * This method exists for backwards compatibility, and may disappear in future versions. +**/ +- (BOOL)authenticateWithPassword:(NSString *)password error:(NSError **)errPtr; + +/** + * Returns whether or not the xmpp stream is currently authenticating with the XMPP Server. +**/ +- (BOOL)isAuthenticating; + +/** + * Returns whether or not the xmpp stream has successfully authenticated with the server. +**/ +- (BOOL)isAuthenticated; + +/** + * Returns the date when the xmpp stream successfully authenticated with the server. + **/ +- (NSDate *)authenticationDate; + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Compression +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + +/** + * Returns the server's list of supported compression methods in accordance to XEP-0138: Stream Compression + * Each item in the array will be of type NSString. + * + * For example, if the server supplied this stanza within it's reported stream:features: + * + * + * zlib + * lzw + * + * + * Then this method would return [@"zlib", @"lzw"]. + **/ +- (NSArray *)supportedCompressionMethods; + + +/** + * Returns whether or not the given compression method name was specified in the + * server's list of supported compression methods. + * + * Note: The XMPPStream doesn't currently support any compression methods +**/ + +- (BOOL)supportsCompressionMethod:(NSString *)compressionMethod; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Server Info +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method will return the root element of the document. + * This element contains the opening and tags received from the server. + * + * If multiple have been received during the course of stream negotiation, + * the root element contains only the most recent (current) version. + * + * Note: The rootElement is "empty", in-so-far as it does not contain all the XML elements the stream has + * received during it's connection. This is done for performance reasons and for the obvious benefit + * of being more memory efficient. +**/ +- (NSXMLElement *)rootElement; + +/** + * Returns the version attribute from the servers's element. + * This should be at least 1.0 to be RFC 3920 compliant. + * If no version number was set, the server is not RFC compliant, and 0 is returned. +**/ +- (float)serverXmppStreamVersionNumber; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Sending +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Sends the given XML element. + * If the stream is not yet connected, this method does nothing. +**/ +- (void)sendElement:(NSXMLElement *)element; + +/** + * Just like the sendElement: method above, + * but allows you to receive a receipt that can later be used to verify the element has been sent. + * + * If you later want to check to see if the element has been sent: + * + * if ([receipt wait:0]) { + * // Element has been sent + * } + * + * If you later want to wait until the element has been sent: + * + * if ([receipt wait:-1]) { + * // Element was sent + * } else { + * // Element failed to send due to disconnection + * } + * + * It is important to understand what it means when [receipt wait:timeout] returns YES. + * It does NOT mean the server has received the element. + * It only means the data has been queued for sending in the underlying OS socket buffer. + * + * So at this point the OS will do everything in its capacity to send the data to the server, + * which generally means the server will eventually receive the data. + * Unless, of course, something horrible happens such as a network failure, + * or a system crash, or the server crashes, etc. + * + * Even if you close the xmpp stream after this point, the OS will still do everything it can to send the data. +**/ +- (void)sendElement:(NSXMLElement *)element andGetReceipt:(XMPPElementReceipt **)receiptPtr; + +/** + * Fetches and resends the myPresence element (if available) in a single atomic operation. + * + * There are various xmpp extensions that hook into the xmpp stream and append information to outgoing presence stanzas. + * For example, the XMPPCapabilities module automatically appends capabilities information (as a hash). + * When these modules need to update/change their appended information, + * they should use this method to do so. + * + * The alternative is to fetch the myPresence element, and resend it manually using the sendElement method. + * However, that is 2 seperate operations, and the user, may send a different presence element inbetween. + * Using this method guarantees everything is done as an atomic operation. +**/ +- (void)resendMyPresence; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Stanza Validation +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Validates that a response element is FROM the jid that the request element was sent TO. + * Supports validating responses when request didn't specify a TO. +**/ +- (BOOL)isValidResponseElementFrom:(XMPPJID *)from forRequestElementTo:(XMPPJID *)to; + +- (BOOL)isValidResponseElement:(XMPPElement *)response forRequestElement:(XMPPElement *)request; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Module Plug-In System +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The XMPPModule class automatically invokes these methods when it is activated/deactivated. + * + * The registerModule method registers the module with the xmppStream. + * If there are any other modules that have requested to be automatically added as delegates to modules of this type, + * then those modules are automatically added as delegates during the asynchronous execution of this method. + * + * The registerModule method is asynchronous. + * + * The unregisterModule method unregisters the module with the xmppStream, + * and automatically removes it as a delegate of any other module. + * + * The unregisterModule method is fully synchronous. + * That is, after this method returns, the module will not be scheduled in any more delegate calls from other modules. + * However, if the module was already scheduled in an existing asynchronous delegate call from another module, + * the scheduled delegate invocation remains queued and will fire in the near future. + * Since the delegate invocation is already queued, + * the module's retainCount has been incremented, + * and the module will not be deallocated until after the delegate invocation has fired. +**/ +- (void)registerModule:(XMPPModule *)module; +- (void)unregisterModule:(XMPPModule *)module; + +/** + * Automatically registers the given delegate with all current and future registered modules of the given class. + * + * That is, the given delegate will be added to the delegate list ([module addDelegate:delegate delegateQueue:dq]) to + * all current and future registered modules that respond YES to [module isKindOfClass:aClass]. + * + * This method is used by modules to automatically integrate with other modules. + * For example, a module may auto-add itself as a delegate to XMPPCapabilities + * so that it can broadcast its implemented features. + * + * This may also be useful to clients, for example, to add a delegate to instances of something like XMPPChatRoom, + * where there may be multiple instances of the module that get created during the course of an xmpp session. + * + * If you auto register on multiple queues, you can remove all registrations with a single + * call to removeAutoDelegate::: by passing NULL as the 'dq' parameter. + * + * If you auto register for multiple classes, you can remove all registrations with a single + * call to removeAutoDelegate::: by passing nil as the 'aClass' parameter. +**/ +- (void)autoAddDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue toModulesOfClass:(Class)aClass; +- (void)removeAutoDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue fromModulesOfClass:(Class)aClass; + +/** + * Allows for enumeration of the currently registered modules. + * + * This may be useful if the stream needs to be queried for modules of a particular type. +**/ +- (void)enumerateModulesWithBlock:(void (^)(XMPPModule *module, NSUInteger idx, BOOL *stop))block; + +/** + * Allows for enumeration of the currently registered modules that are a kind of Class. + * idx is in relation to all modules not just those of the given class. +**/ +- (void)enumerateModulesOfClass:(Class)aClass withBlock:(void (^)(XMPPModule *module, NSUInteger idx, BOOL *stop))block; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Utilities +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Generates and returns a new autoreleased UUID. + * UUIDs (Universally Unique Identifiers) may also be known as GUIDs (Globally Unique Identifiers). + * + * The UUID is generated using the CFUUID library, which generates a unique 128 bit value. + * The uuid is then translated into a string using the standard format for UUIDs: + * "68753A44-4D6F-1226-9C60-0050E4C00067" + * + * This method is most commonly used to generate a unique id value for an xmpp element. +**/ ++ (NSString *)generateUUID; +- (NSString *)generateUUID; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPElementReceipt : NSObject +{ + uint32_t atomicFlags; + dispatch_semaphore_t semaphore; +} + +/** + * Element receipts allow you to check to see if the element has been sent. + * The timeout parameter allows you to do any of the following: + * + * - Do an instantaneous check (pass timeout == 0) + * - Wait until the element has been sent (pass timeout < 0) + * - Wait up to a certain amount of time (pass timeout > 0) + * + * It is important to understand what it means when [receipt wait:timeout] returns YES. + * It does NOT mean the server has received the element. + * It only means the data has been queued for sending in the underlying OS socket buffer. + * + * So at this point the OS will do everything in its capacity to send the data to the server, + * which generally means the server will eventually receive the data. + * Unless, of course, something horrible happens such as a network failure, + * or a system crash, or the server crashes, etc. + * + * Even if you close the xmpp stream after this point, the OS will still do everything it can to send the data. +**/ +- (BOOL)wait:(NSTimeInterval)timeout; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPStreamDelegate +@optional + +/** + * This method is called before the stream begins the connection process. + * + * If developing an iOS app that runs in the background, this may be a good place to indicate + * that this is a task that needs to continue running in the background. +**/ +- (void)xmppStreamWillConnect:(XMPPStream *)sender; + +/** + * This method is called after the tcp socket has connected to the remote host. + * It may be used as a hook for various things, such as updating the UI or extracting the server's IP address. + * + * If developing an iOS app that runs in the background, + * please use XMPPStream's enableBackgroundingOnSocket property as opposed to doing it directly on the socket here. +**/ +- (void)xmppStream:(XMPPStream *)sender socketDidConnect:(GCDAsyncSocket *)socket; + +/** + * This method is called after a TCP connection has been established with the server, + * and the opening XML stream negotiation has started. +**/ +- (void)xmppStreamDidStartNegotiation:(XMPPStream *)sender; + +/** + * This method is called immediately prior to the stream being secured via TLS/SSL. + * Note that this delegate may be called even if you do not explicitly invoke the startTLS method. + * Servers have the option of requiring connections to be secured during the opening process. + * If this is the case, the XMPPStream will automatically attempt to properly secure the connection. + * + * The dictionary of settings is what will be passed to the startTLS method of the underlying GCDAsyncSocket. + * The GCDAsyncSocket header file contains a discussion of the available key/value pairs, + * as well as the security consequences of various options. + * It is recommended reading if you are planning on implementing this method. + * + * The dictionary of settings that are initially passed will be an empty dictionary. + * If you choose not to implement this method, or simply do not edit the dictionary, + * then the default settings will be used. + * That is, the kCFStreamSSLPeerName will be set to the configured host name, + * and the default security validation checks will be performed. + * + * This means that authentication will fail if the name on the X509 certificate of + * the server does not match the value of the hostname for the xmpp stream. + * It will also fail if the certificate is self-signed, or if it is expired, etc. + * + * These settings are most likely the right fit for most production environments, + * but may need to be tweaked for development or testing, + * where the development server may be using a self-signed certificate. + * + * Note: If your development server is using a self-signed certificate, + * you likely need to add GCDAsyncSocketManuallyEvaluateTrust=YES to the settings. + * Then implement the xmppStream:didReceiveTrust:completionHandler: delegate method to perform custom validation. +**/ +- (void)xmppStream:(XMPPStream *)sender willSecureWithSettings:(NSMutableDictionary *)settings; + +/** + * Allows a delegate to hook into the TLS handshake and manually validate the peer it's connecting to. + * + * This is only called if the stream is secured with settings that include: + * - GCDAsyncSocketManuallyEvaluateTrust == YES + * That is, if a delegate implements xmppStream:willSecureWithSettings:, and plugs in that key/value pair. + * + * Thus this delegate method is forwarding the TLS evaluation callback from the underlying GCDAsyncSocket. + * + * Typically the delegate will use SecTrustEvaluate (and related functions) to properly validate the peer. + * + * Note from Apple's documentation: + * Because [SecTrustEvaluate] might look on the network for certificates in the certificate chain, + * [it] might block while attempting network access. You should never call it from your main thread; + * call it only from within a function running on a dispatch queue or on a separate thread. + * + * This is why this method uses a completionHandler block rather than a normal return value. + * The idea is that you should be performing SecTrustEvaluate on a background thread. + * The completionHandler block is thread-safe, and may be invoked from a background queue/thread. + * It is safe to invoke the completionHandler block even if the socket has been closed. + * + * Keep in mind that you can do all kinds of cool stuff here. + * For example: + * + * If your development server is using a self-signed certificate, + * then you could embed info about the self-signed cert within your app, and use this callback to ensure that + * you're actually connecting to the expected dev server. + * + * Also, you could present certificates that don't pass SecTrustEvaluate to the client. + * That is, if SecTrustEvaluate comes back with problems, you could invoke the completionHandler with NO, + * and then ask the client if the cert can be trusted. This is similar to how most browsers act. + * + * Generally, only one delegate should implement this method. + * However, if multiple delegates implement this method, then the first to invoke the completionHandler "wins". + * And subsequent invocations of the completionHandler are ignored. +**/ +- (void)xmppStream:(XMPPStream *)sender didReceiveTrust:(SecTrustRef)trust + completionHandler:(void (^)(BOOL shouldTrustPeer))completionHandler; + +/** + * This method is called after the stream has been secured via SSL/TLS. + * This method may be called if the server required a secure connection during the opening process, + * or if the secureConnection: method was manually invoked. +**/ +- (void)xmppStreamDidSecure:(XMPPStream *)sender; + +/** + * This method is called after the XML stream has been fully opened. + * More precisely, this method is called after an opening and tag have been sent and received, + * and after the stream features have been received, and any required features have been fullfilled. + * At this point it's safe to begin communication with the server. +**/ +- (void)xmppStreamDidConnect:(XMPPStream *)sender; + +/** + * This method is called after registration of a new user has successfully finished. + * If registration fails for some reason, the xmppStream:didNotRegister: method will be called instead. +**/ +- (void)xmppStreamDidRegister:(XMPPStream *)sender; + +/** + * This method is called if registration fails. +**/ +- (void)xmppStream:(XMPPStream *)sender didNotRegister:(NSXMLElement *)error; + +/** + * This method is called after authentication has successfully finished. + * If authentication fails for some reason, the xmppStream:didNotAuthenticate: method will be called instead. +**/ +- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender; + +/** + * This method is called if authentication fails. +**/ +- (void)xmppStream:(XMPPStream *)sender didNotAuthenticate:(NSXMLElement *)error; + +/** + * Binding a JID resource is a standard part of the authentication process, + * and occurs after SASL authentication completes (which generally authenticates the JID username). + * + * This delegate method allows for a custom binding procedure to be used. + * For example: + * - a custom SASL authentication scheme might combine auth with binding + * - stream management (xep-0198) replaces binding if it can resume a previous session + * + * Return nil (or don't implement this method) if you wish to use the standard binding procedure. +**/ +- (id )xmppStreamWillBind:(XMPPStream *)sender; + +/** + * This method is called if the XMPP server doesn't allow our resource of choice + * because it conflicts with an existing resource. + * + * Return an alternative resource or return nil to let the server automatically pick a resource for us. +**/ +- (NSString *)xmppStream:(XMPPStream *)sender alternativeResourceForConflictingResource:(NSString *)conflictingResource; + +/** + * These methods are called before their respective XML elements are broadcast as received to the rest of the stack. + * These methods can be used to modify elements on the fly. + * (E.g. perform custom decryption so the rest of the stack sees readable text.) + * + * You may also filter incoming elements by returning nil. + * + * When implementing these methods to modify the element, you do not need to copy the given element. + * You can simply edit the given element, and return it. + * The reason these methods return an element, instead of void, is to allow filtering. + * + * Concerning thread-safety, delegates implementing the method are invoked one-at-a-time to + * allow thread-safe modification of the given elements. + * + * You should NOT implement these methods unless you have good reason to do so. + * For general processing and notification of received elements, please use xmppStream:didReceiveX: methods. + * + * @see xmppStream:didReceiveIQ: + * @see xmppStream:didReceiveMessage: + * @see xmppStream:didReceivePresence: +**/ +- (XMPPIQ *)xmppStream:(XMPPStream *)sender willReceiveIQ:(XMPPIQ *)iq; +- (XMPPMessage *)xmppStream:(XMPPStream *)sender willReceiveMessage:(XMPPMessage *)message; +- (XMPPPresence *)xmppStream:(XMPPStream *)sender willReceivePresence:(XMPPPresence *)presence; + +/** + * This method is called if any of the xmppStream:willReceiveX: methods filter the incoming stanza. + * + * It may be useful for some extensions to know that something was received, + * even if it was filtered for some reason. +**/ +- (void)xmppStreamDidFilterStanza:(XMPPStream *)sender; + +/** + * These methods are called after their respective XML elements are received on the stream. + * + * In the case of an IQ, the delegate method should return YES if it has or will respond to the given IQ. + * If the IQ is of type 'get' or 'set', and no delegates respond to the IQ, + * then xmpp stream will automatically send an error response. + * + * Concerning thread-safety, delegates shouldn't modify the given elements. + * As documented in NSXML / KissXML, elements are read-access thread-safe, but write-access thread-unsafe. + * If you have need to modify an element for any reason, + * you should copy the element first, and then modify and use the copy. +**/ +- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq; +- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message; +- (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence; + +/** + * This method is called if an XMPP error is received. + * In other words, a . + * + * However, this method may also be called for any unrecognized xml stanzas. + * + * Note that standard errors ( for example) are delivered normally, + * via the other didReceive...: methods. +**/ +- (void)xmppStream:(XMPPStream *)sender didReceiveError:(NSXMLElement *)error; + +/** + * These methods are called before their respective XML elements are sent over the stream. + * These methods can be used to modify outgoing elements on the fly. + * (E.g. add standard information for custom protocols.) + * + * You may also filter outgoing elements by returning nil. + * + * When implementing these methods to modify the element, you do not need to copy the given element. + * You can simply edit the given element, and return it. + * The reason these methods return an element, instead of void, is to allow filtering. + * + * Concerning thread-safety, delegates implementing the method are invoked one-at-a-time to + * allow thread-safe modification of the given elements. + * + * You should NOT implement these methods unless you have good reason to do so. + * For general processing and notification of sent elements, please use xmppStream:didSendX: methods. + * + * @see xmppStream:didSendIQ: + * @see xmppStream:didSendMessage: + * @see xmppStream:didSendPresence: +**/ +- (XMPPIQ *)xmppStream:(XMPPStream *)sender willSendIQ:(XMPPIQ *)iq; +- (XMPPMessage *)xmppStream:(XMPPStream *)sender willSendMessage:(XMPPMessage *)message; +- (XMPPPresence *)xmppStream:(XMPPStream *)sender willSendPresence:(XMPPPresence *)presence; + +/** + * These methods are called after their respective XML elements are sent over the stream. + * These methods may be used to listen for certain events (such as an unavailable presence having been sent), + * or for general logging purposes. (E.g. a central history logging mechanism). +**/ +- (void)xmppStream:(XMPPStream *)sender didSendIQ:(XMPPIQ *)iq; +- (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message; +- (void)xmppStream:(XMPPStream *)sender didSendPresence:(XMPPPresence *)presence; + +/** + * These methods are called after failing to send the respective XML elements over the stream. + * This occurs when the stream gets disconnected before the element can get sent out. +**/ +- (void)xmppStream:(XMPPStream *)sender didFailToSendIQ:(XMPPIQ *)iq error:(NSError *)error; +- (void)xmppStream:(XMPPStream *)sender didFailToSendMessage:(XMPPMessage *)message error:(NSError *)error; +- (void)xmppStream:(XMPPStream *)sender didFailToSendPresence:(XMPPPresence *)presence error:(NSError *)error; + +/** + * This method is called if the XMPP Stream's jid changes. +**/ +- (void)xmppStreamDidChangeMyJID:(XMPPStream *)xmppStream; + +/** + * This method is called if the disconnect method is called. + * It may be used to determine if a disconnection was purposeful, or due to an error. + * + * Note: A disconnect may be either "clean" or "dirty". + * A "clean" disconnect is when the stream sends the closing stanza before disconnecting. + * A "dirty" disconnect is when the stream simply closes its TCP socket. + * In most cases it makes no difference how the disconnect occurs, + * but there are a few contexts in which the difference has various protocol implications. + * + * @see xmppStreamDidSendClosingStreamStanza +**/ +- (void)xmppStreamWasToldToDisconnect:(XMPPStream *)sender; + +/** + * This method is called after the stream has sent the closing stanza. + * This signifies a "clean" disconnect. + * + * Note: A disconnect may be either "clean" or "dirty". + * A "clean" disconnect is when the stream sends the closing stanza before disconnecting. + * A "dirty" disconnect is when the stream simply closes its TCP socket. + * In most cases it makes no difference how the disconnect occurs, + * but there are a few contexts in which the difference has various protocol implications. +**/ +- (void)xmppStreamDidSendClosingStreamStanza:(XMPPStream *)sender; + +/** + * This method is called if the XMPP stream's connect times out. +**/ +- (void)xmppStreamConnectDidTimeout:(XMPPStream *)sender; + +/** + * This method is called after the stream is closed. + * + * The given error parameter will be non-nil if the error was due to something outside the general xmpp realm. + * Some examples: + * - The TCP socket was unexpectedly disconnected. + * - The SRV resolution of the domain failed. + * - Error parsing xml sent from server. + * + * @see xmppStreamConnectDidTimeout: +**/ +- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error; + +/** + * This method is only used in P2P mode when the connectTo:withAddress: method was used. + * + * It allows the delegate to read the element if/when they arrive. + * Recall that the XEP specifies that SHOULD be sent. +**/ +- (void)xmppStream:(XMPPStream *)sender didReceiveP2PFeatures:(NSXMLElement *)streamFeatures; + +/** + * This method is only used in P2P mode when the connectTo:withSocket: method was used. + * + * It allows the delegate to customize the element, + * adding any specific featues the delegate might support. +**/ +- (void)xmppStream:(XMPPStream *)sender willSendP2PFeatures:(NSXMLElement *)streamFeatures; + +/** + * These methods are called as xmpp modules are registered and unregistered with the stream. + * This generally corresponds to xmpp modules being initailzed and deallocated. + * + * The methods may be useful, for example, if a more precise auto delegation mechanism is needed + * than what is available with the autoAddDelegate:toModulesOfClass: method. +**/ +- (void)xmppStream:(XMPPStream *)sender didRegisterModule:(id)module; +- (void)xmppStream:(XMPPStream *)sender willUnregisterModule:(id)module; + +/** + * Custom elements are Non-XMPP elements. + * In other words, not , or elements. + * + * Typically these kinds of elements are not allowed by the XMPP server. + * But some custom implementations may use them. + * The standard example is XEP-0198, which uses & elements. + * + * If you're using custom elements, you must register the custom element name(s). + * Otherwise the xmppStream will treat non-XMPP elements as errors (xmppStream:didReceiveError:). + * + * @see registerCustomElementNames (in XMPPInternal.h) +**/ +- (void)xmppStream:(XMPPStream *)sender didSendCustomElement:(NSXMLElement *)element; +- (void)xmppStream:(XMPPStream *)sender didReceiveCustomElement:(NSXMLElement *)element; + +@end diff --git a/Core/XMPPStream.m b/Core/XMPPStream.m new file mode 100644 index 0000000..1e329e9 --- /dev/null +++ b/Core/XMPPStream.m @@ -0,0 +1,5105 @@ +#import "XMPP.h" +#import "XMPPParser.h" +#import "XMPPLogging.h" +#import "XMPPInternal.h" +#import "XMPPIDTracker.h" +#import "XMPPSRVResolver.h" +#import "NSData+XMPP.h" + +#import +#import + +#if TARGET_OS_IPHONE + // Note: You may need to add the CFNetwork Framework to your project + #import +#endif + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_INFO | XMPP_LOG_FLAG_SEND_RECV; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +/** + * Seeing a return statements within an inner block + * can sometimes be mistaken for a return point of the enclosing method. + * This makes inline blocks a bit easier to read. +**/ +#define return_from_block return + +// Define the timeouts (in seconds) for retreiving various parts of the XML stream +#define TIMEOUT_XMPP_WRITE -1 +#define TIMEOUT_XMPP_READ_START 10 +#define TIMEOUT_XMPP_READ_STREAM -1 + +// Define the tags we'll use to differentiate what it is we're currently reading or writing +#define TAG_XMPP_READ_START 100 +#define TAG_XMPP_READ_STREAM 101 +#define TAG_XMPP_WRITE_START 200 +#define TAG_XMPP_WRITE_STOP 201 +#define TAG_XMPP_WRITE_STREAM 202 +#define TAG_XMPP_WRITE_RECEIPT 203 + +// Define the timeouts (in seconds) for SRV +#define TIMEOUT_SRV_RESOLUTION 30.0 + +NSString *const XMPPStreamErrorDomain = @"XMPPStreamErrorDomain"; +NSString *const XMPPStreamDidChangeMyJIDNotification = @"XMPPStreamDidChangeMyJID"; + +const NSTimeInterval XMPPStreamTimeoutNone = -1; + +enum XMPPStreamFlags +{ + kP2PInitiator = 1 << 0, // If set, we are the P2P initializer + kIsSecure = 1 << 1, // If set, connection has been secured via SSL/TLS + kIsAuthenticated = 1 << 2, // If set, authentication has succeeded + kDidStartNegotiation = 1 << 3, // If set, negotiation has started at least once +}; + +enum XMPPStreamConfig +{ + kP2PMode = 1 << 0, // If set, the XMPPStream was initialized in P2P mode + kResetByteCountPerConnection = 1 << 1, // If set, byte count should be reset per connection +#if TARGET_OS_IPHONE + kEnableBackgroundingOnSocket = 1 << 2, // If set, the VoIP flag should be set on the socket +#endif +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPStream () +{ + dispatch_queue_t xmppQueue; + void *xmppQueueTag; + + dispatch_queue_t willSendIqQueue; + dispatch_queue_t willSendMessageQueue; + dispatch_queue_t willSendPresenceQueue; + + dispatch_queue_t willReceiveStanzaQueue; + + dispatch_queue_t didReceiveIqQueue; + + dispatch_source_t connectTimer; + + GCDMulticastDelegate *multicastDelegate; + + XMPPStreamState state; + + GCDAsyncSocket *asyncSocket; + + uint64_t numberOfBytesSent; + uint64_t numberOfBytesReceived; + + XMPPParser *parser; + NSError *parserError; + NSError *otherError; + + Byte flags; + Byte config; + + NSString *hostName; + UInt16 hostPort; + + XMPPStreamStartTLSPolicy startTLSPolicy; + BOOL skipStartSession; + BOOL validatesResponses; + + id auth; + id customBinding; + NSDate *authenticationDate; + + XMPPJID *myJID_setByClient; + XMPPJID *myJID_setByServer; + XMPPJID *remoteJID; + + XMPPPresence *myPresence; + NSXMLElement *rootElement; + + NSTimeInterval keepAliveInterval; + dispatch_source_t keepAliveTimer; + NSTimeInterval lastSendReceiveTime; + NSData *keepAliveData; + + NSMutableArray *registeredModules; + NSMutableDictionary *autoDelegateDict; + + XMPPSRVResolver *srvResolver; + NSArray *srvResults; + NSUInteger srvResultsIndex; + + XMPPIDTracker *idTracker; + + NSMutableArray *receipts; + NSCountedSet *customElementNames; + + id userTag; +} + +@end + +@interface XMPPElementReceipt (PrivateAPI) + +- (void)signalSuccess; +- (void)signalFailure; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPStream + +@synthesize tag = userTag; + +/** + * Shared initialization between the various init methods. +**/ +- (void)commonInit +{ + xmppQueueTag = &xmppQueueTag; + xmppQueue = dispatch_queue_create("xmpp", DISPATCH_QUEUE_SERIAL); + dispatch_queue_set_specific(xmppQueue, xmppQueueTag, xmppQueueTag, NULL); + + willSendIqQueue = dispatch_queue_create("xmpp.willSendIq", DISPATCH_QUEUE_SERIAL); + willSendMessageQueue = dispatch_queue_create("xmpp.willSendMessage", DISPATCH_QUEUE_SERIAL); + willSendPresenceQueue = dispatch_queue_create("xmpp.willSendPresence", DISPATCH_QUEUE_SERIAL); + + didReceiveIqQueue = dispatch_queue_create("xmpp.didReceiveIq", DISPATCH_QUEUE_SERIAL); + + multicastDelegate = (GCDMulticastDelegate *)[[GCDMulticastDelegate alloc] init]; + + state = STATE_XMPP_DISCONNECTED; + + flags = 0; + config = 0; + + numberOfBytesSent = 0; + numberOfBytesReceived = 0; + + hostPort = 5222; + keepAliveInterval = DEFAULT_KEEPALIVE_INTERVAL; + keepAliveData = [@" " dataUsingEncoding:NSUTF8StringEncoding]; + + registeredModules = [[NSMutableArray alloc] init]; + autoDelegateDict = [[NSMutableDictionary alloc] init]; + + idTracker = [[XMPPIDTracker alloc] initWithStream:self dispatchQueue:xmppQueue]; + + receipts = [[NSMutableArray alloc] init]; +} + +/** + * Standard XMPP initialization. + * The stream is a standard client to server connection. +**/ +- (id)init +{ + if ((self = [super init])) + { + // Common initialization + [self commonInit]; + + // Initialize socket + asyncSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:xmppQueue]; + } + return self; +} + +/** + * Peer to Peer XMPP initialization. + * The stream is a direct client to client connection as outlined in XEP-0174. +**/ +- (id)initP2PFrom:(XMPPJID *)jid +{ + if ((self = [super init])) + { + // Common initialization + [self commonInit]; + + // Store JID + myJID_setByClient = jid; + + // We do not initialize the socket, since the connectP2PWithSocket: method might be used. + + // Initialize configuration + config = kP2PMode; + } + return self; +} + +/** + * Standard deallocation method. + * Every object variable declared in the header file should be released here. +**/ +- (void)dealloc +{ + #if !OS_OBJECT_USE_OBJC + dispatch_release(xmppQueue); + + dispatch_release(willSendIqQueue); + dispatch_release(willSendMessageQueue); + dispatch_release(willSendPresenceQueue); + + if (willReceiveStanzaQueue) { + dispatch_release(willReceiveStanzaQueue); + } + + dispatch_release(didReceiveIqQueue); + #endif + + [asyncSocket setDelegate:nil delegateQueue:NULL]; + [asyncSocket disconnect]; + + [parser setDelegate:nil delegateQueue:NULL]; + + if (keepAliveTimer) + { + dispatch_source_cancel(keepAliveTimer); + } + + [idTracker removeAllIDs]; + + for (XMPPElementReceipt *receipt in receipts) + { + [receipt signalFailure]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Properties +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@synthesize xmppQueue; +@synthesize xmppQueueTag; + +- (XMPPStreamState)state +{ + __block XMPPStreamState result = STATE_XMPP_DISCONNECTED; + + dispatch_block_t block = ^{ + result = state; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +- (NSString *)hostName +{ + if (dispatch_get_specific(xmppQueueTag)) + { + return hostName; + } + else + { + __block NSString *result; + + dispatch_sync(xmppQueue, ^{ + result = hostName; + }); + + return result; + } +} + +- (void)setHostName:(NSString *)newHostName +{ + if (dispatch_get_specific(xmppQueueTag)) + { + if (hostName != newHostName) + { + hostName = [newHostName copy]; + } + } + else + { + NSString *newHostNameCopy = [newHostName copy]; + + dispatch_async(xmppQueue, ^{ + hostName = newHostNameCopy; + }); + + } +} + +- (UInt16)hostPort +{ + if (dispatch_get_specific(xmppQueueTag)) + { + return hostPort; + } + else + { + __block UInt16 result; + + dispatch_sync(xmppQueue, ^{ + result = hostPort; + }); + + return result; + } +} + +- (void)setHostPort:(UInt16)newHostPort +{ + dispatch_block_t block = ^{ + hostPort = newHostPort; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (XMPPStreamStartTLSPolicy)startTLSPolicy +{ + __block XMPPStreamStartTLSPolicy result; + + dispatch_block_t block = ^{ + result = startTLSPolicy; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +- (void)setStartTLSPolicy:(XMPPStreamStartTLSPolicy)flag +{ + dispatch_block_t block = ^{ + startTLSPolicy = flag; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (XMPPJID *)myJID +{ + __block XMPPJID *result = nil; + + dispatch_block_t block = ^{ + + if (myJID_setByServer) + result = myJID_setByServer; + else + result = myJID_setByClient; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +- (void)setMyJID_setByClient:(XMPPJID *)newMyJID +{ + // XMPPJID is an immutable class (copy == retain) + + dispatch_block_t block = ^{ + + if (![myJID_setByClient isEqualToJID:newMyJID]) + { + myJID_setByClient = newMyJID; + + if (myJID_setByServer == nil) + { + [[NSNotificationCenter defaultCenter] postNotificationName:XMPPStreamDidChangeMyJIDNotification + object:self]; + [multicastDelegate xmppStreamDidChangeMyJID:self]; + } + } + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (void)setMyJID_setByServer:(XMPPJID *)newMyJID +{ + // XMPPJID is an immutable class (copy == retain) + + dispatch_block_t block = ^{ + + if (![myJID_setByServer isEqualToJID:newMyJID]) + { + XMPPJID *oldMyJID; + if (myJID_setByServer) + oldMyJID = myJID_setByServer; + else + oldMyJID = myJID_setByClient; + + myJID_setByServer = newMyJID; + + if (![oldMyJID isEqualToJID:newMyJID]) + { + [[NSNotificationCenter defaultCenter] postNotificationName:XMPPStreamDidChangeMyJIDNotification + object:self]; + [multicastDelegate xmppStreamDidChangeMyJID:self]; + } + } + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (void)setMyJID:(XMPPJID *)newMyJID +{ + [self setMyJID_setByClient:newMyJID]; +} + +- (XMPPJID *)remoteJID +{ + if (dispatch_get_specific(xmppQueueTag)) + { + return remoteJID; + } + else + { + __block XMPPJID *result; + + dispatch_sync(xmppQueue, ^{ + result = remoteJID; + }); + + return result; + } +} + +- (XMPPPresence *)myPresence +{ + if (dispatch_get_specific(xmppQueueTag)) + { + return myPresence; + } + else + { + __block XMPPPresence *result; + + dispatch_sync(xmppQueue, ^{ + result = myPresence; + }); + + return result; + } +} + +- (NSTimeInterval)keepAliveInterval +{ + __block NSTimeInterval result = 0.0; + + dispatch_block_t block = ^{ + result = keepAliveInterval; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +- (void)setKeepAliveInterval:(NSTimeInterval)interval +{ + dispatch_block_t block = ^{ + + if (keepAliveInterval != interval) + { + if (interval <= 0.0) + keepAliveInterval = interval; + else + keepAliveInterval = MAX(interval, MIN_KEEPALIVE_INTERVAL); + + [self setupKeepAliveTimer]; + } + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (char)keepAliveWhitespaceCharacter +{ + __block char keepAliveChar = ' '; + + dispatch_block_t block = ^{ + + NSString *keepAliveString = [[NSString alloc] initWithData:keepAliveData encoding:NSUTF8StringEncoding]; + if ([keepAliveString length] > 0) + { + keepAliveChar = (char)[keepAliveString characterAtIndex:0]; + } + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return keepAliveChar; +} + +- (void)setKeepAliveWhitespaceCharacter:(char)keepAliveChar +{ + dispatch_block_t block = ^{ + + if (keepAliveChar == ' ' || keepAliveChar == '\n' || keepAliveChar == '\t') + { + keepAliveData = [[NSString stringWithFormat:@"%c", keepAliveChar] dataUsingEncoding:NSUTF8StringEncoding]; + } + else + { + XMPPLogWarn(@"Invalid whitespace character! Must be: space, newline, or tab"); + } + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (uint64_t)numberOfBytesSent +{ + __block uint64_t result = 0; + + dispatch_block_t block = ^{ + result = numberOfBytesSent; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +- (uint64_t)numberOfBytesReceived +{ + __block uint64_t result = 0; + + dispatch_block_t block = ^{ + result = numberOfBytesReceived; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +- (void)getNumberOfBytesSent:(uint64_t *)bytesSentPtr numberOfBytesReceived:(uint64_t *)bytesReceivedPtr +{ + __block uint64_t bytesSent = 0; + __block uint64_t bytesReceived = 0; + + dispatch_block_t block = ^{ + bytesSent = numberOfBytesSent; + bytesReceived = numberOfBytesReceived; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + if (bytesSentPtr) *bytesSentPtr = bytesSent; + if (bytesReceivedPtr) *bytesReceivedPtr = bytesReceived; +} + +- (BOOL)resetByteCountPerConnection +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (config & kResetByteCountPerConnection) ? YES : NO; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +- (void)setResetByteCountPerConnection:(BOOL)flag +{ + dispatch_block_t block = ^{ + if (flag) + config |= kResetByteCountPerConnection; + else + config &= ~kResetByteCountPerConnection; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (BOOL)skipStartSession +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = skipStartSession; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +- (void)setSkipStartSession:(BOOL)flag +{ + dispatch_block_t block = ^{ + skipStartSession = flag; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (BOOL)validatesResponses +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = validatesResponses; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +- (void)setValidatesResponses:(BOOL)flag +{ + dispatch_block_t block = ^{ + validatesResponses = flag; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +#if TARGET_OS_IPHONE + +- (BOOL)enableBackgroundingOnSocket +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (config & kEnableBackgroundingOnSocket) ? YES : NO; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +- (void)setEnableBackgroundingOnSocket:(BOOL)flag +{ + dispatch_block_t block = ^{ + if (flag) + config |= kEnableBackgroundingOnSocket; + else + config &= ~kEnableBackgroundingOnSocket; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)addDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue +{ + // Asynchronous operation (if outside xmppQueue) + + dispatch_block_t block = ^{ + [multicastDelegate addDelegate:delegate delegateQueue:delegateQueue]; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (void)removeDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue +{ + // Synchronous operation + + dispatch_block_t block = ^{ + [multicastDelegate removeDelegate:delegate delegateQueue:delegateQueue]; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); +} + +- (void)removeDelegate:(id)delegate +{ + // Synchronous operation + + dispatch_block_t block = ^{ + [multicastDelegate removeDelegate:delegate]; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); +} + +/** + * Returns YES if the stream was opened in P2P mode. + * In other words, the stream was created via initP2PFrom: to use XEP-0174. +**/ +- (BOOL)isP2P +{ + if (dispatch_get_specific(xmppQueueTag)) + { + return (config & kP2PMode) ? YES : NO; + } + else + { + __block BOOL result; + + dispatch_sync(xmppQueue, ^{ + result = (config & kP2PMode) ? YES : NO; + }); + + return result; + } +} + +- (BOOL)isP2PInitiator +{ + if (dispatch_get_specific(xmppQueueTag)) + { + return ((config & kP2PMode) && (flags & kP2PInitiator)); + } + else + { + __block BOOL result; + + dispatch_sync(xmppQueue, ^{ + result = ((config & kP2PMode) && (flags & kP2PInitiator)); + }); + + return result; + } +} + +- (BOOL)isP2PRecipient +{ + if (dispatch_get_specific(xmppQueueTag)) + { + return ((config & kP2PMode) && !(flags & kP2PInitiator)); + } + else + { + __block BOOL result; + + dispatch_sync(xmppQueue, ^{ + result = ((config & kP2PMode) && !(flags & kP2PInitiator)); + }); + + return result; + } +} + +- (BOOL)didStartNegotiation +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + return (flags & kDidStartNegotiation) ? YES : NO; +} + +- (void)setDidStartNegotiation:(BOOL)flag +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + if (flag) + flags |= kDidStartNegotiation; + else + flags &= ~kDidStartNegotiation; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Connection State +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Returns YES if the connection is closed, and thus no stream is open. + * If the stream is neither disconnected, nor connected, then a connection is currently being established. +**/ +- (BOOL)isDisconnected +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (state == STATE_XMPP_DISCONNECTED); + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +/** + * Returns YES is the connection is currently connecting + **/ + +- (BOOL)isConnecting +{ + XMPPLogTrace(); + + __block BOOL result = NO; + + dispatch_block_t block = ^{ @autoreleasepool { + result = (state == STATE_XMPP_CONNECTING); + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} +/** + * Returns YES if the connection is open, and the stream has been properly established. + * If the stream is neither disconnected, nor connected, then a connection is currently being established. +**/ +- (BOOL)isConnected +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (state == STATE_XMPP_CONNECTED); + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Connect Timeout +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Start Connect Timeout +**/ +- (void)startConnectTimeout:(NSTimeInterval)timeout +{ + XMPPLogTrace(); + + if (timeout >= 0.0 && !connectTimer) + { + connectTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, xmppQueue); + + dispatch_source_set_event_handler(connectTimer, ^{ @autoreleasepool { + + [self doConnectTimeout]; + }}); + +#if !OS_OBJECT_USE_OBJC + dispatch_source_t theConnectTimer = connectTimer; + dispatch_source_set_cancel_handler(connectTimer, ^{ + XMPPLogVerbose(@"%@: dispatch_release(connectTimer)", THIS_FILE); + dispatch_release(theConnectTimer); + }); +#endif + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (timeout * NSEC_PER_SEC)); + dispatch_source_set_timer(connectTimer, tt, DISPATCH_TIME_FOREVER, 0); + + dispatch_resume(connectTimer); + } +} + +/** + * End Connect Timeout +**/ +- (void)endConnectTimeout +{ + XMPPLogTrace(); + + if (connectTimer) + { + dispatch_source_cancel(connectTimer); + connectTimer = NULL; + } +} + +/** + * Connect has timed out, so inform the delegates and close the connection +**/ +- (void)doConnectTimeout +{ + XMPPLogTrace(); + + [self endConnectTimeout]; + + if (state != STATE_XMPP_DISCONNECTED) + { + [multicastDelegate xmppStreamConnectDidTimeout:self]; + + if (state == STATE_XMPP_RESOLVING_SRV) + { + [srvResolver stop]; + srvResolver = nil; + + state = STATE_XMPP_DISCONNECTED; + } + else + { + [asyncSocket disconnect]; + + // Everthing will be handled in socketDidDisconnect:withError: + } + } + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark C2S Connection +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)connectToHost:(NSString *)host onPort:(UInt16)port withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + BOOL result = [asyncSocket connectToHost:host onPort:port error:errPtr]; + + if (result && [self resetByteCountPerConnection]) + { + numberOfBytesSent = 0; + numberOfBytesReceived = 0; + } + + if(result) + { + [self startConnectTimeout:timeout]; + } + + return result; +} + +- (BOOL)connectWithTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr +{ + XMPPLogTrace(); + + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (state != STATE_XMPP_DISCONNECTED) + { + NSString *errMsg = @"Attempting to connect while already connected or connecting."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidState userInfo:info]; + + result = NO; + return_from_block; + } + + if ([self isP2P]) + { + NSString *errMsg = @"P2P streams must use either connectTo:withAddress: or connectP2PWithSocket:."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidType userInfo:info]; + + result = NO; + return_from_block; + } + + if (myJID_setByClient == nil) + { + // Note: If you wish to use anonymous authentication, you should still set myJID prior to calling connect. + // You can simply set it to something like "anonymous@", where "" is the proper domain. + // After the authentication process, you can query the myJID property to see what your assigned JID is. + // + // Setting myJID allows the framework to follow the xmpp protocol properly, + // and it allows the framework to connect to servers without a DNS entry. + // + // For example, one may setup a private xmpp server for internal testing on their local network. + // The xmpp domain of the server may be something like "testing.mycompany.com", + // but since the server is internal, an IP (192.168.1.22) is used as the hostname to connect. + // + // Proper connection requires a TCP connection to the IP (192.168.1.22), + // but the xmpp handshake requires the xmpp domain (testing.mycompany.com). + + NSString *errMsg = @"You must set myJID before calling connect."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidProperty userInfo:info]; + + result = NO; + return_from_block; + } + + // Notify delegates + [multicastDelegate xmppStreamWillConnect:self]; + + if ([hostName length] == 0) + { + // Resolve the hostName via myJID SRV resolution + + state = STATE_XMPP_RESOLVING_SRV; + + srvResolver = [[XMPPSRVResolver alloc] initWithdDelegate:self delegateQueue:xmppQueue resolverQueue:NULL]; + + srvResults = nil; + srvResultsIndex = 0; + + NSString *srvName = [XMPPSRVResolver srvNameFromXMPPDomain:[myJID_setByClient domain]]; + + [srvResolver startWithSRVName:srvName timeout:TIMEOUT_SRV_RESOLUTION]; + + result = YES; + } + else + { + // Open TCP connection to the configured hostName. + + state = STATE_XMPP_CONNECTING; + + NSError *connectErr = nil; + result = [self connectToHost:hostName onPort:hostPort withTimeout:XMPPStreamTimeoutNone error:&connectErr]; + + if (!result) + { + err = connectErr; + state = STATE_XMPP_DISCONNECTED; + } + } + + if(result) + { + [self startConnectTimeout:timeout]; + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +- (BOOL)oldSchoolSecureConnectWithTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr +{ + XMPPLogTrace(); + + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Go through the regular connect routine + NSError *connectErr = nil; + result = [self connectWithTimeout:timeout error:&connectErr]; + + if (result) + { + // Mark the secure flag. + // We will check the flag in socket:didConnectToHost:port: + + [self setIsSecure:YES]; + } + else + { + err = connectErr; + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark P2P Connection +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Starts a P2P connection to the given user and given address. + * This method only works with XMPPStream objects created using the initP2P method. + * + * The given address is specified as a sockaddr structure wrapped in a NSData object. + * For example, a NSData object returned from NSNetservice's addresses method. +**/ +- (BOOL)connectTo:(XMPPJID *)jid withAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr +{ + XMPPLogTrace(); + + __block BOOL result = YES; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (state != STATE_XMPP_DISCONNECTED) + { + NSString *errMsg = @"Attempting to connect while already connected or connecting."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidState userInfo:info]; + + result = NO; + return_from_block; + } + + if (![self isP2P]) + { + NSString *errMsg = @"Non P2P streams must use the connect: method"; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidType userInfo:info]; + + result = NO; + return_from_block; + } + + // Turn on P2P initiator flag + flags |= kP2PInitiator; + + // Store remoteJID + remoteJID = [jid copy]; + + NSAssert((asyncSocket == nil), @"Forgot to release the previous asyncSocket instance."); + + // Notify delegates + [multicastDelegate xmppStreamWillConnect:self]; + + // Update state + state = STATE_XMPP_CONNECTING; + + // Initailize socket + asyncSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:xmppQueue]; + + NSError *connectErr = nil; + result = [asyncSocket connectToAddress:remoteAddr error:&connectErr]; + + if (result == NO) + { + err = connectErr; + state = STATE_XMPP_DISCONNECTED; + } + else if ([self resetByteCountPerConnection]) + { + numberOfBytesSent = 0; + numberOfBytesReceived = 0; + } + + if(result) + { + [self startConnectTimeout:timeout]; + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +/** + * Starts a P2P connection with the given accepted socket. + * This method only works with XMPPStream objects created using the initP2P method. + * + * The given socket should be a socket that has already been accepted. + * The remoteJID will be extracted from the opening stream negotiation. +**/ +- (BOOL)connectP2PWithSocket:(GCDAsyncSocket *)acceptedSocket error:(NSError **)errPtr +{ + XMPPLogTrace(); + + __block BOOL result = YES; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (state != STATE_XMPP_DISCONNECTED) + { + NSString *errMsg = @"Attempting to connect while already connected or connecting."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidState userInfo:info]; + + result = NO; + return_from_block; + } + + if (![self isP2P]) + { + NSString *errMsg = @"Non P2P streams must use the connect: method"; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidType userInfo:info]; + + result = NO; + return_from_block; + } + + if (acceptedSocket == nil) + { + NSString *errMsg = @"Parameter acceptedSocket is nil."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidParameter userInfo:info]; + + result = NO; + return_from_block; + } + + // Turn off P2P initiator flag + flags &= ~kP2PInitiator; + + NSAssert((asyncSocket == nil), @"Forgot to release the previous asyncSocket instance."); + + // Store and configure socket + asyncSocket = acceptedSocket; + [asyncSocket setDelegate:self delegateQueue:xmppQueue]; + + // Notify delegates + [multicastDelegate xmppStream:self socketDidConnect:asyncSocket]; + + // Update state + state = STATE_XMPP_CONNECTING; + + if ([self resetByteCountPerConnection]) + { + numberOfBytesSent = 0; + numberOfBytesReceived = 0; + } + + // Start the XML stream + [self startNegotiation]; + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Disconnect +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Closes the connection to the remote host. +**/ +- (void)disconnect +{ + XMPPLogTrace(); + + dispatch_block_t block = ^{ @autoreleasepool { + + if (state != STATE_XMPP_DISCONNECTED) + { + [multicastDelegate xmppStreamWasToldToDisconnect:self]; + + if (state == STATE_XMPP_RESOLVING_SRV) + { + [srvResolver stop]; + srvResolver = nil; + + state = STATE_XMPP_DISCONNECTED; + + [multicastDelegate xmppStreamDidDisconnect:self withError:nil]; + } + else + { + [asyncSocket disconnect]; + + // Everthing will be handled in socketDidDisconnect:withError: + } + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); +} + +- (void)disconnectAfterSending +{ + XMPPLogTrace(); + + dispatch_block_t block = ^{ @autoreleasepool { + + if (state != STATE_XMPP_DISCONNECTED) + { + [multicastDelegate xmppStreamWasToldToDisconnect:self]; + + if (state == STATE_XMPP_RESOLVING_SRV) + { + [srvResolver stop]; + srvResolver = nil; + + state = STATE_XMPP_DISCONNECTED; + + [multicastDelegate xmppStreamDidDisconnect:self withError:nil]; + } + else + { + NSString *termStr = @""; + NSData *termData = [termStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", termStr); + numberOfBytesSent += [termData length]; + + [asyncSocket writeData:termData withTimeout:TIMEOUT_XMPP_WRITE tag:TAG_XMPP_WRITE_STOP]; + [asyncSocket disconnectAfterWriting]; + + // Everthing will be handled in socketDidDisconnect:withError: + } + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Security +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Returns YES if SSL/TLS has been used to secure the connection. +**/ +- (BOOL)isSecure +{ + if (dispatch_get_specific(xmppQueueTag)) + { + return (flags & kIsSecure) ? YES : NO; + } + else + { + __block BOOL result; + + dispatch_sync(xmppQueue, ^{ + result = (flags & kIsSecure) ? YES : NO; + }); + + return result; + } +} + +- (void)setIsSecure:(BOOL)flag +{ + dispatch_block_t block = ^{ + if(flag) + flags |= kIsSecure; + else + flags &= ~kIsSecure; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (BOOL)supportsStartTLS +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ @autoreleasepool { + + // The root element can be properly queried for authentication mechanisms anytime after the + // stream:features are received, and TLS has been setup (if required) + if (state >= STATE_XMPP_POST_NEGOTIATION) + { + NSXMLElement *features = [rootElement elementForName:@"stream:features"]; + NSXMLElement *starttls = [features elementForName:@"starttls" xmlns:@"urn:ietf:params:xml:ns:xmpp-tls"]; + + result = (starttls != nil); + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +- (void)sendStartTLSRequest +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + NSString *starttls = @""; + + NSData *outgoingData = [starttls dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", starttls); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:TAG_XMPP_WRITE_STREAM]; +} + +- (BOOL)secureConnection:(NSError **)errPtr +{ + XMPPLogTrace(); + + __block BOOL result = YES; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (state != STATE_XMPP_CONNECTED) + { + NSString *errMsg = @"Please wait until the stream is connected."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidState userInfo:info]; + + result = NO; + return_from_block; + } + + if ([self isSecure]) + { + NSString *errMsg = @"The connection is already secure."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidState userInfo:info]; + + result = NO; + return_from_block; + } + + if (![self supportsStartTLS]) + { + NSString *errMsg = @"The server does not support startTLS."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamUnsupportedAction userInfo:info]; + + result = NO; + return_from_block; + } + + // Update state + state = STATE_XMPP_STARTTLS_1; + + // Send the startTLS XML request + [self sendStartTLSRequest]; + + // We do not mark the stream as secure yet. + // We're waiting to receive the response from the + // server before we actually start the TLS handshake. + + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Registration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method checks the stream features of the connected server to determine if in-band registartion is supported. + * If we are not connected to a server, this method simply returns NO. +**/ +- (BOOL)supportsInBandRegistration +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ @autoreleasepool { + + // The root element can be properly queried for authentication mechanisms anytime after the + // stream:features are received, and TLS has been setup (if required) + if (state >= STATE_XMPP_POST_NEGOTIATION) + { + NSXMLElement *features = [rootElement elementForName:@"stream:features"]; + NSXMLElement *reg = [features elementForName:@"register" xmlns:@"http://jabber.org/features/iq-register"]; + + result = (reg != nil); + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +/** + * This method attempts to register a new user on the server using the given elements. + * The result of this action will be returned via the delegate methods. + * + * If the XMPPStream is not connected, or the server doesn't support in-band registration, this method does nothing. +**/ +- (BOOL)registerWithElements:(NSArray *)elements error:(NSError **)errPtr +{ + XMPPLogTrace(); + + __block BOOL result = YES; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (state != STATE_XMPP_CONNECTED) + { + NSString *errMsg = @"Please wait until the stream is connected."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidState userInfo:info]; + + result = NO; + return_from_block; + } + + if (![self supportsInBandRegistration]) + { + NSString *errMsg = @"The server does not support in band registration."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamUnsupportedAction userInfo:info]; + + result = NO; + return_from_block; + } + + NSXMLElement *queryElement = [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:register"]; + + for(NSXMLElement *element in elements) + { + [queryElement addChild:element]; + } + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set"]; + [iq addChild:queryElement]; + + NSString *outgoingStr = [iq compactXMLString]; + NSData *outgoingData = [outgoingStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", outgoingStr); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:TAG_XMPP_WRITE_STREAM]; + + // Update state + state = STATE_XMPP_REGISTERING; + + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + if (errPtr) + *errPtr = err; + + return result; + +} + +/** + * This method attempts to register a new user on the server using the given username and password. + * The result of this action will be returned via the delegate methods. + * + * If the XMPPStream is not connected, or the server doesn't support in-band registration, this method does nothing. +**/ +- (BOOL)registerWithPassword:(NSString *)password error:(NSError **)errPtr +{ + XMPPLogTrace(); + + __block BOOL result = YES; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (myJID_setByClient == nil) + { + NSString *errMsg = @"You must set myJID before calling registerWithPassword:error:."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidProperty userInfo:info]; + + result = NO; + return_from_block; + } + + NSString *username = [myJID_setByClient user]; + + NSMutableArray *elements = [NSMutableArray array]; + [elements addObject:[NSXMLElement elementWithName:@"username" stringValue:username]]; + [elements addObject:[NSXMLElement elementWithName:@"password" stringValue:password]]; + + [self registerWithElements:elements error:errPtr]; + + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Authentication +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSArray *)supportedAuthenticationMechanisms +{ + __block NSMutableArray *result = [[NSMutableArray alloc] init]; + + dispatch_block_t block = ^{ @autoreleasepool { + + // The root element can be properly queried for authentication mechanisms anytime after the + // stream:features are received, and TLS has been setup (if required). + + if (state >= STATE_XMPP_POST_NEGOTIATION) + { + NSXMLElement *features = [rootElement elementForName:@"stream:features"]; + NSXMLElement *mech = [features elementForName:@"mechanisms" xmlns:@"urn:ietf:params:xml:ns:xmpp-sasl"]; + + NSArray *mechanisms = [mech elementsForName:@"mechanism"]; + + for (NSXMLElement *mechanism in mechanisms) + { + [result addObject:[mechanism stringValue]]; + } + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +/** + * This method checks the stream features of the connected server to determine + * if the given authentication mechanism is supported. + * + * If we are not connected to a server, this method simply returns NO. +**/ +- (BOOL)supportsAuthenticationMechanism:(NSString *)mechanismType +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ @autoreleasepool { + + // The root element can be properly queried for authentication mechanisms anytime after the + // stream:features are received, and TLS has been setup (if required). + + if (state >= STATE_XMPP_POST_NEGOTIATION) + { + NSXMLElement *features = [rootElement elementForName:@"stream:features"]; + NSXMLElement *mech = [features elementForName:@"mechanisms" xmlns:@"urn:ietf:params:xml:ns:xmpp-sasl"]; + + NSArray *mechanisms = [mech elementsForName:@"mechanism"]; + + for (NSXMLElement *mechanism in mechanisms) + { + if ([[mechanism stringValue] isEqualToString:mechanismType]) + { + result = YES; + break; + } + } + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +- (BOOL)authenticate:(id )inAuth error:(NSError **)errPtr +{ + XMPPLogTrace(); + + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (state != STATE_XMPP_CONNECTED) + { + NSString *errMsg = @"Please wait until the stream is connected."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidState userInfo:info]; + + result = NO; + return_from_block; + } + + if (myJID_setByClient == nil) + { + NSString *errMsg = @"You must set myJID before calling authenticate:error:."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidProperty userInfo:info]; + + result = NO; + return_from_block; + } + + // Change state. + // We do this now because when we invoke the start method below, + // it may in turn invoke our sendAuthElement method, which expects us to be in STATE_XMPP_AUTH. + state = STATE_XMPP_AUTH; + + if ([inAuth start:&err]) + { + auth = inAuth; + result = YES; + } + else + { + // Unable to start authentication for some reason. + // Revert back to connected state. + state = STATE_XMPP_CONNECTED; + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +/** + * This method applies to standard password authentication schemes only. + * This is NOT the primary authentication method. + * + * @see authenticate:error: + * + * This method exists for backwards compatibility, and may disappear in future versions. +**/ +- (BOOL)authenticateWithPassword:(NSString *)inPassword error:(NSError **)errPtr +{ + XMPPLogTrace(); + + // The given password parameter could be mutable + NSString *password = [inPassword copy]; + + + __block BOOL result = YES; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (state != STATE_XMPP_CONNECTED) + { + NSString *errMsg = @"Please wait until the stream is connected."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidState userInfo:info]; + + result = NO; + return_from_block; + } + + if (myJID_setByClient == nil) + { + NSString *errMsg = @"You must set myJID before calling authenticate:error:."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidProperty userInfo:info]; + + result = NO; + return_from_block; + } + + // Choose the best authentication method. + // + // P.S. - This method is deprecated. + + id someAuth = nil; + + if ([self supportsSCRAMSHA1Authentication]) + { + someAuth = [[XMPPSCRAMSHA1Authentication alloc] initWithStream:self password:password]; + result = [self authenticate:someAuth error:&err]; + } + else if ([self supportsDigestMD5Authentication]) + { + someAuth = [[XMPPDigestMD5Authentication alloc] initWithStream:self password:password]; + result = [self authenticate:someAuth error:&err]; + } + else if ([self supportsPlainAuthentication]) + { + someAuth = [[XMPPPlainAuthentication alloc] initWithStream:self password:password]; + result = [self authenticate:someAuth error:&err]; + } + else if ([self supportsDeprecatedDigestAuthentication]) + { + someAuth = [[XMPPDeprecatedDigestAuthentication alloc] initWithStream:self password:password]; + result = [self authenticate:someAuth error:&err]; + } + else if ([self supportsDeprecatedPlainAuthentication]) + { + someAuth = [[XMPPDeprecatedDigestAuthentication alloc] initWithStream:self password:password]; + result = [self authenticate:someAuth error:&err]; + } + else + { + NSString *errMsg = @"No suitable authentication method found"; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamUnsupportedAction userInfo:info]; + + result = NO; + } + }}; + + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +- (BOOL)isAuthenticating +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ @autoreleasepool { + result = (state == STATE_XMPP_AUTH); + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +- (BOOL)isAuthenticated +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (flags & kIsAuthenticated) ? YES : NO; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +- (void)setIsAuthenticated:(BOOL)flag +{ + dispatch_block_t block = ^{ + if(flag) + { + flags |= kIsAuthenticated; + authenticationDate = [NSDate date]; + } + else + { + flags &= ~kIsAuthenticated; + authenticationDate = nil; + } + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (NSDate *)authenticationDate +{ + __block NSDate *result = nil; + + dispatch_block_t block = ^{ + if(flags & kIsAuthenticated) + { + result = authenticationDate; + } + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Compression +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSArray *)supportedCompressionMethods +{ + __block NSMutableArray *result = [[NSMutableArray alloc] init]; + + dispatch_block_t block = ^{ @autoreleasepool { + + // The root element can be properly queried for compression methods anytime after the + // stream:features are received, and TLS has been setup (if required). + + if (state >= STATE_XMPP_POST_NEGOTIATION) + { + NSXMLElement *features = [rootElement elementForName:@"stream:features"]; + NSXMLElement *compression = [features elementForName:@"compression" xmlns:@"http://jabber.org/features/compress"]; + + NSArray *methods = [compression elementsForName:@"method"]; + + for (NSXMLElement *method in methods) + { + [result addObject:[method stringValue]]; + } + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +/** + * This method checks the stream features of the connected server to determine + * if the given compression method is supported. + * + * If we are not connected to a server, this method simply returns NO. +**/ +- (BOOL)supportsCompressionMethod:(NSString *)compressionMethod +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ @autoreleasepool { + + // The root element can be properly queried for compression methods anytime after the + // stream:features are received, and TLS has been setup (if required). + + if (state >= STATE_XMPP_POST_NEGOTIATION) + { + NSXMLElement *features = [rootElement elementForName:@"stream:features"]; + NSXMLElement *compression = [features elementForName:@"compression" xmlns:@"http://jabber.org/features/compress"]; + + NSArray *methods = [compression elementsForName:@"method"]; + + for (NSXMLElement *method in methods) + { + if ([[method stringValue] isEqualToString:compressionMethod]) + { + result = YES; + break; + } + } + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark General Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method will return the root element of the document. + * This element contains the opening and tags received from the server + * when the XML stream was opened. + * + * Note: The rootElement is empty, and does not contain all the XML elements the stream has received during it's + * connection. This is done for performance reasons and for the obvious benefit of being more memory efficient. +**/ +- (NSXMLElement *)rootElement +{ + if (dispatch_get_specific(xmppQueueTag)) + { + return rootElement; + } + else + { + __block NSXMLElement *result = nil; + + dispatch_sync(xmppQueue, ^{ + result = [rootElement copy]; + }); + + return result; + } +} + +/** + * Returns the version attribute from the servers's element. + * This should be at least 1.0 to be RFC 3920 compliant. + * If no version number was set, the server is not RFC compliant, and 0 is returned. +**/ +- (float)serverXmppStreamVersionNumber +{ + if (dispatch_get_specific(xmppQueueTag)) + { + return [rootElement attributeFloatValueForName:@"version" withDefaultValue:0.0F]; + } + else + { + __block float result; + + dispatch_sync(xmppQueue, ^{ + result = [rootElement attributeFloatValueForName:@"version" withDefaultValue:0.0F]; + }); + + return result; + } +} + +- (void)sendIQ:(XMPPIQ *)iq withTag:(long)tag +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); + + // We're getting ready to send an IQ. + // Notify delegates to allow them to optionally alter/filter the outgoing IQ. + + SEL selector = @selector(xmppStream:willSendIQ:); + + if (![multicastDelegate hasDelegateThatRespondsToSelector:selector]) + { + // None of the delegates implement the method. + // Use a shortcut. + + [self continueSendIQ:iq withTag:tag]; + } + else + { + // Notify all interested delegates. + // This must be done serially to allow them to alter the element in a thread-safe manner. + + GCDMulticastDelegateEnumerator *delegateEnumerator = [multicastDelegate delegateEnumerator]; + + dispatch_async(willSendIqQueue, ^{ @autoreleasepool { + + // Allow delegates to modify and/or filter outgoing element + + __block XMPPIQ *modifiedIQ = iq; + + id del; + dispatch_queue_t dq; + + while (modifiedIQ && [delegateEnumerator getNextDelegate:&del delegateQueue:&dq forSelector:selector]) + { + #if DEBUG + { + char methodReturnType[32]; + + Method method = class_getInstanceMethod([del class], selector); + method_getReturnType(method, methodReturnType, sizeof(methodReturnType)); + + if (strcmp(methodReturnType, @encode(XMPPIQ*)) != 0) + { + NSAssert(NO, @"Method xmppStream:willSendIQ: is no longer void (see XMPPStream.h). " + @"Culprit = %@", NSStringFromClass([del class])); + } + } + #endif + + dispatch_sync(dq, ^{ @autoreleasepool { + + modifiedIQ = [del xmppStream:self willSendIQ:modifiedIQ]; + + }}); + } + + if (modifiedIQ) + { + dispatch_async(xmppQueue, ^{ @autoreleasepool { + + if (state == STATE_XMPP_CONNECTED) { + [self continueSendIQ:modifiedIQ withTag:tag]; + } else { + [self failToSendIQ:modifiedIQ]; + } + }}); + } + }}); + } +} + +- (void)sendMessage:(XMPPMessage *)message withTag:(long)tag +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); + + // We're getting ready to send a message. + // Notify delegates to allow them to optionally alter/filter the outgoing message. + + SEL selector = @selector(xmppStream:willSendMessage:); + + if (![multicastDelegate hasDelegateThatRespondsToSelector:selector]) + { + // None of the delegates implement the method. + // Use a shortcut. + + [self continueSendMessage:message withTag:tag]; + } + else + { + // Notify all interested delegates. + // This must be done serially to allow them to alter the element in a thread-safe manner. + + GCDMulticastDelegateEnumerator *delegateEnumerator = [multicastDelegate delegateEnumerator]; + + dispatch_async(willSendMessageQueue, ^{ @autoreleasepool { + + // Allow delegates to modify outgoing element + + __block XMPPMessage *modifiedMessage = message; + + id del; + dispatch_queue_t dq; + + while (modifiedMessage && [delegateEnumerator getNextDelegate:&del delegateQueue:&dq forSelector:selector]) + { + #if DEBUG + { + char methodReturnType[32]; + + Method method = class_getInstanceMethod([del class], selector); + method_getReturnType(method, methodReturnType, sizeof(methodReturnType)); + + if (strcmp(methodReturnType, @encode(XMPPMessage*)) != 0) + { + NSAssert(NO, @"Method xmppStream:willSendMessage: is no longer void (see XMPPStream.h). " + @"Culprit = %@", NSStringFromClass([del class])); + } + } + #endif + + dispatch_sync(dq, ^{ @autoreleasepool { + + modifiedMessage = [del xmppStream:self willSendMessage:modifiedMessage]; + + }}); + } + + if (modifiedMessage) + { + dispatch_async(xmppQueue, ^{ @autoreleasepool { + + if (state == STATE_XMPP_CONNECTED) { + [self continueSendMessage:modifiedMessage withTag:tag]; + } + else { + [self failToSendMessage:modifiedMessage]; + } + }}); + } + }}); + } +} + +- (void)sendPresence:(XMPPPresence *)presence withTag:(long)tag +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); + + // We're getting ready to send a presence element. + // Notify delegates to allow them to optionally alter/filter the outgoing presence. + + SEL selector = @selector(xmppStream:willSendPresence:); + + if (![multicastDelegate hasDelegateThatRespondsToSelector:selector]) + { + // None of the delegates implement the method. + // Use a shortcut. + + [self continueSendPresence:presence withTag:tag]; + } + else + { + // Notify all interested delegates. + // This must be done serially to allow them to alter the element in a thread-safe manner. + + GCDMulticastDelegateEnumerator *delegateEnumerator = [multicastDelegate delegateEnumerator]; + + dispatch_async(willSendPresenceQueue, ^{ @autoreleasepool { + + // Allow delegates to modify outgoing element + + __block XMPPPresence *modifiedPresence = presence; + + id del; + dispatch_queue_t dq; + + while (modifiedPresence && [delegateEnumerator getNextDelegate:&del delegateQueue:&dq forSelector:selector]) + { + #if DEBUG + { + char methodReturnType[32]; + + Method method = class_getInstanceMethod([del class], selector); + method_getReturnType(method, methodReturnType, sizeof(methodReturnType)); + + if (strcmp(methodReturnType, @encode(XMPPPresence*)) != 0) + { + NSAssert(NO, @"Method xmppStream:willSendPresence: is no longer void (see XMPPStream.h). " + @"Culprit = %@", NSStringFromClass([del class])); + } + } + #endif + + dispatch_sync(dq, ^{ @autoreleasepool { + + modifiedPresence = [del xmppStream:self willSendPresence:modifiedPresence]; + + }}); + } + + if (modifiedPresence) + { + dispatch_async(xmppQueue, ^{ @autoreleasepool { + + if (state == STATE_XMPP_CONNECTED) { + [self continueSendPresence:modifiedPresence withTag:tag]; + } else { + [self failToSendPresence:modifiedPresence]; + } + }}); + } + }}); + } +} + +- (void)continueSendIQ:(XMPPIQ *)iq withTag:(long)tag +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); + + NSString *outgoingStr = [iq compactXMLString]; + NSData *outgoingData = [outgoingStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", outgoingStr); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:tag]; + + [multicastDelegate xmppStream:self didSendIQ:iq]; +} + +- (void)continueSendMessage:(XMPPMessage *)message withTag:(long)tag +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); + + NSString *outgoingStr = [message compactXMLString]; + NSData *outgoingData = [outgoingStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", outgoingStr); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:tag]; + + [multicastDelegate xmppStream:self didSendMessage:message]; +} + +- (void)continueSendPresence:(XMPPPresence *)presence withTag:(long)tag +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); + + NSString *outgoingStr = [presence compactXMLString]; + NSData *outgoingData = [outgoingStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", outgoingStr); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:tag]; + + // Update myPresence if this is a normal presence element. + // In other words, ignore presence subscription stuff, MUC room stuff, etc. + // + // We use the built-in [presence type] which guarantees lowercase strings, + // and will return @"available" if there was no set type (as available is implicit). + + NSString *type = [presence type]; + if ([type isEqualToString:@"available"] || [type isEqualToString:@"unavailable"]) + { + if ([presence toStr] == nil && myPresence != presence) + { + myPresence = presence; + } + } + + [multicastDelegate xmppStream:self didSendPresence:presence]; +} + +- (void)continueSendElement:(NSXMLElement *)element withTag:(long)tag +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); + + NSString *outgoingStr = [element compactXMLString]; + NSData *outgoingData = [outgoingStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", outgoingStr); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:tag]; + + if ([customElementNames countForObject:[element name]]) + { + [multicastDelegate xmppStream:self didSendCustomElement:element]; + } +} + +/** + * Private method. + * Presencts a common method for the various public sendElement methods. +**/ +- (void)sendElement:(NSXMLElement *)element withTag:(long)tag +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + + if ([element isKindOfClass:[XMPPIQ class]]) + { + [self sendIQ:(XMPPIQ *)element withTag:tag]; + } + else if ([element isKindOfClass:[XMPPMessage class]]) + { + [self sendMessage:(XMPPMessage *)element withTag:tag]; + } + else if ([element isKindOfClass:[XMPPPresence class]]) + { + [self sendPresence:(XMPPPresence *)element withTag:tag]; + } + else + { + NSString *elementName = [element name]; + + if ([elementName isEqualToString:@"iq"]) + { + [self sendIQ:[XMPPIQ iqFromElement:element] withTag:tag]; + } + else if ([elementName isEqualToString:@"message"]) + { + [self sendMessage:[XMPPMessage messageFromElement:element] withTag:tag]; + } + else if ([elementName isEqualToString:@"presence"]) + { + [self sendPresence:[XMPPPresence presenceFromElement:element] withTag:tag]; + } + else + { + [self continueSendElement:element withTag:tag]; + } + } +} + +/** + * This method handles sending an XML stanza. + * If the XMPPStream is not connected, this method does nothing. +**/ +- (void)sendElement:(NSXMLElement *)element +{ + if (element == nil) return; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (state == STATE_XMPP_CONNECTED) + { + [self sendElement:element withTag:TAG_XMPP_WRITE_STREAM]; + } + else + { + [self failToSendElement:element]; + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +/** + * This method handles sending an XML stanza. + * If the XMPPStream is not connected, this method does nothing. + * + * After the element has been successfully sent, + * the xmppStream:didSendElementWithTag: delegate method is called. +**/ +- (void)sendElement:(NSXMLElement *)element andGetReceipt:(XMPPElementReceipt **)receiptPtr +{ + if (element == nil) return; + + if (receiptPtr == nil) + { + [self sendElement:element]; + } + else + { + __block XMPPElementReceipt *receipt = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (state == STATE_XMPP_CONNECTED) + { + receipt = [[XMPPElementReceipt alloc] init]; + [receipts addObject:receipt]; + + [self sendElement:element withTag:TAG_XMPP_WRITE_RECEIPT]; + } + else + { + [self failToSendElement:element]; + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + *receiptPtr = receipt; + } +} + +- (void)failToSendElement:(NSXMLElement *)element +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + if ([element isKindOfClass:[XMPPIQ class]]) + { + [self failToSendIQ:(XMPPIQ *)element]; + } + else if ([element isKindOfClass:[XMPPMessage class]]) + { + [self failToSendMessage:(XMPPMessage *)element]; + } + else if ([element isKindOfClass:[XMPPPresence class]]) + { + [self failToSendPresence:(XMPPPresence *)element]; + } + else + { + NSString *elementName = [element name]; + + if ([elementName isEqualToString:@"iq"]) + { + [self failToSendIQ:[XMPPIQ iqFromElement:element]]; + } + else if ([elementName isEqualToString:@"message"]) + { + [self failToSendMessage:[XMPPMessage messageFromElement:element]]; + } + else if ([elementName isEqualToString:@"presence"]) + { + [self failToSendPresence:[XMPPPresence presenceFromElement:element]]; + } + } +} + +- (void)failToSendIQ:(XMPPIQ *)iq +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + NSError *error = [NSError errorWithDomain:XMPPStreamErrorDomain + code:XMPPStreamInvalidState + userInfo:nil]; + + [multicastDelegate xmppStream:self didFailToSendIQ:iq error:error]; +} + +- (void)failToSendMessage:(XMPPMessage *)message +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + NSError *error = [NSError errorWithDomain:XMPPStreamErrorDomain + code:XMPPStreamInvalidState + userInfo:nil]; + + [multicastDelegate xmppStream:self didFailToSendMessage:message error:error]; +} + +- (void)failToSendPresence:(XMPPPresence *)presence +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + NSError *error = [NSError errorWithDomain:XMPPStreamErrorDomain + code:XMPPStreamInvalidState + userInfo:nil]; + + [multicastDelegate xmppStream:self didFailToSendPresence:presence error:error]; +} + +/** + * Retrieves the current presence and resends it in once atomic operation. + * Useful for various components that need to update injected information in the presence stanza. +**/ +- (void)resendMyPresence +{ + dispatch_block_t block = ^{ @autoreleasepool { + + if (myPresence && [[myPresence type] isEqualToString:@"available"]) + { + [self sendElement:myPresence]; + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +/** + * This method is for use by xmpp authentication mechanism classes. + * They should send elements using this method instead of the public sendElement methods, + * as those methods don't send the elements while authentication is in progress. + * + * @see XMPPSASLAuthentication +**/ +- (void)sendAuthElement:(NSXMLElement *)element +{ + dispatch_block_t block = ^{ @autoreleasepool { + + if (state == STATE_XMPP_AUTH) + { + NSString *outgoingStr = [element compactXMLString]; + NSData *outgoingData = [outgoingStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", outgoingStr); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:TAG_XMPP_WRITE_STREAM]; + } + else + { + XMPPLogWarn(@"Unable to send element while not in STATE_XMPP_AUTH: %@", [element compactXMLString]); + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +/** + * This method is for use by xmpp custom binding classes. + * They should send elements using this method instead of the public sendElement methods, + * as those methods don't send the elements while authentication/binding is in progress. + * + * @see XMPPCustomBinding +**/ +- (void)sendBindElement:(NSXMLElement *)element +{ + dispatch_block_t block = ^{ @autoreleasepool { + + if (state == STATE_XMPP_BINDING) + { + NSString *outgoingStr = [element compactXMLString]; + NSData *outgoingData = [outgoingStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", outgoingStr); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:TAG_XMPP_WRITE_STREAM]; + } + else + { + XMPPLogWarn(@"Unable to send element while not in STATE_XMPP_BINDING: %@", [element compactXMLString]); + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (void)receiveIQ:(XMPPIQ *)iq +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); + + // We're getting ready to receive an IQ. + // Notify delegates to allow them to optionally alter/filter the incoming IQ element. + + SEL selector = @selector(xmppStream:willReceiveIQ:); + + if (![multicastDelegate hasDelegateThatRespondsToSelector:selector]) + { + // None of the delegates implement the method. + // Use a shortcut. + + if (willReceiveStanzaQueue) + { + // But still go through the stanzaQueue in order to guarantee in-order-delivery of all received stanzas. + + dispatch_async(willReceiveStanzaQueue, ^{ + dispatch_async(xmppQueue, ^{ @autoreleasepool { + if (state == STATE_XMPP_CONNECTED) { + [self continueReceiveIQ:iq]; + } + }}); + }); + } + else + { + [self continueReceiveIQ:iq]; + } + } + else + { + // Notify all interested delegates. + // This must be done serially to allow them to alter the element in a thread-safe manner. + + GCDMulticastDelegateEnumerator *delegateEnumerator = [multicastDelegate delegateEnumerator]; + + if (willReceiveStanzaQueue == NULL) + willReceiveStanzaQueue = dispatch_queue_create("xmpp.willReceiveStanza", DISPATCH_QUEUE_SERIAL); + + dispatch_async(willReceiveStanzaQueue, ^{ @autoreleasepool { + + // Allow delegates to modify and/or filter incoming element + + __block XMPPIQ *modifiedIQ = iq; + + id del; + dispatch_queue_t dq; + + while (modifiedIQ && [delegateEnumerator getNextDelegate:&del delegateQueue:&dq forSelector:selector]) + { + dispatch_sync(dq, ^{ @autoreleasepool { + + modifiedIQ = [del xmppStream:self willReceiveIQ:modifiedIQ]; + + }}); + } + + dispatch_async(xmppQueue, ^{ @autoreleasepool { + + if (state == STATE_XMPP_CONNECTED) + { + if (modifiedIQ) + [self continueReceiveIQ:modifiedIQ]; + else + [multicastDelegate xmppStreamDidFilterStanza:self]; + } + }}); + }}); + } +} + +- (void)receiveMessage:(XMPPMessage *)message +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); + + // We're getting ready to receive a message. + // Notify delegates to allow them to optionally alter/filter the incoming message. + + SEL selector = @selector(xmppStream:willReceiveMessage:); + + if (![multicastDelegate hasDelegateThatRespondsToSelector:selector]) + { + // None of the delegates implement the method. + // Use a shortcut. + + if (willReceiveStanzaQueue) + { + // But still go through the stanzaQueue in order to guarantee in-order-delivery of all received stanzas. + + dispatch_async(willReceiveStanzaQueue, ^{ + dispatch_async(xmppQueue, ^{ @autoreleasepool { + + if (state == STATE_XMPP_CONNECTED) { + [self continueReceiveMessage:message]; + } + }}); + }); + } + else + { + [self continueReceiveMessage:message]; + } + } + else + { + // Notify all interested delegates. + // This must be done serially to allow them to alter the element in a thread-safe manner. + + GCDMulticastDelegateEnumerator *delegateEnumerator = [multicastDelegate delegateEnumerator]; + + if (willReceiveStanzaQueue == NULL) + willReceiveStanzaQueue = dispatch_queue_create("xmpp.willReceiveStanza", DISPATCH_QUEUE_SERIAL); + + dispatch_async(willReceiveStanzaQueue, ^{ @autoreleasepool { + + // Allow delegates to modify incoming element + + __block XMPPMessage *modifiedMessage = message; + + id del; + dispatch_queue_t dq; + + while (modifiedMessage && [delegateEnumerator getNextDelegate:&del delegateQueue:&dq forSelector:selector]) + { + dispatch_sync(dq, ^{ @autoreleasepool { + + modifiedMessage = [del xmppStream:self willReceiveMessage:modifiedMessage]; + + }}); + } + + dispatch_async(xmppQueue, ^{ @autoreleasepool { + + if (state == STATE_XMPP_CONNECTED) + { + if (modifiedMessage) + [self continueReceiveMessage:modifiedMessage]; + else + [multicastDelegate xmppStreamDidFilterStanza:self]; + } + }}); + }}); + } +} + +- (void)receivePresence:(XMPPPresence *)presence +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); + + // We're getting ready to receive a presence element. + // Notify delegates to allow them to optionally alter/filter the incoming presence. + + SEL selector = @selector(xmppStream:willReceivePresence:); + + if (![multicastDelegate hasDelegateThatRespondsToSelector:selector]) + { + // None of the delegates implement the method. + // Use a shortcut. + + if (willReceiveStanzaQueue) + { + // But still go through the stanzaQueue in order to guarantee in-order-delivery of all received stanzas. + + dispatch_async(willReceiveStanzaQueue, ^{ + dispatch_async(xmppQueue, ^{ @autoreleasepool { + + if (state == STATE_XMPP_CONNECTED) { + [self continueReceivePresence:presence]; + } + }}); + }); + } + else + { + [self continueReceivePresence:presence]; + } + } + else + { + // Notify all interested delegates. + // This must be done serially to allow them to alter the element in a thread-safe manner. + + GCDMulticastDelegateEnumerator *delegateEnumerator = [multicastDelegate delegateEnumerator]; + + if (willReceiveStanzaQueue == NULL) + willReceiveStanzaQueue = dispatch_queue_create("xmpp.willReceiveStanza", DISPATCH_QUEUE_SERIAL); + + dispatch_async(willReceiveStanzaQueue, ^{ @autoreleasepool { + + // Allow delegates to modify outgoing element + + __block XMPPPresence *modifiedPresence = presence; + + id del; + dispatch_queue_t dq; + + while (modifiedPresence && [delegateEnumerator getNextDelegate:&del delegateQueue:&dq forSelector:selector]) + { + dispatch_sync(dq, ^{ @autoreleasepool { + + modifiedPresence = [del xmppStream:self willReceivePresence:modifiedPresence]; + + }}); + } + + dispatch_async(xmppQueue, ^{ @autoreleasepool { + + if (state == STATE_XMPP_CONNECTED) + { + if (modifiedPresence) + [self continueReceivePresence:presence]; + else + [multicastDelegate xmppStreamDidFilterStanza:self]; + } + }}); + }}); + } +} + +- (void)continueReceiveIQ:(XMPPIQ *)iq +{ + if ([iq requiresResponse]) + { + // As per the XMPP specificiation, if the IQ requires a response, + // and we don't have any delegates or modules that can properly respond to the IQ, + // we MUST send back and error IQ. + // + // So we notifiy all interested delegates and modules about the received IQ, + // keeping track of whether or not any of them have handled it. + + GCDMulticastDelegateEnumerator *delegateEnumerator = [multicastDelegate delegateEnumerator]; + + id del; + dispatch_queue_t dq; + + SEL selector = @selector(xmppStream:didReceiveIQ:); + + dispatch_semaphore_t delSemaphore = dispatch_semaphore_create(0); + dispatch_group_t delGroup = dispatch_group_create(); + + while ([delegateEnumerator getNextDelegate:&del delegateQueue:&dq forSelector:selector]) + { + dispatch_group_async(delGroup, dq, ^{ @autoreleasepool { + + if ([del xmppStream:self didReceiveIQ:iq]) + { + dispatch_semaphore_signal(delSemaphore); + } + + }}); + } + + dispatch_async(didReceiveIqQueue, ^{ @autoreleasepool { + + dispatch_group_wait(delGroup, DISPATCH_TIME_FOREVER); + + // Did any of the delegates handle the IQ? (handle == will response) + + BOOL handled = (dispatch_semaphore_wait(delSemaphore, DISPATCH_TIME_NOW) == 0); + + // An entity that receives an IQ request of type "get" or "set" MUST reply + // with an IQ response of type "result" or "error". + // + // The response MUST preserve the 'id' attribute of the request. + + if (!handled) + { + // Return error message: + // + // + // + // + // + // + // + + NSXMLElement *reason = [NSXMLElement elementWithName:@"feature-not-implemented" + xmlns:@"urn:ietf:params:xml:ns:xmpp-stanzas"]; + + NSXMLElement *error = [NSXMLElement elementWithName:@"error"]; + [error addAttributeWithName:@"type" stringValue:@"cancel"]; + [error addAttributeWithName:@"code" stringValue:@"501"]; + [error addChild:reason]; + + XMPPIQ *iqResponse = [XMPPIQ iqWithType:@"error" + to:[iq from] + elementID:[iq elementID] + child:error]; + + NSXMLElement *iqChild = [iq childElement]; + if (iqChild) + { + NSXMLNode *iqChildCopy = [iqChild copy]; + [iqResponse insertChild:iqChildCopy atIndex:0]; + } + + // Purposefully go through the sendElement: method + // so that it gets dispatched onto the xmppQueue, + // and so that modules may get notified of the outgoing error message. + + [self sendElement:iqResponse]; + } + + #if !OS_OBJECT_USE_OBJC + dispatch_release(delSemaphore); + dispatch_release(delGroup); + #endif + + }}); + } + else + { + // The IQ doesn't require a response. + // So we can just fire the delegate method and ignore the responses. + + [multicastDelegate xmppStream:self didReceiveIQ:iq]; + } +} + +- (void)continueReceiveMessage:(XMPPMessage *)message +{ + [multicastDelegate xmppStream:self didReceiveMessage:message]; +} + +- (void)continueReceivePresence:(XMPPPresence *)presence +{ + [multicastDelegate xmppStream:self didReceivePresence:presence]; +} + +/** + * This method allows you to inject an element into the stream as if it was received on the socket. + * This is an advanced technique, but makes for some interesting possibilities. +**/ +- (void)injectElement:(NSXMLElement *)element +{ + if (element == nil) return; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (state != STATE_XMPP_CONNECTED) + { + return_from_block; + } + + if ([element isKindOfClass:[XMPPIQ class]]) + { + [self receiveIQ:(XMPPIQ *)element]; + } + else if ([element isKindOfClass:[XMPPMessage class]]) + { + [self receiveMessage:(XMPPMessage *)element]; + } + else if ([element isKindOfClass:[XMPPPresence class]]) + { + [self receivePresence:(XMPPPresence *)element]; + } + else + { + NSString *elementName = [element name]; + + if ([elementName isEqualToString:@"iq"]) + { + [self receiveIQ:[XMPPIQ iqFromElement:element]]; + } + else if ([elementName isEqualToString:@"message"]) + { + [self receiveMessage:[XMPPMessage messageFromElement:element]]; + } + else if ([elementName isEqualToString:@"presence"]) + { + [self receivePresence:[XMPPPresence presenceFromElement:element]]; + } + else if ([customElementNames countForObject:elementName]) + { + [multicastDelegate xmppStream:self didReceiveCustomElement:element]; + } + else + { + [multicastDelegate xmppStream:self didReceiveError:element]; + } + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (void)registerCustomElementNames:(NSSet *)names +{ + dispatch_block_t block = ^{ + + if (customElementNames == nil) + customElementNames = [[NSCountedSet alloc] init]; + + for (NSString *name in names) + { + [customElementNames addObject:name]; + } + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); +} + +- (void)unregisterCustomElementNames:(NSSet *)names +{ + dispatch_block_t block = ^{ + + for (NSString *name in names) + { + [customElementNames removeObject:name]; + } + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Stream Negotiation +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method is called to start the initial negotiation process. +**/ +- (void)startNegotiation +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + NSAssert(![self didStartNegotiation], @"Invoked after initial negotiation has started"); + + XMPPLogTrace(); + + // Initialize the XML stream + [self sendOpeningNegotiation]; + + // Inform delegate that the TCP connection is open, and the stream handshake has begun + [multicastDelegate xmppStreamDidStartNegotiation:self]; + + // And start reading in the server's XML stream + [asyncSocket readDataWithTimeout:TIMEOUT_XMPP_READ_START tag:TAG_XMPP_READ_START]; +} + +/** + * This method handles sending the opening element which is needed in several situations. +**/ +- (void)sendOpeningNegotiation +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + if (![self didStartNegotiation]) + { + // TCP connection was just opened - We need to include the opening XML stanza + NSString *s1 = @""; + + NSData *outgoingData = [s1 dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", s1); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:TAG_XMPP_WRITE_START]; + + [self setDidStartNegotiation:YES]; + } + + if (parser == nil) + { + XMPPLogVerbose(@"%@: Initializing parser...", THIS_FILE); + + // Need to create the parser. + parser = [[XMPPParser alloc] initWithDelegate:self delegateQueue:xmppQueue]; + } + else + { + XMPPLogVerbose(@"%@: Resetting parser...", THIS_FILE); + + // We're restarting our negotiation, so we need to reset the parser. + parser = [[XMPPParser alloc] initWithDelegate:self delegateQueue:xmppQueue]; + } + + NSString *xmlns = @"jabber:client"; + NSString *xmlns_stream = @"http://etherx.jabber.org/streams"; + + NSString *temp, *s2; + if ([self isP2P]) + { + if (myJID_setByClient && remoteJID) + { + temp = @""; + s2 = [NSString stringWithFormat:temp, xmlns, xmlns_stream, [myJID_setByClient bare], [remoteJID bare]]; + } + else if (myJID_setByClient) + { + temp = @""; + s2 = [NSString stringWithFormat:temp, xmlns, xmlns_stream, [myJID_setByClient bare]]; + } + else if (remoteJID) + { + temp = @""; + s2 = [NSString stringWithFormat:temp, xmlns, xmlns_stream, [remoteJID bare]]; + } + else + { + temp = @""; + s2 = [NSString stringWithFormat:temp, xmlns, xmlns_stream]; + } + } + else + { + if (myJID_setByClient) + { + temp = @""; + s2 = [NSString stringWithFormat:temp, xmlns, xmlns_stream, [myJID_setByClient domain]]; + } + else if ([hostName length] > 0) + { + temp = @""; + s2 = [NSString stringWithFormat:temp, xmlns, xmlns_stream, hostName]; + } + else + { + temp = @""; + s2 = [NSString stringWithFormat:temp, xmlns, xmlns_stream]; + } + } + + NSData *outgoingData = [s2 dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", s2); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:TAG_XMPP_WRITE_START]; + + // Update status + state = STATE_XMPP_OPENING; +} + +/** + * This method handles starting TLS negotiation on the socket, using the proper settings. +**/ +- (void)startTLS +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + // Update state (part 2 - prompting delegates) + state = STATE_XMPP_STARTTLS_2; + + // Create a mutable dictionary for security settings + NSMutableDictionary *settings = [NSMutableDictionary dictionaryWithCapacity:5]; + + SEL selector = @selector(xmppStream:willSecureWithSettings:); + + if (![multicastDelegate hasDelegateThatRespondsToSelector:selector]) + { + // None of the delegates implement the method. + // Use a shortcut. + + [self continueStartTLS:settings]; + } + else + { + // Query all interested delegates. + // This must be done serially to maintain thread safety. + + GCDMulticastDelegateEnumerator *delegateEnumerator = [multicastDelegate delegateEnumerator]; + + dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(concurrentQueue, ^{ @autoreleasepool { + + // Prompt the delegate(s) to populate the security settings + + id delegate; + dispatch_queue_t delegateQueue; + + while ([delegateEnumerator getNextDelegate:&delegate delegateQueue:&delegateQueue forSelector:selector]) + { + dispatch_sync(delegateQueue, ^{ @autoreleasepool { + + [delegate xmppStream:self willSecureWithSettings:settings]; + + }}); + } + + dispatch_async(xmppQueue, ^{ @autoreleasepool { + + [self continueStartTLS:settings]; + + }}); + + }}); + } +} + +- (void)continueStartTLS:(NSMutableDictionary *)settings +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace2(@"%@: %@ %@", THIS_FILE, THIS_METHOD, settings); + + if (state == STATE_XMPP_STARTTLS_2) + { + // If the delegates didn't respond + if ([settings count] == 0) + { + // Use the default settings, and set the peer name + + NSString *expectedCertName = hostName; + if (expectedCertName == nil) + { + expectedCertName = [myJID_setByClient domain]; + } + + if ([expectedCertName length] > 0) + { + settings[(NSString *) kCFStreamSSLPeerName] = expectedCertName; + } + } + + [asyncSocket startTLS:settings]; + [self setIsSecure:YES]; + + // Note: We don't need to wait for asyncSocket to complete TLS negotiation. + // We can just continue reading/writing to the socket, and it will handle queueing everything for us! + + if ([self didStartNegotiation]) + { + // Now we start our negotiation over again... + [self sendOpeningNegotiation]; + + // We paused reading from the socket. + // We're ready to continue now. + [asyncSocket readDataWithTimeout:TIMEOUT_XMPP_READ_STREAM tag:TAG_XMPP_READ_STREAM]; + } + else + { + // First time starting negotiation + [self startNegotiation]; + } + } +} + +/** + * This method is called anytime we receive the server's stream features. + * This method looks at the stream features, and handles any requirements so communication can continue. +**/ +- (void)handleStreamFeatures +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + // Extract the stream features + NSXMLElement *features = [rootElement elementForName:@"stream:features"]; + + // Check to see if TLS is required + // Don't forget about that NSXMLElement bug you reported to apple (xmlns is required or element won't be found) + NSXMLElement *f_starttls = [features elementForName:@"starttls" xmlns:@"urn:ietf:params:xml:ns:xmpp-tls"]; + + if (f_starttls) + { + if ([f_starttls elementForName:@"required"] || [self startTLSPolicy] >= XMPPStreamStartTLSPolicyPreferred) + { + // TLS is required for this connection + + // Update state + state = STATE_XMPP_STARTTLS_1; + + // Send the startTLS XML request + [self sendStartTLSRequest]; + + // We do not mark the stream as secure yet. + // We're waiting to receive the response from the + // server before we actually start the TLS handshake. + + // We're already listening for the response... + return; + } + } + else if (![self isSecure] && [self startTLSPolicy] == XMPPStreamStartTLSPolicyRequired) + { + // We must abort the connection as the server doesn't support our requirements. + + NSString *errMsg = @"The server does not support startTLS. And the startTLSPolicy is Required."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + otherError = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamUnsupportedAction userInfo:info]; + + // Close the TCP connection. + [self disconnect]; + + // The socketDidDisconnect:withError: method will handle everything else + return; + } + + // Check to see if resource binding is required + // Don't forget about that NSXMLElement bug you reported to apple (xmlns is required or element won't be found) + NSXMLElement *f_bind = [features elementForName:@"bind" xmlns:@"urn:ietf:params:xml:ns:xmpp-bind"]; + + if (f_bind) + { + // Start the binding process + [self startBinding]; + + // We're already listening for the response... + return; + } + + // It looks like all has gone well, and the connection should be ready to use now + state = STATE_XMPP_CONNECTED; + + if (![self isAuthenticated]) + { + [self setupKeepAliveTimer]; + + // Notify delegates + [multicastDelegate xmppStreamDidConnect:self]; + } +} + +- (void)handleStartTLSResponse:(NSXMLElement *)response +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + // We're expecting a proceed response + // If we get anything else we can safely assume it's the equivalent of a failure response + if ( ![[response name] isEqualToString:@"proceed"]) + { + // We can close our TCP connection now + [self disconnect]; + + // The socketDidDisconnect:withError: method will handle everything else + return; + } + + // Start TLS negotiation + [self startTLS]; +} + +/** + * After the registerUser:withPassword: method is invoked, a registration message is sent to the server. + * We're waiting for the result from this registration request. +**/ +- (void)handleRegistration:(NSXMLElement *)response +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + if ([[response attributeStringValueForName:@"type"] isEqualToString:@"error"]) + { + // Revert back to connected state (from authenticating state) + state = STATE_XMPP_CONNECTED; + + [multicastDelegate xmppStream:self didNotRegister:response]; + } + else + { + // Revert back to connected state (from authenticating state) + state = STATE_XMPP_CONNECTED; + + [multicastDelegate xmppStreamDidRegister:self]; + } +} + +/** + * After the authenticate:error: or authenticateWithPassword:error: methods are invoked, some kind of + * authentication message is sent to the server. + * This method forwards the response to the authentication module, and handles the resulting authentication state. +**/ +- (void)handleAuth:(NSXMLElement *)authResponse +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + XMPPHandleAuthResponse result = [auth handleAuth:authResponse]; + + if (result == XMPP_AUTH_SUCCESS) + { + // We are successfully authenticated (via sasl:digest-md5) + [self setIsAuthenticated:YES]; + + BOOL shouldRenegotiate = YES; + if ([auth respondsToSelector:@selector(shouldResendOpeningNegotiationAfterSuccessfulAuthentication)]) + { + shouldRenegotiate = [auth shouldResendOpeningNegotiationAfterSuccessfulAuthentication]; + } + + if (shouldRenegotiate) + { + // Now we start our negotiation over again... + [self sendOpeningNegotiation]; + + if (![self isSecure]) + { + // Normally we requeue our read operation in xmppParserDidParseData:. + // But we just reset the parser, so that code path isn't going to happen. + // So start read request here. + // The state is STATE_XMPP_OPENING, set via sendOpeningNegotiation method. + + [asyncSocket readDataWithTimeout:TIMEOUT_XMPP_READ_START tag:TAG_XMPP_READ_START]; + } + } + else + { + // Revert back to connected state (from authenticating state) + state = STATE_XMPP_CONNECTED; + + [multicastDelegate xmppStreamDidAuthenticate:self]; + } + + // Done with auth + auth = nil; + + } + else if (result == XMPP_AUTH_FAIL) + { + // Revert back to connected state (from authenticating state) + state = STATE_XMPP_CONNECTED; + + // Notify delegate + [multicastDelegate xmppStream:self didNotAuthenticate:authResponse]; + + // Done with auth + auth = nil; + + } + else if (result == XMPP_AUTH_CONTINUE) + { + // Authentication continues. + // State doesn't change. + } + else + { + XMPPLogError(@"Authentication class (%@) returned invalid response code (%i)", + NSStringFromClass([auth class]), (int)result); + + NSAssert(NO, @"Authentication class (%@) returned invalid response code (%i)", + NSStringFromClass([auth class]), (int)result); + } +} + +- (void)startBinding +{ + XMPPLogTrace(); + + state = STATE_XMPP_BINDING; + + SEL selector = @selector(xmppStreamWillBind:); + + if (![multicastDelegate hasDelegateThatRespondsToSelector:selector]) + { + [self startStandardBinding]; + } + else + { + GCDMulticastDelegateEnumerator *delegateEnumerator = [multicastDelegate delegateEnumerator]; + + dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(concurrentQueue, ^{ @autoreleasepool { + + __block id delegateCustomBinding = nil; + + id delegate; + dispatch_queue_t dq; + + while ([delegateEnumerator getNextDelegate:&delegate delegateQueue:&dq forSelector:selector]) + { + dispatch_sync(dq, ^{ @autoreleasepool { + + delegateCustomBinding = [delegate xmppStreamWillBind:self]; + }}); + + if (delegateCustomBinding) { + break; + } + } + + dispatch_async(xmppQueue, ^{ @autoreleasepool { + + if (delegateCustomBinding) + [self startCustomBinding:delegateCustomBinding]; + else + [self startStandardBinding]; + }}); + }}); + } +} + +- (void)startCustomBinding:(id )delegateCustomBinding +{ + XMPPLogTrace(); + + customBinding = delegateCustomBinding; + + NSError *bindError = nil; + XMPPBindResult result = [customBinding start:&bindError]; + + if (result == XMPP_BIND_CONTINUE) + { + // Expected result + // Wait for reply from server, and forward to customBinding module. + } + else + { + if (result == XMPP_BIND_SUCCESS) + { + // It appears binding isn't needed (perhaps handled via auth) + + BOOL skipStartSessionOverride = NO; + if ([customBinding respondsToSelector:@selector(shouldSkipStartSessionAfterSuccessfulBinding)]) { + skipStartSessionOverride = [customBinding shouldSkipStartSessionAfterSuccessfulBinding]; + } + + [self continuePostBinding:skipStartSessionOverride]; + } + else if (result == XMPP_BIND_FAIL_FALLBACK) + { + // Custom binding isn't available for whatever reason, + // but the module has requested we fallback to standard binding. + + [self startStandardBinding]; + } + else if (result == XMPP_BIND_FAIL_ABORT) + { + // Custom binding failed, + // and the module requested we abort. + + otherError = bindError; + [asyncSocket disconnect]; + } + + customBinding = nil; + } +} + +- (void)handleCustomBinding:(NSXMLElement *)response +{ + XMPPLogTrace(); + + NSError *bindError = nil; + XMPPBindResult result = [customBinding handleBind:response withError:&bindError]; + + if (result == XMPP_BIND_CONTINUE) + { + // Binding still in progress + } + else + { + if (result == XMPP_BIND_SUCCESS) + { + // Binding complete. Continue. + + BOOL skipStartSessionOverride = NO; + if ([customBinding respondsToSelector:@selector(shouldSkipStartSessionAfterSuccessfulBinding)]) { + skipStartSessionOverride = [customBinding shouldSkipStartSessionAfterSuccessfulBinding]; + } + + [self continuePostBinding:skipStartSessionOverride]; + } + else if (result == XMPP_BIND_FAIL_FALLBACK) + { + // Custom binding failed for whatever reason, + // but the module has requested we fallback to standard binding. + + [self startStandardBinding]; + } + else if (result == XMPP_BIND_FAIL_ABORT) + { + // Custom binding failed, + // and the module requested we abort. + + otherError = bindError; + [asyncSocket disconnect]; + } + + customBinding = nil; + } +} + +- (void)startStandardBinding +{ + XMPPLogTrace(); + + NSString *requestedResource = [myJID_setByClient resource]; + + if ([requestedResource length] > 0) + { + // Ask the server to bind the user specified resource + + NSXMLElement *resource = [NSXMLElement elementWithName:@"resource"]; + [resource setStringValue:requestedResource]; + + NSXMLElement *bind = [NSXMLElement elementWithName:@"bind" xmlns:@"urn:ietf:params:xml:ns:xmpp-bind"]; + [bind addChild:resource]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" elementID:[self generateUUID]]; + [iq addChild:bind]; + + NSString *outgoingStr = [iq compactXMLString]; + NSData *outgoingData = [outgoingStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", outgoingStr); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:TAG_XMPP_WRITE_STREAM]; + + [idTracker addElement:iq + target:nil + selector:NULL + timeout:XMPPIDTrackerTimeoutNone]; + } + else + { + // The user didn't specify a resource, so we ask the server to bind one for us + + NSXMLElement *bind = [NSXMLElement elementWithName:@"bind" xmlns:@"urn:ietf:params:xml:ns:xmpp-bind"]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" elementID:[self generateUUID]]; + [iq addChild:bind]; + + NSString *outgoingStr = [iq compactXMLString]; + NSData *outgoingData = [outgoingStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", outgoingStr); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:TAG_XMPP_WRITE_STREAM]; + + [idTracker addElement:iq + target:nil + selector:NULL + timeout:XMPPIDTrackerTimeoutNone]; + } +} + +- (void)handleStandardBinding:(NSXMLElement *)response +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + NSXMLElement *r_bind = [response elementForName:@"bind" xmlns:@"urn:ietf:params:xml:ns:xmpp-bind"]; + NSXMLElement *r_jid = [r_bind elementForName:@"jid"]; + + if (r_jid) + { + // We're properly binded to a resource now + // Extract and save our resource (it may not be what we originally requested) + NSString *fullJIDStr = [r_jid stringValue]; + + [self setMyJID_setByServer:[XMPPJID jidWithString:fullJIDStr]]; + + // On to the next step + BOOL skipStartSessionOverride = NO; + [self continuePostBinding:skipStartSessionOverride]; + } + else + { + // It appears the server didn't allow our resource choice + // First check if we want to try an alternative resource + + NSXMLElement *r_error = [response elementForName:@"error"]; + NSXMLElement *r_conflict = [r_error elementForName:@"conflict" xmlns:@"urn:ietf:params:xml:ns:xmpp-stanzas"]; + + if (r_conflict) + { + SEL selector = @selector(xmppStream:alternativeResourceForConflictingResource:); + + if (![multicastDelegate hasDelegateThatRespondsToSelector:selector]) + { + // None of the delegates implement the method. + // Use a shortcut. + + [self continueHandleStandardBinding:nil]; + } + else + { + // Query all interested delegates. + // This must be done serially to maintain thread safety. + + GCDMulticastDelegateEnumerator *delegateEnumerator = [multicastDelegate delegateEnumerator]; + + dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(concurrentQueue, ^{ @autoreleasepool { + + // Query delegates for alternative resource + + NSString *currentResource = [[self myJID] resource]; + __block NSString *alternativeResource = nil; + + id delegate; + dispatch_queue_t dq; + + while ([delegateEnumerator getNextDelegate:&delegate delegateQueue:&dq forSelector:selector]) + { + dispatch_sync(dq, ^{ @autoreleasepool { + + NSString *delegateAlternativeResource = + [delegate xmppStream:self alternativeResourceForConflictingResource:currentResource]; + + if (delegateAlternativeResource) + { + alternativeResource = delegateAlternativeResource; + } + }}); + + if (alternativeResource) { + break; + } + } + + dispatch_async(xmppQueue, ^{ @autoreleasepool { + + [self continueHandleStandardBinding:alternativeResource]; + + }}); + + }}); + } + } + else + { + // Appears to be a conflicting resource, but server didn't specify conflict + [self continueHandleStandardBinding:nil]; + } + } +} + +- (void)continueHandleStandardBinding:(NSString *)alternativeResource +{ + XMPPLogTrace(); + + if ([alternativeResource length] > 0) + { + // Update myJID + + [self setMyJID_setByClient:[myJID_setByClient jidWithNewResource:alternativeResource]]; + + NSXMLElement *resource = [NSXMLElement elementWithName:@"resource"]; + [resource setStringValue:alternativeResource]; + + NSXMLElement *bind = [NSXMLElement elementWithName:@"bind" xmlns:@"urn:ietf:params:xml:ns:xmpp-bind"]; + [bind addChild:resource]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set"]; + [iq addChild:bind]; + + NSString *outgoingStr = [iq compactXMLString]; + NSData *outgoingData = [outgoingStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", outgoingStr); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:TAG_XMPP_WRITE_STREAM]; + + [idTracker addElement:iq + target:nil + selector:NULL + timeout:XMPPIDTrackerTimeoutNone]; + + // The state remains in STATE_XMPP_BINDING + } + else + { + // We'll simply let the server choose then + + NSXMLElement *bind = [NSXMLElement elementWithName:@"bind" xmlns:@"urn:ietf:params:xml:ns:xmpp-bind"]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set"]; + [iq addChild:bind]; + + NSString *outgoingStr = [iq compactXMLString]; + NSData *outgoingData = [outgoingStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", outgoingStr); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:TAG_XMPP_WRITE_STREAM]; + + [idTracker addElement:iq + target:nil + selector:NULL + timeout:XMPPIDTrackerTimeoutNone]; + + // The state remains in STATE_XMPP_BINDING + } +} + +- (void)continuePostBinding:(BOOL)skipStartSessionOverride +{ + XMPPLogTrace(); + + // And we may now have to do one last thing before we're ready - start an IM session + NSXMLElement *features = [rootElement elementForName:@"stream:features"]; + + // Check to see if a session is required + // Don't forget about that NSXMLElement bug you reported to apple (xmlns is required or element won't be found) + NSXMLElement *f_session = [features elementForName:@"session" xmlns:@"urn:ietf:params:xml:ns:xmpp-session"]; + + if (f_session && !skipStartSession && !skipStartSessionOverride) + { + NSXMLElement *session = [NSXMLElement elementWithName:@"session"]; + [session setXmlns:@"urn:ietf:params:xml:ns:xmpp-session"]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" elementID:[self generateUUID]]; + [iq addChild:session]; + + NSString *outgoingStr = [iq compactXMLString]; + NSData *outgoingData = [outgoingStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", outgoingStr); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:TAG_XMPP_WRITE_STREAM]; + + [idTracker addElement:iq + target:nil + selector:NULL + timeout:XMPPIDTrackerTimeoutNone]; + + // Update state + state = STATE_XMPP_START_SESSION; + } + else + { + // Revert back to connected state (from binding state) + state = STATE_XMPP_CONNECTED; + + [multicastDelegate xmppStreamDidAuthenticate:self]; + } +} + +- (void)handleStartSessionResponse:(NSXMLElement *)response +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + if ([[response attributeStringValueForName:@"type"] isEqualToString:@"result"]) + { + // Revert back to connected state (from start session state) + state = STATE_XMPP_CONNECTED; + + [multicastDelegate xmppStreamDidAuthenticate:self]; + } + else + { + // Revert back to connected state (from start session state) + state = STATE_XMPP_CONNECTED; + + [multicastDelegate xmppStream:self didNotAuthenticate:response]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPSRVResolver Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)tryNextSrvResult +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + NSError *connectError = nil; + BOOL success = NO; + + while (srvResultsIndex < [srvResults count]) + { + XMPPSRVRecord *srvRecord = srvResults[srvResultsIndex]; + NSString *srvHost = srvRecord.target; + UInt16 srvPort = srvRecord.port; + + success = [self connectToHost:srvHost onPort:srvPort withTimeout:XMPPStreamTimeoutNone error:&connectError]; + + if (success) + { + break; + } + else + { + srvResultsIndex++; + } + } + + if (!success) + { + // SRV resolution of the JID domain failed. + // As per the RFC: + // + // "If the SRV lookup fails, the fallback is a normal IPv4/IPv6 address record resolution + // to determine the IP address, using the "xmpp-client" port 5222, registered with the IANA." + // + // In other words, just try connecting to the domain specified in the JID. + + success = [self connectToHost:[myJID_setByClient domain] onPort:5222 withTimeout:XMPPStreamTimeoutNone error:&connectError]; + } + + if (!success) + { + [self endConnectTimeout]; + + state = STATE_XMPP_DISCONNECTED; + + [multicastDelegate xmppStreamDidDisconnect:self withError:connectError]; + } +} + +- (void)xmppSRVResolver:(XMPPSRVResolver *)sender didResolveRecords:(NSArray *)records +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + if (sender != srvResolver) return; + + XMPPLogTrace(); + + srvResults = [records copy]; + srvResultsIndex = 0; + + state = STATE_XMPP_CONNECTING; + + [self tryNextSrvResult]; +} + +- (void)xmppSRVResolver:(XMPPSRVResolver *)sender didNotResolveDueToError:(NSError *)error +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + if (sender != srvResolver) return; + + XMPPLogTrace(); + + state = STATE_XMPP_CONNECTING; + + [self tryNextSrvResult]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark AsyncSocket Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Called when a socket connects and is ready for reading and writing. "host" will be an IP address, not a DNS name. +**/ +- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port +{ + // This method is invoked on the xmppQueue. + // + // The TCP connection is now established. + + XMPPLogTrace(); + + [self endConnectTimeout]; + + #if TARGET_OS_IPHONE + { + if (self.enableBackgroundingOnSocket) + { + __block BOOL result; + + [asyncSocket performBlock:^{ + result = [asyncSocket enableBackgroundingOnSocket]; + }]; + + if (result) + XMPPLogVerbose(@"%@: Enabled backgrounding on socket", THIS_FILE); + else + XMPPLogError(@"%@: Error enabling backgrounding on socket!", THIS_FILE); + } + } + #endif + + [multicastDelegate xmppStream:self socketDidConnect:sock]; + + srvResolver = nil; + srvResults = nil; + + // Are we using old-style SSL? (Not the upgrade to TLS technique specified in the XMPP RFC) + if ([self isSecure]) + { + // The connection must be secured immediately (just like with HTTPS) + [self startTLS]; + } + else + { + [self startNegotiation]; + } +} + +- (void)socket:(GCDAsyncSocket *)sock didReceiveTrust:(SecTrustRef)trust + completionHandler:(void (^)(BOOL shouldTrustPeer))completionHandler +{ + XMPPLogTrace(); + + SEL selector = @selector(xmppStream:didReceiveTrust:completionHandler:); + + if ([multicastDelegate hasDelegateThatRespondsToSelector:selector]) + { + [multicastDelegate xmppStream:self didReceiveTrust:trust completionHandler:completionHandler]; + } + else + { + XMPPLogWarn(@"%@: Stream secured with (GCDAsyncSocketManuallyEvaluateTrust == YES)," + @" but there are no delegates that implement xmppStream:didReceiveTrust:completionHandler:." + @" This is likely a mistake.", THIS_FILE); + + // The delegate method should likely have code similar to this, + // but will presumably perform some extra security code stuff. + // For example, allowing a specific self-signed certificate that is known to the app. + + dispatch_queue_t bgQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(bgQueue, ^{ + + SecTrustResultType result = kSecTrustResultDeny; + OSStatus status = SecTrustEvaluate(trust, &result); + + if (status == noErr && (result == kSecTrustResultProceed || result == kSecTrustResultUnspecified)) { + completionHandler(YES); + } + else { + completionHandler(NO); + } + }); + } +} + +- (void)socketDidSecure:(GCDAsyncSocket *)sock +{ + // This method is invoked on the xmppQueue. + + XMPPLogTrace(); + + [multicastDelegate xmppStreamDidSecure:self]; +} + +/** + * Called when a socket has completed reading the requested data. Not called if there is an error. +**/ +- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag +{ + // This method is invoked on the xmppQueue. + + XMPPLogTrace(); + + lastSendReceiveTime = [NSDate timeIntervalSinceReferenceDate]; + numberOfBytesReceived += [data length]; + + XMPPLogRecvPre(@"RECV: %@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); + + // Asynchronously parse the xml data + [parser parseData:data]; + + if ([self isSecure]) + { + // Continue reading for XML elements + if (state == STATE_XMPP_OPENING) + { + [asyncSocket readDataWithTimeout:TIMEOUT_XMPP_READ_START tag:TAG_XMPP_READ_START]; + } + else + { + [asyncSocket readDataWithTimeout:TIMEOUT_XMPP_READ_STREAM tag:TAG_XMPP_READ_STREAM]; + } + } + else + { + // Don't queue up a read on the socket as we may need to upgrade to TLS. + // We'll read more data after we've parsed the current chunk of data. + } +} + +/** + * Called after data with the given tag has been successfully sent. +**/ +- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag +{ + // This method is invoked on the xmppQueue. + + XMPPLogTrace(); + + lastSendReceiveTime = [NSDate timeIntervalSinceReferenceDate]; + + if (tag == TAG_XMPP_WRITE_RECEIPT) + { + if ([receipts count] == 0) + { + XMPPLogWarn(@"%@: Found TAG_XMPP_WRITE_RECEIPT with no pending receipts!", THIS_FILE); + return; + } + + XMPPElementReceipt *receipt = receipts[0]; + [receipt signalSuccess]; + [receipts removeObjectAtIndex:0]; + } + else if (tag == TAG_XMPP_WRITE_STOP) + { + [multicastDelegate xmppStreamDidSendClosingStreamStanza:self]; + } +} + +/** + * Called when a socket disconnects with or without error. +**/ +- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err +{ + // This method is invoked on the xmppQueue. + + XMPPLogTrace(); + + [self endConnectTimeout]; + + if (srvResults && (++srvResultsIndex < [srvResults count])) + { + [self tryNextSrvResult]; + } + else + { + // Update state + state = STATE_XMPP_DISCONNECTED; + + // Release the parser (to free underlying resources) + [parser setDelegate:nil delegateQueue:NULL]; + parser = nil; + + // Clear any saved authentication information + auth = nil; + + authenticationDate = nil; + + // Clear stored elements + myJID_setByServer = nil; + myPresence = nil; + rootElement = nil; + + // Stop the keep alive timer + if (keepAliveTimer) + { + dispatch_source_cancel(keepAliveTimer); + keepAliveTimer = NULL; + } + + // Clear srv results + srvResolver = nil; + srvResults = nil; + + // Stop tracking IDs + [idTracker removeAllIDs]; + + // Clear any pending receipts + for (XMPPElementReceipt *receipt in receipts) + { + [receipt signalFailure]; + } + [receipts removeAllObjects]; + + // Clear flags + flags = 0; + + // Notify delegate + + if (parserError || otherError) + { + NSError *error = parserError ? : otherError; + + [multicastDelegate xmppStreamDidDisconnect:self withError:error]; + + parserError = nil; + otherError = nil; + } + else + { + [multicastDelegate xmppStreamDidDisconnect:self withError:err]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPParser Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Called when the xmpp parser has read in the entire root element. +**/ +- (void)xmppParser:(XMPPParser *)sender didReadRoot:(NSXMLElement *)root +{ + // This method is invoked on the xmppQueue. + + if (sender != parser) return; + + XMPPLogTrace(); + XMPPLogRecvPost(@"RECV: %@", [root compactXMLString]); + + // At this point we've sent our XML stream header, and we've received the response XML stream header. + // We save the root element of our stream for future reference. + + rootElement = root; + + if ([self isP2P]) + { + // XEP-0174 specifies that SHOULD be sent by the receiver. + // In other words, if we're the recipient we will now send our features. + // But if we're the initiator, we can't depend on receiving their features. + + // Either way, we're connected at this point. + state = STATE_XMPP_CONNECTED; + + if ([self isP2PRecipient]) + { + // Extract the remoteJID: + // + // + + NSString *from = [[rootElement attributeForName:@"from"] stringValue]; + remoteJID = [XMPPJID jidWithString:from]; + + // Send our stream features. + // To do so we need to ask the delegate to fill it out for us. + + NSXMLElement *streamFeatures = [NSXMLElement elementWithName:@"stream:features"]; + + [multicastDelegate xmppStream:self willSendP2PFeatures:streamFeatures]; + + NSString *outgoingStr = [streamFeatures compactXMLString]; + NSData *outgoingData = [outgoingStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", outgoingStr); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:TAG_XMPP_WRITE_STREAM]; + } + + // Make sure the delegate didn't disconnect us in the xmppStream:willSendP2PFeatures: method. + + if ([self isConnected]) + { + [multicastDelegate xmppStreamDidConnect:self]; + } + } + else + { + // Check for RFC compliance + if ([self serverXmppStreamVersionNumber] >= 1.0) + { + // Update state - we're now onto stream negotiations + state = STATE_XMPP_NEGOTIATING; + + // Note: We're waiting for the now + } + else + { + // The server isn't RFC comliant, and won't be sending any stream features. + + // We would still like to know what authentication features it supports though, + // so we'll use the jabber:iq:auth namespace, which was used prior to the RFC spec. + + // Update state - we're onto psuedo negotiation + state = STATE_XMPP_NEGOTIATING; + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:auth"]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"get" elementID:[self generateUUID]]; + [iq addChild:query]; + + NSString *outgoingStr = [iq compactXMLString]; + NSData *outgoingData = [outgoingStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", outgoingStr); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:TAG_XMPP_WRITE_STREAM]; + + // Now wait for the response IQ + } + } +} + +- (void)xmppParser:(XMPPParser *)sender didReadElement:(NSXMLElement *)element +{ + // This method is invoked on the xmppQueue. + + if (sender != parser) return; + + XMPPLogTrace(); + XMPPLogRecvPost(@"RECV: %@", [element compactXMLString]); + + NSString *elementName = [element name]; + + if ([elementName isEqualToString:@"stream:error"] || [elementName isEqualToString:@"error"]) + { + [multicastDelegate xmppStream:self didReceiveError:element]; + + return; + } + + if (state == STATE_XMPP_NEGOTIATING) + { + // We've just read in the stream features + // We consider this part of the root element, so we'll add it (replacing any previously sent features) + [rootElement setChildren:@[element]]; + + // Call a method to handle any requirements set forth in the features + [self handleStreamFeatures]; + } + else if (state == STATE_XMPP_STARTTLS_1) + { + // The response from our starttls message + [self handleStartTLSResponse:element]; + } + else if (state == STATE_XMPP_REGISTERING) + { + // The iq response from our registration request + [self handleRegistration:element]; + } + else if (state == STATE_XMPP_AUTH) + { + // Some response to the authentication process + [self handleAuth:element]; + } + else if (state == STATE_XMPP_BINDING) + { + if (customBinding) + { + [self handleCustomBinding:element]; + } + else + { + BOOL invalid = NO; + if (validatesResponses) + { + XMPPIQ *iq = [XMPPIQ iqFromElement:element]; + if (![idTracker invokeForElement:iq withObject:nil]) + { + invalid = YES; + } + } + if (!invalid) + { + // The response from our binding request + [self handleStandardBinding:element]; + } + } + } + else if (state == STATE_XMPP_START_SESSION) + { + BOOL invalid = NO; + if (validatesResponses) + { + XMPPIQ *iq = [XMPPIQ iqFromElement:element]; + if (![idTracker invokeForElement:iq withObject:nil]) + { + invalid = YES; + } + } + if (!invalid) + { + // The response from our start session request + [self handleStartSessionResponse:element]; + } + } + else + { + if ([elementName isEqualToString:@"iq"]) + { + [self receiveIQ:[XMPPIQ iqFromElement:element]]; + } + else if ([elementName isEqualToString:@"message"]) + { + [self receiveMessage:[XMPPMessage messageFromElement:element]]; + } + else if ([elementName isEqualToString:@"presence"]) + { + [self receivePresence:[XMPPPresence presenceFromElement:element]]; + } + else if ([self isP2P] && + ([elementName isEqualToString:@"stream:features"] || [elementName isEqualToString:@"features"])) + { + [multicastDelegate xmppStream:self didReceiveP2PFeatures:element]; + } + else if ([customElementNames countForObject:elementName]) + { + [multicastDelegate xmppStream:self didReceiveCustomElement:element]; + } + else + { + [multicastDelegate xmppStream:self didReceiveError:element]; + } + } +} + +- (void)xmppParserDidParseData:(XMPPParser *)sender +{ + // This method is invoked on the xmppQueue. + + if (sender != parser) return; + + XMPPLogTrace(); + + if (![self isSecure]) + { + // Continue reading for XML elements + if (state == STATE_XMPP_OPENING) + { + [asyncSocket readDataWithTimeout:TIMEOUT_XMPP_READ_START tag:TAG_XMPP_READ_START]; + } + else if (state != STATE_XMPP_STARTTLS_2) // Don't queue read operation prior to [asyncSocket startTLS:] + { + [asyncSocket readDataWithTimeout:TIMEOUT_XMPP_READ_STREAM tag:TAG_XMPP_READ_STREAM]; + } + } +} + +- (void)xmppParserDidEnd:(XMPPParser *)sender +{ + // This method is invoked on the xmppQueue. + + if (sender != parser) return; + + XMPPLogTrace(); + + [asyncSocket disconnect]; +} + +- (void)xmppParser:(XMPPParser *)sender didFail:(NSError *)error +{ + // This method is invoked on the xmppQueue. + + if (sender != parser) return; + + XMPPLogTrace(); + + parserError = error; + [asyncSocket disconnect]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Keep Alive +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)setupKeepAliveTimer +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + if (keepAliveTimer) + { + dispatch_source_cancel(keepAliveTimer); + keepAliveTimer = NULL; + } + + if (state == STATE_XMPP_CONNECTED) + { + if (keepAliveInterval > 0) + { + keepAliveTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, xmppQueue); + + dispatch_source_set_event_handler(keepAliveTimer, ^{ @autoreleasepool { + + [self keepAlive]; + }}); + + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theKeepAliveTimer = keepAliveTimer; + + dispatch_source_set_cancel_handler(keepAliveTimer, ^{ + XMPPLogVerbose(@"dispatch_release(keepAliveTimer)"); + dispatch_release(theKeepAliveTimer); + }); + #endif + + // Everytime we send or receive data, we update our lastSendReceiveTime. + // We set our timer to fire several times per keepAliveInterval. + // This allows us to maintain a single timer, + // and an acceptable timer resolution (assuming larger keepAliveIntervals). + + uint64_t interval = ((keepAliveInterval / 4.0) * NSEC_PER_SEC); + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, interval); + + dispatch_source_set_timer(keepAliveTimer, tt, interval, 1.0); + dispatch_resume(keepAliveTimer); + } + } +} + +- (void)keepAlive +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + if (state == STATE_XMPP_CONNECTED) + { + NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate]; + NSTimeInterval elapsed = (now - lastSendReceiveTime); + + if (elapsed < 0 || elapsed >= keepAliveInterval) + { + numberOfBytesSent += [keepAliveData length]; + + [asyncSocket writeData:keepAliveData + withTimeout:TIMEOUT_XMPP_WRITE + tag:TAG_XMPP_WRITE_STREAM]; + + // Force update the lastSendReceiveTime here just to be safe. + // + // In case the TCP socket comes to a crawl with a giant element in the queue, + // which would prevent the socket:didWriteDataWithTag: method from being called for some time. + + lastSendReceiveTime = [NSDate timeIntervalSinceReferenceDate]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Stanza Validation +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)isValidResponseElementFrom:(XMPPJID *)from forRequestElementTo:(XMPPJID *)to +{ + BOOL valid = YES; + + if(to) + { + if(![to isEqualToJID:from]) + { + valid = NO; + } + } +/** + * Replies for Stanza's that had no TO will be accepted if the FROM is: + * + * No from. + * from = the bare account JID. + * from = the full account JID (legal in 3920, but not 6120). + * from = the server's domain. +**/ + else if(!to && from) + { + if(![from isEqualToJID:self.myJID options:XMPPJIDCompareBare] + && ![from isEqualToJID:self.myJID options:XMPPJIDCompareFull] + && ![from isEqualToJID:[self.myJID domainJID] options:XMPPJIDCompareFull]) + { + valid = NO; + } + } + + return valid; +} + +- (BOOL)isValidResponseElement:(XMPPElement *)response forRequestElement:(XMPPElement *)request +{ + if(!response || !request) return NO; + + return [self isValidResponseElementFrom:[response from] forRequestElementTo:[request to]]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Module Plug-In System +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)registerModule:(XMPPModule *)module +{ + if (module == nil) return; + + // Asynchronous operation + + dispatch_block_t block = ^{ @autoreleasepool { + + // Register module + + [registeredModules addObject:module]; + + // Add auto delegates (if there are any) + + NSString *className = NSStringFromClass([module class]); + GCDMulticastDelegate *autoDelegates = autoDelegateDict[className]; + + GCDMulticastDelegateEnumerator *autoDelegatesEnumerator = [autoDelegates delegateEnumerator]; + id delegate; + dispatch_queue_t delegateQueue; + + while ([autoDelegatesEnumerator getNextDelegate:&delegate delegateQueue:&delegateQueue]) + { + [module addDelegate:delegate delegateQueue:delegateQueue]; + } + + // Notify our own delegate(s) + + [multicastDelegate xmppStream:self didRegisterModule:module]; + + }}; + + // Asynchronous operation + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (void)unregisterModule:(XMPPModule *)module +{ + if (module == nil) return; + + // Synchronous operation + + dispatch_block_t block = ^{ @autoreleasepool { + + // Notify our own delegate(s) + + [multicastDelegate xmppStream:self willUnregisterModule:module]; + + // Remove auto delegates (if there are any) + + NSString *className = NSStringFromClass([module class]); + GCDMulticastDelegate *autoDelegates = autoDelegateDict[className]; + + GCDMulticastDelegateEnumerator *autoDelegatesEnumerator = [autoDelegates delegateEnumerator]; + id delegate; + dispatch_queue_t delegateQueue; + + while ([autoDelegatesEnumerator getNextDelegate:&delegate delegateQueue:&delegateQueue]) + { + // The module itself has dispatch_sync'd in order to invoke its deactivate method, + // which has in turn invoked this method. If we call back into the module, + // and have it dispatch_sync again, we're going to get a deadlock. + // So we must remove the delegate(s) asynchronously. + + [module removeDelegate:delegate delegateQueue:delegateQueue synchronously:NO]; + } + + // Unregister modules + + [registeredModules removeObject:module]; + + }}; + + // Synchronous operation + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); +} + +- (void)autoAddDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue toModulesOfClass:(Class)aClass +{ + if (delegate == nil) return; + if (aClass == nil) return; + + // Asynchronous operation + + dispatch_block_t block = ^{ @autoreleasepool { + + NSString *className = NSStringFromClass(aClass); + + // Add the delegate to all currently registered modules of the given class. + + for (XMPPModule *module in registeredModules) + { + if ([module isKindOfClass:aClass]) + { + [module addDelegate:delegate delegateQueue:delegateQueue]; + } + } + + // Add the delegate to list of auto delegates for the given class. + // It will be added as a delegate to future registered modules of the given class. + + id delegates = autoDelegateDict[className]; + if (delegates == nil) + { + delegates = [[GCDMulticastDelegate alloc] init]; + + autoDelegateDict[className] = delegates; + } + + [delegates addDelegate:delegate delegateQueue:delegateQueue]; + + }}; + + // Asynchronous operation + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (void)removeAutoDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue fromModulesOfClass:(Class)aClass +{ + if (delegate == nil) return; + // delegateQueue may be NULL + // aClass may be NULL + + // Synchronous operation + + dispatch_block_t block = ^{ @autoreleasepool { + + if (aClass == NULL) + { + // Remove the delegate from all currently registered modules of ANY class. + + for (XMPPModule *module in registeredModules) + { + [module removeDelegate:delegate delegateQueue:delegateQueue]; + } + + // Remove the delegate from list of auto delegates for all classes, + // so that it will not be auto added as a delegate to future registered modules. + + for (GCDMulticastDelegate *delegates in [autoDelegateDict objectEnumerator]) + { + [delegates removeDelegate:delegate delegateQueue:delegateQueue]; + } + } + else + { + NSString *className = NSStringFromClass(aClass); + + // Remove the delegate from all currently registered modules of the given class. + + for (XMPPModule *module in registeredModules) + { + if ([module isKindOfClass:aClass]) + { + [module removeDelegate:delegate delegateQueue:delegateQueue]; + } + } + + // Remove the delegate from list of auto delegates for the given class, + // so that it will not be added as a delegate to future registered modules of the given class. + + GCDMulticastDelegate *delegates = autoDelegateDict[className]; + [delegates removeDelegate:delegate delegateQueue:delegateQueue]; + + if ([delegates count] == 0) + { + [autoDelegateDict removeObjectForKey:className]; + } + } + + }}; + + // Synchronous operation + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); +} + +- (void)enumerateModulesWithBlock:(void (^)(XMPPModule *module, NSUInteger idx, BOOL *stop))enumBlock +{ + if (enumBlock == NULL) return; + + dispatch_block_t block = ^{ @autoreleasepool { + + NSUInteger i = 0; + BOOL stop = NO; + + for (XMPPModule *module in registeredModules) + { + enumBlock(module, i, &stop); + + if (stop) + break; + else + i++; + } + }}; + + // Synchronous operation + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); +} + +- (void)enumerateModulesOfClass:(Class)aClass withBlock:(void (^)(XMPPModule *module, NSUInteger idx, BOOL *stop))block +{ + [self enumerateModulesWithBlock:^(XMPPModule *module, NSUInteger idx, BOOL *stop) + { + if([module isKindOfClass:aClass]) + { + block(module,idx,stop); + } + }]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Utilities +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ++ (NSString *)generateUUID +{ + NSString *result = nil; + + CFUUIDRef uuid = CFUUIDCreate(NULL); + if (uuid) + { + result = (__bridge_transfer NSString *)CFUUIDCreateString(NULL, uuid); + CFRelease(uuid); + } + + return result; +} + +- (NSString *)generateUUID +{ + return [[self class] generateUUID]; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPElementReceipt + +static const uint32_t receipt_unknown = 0 << 0; +static const uint32_t receipt_failure = 1 << 0; +static const uint32_t receipt_success = 1 << 1; + + +- (id)init +{ + if ((self = [super init])) + { + atomicFlags = receipt_unknown; + semaphore = dispatch_semaphore_create(0); + } + return self; +} + +- (void)signalSuccess +{ + uint32_t mask = receipt_success; + OSAtomicOr32Barrier(mask, &atomicFlags); + + dispatch_semaphore_signal(semaphore); +} + +- (void)signalFailure +{ + uint32_t mask = receipt_failure; + OSAtomicOr32Barrier(mask, &atomicFlags); + + dispatch_semaphore_signal(semaphore); +} + +- (BOOL)wait:(NSTimeInterval)timeout_seconds +{ + uint32_t mask = 0; + uint32_t flags = OSAtomicOr32Barrier(mask, &atomicFlags); + + if (flags != receipt_unknown) return (flags == receipt_success); + + dispatch_time_t timeout_nanos; + + if (isless(timeout_seconds, 0.0)) + timeout_nanos = DISPATCH_TIME_FOREVER; + else + timeout_nanos = dispatch_time(DISPATCH_TIME_NOW, (timeout_seconds * NSEC_PER_SEC)); + + // dispatch_semaphore_wait + // + // Decrement the counting semaphore. If the resulting value is less than zero, + // this function waits in FIFO order for a signal to occur before returning. + // + // Returns zero on success, or non-zero if the timeout occurred. + // + // Note: If the timeout occurs, the semaphore value is incremented (without signaling). + + long result = dispatch_semaphore_wait(semaphore, timeout_nanos); + + if (result == 0) + { + flags = OSAtomicOr32Barrier(mask, &atomicFlags); + + return (flags == receipt_success); + } + else + { + // Timed out waiting... + return NO; + } +} + +- (void)dealloc +{ + #if !OS_OBJECT_USE_OBJC + dispatch_release(semaphore); + #endif +} + +@end diff --git a/Example/PNXMPPFramework.xcodeproj/project.pbxproj b/Example/PNXMPPFramework.xcodeproj/project.pbxproj index 32f662e..586e13c 100644 --- a/Example/PNXMPPFramework.xcodeproj/project.pbxproj +++ b/Example/PNXMPPFramework.xcodeproj/project.pbxproj @@ -467,7 +467,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 7.1; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -500,7 +500,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 7.1; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; diff --git a/Example/Podfile.lock b/Example/Podfile.lock index e9e0ea4..3fc1cbe 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -6,14 +6,11 @@ PODS: - CocoaAsyncSocket/RunLoop - CocoaAsyncSocket/GCD (7.4.3) - CocoaAsyncSocket/RunLoop (7.4.3) - - CocoaLumberjack (2.2.0): - - CocoaLumberjack/Default (= 2.2.0) - - CocoaLumberjack/Extensions (= 2.2.0) - - CocoaLumberjack/Core (2.2.0) - - CocoaLumberjack/Default (2.2.0): + - CocoaLumberjack (1.9.2): + - CocoaLumberjack/Extensions (= 1.9.2) + - CocoaLumberjack/Core (1.9.2) + - CocoaLumberjack/Extensions (1.9.2): - CocoaLumberjack/Core - - CocoaLumberjack/Extensions (2.2.0): - - CocoaLumberjack/Default - FBSnapshotTestCase (2.0.7): - FBSnapshotTestCase/SwiftSupport (= 2.0.7) - FBSnapshotTestCase/Core (2.0.7) @@ -25,9 +22,145 @@ PODS: - KissXML/Standard (5.0.3): - KissXML/Core - PNXMPPFramework (0.1.0): + - PNXMPPFramework/Authentication (= 0.1.0) + - PNXMPPFramework/BandwidthMonitor (= 0.1.0) + - PNXMPPFramework/Categories (= 0.1.0) + - PNXMPPFramework/Core (= 0.1.0) + - PNXMPPFramework/CoreDataStorage (= 0.1.0) + - PNXMPPFramework/GoogleSharedStatus (= 0.1.0) + - PNXMPPFramework/ProcessOne (= 0.1.0) + - PNXMPPFramework/Reconnect (= 0.1.0) + - PNXMPPFramework/Roster (= 0.1.0) + - PNXMPPFramework/SystemInputActivityMonitor (= 0.1.0) + - PNXMPPFramework/Utilities (= 0.1.0) + - PNXMPPFramework/XEP-0009 (= 0.1.0) + - PNXMPPFramework/XEP-0012 (= 0.1.0) + - PNXMPPFramework/XEP-0016 (= 0.1.0) + - PNXMPPFramework/XEP-0045 (= 0.1.0) + - PNXMPPFramework/XEP-0054 (= 0.1.0) + - PNXMPPFramework/XEP-0059 (= 0.1.0) + - PNXMPPFramework/XEP-0060 (= 0.1.0) + - PNXMPPFramework/XEP-0065 (= 0.1.0) + - PNXMPPFramework/XEP-0066 (= 0.1.0) + - PNXMPPFramework/XEP-0082 (= 0.1.0) + - PNXMPPFramework/XEP-0085 (= 0.1.0) + - PNXMPPFramework/XEP-0092 (= 0.1.0) + - PNXMPPFramework/XEP-0100 (= 0.1.0) + - PNXMPPFramework/XEP-0106 (= 0.1.0) + - PNXMPPFramework/XEP-0115 (= 0.1.0) + - PNXMPPFramework/XEP-0136 (= 0.1.0) + - PNXMPPFramework/XEP-0153 (= 0.1.0) + - PNXMPPFramework/XEP-0172 (= 0.1.0) + - PNXMPPFramework/XEP-0184 (= 0.1.0) + - PNXMPPFramework/XEP-0191 (= 0.1.0) + - PNXMPPFramework/XEP-0198 (= 0.1.0) + - PNXMPPFramework/XEP-0199 (= 0.1.0) + - PNXMPPFramework/XEP-0202 (= 0.1.0) + - PNXMPPFramework/XEP-0203 (= 0.1.0) + - PNXMPPFramework/XEP-0223 (= 0.1.0) + - PNXMPPFramework/XEP-0224 (= 0.1.0) + - PNXMPPFramework/XEP-0280 (= 0.1.0) + - PNXMPPFramework/XEP-0297 (= 0.1.0) + - PNXMPPFramework/XEP-0308 (= 0.1.0) + - PNXMPPFramework/XEP-0333 (= 0.1.0) + - PNXMPPFramework/XEP-0335 (= 0.1.0) + - PNXMPPFramework/Authentication (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/BandwidthMonitor (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/Categories (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/Core (0.1.0): - CocoaAsyncSocket - - CocoaLumberjack + - CocoaLumberjack (~> 1.9) - KissXML + - PNXMPPFramework/CoreDataStorage (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/GoogleSharedStatus (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/ProcessOne (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/Reconnect (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/Roster (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/CoreDataStorage + - PNXMPPFramework/SystemInputActivityMonitor (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/Utilities (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0009 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0012 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0016 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0045 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/CoreDataStorage + - PNXMPPFramework/XEP-0203 + - PNXMPPFramework/XEP-0054 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/Roster + - PNXMPPFramework/XEP-0059 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0060 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0065 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0066 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0082 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0085 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0092 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0100 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0106 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0115 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/CoreDataStorage + - PNXMPPFramework/XEP-0136 (0.1.0): + - PNXMPPFramework/CoreDataStorage + - PNXMPPFramework/XEP-0085 + - PNXMPPFramework/XEP-0203 + - PNXMPPFramework/XEP-0153 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0054 + - PNXMPPFramework/XEP-0172 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0184 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0191 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0198 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0199 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0202 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0082 + - PNXMPPFramework/XEP-0203 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0082 + - PNXMPPFramework/XEP-0223 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0224 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0280 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0297 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0203 + - PNXMPPFramework/XEP-0308 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0333 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0335 (0.1.0): + - PNXMPPFramework/Core DEPENDENCIES: - FBSnapshotTestCase @@ -39,10 +172,10 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: CocoaAsyncSocket: a18c75dca4b08723628a0bacca6e94803d90be91 - CocoaLumberjack: 17fe8581f84914d5d7e6360f7c70022b173c3ae0 + CocoaLumberjack: 628fca2e88ef06f7cf6817309aa405f325d9a6fa FBSnapshotTestCase: 7e85180d0d141a0cf472352edda7e80d7eaeb547 KissXML: d19dd6dc65e0dc721ba92b3077b8ebdd240f1c1e - PNXMPPFramework: 85a177de196fd742392f6ed0053c9cd2dd160f06 + PNXMPPFramework: 69989a4b1f7470763acd3e9a3d982ddfa5c161df PODFILE CHECKSUM: c24dacdc80a49fe0e0fea049a6d762eb76667498 diff --git a/Example/Pods/CocoaLumberjack/Classes/CocoaLumberjack.h b/Example/Pods/CocoaLumberjack/Classes/CocoaLumberjack.h deleted file mode 100644 index 0b568fb..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/CocoaLumberjack.h +++ /dev/null @@ -1,81 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -/** - * Welcome to CocoaLumberjack! - * - * The project page has a wealth of documentation if you have any questions. - * https://github.com/CocoaLumberjack/CocoaLumberjack - * - * If you're new to the project you may wish to read "Getting Started" at: - * Documentation/GettingStarted.md - * - * Otherwise, here is a quick refresher. - * There are three steps to using the macros: - * - * Step 1: - * Import the header in your implementation or prefix file: - * - * #import - * - * Step 2: - * Define your logging level in your implementation file: - * - * // Log levels: off, error, warn, info, verbose - * static const DDLogLevel ddLogLevel = DDLogLevelVerbose; - * - * Step 2 [3rd party frameworks]: - * - * Define your LOG_LEVEL_DEF to a different variable/function than ddLogLevel: - * - * // #undef LOG_LEVEL_DEF // Undefine first only if needed - * #define LOG_LEVEL_DEF myLibLogLevel - * - * Define your logging level in your implementation file: - * - * // Log levels: off, error, warn, info, verbose - * static const DDLogLevel myLibLogLevel = DDLogLevelVerbose; - * - * Step 3: - * Replace your NSLog statements with DDLog statements according to the severity of the message. - * - * NSLog(@"Fatal error, no dohickey found!"); -> DDLogError(@"Fatal error, no dohickey found!"); - * - * DDLog works exactly the same as NSLog. - * This means you can pass it multiple variables just like NSLog. - **/ - -#import - -// Disable legacy macros -#ifndef DD_LEGACY_MACROS - #define DD_LEGACY_MACROS 0 -#endif - -// Core -#import "DDLog.h" - -// Main macros -#import "DDLogMacros.h" -#import "DDAssertMacros.h" - -// Capture ASL -#import "DDASLLogCapture.h" - -// Loggers -#import "DDTTYLogger.h" -#import "DDASLLogger.h" -#import "DDFileLogger.h" - diff --git a/Example/Pods/CocoaLumberjack/Classes/CocoaLumberjack.swift b/Example/Pods/CocoaLumberjack/Classes/CocoaLumberjack.swift deleted file mode 100644 index 5f022ce..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/CocoaLumberjack.swift +++ /dev/null @@ -1,91 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2014-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -import Foundation - -extension DDLogFlag { - public static func fromLogLevel(logLevel: DDLogLevel) -> DDLogFlag { - return DDLogFlag(rawValue: logLevel.rawValue) - } - - public init(_ logLevel: DDLogLevel) { - self = DDLogFlag(rawValue: logLevel.rawValue) - } - - ///returns the log level, or the lowest equivalant. - public func toLogLevel() -> DDLogLevel { - if let ourValid = DDLogLevel(rawValue: self.rawValue) { - return ourValid - } else { - let logFlag:DDLogFlag = self - - if logFlag.contains(.Verbose) { - return .Verbose - } else if logFlag.contains(.Debug) { - return .Debug - } else if logFlag.contains(.Info) { - return .Info - } else if logFlag.contains(.Warning) { - return .Warning - } else if logFlag.contains(.Error) { - return .Error - } else { - return .Off - } - } - } -} - -public var defaultDebugLevel = DDLogLevel.Verbose - -public func resetDefaultDebugLevel() { - defaultDebugLevel = DDLogLevel.Verbose -} - -public func SwiftLogMacro(isAsynchronous: Bool, level: DDLogLevel, flag flg: DDLogFlag, context: Int = 0, file: StaticString = __FILE__, function: StaticString = __FUNCTION__, line: UInt = __LINE__, tag: AnyObject? = nil, @autoclosure string: () -> String) { - if level.rawValue & flg.rawValue != 0 { - // Tell the DDLogMessage constructor to copy the C strings that get passed to it. - // Using string interpolation to prevent integer overflow warning when using StaticString.stringValue - let logMessage = DDLogMessage(message: string(), level: level, flag: flg, context: context, file: "\(file)", function: "\(function)", line: line, tag: tag, options: [.CopyFile, .CopyFunction], timestamp: nil) - DDLog.log(isAsynchronous, message: logMessage) - } -} - -public func DDLogDebug(@autoclosure logText: () -> String, level: DDLogLevel = defaultDebugLevel, context: Int = 0, file: StaticString = __FILE__, function: StaticString = __FUNCTION__, line: UInt = __LINE__, tag: AnyObject? = nil, asynchronous async: Bool = true) { - SwiftLogMacro(async, level: level, flag: .Debug, context: context, file: file, function: function, line: line, tag: tag, string: logText) -} - -public func DDLogInfo(@autoclosure logText: () -> String, level: DDLogLevel = defaultDebugLevel, context: Int = 0, file: StaticString = __FILE__, function: StaticString = __FUNCTION__, line: UInt = __LINE__, tag: AnyObject? = nil, asynchronous async: Bool = true) { - SwiftLogMacro(async, level: level, flag: .Info, context: context, file: file, function: function, line: line, tag: tag, string: logText) -} - -public func DDLogWarn(@autoclosure logText: () -> String, level: DDLogLevel = defaultDebugLevel, context: Int = 0, file: StaticString = __FILE__, function: StaticString = __FUNCTION__, line: UInt = __LINE__, tag: AnyObject? = nil, asynchronous async: Bool = true) { - SwiftLogMacro(async, level: level, flag: .Warning, context: context, file: file, function: function, line: line, tag: tag, string: logText) -} - -public func DDLogVerbose(@autoclosure logText: () -> String, level: DDLogLevel = defaultDebugLevel, context: Int = 0, file: StaticString = __FILE__, function: StaticString = __FUNCTION__, line: UInt = __LINE__, tag: AnyObject? = nil, asynchronous async: Bool = true) { - SwiftLogMacro(async, level: level, flag: .Verbose, context: context, file: file, function: function, line: line, tag: tag, string: logText) -} - -public func DDLogError(@autoclosure logText: () -> String, level: DDLogLevel = defaultDebugLevel, context: Int = 0, file: StaticString = __FILE__, function: StaticString = __FUNCTION__, line: UInt = __LINE__, tag: AnyObject? = nil, asynchronous async: Bool = false) { - SwiftLogMacro(async, level: level, flag: .Error, context: context, file: file, function: function, line: line, tag: tag, string: logText) -} - -/// Analogous to the C preprocessor macro `THIS_FILE`. -public func CurrentFileName(fileName: StaticString = __FILE__) -> String { - // Using string interpolation to prevent integer overflow warning when using StaticString.stringValue - // This double-casting to NSString is necessary as changes to how Swift handles NSPathUtilities requres the string to be an NSString - return (("\(fileName)" as NSString).lastPathComponent as NSString).stringByDeletingPathExtension -} diff --git a/Example/Pods/CocoaLumberjack/Classes/DDASLLogCapture.h b/Example/Pods/CocoaLumberjack/Classes/DDASLLogCapture.h deleted file mode 100644 index f7fa79f..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/DDASLLogCapture.h +++ /dev/null @@ -1,48 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -#import "DDASLLogger.h" - -@protocol DDLogger; - -/** - * This class provides the ability to capture the ASL (Apple System Logs) - */ -@interface DDASLLogCapture : NSObject - -/** - * Start capturing logs - */ -+ (void)start; - -/** - * Stop capturing logs - */ -+ (void)stop; - -/** - * Returns the current capture level. - * @note Default log level: DDLogLevelVerbose (i.e. capture all ASL messages). - */ -+ (DDLogLevel)captureLevel; - -/** - * Set the capture level - * - * @param level new level - */ -+ (void)setCaptureLevel:(DDLogLevel)level; - -@end diff --git a/Example/Pods/CocoaLumberjack/Classes/DDASLLogCapture.m b/Example/Pods/CocoaLumberjack/Classes/DDASLLogCapture.m deleted file mode 100644 index 98d5342..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/DDASLLogCapture.m +++ /dev/null @@ -1,230 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -#import "DDASLLogCapture.h" - -// Disable legacy macros -#ifndef DD_LEGACY_MACROS - #define DD_LEGACY_MACROS 0 -#endif - -#import "DDLog.h" - -#include -#include -#include -#include - -static BOOL _cancel = YES; -static DDLogLevel _captureLevel = DDLogLevelVerbose; - -#ifdef __IPHONE_8_0 - #define DDASL_IOS_PIVOT_VERSION __IPHONE_8_0 -#endif -#ifdef __MAC_10_10 - #define DDASL_OSX_PIVOT_VERSION __MAC_10_10 -#endif - -@implementation DDASLLogCapture - -static aslmsg (*dd_asl_next)(aslresponse obj); -static void (*dd_asl_release)(aslresponse obj); - -+ (void)initialize -{ - #if (defined(DDASL_IOS_PIVOT_VERSION) && __IPHONE_OS_VERSION_MAX_ALLOWED >= DDASL_IOS_PIVOT_VERSION) || (defined(DDASL_OSX_PIVOT_VERSION) && __MAC_OS_X_VERSION_MAX_ALLOWED >= DDASL_OSX_PIVOT_VERSION) - #if __IPHONE_OS_VERSION_MIN_REQUIRED < DDASL_IOS_PIVOT_VERSION || __MAC_OS_X_VERSION_MIN_REQUIRED < DDASL_OSX_PIVOT_VERSION - #pragma GCC diagnostic push - #pragma GCC diagnostic ignored "-Wdeprecated-declarations" - // Building on falsely advertised SDK, targeting deprecated API - dd_asl_next = &aslresponse_next; - dd_asl_release = &aslresponse_free; - #pragma GCC diagnostic pop - #else - // Building on lastest, correct SDK, targeting latest API - dd_asl_next = &asl_next; - dd_asl_release = &asl_release; - #endif - #else - // Building on old SDKs, targeting deprecated API - dd_asl_next = &aslresponse_next; - dd_asl_release = &aslresponse_free; - #endif -} - -+ (void)start { - // Ignore subsequent calls - if (!_cancel) { - return; - } - - _cancel = NO; - - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) { - [self captureAslLogs]; - }); -} - -+ (void)stop { - _cancel = YES; -} - -+ (DDLogLevel)captureLevel { - return _captureLevel; -} - -+ (void)setCaptureLevel:(DDLogLevel)level { - _captureLevel = level; -} - -#pragma mark - Private methods - -+ (void)configureAslQuery:(aslmsg)query { - const char param[] = "7"; // ASL_LEVEL_DEBUG, which is everything. We'll rely on regular DDlog log level to filter - - asl_set_query(query, ASL_KEY_LEVEL, param, ASL_QUERY_OP_LESS_EQUAL | ASL_QUERY_OP_NUMERIC); - - // Don't retrieve logs from our own DDASLLogger - asl_set_query(query, kDDASLKeyDDLog, kDDASLDDLogValue, ASL_QUERY_OP_NOT_EQUAL); - -#if !TARGET_OS_IPHONE || TARGET_SIMULATOR - int processId = [[NSProcessInfo processInfo] processIdentifier]; - char pid[16]; - sprintf(pid, "%d", processId); - asl_set_query(query, ASL_KEY_PID, pid, ASL_QUERY_OP_EQUAL | ASL_QUERY_OP_NUMERIC); -#endif -} - -+ (void)aslMessageReceived:(aslmsg)msg { - const char* messageCString = asl_get( msg, ASL_KEY_MSG ); - if ( messageCString == NULL ) - return; - - int flag; - BOOL async; - - const char* levelCString = asl_get(msg, ASL_KEY_LEVEL); - switch (levelCString? atoi(levelCString) : 0) { - // By default all NSLog's with a ASL_LEVEL_WARNING level - case ASL_LEVEL_EMERG : - case ASL_LEVEL_ALERT : - case ASL_LEVEL_CRIT : flag = DDLogFlagError; async = NO; break; - case ASL_LEVEL_ERR : flag = DDLogFlagWarning; async = YES; break; - case ASL_LEVEL_WARNING : flag = DDLogFlagInfo; async = YES; break; - case ASL_LEVEL_NOTICE : flag = DDLogFlagDebug; async = YES; break; - case ASL_LEVEL_INFO : - case ASL_LEVEL_DEBUG : - default : flag = DDLogFlagVerbose; async = YES; break; - } - - if (!(_captureLevel & flag)) { - return; - } - - // NSString * sender = [NSString stringWithCString:asl_get(msg, ASL_KEY_SENDER) encoding:NSUTF8StringEncoding]; - NSString *message = @(messageCString); - - const char* secondsCString = asl_get( msg, ASL_KEY_TIME ); - const char* nanoCString = asl_get( msg, ASL_KEY_TIME_NSEC ); - NSTimeInterval seconds = secondsCString ? strtod(secondsCString, NULL) : [NSDate timeIntervalSinceReferenceDate] - NSTimeIntervalSince1970; - double nanoSeconds = nanoCString? strtod(nanoCString, NULL) : 0; - NSTimeInterval totalSeconds = seconds + (nanoSeconds / 1e9); - - NSDate *timeStamp = [NSDate dateWithTimeIntervalSince1970:totalSeconds]; - - DDLogMessage *logMessage = [[DDLogMessage alloc]initWithMessage:message - level:_captureLevel - flag:flag - context:0 - file:@"DDASLLogCapture" - function:0 - line:0 - tag:nil - options:0 - timestamp:timeStamp]; - - [DDLog log:async message:logMessage]; -} - -+ (void)captureAslLogs { - @autoreleasepool - { - /* - We use ASL_KEY_MSG_ID to see each message once, but there's no - obvious way to get the "next" ID. To bootstrap the process, we'll - search by timestamp until we've seen a message. - */ - - struct timeval timeval = { - .tv_sec = 0 - }; - gettimeofday(&timeval, NULL); - unsigned long long startTime = timeval.tv_sec; - __block unsigned long long lastSeenID = 0; - - /* - syslogd posts kNotifyASLDBUpdate (com.apple.system.logger.message) - through the notify API when it saves messages to the ASL database. - There is some coalescing - currently it is sent at most twice per - second - but there is no documented guarantee about this. In any - case, there may be multiple messages per notification. - - Notify notifications don't carry any payload, so we need to search - for the messages. - */ - int notifyToken = 0; // Can be used to unregister with notify_cancel(). - notify_register_dispatch(kNotifyASLDBUpdate, ¬ifyToken, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(int token) - { - // At least one message has been posted; build a search query. - @autoreleasepool - { - aslmsg query = asl_new(ASL_TYPE_QUERY); - char stringValue[64]; - - if (lastSeenID > 0) { - snprintf(stringValue, sizeof stringValue, "%llu", lastSeenID); - asl_set_query(query, ASL_KEY_MSG_ID, stringValue, ASL_QUERY_OP_GREATER | ASL_QUERY_OP_NUMERIC); - } else { - snprintf(stringValue, sizeof stringValue, "%llu", startTime); - asl_set_query(query, ASL_KEY_TIME, stringValue, ASL_QUERY_OP_GREATER_EQUAL | ASL_QUERY_OP_NUMERIC); - } - - [self configureAslQuery:query]; - - // Iterate over new messages. - aslmsg msg; - aslresponse response = asl_search(NULL, query); - - while ((msg = dd_asl_next(response))) - { - [self aslMessageReceived:msg]; - - // Keep track of which messages we've seen. - lastSeenID = atoll(asl_get(msg, ASL_KEY_MSG_ID)); - } - dd_asl_release(response); - asl_free(query); - - if (_cancel) { - notify_cancel(token); - return; - } - - } - }); - } -} - -@end diff --git a/Example/Pods/CocoaLumberjack/Classes/DDASLLogger.h b/Example/Pods/CocoaLumberjack/Classes/DDASLLogger.h deleted file mode 100644 index 24cc1c3..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/DDASLLogger.h +++ /dev/null @@ -1,58 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -#import - -// Disable legacy macros -#ifndef DD_LEGACY_MACROS - #define DD_LEGACY_MACROS 0 -#endif - -#import "DDLog.h" - -// Custom key set on messages sent to ASL -extern const char* const kDDASLKeyDDLog; - -// Value set for kDDASLKeyDDLog -extern const char* const kDDASLDDLogValue; - -/** - * This class provides a logger for the Apple System Log facility. - * - * As described in the "Getting Started" page, - * the traditional NSLog() function directs its output to two places: - * - * - Apple System Log - * - StdErr (if stderr is a TTY) so log statements show up in Xcode console - * - * To duplicate NSLog() functionality you can simply add this logger and a tty logger. - * However, if you instead choose to use file logging (for faster performance), - * you may choose to use a file logger and a tty logger. - **/ -@interface DDASLLogger : DDAbstractLogger - -/** - * Singleton method - * - * @return the shared instance - */ -+ (instancetype)sharedInstance; - -// Inherited from DDAbstractLogger - -// - (id )logFormatter; -// - (void)setLogFormatter:(id )formatter; - -@end diff --git a/Example/Pods/CocoaLumberjack/Classes/DDASLLogger.m b/Example/Pods/CocoaLumberjack/Classes/DDASLLogger.m deleted file mode 100644 index 90061c8..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/DDASLLogger.m +++ /dev/null @@ -1,121 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -#import "DDASLLogger.h" -#import - -#if !__has_feature(objc_arc) -#error This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). -#endif - -const char* const kDDASLKeyDDLog = "DDLog"; - -const char* const kDDASLDDLogValue = "1"; - -static DDASLLogger *sharedInstance; - -@interface DDASLLogger () { - aslclient _client; -} - -@end - - -@implementation DDASLLogger - -+ (instancetype)sharedInstance { - static dispatch_once_t DDASLLoggerOnceToken; - - dispatch_once(&DDASLLoggerOnceToken, ^{ - sharedInstance = [[[self class] alloc] init]; - }); - - return sharedInstance; -} - -- (instancetype)init { - if (sharedInstance != nil) { - return nil; - } - - if ((self = [super init])) { - // A default asl client is provided for the main thread, - // but background threads need to create their own client. - - _client = asl_open(NULL, "com.apple.console", 0); - } - - return self; -} - -- (void)logMessage:(DDLogMessage *)logMessage { - // Skip captured log messages - if ([logMessage->_fileName isEqualToString:@"DDASLLogCapture"]) { - return; - } - - NSString * message = _logFormatter ? [_logFormatter formatLogMessage:logMessage] : logMessage->_message; - - if (logMessage) { - const char *msg = [message UTF8String]; - - size_t aslLogLevel; - switch (logMessage->_flag) { - // Note: By default ASL will filter anything above level 5 (Notice). - // So our mappings shouldn't go above that level. - case DDLogFlagError : aslLogLevel = ASL_LEVEL_CRIT; break; - case DDLogFlagWarning : aslLogLevel = ASL_LEVEL_ERR; break; - case DDLogFlagInfo : aslLogLevel = ASL_LEVEL_WARNING; break; // Regular NSLog's level - case DDLogFlagDebug : - case DDLogFlagVerbose : - default : aslLogLevel = ASL_LEVEL_NOTICE; break; - } - - static char const *const level_strings[] = { "0", "1", "2", "3", "4", "5", "6", "7" }; - - // NSLog uses the current euid to set the ASL_KEY_READ_UID. - uid_t const readUID = geteuid(); - - char readUIDString[16]; -#ifndef NS_BLOCK_ASSERTIONS - int l = snprintf(readUIDString, sizeof(readUIDString), "%d", readUID); -#else - snprintf(readUIDString, sizeof(readUIDString), "%d", readUID); -#endif - - NSAssert(l < sizeof(readUIDString), - @"Formatted euid is too long."); - NSAssert(aslLogLevel < (sizeof(level_strings) / sizeof(level_strings[0])), - @"Unhandled ASL log level."); - - aslmsg m = asl_new(ASL_TYPE_MSG); - if (m != NULL) { - if (asl_set(m, ASL_KEY_LEVEL, level_strings[aslLogLevel]) == 0 && - asl_set(m, ASL_KEY_MSG, msg) == 0 && - asl_set(m, ASL_KEY_READ_UID, readUIDString) == 0 && - asl_set(m, kDDASLKeyDDLog, kDDASLDDLogValue) == 0) { - asl_send(_client, m); - } - asl_free(m); - } - //TODO handle asl_* failures non-silently? - } -} - -- (NSString *)loggerName { - return @"cocoa.lumberjack.aslLogger"; -} - -@end diff --git a/Example/Pods/CocoaLumberjack/Classes/DDAbstractDatabaseLogger.m b/Example/Pods/CocoaLumberjack/Classes/DDAbstractDatabaseLogger.m deleted file mode 100644 index c8782de..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/DDAbstractDatabaseLogger.m +++ /dev/null @@ -1,660 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -#import "DDAbstractDatabaseLogger.h" -#import - - -#if !__has_feature(objc_arc) -#error This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). -#endif - -@interface DDAbstractDatabaseLogger () - -- (void)destroySaveTimer; -- (void)destroyDeleteTimer; - -@end - -#pragma mark - - -@implementation DDAbstractDatabaseLogger - -- (instancetype)init { - if ((self = [super init])) { - _saveThreshold = 500; - _saveInterval = 60; // 60 seconds - _maxAge = (60 * 60 * 24 * 7); // 7 days - _deleteInterval = (60 * 5); // 5 minutes - } - - return self; -} - -- (void)dealloc { - [self destroySaveTimer]; - [self destroyDeleteTimer]; -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Override Me -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -- (BOOL)db_log:(DDLogMessage *)logMessage { - // Override me and add your implementation. - // - // Return YES if an item was added to the buffer. - // Return NO if the logMessage was ignored. - - return NO; -} - -- (void)db_save { - // Override me and add your implementation. -} - -- (void)db_delete { - // Override me and add your implementation. -} - -- (void)db_saveAndDelete { - // Override me and add your implementation. -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Private API -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -- (void)performSaveAndSuspendSaveTimer { - if (_unsavedCount > 0) { - if (_deleteOnEverySave) { - [self db_saveAndDelete]; - } else { - [self db_save]; - } - } - - _unsavedCount = 0; - _unsavedTime = 0; - - if (_saveTimer && !_saveTimerSuspended) { - dispatch_suspend(_saveTimer); - _saveTimerSuspended = YES; - } -} - -- (void)performDelete { - if (_maxAge > 0.0) { - [self db_delete]; - - _lastDeleteTime = dispatch_time(DISPATCH_TIME_NOW, 0); - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Timers -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -- (void)destroySaveTimer { - if (_saveTimer) { - dispatch_source_cancel(_saveTimer); - - if (_saveTimerSuspended) { - // Must resume a timer before releasing it (or it will crash) - dispatch_resume(_saveTimer); - _saveTimerSuspended = NO; - } - - #if !OS_OBJECT_USE_OBJC - dispatch_release(_saveTimer); - #endif - _saveTimer = NULL; - } -} - -- (void)updateAndResumeSaveTimer { - if ((_saveTimer != NULL) && (_saveInterval > 0.0) && (_unsavedTime > 0.0)) { - uint64_t interval = (uint64_t)(_saveInterval * NSEC_PER_SEC); - dispatch_time_t startTime = dispatch_time(_unsavedTime, interval); - - dispatch_source_set_timer(_saveTimer, startTime, interval, 1.0); - - if (_saveTimerSuspended) { - dispatch_resume(_saveTimer); - _saveTimerSuspended = NO; - } - } -} - -- (void)createSuspendedSaveTimer { - if ((_saveTimer == NULL) && (_saveInterval > 0.0)) { - _saveTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.loggerQueue); - - dispatch_source_set_event_handler(_saveTimer, ^{ @autoreleasepool { - [self performSaveAndSuspendSaveTimer]; - } }); - - _saveTimerSuspended = YES; - } -} - -- (void)destroyDeleteTimer { - if (_deleteTimer) { - dispatch_source_cancel(_deleteTimer); - #if !OS_OBJECT_USE_OBJC - dispatch_release(_deleteTimer); - #endif - _deleteTimer = NULL; - } -} - -- (void)updateDeleteTimer { - if ((_deleteTimer != NULL) && (_deleteInterval > 0.0) && (_maxAge > 0.0)) { - uint64_t interval = (uint64_t)(_deleteInterval * NSEC_PER_SEC); - dispatch_time_t startTime; - - if (_lastDeleteTime > 0) { - startTime = dispatch_time(_lastDeleteTime, interval); - } else { - startTime = dispatch_time(DISPATCH_TIME_NOW, interval); - } - - dispatch_source_set_timer(_deleteTimer, startTime, interval, 1.0); - } -} - -- (void)createAndStartDeleteTimer { - if ((_deleteTimer == NULL) && (_deleteInterval > 0.0) && (_maxAge > 0.0)) { - _deleteTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.loggerQueue); - - if (_deleteTimer != NULL) { - dispatch_source_set_event_handler(_deleteTimer, ^{ @autoreleasepool { - [self performDelete]; - } }); - - [self updateDeleteTimer]; - - if (_deleteTimer != NULL) { - dispatch_resume(_deleteTimer); - } - } - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Configuration -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -- (NSUInteger)saveThreshold { - // The design of this method is taken from the DDAbstractLogger implementation. - // For extensive documentation please refer to the DDAbstractLogger implementation. - - // Note: The internal implementation MUST access the colorsEnabled variable directly, - // This method is designed explicitly for external access. - // - // Using "self." syntax to go through this method will cause immediate deadlock. - // This is the intended result. Fix it by accessing the ivar directly. - // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. - - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); - - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - - __block NSUInteger result; - - dispatch_sync(globalLoggingQueue, ^{ - dispatch_sync(self.loggerQueue, ^{ - result = _saveThreshold; - }); - }); - - return result; -} - -- (void)setSaveThreshold:(NSUInteger)threshold { - dispatch_block_t block = ^{ - @autoreleasepool { - if (_saveThreshold != threshold) { - _saveThreshold = threshold; - - // Since the saveThreshold has changed, - // we check to see if the current unsavedCount has surpassed the new threshold. - // - // If it has, we immediately save the log. - - if ((_unsavedCount >= _saveThreshold) && (_saveThreshold > 0)) { - [self performSaveAndSuspendSaveTimer]; - } - } - } - }; - - // The design of the setter logic below is taken from the DDAbstractLogger implementation. - // For documentation please refer to the DDAbstractLogger implementation. - - if ([self isOnInternalLoggerQueue]) { - block(); - } else { - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - - dispatch_async(globalLoggingQueue, ^{ - dispatch_async(self.loggerQueue, block); - }); - } -} - -- (NSTimeInterval)saveInterval { - // The design of this method is taken from the DDAbstractLogger implementation. - // For extensive documentation please refer to the DDAbstractLogger implementation. - - // Note: The internal implementation MUST access the colorsEnabled variable directly, - // This method is designed explicitly for external access. - // - // Using "self." syntax to go through this method will cause immediate deadlock. - // This is the intended result. Fix it by accessing the ivar directly. - // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. - - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); - - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - - __block NSTimeInterval result; - - dispatch_sync(globalLoggingQueue, ^{ - dispatch_sync(self.loggerQueue, ^{ - result = _saveInterval; - }); - }); - - return result; -} - -- (void)setSaveInterval:(NSTimeInterval)interval { - dispatch_block_t block = ^{ - @autoreleasepool { - // C99 recommended floating point comparison macro - // Read: isLessThanOrGreaterThan(floatA, floatB) - - if (/* saveInterval != interval */ islessgreater(_saveInterval, interval)) { - _saveInterval = interval; - - // There are several cases we need to handle here. - // - // 1. If the saveInterval was previously enabled and it just got disabled, - // then we need to stop the saveTimer. (And we might as well release it.) - // - // 2. If the saveInterval was previously disabled and it just got enabled, - // then we need to setup the saveTimer. (Plus we might need to do an immediate save.) - // - // 3. If the saveInterval increased, then we need to reset the timer so that it fires at the later date. - // - // 4. If the saveInterval decreased, then we need to reset the timer so that it fires at an earlier date. - // (Plus we might need to do an immediate save.) - - if (_saveInterval > 0.0) { - if (_saveTimer == NULL) { - // Handles #2 - // - // Since the saveTimer uses the unsavedTime to calculate it's first fireDate, - // if a save is needed the timer will fire immediately. - - [self createSuspendedSaveTimer]; - [self updateAndResumeSaveTimer]; - } else { - // Handles #3 - // Handles #4 - // - // Since the saveTimer uses the unsavedTime to calculate it's first fireDate, - // if a save is needed the timer will fire immediately. - - [self updateAndResumeSaveTimer]; - } - } else if (_saveTimer) { - // Handles #1 - - [self destroySaveTimer]; - } - } - } - }; - - // The design of the setter logic below is taken from the DDAbstractLogger implementation. - // For documentation please refer to the DDAbstractLogger implementation. - - if ([self isOnInternalLoggerQueue]) { - block(); - } else { - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - - dispatch_async(globalLoggingQueue, ^{ - dispatch_async(self.loggerQueue, block); - }); - } -} - -- (NSTimeInterval)maxAge { - // The design of this method is taken from the DDAbstractLogger implementation. - // For extensive documentation please refer to the DDAbstractLogger implementation. - - // Note: The internal implementation MUST access the colorsEnabled variable directly, - // This method is designed explicitly for external access. - // - // Using "self." syntax to go through this method will cause immediate deadlock. - // This is the intended result. Fix it by accessing the ivar directly. - // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. - - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); - - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - - __block NSTimeInterval result; - - dispatch_sync(globalLoggingQueue, ^{ - dispatch_sync(self.loggerQueue, ^{ - result = _maxAge; - }); - }); - - return result; -} - -- (void)setMaxAge:(NSTimeInterval)interval { - dispatch_block_t block = ^{ - @autoreleasepool { - // C99 recommended floating point comparison macro - // Read: isLessThanOrGreaterThan(floatA, floatB) - - if (/* maxAge != interval */ islessgreater(_maxAge, interval)) { - NSTimeInterval oldMaxAge = _maxAge; - NSTimeInterval newMaxAge = interval; - - _maxAge = interval; - - // There are several cases we need to handle here. - // - // 1. If the maxAge was previously enabled and it just got disabled, - // then we need to stop the deleteTimer. (And we might as well release it.) - // - // 2. If the maxAge was previously disabled and it just got enabled, - // then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.) - // - // 3. If the maxAge was increased, - // then we don't need to do anything. - // - // 4. If the maxAge was decreased, - // then we should do an immediate delete. - - BOOL shouldDeleteNow = NO; - - if (oldMaxAge > 0.0) { - if (newMaxAge <= 0.0) { - // Handles #1 - - [self destroyDeleteTimer]; - } else if (oldMaxAge > newMaxAge) { - // Handles #4 - shouldDeleteNow = YES; - } - } else if (newMaxAge > 0.0) { - // Handles #2 - shouldDeleteNow = YES; - } - - if (shouldDeleteNow) { - [self performDelete]; - - if (_deleteTimer) { - [self updateDeleteTimer]; - } else { - [self createAndStartDeleteTimer]; - } - } - } - } - }; - - // The design of the setter logic below is taken from the DDAbstractLogger implementation. - // For documentation please refer to the DDAbstractLogger implementation. - - if ([self isOnInternalLoggerQueue]) { - block(); - } else { - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - - dispatch_async(globalLoggingQueue, ^{ - dispatch_async(self.loggerQueue, block); - }); - } -} - -- (NSTimeInterval)deleteInterval { - // The design of this method is taken from the DDAbstractLogger implementation. - // For extensive documentation please refer to the DDAbstractLogger implementation. - - // Note: The internal implementation MUST access the colorsEnabled variable directly, - // This method is designed explicitly for external access. - // - // Using "self." syntax to go through this method will cause immediate deadlock. - // This is the intended result. Fix it by accessing the ivar directly. - // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. - - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); - - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - - __block NSTimeInterval result; - - dispatch_sync(globalLoggingQueue, ^{ - dispatch_sync(self.loggerQueue, ^{ - result = _deleteInterval; - }); - }); - - return result; -} - -- (void)setDeleteInterval:(NSTimeInterval)interval { - dispatch_block_t block = ^{ - @autoreleasepool { - // C99 recommended floating point comparison macro - // Read: isLessThanOrGreaterThan(floatA, floatB) - - if (/* deleteInterval != interval */ islessgreater(_deleteInterval, interval)) { - _deleteInterval = interval; - - // There are several cases we need to handle here. - // - // 1. If the deleteInterval was previously enabled and it just got disabled, - // then we need to stop the deleteTimer. (And we might as well release it.) - // - // 2. If the deleteInterval was previously disabled and it just got enabled, - // then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.) - // - // 3. If the deleteInterval increased, then we need to reset the timer so that it fires at the later date. - // - // 4. If the deleteInterval decreased, then we need to reset the timer so that it fires at an earlier date. - // (Plus we might need to do an immediate delete.) - - if (_deleteInterval > 0.0) { - if (_deleteTimer == NULL) { - // Handles #2 - // - // Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate, - // if a delete is needed the timer will fire immediately. - - [self createAndStartDeleteTimer]; - } else { - // Handles #3 - // Handles #4 - // - // Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate, - // if a save is needed the timer will fire immediately. - - [self updateDeleteTimer]; - } - } else if (_deleteTimer) { - // Handles #1 - - [self destroyDeleteTimer]; - } - } - } - }; - - // The design of the setter logic below is taken from the DDAbstractLogger implementation. - // For documentation please refer to the DDAbstractLogger implementation. - - if ([self isOnInternalLoggerQueue]) { - block(); - } else { - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - - dispatch_async(globalLoggingQueue, ^{ - dispatch_async(self.loggerQueue, block); - }); - } -} - -- (BOOL)deleteOnEverySave { - // The design of this method is taken from the DDAbstractLogger implementation. - // For extensive documentation please refer to the DDAbstractLogger implementation. - - // Note: The internal implementation MUST access the colorsEnabled variable directly, - // This method is designed explicitly for external access. - // - // Using "self." syntax to go through this method will cause immediate deadlock. - // This is the intended result. Fix it by accessing the ivar directly. - // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. - - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); - - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - - __block BOOL result; - - dispatch_sync(globalLoggingQueue, ^{ - dispatch_sync(self.loggerQueue, ^{ - result = _deleteOnEverySave; - }); - }); - - return result; -} - -- (void)setDeleteOnEverySave:(BOOL)flag { - dispatch_block_t block = ^{ - _deleteOnEverySave = flag; - }; - - // The design of the setter logic below is taken from the DDAbstractLogger implementation. - // For documentation please refer to the DDAbstractLogger implementation. - - if ([self isOnInternalLoggerQueue]) { - block(); - } else { - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - - dispatch_async(globalLoggingQueue, ^{ - dispatch_async(self.loggerQueue, block); - }); - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Public API -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -- (void)savePendingLogEntries { - dispatch_block_t block = ^{ - @autoreleasepool { - [self performSaveAndSuspendSaveTimer]; - } - }; - - if ([self isOnInternalLoggerQueue]) { - block(); - } else { - dispatch_async(self.loggerQueue, block); - } -} - -- (void)deleteOldLogEntries { - dispatch_block_t block = ^{ - @autoreleasepool { - [self performDelete]; - } - }; - - if ([self isOnInternalLoggerQueue]) { - block(); - } else { - dispatch_async(self.loggerQueue, block); - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark DDLogger -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -- (void)didAddLogger { - // If you override me be sure to invoke [super didAddLogger]; - - [self createSuspendedSaveTimer]; - - [self createAndStartDeleteTimer]; -} - -- (void)willRemoveLogger { - // If you override me be sure to invoke [super willRemoveLogger]; - - [self performSaveAndSuspendSaveTimer]; - - [self destroySaveTimer]; - [self destroyDeleteTimer]; -} - -- (void)logMessage:(DDLogMessage *)logMessage { - if ([self db_log:logMessage]) { - BOOL firstUnsavedEntry = (++_unsavedCount == 1); - - if ((_unsavedCount >= _saveThreshold) && (_saveThreshold > 0)) { - [self performSaveAndSuspendSaveTimer]; - } else if (firstUnsavedEntry) { - _unsavedTime = dispatch_time(DISPATCH_TIME_NOW, 0); - [self updateAndResumeSaveTimer]; - } - } -} - -- (void)flush { - // This method is invoked by DDLog's flushLog method. - // - // It is called automatically when the application quits, - // or if the developer invokes DDLog's flushLog method prior to crashing or something. - - [self performSaveAndSuspendSaveTimer]; -} - -@end diff --git a/Example/Pods/CocoaLumberjack/Classes/DDAssertMacros.h b/Example/Pods/CocoaLumberjack/Classes/DDAssertMacros.h deleted file mode 100644 index 870d31f..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/DDAssertMacros.h +++ /dev/null @@ -1,26 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -/** - * NSAsset replacement that will output a log message even when assertions are disabled. - **/ -#define DDAssert(condition, frmt, ...) \ - if (!(condition)) { \ - NSString *description = [NSString stringWithFormat:frmt, ## __VA_ARGS__]; \ - DDLogError(@"%@", description); \ - NSAssert(NO, description); \ - } -#define DDAssertCondition(condition) DDAssert(condition, @"Condition not satisfied: %s", #condition) - diff --git a/Example/Pods/CocoaLumberjack/Classes/DDLegacyMacros.h b/Example/Pods/CocoaLumberjack/Classes/DDLegacyMacros.h deleted file mode 100644 index e0671b9..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/DDLegacyMacros.h +++ /dev/null @@ -1,75 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -/** - * Legacy macros used for 1.9.x backwards compatibility. - * - * Imported by default when importing a DDLog.h directly and DD_LEGACY_MACROS is not defined and set to 0. - **/ -#if DD_LEGACY_MACROS - -#warning CocoaLumberjack 1.9.x legacy macros enabled. \ -Disable legacy macros by importing CocoaLumberjack.h or DDLogMacros.h instead of DDLog.h or add `#define DD_LEGACY_MACROS 0` before importing DDLog.h. - -#ifndef LOG_LEVEL_DEF - #define LOG_LEVEL_DEF ddLogLevel -#endif - -#define LOG_FLAG_ERROR DDLogFlagError -#define LOG_FLAG_WARN DDLogFlagWarning -#define LOG_FLAG_INFO DDLogFlagInfo -#define LOG_FLAG_DEBUG DDLogFlagDebug -#define LOG_FLAG_VERBOSE DDLogFlagVerbose - -#define LOG_LEVEL_OFF DDLogLevelOff -#define LOG_LEVEL_ERROR DDLogLevelError -#define LOG_LEVEL_WARN DDLogLevelWarning -#define LOG_LEVEL_INFO DDLogLevelInfo -#define LOG_LEVEL_DEBUG DDLogLevelDebug -#define LOG_LEVEL_VERBOSE DDLogLevelVerbose -#define LOG_LEVEL_ALL DDLogLevelAll - -#define LOG_ASYNC_ENABLED YES - -#define LOG_ASYNC_ERROR ( NO && LOG_ASYNC_ENABLED) -#define LOG_ASYNC_WARN (YES && LOG_ASYNC_ENABLED) -#define LOG_ASYNC_INFO (YES && LOG_ASYNC_ENABLED) -#define LOG_ASYNC_DEBUG (YES && LOG_ASYNC_ENABLED) -#define LOG_ASYNC_VERBOSE (YES && LOG_ASYNC_ENABLED) - -#define LOG_MACRO(isAsynchronous, lvl, flg, ctx, atag, fnct, frmt, ...) \ - [DDLog log : isAsynchronous \ - level : lvl \ - flag : flg \ - context : ctx \ - file : __FILE__ \ - function : fnct \ - line : __LINE__ \ - tag : atag \ - format : (frmt), ## __VA_ARGS__] - -#define LOG_MAYBE(async, lvl, flg, ctx, fnct, frmt, ...) \ - do { if(lvl & flg) LOG_MACRO(async, lvl, flg, ctx, nil, fnct, frmt, ##__VA_ARGS__); } while(0) - -#define LOG_OBJC_MAYBE(async, lvl, flg, ctx, frmt, ...) \ - LOG_MAYBE(async, lvl, flg, ctx, __PRETTY_FUNCTION__, frmt, ## __VA_ARGS__) - -#define DDLogError(frmt, ...) LOG_OBJC_MAYBE(LOG_ASYNC_ERROR, LOG_LEVEL_DEF, LOG_FLAG_ERROR, 0, frmt, ##__VA_ARGS__) -#define DDLogWarn(frmt, ...) LOG_OBJC_MAYBE(LOG_ASYNC_WARN, LOG_LEVEL_DEF, LOG_FLAG_WARN, 0, frmt, ##__VA_ARGS__) -#define DDLogInfo(frmt, ...) LOG_OBJC_MAYBE(LOG_ASYNC_INFO, LOG_LEVEL_DEF, LOG_FLAG_INFO, 0, frmt, ##__VA_ARGS__) -#define DDLogDebug(frmt, ...) LOG_OBJC_MAYBE(LOG_ASYNC_DEBUG, LOG_LEVEL_DEF, LOG_FLAG_DEBUG, 0, frmt, ##__VA_ARGS__) -#define DDLogVerbose(frmt, ...) LOG_OBJC_MAYBE(LOG_ASYNC_VERBOSE, LOG_LEVEL_DEF, LOG_FLAG_VERBOSE, 0, frmt, ##__VA_ARGS__) - -#endif diff --git a/Example/Pods/CocoaLumberjack/Classes/DDLog+LOGV.h b/Example/Pods/CocoaLumberjack/Classes/DDLog+LOGV.h deleted file mode 100644 index cf4bfc3..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/DDLog+LOGV.h +++ /dev/null @@ -1,83 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -// Disable legacy macros -#ifndef DD_LEGACY_MACROS - #define DD_LEGACY_MACROS 0 -#endif - -#import "DDLog.h" - -/** - * The constant/variable/method responsible for controlling the current log level. - **/ -#ifndef LOG_LEVEL_DEF - #define LOG_LEVEL_DEF ddLogLevel -#endif - -/** - * Whether async should be used by log messages, excluding error messages that are always sent sync. - **/ -#ifndef LOG_ASYNC_ENABLED - #define LOG_ASYNC_ENABLED YES -#endif - -/** - * This is the single macro that all other macros below compile into. - * This big multiline macro makes all the other macros easier to read. - **/ -#define LOGV_MACRO(isAsynchronous, lvl, flg, ctx, atag, fnct, frmt, avalist) \ - [DDLog log : isAsynchronous \ - level : lvl \ - flag : flg \ - context : ctx \ - file : __FILE__ \ - function : fnct \ - line : __LINE__ \ - tag : atag \ - format : frmt \ - args : avalist] - -/** - * Define version of the macro that only execute if the log level is above the threshold. - * The compiled versions essentially look like this: - * - * if (logFlagForThisLogMsg & ddLogLevel) { execute log message } - * - * When LOG_LEVEL_DEF is defined as ddLogLevel. - * - * As shown further below, Lumberjack actually uses a bitmask as opposed to primitive log levels. - * This allows for a great amount of flexibility and some pretty advanced fine grained logging techniques. - * - * Note that when compiler optimizations are enabled (as they are for your release builds), - * the log messages above your logging threshold will automatically be compiled out. - * - * (If the compiler sees LOG_LEVEL_DEF/ddLogLevel declared as a constant, the compiler simply checks to see - * if the 'if' statement would execute, and if not it strips it from the binary.) - * - * We also define shorthand versions for asynchronous and synchronous logging. - **/ -#define LOGV_MAYBE(async, lvl, flg, ctx, tag, fnct, frmt, avalist) \ - do { if(lvl & flg) LOGV_MACRO(async, lvl, flg, ctx, tag, fnct, frmt, avalist); } while(0) - -/** - * Ready to use log macros with no context or tag. - **/ -#define DDLogVError(frmt, avalist) LOGV_MAYBE(NO, LOG_LEVEL_DEF, DDLogFlagError, 0, nil, __PRETTY_FUNCTION__, frmt, avalist) -#define DDLogVWarn(frmt, avalist) LOGV_MAYBE(LOG_ASYNC_ENABLED, LOG_LEVEL_DEF, DDLogFlagWarning, 0, nil, __PRETTY_FUNCTION__, frmt, avalist) -#define DDLogVInfo(frmt, avalist) LOGV_MAYBE(LOG_ASYNC_ENABLED, LOG_LEVEL_DEF, DDLogFlagInfo, 0, nil, __PRETTY_FUNCTION__, frmt, avalist) -#define DDLogVDebug(frmt, avalist) LOGV_MAYBE(LOG_ASYNC_ENABLED, LOG_LEVEL_DEF, DDLogFlagDebug, 0, nil, __PRETTY_FUNCTION__, frmt, avalist) -#define DDLogVVerbose(frmt, avalist) LOGV_MAYBE(LOG_ASYNC_ENABLED, LOG_LEVEL_DEF, DDLogFlagVerbose, 0, nil, __PRETTY_FUNCTION__, frmt, avalist) - diff --git a/Example/Pods/CocoaLumberjack/Classes/DDLog.h b/Example/Pods/CocoaLumberjack/Classes/DDLog.h deleted file mode 100644 index b7f1074..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/DDLog.h +++ /dev/null @@ -1,743 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -#import - -// Enable 1.9.x legacy macros if imported directly -#ifndef DD_LEGACY_MACROS - #define DD_LEGACY_MACROS 1 -#endif -// DD_LEGACY_MACROS is checked in the file itself -#import "DDLegacyMacros.h" - -#if OS_OBJECT_USE_OBJC - #define DISPATCH_QUEUE_REFERENCE_TYPE strong -#else - #define DISPATCH_QUEUE_REFERENCE_TYPE assign -#endif - -@class DDLogMessage; -@protocol DDLogger; -@protocol DDLogFormatter; - -/** - * Define the standard options. - * - * We default to only 4 levels because it makes it easier for beginners - * to make the transition to a logging framework. - * - * More advanced users may choose to completely customize the levels (and level names) to suite their needs. - * For more information on this see the "Custom Log Levels" page: - * Documentation/CustomLogLevels.md - * - * Advanced users may also notice that we're using a bitmask. - * This is to allow for custom fine grained logging: - * Documentation/FineGrainedLogging.md - * - * -- Flags -- - * - * Typically you will use the LOG_LEVELS (see below), but the flags may be used directly in certain situations. - * For example, say you have a lot of warning log messages, and you wanted to disable them. - * However, you still needed to see your error and info log messages. - * You could accomplish that with the following: - * - * static const DDLogLevel ddLogLevel = DDLogFlagError | DDLogFlagInfo; - * - * When LOG_LEVEL_DEF is defined as ddLogLevel. - * - * Flags may also be consulted when writing custom log formatters, - * as the DDLogMessage class captures the individual flag that caused the log message to fire. - * - * -- Levels -- - * - * Log levels are simply the proper bitmask of the flags. - * - * -- Booleans -- - * - * The booleans may be used when your logging code involves more than one line. - * For example: - * - * if (LOG_VERBOSE) { - * for (id sprocket in sprockets) - * DDLogVerbose(@"sprocket: %@", [sprocket description]) - * } - * - * -- Async -- - * - * Defines the default asynchronous options. - * The default philosophy for asynchronous logging is very simple: - * - * Log messages with errors should be executed synchronously. - * After all, an error just occurred. The application could be unstable. - * - * All other log messages, such as debug output, are executed asynchronously. - * After all, if it wasn't an error, then it was just informational output, - * or something the application was easily able to recover from. - * - * -- Changes -- - * - * You are strongly discouraged from modifying this file. - * If you do, you make it more difficult on yourself to merge future bug fixes and improvements from the project. - * Instead, create your own MyLogging.h or ApplicationNameLogging.h or CompanyLogging.h - * - * For an example of customizing your logging experience, see the "Custom Log Levels" page: - * Documentation/CustomLogLevels.md - **/ - -/** - * Flags accompany each log. They are used together with levels to filter out logs. - */ -typedef NS_OPTIONS(NSUInteger, DDLogFlag){ - /** - * 0...00000 DDLogFlagError - */ - DDLogFlagError = (1 << 0), - - /** - * 0...00001 DDLogFlagWarning - */ - DDLogFlagWarning = (1 << 1), - - /** - * 0...00010 DDLogFlagInfo - */ - DDLogFlagInfo = (1 << 2), - - /** - * 0...00100 DDLogFlagDebug - */ - DDLogFlagDebug = (1 << 3), - - /** - * 0...01000 DDLogFlagVerbose - */ - DDLogFlagVerbose = (1 << 4) -}; - -/** - * Log levels are used to filter out logs. Used together with flags. - */ -typedef NS_ENUM(NSUInteger, DDLogLevel){ - /** - * No logs - */ - DDLogLevelOff = 0, - - /** - * Error logs only - */ - DDLogLevelError = (DDLogFlagError), - - /** - * Error and warning logs - */ - DDLogLevelWarning = (DDLogLevelError | DDLogFlagWarning), - - /** - * Error, warning and info logs - */ - DDLogLevelInfo = (DDLogLevelWarning | DDLogFlagInfo), - - /** - * Error, warning, info and debug logs - */ - DDLogLevelDebug = (DDLogLevelInfo | DDLogFlagDebug), - - /** - * Error, warning, info, debug and verbose logs - */ - DDLogLevelVerbose = (DDLogLevelDebug | DDLogFlagVerbose), - - /** - * All logs (1...11111) - */ - DDLogLevelAll = NSUIntegerMax -}; - -/** - * Extracts just the file name, no path or extension - * - * @param filePath input file path - * @param copy YES if we want the result to be copied - * - * @return the file name - */ -NSString * DDExtractFileNameWithoutExtension(const char *filePath, BOOL copy); - -/** - * The THIS_FILE macro gives you an NSString of the file name. - * For simplicity and clarity, the file name does not include the full path or file extension. - * - * For example: DDLogWarn(@"%@: Unable to find thingy", THIS_FILE) -> @"MyViewController: Unable to find thingy" - **/ -#define THIS_FILE (DDExtractFileNameWithoutExtension(__FILE__, NO)) - -/** - * The THIS_METHOD macro gives you the name of the current objective-c method. - * - * For example: DDLogWarn(@"%@ - Requires non-nil strings", THIS_METHOD) -> @"setMake:model: requires non-nil strings" - * - * Note: This does NOT work in straight C functions (non objective-c). - * Instead you should use the predefined __FUNCTION__ macro. - **/ -#define THIS_METHOD NSStringFromSelector(_cmd) - - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -/** - * The main class, exposes all logging mechanisms, loggers, ... - * For most of the users, this class is hidden behind the logging functions like `DDLogInfo` - */ -@interface DDLog : NSObject - -/** - * Provides access to the underlying logging queue. - * This may be helpful to Logger classes for things like thread synchronization. - **/ -+ (dispatch_queue_t)loggingQueue; - -/** - * Logging Primitive. - * - * This method is used by the macros or logging functions. - * It is suggested you stick with the macros as they're easier to use. - * - * @param asynchronous YES if the logging is done async, NO if you want to force sync - * @param level the log level - * @param flag the log flag - * @param context the context (if any is defined) - * @param file the current file - * @param function the current function - * @param line the current code line - * @param tag potential tag - * @param format the log format - */ -+ (void)log:(BOOL)asynchronous - level:(DDLogLevel)level - flag:(DDLogFlag)flag - context:(NSInteger)context - file:(const char *)file - function:(const char *)function - line:(NSUInteger)line - tag:(id)tag - format:(NSString *)format, ... NS_FORMAT_FUNCTION(9,10); - -/** - * Logging Primitive. - * - * This method can be used if you have a prepared va_list. - * Similar to `log:level:flag:context:file:function:line:tag:format:...` - * - * @param asynchronous YES if the logging is done async, NO if you want to force sync - * @param level the log level - * @param flag the log flag - * @param context the context (if any is defined) - * @param file the current file - * @param function the current function - * @param line the current code line - * @param tag potential tag - * @param format the log format - * @param argList the arguments list as a va_list - */ -+ (void)log:(BOOL)asynchronous - level:(DDLogLevel)level - flag:(DDLogFlag)flag - context:(NSInteger)context - file:(const char *)file - function:(const char *)function - line:(NSUInteger)line - tag:(id)tag - format:(NSString *)format - args:(va_list)argList; - -/** - * Logging Primitive. - * - * @param asynchronous YES if the logging is done async, NO if you want to force sync - * @param message the message - * @param level the log level - * @param flag the log flag - * @param context the context (if any is defined) - * @param file the current file - * @param function the current function - * @param line the current code line - * @param tag potential tag - */ -+ (void)log:(BOOL)asynchronous - message:(NSString *)message - level:(DDLogLevel)level - flag:(DDLogFlag)flag - context:(NSInteger)context - file:(const char *)file - function:(const char *)function - line:(NSUInteger)line - tag:(id)tag; - -/** - * Logging Primitive. - * - * This method can be used if you manualy prepared DDLogMessage. - * - * @param asynchronous YES if the logging is done async, NO if you want to force sync - * @param logMessage the log message stored in a `DDLogMessage` model object - */ -+ (void)log:(BOOL)asynchronous - message:(DDLogMessage *)logMessage; - -/** - * Since logging can be asynchronous, there may be times when you want to flush the logs. - * The framework invokes this automatically when the application quits. - **/ -+ (void)flushLog; - -/** - * Loggers - * - * In order for your log statements to go somewhere, you should create and add a logger. - * - * You can add multiple loggers in order to direct your log statements to multiple places. - * And each logger can be configured separately. - * So you could have, for example, verbose logging to the console, but a concise log file with only warnings & errors. - **/ - -/** - * Adds the logger to the system. - * - * This is equivalent to invoking `[DDLog addLogger:logger withLogLevel:DDLogLevelAll]`. - **/ -+ (void)addLogger:(id )logger; - -/** - * Adds the logger to the system. - * - * The level that you provide here is a preemptive filter (for performance). - * That is, the level specified here will be used to filter out logMessages so that - * the logger is never even invoked for the messages. - * - * More information: - * When you issue a log statement, the logging framework iterates over each logger, - * and checks to see if it should forward the logMessage to the logger. - * This check is done using the level parameter passed to this method. - * - * For example: - * - * `[DDLog addLogger:consoleLogger withLogLevel:DDLogLevelVerbose];` - * `[DDLog addLogger:fileLogger withLogLevel:DDLogLevelWarning];` - * - * `DDLogError(@"oh no");` => gets forwarded to consoleLogger & fileLogger - * `DDLogInfo(@"hi");` => gets forwarded to consoleLogger only - * - * It is important to remember that Lumberjack uses a BITMASK. - * Many developers & third party frameworks may define extra log levels & flags. - * For example: - * - * `#define SOME_FRAMEWORK_LOG_FLAG_TRACE (1 << 6) // 0...1000000` - * - * So if you specify `DDLogLevelVerbose` to this method, you won't see the framework's trace messages. - * - * `(SOME_FRAMEWORK_LOG_FLAG_TRACE & DDLogLevelVerbose) => (01000000 & 00011111) => NO` - * - * Consider passing `DDLogLevelAll` to this method, which has all bits set. - * You can also use the exclusive-or bitwise operator to get a bitmask that has all flags set, - * except the ones you explicitly don't want. For example, if you wanted everything except verbose & debug: - * - * `((DDLogLevelAll ^ DDLogLevelVerbose) | DDLogLevelInfo)` - **/ -+ (void)addLogger:(id )logger withLevel:(DDLogLevel)level; - -/** - * Remove the logger from the system - */ -+ (void)removeLogger:(id )logger; - -/** - * Remove all the current loggers - */ -+ (void)removeAllLoggers; - -/** - * Return all the current loggers - */ -+ (NSArray *)allLoggers; - -/** - * Registered Dynamic Logging - * - * These methods allow you to obtain a list of classes that are using registered dynamic logging, - * and also provides methods to get and set their log level during run time. - **/ - -/** - * Returns an array with the classes that are using registered dynamic logging - */ -+ (NSArray *)registeredClasses; - -/** - * Returns an array with the classes names that are using registered dynamic logging - */ -+ (NSArray *)registeredClassNames; - -/** - * Returns the current log level for a certain class - * - * @param aClass `Class` param - */ -+ (DDLogLevel)levelForClass:(Class)aClass; - -/** - * Returns the current log level for a certain class - * - * @param aClassName string param - */ -+ (DDLogLevel)levelForClassWithName:(NSString *)aClassName; - -/** - * Set the log level for a certain class - * - * @param level the new level - * @param aClass `Class` param - */ -+ (void)setLevel:(DDLogLevel)level forClass:(Class)aClass; - -/** - * Set the log level for a certain class - * - * @param level the new level - * @param aClassName string param - */ -+ (void)setLevel:(DDLogLevel)level forClassWithName:(NSString *)aClassName; - -@end - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -/** - * This protocol describes a basic logger behavior. - * Basically, it can log messages, store a logFormatter plus a bunch of optional behaviors. - * (i.e. flush, get its loggerQueue, get its name, ... - */ -@protocol DDLogger - -/** - * The log message method - * - * @param logMessage the message (model) - */ -- (void)logMessage:(DDLogMessage *)logMessage; - -/** - * Formatters may optionally be added to any logger. - * - * If no formatter is set, the logger simply logs the message as it is given in logMessage, - * or it may use its own built in formatting style. - **/ -@property (nonatomic, strong) id logFormatter; - -@optional - -/** - * Since logging is asynchronous, adding and removing loggers is also asynchronous. - * In other words, the loggers are added and removed at appropriate times with regards to log messages. - * - * - Loggers will not receive log messages that were executed prior to when they were added. - * - Loggers will not receive log messages that were executed after they were removed. - * - * These methods are executed in the logging thread/queue. - * This is the same thread/queue that will execute every logMessage: invocation. - * Loggers may use these methods for thread synchronization or other setup/teardown tasks. - **/ -- (void)didAddLogger; - -/** - * See the above description for `didAddLoger` - */ -- (void)willRemoveLogger; - -/** - * Some loggers may buffer IO for optimization purposes. - * For example, a database logger may only save occasionaly as the disk IO is slow. - * In such loggers, this method should be implemented to flush any pending IO. - * - * This allows invocations of DDLog's flushLog method to be propogated to loggers that need it. - * - * Note that DDLog's flushLog method is invoked automatically when the application quits, - * and it may be also invoked manually by the developer prior to application crashes, or other such reasons. - **/ -- (void)flush; - -/** - * Each logger is executed concurrently with respect to the other loggers. - * Thus, a dedicated dispatch queue is used for each logger. - * Logger implementations may optionally choose to provide their own dispatch queue. - **/ -@property (nonatomic, DISPATCH_QUEUE_REFERENCE_TYPE, readonly) dispatch_queue_t loggerQueue; - -/** - * If the logger implementation does not choose to provide its own queue, - * one will automatically be created for it. - * The created queue will receive its name from this method. - * This may be helpful for debugging or profiling reasons. - **/ -@property (nonatomic, readonly) NSString *loggerName; - -@end - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -/** - * This protocol describes the behavior of a log formatter - */ -@protocol DDLogFormatter -@required - -/** - * Formatters may optionally be added to any logger. - * This allows for increased flexibility in the logging environment. - * For example, log messages for log files may be formatted differently than log messages for the console. - * - * For more information about formatters, see the "Custom Formatters" page: - * Documentation/CustomFormatters.md - * - * The formatter may also optionally filter the log message by returning nil, - * in which case the logger will not log the message. - **/ -- (NSString *)formatLogMessage:(DDLogMessage *)logMessage; - -@optional - -/** - * A single formatter instance can be added to multiple loggers. - * These methods provides hooks to notify the formatter of when it's added/removed. - * - * This is primarily for thread-safety. - * If a formatter is explicitly not thread-safe, it may wish to throw an exception if added to multiple loggers. - * Or if a formatter has potentially thread-unsafe code (e.g. NSDateFormatter), - * it could possibly use these hooks to switch to thread-safe versions of the code. - **/ -- (void)didAddToLogger:(id )logger; - -/** - * See the above description for `didAddToLogger:` - */ -- (void)willRemoveFromLogger:(id )logger; - -@end - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -/** - * This protocol describes a dynamic logging component - */ -@protocol DDRegisteredDynamicLogging - -/** - * Implement these methods to allow a file's log level to be managed from a central location. - * - * This is useful if you'd like to be able to change log levels for various parts - * of your code from within the running application. - * - * Imagine pulling up the settings for your application, - * and being able to configure the logging level on a per file basis. - * - * The implementation can be very straight-forward: - * - * ``` - * + (int)ddLogLevel - * { - * return ddLogLevel; - * } - * - * + (void)ddSetLogLevel:(DDLogLevel)level - * { - * ddLogLevel = level; - * } - * ``` - **/ -+ (DDLogLevel)ddLogLevel; - -/** - * See the above description for `ddLogLevel` - */ -+ (void)ddSetLogLevel:(DDLogLevel)level; - -@end - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -#ifndef NS_DESIGNATED_INITIALIZER - #define NS_DESIGNATED_INITIALIZER -#endif - -/** - * Log message options, allow copying certain log elements - */ -typedef NS_OPTIONS(NSInteger, DDLogMessageOptions){ - /** - * Use this to use a copy of the file path - */ - DDLogMessageCopyFile = 1 << 0, - /** - * Use this to use a copy of the function name - */ - DDLogMessageCopyFunction = 1 << 1 -}; - -/** - * The `DDLogMessage` class encapsulates information about the log message. - * If you write custom loggers or formatters, you will be dealing with objects of this class. - **/ -@interface DDLogMessage : NSObject -{ - // Direct accessors to be used only for performance - @public - NSString *_message; - DDLogLevel _level; - DDLogFlag _flag; - NSInteger _context; - NSString *_file; - NSString *_fileName; - NSString *_function; - NSUInteger _line; - id _tag; - DDLogMessageOptions _options; - NSDate *_timestamp; - NSString *_threadID; - NSString *_threadName; - NSString *_queueLabel; -} - -/** - * Default `init` is not available - */ -- (instancetype)init NS_UNAVAILABLE; - -/** - * Standard init method for a log message object. - * Used by the logging primitives. (And the macros use the logging primitives.) - * - * If you find need to manually create logMessage objects, there is one thing you should be aware of: - * - * If no flags are passed, the method expects the file and function parameters to be string literals. - * That is, it expects the given strings to exist for the duration of the object's lifetime, - * and it expects the given strings to be immutable. - * In other words, it does not copy these strings, it simply points to them. - * This is due to the fact that __FILE__ and __FUNCTION__ are usually used to specify these parameters, - * so it makes sense to optimize and skip the unnecessary allocations. - * However, if you need them to be copied you may use the options parameter to specify this. - * - * @param message the message - * @param level the log level - * @param flag the log flag - * @param context the context (if any is defined) - * @param file the current file - * @param function the current function - * @param line the current code line - * @param tag potential tag - * @param options a bitmask which supports DDLogMessageCopyFile and DDLogMessageCopyFunction. - * @param timestamp the log timestamp - * - * @return a new instance of a log message model object - */ -- (instancetype)initWithMessage:(NSString *)message - level:(DDLogLevel)level - flag:(DDLogFlag)flag - context:(NSInteger)context - file:(NSString *)file - function:(NSString *)function - line:(NSUInteger)line - tag:(id)tag - options:(DDLogMessageOptions)options - timestamp:(NSDate *)timestamp NS_DESIGNATED_INITIALIZER; - -/** - * Read-only properties - **/ - -/** - * The log message - */ -@property (readonly, nonatomic) NSString *message; -@property (readonly, nonatomic) DDLogLevel level; -@property (readonly, nonatomic) DDLogFlag flag; -@property (readonly, nonatomic) NSInteger context; -@property (readonly, nonatomic) NSString *file; -@property (readonly, nonatomic) NSString *fileName; -@property (readonly, nonatomic) NSString *function; -@property (readonly, nonatomic) NSUInteger line; -@property (readonly, nonatomic) id tag; -@property (readonly, nonatomic) DDLogMessageOptions options; -@property (readonly, nonatomic) NSDate *timestamp; -@property (readonly, nonatomic) NSString *threadID; // ID as it appears in NSLog calculated from the machThreadID -@property (readonly, nonatomic) NSString *threadName; -@property (readonly, nonatomic) NSString *queueLabel; - -@end - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -/** - * The `DDLogger` protocol specifies that an optional formatter can be added to a logger. - * Most (but not all) loggers will want to support formatters. - * - * However, writting getters and setters in a thread safe manner, - * while still maintaining maximum speed for the logging process, is a difficult task. - * - * To do it right, the implementation of the getter/setter has strict requiremenets: - * - Must NOT require the `logMessage:` method to acquire a lock. - * - Must NOT require the `logMessage:` method to access an atomic property (also a lock of sorts). - * - * To simplify things, an abstract logger is provided that implements the getter and setter. - * - * Logger implementations may simply extend this class, - * and they can ACCESS THE FORMATTER VARIABLE DIRECTLY from within their `logMessage:` method! - **/ -@interface DDAbstractLogger : NSObject -{ - // Direct accessors to be used only for performance - @public - id _logFormatter; - dispatch_queue_t _loggerQueue; -} - -@property (nonatomic, strong) id logFormatter; -@property (nonatomic, DISPATCH_QUEUE_REFERENCE_TYPE) dispatch_queue_t loggerQueue; - -// For thread-safety assertions - -/** - * Return YES if the current logger uses a global queue for logging - */ -@property (nonatomic, readonly, getter=isOnGlobalLoggingQueue) BOOL onGlobalLoggingQueue; - -/** - * Return YES if the current logger uses the internal designated queue for logging - */ -@property (nonatomic, readonly, getter=isOnInternalLoggerQueue) BOOL onInternalLoggerQueue; - -@end - diff --git a/Example/Pods/CocoaLumberjack/Classes/DDLogMacros.h b/Example/Pods/CocoaLumberjack/Classes/DDLogMacros.h deleted file mode 100644 index 975d00a..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/DDLogMacros.h +++ /dev/null @@ -1,82 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -// Disable legacy macros -#ifndef DD_LEGACY_MACROS - #define DD_LEGACY_MACROS 0 -#endif - -#import "DDLog.h" - -/** - * The constant/variable/method responsible for controlling the current log level. - **/ -#ifndef LOG_LEVEL_DEF - #define LOG_LEVEL_DEF ddLogLevel -#endif - -/** - * Whether async should be used by log messages, excluding error messages that are always sent sync. - **/ -#ifndef LOG_ASYNC_ENABLED - #define LOG_ASYNC_ENABLED YES -#endif - -/** - * This is the single macro that all other macros below compile into. - * This big multiline macro makes all the other macros easier to read. - **/ -#define LOG_MACRO(isAsynchronous, lvl, flg, ctx, atag, fnct, frmt, ...) \ - [DDLog log : isAsynchronous \ - level : lvl \ - flag : flg \ - context : ctx \ - file : __FILE__ \ - function : fnct \ - line : __LINE__ \ - tag : atag \ - format : (frmt), ## __VA_ARGS__] - -/** - * Define version of the macro that only execute if the log level is above the threshold. - * The compiled versions essentially look like this: - * - * if (logFlagForThisLogMsg & ddLogLevel) { execute log message } - * - * When LOG_LEVEL_DEF is defined as ddLogLevel. - * - * As shown further below, Lumberjack actually uses a bitmask as opposed to primitive log levels. - * This allows for a great amount of flexibility and some pretty advanced fine grained logging techniques. - * - * Note that when compiler optimizations are enabled (as they are for your release builds), - * the log messages above your logging threshold will automatically be compiled out. - * - * (If the compiler sees LOG_LEVEL_DEF/ddLogLevel declared as a constant, the compiler simply checks to see - * if the 'if' statement would execute, and if not it strips it from the binary.) - * - * We also define shorthand versions for asynchronous and synchronous logging. - **/ -#define LOG_MAYBE(async, lvl, flg, ctx, tag, fnct, frmt, ...) \ - do { if(lvl & flg) LOG_MACRO(async, lvl, flg, ctx, tag, fnct, frmt, ##__VA_ARGS__); } while(0) - -/** - * Ready to use log macros with no context or tag. - **/ -#define DDLogError(frmt, ...) LOG_MAYBE(NO, LOG_LEVEL_DEF, DDLogFlagError, 0, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__) -#define DDLogWarn(frmt, ...) LOG_MAYBE(LOG_ASYNC_ENABLED, LOG_LEVEL_DEF, DDLogFlagWarning, 0, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__) -#define DDLogInfo(frmt, ...) LOG_MAYBE(LOG_ASYNC_ENABLED, LOG_LEVEL_DEF, DDLogFlagInfo, 0, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__) -#define DDLogDebug(frmt, ...) LOG_MAYBE(LOG_ASYNC_ENABLED, LOG_LEVEL_DEF, DDLogFlagDebug, 0, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__) -#define DDLogVerbose(frmt, ...) LOG_MAYBE(LOG_ASYNC_ENABLED, LOG_LEVEL_DEF, DDLogFlagVerbose, 0, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__) - diff --git a/Example/Pods/CocoaLumberjack/Classes/DDTTYLogger.m b/Example/Pods/CocoaLumberjack/Classes/DDTTYLogger.m deleted file mode 100644 index 41592ca..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/DDTTYLogger.m +++ /dev/null @@ -1,1481 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -#import "DDTTYLogger.h" - -#import -#import - -#if !__has_feature(objc_arc) -#error This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). -#endif - -// We probably shouldn't be using DDLog() statements within the DDLog implementation. -// But we still want to leave our log statements for any future debugging, -// and to allow other developers to trace the implementation (which is a great learning tool). -// -// So we use primitive logging macros around NSLog. -// We maintain the NS prefix on the macros to be explicit about the fact that we're using NSLog. - -#ifndef DD_NSLOG_LEVEL - #define DD_NSLOG_LEVEL 2 -#endif - -#define NSLogError(frmt, ...) do{ if(DD_NSLOG_LEVEL >= 1) NSLog((frmt), ##__VA_ARGS__); } while(0) -#define NSLogWarn(frmt, ...) do{ if(DD_NSLOG_LEVEL >= 2) NSLog((frmt), ##__VA_ARGS__); } while(0) -#define NSLogInfo(frmt, ...) do{ if(DD_NSLOG_LEVEL >= 3) NSLog((frmt), ##__VA_ARGS__); } while(0) -#define NSLogDebug(frmt, ...) do{ if(DD_NSLOG_LEVEL >= 4) NSLog((frmt), ##__VA_ARGS__); } while(0) -#define NSLogVerbose(frmt, ...) do{ if(DD_NSLOG_LEVEL >= 5) NSLog((frmt), ##__VA_ARGS__); } while(0) - -// Xcode does NOT natively support colors in the Xcode debugging console. -// You'll need to install the XcodeColors plugin to see colors in the Xcode console. -// https://github.com/robbiehanson/XcodeColors -// -// The following is documentation from the XcodeColors project: -// -// -// How to apply color formatting to your log statements: -// -// To set the foreground color: -// Insert the ESCAPE_SEQ into your string, followed by "fg124,12,255;" where r=124, g=12, b=255. -// -// To set the background color: -// Insert the ESCAPE_SEQ into your string, followed by "bg12,24,36;" where r=12, g=24, b=36. -// -// To reset the foreground color (to default value): -// Insert the ESCAPE_SEQ into your string, followed by "fg;" -// -// To reset the background color (to default value): -// Insert the ESCAPE_SEQ into your string, followed by "bg;" -// -// To reset the foreground and background color (to default values) in one operation: -// Insert the ESCAPE_SEQ into your string, followed by ";" - -#define XCODE_COLORS_ESCAPE_SEQ "\033[" - -#define XCODE_COLORS_RESET_FG XCODE_COLORS_ESCAPE_SEQ "fg;" // Clear any foreground color -#define XCODE_COLORS_RESET_BG XCODE_COLORS_ESCAPE_SEQ "bg;" // Clear any background color -#define XCODE_COLORS_RESET XCODE_COLORS_ESCAPE_SEQ ";" // Clear any foreground or background color - -// If running in a shell, not all RGB colors will be supported. -// In this case we automatically map to the closest available color. -// In order to provide this mapping, we have a hard-coded set of the standard RGB values available in the shell. -// However, not every shell is the same, and Apple likes to think different even when it comes to shell colors. -// -// Map to standard Terminal.app colors (1), or -// map to standard xterm colors (0). - -#define MAP_TO_TERMINAL_APP_COLORS 1 - - -@interface DDTTYLoggerColorProfile : NSObject { - @public - DDLogFlag mask; - NSInteger context; - - uint8_t fg_r; - uint8_t fg_g; - uint8_t fg_b; - - uint8_t bg_r; - uint8_t bg_g; - uint8_t bg_b; - - NSUInteger fgCodeIndex; - NSString *fgCodeRaw; - - NSUInteger bgCodeIndex; - NSString *bgCodeRaw; - - char fgCode[24]; - size_t fgCodeLen; - - char bgCode[24]; - size_t bgCodeLen; - - char resetCode[8]; - size_t resetCodeLen; -} - -- (instancetype)initWithForegroundColor:(DDColor *)fgColor backgroundColor:(DDColor *)bgColor flag:(DDLogFlag)mask context:(NSInteger)ctxt; - -@end - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -@interface DDTTYLogger () { - NSUInteger _calendarUnitFlags; - - NSString *_appName; - char *_app; - size_t _appLen; - - NSString *_processID; - char *_pid; - size_t _pidLen; - - BOOL _colorsEnabled; - NSMutableArray *_colorProfilesArray; - NSMutableDictionary *_colorProfilesDict; -} - -@end - - -@implementation DDTTYLogger - -static BOOL isaColorTTY; -static BOOL isaColor256TTY; -static BOOL isaXcodeColorTTY; - -static NSArray *codes_fg = nil; -static NSArray *codes_bg = nil; -static NSArray *colors = nil; - -static DDTTYLogger *sharedInstance; - -/** - * Initializes the colors array, as well as the codes_fg and codes_bg arrays, for 16 color mode. - * - * This method is used when the application is running from within a shell that only supports 16 color mode. - * This method is not invoked if the application is running within Xcode, or via normal UI app launch. - **/ -+ (void)initialize_colors_16 { - if (codes_fg || codes_bg || colors) { - return; - } - - NSMutableArray *m_codes_fg = [NSMutableArray arrayWithCapacity:16]; - NSMutableArray *m_codes_bg = [NSMutableArray arrayWithCapacity:16]; - NSMutableArray *m_colors = [NSMutableArray arrayWithCapacity:16]; - - // In a standard shell only 16 colors are supported. - // - // More information about ansi escape codes can be found online. - // http://en.wikipedia.org/wiki/ANSI_escape_code - - [m_codes_fg addObject:@"30m"]; // normal - black - [m_codes_fg addObject:@"31m"]; // normal - red - [m_codes_fg addObject:@"32m"]; // normal - green - [m_codes_fg addObject:@"33m"]; // normal - yellow - [m_codes_fg addObject:@"34m"]; // normal - blue - [m_codes_fg addObject:@"35m"]; // normal - magenta - [m_codes_fg addObject:@"36m"]; // normal - cyan - [m_codes_fg addObject:@"37m"]; // normal - gray - [m_codes_fg addObject:@"1;30m"]; // bright - darkgray - [m_codes_fg addObject:@"1;31m"]; // bright - red - [m_codes_fg addObject:@"1;32m"]; // bright - green - [m_codes_fg addObject:@"1;33m"]; // bright - yellow - [m_codes_fg addObject:@"1;34m"]; // bright - blue - [m_codes_fg addObject:@"1;35m"]; // bright - magenta - [m_codes_fg addObject:@"1;36m"]; // bright - cyan - [m_codes_fg addObject:@"1;37m"]; // bright - white - - [m_codes_bg addObject:@"40m"]; // normal - black - [m_codes_bg addObject:@"41m"]; // normal - red - [m_codes_bg addObject:@"42m"]; // normal - green - [m_codes_bg addObject:@"43m"]; // normal - yellow - [m_codes_bg addObject:@"44m"]; // normal - blue - [m_codes_bg addObject:@"45m"]; // normal - magenta - [m_codes_bg addObject:@"46m"]; // normal - cyan - [m_codes_bg addObject:@"47m"]; // normal - gray - [m_codes_bg addObject:@"1;40m"]; // bright - darkgray - [m_codes_bg addObject:@"1;41m"]; // bright - red - [m_codes_bg addObject:@"1;42m"]; // bright - green - [m_codes_bg addObject:@"1;43m"]; // bright - yellow - [m_codes_bg addObject:@"1;44m"]; // bright - blue - [m_codes_bg addObject:@"1;45m"]; // bright - magenta - [m_codes_bg addObject:@"1;46m"]; // bright - cyan - [m_codes_bg addObject:@"1;47m"]; // bright - white - -#if MAP_TO_TERMINAL_APP_COLORS - - // Standard Terminal.app colors: - // - // These are the default colors used by Apple's Terminal.app. - - [m_colors addObject:DDMakeColor( 0, 0, 0)]; // normal - black - [m_colors addObject:DDMakeColor(194, 54, 33)]; // normal - red - [m_colors addObject:DDMakeColor( 37, 188, 36)]; // normal - green - [m_colors addObject:DDMakeColor(173, 173, 39)]; // normal - yellow - [m_colors addObject:DDMakeColor( 73, 46, 225)]; // normal - blue - [m_colors addObject:DDMakeColor(211, 56, 211)]; // normal - magenta - [m_colors addObject:DDMakeColor( 51, 187, 200)]; // normal - cyan - [m_colors addObject:DDMakeColor(203, 204, 205)]; // normal - gray - [m_colors addObject:DDMakeColor(129, 131, 131)]; // bright - darkgray - [m_colors addObject:DDMakeColor(252, 57, 31)]; // bright - red - [m_colors addObject:DDMakeColor( 49, 231, 34)]; // bright - green - [m_colors addObject:DDMakeColor(234, 236, 35)]; // bright - yellow - [m_colors addObject:DDMakeColor( 88, 51, 255)]; // bright - blue - [m_colors addObject:DDMakeColor(249, 53, 248)]; // bright - magenta - [m_colors addObject:DDMakeColor( 20, 240, 240)]; // bright - cyan - [m_colors addObject:DDMakeColor(233, 235, 235)]; // bright - white - -#else /* if MAP_TO_TERMINAL_APP_COLORS */ - - // Standard xterm colors: - // - // These are the default colors used by most xterm shells. - - [m_colors addObject:DDMakeColor( 0, 0, 0)]; // normal - black - [m_colors addObject:DDMakeColor(205, 0, 0)]; // normal - red - [m_colors addObject:DDMakeColor( 0, 205, 0)]; // normal - green - [m_colors addObject:DDMakeColor(205, 205, 0)]; // normal - yellow - [m_colors addObject:DDMakeColor( 0, 0, 238)]; // normal - blue - [m_colors addObject:DDMakeColor(205, 0, 205)]; // normal - magenta - [m_colors addObject:DDMakeColor( 0, 205, 205)]; // normal - cyan - [m_colors addObject:DDMakeColor(229, 229, 229)]; // normal - gray - [m_colors addObject:DDMakeColor(127, 127, 127)]; // bright - darkgray - [m_colors addObject:DDMakeColor(255, 0, 0)]; // bright - red - [m_colors addObject:DDMakeColor( 0, 255, 0)]; // bright - green - [m_colors addObject:DDMakeColor(255, 255, 0)]; // bright - yellow - [m_colors addObject:DDMakeColor( 92, 92, 255)]; // bright - blue - [m_colors addObject:DDMakeColor(255, 0, 255)]; // bright - magenta - [m_colors addObject:DDMakeColor( 0, 255, 255)]; // bright - cyan - [m_colors addObject:DDMakeColor(255, 255, 255)]; // bright - white - -#endif /* if MAP_TO_TERMINAL_APP_COLORS */ - - codes_fg = [m_codes_fg copy]; - codes_bg = [m_codes_bg copy]; - colors = [m_colors copy]; - - NSAssert([codes_fg count] == [codes_bg count], @"Invalid colors/codes array(s)"); - NSAssert([codes_fg count] == [colors count], @"Invalid colors/codes array(s)"); -} - -/** - * Initializes the colors array, as well as the codes_fg and codes_bg arrays, for 256 color mode. - * - * This method is used when the application is running from within a shell that supports 256 color mode. - * This method is not invoked if the application is running within Xcode, or via normal UI app launch. - **/ -+ (void)initialize_colors_256 { - if (codes_fg || codes_bg || colors) { - return; - } - - NSMutableArray *m_codes_fg = [NSMutableArray arrayWithCapacity:(256 - 16)]; - NSMutableArray *m_codes_bg = [NSMutableArray arrayWithCapacity:(256 - 16)]; - NSMutableArray *m_colors = [NSMutableArray arrayWithCapacity:(256 - 16)]; - - #if MAP_TO_TERMINAL_APP_COLORS - - // Standard Terminal.app colors: - // - // These are the colors the Terminal.app uses in xterm-256color mode. - // In this mode, the terminal supports 256 different colors, specified by 256 color codes. - // - // The first 16 color codes map to the original 16 color codes supported by the earlier xterm-color mode. - // These are actually configurable, and thus we ignore them for the purposes of mapping, - // as we can't rely on them being constant. They are largely duplicated anyway. - // - // The next 216 color codes are designed to run the spectrum, with several shades of every color. - // While the color codes are standardized, the actual RGB values for each color code is not. - // Apple's Terminal.app uses different RGB values from that of a standard xterm. - // Apple's choices in colors are designed to be a little nicer on the eyes. - // - // The last 24 color codes represent a grayscale. - // - // Unfortunately, unlike the standard xterm color chart, - // Apple's RGB values cannot be calculated using a simple formula (at least not that I know of). - // Also, I don't know of any ways to programmatically query the shell for the RGB values. - // So this big giant color chart had to be made by hand. - // - // More information about ansi escape codes can be found online. - // http://en.wikipedia.org/wiki/ANSI_escape_code - - // Colors - - [m_colors addObject:DDMakeColor( 47, 49, 49)]; - [m_colors addObject:DDMakeColor( 60, 42, 144)]; - [m_colors addObject:DDMakeColor( 66, 44, 183)]; - [m_colors addObject:DDMakeColor( 73, 46, 222)]; - [m_colors addObject:DDMakeColor( 81, 50, 253)]; - [m_colors addObject:DDMakeColor( 88, 51, 255)]; - - [m_colors addObject:DDMakeColor( 42, 128, 37)]; - [m_colors addObject:DDMakeColor( 42, 127, 128)]; - [m_colors addObject:DDMakeColor( 44, 126, 169)]; - [m_colors addObject:DDMakeColor( 56, 125, 209)]; - [m_colors addObject:DDMakeColor( 59, 124, 245)]; - [m_colors addObject:DDMakeColor( 66, 123, 255)]; - - [m_colors addObject:DDMakeColor( 51, 163, 41)]; - [m_colors addObject:DDMakeColor( 39, 162, 121)]; - [m_colors addObject:DDMakeColor( 42, 161, 162)]; - [m_colors addObject:DDMakeColor( 53, 160, 202)]; - [m_colors addObject:DDMakeColor( 45, 159, 240)]; - [m_colors addObject:DDMakeColor( 58, 158, 255)]; - - [m_colors addObject:DDMakeColor( 31, 196, 37)]; - [m_colors addObject:DDMakeColor( 48, 196, 115)]; - [m_colors addObject:DDMakeColor( 39, 195, 155)]; - [m_colors addObject:DDMakeColor( 49, 195, 195)]; - [m_colors addObject:DDMakeColor( 32, 194, 235)]; - [m_colors addObject:DDMakeColor( 53, 193, 255)]; - - [m_colors addObject:DDMakeColor( 50, 229, 35)]; - [m_colors addObject:DDMakeColor( 40, 229, 109)]; - [m_colors addObject:DDMakeColor( 27, 229, 149)]; - [m_colors addObject:DDMakeColor( 49, 228, 189)]; - [m_colors addObject:DDMakeColor( 33, 228, 228)]; - [m_colors addObject:DDMakeColor( 53, 227, 255)]; - - [m_colors addObject:DDMakeColor( 27, 254, 30)]; - [m_colors addObject:DDMakeColor( 30, 254, 103)]; - [m_colors addObject:DDMakeColor( 45, 254, 143)]; - [m_colors addObject:DDMakeColor( 38, 253, 182)]; - [m_colors addObject:DDMakeColor( 38, 253, 222)]; - [m_colors addObject:DDMakeColor( 42, 253, 252)]; - - [m_colors addObject:DDMakeColor(140, 48, 40)]; - [m_colors addObject:DDMakeColor(136, 51, 136)]; - [m_colors addObject:DDMakeColor(135, 52, 177)]; - [m_colors addObject:DDMakeColor(134, 52, 217)]; - [m_colors addObject:DDMakeColor(135, 56, 248)]; - [m_colors addObject:DDMakeColor(134, 53, 255)]; - - [m_colors addObject:DDMakeColor(125, 125, 38)]; - [m_colors addObject:DDMakeColor(124, 125, 125)]; - [m_colors addObject:DDMakeColor(122, 124, 166)]; - [m_colors addObject:DDMakeColor(123, 124, 207)]; - [m_colors addObject:DDMakeColor(123, 122, 247)]; - [m_colors addObject:DDMakeColor(124, 121, 255)]; - - [m_colors addObject:DDMakeColor(119, 160, 35)]; - [m_colors addObject:DDMakeColor(117, 160, 120)]; - [m_colors addObject:DDMakeColor(117, 160, 160)]; - [m_colors addObject:DDMakeColor(115, 159, 201)]; - [m_colors addObject:DDMakeColor(116, 158, 240)]; - [m_colors addObject:DDMakeColor(117, 157, 255)]; - - [m_colors addObject:DDMakeColor(113, 195, 39)]; - [m_colors addObject:DDMakeColor(110, 194, 114)]; - [m_colors addObject:DDMakeColor(111, 194, 154)]; - [m_colors addObject:DDMakeColor(108, 194, 194)]; - [m_colors addObject:DDMakeColor(109, 193, 234)]; - [m_colors addObject:DDMakeColor(108, 192, 255)]; - - [m_colors addObject:DDMakeColor(105, 228, 30)]; - [m_colors addObject:DDMakeColor(103, 228, 109)]; - [m_colors addObject:DDMakeColor(105, 228, 148)]; - [m_colors addObject:DDMakeColor(100, 227, 188)]; - [m_colors addObject:DDMakeColor( 99, 227, 227)]; - [m_colors addObject:DDMakeColor( 99, 226, 253)]; - - [m_colors addObject:DDMakeColor( 92, 253, 34)]; - [m_colors addObject:DDMakeColor( 96, 253, 103)]; - [m_colors addObject:DDMakeColor( 97, 253, 142)]; - [m_colors addObject:DDMakeColor( 88, 253, 182)]; - [m_colors addObject:DDMakeColor( 93, 253, 221)]; - [m_colors addObject:DDMakeColor( 88, 254, 251)]; - - [m_colors addObject:DDMakeColor(177, 53, 34)]; - [m_colors addObject:DDMakeColor(174, 54, 131)]; - [m_colors addObject:DDMakeColor(172, 55, 172)]; - [m_colors addObject:DDMakeColor(171, 57, 213)]; - [m_colors addObject:DDMakeColor(170, 55, 249)]; - [m_colors addObject:DDMakeColor(170, 57, 255)]; - - [m_colors addObject:DDMakeColor(165, 123, 37)]; - [m_colors addObject:DDMakeColor(163, 123, 123)]; - [m_colors addObject:DDMakeColor(162, 123, 164)]; - [m_colors addObject:DDMakeColor(161, 122, 205)]; - [m_colors addObject:DDMakeColor(161, 121, 241)]; - [m_colors addObject:DDMakeColor(161, 121, 255)]; - - [m_colors addObject:DDMakeColor(158, 159, 33)]; - [m_colors addObject:DDMakeColor(157, 158, 118)]; - [m_colors addObject:DDMakeColor(157, 158, 159)]; - [m_colors addObject:DDMakeColor(155, 157, 199)]; - [m_colors addObject:DDMakeColor(155, 157, 239)]; - [m_colors addObject:DDMakeColor(154, 156, 255)]; - - [m_colors addObject:DDMakeColor(152, 193, 40)]; - [m_colors addObject:DDMakeColor(151, 193, 113)]; - [m_colors addObject:DDMakeColor(150, 193, 153)]; - [m_colors addObject:DDMakeColor(150, 192, 193)]; - [m_colors addObject:DDMakeColor(148, 192, 232)]; - [m_colors addObject:DDMakeColor(149, 191, 253)]; - - [m_colors addObject:DDMakeColor(146, 227, 28)]; - [m_colors addObject:DDMakeColor(144, 227, 108)]; - [m_colors addObject:DDMakeColor(144, 227, 147)]; - [m_colors addObject:DDMakeColor(144, 227, 187)]; - [m_colors addObject:DDMakeColor(142, 226, 227)]; - [m_colors addObject:DDMakeColor(142, 225, 252)]; - - [m_colors addObject:DDMakeColor(138, 253, 36)]; - [m_colors addObject:DDMakeColor(137, 253, 102)]; - [m_colors addObject:DDMakeColor(136, 253, 141)]; - [m_colors addObject:DDMakeColor(138, 254, 181)]; - [m_colors addObject:DDMakeColor(135, 255, 220)]; - [m_colors addObject:DDMakeColor(133, 255, 250)]; - - [m_colors addObject:DDMakeColor(214, 57, 30)]; - [m_colors addObject:DDMakeColor(211, 59, 126)]; - [m_colors addObject:DDMakeColor(209, 57, 168)]; - [m_colors addObject:DDMakeColor(208, 55, 208)]; - [m_colors addObject:DDMakeColor(207, 58, 247)]; - [m_colors addObject:DDMakeColor(206, 61, 255)]; - - [m_colors addObject:DDMakeColor(204, 121, 32)]; - [m_colors addObject:DDMakeColor(202, 121, 121)]; - [m_colors addObject:DDMakeColor(201, 121, 161)]; - [m_colors addObject:DDMakeColor(200, 120, 202)]; - [m_colors addObject:DDMakeColor(200, 120, 241)]; - [m_colors addObject:DDMakeColor(198, 119, 255)]; - - [m_colors addObject:DDMakeColor(198, 157, 37)]; - [m_colors addObject:DDMakeColor(196, 157, 116)]; - [m_colors addObject:DDMakeColor(195, 156, 157)]; - [m_colors addObject:DDMakeColor(195, 156, 197)]; - [m_colors addObject:DDMakeColor(194, 155, 236)]; - [m_colors addObject:DDMakeColor(193, 155, 255)]; - - [m_colors addObject:DDMakeColor(191, 192, 36)]; - [m_colors addObject:DDMakeColor(190, 191, 112)]; - [m_colors addObject:DDMakeColor(189, 191, 152)]; - [m_colors addObject:DDMakeColor(189, 191, 191)]; - [m_colors addObject:DDMakeColor(188, 190, 230)]; - [m_colors addObject:DDMakeColor(187, 190, 253)]; - - [m_colors addObject:DDMakeColor(185, 226, 28)]; - [m_colors addObject:DDMakeColor(184, 226, 106)]; - [m_colors addObject:DDMakeColor(183, 225, 146)]; - [m_colors addObject:DDMakeColor(183, 225, 186)]; - [m_colors addObject:DDMakeColor(182, 225, 225)]; - [m_colors addObject:DDMakeColor(181, 224, 252)]; - - [m_colors addObject:DDMakeColor(178, 255, 35)]; - [m_colors addObject:DDMakeColor(178, 255, 101)]; - [m_colors addObject:DDMakeColor(177, 254, 141)]; - [m_colors addObject:DDMakeColor(176, 254, 180)]; - [m_colors addObject:DDMakeColor(176, 254, 220)]; - [m_colors addObject:DDMakeColor(175, 253, 249)]; - - [m_colors addObject:DDMakeColor(247, 56, 30)]; - [m_colors addObject:DDMakeColor(245, 57, 122)]; - [m_colors addObject:DDMakeColor(243, 59, 163)]; - [m_colors addObject:DDMakeColor(244, 60, 204)]; - [m_colors addObject:DDMakeColor(242, 59, 241)]; - [m_colors addObject:DDMakeColor(240, 55, 255)]; - - [m_colors addObject:DDMakeColor(241, 119, 36)]; - [m_colors addObject:DDMakeColor(240, 120, 118)]; - [m_colors addObject:DDMakeColor(238, 119, 158)]; - [m_colors addObject:DDMakeColor(237, 119, 199)]; - [m_colors addObject:DDMakeColor(237, 118, 238)]; - [m_colors addObject:DDMakeColor(236, 118, 255)]; - - [m_colors addObject:DDMakeColor(235, 154, 36)]; - [m_colors addObject:DDMakeColor(235, 154, 114)]; - [m_colors addObject:DDMakeColor(234, 154, 154)]; - [m_colors addObject:DDMakeColor(232, 154, 194)]; - [m_colors addObject:DDMakeColor(232, 153, 234)]; - [m_colors addObject:DDMakeColor(232, 153, 255)]; - - [m_colors addObject:DDMakeColor(230, 190, 30)]; - [m_colors addObject:DDMakeColor(229, 189, 110)]; - [m_colors addObject:DDMakeColor(228, 189, 150)]; - [m_colors addObject:DDMakeColor(227, 189, 190)]; - [m_colors addObject:DDMakeColor(227, 189, 229)]; - [m_colors addObject:DDMakeColor(226, 188, 255)]; - - [m_colors addObject:DDMakeColor(224, 224, 35)]; - [m_colors addObject:DDMakeColor(223, 224, 105)]; - [m_colors addObject:DDMakeColor(222, 224, 144)]; - [m_colors addObject:DDMakeColor(222, 223, 184)]; - [m_colors addObject:DDMakeColor(222, 223, 224)]; - [m_colors addObject:DDMakeColor(220, 223, 253)]; - - [m_colors addObject:DDMakeColor(217, 253, 28)]; - [m_colors addObject:DDMakeColor(217, 253, 99)]; - [m_colors addObject:DDMakeColor(216, 252, 139)]; - [m_colors addObject:DDMakeColor(216, 252, 179)]; - [m_colors addObject:DDMakeColor(215, 252, 218)]; - [m_colors addObject:DDMakeColor(215, 251, 250)]; - - [m_colors addObject:DDMakeColor(255, 61, 30)]; - [m_colors addObject:DDMakeColor(255, 60, 118)]; - [m_colors addObject:DDMakeColor(255, 58, 159)]; - [m_colors addObject:DDMakeColor(255, 56, 199)]; - [m_colors addObject:DDMakeColor(255, 55, 238)]; - [m_colors addObject:DDMakeColor(255, 59, 255)]; - - [m_colors addObject:DDMakeColor(255, 117, 29)]; - [m_colors addObject:DDMakeColor(255, 117, 115)]; - [m_colors addObject:DDMakeColor(255, 117, 155)]; - [m_colors addObject:DDMakeColor(255, 117, 195)]; - [m_colors addObject:DDMakeColor(255, 116, 235)]; - [m_colors addObject:DDMakeColor(254, 116, 255)]; - - [m_colors addObject:DDMakeColor(255, 152, 27)]; - [m_colors addObject:DDMakeColor(255, 152, 111)]; - [m_colors addObject:DDMakeColor(254, 152, 152)]; - [m_colors addObject:DDMakeColor(255, 152, 192)]; - [m_colors addObject:DDMakeColor(254, 151, 231)]; - [m_colors addObject:DDMakeColor(253, 151, 253)]; - - [m_colors addObject:DDMakeColor(255, 187, 33)]; - [m_colors addObject:DDMakeColor(253, 187, 107)]; - [m_colors addObject:DDMakeColor(252, 187, 148)]; - [m_colors addObject:DDMakeColor(253, 187, 187)]; - [m_colors addObject:DDMakeColor(254, 187, 227)]; - [m_colors addObject:DDMakeColor(252, 186, 252)]; - - [m_colors addObject:DDMakeColor(252, 222, 34)]; - [m_colors addObject:DDMakeColor(251, 222, 103)]; - [m_colors addObject:DDMakeColor(251, 222, 143)]; - [m_colors addObject:DDMakeColor(250, 222, 182)]; - [m_colors addObject:DDMakeColor(251, 221, 222)]; - [m_colors addObject:DDMakeColor(252, 221, 252)]; - - [m_colors addObject:DDMakeColor(251, 252, 15)]; - [m_colors addObject:DDMakeColor(251, 252, 97)]; - [m_colors addObject:DDMakeColor(249, 252, 137)]; - [m_colors addObject:DDMakeColor(247, 252, 177)]; - [m_colors addObject:DDMakeColor(247, 253, 217)]; - [m_colors addObject:DDMakeColor(254, 255, 255)]; - - // Grayscale - - [m_colors addObject:DDMakeColor( 52, 53, 53)]; - [m_colors addObject:DDMakeColor( 57, 58, 59)]; - [m_colors addObject:DDMakeColor( 66, 67, 67)]; - [m_colors addObject:DDMakeColor( 75, 76, 76)]; - [m_colors addObject:DDMakeColor( 83, 85, 85)]; - [m_colors addObject:DDMakeColor( 92, 93, 94)]; - - [m_colors addObject:DDMakeColor(101, 102, 102)]; - [m_colors addObject:DDMakeColor(109, 111, 111)]; - [m_colors addObject:DDMakeColor(118, 119, 119)]; - [m_colors addObject:DDMakeColor(126, 127, 128)]; - [m_colors addObject:DDMakeColor(134, 136, 136)]; - [m_colors addObject:DDMakeColor(143, 144, 145)]; - - [m_colors addObject:DDMakeColor(151, 152, 153)]; - [m_colors addObject:DDMakeColor(159, 161, 161)]; - [m_colors addObject:DDMakeColor(167, 169, 169)]; - [m_colors addObject:DDMakeColor(176, 177, 177)]; - [m_colors addObject:DDMakeColor(184, 185, 186)]; - [m_colors addObject:DDMakeColor(192, 193, 194)]; - - [m_colors addObject:DDMakeColor(200, 201, 202)]; - [m_colors addObject:DDMakeColor(208, 209, 210)]; - [m_colors addObject:DDMakeColor(216, 218, 218)]; - [m_colors addObject:DDMakeColor(224, 226, 226)]; - [m_colors addObject:DDMakeColor(232, 234, 234)]; - [m_colors addObject:DDMakeColor(240, 242, 242)]; - - // Color codes - - int index = 16; - - while (index < 256) { - [m_codes_fg addObject:[NSString stringWithFormat:@"38;5;%dm", index]]; - [m_codes_bg addObject:[NSString stringWithFormat:@"48;5;%dm", index]]; - - index++; - } - - #else /* if MAP_TO_TERMINAL_APP_COLORS */ - - // Standard xterm colors: - // - // These are the colors xterm shells use in xterm-256color mode. - // In this mode, the shell supports 256 different colors, specified by 256 color codes. - // - // The first 16 color codes map to the original 16 color codes supported by the earlier xterm-color mode. - // These are generally configurable, and thus we ignore them for the purposes of mapping, - // as we can't rely on them being constant. They are largely duplicated anyway. - // - // The next 216 color codes are designed to run the spectrum, with several shades of every color. - // The last 24 color codes represent a grayscale. - // - // While the color codes are standardized, the actual RGB values for each color code is not. - // However most standard xterms follow a well known color chart, - // which can easily be calculated using the simple formula below. - // - // More information about ansi escape codes can be found online. - // http://en.wikipedia.org/wiki/ANSI_escape_code - - int index = 16; - - int r; // red - int g; // green - int b; // blue - - int ri; // r increment - int gi; // g increment - int bi; // b increment - - // Calculate xterm colors (using standard algorithm) - - int r = 0; - int g = 0; - int b = 0; - - for (ri = 0; ri < 6; ri++) { - r = (ri == 0) ? 0 : 95 + (40 * (ri - 1)); - - for (gi = 0; gi < 6; gi++) { - g = (gi == 0) ? 0 : 95 + (40 * (gi - 1)); - - for (bi = 0; bi < 6; bi++) { - b = (bi == 0) ? 0 : 95 + (40 * (bi - 1)); - - [m_codes_fg addObject:[NSString stringWithFormat:@"38;5;%dm", index]]; - [m_codes_bg addObject:[NSString stringWithFormat:@"48;5;%dm", index]]; - [m_colors addObject:DDMakeColor(r, g, b)]; - - index++; - } - } - } - - // Calculate xterm grayscale (using standard algorithm) - - r = 8; - g = 8; - b = 8; - - while (index < 256) { - [m_codes_fg addObject:[NSString stringWithFormat:@"38;5;%dm", index]]; - [m_codes_bg addObject:[NSString stringWithFormat:@"48;5;%dm", index]]; - [m_colors addObject:DDMakeColor(r, g, b)]; - - r += 10; - g += 10; - b += 10; - - index++; - } - - #endif /* if MAP_TO_TERMINAL_APP_COLORS */ - - codes_fg = [m_codes_fg copy]; - codes_bg = [m_codes_bg copy]; - colors = [m_colors copy]; - - NSAssert([codes_fg count] == [codes_bg count], @"Invalid colors/codes array(s)"); - NSAssert([codes_fg count] == [colors count], @"Invalid colors/codes array(s)"); -} - -+ (void)getRed:(CGFloat *)rPtr green:(CGFloat *)gPtr blue:(CGFloat *)bPtr fromColor:(DDColor *)color { - #if TARGET_OS_IPHONE - - // iOS - - BOOL done = NO; - - if ([color respondsToSelector:@selector(getRed:green:blue:alpha:)]) { - done = [color getRed:rPtr green:gPtr blue:bPtr alpha:NULL]; - } - - if (!done) { - // The method getRed:green:blue:alpha: was only available starting iOS 5. - // So in iOS 4 and earlier, we have to jump through hoops. - - CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB(); - - unsigned char pixel[4]; - CGContextRef context = CGBitmapContextCreate(&pixel, 1, 1, 8, 4, rgbColorSpace, (CGBitmapInfo)(kCGBitmapAlphaInfoMask & kCGImageAlphaNoneSkipLast)); - - CGContextSetFillColorWithColor(context, [color CGColor]); - CGContextFillRect(context, CGRectMake(0, 0, 1, 1)); - - if (rPtr) { - *rPtr = pixel[0] / 255.0f; - } - - if (gPtr) { - *gPtr = pixel[1] / 255.0f; - } - - if (bPtr) { - *bPtr = pixel[2] / 255.0f; - } - - CGContextRelease(context); - CGColorSpaceRelease(rgbColorSpace); - } - - #elif defined(DD_CLI) || !__has_include() - - // OS X without AppKit - - [color getRed:rPtr green:gPtr blue:bPtr alpha:NULL]; - - #else /* if TARGET_OS_IPHONE */ - - // OS X with AppKit - - NSColor *safeColor = [color colorUsingColorSpaceName:NSCalibratedRGBColorSpace]; - - [safeColor getRed:rPtr green:gPtr blue:bPtr alpha:NULL]; - #endif /* if TARGET_OS_IPHONE */ -} - -/** - * Maps the given color to the closest available color supported by the shell. - * The shell may support 256 colors, or only 16. - * - * This method loops through the known supported color set, and calculates the closest color. - * The array index of that color, within the colors array, is then returned. - * This array index may also be used as the index within the codes_fg and codes_bg arrays. - **/ -+ (NSUInteger)codeIndexForColor:(DDColor *)inColor { - CGFloat inR, inG, inB; - - [self getRed:&inR green:&inG blue:&inB fromColor:inColor]; - - NSUInteger bestIndex = 0; - CGFloat lowestDistance = 100.0f; - - NSUInteger i = 0; - - for (DDColor *color in colors) { - // Calculate Euclidean distance (lower value means closer to given color) - - CGFloat r, g, b; - [self getRed:&r green:&g blue:&b fromColor:color]; - - #if CGFLOAT_IS_DOUBLE - CGFloat distance = sqrt(pow(r - inR, 2.0) + pow(g - inG, 2.0) + pow(b - inB, 2.0)); - #else - CGFloat distance = sqrtf(powf(r - inR, 2.0f) + powf(g - inG, 2.0f) + powf(b - inB, 2.0f)); - #endif - - NSLogVerbose(@"DDTTYLogger: %3lu : %.3f,%.3f,%.3f & %.3f,%.3f,%.3f = %.6f", - (unsigned long)i, inR, inG, inB, r, g, b, distance); - - if (distance < lowestDistance) { - bestIndex = i; - lowestDistance = distance; - - NSLogVerbose(@"DDTTYLogger: New best index = %lu", (unsigned long)bestIndex); - } - - i++; - } - - return bestIndex; -} - -+ (instancetype)sharedInstance { - static dispatch_once_t DDTTYLoggerOnceToken; - - dispatch_once(&DDTTYLoggerOnceToken, ^{ - // Xcode does NOT natively support colors in the Xcode debugging console. - // You'll need to install the XcodeColors plugin to see colors in the Xcode console. - // - // PS - Please read the header file before diving into the source code. - - char *xcode_colors = getenv("XcodeColors"); - char *term = getenv("TERM"); - - if (xcode_colors && (strcmp(xcode_colors, "YES") == 0)) { - isaXcodeColorTTY = YES; - } else if (term) { - if (strcasestr(term, "color") != NULL) { - isaColorTTY = YES; - isaColor256TTY = (strcasestr(term, "256") != NULL); - - if (isaColor256TTY) { - [self initialize_colors_256]; - } else { - [self initialize_colors_16]; - } - } - } - - NSLogInfo(@"DDTTYLogger: isaColorTTY = %@", (isaColorTTY ? @"YES" : @"NO")); - NSLogInfo(@"DDTTYLogger: isaColor256TTY: %@", (isaColor256TTY ? @"YES" : @"NO")); - NSLogInfo(@"DDTTYLogger: isaXcodeColorTTY: %@", (isaXcodeColorTTY ? @"YES" : @"NO")); - - sharedInstance = [[[self class] alloc] init]; - }); - - return sharedInstance; -} - -- (instancetype)init { - if (sharedInstance != nil) { - return nil; - } - - if ((self = [super init])) { - _calendarUnitFlags = (NSCalendarUnitYear | - NSCalendarUnitMonth | - NSCalendarUnitDay | - NSCalendarUnitHour | - NSCalendarUnitMinute | - NSCalendarUnitSecond); - - // Initialze 'app' variable (char *) - - _appName = [[NSProcessInfo processInfo] processName]; - - _appLen = [_appName lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; - - if (_appLen == 0) { - _appName = @""; - _appLen = [_appName lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; - } - - _app = (char *)malloc(_appLen + 1); - - if (_app == NULL) { - return nil; - } - - BOOL processedAppName = [_appName getCString:_app maxLength:(_appLen + 1) encoding:NSUTF8StringEncoding]; - - if (NO == processedAppName) { - free(_app); - return nil; - } - - // Initialize 'pid' variable (char *) - - _processID = [NSString stringWithFormat:@"%i", (int)getpid()]; - - _pidLen = [_processID lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; - _pid = (char *)malloc(_pidLen + 1); - - if (_pid == NULL) { - free(_app); - return nil; - } - - BOOL processedID = [_processID getCString:_pid maxLength:(_pidLen + 1) encoding:NSUTF8StringEncoding]; - - if (NO == processedID) { - free(_app); - free(_pid); - return nil; - } - - // Initialize color stuff - - _colorsEnabled = NO; - _colorProfilesArray = [[NSMutableArray alloc] initWithCapacity:8]; - _colorProfilesDict = [[NSMutableDictionary alloc] initWithCapacity:8]; - - _automaticallyAppendNewlineForCustomFormatters = YES; - } - - return self; -} - -- (void)loadDefaultColorProfiles { - [self setForegroundColor:DDMakeColor(214, 57, 30) backgroundColor:nil forFlag:DDLogFlagError]; - [self setForegroundColor:DDMakeColor(204, 121, 32) backgroundColor:nil forFlag:DDLogFlagWarning]; -} - -- (BOOL)colorsEnabled { - // The design of this method is taken from the DDAbstractLogger implementation. - // For extensive documentation please refer to the DDAbstractLogger implementation. - - // Note: The internal implementation MUST access the colorsEnabled variable directly, - // This method is designed explicitly for external access. - // - // Using "self." syntax to go through this method will cause immediate deadlock. - // This is the intended result. Fix it by accessing the ivar directly. - // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. - - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); - - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - - __block BOOL result; - - dispatch_sync(globalLoggingQueue, ^{ - dispatch_sync(self.loggerQueue, ^{ - result = _colorsEnabled; - }); - }); - - return result; -} - -- (void)setColorsEnabled:(BOOL)newColorsEnabled { - dispatch_block_t block = ^{ - @autoreleasepool { - _colorsEnabled = newColorsEnabled; - - if ([_colorProfilesArray count] == 0) { - [self loadDefaultColorProfiles]; - } - } - }; - - // The design of this method is taken from the DDAbstractLogger implementation. - // For extensive documentation please refer to the DDAbstractLogger implementation. - - // Note: The internal implementation MUST access the colorsEnabled variable directly, - // This method is designed explicitly for external access. - // - // Using "self." syntax to go through this method will cause immediate deadlock. - // This is the intended result. Fix it by accessing the ivar directly. - // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. - - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); - - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - - dispatch_async(globalLoggingQueue, ^{ - dispatch_async(self.loggerQueue, block); - }); -} - -- (void)setForegroundColor:(DDColor *)txtColor backgroundColor:(DDColor *)bgColor forFlag:(DDLogFlag)mask { - [self setForegroundColor:txtColor backgroundColor:bgColor forFlag:mask context:LOG_CONTEXT_ALL]; -} - -- (void)setForegroundColor:(DDColor *)txtColor backgroundColor:(DDColor *)bgColor forFlag:(DDLogFlag)mask context:(NSInteger)ctxt { - dispatch_block_t block = ^{ - @autoreleasepool { - DDTTYLoggerColorProfile *newColorProfile = - [[DDTTYLoggerColorProfile alloc] initWithForegroundColor:txtColor - backgroundColor:bgColor - flag:mask - context:ctxt]; - - NSLogInfo(@"DDTTYLogger: newColorProfile: %@", newColorProfile); - - NSUInteger i = 0; - - for (DDTTYLoggerColorProfile *colorProfile in _colorProfilesArray) { - if ((colorProfile->mask == mask) && (colorProfile->context == ctxt)) { - break; - } - - i++; - } - - if (i < [_colorProfilesArray count]) { - _colorProfilesArray[i] = newColorProfile; - } else { - [_colorProfilesArray addObject:newColorProfile]; - } - } - }; - - // The design of the setter logic below is taken from the DDAbstractLogger implementation. - // For documentation please refer to the DDAbstractLogger implementation. - - if ([self isOnInternalLoggerQueue]) { - block(); - } else { - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - - dispatch_async(globalLoggingQueue, ^{ - dispatch_async(self.loggerQueue, block); - }); - } -} - -- (void)setForegroundColor:(DDColor *)txtColor backgroundColor:(DDColor *)bgColor forTag:(id )tag { - NSAssert([(id < NSObject >) tag conformsToProtocol: @protocol(NSCopying)], @"Invalid tag"); - - dispatch_block_t block = ^{ - @autoreleasepool { - DDTTYLoggerColorProfile *newColorProfile = - [[DDTTYLoggerColorProfile alloc] initWithForegroundColor:txtColor - backgroundColor:bgColor - flag:(DDLogFlag)0 - context:0]; - - NSLogInfo(@"DDTTYLogger: newColorProfile: %@", newColorProfile); - - _colorProfilesDict[tag] = newColorProfile; - } - }; - - // The design of the setter logic below is taken from the DDAbstractLogger implementation. - // For documentation please refer to the DDAbstractLogger implementation. - - if ([self isOnInternalLoggerQueue]) { - block(); - } else { - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - - dispatch_async(globalLoggingQueue, ^{ - dispatch_async(self.loggerQueue, block); - }); - } -} - -- (void)clearColorsForFlag:(DDLogFlag)mask { - [self clearColorsForFlag:mask context:0]; -} - -- (void)clearColorsForFlag:(DDLogFlag)mask context:(NSInteger)context { - dispatch_block_t block = ^{ - @autoreleasepool { - NSUInteger i = 0; - - for (DDTTYLoggerColorProfile *colorProfile in _colorProfilesArray) { - if ((colorProfile->mask == mask) && (colorProfile->context == context)) { - break; - } - - i++; - } - - if (i < [_colorProfilesArray count]) { - [_colorProfilesArray removeObjectAtIndex:i]; - } - } - }; - - // The design of the setter logic below is taken from the DDAbstractLogger implementation. - // For documentation please refer to the DDAbstractLogger implementation. - - if ([self isOnInternalLoggerQueue]) { - block(); - } else { - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - - dispatch_async(globalLoggingQueue, ^{ - dispatch_async(self.loggerQueue, block); - }); - } -} - -- (void)clearColorsForTag:(id )tag { - NSAssert([(id < NSObject >) tag conformsToProtocol: @protocol(NSCopying)], @"Invalid tag"); - - dispatch_block_t block = ^{ - @autoreleasepool { - [_colorProfilesDict removeObjectForKey:tag]; - } - }; - - // The design of the setter logic below is taken from the DDAbstractLogger implementation. - // For documentation please refer to the DDAbstractLogger implementation. - - if ([self isOnInternalLoggerQueue]) { - block(); - } else { - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - - dispatch_async(globalLoggingQueue, ^{ - dispatch_async(self.loggerQueue, block); - }); - } -} - -- (void)clearColorsForAllFlags { - dispatch_block_t block = ^{ - @autoreleasepool { - [_colorProfilesArray removeAllObjects]; - } - }; - - // The design of the setter logic below is taken from the DDAbstractLogger implementation. - // For documentation please refer to the DDAbstractLogger implementation. - - if ([self isOnInternalLoggerQueue]) { - block(); - } else { - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - - dispatch_async(globalLoggingQueue, ^{ - dispatch_async(self.loggerQueue, block); - }); - } -} - -- (void)clearColorsForAllTags { - dispatch_block_t block = ^{ - @autoreleasepool { - [_colorProfilesDict removeAllObjects]; - } - }; - - // The design of the setter logic below is taken from the DDAbstractLogger implementation. - // For documentation please refer to the DDAbstractLogger implementation. - - if ([self isOnInternalLoggerQueue]) { - block(); - } else { - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - - dispatch_async(globalLoggingQueue, ^{ - dispatch_async(self.loggerQueue, block); - }); - } -} - -- (void)clearAllColors { - dispatch_block_t block = ^{ - @autoreleasepool { - [_colorProfilesArray removeAllObjects]; - [_colorProfilesDict removeAllObjects]; - } - }; - - // The design of the setter logic below is taken from the DDAbstractLogger implementation. - // For documentation please refer to the DDAbstractLogger implementation. - - if ([self isOnInternalLoggerQueue]) { - block(); - } else { - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - - dispatch_async(globalLoggingQueue, ^{ - dispatch_async(self.loggerQueue, block); - }); - } -} - -- (void)logMessage:(DDLogMessage *)logMessage { - NSString *logMsg = logMessage->_message; - BOOL isFormatted = NO; - - if (_logFormatter) { - logMsg = [_logFormatter formatLogMessage:logMessage]; - isFormatted = logMsg != logMessage->_message; - } - - if (logMsg) { - // Search for a color profile associated with the log message - - DDTTYLoggerColorProfile *colorProfile = nil; - - if (_colorsEnabled) { - if (logMessage->_tag) { - colorProfile = _colorProfilesDict[logMessage->_tag]; - } - - if (colorProfile == nil) { - for (DDTTYLoggerColorProfile *cp in _colorProfilesArray) { - if (logMessage->_flag & cp->mask) { - // Color profile set for this context? - if (logMessage->_context == cp->context) { - colorProfile = cp; - - // Stop searching - break; - } - - // Check if LOG_CONTEXT_ALL was specified as a default color for this flag - if (cp->context == LOG_CONTEXT_ALL) { - colorProfile = cp; - - // We don't break to keep searching for more specific color profiles for the context - } - } - } - } - } - - // Convert log message to C string. - // - // We use the stack instead of the heap for speed if possible. - // But we're extra cautious to avoid a stack overflow. - - NSUInteger msgLen = [logMsg lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; - const BOOL useStack = msgLen < (1024 * 4); - - char msgStack[useStack ? (msgLen + 1) : 1]; // Analyzer doesn't like zero-size array, hence the 1 - char *msg = useStack ? msgStack : (char *)malloc(msgLen + 1); - - if (msg == NULL) { - return; - } - - BOOL logMsgEnc = [logMsg getCString:msg maxLength:(msgLen + 1) encoding:NSUTF8StringEncoding]; - - if (!logMsgEnc) { - if (!useStack && msg != NULL) { - free(msg); - } - - return; - } - - // Write the log message to STDERR - - if (isFormatted) { - // The log message has already been formatted. - int iovec_len = (_automaticallyAppendNewlineForCustomFormatters) ? 5 : 4; - struct iovec v[iovec_len]; - - if (colorProfile) { - v[0].iov_base = colorProfile->fgCode; - v[0].iov_len = colorProfile->fgCodeLen; - - v[1].iov_base = colorProfile->bgCode; - v[1].iov_len = colorProfile->bgCodeLen; - - v[iovec_len - 1].iov_base = colorProfile->resetCode; - v[iovec_len - 1].iov_len = colorProfile->resetCodeLen; - } else { - v[0].iov_base = ""; - v[0].iov_len = 0; - - v[1].iov_base = ""; - v[1].iov_len = 0; - - v[iovec_len - 1].iov_base = ""; - v[iovec_len - 1].iov_len = 0; - } - - v[2].iov_base = (char *)msg; - v[2].iov_len = msgLen; - - if (iovec_len == 5) { - v[3].iov_base = "\n"; - v[3].iov_len = (msg[msgLen] == '\n') ? 0 : 1; - } - - writev(STDERR_FILENO, v, iovec_len); - } else { - // The log message is unformatted, so apply standard NSLog style formatting. - - int len; - char ts[24] = ""; - size_t tsLen = 0; - - // Calculate timestamp. - // The technique below is faster than using NSDateFormatter. - if (logMessage->_timestamp) { - NSDateComponents *components = [[NSCalendar autoupdatingCurrentCalendar] components:_calendarUnitFlags fromDate:logMessage->_timestamp]; - - NSTimeInterval epoch = [logMessage->_timestamp timeIntervalSinceReferenceDate]; - int milliseconds = (int)((epoch - floor(epoch)) * 1000); - - len = snprintf(ts, 24, "%04ld-%02ld-%02ld %02ld:%02ld:%02ld:%03d", // yyyy-MM-dd HH:mm:ss:SSS - (long)components.year, - (long)components.month, - (long)components.day, - (long)components.hour, - (long)components.minute, - (long)components.second, milliseconds); - - tsLen = (NSUInteger)MAX(MIN(24 - 1, len), 0); - } - - // Calculate thread ID - // - // How many characters do we need for the thread id? - // logMessage->machThreadID is of type mach_port_t, which is an unsigned int. - // - // 1 hex char = 4 bits - // 8 hex chars for 32 bit, plus ending '\0' = 9 - - char tid[9]; - len = snprintf(tid, 9, "%s", [logMessage->_threadID cStringUsingEncoding:NSUTF8StringEncoding]); - - size_t tidLen = (NSUInteger)MAX(MIN(9 - 1, len), 0); - - // Here is our format: "%s %s[%i:%s] %s", timestamp, appName, processID, threadID, logMsg - - struct iovec v[13]; - - if (colorProfile) { - v[0].iov_base = colorProfile->fgCode; - v[0].iov_len = colorProfile->fgCodeLen; - - v[1].iov_base = colorProfile->bgCode; - v[1].iov_len = colorProfile->bgCodeLen; - - v[12].iov_base = colorProfile->resetCode; - v[12].iov_len = colorProfile->resetCodeLen; - } else { - v[0].iov_base = ""; - v[0].iov_len = 0; - - v[1].iov_base = ""; - v[1].iov_len = 0; - - v[12].iov_base = ""; - v[12].iov_len = 0; - } - - v[2].iov_base = ts; - v[2].iov_len = tsLen; - - v[3].iov_base = " "; - v[3].iov_len = 1; - - v[4].iov_base = _app; - v[4].iov_len = _appLen; - - v[5].iov_base = "["; - v[5].iov_len = 1; - - v[6].iov_base = _pid; - v[6].iov_len = _pidLen; - - v[7].iov_base = ":"; - v[7].iov_len = 1; - - v[8].iov_base = tid; - v[8].iov_len = MIN((size_t)8, tidLen); // snprintf doesn't return what you might think - - v[9].iov_base = "] "; - v[9].iov_len = 2; - - v[10].iov_base = (char *)msg; - v[10].iov_len = msgLen; - - v[11].iov_base = "\n"; - v[11].iov_len = (msg[msgLen] == '\n') ? 0 : 1; - - writev(STDERR_FILENO, v, 13); - } - - if (!useStack) { - free(msg); - } - } -} - -- (NSString *)loggerName { - return @"cocoa.lumberjack.ttyLogger"; -} - -@end - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -@implementation DDTTYLoggerColorProfile - -- (instancetype)initWithForegroundColor:(DDColor *)fgColor backgroundColor:(DDColor *)bgColor flag:(DDLogFlag)aMask context:(NSInteger)ctxt { - if ((self = [super init])) { - mask = aMask; - context = ctxt; - - CGFloat r, g, b; - - if (fgColor) { - [DDTTYLogger getRed:&r green:&g blue:&b fromColor:fgColor]; - - fg_r = (uint8_t)(r * 255.0f); - fg_g = (uint8_t)(g * 255.0f); - fg_b = (uint8_t)(b * 255.0f); - } - - if (bgColor) { - [DDTTYLogger getRed:&r green:&g blue:&b fromColor:bgColor]; - - bg_r = (uint8_t)(r * 255.0f); - bg_g = (uint8_t)(g * 255.0f); - bg_b = (uint8_t)(b * 255.0f); - } - - if (fgColor && isaColorTTY) { - // Map foreground color to closest available shell color - - fgCodeIndex = [DDTTYLogger codeIndexForColor:fgColor]; - fgCodeRaw = codes_fg[fgCodeIndex]; - - NSString *escapeSeq = @"\033["; - - NSUInteger len1 = [escapeSeq lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; - NSUInteger len2 = [fgCodeRaw lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; - - BOOL escapeSeqEnc = [escapeSeq getCString:(fgCode) maxLength:(len1 + 1) encoding:NSUTF8StringEncoding]; - BOOL fgCodeRawEsc = [fgCodeRaw getCString:(fgCode + len1) maxLength:(len2 + 1) encoding:NSUTF8StringEncoding]; - - if (!escapeSeqEnc || !fgCodeRawEsc) { - return nil; - } - - fgCodeLen = len1 + len2; - } else if (fgColor && isaXcodeColorTTY) { - // Convert foreground color to color code sequence - - const char *escapeSeq = XCODE_COLORS_ESCAPE_SEQ; - - int result = snprintf(fgCode, 24, "%sfg%u,%u,%u;", escapeSeq, fg_r, fg_g, fg_b); - fgCodeLen = (NSUInteger)MAX(MIN(result, (24 - 1)), 0); - } else { - // No foreground color or no color support - - fgCode[0] = '\0'; - fgCodeLen = 0; - } - - if (bgColor && isaColorTTY) { - // Map background color to closest available shell color - - bgCodeIndex = [DDTTYLogger codeIndexForColor:bgColor]; - bgCodeRaw = codes_bg[bgCodeIndex]; - - NSString *escapeSeq = @"\033["; - - NSUInteger len1 = [escapeSeq lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; - NSUInteger len2 = [bgCodeRaw lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; - - BOOL escapeSeqEnc = [escapeSeq getCString:(bgCode) maxLength:(len1 + 1) encoding:NSUTF8StringEncoding]; - BOOL bgCodeRawEsc = [bgCodeRaw getCString:(bgCode + len1) maxLength:(len2 + 1) encoding:NSUTF8StringEncoding]; - - if (!escapeSeqEnc || !bgCodeRawEsc) { - return nil; - } - - bgCodeLen = len1 + len2; - } else if (bgColor && isaXcodeColorTTY) { - // Convert background color to color code sequence - - const char *escapeSeq = XCODE_COLORS_ESCAPE_SEQ; - - int result = snprintf(bgCode, 24, "%sbg%u,%u,%u;", escapeSeq, bg_r, bg_g, bg_b); - bgCodeLen = (NSUInteger)MAX(MIN(result, (24 - 1)), 0); - } else { - // No background color or no color support - - bgCode[0] = '\0'; - bgCodeLen = 0; - } - - if (isaColorTTY) { - resetCodeLen = (NSUInteger)MAX(snprintf(resetCode, 8, "\033[0m"), 0); - } else if (isaXcodeColorTTY) { - resetCodeLen = (NSUInteger)MAX(snprintf(resetCode, 8, XCODE_COLORS_RESET), 0); - } else { - resetCode[0] = '\0'; - resetCodeLen = 0; - } - } - - return self; -} - -- (NSString *)description { - return [NSString stringWithFormat: - @"", - self, (int)mask, (long)context, fg_r, fg_g, fg_b, bg_r, bg_g, bg_b, fgCodeRaw, bgCodeRaw]; -} - -@end diff --git a/Example/Pods/CocoaLumberjack/Classes/Extensions/DDContextFilterLogFormatter.h b/Example/Pods/CocoaLumberjack/Classes/Extensions/DDContextFilterLogFormatter.h deleted file mode 100644 index 1657f1f..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/Extensions/DDContextFilterLogFormatter.h +++ /dev/null @@ -1,117 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -#import - -// Disable legacy macros -#ifndef DD_LEGACY_MACROS - #define DD_LEGACY_MACROS 0 -#endif - -#import "DDLog.h" - -/** - * This class provides a log formatter that filters log statements from a logging context not on the whitelist. - * - * A log formatter can be added to any logger to format and/or filter its output. - * You can learn more about log formatters here: - * Documentation/CustomFormatters.md - * - * You can learn more about logging context's here: - * Documentation/CustomContext.md - * - * But here's a quick overview / refresher: - * - * Every log statement has a logging context. - * These come from the underlying logging macros defined in DDLog.h. - * The default logging context is zero. - * You can define multiple logging context's for use in your application. - * For example, logically separate parts of your app each have a different logging context. - * Also 3rd party frameworks that make use of Lumberjack generally use their own dedicated logging context. - **/ -@interface DDContextWhitelistFilterLogFormatter : NSObject - -/** - * Designated default initializer - */ -- (instancetype)init NS_DESIGNATED_INITIALIZER; - -/** - * Add a context to the whitelist - * - * @param loggingContext the context - */ -- (void)addToWhitelist:(NSUInteger)loggingContext; - -/** - * Remove context from whitelist - * - * @param loggingContext the context - */ -- (void)removeFromWhitelist:(NSUInteger)loggingContext; - -/** - * Return the whitelist - */ -@property (readonly, copy) NSArray *whitelist; - -/** - * Check if a context is on the whitelist - * - * @param loggingContext the context - */ -- (BOOL)isOnWhitelist:(NSUInteger)loggingContext; - -@end - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -/** - * This class provides a log formatter that filters log statements from a logging context on the blacklist. - **/ -@interface DDContextBlacklistFilterLogFormatter : NSObject - -- (instancetype)init NS_DESIGNATED_INITIALIZER; - -/** - * Add a context to the blacklist - * - * @param loggingContext the context - */ -- (void)addToBlacklist:(NSUInteger)loggingContext; - -/** - * Remove context from blacklist - * - * @param loggingContext the context - */ -- (void)removeFromBlacklist:(NSUInteger)loggingContext; - -/** - * Return the blacklist - */ -@property (readonly, copy) NSArray *blacklist; - - -/** - * Check if a context is on the blacklist - * - * @param loggingContext the context - */ -- (BOOL)isOnBlacklist:(NSUInteger)loggingContext; - -@end diff --git a/Example/Pods/CocoaLumberjack/Classes/Extensions/DDContextFilterLogFormatter.m b/Example/Pods/CocoaLumberjack/Classes/Extensions/DDContextFilterLogFormatter.m deleted file mode 100644 index 14a6ae9..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/Extensions/DDContextFilterLogFormatter.m +++ /dev/null @@ -1,191 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -#import "DDContextFilterLogFormatter.h" -#import - -#if !__has_feature(objc_arc) -#error This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). -#endif - -@interface DDLoggingContextSet : NSObject - -- (void)addToSet:(NSUInteger)loggingContext; -- (void)removeFromSet:(NSUInteger)loggingContext; - -@property (readonly, copy) NSArray *currentSet; - -- (BOOL)isInSet:(NSUInteger)loggingContext; - -@end - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -@interface DDContextWhitelistFilterLogFormatter () { - DDLoggingContextSet *_contextSet; -} - -@end - - -@implementation DDContextWhitelistFilterLogFormatter - -- (instancetype)init { - if ((self = [super init])) { - _contextSet = [[DDLoggingContextSet alloc] init]; - } - - return self; -} - -- (void)addToWhitelist:(NSUInteger)loggingContext { - [_contextSet addToSet:loggingContext]; -} - -- (void)removeFromWhitelist:(NSUInteger)loggingContext { - [_contextSet removeFromSet:loggingContext]; -} - -- (NSArray *)whitelist { - return [_contextSet currentSet]; -} - -- (BOOL)isOnWhitelist:(NSUInteger)loggingContext { - return [_contextSet isInSet:loggingContext]; -} - -- (NSString *)formatLogMessage:(DDLogMessage *)logMessage { - if ([self isOnWhitelist:logMessage->_context]) { - return logMessage->_message; - } else { - return nil; - } -} - -@end - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -@interface DDContextBlacklistFilterLogFormatter () { - DDLoggingContextSet *_contextSet; -} - -@end - - -@implementation DDContextBlacklistFilterLogFormatter - -- (instancetype)init { - if ((self = [super init])) { - _contextSet = [[DDLoggingContextSet alloc] init]; - } - - return self; -} - -- (void)addToBlacklist:(NSUInteger)loggingContext { - [_contextSet addToSet:loggingContext]; -} - -- (void)removeFromBlacklist:(NSUInteger)loggingContext { - [_contextSet removeFromSet:loggingContext]; -} - -- (NSArray *)blacklist { - return [_contextSet currentSet]; -} - -- (BOOL)isOnBlacklist:(NSUInteger)loggingContext { - return [_contextSet isInSet:loggingContext]; -} - -- (NSString *)formatLogMessage:(DDLogMessage *)logMessage { - if ([self isOnBlacklist:logMessage->_context]) { - return nil; - } else { - return logMessage->_message; - } -} - -@end - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - -@interface DDLoggingContextSet () { - OSSpinLock _lock; - NSMutableSet *_set; -} - -@end - - -@implementation DDLoggingContextSet - -- (instancetype)init { - if ((self = [super init])) { - _set = [[NSMutableSet alloc] init]; - } - - return self; -} - -- (void)addToSet:(NSUInteger)loggingContext { - OSSpinLockLock(&_lock); - { - [_set addObject:@(loggingContext)]; - } - OSSpinLockUnlock(&_lock); -} - -- (void)removeFromSet:(NSUInteger)loggingContext { - OSSpinLockLock(&_lock); - { - [_set removeObject:@(loggingContext)]; - } - OSSpinLockUnlock(&_lock); -} - -- (NSArray *)currentSet { - NSArray *result = nil; - - OSSpinLockLock(&_lock); - { - result = [_set allObjects]; - } - OSSpinLockUnlock(&_lock); - - return result; -} - -- (BOOL)isInSet:(NSUInteger)loggingContext { - BOOL result = NO; - - OSSpinLockLock(&_lock); - { - result = [_set containsObject:@(loggingContext)]; - } - OSSpinLockUnlock(&_lock); - - return result; -} - -@end diff --git a/Example/Pods/CocoaLumberjack/Classes/Extensions/DDDispatchQueueLogFormatter.h b/Example/Pods/CocoaLumberjack/Classes/Extensions/DDDispatchQueueLogFormatter.h deleted file mode 100644 index 129f6e1..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/Extensions/DDDispatchQueueLogFormatter.h +++ /dev/null @@ -1,178 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -#import -#import - -// Disable legacy macros -#ifndef DD_LEGACY_MACROS - #define DD_LEGACY_MACROS 0 -#endif - -#import "DDLog.h" - -/** - * Log formatter mode - */ -typedef NS_ENUM(NSUInteger, DDDispatchQueueLogFormatterMode){ - /** - * This is the default option, means the formatter can be reused between multiple loggers and therefore is thread-safe. - * There is, of course, a performance cost for the thread-safety - */ - DDDispatchQueueLogFormatterModeShareble = 0, - /** - * If the formatter will only be used by a single logger, then the thread-safety can be removed - * @note: there is an assert checking if the formatter is added to multiple loggers and the mode is non-shareble - */ - DDDispatchQueueLogFormatterModeNonShareble, -}; - - -/** - * This class provides a log formatter that prints the dispatch_queue label instead of the mach_thread_id. - * - * A log formatter can be added to any logger to format and/or filter its output. - * You can learn more about log formatters here: - * Documentation/CustomFormatters.md - * - * A typical `NSLog` (or `DDTTYLogger`) prints detailed info as `[:]`. - * For example: - * - * `2011-10-17 20:21:45.435 AppName[19928:5207] Your log message here` - * - * Where: - * `- 19928 = process id` - * `- 5207 = thread id (mach_thread_id printed in hex)` - * - * When using grand central dispatch (GCD), this information is less useful. - * This is because a single serial dispatch queue may be run on any thread from an internally managed thread pool. - * For example: - * - * `2011-10-17 20:32:31.111 AppName[19954:4d07] Message from my_serial_dispatch_queue` - * `2011-10-17 20:32:31.112 AppName[19954:5207] Message from my_serial_dispatch_queue` - * `2011-10-17 20:32:31.113 AppName[19954:2c55] Message from my_serial_dispatch_queue` - * - * This formatter allows you to replace the standard `[box:info]` with the dispatch_queue name. - * For example: - * - * `2011-10-17 20:32:31.111 AppName[img-scaling] Message from my_serial_dispatch_queue` - * `2011-10-17 20:32:31.112 AppName[img-scaling] Message from my_serial_dispatch_queue` - * `2011-10-17 20:32:31.113 AppName[img-scaling] Message from my_serial_dispatch_queue` - * - * If the dispatch_queue doesn't have a set name, then it falls back to the thread name. - * If the current thread doesn't have a set name, then it falls back to the mach_thread_id in hex (like normal). - * - * Note: If manually creating your own background threads (via `NSThread/alloc/init` or `NSThread/detachNeThread`), - * you can use `[[NSThread currentThread] setName:(NSString *)]`. - **/ -@interface DDDispatchQueueLogFormatter : NSObject - -/** - * Standard init method. - * Configure using properties as desired. - **/ -- (instancetype)init NS_DESIGNATED_INITIALIZER; - -/** - * Initializer with ability to set the queue mode - * - * @param mode choose between DDDispatchQueueLogFormatterModeShareble and DDDispatchQueueLogFormatterModeNonShareble, depending if the formatter is shared between several loggers or not - */ -- (instancetype)initWithMode:(DDDispatchQueueLogFormatterMode)mode; - -/** - * The minQueueLength restricts the minimum size of the [detail box]. - * If the minQueueLength is set to 0, there is no restriction. - * - * For example, say a dispatch_queue has a label of "diskIO": - * - * If the minQueueLength is 0: [diskIO] - * If the minQueueLength is 4: [diskIO] - * If the minQueueLength is 5: [diskIO] - * If the minQueueLength is 6: [diskIO] - * If the minQueueLength is 7: [diskIO ] - * If the minQueueLength is 8: [diskIO ] - * - * The default minQueueLength is 0 (no minimum, so [detail box] won't be padded). - * - * If you want every [detail box] to have the exact same width, - * set both minQueueLength and maxQueueLength to the same value. - **/ -@property (assign, atomic) NSUInteger minQueueLength; - -/** - * The maxQueueLength restricts the number of characters that will be inside the [detail box]. - * If the maxQueueLength is 0, there is no restriction. - * - * For example, say a dispatch_queue has a label of "diskIO": - * - * If the maxQueueLength is 0: [diskIO] - * If the maxQueueLength is 4: [disk] - * If the maxQueueLength is 5: [diskI] - * If the maxQueueLength is 6: [diskIO] - * If the maxQueueLength is 7: [diskIO] - * If the maxQueueLength is 8: [diskIO] - * - * The default maxQueueLength is 0 (no maximum, so [detail box] won't be truncated). - * - * If you want every [detail box] to have the exact same width, - * set both minQueueLength and maxQueueLength to the same value. - **/ -@property (assign, atomic) NSUInteger maxQueueLength; - -/** - * Sometimes queue labels have long names like "com.apple.main-queue", - * but you'd prefer something shorter like simply "main". - * - * This method allows you to set such preferred replacements. - * The above example is set by default. - * - * To remove/undo a previous replacement, invoke this method with nil for the 'shortLabel' parameter. - **/ -- (NSString *)replacementStringForQueueLabel:(NSString *)longLabel; - -/** - * See the `replacementStringForQueueLabel:` description - */ -- (void)setReplacementString:(NSString *)shortLabel forQueueLabel:(NSString *)longLabel; - -@end - -/** - * Category on `DDDispatchQueueLogFormatter` to make method declarations easier to extend/modify - **/ -@interface DDDispatchQueueLogFormatter (OverridableMethods) - -/** - * Date formatter default configuration - */ -- (void)configureDateFormatter:(NSDateFormatter *)dateFormatter; - -/** - * Formatter method to transfrom from date to string - */ -- (NSString *)stringFromDate:(NSDate *)date; - -/** - * Method to compute the queue thread label - */ -- (NSString *)queueThreadLabelForLogMessage:(DDLogMessage *)logMessage; - -/** - * The actual method that formats a message (transforms a `DDLogMessage` model into a printable string) - */ -- (NSString *)formatLogMessage:(DDLogMessage *)logMessage; - -@end diff --git a/Example/Pods/CocoaLumberjack/Classes/Extensions/DDDispatchQueueLogFormatter.m b/Example/Pods/CocoaLumberjack/Classes/Extensions/DDDispatchQueueLogFormatter.m deleted file mode 100644 index fdcc87b..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/Extensions/DDDispatchQueueLogFormatter.m +++ /dev/null @@ -1,277 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -#import "DDDispatchQueueLogFormatter.h" -#import -#import - - -#if !__has_feature(objc_arc) -#error This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). -#endif - -@interface DDDispatchQueueLogFormatter () { - DDDispatchQueueLogFormatterMode _mode; - NSString *_dateFormatterKey; - - int32_t _atomicLoggerCount; - NSDateFormatter *_threadUnsafeDateFormatter; // Use [self stringFromDate] - - OSSpinLock _lock; - - NSUInteger _minQueueLength; // _prefix == Only access via atomic property - NSUInteger _maxQueueLength; // _prefix == Only access via atomic property - NSMutableDictionary *_replacements; // _prefix == Only access from within spinlock -} - -@end - - -@implementation DDDispatchQueueLogFormatter - -- (instancetype)init { - if ((self = [super init])) { - _mode = DDDispatchQueueLogFormatterModeShareble; - - // We need to carefully pick the name for storing in thread dictionary to not - // use a formatter configured by subclass and avoid surprises. - Class cls = [self class]; - Class superClass = class_getSuperclass(cls); - SEL configMethodName = @selector(configureDateFormatter:); - Method configMethod = class_getInstanceMethod(cls, configMethodName); - while (class_getInstanceMethod(superClass, configMethodName) == configMethod) { - cls = superClass; - superClass = class_getSuperclass(cls); - } - // now `cls` is the class that provides implementation for `configureDateFormatter:` - _dateFormatterKey = [NSString stringWithFormat:@"%s_NSDateFormatter", class_getName(cls)]; - - _atomicLoggerCount = 0; - _threadUnsafeDateFormatter = nil; - - _minQueueLength = 0; - _maxQueueLength = 0; - _replacements = [[NSMutableDictionary alloc] init]; - - // Set default replacements: - - _replacements[@"com.apple.main-thread"] = @"main"; - } - - return self; -} - -- (instancetype)initWithMode:(DDDispatchQueueLogFormatterMode)mode { - if ((self = [self init])) { - _mode = mode; - } - return self; -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Configuration -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -@synthesize minQueueLength = _minQueueLength; -@synthesize maxQueueLength = _maxQueueLength; - -- (NSString *)replacementStringForQueueLabel:(NSString *)longLabel { - NSString *result = nil; - - OSSpinLockLock(&_lock); - { - result = _replacements[longLabel]; - } - OSSpinLockUnlock(&_lock); - - return result; -} - -- (void)setReplacementString:(NSString *)shortLabel forQueueLabel:(NSString *)longLabel { - OSSpinLockLock(&_lock); - { - if (shortLabel) { - _replacements[longLabel] = shortLabel; - } else { - [_replacements removeObjectForKey:longLabel]; - } - } - OSSpinLockUnlock(&_lock); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark DDLogFormatter -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -- (NSDateFormatter *)createDateFormatter { - NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; - [self configureDateFormatter:formatter]; - return formatter; -} - -- (void)configureDateFormatter:(NSDateFormatter *)dateFormatter { - [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; - [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss:SSS"]; - [dateFormatter setLocale:[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]]; - - NSString *calendarIdentifier = nil; -#if defined(__IPHONE_8_0) || defined(__MAC_10_10) - calendarIdentifier = NSCalendarIdentifierGregorian; -#else - calendarIdentifier = NSGregorianCalendar; -#endif - - [dateFormatter setCalendar:[[NSCalendar alloc] initWithCalendarIdentifier:calendarIdentifier]]; -} - -- (NSString *)stringFromDate:(NSDate *)date { - - NSDateFormatter *dateFormatter = nil; - if (_mode == DDDispatchQueueLogFormatterModeNonShareble) { - // Single-threaded mode. - - dateFormatter = _threadUnsafeDateFormatter; - if (dateFormatter == nil) { - dateFormatter = [self createDateFormatter]; - _threadUnsafeDateFormatter = dateFormatter; - } - } else { - // Multi-threaded mode. - // NSDateFormatter is NOT thread-safe. - - NSString *key = _dateFormatterKey; - - NSMutableDictionary *threadDictionary = [[NSThread currentThread] threadDictionary]; - dateFormatter = threadDictionary[key]; - - if (dateFormatter == nil) { - dateFormatter = [self createDateFormatter]; - threadDictionary[key] = dateFormatter; - } - } - - return [dateFormatter stringFromDate:date]; -} - -- (NSString *)queueThreadLabelForLogMessage:(DDLogMessage *)logMessage { - // As per the DDLogFormatter contract, this method is always invoked on the same thread/dispatch_queue - - NSUInteger minQueueLength = self.minQueueLength; - NSUInteger maxQueueLength = self.maxQueueLength; - - // Get the name of the queue, thread, or machID (whichever we are to use). - - NSString *queueThreadLabel = nil; - - BOOL useQueueLabel = YES; - BOOL useThreadName = NO; - - if (logMessage->_queueLabel) { - // If you manually create a thread, it's dispatch_queue will have one of the thread names below. - // Since all such threads have the same name, we'd prefer to use the threadName or the machThreadID. - - NSArray *names = @[ - @"com.apple.root.low-priority", - @"com.apple.root.default-priority", - @"com.apple.root.high-priority", - @"com.apple.root.low-overcommit-priority", - @"com.apple.root.default-overcommit-priority", - @"com.apple.root.high-overcommit-priority" - ]; - - for (NSString * name in names) { - if ([logMessage->_queueLabel isEqualToString:name]) { - useQueueLabel = NO; - useThreadName = [logMessage->_threadName length] > 0; - break; - } - } - } else { - useQueueLabel = NO; - useThreadName = [logMessage->_threadName length] > 0; - } - - if (useQueueLabel || useThreadName) { - NSString *fullLabel; - NSString *abrvLabel; - - if (useQueueLabel) { - fullLabel = logMessage->_queueLabel; - } else { - fullLabel = logMessage->_threadName; - } - - OSSpinLockLock(&_lock); - { - abrvLabel = _replacements[fullLabel]; - } - OSSpinLockUnlock(&_lock); - - if (abrvLabel) { - queueThreadLabel = abrvLabel; - } else { - queueThreadLabel = fullLabel; - } - } else { - queueThreadLabel = logMessage->_threadID; - } - - // Now use the thread label in the output - - NSUInteger labelLength = [queueThreadLabel length]; - - // labelLength > maxQueueLength : truncate - // labelLength < minQueueLength : padding - // : exact - - if ((maxQueueLength > 0) && (labelLength > maxQueueLength)) { - // Truncate - - return [queueThreadLabel substringToIndex:maxQueueLength]; - } else if (labelLength < minQueueLength) { - // Padding - - NSUInteger numSpaces = minQueueLength - labelLength; - - char spaces[numSpaces + 1]; - memset(spaces, ' ', numSpaces); - spaces[numSpaces] = '\0'; - - return [NSString stringWithFormat:@"%@%s", queueThreadLabel, spaces]; - } else { - // Exact - - return queueThreadLabel; - } -} - -- (NSString *)formatLogMessage:(DDLogMessage *)logMessage { - NSString *timestamp = [self stringFromDate:(logMessage->_timestamp)]; - NSString *queueThreadLabel = [self queueThreadLabelForLogMessage:logMessage]; - - return [NSString stringWithFormat:@"%@ [%@] %@", timestamp, queueThreadLabel, logMessage->_message]; -} - -- (void)didAddToLogger:(id __attribute__((unused)))logger { - int32_t count = 0; - count = OSAtomicIncrement32(&_atomicLoggerCount); - NSAssert(count <= 1 || _mode == DDDispatchQueueLogFormatterModeShareble, @"Can't reuse formatter with multiple loggers in non-shareable mode."); -} - -- (void)willRemoveFromLogger:(id __attribute__((unused)))logger { - OSAtomicDecrement32(&_atomicLoggerCount); -} - -@end diff --git a/Example/Pods/CocoaLumberjack/Classes/Extensions/DDMultiFormatter.h b/Example/Pods/CocoaLumberjack/Classes/Extensions/DDMultiFormatter.h deleted file mode 100644 index 1d6ceea..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/Extensions/DDMultiFormatter.h +++ /dev/null @@ -1,56 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -#import - -// Disable legacy macros -#ifndef DD_LEGACY_MACROS - #define DD_LEGACY_MACROS 0 -#endif - -#import "DDLog.h" - -/** - * This formatter can be used to chain different formatters together. - * The log message will processed in the order of the formatters added. - **/ -@interface DDMultiFormatter : NSObject - -/** - * Array of chained formatters - */ -@property (readonly) NSArray *formatters; - -/** - * Add a new formatter - */ -- (void)addFormatter:(id)formatter; - -/** - * Remove a formatter - */ -- (void)removeFormatter:(id)formatter; - -/** - * Remove all existing formatters - */ -- (void)removeAllFormatters; - -/** - * Check if a certain formatter is used - */ -- (BOOL)isFormattingWithFormatter:(id)formatter; - -@end diff --git a/Example/Pods/CocoaLumberjack/Framework/Lumberjack/CocoaLumberjack.modulemap b/Example/Pods/CocoaLumberjack/Framework/Lumberjack/CocoaLumberjack.modulemap deleted file mode 100644 index 032ae65..0000000 --- a/Example/Pods/CocoaLumberjack/Framework/Lumberjack/CocoaLumberjack.modulemap +++ /dev/null @@ -1,36 +0,0 @@ -framework module CocoaLumberjack { - umbrella header "CocoaLumberjack.h" - - export * - module * { export * } - - textual header "DDLogMacros.h" - - exclude header "DDLog+LOGV.h" - exclude header "DDLegacyMacros.h" - - explicit module DDContextFilterLogFormatter { - header "DDContextFilterLogFormatter.h" - export * - } - - explicit module DDDispatchQueueLogFormatter { - header "DDDispatchQueueLogFormatter.h" - export * - } - - explicit module DDMultiFormatter { - header "DDMultiFormatter.h" - export * - } - - explicit module DDASLLogCapture { - header "DDASLLogCapture.h" - export * - } - - explicit module DDAbstractDatabaseLogger { - header "DDAbstractDatabaseLogger.h" - export * - } -} diff --git a/Example/Pods/CocoaLumberjack/LICENSE.txt b/Example/Pods/CocoaLumberjack/LICENSE.txt index 9c29fac..66a942c 100644 --- a/Example/Pods/CocoaLumberjack/LICENSE.txt +++ b/Example/Pods/CocoaLumberjack/LICENSE.txt @@ -1,6 +1,6 @@ Software License Agreement (BSD License) -Copyright (c) 2010-2015, Deusty, LLC +Copyright (c) 2010, Deusty, LLC All rights reserved. Redistribution and use of this software in source and binary forms, diff --git a/Example/Pods/CocoaLumberjack/Lumberjack/DDASLLogCapture.h b/Example/Pods/CocoaLumberjack/Lumberjack/DDASLLogCapture.h new file mode 100644 index 0000000..53dc6b0 --- /dev/null +++ b/Example/Pods/CocoaLumberjack/Lumberjack/DDASLLogCapture.h @@ -0,0 +1,23 @@ +// +// DDASLLogCapture.h +// Lumberjack +// +// Created by Dario Ahdoot on 3/17/14. +// +// + +#import "DDASLLogger.h" + +@protocol DDLogger; + +@interface DDASLLogCapture : NSObject + ++ (void)start; ++ (void)stop; + +// Default log level: LOG_LEVEL_VERBOSE (i.e. capture all ASL messages). ++ (int)captureLogLevel; ++ (void)setCaptureLogLevel:(int)LOG_LEVEL_XXX; + +@end + diff --git a/Example/Pods/CocoaLumberjack/Lumberjack/DDASLLogCapture.m b/Example/Pods/CocoaLumberjack/Lumberjack/DDASLLogCapture.m new file mode 100644 index 0000000..52bfc1b --- /dev/null +++ b/Example/Pods/CocoaLumberjack/Lumberjack/DDASLLogCapture.m @@ -0,0 +1,188 @@ +// +// DDASLLogCapture.m +// Lumberjack +// +// Created by Dario Ahdoot on 3/17/14. +// +// + +#import "DDASLLogCapture.h" +#import "DDLog.h" + +#include +#include +#include +#include + +static BOOL _cancel = YES; +static int _captureLogLevel = LOG_LEVEL_VERBOSE; + +@implementation DDASLLogCapture + ++ (void)start +{ + // Ignore subsequent calls + if (!_cancel) + return; + + _cancel = NO; + + dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) + { + [DDASLLogCapture captureAslLogs]; + }); +} + ++ (void)stop +{ + _cancel = YES; +} + ++ (int)captureLogLevel +{ + return _captureLogLevel; +} + ++ (void)setCaptureLogLevel:(int)LOG_LEVEL_XXX +{ + _captureLogLevel = LOG_LEVEL_XXX; +} + +# pragma mark - Private methods + ++ (void)configureAslQuery:(aslmsg)query +{ + const char param[] = "7"; // ASL_LEVEL_DEBUG, which is everything. We'll rely on regular DDlog log level to filter + asl_set_query(query, ASL_KEY_LEVEL, param, ASL_QUERY_OP_LESS_EQUAL | ASL_QUERY_OP_NUMERIC); + +#if !TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR + int processId = [[NSProcessInfo processInfo] processIdentifier]; + char pid[16]; + sprintf(pid, "%d", processId); + asl_set_query(query, ASL_KEY_PID, pid, ASL_QUERY_OP_EQUAL | ASL_QUERY_OP_NUMERIC); +#endif +} + ++ (void)aslMessageRecieved:(aslmsg)msg +{ + // NSString * sender = [NSString stringWithCString:asl_get(msg, ASL_KEY_SENDER) encoding:NSUTF8StringEncoding]; + NSString * message = [NSString stringWithCString:asl_get(msg, ASL_KEY_MSG) encoding:NSUTF8StringEncoding]; + NSString * level = [NSString stringWithCString:asl_get(msg, ASL_KEY_LEVEL) encoding:NSUTF8StringEncoding]; + NSString * secondsStr = [NSString stringWithCString:asl_get(msg, ASL_KEY_TIME) encoding:NSUTF8StringEncoding]; + NSString * nanoStr = [NSString stringWithCString:asl_get(msg, ASL_KEY_TIME_NSEC) encoding:NSUTF8StringEncoding]; + + NSTimeInterval seconds = [secondsStr doubleValue]; + NSTimeInterval nanoSeconds = [nanoStr doubleValue]; + NSTimeInterval totalSeconds = seconds + (nanoSeconds / 1e9); + + NSDate * timeStamp = [NSDate dateWithTimeIntervalSince1970:totalSeconds]; + + int flag; + BOOL async; + switch([level intValue]) + { + // By default all NSLog's with a ASL_LEVEL_WARNING level + case ASL_LEVEL_EMERG : + case ASL_LEVEL_ALERT : + case ASL_LEVEL_CRIT : flag = LOG_FLAG_ERROR; async = LOG_ASYNC_ERROR; break; + case ASL_LEVEL_ERR : flag = LOG_FLAG_WARN; async = LOG_ASYNC_WARN; break; + case ASL_LEVEL_WARNING : flag = LOG_FLAG_INFO; async = LOG_ASYNC_INFO; break; + case ASL_LEVEL_NOTICE : flag = LOG_FLAG_DEBUG; async = LOG_ASYNC_DEBUG; break; + case ASL_LEVEL_INFO : + case ASL_LEVEL_DEBUG : + default : flag = LOG_FLAG_VERBOSE; async = LOG_ASYNC_VERBOSE; break; + } + + if (!(_captureLogLevel & flag)) + return; + + DDLogMessage * logMessage = [[DDLogMessage alloc]initWithLogMsg:message + level:_captureLogLevel + flag:flag + context:0 + file:"DDASLLogCapture" + function:0 + line:0 + tag:nil + options:0 + timestamp:timeStamp]; + + [DDLog log:async message:logMessage]; +} + ++ (void)captureAslLogs +{ + @autoreleasepool + { + /* + We use ASL_KEY_MSG_ID to see each message once, but there's no + obvious way to get the "next" ID. To bootstrap the process, we'll + search by timestamp until we've seen a message. + */ + + struct timeval timeval = { .tv_sec = 0 }; + gettimeofday(&timeval, NULL); + unsigned long long startTime = timeval.tv_sec; + __block unsigned long long lastSeenID = 0; + + /* + syslogd posts kNotifyASLDBUpdate (com.apple.system.logger.message) + through the notify API when it saves messages to the ASL database. + There is some coalescing - currently it is sent at most twice per + second - but there is no documented guarantee about this. In any + case, there may be multiple messages per notification. + + Notify notifications don't carry any payload, so we need to search + for the messages. + */ + int notifyToken = 0; // Can be used to unregister with notify_cancel(). + notify_register_dispatch(kNotifyASLDBUpdate, ¬ifyToken, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(int token) + { + // At least one message has been posted; build a search query. + @autoreleasepool + { + aslmsg query = asl_new(ASL_TYPE_QUERY); + char stringValue[64]; + if (lastSeenID > 0) + { + snprintf(stringValue, sizeof stringValue, "%llu", lastSeenID); + asl_set_query(query, ASL_KEY_MSG_ID, stringValue, ASL_QUERY_OP_GREATER | ASL_QUERY_OP_NUMERIC); + } + else + { + snprintf(stringValue, sizeof stringValue, "%llu", startTime); + asl_set_query(query, ASL_KEY_TIME, stringValue, ASL_QUERY_OP_GREATER_EQUAL | ASL_QUERY_OP_NUMERIC); + } + [DDASLLogCapture configureAslQuery:query]; + + // Iterate over new messages. + aslmsg msg; + aslresponse response = asl_search(NULL, query); +#if defined(__IPHONE_8_0) || defined(__MAC_10_10) + while ((msg = asl_next(response))) +#else + while ((msg = aslresponse_next(response))) +#endif + { + [DDASLLogCapture aslMessageRecieved:msg]; + + // Keep track of which messages we've seen. + lastSeenID = atoll(asl_get(msg, ASL_KEY_MSG_ID)); + } +#if defined(__IPHONE_8_0) || defined(__MAC_10_10) + asl_release(response); +#else + aslresponse_free(response); +#endif + if(_cancel) + { + notify_cancel(notifyToken); + return; + } + free(query); + } + }); + } +} + +@end \ No newline at end of file diff --git a/Example/Pods/CocoaLumberjack/Lumberjack/DDASLLogger.h b/Example/Pods/CocoaLumberjack/Lumberjack/DDASLLogger.h new file mode 100755 index 0000000..a55eb6f --- /dev/null +++ b/Example/Pods/CocoaLumberjack/Lumberjack/DDASLLogger.h @@ -0,0 +1,37 @@ +#import + +#import "DDLog.h" + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/CocoaLumberjack/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/CocoaLumberjack/CocoaLumberjack/wiki/GettingStarted + * + * + * This class provides a logger for the Apple System Log facility. + * + * As described in the "Getting Started" page, + * the traditional NSLog() function directs it's output to two places: + * + * - Apple System Log + * - StdErr (if stderr is a TTY) so log statements show up in Xcode console + * + * To duplicate NSLog() functionality you can simply add this logger and a tty logger. + * However, if you instead choose to use file logging (for faster performance), + * you may choose to use a file logger and a tty logger. +**/ + +@interface DDASLLogger : DDAbstractLogger + ++ (instancetype)sharedInstance; + +// Inherited from DDAbstractLogger + +// - (id )logFormatter; +// - (void)setLogFormatter:(id )formatter; + +@end diff --git a/Example/Pods/CocoaLumberjack/Lumberjack/DDASLLogger.m b/Example/Pods/CocoaLumberjack/Lumberjack/DDASLLogger.m new file mode 100755 index 0000000..020acb2 --- /dev/null +++ b/Example/Pods/CocoaLumberjack/Lumberjack/DDASLLogger.m @@ -0,0 +1,95 @@ +#import "DDASLLogger.h" +#import +#import + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/CocoaLumberjack/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/CocoaLumberjack/CocoaLumberjack/wiki/GettingStarted +**/ + +#if ! __has_feature(objc_arc) +#error This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +static DDASLLogger *sharedInstance; + +@implementation DDASLLogger +{ + aslclient client; +} + ++ (instancetype)sharedInstance +{ + static dispatch_once_t DDASLLoggerOnceToken; + dispatch_once(&DDASLLoggerOnceToken, ^{ + sharedInstance = [[[self class] alloc] init]; + }); + + return sharedInstance; +} + +- (id)init +{ + if (sharedInstance != nil) + { + return nil; + } + + if ((self = [super init])) + { + // A default asl client is provided for the main thread, + // but background threads need to create their own client. + + client = asl_open(NULL, "com.apple.console", 0); + } + return self; +} + +- (void)logMessage:(DDLogMessage *)logMessage +{ + // Skip captured log messages. + if (strcmp(logMessage->file, "DDASLLogCapture") == 0) + return; + + NSString *logMsg = logMessage->logMsg; + + if (formatter) + { + logMsg = [formatter formatLogMessage:logMessage]; + } + + if (logMsg) + { + const char *msg = [logMsg UTF8String]; + + int aslLogLevel; + switch (logMessage->logFlag) + { + // Note: By default ASL will filter anything above level 5 (Notice). + // So our mappings shouldn't go above that level. + case LOG_FLAG_ERROR : aslLogLevel = ASL_LEVEL_CRIT; break; + case LOG_FLAG_WARN : aslLogLevel = ASL_LEVEL_ERR; break; + case LOG_FLAG_INFO : aslLogLevel = ASL_LEVEL_WARNING; break; // Regular NSLog's level + case LOG_FLAG_DEBUG : + case LOG_FLAG_VERBOSE : + default : aslLogLevel = ASL_LEVEL_NOTICE; break; + } + + aslmsg m = asl_new(ASL_TYPE_MSG); + asl_set(m, ASL_KEY_READ_UID, "501"); + asl_log(client, m, aslLogLevel, "%s", msg); + asl_free(m); + } +} + +- (NSString *)loggerName +{ + return @"cocoa.lumberjack.aslLogger"; +} + +@end diff --git a/Example/Pods/CocoaLumberjack/Classes/DDAbstractDatabaseLogger.h b/Example/Pods/CocoaLumberjack/Lumberjack/DDAbstractDatabaseLogger.h similarity index 68% rename from Example/Pods/CocoaLumberjack/Classes/DDAbstractDatabaseLogger.h rename to Example/Pods/CocoaLumberjack/Lumberjack/DDAbstractDatabaseLogger.h index aad3666..4e0c33c 100644 --- a/Example/Pods/CocoaLumberjack/Classes/DDAbstractDatabaseLogger.h +++ b/Example/Pods/CocoaLumberjack/Lumberjack/DDAbstractDatabaseLogger.h @@ -1,123 +1,102 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -// Disable legacy macros -#ifndef DD_LEGACY_MACROS - #define DD_LEGACY_MACROS 0 -#endif +#import #import "DDLog.h" /** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/CocoaLumberjack/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/CocoaLumberjack/CocoaLumberjack/wiki/GettingStarted + * + * * This class provides an abstract implementation of a database logger. - * + * * That is, it provides the base implementation for a database logger to build atop of. * All that is needed for a concrete database logger is to extend this class * and override the methods in the implementation file that are prefixed with "db_". - **/ +**/ + @interface DDAbstractDatabaseLogger : DDAbstractLogger { - @protected - NSUInteger _saveThreshold; - NSTimeInterval _saveInterval; - NSTimeInterval _maxAge; - NSTimeInterval _deleteInterval; - BOOL _deleteOnEverySave; + NSUInteger saveThreshold; + NSTimeInterval saveInterval; + NSTimeInterval maxAge; + NSTimeInterval deleteInterval; + BOOL deleteOnEverySave; - BOOL _saveTimerSuspended; - NSUInteger _unsavedCount; - dispatch_time_t _unsavedTime; - dispatch_source_t _saveTimer; - dispatch_time_t _lastDeleteTime; - dispatch_source_t _deleteTimer; + BOOL saveTimerSuspended; + NSUInteger unsavedCount; + dispatch_time_t unsavedTime; + dispatch_source_t saveTimer; + dispatch_time_t lastDeleteTime; + dispatch_source_t deleteTimer; } /** * Specifies how often to save the data to disk. * Since saving is an expensive operation (disk io) it is not done after every log statement. * These properties allow you to configure how/when the logger saves to disk. - * + * * A save is done when either (whichever happens first): - * + * * - The number of unsaved log entries reaches saveThreshold * - The amount of time since the oldest unsaved log entry was created reaches saveInterval - * + * * You can optionally disable the saveThreshold by setting it to zero. * If you disable the saveThreshold you are entirely dependent on the saveInterval. - * + * * You can optionally disable the saveInterval by setting it to zero (or a negative value). * If you disable the saveInterval you are entirely dependent on the saveThreshold. - * + * * It's not wise to disable both saveThreshold and saveInterval. - * + * * The default saveThreshold is 500. * The default saveInterval is 60 seconds. - **/ +**/ @property (assign, readwrite) NSUInteger saveThreshold; - -/** - * See the description for the `saveThreshold` property - */ @property (assign, readwrite) NSTimeInterval saveInterval; /** * It is likely you don't want the log entries to persist forever. * Doing so would allow the database to grow infinitely large over time. - * + * * The maxAge property provides a way to specify how old a log statement can get * before it should get deleted from the database. - * + * * The deleteInterval specifies how often to sweep for old log entries. * Since deleting is an expensive operation (disk io) is is done on a fixed interval. - * + * * An alternative to the deleteInterval is the deleteOnEverySave option. * This specifies that old log entries should be deleted during every save operation. - * + * * You can optionally disable the maxAge by setting it to zero (or a negative value). * If you disable the maxAge then old log statements are not deleted. - * + * * You can optionally disable the deleteInterval by setting it to zero (or a negative value). - * + * * If you disable both deleteInterval and deleteOnEverySave then old log statements are not deleted. - * + * * It's not wise to enable both deleteInterval and deleteOnEverySave. - * + * * The default maxAge is 7 days. * The default deleteInterval is 5 minutes. * The default deleteOnEverySave is NO. - **/ +**/ @property (assign, readwrite) NSTimeInterval maxAge; - -/** - * See the description for the `maxAge` property - */ @property (assign, readwrite) NSTimeInterval deleteInterval; - -/** - * See the description for the `maxAge` property - */ @property (assign, readwrite) BOOL deleteOnEverySave; /** * Forces a save of any pending log entries (flushes log entries to disk). - **/ +**/ - (void)savePendingLogEntries; /** * Removes any log entries that are older than maxAge. - **/ +**/ - (void)deleteOldLogEntries; @end diff --git a/Example/Pods/CocoaLumberjack/Lumberjack/DDAbstractDatabaseLogger.m b/Example/Pods/CocoaLumberjack/Lumberjack/DDAbstractDatabaseLogger.m new file mode 100644 index 0000000..05fcbcb --- /dev/null +++ b/Example/Pods/CocoaLumberjack/Lumberjack/DDAbstractDatabaseLogger.m @@ -0,0 +1,727 @@ +#import "DDAbstractDatabaseLogger.h" +#import + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/CocoaLumberjack/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/CocoaLumberjack/CocoaLumberjack/wiki/GettingStarted +**/ + +#if ! __has_feature(objc_arc) +#error This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +@interface DDAbstractDatabaseLogger () +- (void)destroySaveTimer; +- (void)destroyDeleteTimer; +@end + +#pragma mark - + +@implementation DDAbstractDatabaseLogger + +- (id)init +{ + if ((self = [super init])) + { + saveThreshold = 500; + saveInterval = 60; // 60 seconds + maxAge = (60 * 60 * 24 * 7); // 7 days + deleteInterval = (60 * 5); // 5 minutes + } + return self; +} + +- (void)dealloc +{ + [self destroySaveTimer]; + [self destroyDeleteTimer]; + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Override Me +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)db_log:(DDLogMessage *)logMessage +{ + // Override me and add your implementation. + // + // Return YES if an item was added to the buffer. + // Return NO if the logMessage was ignored. + + return NO; +} + +- (void)db_save +{ + // Override me and add your implementation. +} + +- (void)db_delete +{ + // Override me and add your implementation. +} + +- (void)db_saveAndDelete +{ + // Override me and add your implementation. +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Private API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)performSaveAndSuspendSaveTimer +{ + if (unsavedCount > 0) + { + if (deleteOnEverySave) + [self db_saveAndDelete]; + else + [self db_save]; + } + + unsavedCount = 0; + unsavedTime = 0; + + if (saveTimer && !saveTimerSuspended) + { + dispatch_suspend(saveTimer); + saveTimerSuspended = YES; + } +} + +- (void)performDelete +{ + if (maxAge > 0.0) + { + [self db_delete]; + + lastDeleteTime = dispatch_time(DISPATCH_TIME_NOW, 0); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Timers +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)destroySaveTimer +{ + if (saveTimer) + { + dispatch_source_cancel(saveTimer); + if (saveTimerSuspended) + { + // Must resume a timer before releasing it (or it will crash) + dispatch_resume(saveTimer); + saveTimerSuspended = NO; + } + #if !OS_OBJECT_USE_OBJC + dispatch_release(saveTimer); + #endif + saveTimer = NULL; + } +} + +- (void)updateAndResumeSaveTimer +{ + if ((saveTimer != NULL) && (saveInterval > 0.0) && (unsavedTime > 0.0)) + { + uint64_t interval = (uint64_t)(saveInterval * NSEC_PER_SEC); + dispatch_time_t startTime = dispatch_time(unsavedTime, interval); + + dispatch_source_set_timer(saveTimer, startTime, interval, 1.0); + + if (saveTimerSuspended) + { + dispatch_resume(saveTimer); + saveTimerSuspended = NO; + } + } +} + +- (void)createSuspendedSaveTimer +{ + if ((saveTimer == NULL) && (saveInterval > 0.0)) + { + saveTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, loggerQueue); + + dispatch_source_set_event_handler(saveTimer, ^{ @autoreleasepool { + + [self performSaveAndSuspendSaveTimer]; + + }}); + + saveTimerSuspended = YES; + } +} + +- (void)destroyDeleteTimer +{ + if (deleteTimer) + { + dispatch_source_cancel(deleteTimer); + #if !OS_OBJECT_USE_OBJC + dispatch_release(deleteTimer); + #endif + deleteTimer = NULL; + } +} + +- (void)updateDeleteTimer +{ + if ((deleteTimer != NULL) && (deleteInterval > 0.0) && (maxAge > 0.0)) + { + uint64_t interval = (uint64_t)(deleteInterval * NSEC_PER_SEC); + dispatch_time_t startTime; + + if (lastDeleteTime > 0) + startTime = dispatch_time(lastDeleteTime, interval); + else + startTime = dispatch_time(DISPATCH_TIME_NOW, interval); + + dispatch_source_set_timer(deleteTimer, startTime, interval, 1.0); + } +} + +- (void)createAndStartDeleteTimer +{ + if ((deleteTimer == NULL) && (deleteInterval > 0.0) && (maxAge > 0.0)) + { + deleteTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, loggerQueue); + + if (deleteTimer != NULL) { + dispatch_source_set_event_handler(deleteTimer, ^{ @autoreleasepool { + + [self performDelete]; + + }}); + + [self updateDeleteTimer]; + + if (deleteTimer != NULL) dispatch_resume(deleteTimer); + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSUInteger)saveThreshold +{ + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block NSUInteger result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(loggerQueue, ^{ + result = saveThreshold; + }); + }); + + return result; +} + +- (void)setSaveThreshold:(NSUInteger)threshold +{ + dispatch_block_t block = ^{ @autoreleasepool { + + if (saveThreshold != threshold) + { + saveThreshold = threshold; + + // Since the saveThreshold has changed, + // we check to see if the current unsavedCount has surpassed the new threshold. + // + // If it has, we immediately save the log. + + if ((unsavedCount >= saveThreshold) && (saveThreshold > 0)) + { + [self performSaveAndSuspendSaveTimer]; + } + } + }}; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +- (NSTimeInterval)saveInterval +{ + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block NSTimeInterval result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(loggerQueue, ^{ + result = saveInterval; + }); + }); + + return result; +} + +- (void)setSaveInterval:(NSTimeInterval)interval +{ + dispatch_block_t block = ^{ @autoreleasepool { + + // C99 recommended floating point comparison macro + // Read: isLessThanOrGreaterThan(floatA, floatB) + + if (/* saveInterval != interval */ islessgreater(saveInterval, interval)) + { + saveInterval = interval; + + // There are several cases we need to handle here. + // + // 1. If the saveInterval was previously enabled and it just got disabled, + // then we need to stop the saveTimer. (And we might as well release it.) + // + // 2. If the saveInterval was previously disabled and it just got enabled, + // then we need to setup the saveTimer. (Plus we might need to do an immediate save.) + // + // 3. If the saveInterval increased, then we need to reset the timer so that it fires at the later date. + // + // 4. If the saveInterval decreased, then we need to reset the timer so that it fires at an earlier date. + // (Plus we might need to do an immediate save.) + + if (saveInterval > 0.0) + { + if (saveTimer == NULL) + { + // Handles #2 + // + // Since the saveTimer uses the unsavedTime to calculate it's first fireDate, + // if a save is needed the timer will fire immediately. + + [self createSuspendedSaveTimer]; + [self updateAndResumeSaveTimer]; + } + else + { + // Handles #3 + // Handles #4 + // + // Since the saveTimer uses the unsavedTime to calculate it's first fireDate, + // if a save is needed the timer will fire immediately. + + [self updateAndResumeSaveTimer]; + } + } + else if (saveTimer) + { + // Handles #1 + + [self destroySaveTimer]; + } + } + }}; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +- (NSTimeInterval)maxAge +{ + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block NSTimeInterval result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(loggerQueue, ^{ + result = maxAge; + }); + }); + + return result; +} + +- (void)setMaxAge:(NSTimeInterval)interval +{ + dispatch_block_t block = ^{ @autoreleasepool { + + // C99 recommended floating point comparison macro + // Read: isLessThanOrGreaterThan(floatA, floatB) + + if (/* maxAge != interval */ islessgreater(maxAge, interval)) + { + NSTimeInterval oldMaxAge = maxAge; + NSTimeInterval newMaxAge = interval; + + maxAge = interval; + + // There are several cases we need to handle here. + // + // 1. If the maxAge was previously enabled and it just got disabled, + // then we need to stop the deleteTimer. (And we might as well release it.) + // + // 2. If the maxAge was previously disabled and it just got enabled, + // then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.) + // + // 3. If the maxAge was increased, + // then we don't need to do anything. + // + // 4. If the maxAge was decreased, + // then we should do an immediate delete. + + BOOL shouldDeleteNow = NO; + + if (oldMaxAge > 0.0) + { + if (newMaxAge <= 0.0) + { + // Handles #1 + + [self destroyDeleteTimer]; + } + else if (oldMaxAge > newMaxAge) + { + // Handles #4 + shouldDeleteNow = YES; + } + } + else if (newMaxAge > 0.0) + { + // Handles #2 + shouldDeleteNow = YES; + } + + if (shouldDeleteNow) + { + [self performDelete]; + + if (deleteTimer) + [self updateDeleteTimer]; + else + [self createAndStartDeleteTimer]; + } + } + }}; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +- (NSTimeInterval)deleteInterval +{ + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block NSTimeInterval result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(loggerQueue, ^{ + result = deleteInterval; + }); + }); + + return result; +} + +- (void)setDeleteInterval:(NSTimeInterval)interval +{ + dispatch_block_t block = ^{ @autoreleasepool { + + // C99 recommended floating point comparison macro + // Read: isLessThanOrGreaterThan(floatA, floatB) + + if (/* deleteInterval != interval */ islessgreater(deleteInterval, interval)) + { + deleteInterval = interval; + + // There are several cases we need to handle here. + // + // 1. If the deleteInterval was previously enabled and it just got disabled, + // then we need to stop the deleteTimer. (And we might as well release it.) + // + // 2. If the deleteInterval was previously disabled and it just got enabled, + // then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.) + // + // 3. If the deleteInterval increased, then we need to reset the timer so that it fires at the later date. + // + // 4. If the deleteInterval decreased, then we need to reset the timer so that it fires at an earlier date. + // (Plus we might need to do an immediate delete.) + + if (deleteInterval > 0.0) + { + if (deleteTimer == NULL) + { + // Handles #2 + // + // Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate, + // if a delete is needed the timer will fire immediately. + + [self createAndStartDeleteTimer]; + } + else + { + // Handles #3 + // Handles #4 + // + // Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate, + // if a save is needed the timer will fire immediately. + + [self updateDeleteTimer]; + } + } + else if (deleteTimer) + { + // Handles #1 + + [self destroyDeleteTimer]; + } + } + }}; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +- (BOOL)deleteOnEverySave +{ + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block BOOL result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(loggerQueue, ^{ + result = deleteOnEverySave; + }); + }); + + return result; +} + +- (void)setDeleteOnEverySave:(BOOL)flag +{ + dispatch_block_t block = ^{ + + deleteOnEverySave = flag; + }; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Public API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)savePendingLogEntries +{ + dispatch_block_t block = ^{ @autoreleasepool { + + [self performSaveAndSuspendSaveTimer]; + }}; + + if ([self isOnInternalLoggerQueue]) + block(); + else + dispatch_async(loggerQueue, block); +} + +- (void)deleteOldLogEntries +{ + dispatch_block_t block = ^{ @autoreleasepool { + + [self performDelete]; + }}; + + if ([self isOnInternalLoggerQueue]) + block(); + else + dispatch_async(loggerQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark DDLogger +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)didAddLogger +{ + // If you override me be sure to invoke [super didAddLogger]; + + [self createSuspendedSaveTimer]; + + [self createAndStartDeleteTimer]; +} + +- (void)willRemoveLogger +{ + // If you override me be sure to invoke [super willRemoveLogger]; + + [self performSaveAndSuspendSaveTimer]; + + [self destroySaveTimer]; + [self destroyDeleteTimer]; +} + +- (void)logMessage:(DDLogMessage *)logMessage +{ + if ([self db_log:logMessage]) + { + BOOL firstUnsavedEntry = (++unsavedCount == 1); + + if ((unsavedCount >= saveThreshold) && (saveThreshold > 0)) + { + [self performSaveAndSuspendSaveTimer]; + } + else if (firstUnsavedEntry) + { + unsavedTime = dispatch_time(DISPATCH_TIME_NOW, 0); + [self updateAndResumeSaveTimer]; + } + } +} + +- (void)flush +{ + // This method is invoked by DDLog's flushLog method. + // + // It is called automatically when the application quits, + // or if the developer invokes DDLog's flushLog method prior to crashing or something. + + [self performSaveAndSuspendSaveTimer]; +} + +@end diff --git a/Example/Pods/CocoaLumberjack/Lumberjack/DDAssert.h b/Example/Pods/CocoaLumberjack/Lumberjack/DDAssert.h new file mode 100644 index 0000000..4bad8e2 --- /dev/null +++ b/Example/Pods/CocoaLumberjack/Lumberjack/DDAssert.h @@ -0,0 +1,16 @@ +// +// DDAssert.h +// CocoaLumberjack +// +// Created by Ernesto Rivera on 2014/07/07. +// +// + +#import "DDLog.h" + +#define DDAssert(condition, frmt, ...) if (!(condition)) { \ + NSString * description = [NSString stringWithFormat:frmt, ##__VA_ARGS__]; \ + DDLogError(@"%@", description); \ + NSAssert(NO, description); } +#define DDAssertCondition(condition) DDAssert(condition, @"Condition not satisfied: %s", #condition) + diff --git a/Example/Pods/CocoaLumberjack/Classes/DDFileLogger.h b/Example/Pods/CocoaLumberjack/Lumberjack/DDFileLogger.h similarity index 51% rename from Example/Pods/CocoaLumberjack/Classes/DDFileLogger.h rename to Example/Pods/CocoaLumberjack/Lumberjack/DDFileLogger.h index f0bfdb6..b319c51 100644 --- a/Example/Pods/CocoaLumberjack/Classes/DDFileLogger.h +++ b/Example/Pods/CocoaLumberjack/Lumberjack/DDFileLogger.h @@ -1,74 +1,63 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -// Disable legacy macros -#ifndef DD_LEGACY_MACROS - #define DD_LEGACY_MACROS 0 -#endif - +#import #import "DDLog.h" @class DDLogFileInfo; /** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/CocoaLumberjack/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/CocoaLumberjack/CocoaLumberjack/wiki/GettingStarted + * + * * This class provides a logger to write log statements to a file. - **/ +**/ // Default configuration and safety/sanity values. -// -// maximumFileSize -> kDDDefaultLogMaxFileSize -// rollingFrequency -> kDDDefaultLogRollingFrequency -// maximumNumberOfLogFiles -> kDDDefaultLogMaxNumLogFiles -// logFilesDiskQuota -> kDDDefaultLogFilesDiskQuota -// +// +// maximumFileSize -> DEFAULT_LOG_MAX_FILE_SIZE +// rollingFrequency -> DEFAULT_LOG_ROLLING_FREQUENCY +// maximumNumberOfLogFiles -> DEFAULT_LOG_MAX_NUM_LOG_FILES +// logFilesDiskQuota -> DEFAULT_LOG_FILES_DISK_QUOTA +// // You should carefully consider the proper configuration values for your application. -extern unsigned long long const kDDDefaultLogMaxFileSize; -extern NSTimeInterval const kDDDefaultLogRollingFrequency; -extern NSUInteger const kDDDefaultLogMaxNumLogFiles; -extern unsigned long long const kDDDefaultLogFilesDiskQuota; +#define DEFAULT_LOG_MAX_FILE_SIZE (1024 * 1024) // 1 MB +#define DEFAULT_LOG_ROLLING_FREQUENCY (60 * 60 * 24) // 24 Hours +#define DEFAULT_LOG_MAX_NUM_LOG_FILES (5) // 5 Files +#define DEFAULT_LOG_FILES_DISK_QUOTA (20 * 1024 * 1024) // 20 MB //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -/** - * The LogFileManager protocol is designed to allow you to control all aspects of your log files. - * - * The primary purpose of this is to allow you to do something with the log files after they have been rolled. - * Perhaps you want to compress them to save disk space. - * Perhaps you want to upload them to an FTP server. - * Perhaps you want to run some analytics on the file. - * - * A default LogFileManager is, of course, provided. - * The default LogFileManager simply deletes old log files according to the maximumNumberOfLogFiles property. - * - * This protocol provides various methods to fetch the list of log files. - * - * There are two variants: sorted and unsorted. - * If sorting is not necessary, the unsorted variant is obviously faster. - * The sorted variant will return an array sorted by when the log files were created, - * with the most recently created log file at index 0, and the oldest log file at the end of the array. - * - * You can fetch only the log file paths (full path including name), log file names (name only), - * or an array of `DDLogFileInfo` objects. - * The `DDLogFileInfo` class is documented below, and provides a handy wrapper that - * gives you easy access to various file attributes such as the creation date or the file size. - */ +// The LogFileManager protocol is designed to allow you to control all aspects of your log files. +// +// The primary purpose of this is to allow you to do something with the log files after they have been rolled. +// Perhaps you want to compress them to save disk space. +// Perhaps you want to upload them to an FTP server. +// Perhaps you want to run some analytics on the file. +// +// A default LogFileManager is, of course, provided. +// The default LogFileManager simply deletes old log files according to the maximumNumberOfLogFiles property. +// +// This protocol provides various methods to fetch the list of log files. +// +// There are two variants: sorted and unsorted. +// If sorting is not necessary, the unsorted variant is obviously faster. +// The sorted variant will return an array sorted by when the log files were created, +// with the most recently created log file at index 0, and the oldest log file at the end of the array. +// +// You can fetch only the log file paths (full path including name), log file names (name only), +// or an array of DDLogFileInfo objects. +// The DDLogFileInfo class is documented below, and provides a handy wrapper that +// gives you easy access to various file attributes such as the creation date or the file size. + @protocol DDLogFileManager @required @@ -79,9 +68,9 @@ extern unsigned long long const kDDDefaultLogFilesDiskQuota; * For example, if this property is set to 3, * then the LogFileManager will only keep 3 archived log files (plus the current active log file) on disk. * Once the active log file is rolled/archived, then the oldest of the existing 3 rolled/archived log files is deleted. - * + * * You may optionally disable this option by setting it to zero. - **/ +**/ @property (readwrite, assign, atomic) NSUInteger maximumNumberOfLogFiles; /** @@ -89,75 +78,30 @@ extern unsigned long long const kDDDefaultLogFilesDiskQuota; * be deleted. * * You may optionally disable this option by setting it to zero. - **/ +**/ @property (readwrite, assign, atomic) unsigned long long logFilesDiskQuota; // Public methods -/** - * Returns the logs directory (path) - */ - (NSString *)logsDirectory; -/** - * Returns an array of `NSString` objects, - * each of which is the filePath to an existing log file on disk. - **/ - (NSArray *)unsortedLogFilePaths; - -/** - * Returns an array of `NSString` objects, - * each of which is the fileName of an existing log file on disk. - **/ - (NSArray *)unsortedLogFileNames; - -/** - * Returns an array of `DDLogFileInfo` objects, - * each representing an existing log file on disk, - * and containing important information about the log file such as it's modification date and size. - **/ - (NSArray *)unsortedLogFileInfos; -/** - * Just like the `unsortedLogFilePaths` method, but sorts the array. - * The items in the array are sorted by creation date. - * The first item in the array will be the most recently created log file. - **/ - (NSArray *)sortedLogFilePaths; - -/** - * Just like the `unsortedLogFileNames` method, but sorts the array. - * The items in the array are sorted by creation date. - * The first item in the array will be the most recently created log file. - **/ - (NSArray *)sortedLogFileNames; - -/** - * Just like the `unsortedLogFileInfos` method, but sorts the array. - * The items in the array are sorted by creation date. - * The first item in the array will be the most recently created log file. - **/ - (NSArray *)sortedLogFileInfos; // Private methods (only to be used by DDFileLogger) -/** - * Generates a new unique log file path, and creates the corresponding log file. - **/ - (NSString *)createNewLogFile; @optional // Notifications from DDFileLogger -/** - * Called when a log file was archieved - */ - (void)didArchiveLogFile:(NSString *)logFilePath; - -/** - * Called when the roll action was executed and the log was archieved - */ - (void)didRollAndArchiveLogFile:(NSString *)logFilePath; @end @@ -168,29 +112,29 @@ extern unsigned long long const kDDDefaultLogFilesDiskQuota; /** * Default log file manager. - * + * * All log files are placed inside the logsDirectory. * If a specific logsDirectory isn't specified, the default directory is used. - * On Mac, this is in `~/Library/Logs/`. - * On iPhone, this is in `~/Library/Caches/Logs`. - * - * Log files are named `" . + * + * You can also configure the extension to automatically sends requests. + * @see automaticallyRequestAcksAfterStanzaCount:orTimeout: + * + * When the server replies with an ack, the delegate method will be invoked. + * @see xmppStreamManagement:didReceiveAckForStanzaIds: +**/ +- (void)requestAck; + +/** + * The module can be configured to automatically request acks (send ) based on your criteria. + * The algorithm to do this takes into account: + * + * - The number of stanzas that have been sent since the last request was sent. + * - The amount of time that has elapsed since the first stanza (after the last request) was sent. + * + * So, for example, if you set the stanzaCount to 5, and the timeout to 2.0 seconds then: + * - Sending 5 stanzas back-to-back will automatically trigger an outgoing request + * - Sending 1 stanza will automatically trigger an outgoing request to be sent 2.0 seconds later, + * which will get preempted if 4 more stanzas are sent before the 2.0 second timer expires. + * + * In other words, whichever event takes place FIRST will trigger the request to be sent. + * + * You can disable either trigger by setting its value to zero. + * So, for example, if you only want to use a timeout of 5 seconds, + * then you could set the stanzaCount to zero and the timeout to 5 seconds. + * + * @param stanzaCount + * The stanzaCount to use for the auto request algorithm. + * If stanzaCount is zero, then the number of stanzas will be ignored in the algorithm. + * + * @param timeout + * The timeout to use for the auto request algorithm. + * If the timeout is zero (or negative), then the timer will be ignored in the algorithm. + * + * The default stanzaCount is 0 (disabled). + * The default timeout is 0.0 seconds (disabled). +**/ +- (void)automaticallyRequestAcksAfterStanzaCount:(NSUInteger)stanzaCount orTimeout:(NSTimeInterval)timeout; + +/** + * Returns the current auto-request configuration. + * + * @see automaticallyRequestAcksAfterStanzaCount:orTimeout: +**/ +- (void)getAutomaticallyRequestAcksAfterStanzaCount:(NSUInteger *)stanzaCountPtr orTimeout:(NSTimeInterval *)timeoutPtr; + + +#pragma mark Sending Acks + +/** + * Sends an unrequested ack element, acking the server's recently received (and handled) elements. + * + * You can also configure the extension to automatically sends acks. + * @see automaticallySendAcksAfterStanzaCount:orTimeout: + * + * Keep in mind that the extension will automatically send an ack if it receives an explicit request. +**/ +- (void)sendAck; + +/** + * The module can be configured to automatically send unrequested acks. + * That is, rather than waiting to receive explicit requests from the server, + * the client automatically sends them based on configurable criteria. + * + * The algorithm to do this takes into account: + * + * - The number of stanzas that have been received since the last ack was sent. + * - The amount of time that has elapsed since the first stanza (after the last ack) was received. + * + * In other words, whichever event takes place FIRST will trigger the request to be sent. + * You can disable either trigger by setting its value to zero. + * + * As would be expected, if you manually send an unrequested ack (via the sendAck method), + * or if an ack is sent out in response to a received request from the server, + * then the stanzaCount & timeout are reset. + * + * @param stanzaCount + * The stanzaCount to use for the auto ack algorithm. + * If stanzaCount is zero, then the number of stanzas will be ignored in the algorithm. + * + * @param timeout + * The timeout to sue fo the auto ack algorithm. + * If the timeout is zero (or negative), then the timer will be ignored in the algorithm. + * + * The default stanzaCount is 0 (disabled). + * The default timeout is 0.0 seconds (disabled). +**/ +- (void)automaticallySendAcksAfterStanzaCount:(NSUInteger)stanzaCount orTimeout:(NSTimeInterval)timeout; + +/** + * Returns the current "auto-send unrequested acks" configuration. + * + * @see automaticallySendAcksAfterStanzaCount:orTimeout: +**/ +- (void)getAutomaticallySendAcksAfterStanzaCount:(NSUInteger *)stanzaCountPtr orTimeout:(NSTimeInterval *)timeoutPtr; + +/** + * If an explicit request is received from the server, should we delay sending the ack ? + * From XEP-0198 : + * + * > When an element ("request") is received, the recipient MUST acknowledge it by sending an element + * > to the sender containing a value of 'h' that is equal to the number of stanzas handled by the recipient of + * > the element. The response SHOULD be sent as soon as possible after receiving the element, + * > and MUST NOT be withheld for any condition other than a timeout. For example, a client with a slow connection + * > might want to collect many stanzas over a period of time before acking, and a server might want to throttle + * > incoming stanzas. + * + * Thus the XEP recommends that you do not use a delay. + * However, it acknowledges that there may be certain situations in which a delay could prove helpful. + * + * The default value is 0.0 (as recommended by XEP-0198) +**/ +@property (atomic, assign, readwrite) NSTimeInterval ackResponseDelay; + +/** + * It's critically important to understand what an ACK means. + * + * Every ACK contains an 'h' attribute, which stands for "handled". + * To paraphrase XEP-0198 (in client-side terminology): + * + * Acknowledging a previously ­received element indicates that the stanza has been "handled" by the client. + * By "handled" we mean that the client has successfully processed the stanza + * (including possibly saving the item to the database if needed); + * Until a stanza has been affirmed as handled by the client, that stanza is the responsibility of the server + * (e.g., to resend it or generate an error if it is never affirmed as handled by the client). + * + * This means that if your processing of certain elements includes saving them to a database, + * then you should not mark those elements as handled until after your database has confirmed the data is on disk. + * + * You should note that this is a critical component of any networking app that claims to have "reliable messaging". + * + * By default, all elements will be marked as handled as soon as they arrive. + * You'll want to override the default behavior for important elements that require proper handling by your app. + * For example, messages that need to be saved to the database. + * Here's how to do so: + * + * - Implement the delegate method xmppStreamManagement:getIsHandled:stanzaId:forReceivedElement: + * + * This method is invoked for all received elements. + * You can inspect the element, and if it is important and requires special handling by the app, + * then flag the element as NOT handled (overriding the default). + * Also assign the element a "stanzaId". This can be anything you want, such as the elementID, + * or maybe something more app-specific (e.g. something you already use that's associated with the message). + * + * - Handle the important element however you need to + * + * If you're saving something to the database, + * then wait until after the database commit has completed successfully. + * + * - Notify the module that the element has been handled via the method markHandledStanzaId: + * + * You must pass the stanzaId that you returned from the delegate method. + * + * + * @see xmppStreamManagement:getIsHandled:stanzaId:forReceivedElement: +**/ +- (void)markHandledStanzaId:(id)stanzaId; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPStreamManagementDelegate +@optional + +/** + * Notifies delegates of the server's response from sending the stanza. +**/ +- (void)xmppStreamManagement:(XMPPStreamManagement *)sender wasEnabled:(NSXMLElement *)enabled; +- (void)xmppStreamManagement:(XMPPStreamManagement *)sender wasNotEnabled:(NSXMLElement *)failed; + +/** + * Notifies delegates that a request for an ack from the server was sent. +**/ +- (void)xmppStreamManagementDidRequestAck:(XMPPStreamManagement *)sender; + +/** + * Invoked when an ack is received from the server, and new stanzas have been acked. + * + * @param stanzaIds + * Includes all "stanzaIds" of sent elements that were just acked. + * + * What is a "stanzaId" ? + * + * A stanzaId is a unique identifier that ** YOU can provide ** in order to track an element. + * It could simply be the elementId of the sent element. Or, + * it could be something custom that you provide in order to properly lookup a message in your data store. + * + * For more information, see the delegate method xmppStreamManagement:stanzaIdForSentElement: +**/ +- (void)xmppStreamManagement:(XMPPStreamManagement *)sender didReceiveAckForStanzaIds:(NSArray *)stanzaIds; + +/** + * XEP-0198 reports the following regarding duplicate stanzas: + * + * Because unacknowledged stanzas might have been received by the other party, + * resending them might result in duplicates; there is no way to prevent such a + * result in this protocol, although use of the XMPP 'id' attribute on all stanzas + * can at least assist the intended recipients in weeding out duplicate stanzas. + * + * In other words, there are edge cases in which you might receive duplicates. + * And the proper way to fix this is to use some kind of identifier in order to detect duplicates. + * + * What kind of identifier to use is up to you. (It's app specific.) + * The XEP notes that you might use the 'id' attribute for this purpose. And this is certainly the most common case. + * However, you may have an alternative scheme that works better for your purposes. + * In which case you can use this delegate method to opt-in. + * + * For example: + * You store all your messages in YapDatabase, which is a collection/key/value storage system. + * Perhaps the collection is the conversationId, and the key is a messageId. + * Therefore, to efficiently lookup a message in your datastore you'd prefer a collection/key tuple. + * + * To achieve this, you would implement this method, and return a YapCollectionKey object for message elements. + * This way, when the xmppStreamManagement:didReceiveAckForStanzaIds: method is invoked, + * you'll get a list that contains your collection/key tuple objects. And then you can quickly and efficiently + * fetch and update your message objects. + * + * If there are no delegates that implement this method, + * or all delegates return nil, then the stanza's elementId is used as the stanzaId. + * + * If the stanza isn't assigned a stanzaId (via a delegate method), + * and it doesn't have an elementId, then it isn't reported in the acked stanzaIds array. +**/ +- (id)xmppStreamManagement:(XMPPStreamManagement *)sender stanzaIdForSentElement:(XMPPElement *)element; + +/** + * It's critically important to understand what an ACK means. + * + * Every ACK contains an 'h' attribute, which stands for "handled". + * To paraphrase XEP-0198 (in client-side terminology): + * + * Acknowledging a previously ­received element indicates that the stanza has been "handled" by the client. + * By "handled" we mean that the client has successfully processed the stanza + * (including possibly saving the item to the database if needed); + * Until a stanza has been affirmed as handled by the client, that stanza is the responsibility of the server + * (e.g., to resend it or generate an error if it is never affirmed as handled by the client). + * + * This means that if your processing of certain elements includes saving them to a database, + * then you should not mark those elements as handled until after your database has confirmed the data is on disk. + * + * You should note that this is a critical component of any networking app that claims to have "reliable messaging". + * + * By default, all elements will be marked as handled as soon as they arrive. + * You'll want to override the default behavior for important elements that require proper handling by your app. + * For example, messages that need to be saved to the database. + * Here's how to do so: + * + * - Implement the delegate method xmppStreamManagement:getIsHandled:stanzaId:forReceivedElement: + * + * This method is invoked for all received elements. + * You can inspect the element, and if it is important and requires special handling by the app, + * then flag the element as NOT handled (overriding the default). + * Also assign the element a "stanzaId". This can be anything you want, such as the elementID, + * or maybe something more app-specific (e.g. something you already use that's associated with the message). + * + * - Handle the important element however you need to + * + * If you're saving something to the database, + * then wait until after the database commit has completed successfully. + * + * - Notify the module that the element has been handled via the method markHandledStanzaId: + * + * You must pass the stanzaId that you returned from this delegate method. + * + * + * @see markHandledStanzaId: +**/ +- (void)xmppStreamManagement:(XMPPStreamManagement *)sender + getIsHandled:(BOOL *)isHandledPtr + stanzaId:(id *)stanzaIdPtr + forReceivedElement:(XMPPElement *)element; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPStreamManagementStorage +@required + +// +// +// -- PRIVATE METHODS -- +// +// These methods are designed to be used ONLY by the XMPPStreamManagement class. +// +// + +/** + * Configures the storage class, passing it's parent and the parent's dispatch queue. + * + * This method is called by the init methods of the XMPPStreamManagement class. + * This method is designed to inform the storage class of it's parent + * and of the dispatch queue the parent will be operating on. + * + * A storage class may choose to operate on the same queue as it's parent, + * as the majority of the time it will be getting called by the parent. + * If both are operating on the same queue, the combination may run faster. + * + * Some storage classes support multiple xmppStreams, + * and may choose to operate on their own internal queue. + * + * This method should return YES if it was configured properly. + * It should return NO only if configuration failed. + * For example, a storage class designed to be used only with a single xmppStream is being added to a second stream. +**/ +- (BOOL)configureWithParent:(XMPPStreamManagement *)parent queue:(dispatch_queue_t)queue; + +/** + * Invoked after we receive from the server. + * + * @param resumptionId + * The ID required to resume the session, given to us by the server. + * + * @param timeout + * The timeout in seconds. + * After a disconnect, the server will maintain our state for this long. + * If we attempt to resume the session after this timeout it likely won't work. + * + * @param lastDisconnect + * Used to reset the lastDisconnect value. + * This value is often updated during the session, to ensure it closely resemble the date the server will use. + * That is, if the client application is killed (or crashes) we want a relatively accurate lastDisconnect date. + * + * @param stream + * The associated xmppStream (standard parameter for storage classes) + * + * This method should also nil out the following values (if needed) associated with the account: + * - lastHandledByClient + * - lastHandledByServer + * - pendingOutgoingStanzas +**/ +- (void)setResumptionId:(NSString *)resumptionId + timeout:(uint32_t)timeout + lastDisconnect:(NSDate *)date + forStream:(XMPPStream *)stream; + +/** + * This method is invoked ** often ** during stream operation. + * It is not invoked when the xmppStream is disconnected. + * + * Important: See the note below: "Optimizing storage demands during active stream usage" + * + * @param date + * Updates the previous lastDisconnect value. + * + * @param lastHandledByClient + * The most recent 'h' value we can safely send to the server. + * + * @param stream + * The associated xmppStream (standard parameter for storage classes) +**/ +- (void)setLastDisconnect:(NSDate *)date + lastHandledByClient:(uint32_t)lastHandledByClient + forStream:(XMPPStream *)stream; + +/** + * This method is invoked ** often ** during stream operation. + * It is not invoked when the xmppStream is disconnected. + * + * Important: See the note below: "Optimizing storage demands during active stream usage" + * + * @param date + * Updates the previous lastDisconnect value. + * + * @param lastHandledByServer + * The most recent 'h' value we've received from the server. + * + * @param pendingOutgoingStanzas + * An array of XMPPStreamManagementOutgoingStanza objects. + * The storage layer is in charge of properly persisting this array, including: + * - the array count + * - the stanzaId of each element, including those that are nil + * + * @param stream + * The associated xmppStream (standard parameter for storage classes) +**/ +- (void)setLastDisconnect:(NSDate *)date + lastHandledByServer:(uint32_t)lastHandledByServer + pendingOutgoingStanzas:(NSArray *)pendingOutgoingStanzas + forStream:(XMPPStream *)stream; + + +/// ***** Optimizing storage demands during active stream usage ***** +/// +/// There are 2 methods that are invoked frequently during stream activity: +/// +/// - setLastDisconnect:lastHandledByClient:forStream: +/// - setLastDisconnect:lastHandledByServer:pendingOutgoingStanzas:forStream: +/// +/// They are invoked any time the 'h' values change, or whenver the pendingStanzaIds change. +/// In other words, they are invoked continually as stanzas get sent and received. +/// And it is the job of the storage layer to decide how to handle the traffic. +/// There are a few things to consider here: +/// +/// - How much chatter does the xmppStream do? +/// - How fast is the storage layer? +/// - How does the overhead on the storage layer affect the rest of the app? +/// +/// If your xmppStream isn't very chatty, and you've got a fast concurrent database, +/// then you may be able to simply pipe all these method calls to the database without thinking. +/// However, if your xmppStream is always constantly sending/receiving presence stanzas, and pinging the server, +/// then you might consider a bit of optimzation here. Below is a simple recommendation for how to accomplish this. +/// +/// You could choose to queue the changes from these method calls, and dump them to the database after a timeout. +/// Thus you'll be able to consolidate a large traffic surge into a small handful of database operations. +/// +/// Also, you could expose a 'flush' operation on the storage layer. +/// And invoke the flush operation when the app is backgrounded, or about to quit. + + +/** + * This method is invoked immediately after an accidental disconnect. + * And may be invoked post-disconnect if the state changes, such as for the following edge cases: + * + * - due to continued processing of stanzas received pre-disconnect, + * that are just now being marked as handled by the delegate(s) + * - due to a delayed response from the delegate(s), + * such that we didn't receive the stanzaId for an outgoing stanza until after the disconnect occurred. + * + * This method is not invoked if stream management is started on a connected xmppStream. + * + * @param date + * This value will be the actual disconnect date. + * + * @param lastHandledByClient + * The most recent 'h' value we can safely send to the server. + * + * @param lastHandledByServer + * The most recent 'h' value we've received from the server. + * + * @param pendingOutgoingStanzas + * An array of XMPPStreamManagementOutgoingStanza objects. + * The storage layer is in charge of properly persisting this array, including: + * - the array count + * - the stanzaId of each element, including those that are nil + * + * @param stream + * The associated xmppStream (standard parameter for storage classes) +**/ +- (void)setLastDisconnect:(NSDate *)date + lastHandledByClient:(uint32_t)lastHandledByClient + lastHandledByServer:(uint32_t)lastHandledByServer + pendingOutgoingStanzas:(NSArray *)pendingOutgoingStanzas + forStream:(XMPPStream *)stream; + +/** + * Invoked when the extension needs values from a previous session. + * This method is used to get values needed in order to determine if it can resume a previous stream. +**/ +- (void)getResumptionId:(NSString **)resumptionIdPtr + timeout:(uint32_t *)timeoutPtr + lastDisconnect:(NSDate **)lastDisconnectPtr + forStream:(XMPPStream *)stream; + +/** + * Invoked when the extension needs values from a previous session. + * This method is used to get values needed in order to resume a previous stream. +**/ +- (void)getLastHandledByClient:(uint32_t *)lastHandledByClientPtr + lastHandledByServer:(uint32_t *)lastHandledByServerPtr + pendingOutgoingStanzas:(NSArray **)pendingOutgoingStanzasPtr + forStream:(XMPPStream *)stream; + +/** + * Instructs the storage layer to remove all values stored for the given stream. + * This occurs after the extension detects a "cleanly closed stream", + * in which case the stream cannot be resumed next time. +**/ +- (void)removeAllForStream:(XMPPStream *)stream; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPStream (XMPPStreamManagement) + +/** + * Returns whether or not the server's includes . +**/ +- (BOOL)supportsStreamManagement; + +@end diff --git a/Extensions/XEP-0198/XMPPStreamManagement.m b/Extensions/XEP-0198/XMPPStreamManagement.m new file mode 100644 index 0000000..8e49094 --- /dev/null +++ b/Extensions/XEP-0198/XMPPStreamManagement.m @@ -0,0 +1,1929 @@ +#import "XMPPStreamManagement.h" +#import "XMPPStreamManagementStanzas.h" +#import "XMPPInternal.h" +#import "XMPPTimer.h" +#import "XMPPLogging.h" +#import "NSNumber+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +// Log flags: trace +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +/** + * Define various xmlns values. +**/ +#define XMLNS_STREAM_MANAGEMENT @"urn:xmpp:sm:3" + +/** + * Seeing a return statements within an inner block + * can sometimes be mistaken for a return point of the enclosing method. + * This makes inline blocks a bit easier to read. +**/ +#define return_from_block return + + +@implementation XMPPStreamManagement +{ + // Storage module (may be nil) + + id storage; + + // State machine + + BOOL isStarted; // either or received from server + BOOL enableQueued; // the element is queued in xmppStream + BOOL enableSent; // the element has been sent through xmppStream + + BOOL wasCleanDisconnect; // xmppStream sent + + BOOL didAttemptResume; + BOOL didResume; + + NSXMLElement *resume_response; + NSArray *resume_stanzaIds; + + NSDate *disconnectDate; + + // Configuration + + BOOL autoResume; + + NSUInteger autoRequest_stanzaCount; + NSTimeInterval autoRequest_timeout; + + NSUInteger autoAck_stanzaCount; + NSTimeInterval autoAck_timeout; + + NSTimeInterval ackResponseDelay; + + // Enable + + uint32_t requestedMax; + + // Tracking outgoing stanzas + + uint32_t lastHandledByServer; // last h value received from server + + NSMutableArray *unackedByServer; // array of XMPPStreamManagementOutgoingStanza objects + NSUInteger unackedByServer_lastRequestOffset; // represents point at which we last sent a request + + NSArray *prev_unackedByServer; // from previous connection, used when resuming session + + NSMutableArray *unprocessedReceivedAcks; // acks received from server that we haven't processed yet + + XMPPTimer *autoRequestTimer; // timer to fire a request + + // Tracking incoming stanzas + + uint32_t lastHandledByClient; // latest h value we can send to the server + + NSMutableArray *unackedByClient; // array of XMPPStreamManagementIncomingStanza objects + NSUInteger unackedByClient_lastAckOffset; // number of items removed from array, but ack not sent to server + + NSMutableArray *pendingHandledStanzaIds;// edge case handling + NSUInteger outstandingStanzaIds; // edge case handling + defensive programming + + XMPPTimer *autoAckTimer; // timer to fire ack at server + XMPPTimer *ackResponseTimer; // timer for ackResponseDelay +} + +@synthesize storage = storage; + +- (id)init +{ + // This will cause a crash - it's designed to. + // Only the init methods listed in XMPPStreamManagement.h are supported. + + return [self initWithStorage:nil dispatchQueue:NULL]; +} + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + // This will cause a crash - it's designed to. + // Only the init methods listed in XMPPStreamManagement.h are supported. + + return [self initWithStorage:nil dispatchQueue:queue]; +} + +- (id)initWithStorage:(id )inStorage +{ + return [self initWithStorage:inStorage dispatchQueue:NULL]; +} + +- (id)initWithStorage:(id )inStorage dispatchQueue:(dispatch_queue_t)queue +{ + if ((self = [super initWithDispatchQueue:queue])) + { + if ([inStorage configureWithParent:self queue:moduleQueue]) { + storage = inStorage; + } + else { + XMPPLogError(@"%@: %@ - Unable to configure storage!", THIS_FILE, THIS_METHOD); + } + + unackedByServer = [[NSMutableArray alloc] init]; + unackedByClient = [[NSMutableArray alloc] init]; + } + return self; +} + +- (NSSet *)xep0198Elements +{ + return [NSSet setWithObjects:@"r", @"a", @"enable", @"enabled", @"resume", @"resumed", @"failed", nil]; +} + +- (void)didActivate +{ + [xmppStream registerCustomElementNames:[self xep0198Elements]]; +} + +- (void)didDeactivate +{ + [xmppStream unregisterCustomElementNames:[self xep0198Elements]]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)autoResume +{ + XMPPLogTrace(); + + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = autoResume; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (void)setAutoResume:(BOOL)newAutoResume +{ + XMPPLogTrace(); + + dispatch_block_t block = ^{ + autoResume = newAutoResume; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (void)automaticallyRequestAcksAfterStanzaCount:(NSUInteger)stanzaCount orTimeout:(NSTimeInterval)timeout +{ + XMPPLogTrace(); + + dispatch_block_t block = ^{ @autoreleasepool{ + + autoRequest_stanzaCount = stanzaCount; + autoRequest_timeout = MAX(0.0, timeout); + + if (autoRequestTimer) { + [autoRequestTimer updateTimeout:autoRequest_timeout fromOriginalStartTime:YES]; + } + if (isStarted) { + [self maybeRequestAck]; + } + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (void)getAutomaticallyRequestAcksAfterStanzaCount:(NSUInteger *)stanzaCountPtr orTimeout:(NSTimeInterval *)timeoutPtr +{ + XMPPLogTrace(); + + __block NSUInteger stanzaCount = 0; + __block NSTimeInterval timeout = 0.0; + + dispatch_block_t block = ^{ + + stanzaCount = autoRequest_stanzaCount; + timeout = autoRequest_timeout; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + if (stanzaCountPtr) *stanzaCountPtr = stanzaCount; + if (timeoutPtr) *timeoutPtr = timeout; +} + +- (void)automaticallySendAcksAfterStanzaCount:(NSUInteger)stanzaCount orTimeout:(NSTimeInterval)timeout +{ + XMPPLogTrace(); + + dispatch_block_t block = ^{ @autoreleasepool{ + + autoAck_stanzaCount = stanzaCount; + autoAck_timeout = MAX(0.0, timeout); + + if (autoAckTimer) { + [autoAckTimer updateTimeout:autoAck_timeout fromOriginalStartTime:YES]; + } + if (isStarted) { + [self maybeSendAck]; + } + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (void)getAutomaticallySendAcksAfterStanzaCount:(NSUInteger *)stanzaCountPtr orTimeout:(NSTimeInterval *)timeoutPtr +{ + XMPPLogTrace(); + + __block NSUInteger stanzaCount = 0; + __block NSTimeInterval timeout = 0.0; + + dispatch_block_t block = ^{ + + stanzaCount = autoAck_stanzaCount; + timeout = autoAck_timeout; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + if (stanzaCountPtr) *stanzaCountPtr = stanzaCount; + if (timeoutPtr) *timeoutPtr = timeout; +} + +- (NSTimeInterval)ackResponseDelay +{ + XMPPLogTrace(); + + __block NSUInteger delay = 0.0; + + dispatch_block_t block = ^{ + + delay = ackResponseDelay; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return delay; +} + +- (void)setAckResponseDelay:(NSTimeInterval)delay +{ + XMPPLogTrace(); + + dispatch_block_t block = ^{ + + ackResponseDelay = delay; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Enable +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method sends the stanza to the server to request enabling stream management. + * + * XEP-0198 specifies that the stanza should only be sent by clients after authentication, + * and after binding has occurred. + * + * The servers response is reported via the delegate methods: + * @see xmppStreamManagement:wasEnabled: + * @see xmppStreamManagement:wasNotEnabled: + * + * @param supportsResumption + * Whether the client should request resumptions support. + * If YES, the resume attribute will be included. E.g. + * + * @param maxTimeout + * Allows you to specify the client's preferred maximum resumption time. + * This is optional, and will only be sent if you provide a positive value (maxTimeout > 0.0). + * Note that XEP-0198 only supports sending this value in seconds. + * So it the provided maxTimeout includes millisecond precision, this will be ignored via truncation + * (rounding down to nearest whole seconds value). + * + * @see supportsStreamManagement +**/ +- (void)enableStreamManagementWithResumption:(BOOL)supportsResumption maxTimeout:(NSTimeInterval)maxTimeout +{ + dispatch_block_t block = ^{ @autoreleasepool{ + + if (isStarted) + { + XMPPLogWarn(@"Stream management is already enabled/resumed."); + return; + } + if (enableQueued || enableSent) + { + XMPPLogWarn(@"Stream management is already started (pending response from server)."); + return; + } + + // State transition cleanup + + [unackedByServer removeAllObjects]; + unackedByServer_lastRequestOffset = 0; + + [unackedByClient removeAllObjects]; + unackedByClient_lastAckOffset = 0; + + unprocessedReceivedAcks = nil; + + pendingHandledStanzaIds = nil; + outstandingStanzaIds = 0; + + // Send enable stanza: + // + // + + NSXMLElement *enable = [NSXMLElement elementWithName:@"enable" xmlns:XMLNS_STREAM_MANAGEMENT]; + + if (supportsResumption) { + [enable addAttributeWithName:@"resume" stringValue:@"true"]; + } + if (maxTimeout > 0.0) { + [enable addAttributeWithName:@"max" stringValue:[NSString stringWithFormat:@"%.0f", maxTimeout]]; + } + + [xmppStream sendElement:enable]; + + enableQueued = YES; + requestedMax = (maxTimeout > 0.0) ? (uint32_t)maxTimeout : (uint32_t)0; + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Resume +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Utility method for handling canResume logic. +**/ +- (BOOL)canResumeStreamWithResumptionId:(NSString *)resumptionId + timeout:(uint32_t)timeout + lastDisconnect:(NSDate *)lastDisconnect +{ + if (resumptionId == nil) { + XMPPLogVerbose(@"%@: Cannot resume stream: resumptionId is nil", THIS_FILE); + return NO; + } + if (lastDisconnect == nil) { + XMPPLogVerbose(@"%@: Cannot resume stream: lastDisconnect is nil", THIS_FILE); + return NO; + } + + NSTimeInterval elapsed = [lastDisconnect timeIntervalSinceNow] * -1.0; + + if (elapsed < 0.0) // lastDisconnect is in the future ? + { + XMPPLogVerbose(@"%@: Cannot resume stream: invalid lastDisconnect - appears to be in future", THIS_FILE); + return NO; + } + if ((uint32_t)elapsed > timeout) // too much time has elapsed + { + XMPPLogVerbose(@"%@: Cannot resume stream: elapsed(%u) > timeout(%u)", THIS_FILE, (uint32_t)elapsed, timeout); + return NO; + } + + return YES; +} + +/** + * Returns YES if the stream can be resumed. + * + * This would be the case if there's an available resumptionId for the authenticated xmppStream, + * and the timeout from the last stream has not been exceeded. +**/ +- (BOOL)canResumeStream +{ + XMPPLogTrace(); + + // This is a PUBLIC method + + __block BOOL result = NO; + + dispatch_block_t block = ^{ @autoreleasepool{ + + if (isStarted || enableQueued || enableSent) { + return_from_block; + } + + NSString *resumptionId = nil; + uint32_t timeout = 0; + NSDate *lastDisconnect = nil; + + [storage getResumptionId:&resumptionId + timeout:&timeout + lastDisconnect:&lastDisconnect + forStream:xmppStream]; + + result = [self canResumeStreamWithResumptionId:resumptionId timeout:timeout lastDisconnect:lastDisconnect]; + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +/** + * Internal method that handles sending the element, and the corresponding state transition. +**/ +- (void)sendResumeRequestWithResumptionId:(NSString *)resumptionId +{ + XMPPLogTrace(); + + dispatch_block_t block = ^{ @autoreleasepool { + + // State transition cleanup + + [unackedByServer removeAllObjects]; + unackedByServer_lastRequestOffset = 0; + + [unackedByClient removeAllObjects]; + unackedByClient_lastAckOffset = 0; + + unprocessedReceivedAcks = nil; + + pendingHandledStanzaIds = nil; + outstandingStanzaIds = 0; + + // Restore our state from the last stream + + uint32_t newLastHandledByClient = 0; + uint32_t newLastHandledByServer = 0; + NSArray *pendingOutgoingStanzas = nil; + + [storage getLastHandledByClient:&newLastHandledByClient + lastHandledByServer:&newLastHandledByServer + pendingOutgoingStanzas:&pendingOutgoingStanzas + forStream:xmppStream]; + + lastHandledByClient = newLastHandledByClient; + lastHandledByServer = newLastHandledByServer; + + if ([pendingOutgoingStanzas count] > 0) { + prev_unackedByServer = [[NSMutableArray alloc] initWithArray:pendingOutgoingStanzas copyItems:YES]; + } + + XMPPLogVerbose(@"%@: Attempting to resume: lastHandledByClient(%u) lastHandledByServer(%u)", + THIS_FILE, lastHandledByClient, lastHandledByServer); + + // Send the resume stanza: + // + // + + NSXMLElement *resume = [NSXMLElement elementWithName:@"resume" xmlns:XMLNS_STREAM_MANAGEMENT]; + [resume addAttributeWithName:@"previd" stringValue:resumptionId]; + [resume addAttributeWithName:@"h" stringValue:[NSString stringWithFormat:@"%u", lastHandledByClient]]; + + [xmppStream sendBindElement:resume]; + + didAttemptResume = YES; + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +/** + * Internal method to handle processing a resumed response from the server. +**/ +- (void)processResumed:(NSXMLElement *)resumed +{ + XMPPLogTrace(); + + dispatch_block_t block = ^{ @autoreleasepool { + + uint32_t h = [resumed attributeUInt32ValueForName:@"h" withDefaultValue:lastHandledByServer]; + + uint32_t diff; + if (h >= lastHandledByServer) + diff = h - lastHandledByServer; + else + diff = (UINT32_MAX - lastHandledByServer) + h; + + // IMPORTATNT: + // This code path uses prev_unackedByServer (NOT unackedByServer). + // This is because the ack has to do with stanzas sent from the previous connection. + + if (diff > [prev_unackedByServer count]) + { + XMPPLogWarn(@"Unexpected h value from resume: lastH=%lu, newH=%lu, numPendingStanzas=%lu", + (unsigned long)lastHandledByServer, + (unsigned long)h, + (unsigned long)[prev_unackedByServer count]); + + diff = (uint32_t)[prev_unackedByServer count]; + } + + NSMutableArray *stanzaIds = [NSMutableArray arrayWithCapacity:(NSUInteger)diff]; + + for (uint32_t i = 0; i < diff; i++) + { + XMPPStreamManagementOutgoingStanza *outgoingStanza = prev_unackedByServer[(NSUInteger) i]; + + if (outgoingStanza.stanzaId) { + [stanzaIds addObject:outgoingStanza.stanzaId]; + } + } + + lastHandledByServer = h; + + XMPPLogVerbose(@"%@: processResumed: lastHandledByServer(%u)", THIS_FILE, lastHandledByServer); + + isStarted = YES; + didResume = YES; + + prev_unackedByServer = nil; + + resume_response = resumed; + resume_stanzaIds = [stanzaIds copy]; + + // Update storage + + [storage setLastDisconnect:[NSDate date] + lastHandledByServer:lastHandledByServer + pendingOutgoingStanzas:nil + forStream:xmppStream]; + + // Notify delegate + + [multicastDelegate xmppStreamManagement:self didReceiveAckForStanzaIds:stanzaIds]; + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +/** + * This method is meant to be called by other extensions when they receive an xmppStreamDidAuthenticate callback. + * + * Returns YES if the stream was resumed during the authentication process. + * Returns NO otherwise (if resume wasn't available, or it failed). + * + * Other extensions may wish to skip certain setup processes that aren't + * needed if the stream was resumed (since the previous session state has been restored server-side). +**/ +- (BOOL)didResume +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = didResume; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +/** + * This method is meant to be called when you receive an xmppStreamDidAuthenticate callback. + * + * It is used instead of a standard delegate method in order to provide a cleaner API. + * By using this method, one can put all the logic for handling authentication in a single place. + * But more importantly, it solves several subtle timing and threading issues. + * + * > A delegate method could have hit either before or after xmppStreamDidAuthenticate, depending on thread scheduling. + * > We could have queued it up, and forced it to hit after. + * > But your code would likely still have needed to add a check within xmppStreamDidAuthenticate... + * + * @param stanzaIdsPtr (optional) + * Just like the stanzaIdsPtr provided in xmppStreamManagement:didReceiveAckForStanzaIds:. + * This comes from the h value provided within the stanza sent by the server. + * + * @param responsePtr (optional) + * Returns the response we got from the server. Either or . + * This will be nil if resume wasn't tried. + * + * @return + * YES if the stream was resumed. + * NO otherwise. +**/ +- (BOOL)didResumeWithAckedStanzaIds:(NSArray **)stanzaIdsPtr + serverResponse:(NSXMLElement **)responsePtr +{ + __block BOOL result = NO; + __block NSArray *stanzaIds = nil; + __block NSXMLElement *response = nil; + + dispatch_block_t block = ^{ + + result = didResume; + stanzaIds = resume_stanzaIds; + response = resume_response; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + if (stanzaIdsPtr) *stanzaIdsPtr = stanzaIds; + if (responsePtr) *responsePtr = response; + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPCustomBinding Protocol +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Attempts to start the custom binding process. + * + * If it isn't possible to start the process (perhaps due to missing information), + * this method should return XMPP_BIND_FAIL and set an appropriate error message. + * + * If binding isn't needed (for example, because custom SASL authentication already handled it), + * this method should return XMPP_BIND_SUCCESS. + * In this case, xmppStream will immediately move to its post-binding operations. + * + * Otherwise this method should send whatever stanzas are needed to begin the binding process. + * And then return XMPP_BIND_CONTINUE. + * + * This method is called by automatically XMPPStream. + * You MUST NOT invoke this method manually. +**/ +- (XMPPBindResult)start:(NSError **)errPtr +{ + XMPPLogTrace(); + + // Fetch the resumptionId, + // and check to see if we can resume the stream. + + NSString *resumptionId = nil; + uint32_t timeout = 0; + NSDate *lastDisconnect = nil; + + [storage getResumptionId:&resumptionId + timeout:&timeout + lastDisconnect:&lastDisconnect + forStream:xmppStream]; + + if (![self canResumeStreamWithResumptionId:resumptionId timeout:timeout lastDisconnect:lastDisconnect]) + { + return XMPP_BIND_FAIL_FALLBACK; + } + + // Start the resume proces + [self sendResumeRequestWithResumptionId:resumptionId]; + + return XMPP_BIND_CONTINUE; +} + +/** + * After the custom binding process has started, all incoming xmpp stanzas are routed to this method. + * The method should process the stanza as appropriate, and return the coresponding result. + * If the process is not yet complete, it should return XMPP_BIND_CONTINUE, + * meaning the xmpp stream will continue to forward all incoming xmpp stanzas to this method. + * + * This method is called automatically by XMPPStream. + * You MUST NOT invoke this method manually. +**/ +- (XMPPBindResult)handleBind:(NSXMLElement *)element withError:(NSError **)errPtr +{ + XMPPLogTrace(); + + NSString *elementName = [element name]; + + if ([elementName isEqualToString:@"resumed"]) + { + [self processResumed:element]; + + return XMPP_BIND_SUCCESS; + } + else + { + if (![elementName isEqualToString:@"failed"]) { + XMPPLogError(@"%@: Received unrecognized response from server: %@", THIS_METHOD, element); + } + + dispatch_async(moduleQueue, ^{ @autoreleasepool { + + didResume = NO; + resume_response = element; + + prev_unackedByServer = nil; + }}); + + return XMPP_BIND_FAIL_FALLBACK; + } +} + +/** + * Optionally implement this method to override the default behavior. + * By default behavior, we mean the behavior normally taken by xmppStream, which is: + * + * - IF the server includes in its stream:features + * - AND xmppStream.skipStartSession property is NOT set + * - THEN xmppStream will send the session start request, and await the response before transitioning to authenticated + * + * Thus if you implement this method and return YES, then xmppStream will skip starting a session, + * regardless of the stream:features and the current xmppStream.skipStartSession property value. + * + * If you implement this method and return NO, then xmppStream will follow the default behavior detailed above. + * This means that, even if this method returns NO, the xmppStream may still skip starting a session if + * the server doesn't require it via its stream:features, + * or if the user has explicitly forbidden it via the xmppStream.skipStartSession property. + * + * The default value is NO. +**/ +- (BOOL)shouldSkipStartSessionAfterSuccessfulBinding +{ + return YES; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Requesting Acks +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Sends a request element, requesting the server reply with an ack . + * + * You can also configure the extension to automatically sends requests. + * @see automaticallyRequestAcksAfterStanzaCount:orTimeout: + * + * When the server replies with an ack, the delegate method will be invoked. + * @see xmppStreamManagement:didReceiveAckForStanzaIds: +**/ +- (void)requestAck +{ + XMPPLogTrace(); + + // This is a PUBLIC method + + dispatch_block_t block = ^{ @autoreleasepool{ + + if (isStarted || enableQueued || enableSent) + { + [self _requestAck]; + } + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (void)_requestAck +{ + XMPPLogTrace(); + + if (isStarted || enableQueued || enableSent) + { + // Send the XML element + + NSXMLElement *r = [NSXMLElement elementWithName:@"r" xmlns:XMLNS_STREAM_MANAGEMENT]; + [xmppStream sendElement:r]; + + // Reset offset + + unackedByServer_lastRequestOffset = [unackedByServer count]; + } + + [autoRequestTimer cancel]; + autoRequestTimer = nil; +} + +- (BOOL)maybeRequestAck +{ + XMPPLogTrace(); + + if (!isStarted && !(enableQueued || enableSent)) + { + // cannot request ack if not started (or at least sent ) + return NO; + } + if ((autoRequest_stanzaCount == 0) && (autoRequest_timeout == 0.0)) + { + // auto request disabled + return NO; + } + + NSUInteger pending = [unackedByServer count] - unackedByServer_lastRequestOffset; + if (pending == 0) + { + // nothing new to request + return NO; + } + + if ((autoRequest_stanzaCount > 0) && (pending >= autoRequest_stanzaCount)) + { + [self _requestAck]; + return YES; + } + else if ((autoRequest_timeout > 0.0) && (autoRequestTimer == nil)) + { + __weak id weakSelf = self; + autoRequestTimer = [[XMPPTimer alloc] initWithQueue:moduleQueue eventHandler:^{ @autoreleasepool{ + + [weakSelf _requestAck]; + }}]; + + [autoRequestTimer startWithTimeout:autoRequest_timeout interval:0]; + } + + return NO; +} + +/** + * This method is invoked from one of the xmppStream:didSendX: methods. +**/ +- (void)processSentElement:(XMPPElement *)element +{ + XMPPLogTrace(); + + SEL selector = @selector(xmppStreamManagement:stanzaIdForSentElement:); + + if (![multicastDelegate hasDelegateThatRespondsToSelector:selector]) + { + // There are not any delegates that respond to the selector. + // So the stanzaId is the elementId (if there is one). + + NSString *elementId = [element elementID]; + + XMPPStreamManagementOutgoingStanza *stanza = + [[XMPPStreamManagementOutgoingStanza alloc] initWithStanzaId:elementId]; + [unackedByServer addObject:stanza]; + + [self updateStoredPendingOutgoingStanzas]; + + // At bottom of this method: + // [self maybeRequestAck]; + } + else + { + // We need to query the delegate(s) to see if there's a specific stanzaId for this element. + // This is an asynchronous process, so we put a placeholder in the array for now. + + XMPPStreamManagementOutgoingStanza *stanza = + [[XMPPStreamManagementOutgoingStanza alloc] initAwaitingStanzaId]; + [unackedByServer addObject:stanza]; + + // Start the asynchronous process to find the proper stanzaId + + GCDMulticastDelegateEnumerator *enumerator = [multicastDelegate delegateEnumerator]; + + dispatch_queue_t concurrentQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(concurrentQ, ^{ @autoreleasepool { + + id stanzaId = nil; + + id delegate = nil; + dispatch_queue_t dq = NULL; + + while ([enumerator getNextDelegate:&delegate delegateQueue:&dq forSelector:selector]) + { + stanzaId = [delegate xmppStreamManagement:self stanzaIdForSentElement:element]; + if (stanzaId) + { + break; + } + } + + if (stanzaId == nil) + { + stanzaId = [element elementID]; + } + + dispatch_async(moduleQueue, ^{ @autoreleasepool{ + + // Set the stanzaId. + stanza.stanzaId = stanzaId; + stanza.awaitingStanzaId = NO; + + // It's possible that we received an ack from the sever (acking our stanza) + // before we were able to determine its stanzaId. + // This edge case is handled by storing the ack in the pendingAcks array for later processing. + // We may be able to process it now. + + BOOL dequeuedPendingAck = NO; + + while ([unprocessedReceivedAcks count] > 0) + { + NSXMLElement *ack = unprocessedReceivedAcks[0]; + + if ([self processReceivedAck:ack]) + { + [unprocessedReceivedAcks removeObjectAtIndex:0]; + dequeuedPendingAck = YES; + } + else + { + break; + } + } + + if (!dequeuedPendingAck) + { + [self updateStoredPendingOutgoingStanzas]; + } + }}); + }}); + } + + XMPPLogVerbose(@"%@: processSentElement (%@): lastHandledByServer(%u) pending(%lu)", + THIS_FILE, [element name], lastHandledByServer, (unsigned long)[unackedByServer count]); + + [self maybeRequestAck]; +} + +/** + * This method is invoked when an ack arrives. + * + * It attempts to process the ack. + * That is, there should be adequate outgoing stanzas (in the unackedByServer array) which have a set stanzaId. + * + * Because stanzaId's are set by the delegate(s), its possible (although unlikely) that we receive an ack before + * the delegate tells us the proper stanzaId for a sent element. When this occurs, we won't be able to completely + * process the ack. However, this method will process as many as possible (while maintaining serial order). + * + * @return + * YES if the ack can be marked as 100% processed. + * NO otherwise (if we're still awaiting a stanzaId from a delegate), + * in which case the caller MUST store the ack in the unprocessedReceivedAcks array. +**/ +- (BOOL)processReceivedAck:(NSXMLElement *)ack +{ + XMPPLogTrace(); + + uint32_t h = 0; + if (![NSNumber xmpp_parseString:[ack attributeStringValueForName:@"h"] intoUInt32:&h]) + { + XMPPLogError(@"Error parsing h value from ack: %@", [ack compactXMLString]); + return YES; + } + + uint32_t diff; + if (h >= lastHandledByServer) + diff = h - lastHandledByServer; + else + diff = (UINT32_MAX - lastHandledByServer) + h; + + if (diff == 0) + { + // shortcut: server is reporting no new stanzas have been processed + return YES; + } + + if (diff > [unackedByServer count]) + { + XMPPLogWarn(@"Unexpected h value from ack: lastH=%lu, newH=%lu, numPendingStanzas=%lu", + (unsigned long)lastHandledByServer, + (unsigned long)h, + (unsigned long)[unackedByServer count]); + + diff = (uint32_t)[unackedByServer count]; + } + + BOOL canProcessEntireAck = YES; + NSUInteger processed = 0; + + NSMutableArray *stanzaIds = [NSMutableArray arrayWithCapacity:(NSUInteger)diff]; + + for (uint32_t i = 0; i < diff; i++) + { + XMPPStreamManagementOutgoingStanza *outgoingStanza = unackedByServer[(NSUInteger) i]; + + if ([outgoingStanza awaitingStanzaId]) + { + canProcessEntireAck = NO; + break; + } + else + { + if (outgoingStanza.stanzaId) { + [stanzaIds addObject:outgoingStanza.stanzaId]; + } + processed++; + } + } + + if (canProcessEntireAck || processed > 0) + { + if (canProcessEntireAck) + { + [unackedByServer removeObjectsInRange:NSMakeRange(0, (NSUInteger)diff)]; + if (unackedByServer_lastRequestOffset > diff) + unackedByServer_lastRequestOffset -= diff; + else + unackedByServer_lastRequestOffset = 0; + + lastHandledByServer = h; + + XMPPLogVerbose(@"%@: processReceivedAck (fully processed): lastHandledByServer(%u) pending(%lu)", + THIS_FILE, lastHandledByServer, (unsigned long)[unackedByServer count]); + } + else // if (processed > 0) + { + [unackedByServer removeObjectsInRange:NSMakeRange(0, processed)]; + if (unackedByServer_lastRequestOffset > processed) + unackedByServer_lastRequestOffset -= processed; + else + unackedByServer_lastRequestOffset = 0; + + lastHandledByServer += processed; + + XMPPLogVerbose(@"%@: processReceivedAck (partially processed): lastHandledByServer(%u) pending(%lu)", + THIS_FILE, lastHandledByServer, (unsigned long)[unackedByServer count]); + } + + // Update storage + + NSArray *pending = [[NSArray alloc] initWithArray:unackedByServer copyItems:YES]; + + if (isStarted) + { + [storage setLastDisconnect:[NSDate date] + lastHandledByServer:lastHandledByServer + pendingOutgoingStanzas:pending + forStream:xmppStream]; + } + else // edge case + { + [storage setLastDisconnect:disconnectDate + lastHandledByClient:lastHandledByClient + lastHandledByServer:lastHandledByServer + pendingOutgoingStanzas:pending + forStream:xmppStream]; + } + + // Notify delegate + + [multicastDelegate xmppStreamManagement:self didReceiveAckForStanzaIds:stanzaIds]; + } + else + { + XMPPLogVerbose(@"%@: processReceivedAck (unprocessed): lastHandledByServer(%u) pending(%lu)", + THIS_FILE, lastHandledByServer, (unsigned long)[unackedByServer count]); + } + + return canProcessEntireAck; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Sending Acks +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Sends an unrequested ack element, acking the server's recently received (and handled) elements. + * + * You can also configure the extension to automatically sends acks. + * @see automaticallySendAcksAfterStanzaCount:orTimeout: + * + * Keep in mind that the extension will automatically send an ack if it receives an explicit request. +**/ +- (void)sendAck +{ + XMPPLogTrace(); + + // This is a PUBLIC method + + dispatch_block_t block = ^{ @autoreleasepool{ + + if (isStarted) + { + [self _sendAck]; + } + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +/** + * Sends the ack element, and discards newly acked stanzas from the queue. +**/ +- (void)_sendAck +{ + NSUInteger pending = 0; + for (XMPPStreamManagementIncomingStanza *stanza in unackedByClient) + { + if (stanza.isHandled) + pending++; + else + break; + } + + if (pending > 0) + { + [unackedByClient removeObjectsInRange:NSMakeRange(0, pending)]; + unackedByClient_lastAckOffset += pending; + lastHandledByClient += pending; + + XMPPLogVerbose(@"%@: sendAck: lastHandledByClient(%u) inc(%lu) totalPending(%lu)", THIS_FILE, + lastHandledByClient, + (unsigned long)pending, + (unsigned long)unackedByClient_lastAckOffset); + + // Update info in storage. + + if (isStarted) + { + [storage setLastDisconnect:[NSDate date] + lastHandledByClient:lastHandledByClient + forStream:xmppStream]; + } + else // edge case + { + // An incoming stanza got markedAsHandled post-disconnect + + NSArray *pending = [[NSArray alloc] initWithArray:unackedByServer copyItems:YES]; + + [storage setLastDisconnect:disconnectDate + lastHandledByClient:lastHandledByClient + lastHandledByServer:lastHandledByServer + pendingOutgoingStanzas:pending + forStream:xmppStream]; + } + } + + if (isStarted) + { + // Send the XML element + + NSXMLElement *a = [NSXMLElement elementWithName:@"a" xmlns:XMLNS_STREAM_MANAGEMENT]; + + NSString *h = [NSString stringWithFormat:@"%u", (unsigned int)lastHandledByClient]; + [a addAttributeWithName:@"h" stringValue:h]; + + [xmppStream sendElement:a]; + + // Reset offset + + unackedByClient_lastAckOffset = 0; + } + + // Stop the timer(s) + + [autoAckTimer cancel]; + autoAckTimer = nil; + + [ackResponseTimer cancel]; + ackResponseTimer = nil; + +} + +/** + * Returns the number of incoming stanzas that have been handled on our side, + * but which we haven't yet sent an ack to the server. +**/ +- (NSUInteger)numIncomingStanzasThatCanBeAcked +{ + // What is unackedByClient_lastAckOffset ? + // + // In the method maybeUpdateStoredLastHandledByClient, + // we remove items from the unackedByClient array, and increase the lastHandledByClient value. + // But we do NOT actually send an ack to the server at this point. + // + // Thus unackedByClient_lastAckOffset represents the number of items we're removed from the unackedByClient array, + // and for which we still need to send an ack to the server. + + NSUInteger count = unackedByClient_lastAckOffset; + + for (XMPPStreamManagementIncomingStanza *stanza in unackedByClient) + { + if (stanza.isHandled) + count++; + else + break; + } + + return count; +} + +/** + * Returns the number of incoming stanzas that cannot yet be acked because + * - the stanza hasn't been marked as handled yet + * - or a preceeding stanza has hasn't been marked as handled yet +**/ +- (NSUInteger)numIncomingStanzasThatCannnotBeAcked +{ + BOOL foundUnhandledStanza = NO; + NSUInteger count = 0; + + for (XMPPStreamManagementIncomingStanza *stanza in unackedByClient) + { + if (foundUnhandledStanza) + { + count++; + } + else if (!stanza.isHandled) + { + foundUnhandledStanza = YES; + count++; + } + } + + return count; +} + +/** + * Sends an ack if needed (if pending meets/exceeds autoAck_stanzaCount). +**/ +- (BOOL)maybeSendAck +{ + XMPPLogTrace(); + + if (!isStarted) + { + // cannot send acks if we're not started + return NO; + } + if ((autoAck_stanzaCount == 0) && (autoAck_timeout == 0.0)) + { + // auto ack disabled + return NO; + } + + NSUInteger pending = [self numIncomingStanzasThatCanBeAcked]; + if (pending == 0) + { + // nothing new to ack + return NO; + } + + // Send ack according to autoAck configuration + + if ((autoAck_stanzaCount > 0) && (pending >= autoAck_stanzaCount)) + { + [self _sendAck]; + return YES; + } + else if ((autoAck_timeout > 0.0) && (autoAckTimer == nil)) + { + __weak id weakSelf = self; + autoAckTimer = [[XMPPTimer alloc] initWithQueue:moduleQueue eventHandler:^{ @autoreleasepool{ + + [weakSelf sendAck]; + }}]; + + [autoAckTimer startWithTimeout:autoAck_timeout interval:0]; + } + + return NO; +} + +- (void)markHandledStanzaId:(id)stanzaId +{ + XMPPLogTrace(); + + if (stanzaId == nil) return; + + dispatch_block_t block = ^{ @autoreleasepool { + + // It's theoretically possible that the delegate(s) returned the same stanzaId for multiple elements. + // Although this is strongly discouraged, we should try to do our best to handle such a situation logically. + // + // In light of this edge case, here are the rules: + // + // Find the first stanza in the queue that is + // - not already marked as handled + // - has a matching stanzaId + // + // Mark this as handled, and then break. + // + // We also check to see if marking this stanza as handled has increased the pending count. + // For example (using the following queue): + // + // 0) + // 1) // <-- marking as handled increases pendingCount from 1 to 2 + // 2) // <-- marking as handled doesn't change pendingCount (still 1) + + BOOL found = NO; + + for (XMPPStreamManagementIncomingStanza *stanza in unackedByClient) + { + if (stanza.isHandled) + { + // continue + } + else if ([stanza.stanzaId isEqual:stanzaId]) + { + stanza.isHandled = YES; + found = YES; + break; + } + } + + if (found) + { + if (![self maybeSendAck]) + { + [self maybeUpdateStoredLastHandledByClient]; + } + } + else + { + // Edge case: + // + // The stanzaId was marked as handled before we finished figuring out what the stanzaId is. + // + // In order to get the stanzaId for a received element, we go through an asynchronous process. + // It's possible (but unlikely) that this process ends up taking longer than it does for the app + // to actually "handle" the element. So we have this odd edge case, + // which we handle by queuing up the stanzaId for later processing. + + if (outstandingStanzaIds > 0) + { + if (pendingHandledStanzaIds == nil) + pendingHandledStanzaIds = [[NSMutableArray alloc] init]; + + [pendingHandledStanzaIds addObject:stanzaId]; + } + } + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (void)processReceivedElement:(XMPPElement *)element +{ + XMPPLogTrace(); + + NSAssert(isStarted, @"State machine exception"); + + SEL selector = @selector(xmppStreamManagement:getIsHandled:stanzaId:forReceivedElement:); + + if (![multicastDelegate hasDelegateThatRespondsToSelector:selector]) + { + // None of the delegates implement the method. + // Use a shortcut. + + XMPPStreamManagementIncomingStanza *stanza = + [[XMPPStreamManagementIncomingStanza alloc] initWithStanzaId:nil isHandled:YES]; + [unackedByClient addObject:stanza]; + + // Since we know the element is 'handled' we can immediately check to see if we need to send an ack + + if (![self maybeSendAck]) + { + [self maybeUpdateStoredLastHandledByClient]; + } + } + else + { + // We need to query the delegate(s) to see if the stanza can be marked as handled. + // This is an asynchronous process, so we put a placeholder in the array for now. + // + // Note: stanza.isHandled == NO + + XMPPStreamManagementIncomingStanza *stanza = + [[XMPPStreamManagementIncomingStanza alloc] initWithStanzaId:nil isHandled:NO]; + [unackedByClient addObject:stanza]; + + // Query the delegate(s). The Rules: + // + // If ANY of the delegates says the element is "not handled", then we can immediately set it as so. + // Otherwise the element will be marked as handled. + + GCDMulticastDelegateEnumerator *enumerator = [multicastDelegate delegateEnumerator]; + outstandingStanzaIds++; + + dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(concurrentQueue, ^{ @autoreleasepool + { + __block BOOL isHandled = YES; + __block id stanzaId = nil; + + id delegate; + dispatch_queue_t dq; + + while (isHandled && [enumerator getNextDelegate:&delegate delegateQueue:&dq forSelector:selector]) + { + dispatch_sync(dq, ^{ @autoreleasepool { + + [delegate xmppStreamManagement:self + getIsHandled:&isHandled + stanzaId:&stanzaId + forReceivedElement:element]; + + NSAssert(isHandled || stanzaId != nil, + @"You MUST return a stanzaId for any elements you mark as not-yet-handled"); + }}); + } + + dispatch_async(moduleQueue, ^{ @autoreleasepool + { + if (isHandled) + { + stanza.isHandled = YES; + } + else + { + stanza.stanzaId = stanzaId; + + // Check for edge case: + // - stanzaId was marked as handled before we figured out what the stanzaId was + if ([pendingHandledStanzaIds count] > 0) + { + NSUInteger i = 0; + for (id pendingStanzaId in pendingHandledStanzaIds) + { + if ([pendingStanzaId isEqual:stanzaId]) + { + [pendingHandledStanzaIds removeObjectAtIndex:i]; + + stanza.isHandled = YES; + break; + } + + i++; + } + } + } + + // Defensive programming. + // Don't let this array grow infinitely big (if markHandledStanzaId is being invoked incorrectly). + if (--outstandingStanzaIds == 0) { + [pendingHandledStanzaIds removeAllObjects]; + } + + if (stanza.isHandled) + { + if (![self maybeSendAck]) + { + [self maybeUpdateStoredLastHandledByClient]; + } + } + }}); + }}); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Storage Helpers +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method is used when the pendingStanzaIds have changed (ivar unackedByServer changed), + * but we weren't able to process an ack, or update the lastHandledByServer. +**/ +- (void)updateStoredPendingOutgoingStanzas +{ + XMPPLogTrace(); + + NSArray *pending = [[NSArray alloc] initWithArray:unackedByServer copyItems:YES]; + + if (isStarted) + { + [storage setLastDisconnect:[NSDate date] + lastHandledByServer:lastHandledByServer + pendingOutgoingStanzas:pending + forStream:xmppStream]; + } + else + { + [storage setLastDisconnect:disconnectDate + lastHandledByClient:lastHandledByClient + lastHandledByServer:lastHandledByServer + pendingOutgoingStanzas:pending + forStream:xmppStream]; + } +} + +/** + * This method is used when we can maybe increment the lastHandledByClient value, + * but the change isn't significant enough to trigger an autoAck (or autoAck_stanzaCount is disabled). + * + * It updates the lastHandledByClient value (if needed), and notified storage. +**/ +- (void)maybeUpdateStoredLastHandledByClient +{ + XMPPLogTrace(); + + // Edge case note: + // + // This method may be invoked shortly after being disconnected. + // How is this handled? + // + // The unackedByClient array is cleared when we send or . + // And it cannot be appended to unless isStarted is YES. + // Thus this method works properly shortly after a disconnect, and can increment lastHandledByClient. + // And properly handles the edge case of being called in the middle of resuming a session. + + NSUInteger pending = 0; + for (XMPPStreamManagementIncomingStanza *stanza in unackedByClient) + { + if (stanza.isHandled) + pending++; + else + break; + } + + if (pending > 0) + { + [unackedByClient removeObjectsInRange:NSMakeRange(0, pending)]; + unackedByClient_lastAckOffset += pending; + lastHandledByClient += pending; + + XMPPLogVerbose(@"%@: sendAck: lastHandledByClient(%u) inc(%lu) totalPending(%lu)", THIS_FILE, + lastHandledByClient, + (unsigned long)pending, + (unsigned long)unackedByClient_lastAckOffset); + + if (isStarted) + { + [storage setLastDisconnect:[NSDate date] + lastHandledByClient:lastHandledByClient + forStream:xmppStream]; + } + else // edge case + { + // An incoming stanza got markedAsHandled post-disconnect + + NSArray *pending = [[NSArray alloc] initWithArray:unackedByServer copyItems:YES]; + + [storage setLastDisconnect:disconnectDate + lastHandledByClient:lastHandledByClient + lastHandledByServer:lastHandledByServer + pendingOutgoingStanzas:pending + forStream:xmppStream]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPStream Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Binding a JID resource is a standard part of the authentication process, + * and occurs after SASL authentication completes (which generally authenticates the JID username). + * + * This delegate method allows for a custom binding procedure to be used. + * For example: + * - a custom SASL authentication scheme might combine auth with binding + * - stream management (xep-0198) replaces binding if it can resume a previous session + * + * Return nil (or don't implement this method) if you wish to use the standard binding procedure. +**/ +- (id )xmppStreamWillBind:(XMPPStream *)sender +{ + if (autoResume) + { + // We will check canResume in start: method (part of XMPPCustomBinding protocol) + return self; + } + else + { + return nil; + } +} + +- (void)xmppStream:(XMPPStream *)sender didSendIQ:(XMPPIQ *)iq +{ + XMPPLogTrace(); + + if (isStarted || enableSent) + { + [self processSentElement:iq]; + } +} + +- (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message +{ + XMPPLogTrace(); + + if (isStarted || enableSent) + { + [self processSentElement:message]; + } +} + +- (void)xmppStream:(XMPPStream *)sender didSendPresence:(XMPPPresence *)presence +{ + XMPPLogTrace(); + + if (isStarted || enableSent) + { + [self processSentElement:presence]; + } +} + +- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq +{ + XMPPLogTrace(); + + if (isStarted) + { + [self processReceivedElement:iq]; + } + + return NO; +} + +- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message +{ + XMPPLogTrace(); + + if (isStarted) + { + [self processReceivedElement:message]; + } +} + +- (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence +{ + XMPPLogTrace(); + + if (isStarted) + { + [self processReceivedElement:presence]; + } +} + +/** + * This method is called if any of the xmppStream:willReceiveX: methods filter the incoming stanza. + * + * It may be useful for some extensions to know that something was received, + * even if it was filtered for some reason. +**/ +- (void)xmppStreamDidFilterStanza:(XMPPStream *)sender +{ + XMPPLogTrace(); + + if (isStarted) + { + // The element was filtered/consumed by something in the stack. + // So it is implicitly 'handled'. + + XMPPStreamManagementIncomingStanza *stanza = + [[XMPPStreamManagementIncomingStanza alloc] initWithStanzaId:nil isHandled:YES]; + [unackedByClient addObject:stanza]; + + XMPPLogVerbose(@"%@: xmppStreamDidFilterStanza: lastHandledByClient(%u) pendingToAck(%lu), pendingHandled(%lu)", + THIS_FILE, lastHandledByClient, + (unsigned long)[self numIncomingStanzasThatCanBeAcked], + (unsigned long)[self numIncomingStanzasThatCannnotBeAcked]); + + if (![self maybeSendAck]) + { + [self maybeUpdateStoredLastHandledByClient]; + } + } +} + +- (void)xmppStream:(XMPPStream *)sender didSendCustomElement:(NSXMLElement *)element +{ + XMPPLogTrace(); + + if (enableQueued) + { + if ([[element name] isEqualToString:@"enable"]) + { + enableQueued = NO; + enableSent = YES; + } + } + else if (isStarted) + { + if ([[element name] isEqualToString:@"r"]) + { + [multicastDelegate xmppStreamManagementDidRequestAck:self]; + } + } +} + +- (void)xmppStream:(XMPPStream *)sender didReceiveCustomElement:(NSXMLElement *)element +{ + XMPPLogTrace(); + + NSString *elementName = [element name]; + + if ([elementName isEqualToString:@"r"]) + { + // We received a request from the server. + + if (ackResponseDelay <= 0.0) + { + // Immediately respond to the request, + // as recommended in the XEP. + + [self _sendAck]; + } + else if (ackResponseTimer == nil) + { + // Use client-configured delay before responding to the request. + + __weak id weakSelf = self; + ackResponseTimer = [[XMPPTimer alloc] initWithQueue:moduleQueue eventHandler:^{ @autoreleasepool{ + + [weakSelf _sendAck]; + }}]; + + [ackResponseTimer startWithTimeout:ackResponseDelay interval:0]; + } + } + else if ([elementName isEqualToString:@"a"]) + { + // Try to process the ack. + // If we can't yet, then we'll put it into the pendingAcks array. + + if (![self processReceivedAck:element]) + { + if (unprocessedReceivedAcks == nil) + unprocessedReceivedAcks = [[NSMutableArray alloc] initWithCapacity:1]; + + [unprocessedReceivedAcks addObject:element]; + } + } + else if ([elementName isEqualToString:@"enabled"]) + { + if (enableSent) + { + // + + NSString *resumptionId = nil; + uint32_t max = 0; + + BOOL canResume = [element attributeBoolValueForName:@"resume" withDefaultValue:NO]; + if (canResume) + { + resumptionId = [element attributeStringValueForName:@"id"]; + max = [element attributeUInt32ValueForName:@"max" withDefaultValue:requestedMax]; + } + + [storage setResumptionId:resumptionId + timeout:max + lastDisconnect:[NSDate date] + forStream:xmppStream]; + + [multicastDelegate xmppStreamManagement:self wasEnabled:element]; + + isStarted = YES; + enableSent = NO; + + lastHandledByClient = 0; + lastHandledByServer = 0; + + unprocessedReceivedAcks = nil; + } + else + { + XMPPLogWarn(@"Received unrequested stanza"); + } + } + else if ([elementName isEqualToString:@"failed"]) + { + if (enableSent) + { + [storage removeAllForStream:xmppStream]; + + [multicastDelegate xmppStreamManagement:self wasNotEnabled:element]; + + isStarted = NO; + enableSent = NO; + + [autoRequestTimer cancel]; + autoRequestTimer = nil; + } + } +} + +- (void)xmppStreamDidSendClosingStreamStanza:(XMPPStream *)sender +{ + XMPPLogTrace(); + + wasCleanDisconnect = YES; +} + +- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error +{ + XMPPLogTrace(); + + if (wasCleanDisconnect) + { + disconnectDate = nil; + [storage removeAllForStream:xmppStream]; + } + else + { + disconnectDate = [NSDate date]; + NSArray *pending = [[NSArray alloc] initWithArray:unackedByServer copyItems:YES]; + + [storage setLastDisconnect:disconnectDate + lastHandledByClient:lastHandledByClient + lastHandledByServer:lastHandledByServer + pendingOutgoingStanzas:pending + forStream:xmppStream]; + } + + // Reset temporary state variables + + isStarted = NO; + enableQueued = NO; + enableSent = NO; + + wasCleanDisconnect = NO; + + didAttemptResume = NO; + didResume = NO; + + prev_unackedByServer = nil; + + resume_response = nil; + resume_stanzaIds = nil; + + // Cancel timers + + [autoRequestTimer cancel]; + autoRequestTimer = nil; + + [autoAckTimer cancel]; + autoAckTimer = nil; + + [ackResponseTimer cancel]; + ackResponseTimer = nil; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPStream (XMPPStreamManagement) + +- (BOOL)supportsStreamManagement +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ @autoreleasepool { + + // The root element can be properly queried anytime after the + // stream:features are received, and TLS has been setup (if required). + + if (self.state >= STATE_XMPP_POST_NEGOTIATION) + { + NSXMLElement *features = [self.rootElement elementForName:@"stream:features"]; + NSXMLElement *sm = [features elementForName:@"sm" xmlns:XMLNS_STREAM_MANAGEMENT]; + + result = (sm != nil); + } + }}; + + if (dispatch_get_specific(self.xmppQueueTag)) + block(); + else + dispatch_sync(self.xmppQueue, block); + + return result; +} + +@end diff --git a/Extensions/XEP-0199/XMPPAutoPing.h b/Extensions/XEP-0199/XMPPAutoPing.h new file mode 100644 index 0000000..a224f7f --- /dev/null +++ b/Extensions/XEP-0199/XMPPAutoPing.h @@ -0,0 +1,102 @@ +#import +#import "XMPPModule.h" +#import "XMPPPing.h" + +#define _XMPP_AUTO_PING_H + +@class XMPPJID; + + +/** + * The XMPPAutoPing module sends pings on a designated interval to the target. + * The target may simply be the server, or a specific resource. + * + * The module only sends pings as needed. + * If the xmpp stream is receiving data from the target, there's no need to send a ping. + * Only when no data has been received from the target is a ping sent. +**/ +@interface XMPPAutoPing : XMPPModule { +@private + NSTimeInterval pingInterval; + NSTimeInterval pingTimeout; + XMPPJID *targetJID; + NSString *targetJIDStr; + + NSTimeInterval lastReceiveTime; + dispatch_source_t pingIntervalTimer; + + BOOL awaitingPingResponse; + XMPPPing *xmppPing; +} + +/** + * How often to send a ping. + * + * The internal timer fires every (pingInterval / 4) seconds. + * Upon firing it checks when data was last received from the target, + * and sends a ping if the elapsed time has exceeded the pingInterval. + * Thus the effective resolution of the timer is based on the configured interval. + * + * To temporarily disable auto-ping, set the interval to zero. + * + * The default pingInterval is 60 seconds. +**/ +@property (readwrite) NSTimeInterval pingInterval; + +/** + * How long to wait after sending a ping before timing out. + * + * The timeout is decoupled from the pingInterval to allow for longer pingIntervals, + * which avoids flooding the network, and to allow more precise control overall. + * + * After a ping is sent, if a reply is not received by this timeout, + * the delegate method is invoked. + * + * The default pingTimeout is 10 seconds. +**/ +@property (readwrite) NSTimeInterval pingTimeout; + +/** + * The target to send pings to. + * + * If the targetJID is nil, this implies the target is the xmpp server we're connected to. + * In this case, receiving any data means we've received data from the target. + * + * If the targetJID is non-nil, it must be a full JID (user@domain.tld/rsrc). + * In this case, the module will monitor the stream for data from the given JID. + * + * The default targetJID is nil. +**/ +@property (readwrite, strong) XMPPJID *targetJID; + +/** + * Corresponds to the last time data was received from the target. + * The NSTimeInterval value comes from [NSDate timeIntervalSinceReferenceDate] +**/ +@property (readonly) NSTimeInterval lastReceiveTime; + +/** + * XMPPAutoPing is used to automatically send pings on a regular interval. + * Sometimes the target is also sending pings to us as well. + * If so, you may optionally set respondsToQueries to YES to allow the module to respond to incoming pings. + * + * If you create multiple instances of XMPPAutoPing or XMPPPing, + * then only one instance should respond to queries. + * + * The default value is NO. +**/ +@property (readwrite) BOOL respondsToQueries; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPAutoPingDelegate +@optional + +- (void)xmppAutoPingDidSendPing:(XMPPAutoPing *)sender; +- (void)xmppAutoPingDidReceivePong:(XMPPAutoPing *)sender; + +- (void)xmppAutoPingDidTimeout:(XMPPAutoPing *)sender; + +@end diff --git a/Extensions/XEP-0199/XMPPAutoPing.m b/Extensions/XEP-0199/XMPPAutoPing.m new file mode 100644 index 0000000..5936158 --- /dev/null +++ b/Extensions/XEP-0199/XMPPAutoPing.m @@ -0,0 +1,416 @@ +#import "XMPPAutoPing.h" +#import "XMPPPing.h" +#import "XMPP.h" +#import "XMPPLogging.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 + +@interface XMPPAutoPing () +- (void)updatePingIntervalTimer; +- (void)startPingIntervalTimer; +- (void)stopPingIntervalTimer; +@end + +#pragma mark - + +@implementation XMPPAutoPing + +- (id)init +{ + return [self initWithDispatchQueue:NULL]; +} + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + if ((self = [super initWithDispatchQueue:queue])) + { + pingInterval = 60; + pingTimeout = 10; + + lastReceiveTime = 0; + + xmppPing = [[XMPPPing alloc] initWithDispatchQueue:queue]; + xmppPing.respondsToQueries = NO; + + [xmppPing addDelegate:self delegateQueue:moduleQueue]; + } + return self; +} + +- (BOOL)activate:(XMPPStream *)aXmppStream +{ + if ([super activate:aXmppStream]) + { + [xmppPing activate:aXmppStream]; + + return YES; + } + + return NO; +} + +- (void)deactivate +{ + dispatch_block_t block = ^{ @autoreleasepool { + + [self stopPingIntervalTimer]; + + lastReceiveTime = 0; + awaitingPingResponse = NO; + + [xmppPing deactivate]; + [super deactivate]; + + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); +} + +- (void)dealloc +{ + + [self stopPingIntervalTimer]; + + [xmppPing removeDelegate:self]; + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Properties +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSTimeInterval)pingInterval +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return pingInterval; + } + else + { + __block NSTimeInterval result; + + dispatch_sync(moduleQueue, ^{ + result = pingInterval; + }); + return result; + } +} + +- (void)setPingInterval:(NSTimeInterval)interval +{ + dispatch_block_t block = ^{ + + if (pingInterval != interval) + { + pingInterval = interval; + + // Update the pingTimer. + // + // Depending on new value and current state of the pingTimer, + // this may mean starting, stoping, or simply updating the timer. + + if (pingInterval > 0) + { + // Remember: Only start the pinger after the xmpp stream is up and authenticated + if ([xmppStream isAuthenticated]) + [self startPingIntervalTimer]; + } + else + { + [self stopPingIntervalTimer]; + } + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (NSTimeInterval)pingTimeout +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return pingTimeout; + } + else + { + __block NSTimeInterval result; + + dispatch_sync(moduleQueue, ^{ + result = pingTimeout; + }); + return result; + } +} + +- (void)setPingTimeout:(NSTimeInterval)timeout +{ + dispatch_block_t block = ^{ + + if (pingTimeout != timeout) + { + pingTimeout = timeout; + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (XMPPJID *)targetJID +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return targetJID; + } + else + { + __block XMPPJID *result; + + dispatch_sync(moduleQueue, ^{ + result = targetJID; + }); + return result; + } +} + +- (void)setTargetJID:(XMPPJID *)jid +{ + dispatch_block_t block = ^{ + + if (![targetJID isEqualToJID:jid]) + { + targetJID = jid; + + targetJIDStr = [targetJID full]; + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (NSTimeInterval)lastReceiveTime +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return lastReceiveTime; + } + else + { + __block NSTimeInterval result; + + dispatch_sync(moduleQueue, ^{ + result = lastReceiveTime; + }); + return result; + } +} + +- (BOOL)respondsToQueries +{ + return xmppPing.respondsToQueries; +} + +- (void)setRespondsToQueries:(BOOL)respondsToQueries +{ + xmppPing.respondsToQueries = respondsToQueries; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Ping Interval +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)handlePingIntervalTimerFire +{ + if (awaitingPingResponse) return; + + BOOL sendPing = NO; + + if (lastReceiveTime == 0) + { + sendPing = YES; + } + else + { + NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate]; + NSTimeInterval elapsed = (now - lastReceiveTime); + + XMPPLogTrace2(@"%@: %@ - elapsed(%f)", [self class], THIS_METHOD, elapsed); + + sendPing = ((elapsed < 0) || (elapsed >= pingInterval)); + } + + if (sendPing) + { + awaitingPingResponse = YES; + + if (targetJID) + [xmppPing sendPingToJID:targetJID withTimeout:pingTimeout]; + else + [xmppPing sendPingToServerWithTimeout:pingTimeout]; + + [multicastDelegate xmppAutoPingDidSendPing:self]; + } +} + +- (void)updatePingIntervalTimer +{ + XMPPLogTrace(); + + NSAssert(pingIntervalTimer != NULL, @"Broken logic (1)"); + NSAssert(pingInterval > 0, @"Broken logic (2)"); + + // The timer fires every (pingInterval / 4) seconds. + // Upon firing it checks when data was last received from the target, + // and sends a ping if the elapsed time has exceeded the pingInterval. + // Thus the effective resolution of the timer is based on the configured pingInterval. + + uint64_t interval = ((pingInterval / 4.0) * NSEC_PER_SEC); + + // The timer's first fire should occur 'interval' after lastReceiveTime. + // If there is no lastReceiveTime, then the timer's first fire should occur 'interval' after now. + + NSTimeInterval diff; + if (lastReceiveTime == 0) + diff = 0.0; + else + diff = lastReceiveTime - [NSDate timeIntervalSinceReferenceDate];; + + dispatch_time_t bt = dispatch_time(DISPATCH_TIME_NOW, (diff * NSEC_PER_SEC)); + dispatch_time_t tt = dispatch_time(bt, interval); + + dispatch_source_set_timer(pingIntervalTimer, tt, interval, 0); +} + +- (void)startPingIntervalTimer +{ + XMPPLogTrace(); + + if (pingInterval <= 0) + { + // Pinger is disabled + return; + } + + BOOL newTimer = NO; + + if (pingIntervalTimer == NULL) + { + newTimer = YES; + pingIntervalTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, moduleQueue); + + dispatch_source_set_event_handler(pingIntervalTimer, ^{ @autoreleasepool { + + [self handlePingIntervalTimerFire]; + + }}); + } + + [self updatePingIntervalTimer]; + + if (newTimer) + { + dispatch_resume(pingIntervalTimer); + } +} + +- (void)stopPingIntervalTimer +{ + XMPPLogTrace(); + + if (pingIntervalTimer) + { + #if !OS_OBJECT_USE_OBJC + dispatch_release(pingIntervalTimer); + #endif + pingIntervalTimer = NULL; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPPing Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)xmppPing:(XMPPPing *)sender didReceivePong:(XMPPIQ *)pong withRTT:(NSTimeInterval)rtt +{ + XMPPLogTrace(); + + awaitingPingResponse = NO; + [multicastDelegate xmppAutoPingDidReceivePong:self]; +} + +- (void)xmppPing:(XMPPPing *)sender didNotReceivePong:(NSString *)pingID dueToTimeout:(NSTimeInterval)timeout +{ + XMPPLogTrace(); + + awaitingPingResponse = NO; + [multicastDelegate xmppAutoPingDidTimeout:self]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPStream Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender +{ + lastReceiveTime = [NSDate timeIntervalSinceReferenceDate]; + awaitingPingResponse = NO; + + [self startPingIntervalTimer]; +} + +- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq +{ + if (targetJID == nil || [targetJIDStr isEqualToString:[iq fromStr]]) + { + lastReceiveTime = [NSDate timeIntervalSinceReferenceDate]; + } + + return NO; +} + +- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message +{ + if (targetJID == nil || [targetJIDStr isEqualToString:[message fromStr]]) + { + lastReceiveTime = [NSDate timeIntervalSinceReferenceDate]; + } +} + +- (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence +{ + if (targetJID == nil || [targetJIDStr isEqualToString:[presence fromStr]]) + { + lastReceiveTime = [NSDate timeIntervalSinceReferenceDate]; + } +} + +- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error +{ + [self stopPingIntervalTimer]; + + lastReceiveTime = 0; + awaitingPingResponse = NO; +} + +@end diff --git a/Extensions/XEP-0199/XMPPPing.h b/Extensions/XEP-0199/XMPPPing.h new file mode 100644 index 0000000..6cc145b --- /dev/null +++ b/Extensions/XEP-0199/XMPPPing.h @@ -0,0 +1,51 @@ +#import +#import "XMPP.h" + +#define _XMPP_PING_H + +@class XMPPIDTracker; + + +@interface XMPPPing : XMPPModule +{ + BOOL respondsToQueries; + XMPPIDTracker *pingTracker; +} + +/** + * Whether or not the module should respond to incoming ping queries. + * It you create multiple instances of this module, only one instance should respond to queries. + * + * It is recommended you set this (if needed) before you activate the module. + * The default value is YES. +**/ +@property (readwrite) BOOL respondsToQueries; + +/** + * Send pings to the server or a specific JID. + * The disco module may be used to detect if the target supports ping. + * + * The returned string is the pingID (the elementID of the query that was sent). + * In other words: + * + * SEND: + * RECV: + * + * This may be helpful if you are sending multiple simultaneous pings to the same target. +**/ +- (NSString *)sendPingToServer; +- (NSString *)sendPingToServerWithTimeout:(NSTimeInterval)timeout; +- (NSString *)sendPingToJID:(XMPPJID *)jid; +- (NSString *)sendPingToJID:(XMPPJID *)jid withTimeout:(NSTimeInterval)timeout; + +@end + +@protocol XMPPPingDelegate +@optional + +- (void)xmppPing:(XMPPPing *)sender didReceivePong:(XMPPIQ *)pong withRTT:(NSTimeInterval)rtt; +- (void)xmppPing:(XMPPPing *)sender didNotReceivePong:(NSString *)pingID dueToTimeout:(NSTimeInterval)timeout; + +// Note: If the xmpp stream is disconnected, no delegate methods will be called, and outstanding pings are forgotten. + +@end diff --git a/Extensions/XEP-0199/XMPPPing.m b/Extensions/XEP-0199/XMPPPing.m new file mode 100644 index 0000000..e733ab7 --- /dev/null +++ b/Extensions/XEP-0199/XMPPPing.m @@ -0,0 +1,314 @@ +#import "XMPPPing.h" +#import "XMPPIDTracker.h" +#import "XMPPFramework.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#define DEFAULT_TIMEOUT 30.0 // seconds + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPPingInfo : XMPPBasicTrackingInfo +{ + NSDate *timeSent; +} + +@property (nonatomic, readonly) NSDate *timeSent; + +- (NSTimeInterval)rtt; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPPing + +- (id)init +{ + return [self initWithDispatchQueue:NULL]; +} + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + if ((self = [super initWithDispatchQueue:queue])) + { + respondsToQueries = YES; + } + return self; +} + +- (BOOL)activate:(XMPPStream *)aXmppStream +{ + if ([super activate:aXmppStream]) + { + #ifdef _XMPP_CAPABILITIES_H + [xmppStream autoAddDelegate:self delegateQueue:moduleQueue toModulesOfClass:[XMPPCapabilities class]]; + #endif + + pingTracker = [[XMPPIDTracker alloc] initWithDispatchQueue:moduleQueue]; + + return YES; + } + + return NO; +} + +- (void)deactivate +{ +#ifdef _XMPP_CAPABILITIES_H + [xmppStream removeAutoDelegate:self delegateQueue:moduleQueue fromModulesOfClass:[XMPPCapabilities class]]; +#endif + + dispatch_block_t block = ^{ @autoreleasepool { + + [pingTracker removeAllIDs]; + pingTracker = nil; + + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + [super deactivate]; +} + + +- (BOOL)respondsToQueries +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return respondsToQueries; + } + else + { + __block BOOL result; + + dispatch_sync(moduleQueue, ^{ + result = respondsToQueries; + }); + return result; + } +} + +- (void)setRespondsToQueries:(BOOL)flag +{ + dispatch_block_t block = ^{ + + if (respondsToQueries != flag) + { + respondsToQueries = flag; + + #ifdef _XMPP_CAPABILITIES_H + @autoreleasepool { + // Capabilities may have changed, need to notify others. + [xmppStream resendMyPresence]; + } + #endif + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (NSString *)generatePingIDWithTimeout:(NSTimeInterval)timeout +{ + // This method may be invoked on any thread/queue. + + // Generate unique ID for Ping packet + // It's important the ID be unique as the ID is the only thing that distinguishes a pong packet + + NSString *pingID = [xmppStream generateUUID]; + + dispatch_async(moduleQueue, ^{ @autoreleasepool { + + XMPPPingInfo *pingInfo = [[XMPPPingInfo alloc] initWithTarget:self + selector:@selector(handlePong:withInfo:) + timeout:timeout]; + + [pingTracker addID:pingID trackingInfo:pingInfo]; + + }}); + + return pingID; +} + +- (NSString *)sendPingToServer +{ + // This is a public method. + // It may be invoked on any thread/queue. + + return [self sendPingToServerWithTimeout:DEFAULT_TIMEOUT]; +} + +- (NSString *)sendPingToServerWithTimeout:(NSTimeInterval)timeout +{ + // This is a public method. + // It may be invoked on any thread/queue. + + NSString *pingID = [self generatePingIDWithTimeout:timeout]; + + // Send ping packet + // + // + // + // + + NSXMLElement *ping = [NSXMLElement elementWithName:@"ping" xmlns:@"urn:xmpp:ping"]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:nil elementID:pingID child:ping]; + + [xmppStream sendElement:iq]; + + return pingID; +} + +- (NSString *)sendPingToJID:(XMPPJID *)jid +{ + // This is a public method. + // It may be invoked on any thread/queue. + + return [self sendPingToJID:jid withTimeout:DEFAULT_TIMEOUT]; +} + +- (NSString *)sendPingToJID:(XMPPJID *)jid withTimeout:(NSTimeInterval)timeout +{ + // This is a public method. + // It may be invoked on any thread/queue. + + NSString *pingID = [self generatePingIDWithTimeout:timeout]; + + // Send ping element + // + // + // + // + + NSXMLElement *ping = [NSXMLElement elementWithName:@"ping" xmlns:@"urn:xmpp:ping"]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:jid elementID:pingID child:ping]; + + [xmppStream sendElement:iq]; + + return pingID; +} + +- (void)handlePong:(XMPPIQ *)pongIQ withInfo:(XMPPPingInfo *)pingInfo +{ + if (pongIQ) + { + [multicastDelegate xmppPing:self didReceivePong:pongIQ withRTT:[pingInfo rtt]]; + } + else + { + // Timeout + + [multicastDelegate xmppPing:self didNotReceivePong:[pingInfo elementID] dueToTimeout:[pingInfo timeout]]; + } +} + +- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq +{ + // This method is invoked on the moduleQueue. + + NSString *type = [iq type]; + + if ([type isEqualToString:@"result"] || [type isEqualToString:@"error"]) + { + // Example: + // + // + + // If this is a response to a ping that we've sent, + // then the pingTracker will invoke our handlePong:withInfo: method and return YES. + + return [pingTracker invokeForID:[iq elementID] withObject:iq]; + } + else if (respondsToQueries && [type isEqualToString:@"get"]) + { + // Example: + // + // + // + // + + NSXMLElement *ping = [iq elementForName:@"ping" xmlns:@"urn:xmpp:ping"]; + if (ping) + { + XMPPIQ *pong = [XMPPIQ iqWithType:@"result" to:[iq from] elementID:[iq elementID]]; + + [sender sendElement:pong]; + + return YES; + } + } + + return NO; +} + +- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error +{ + [pingTracker removeAllIDs]; +} + +#ifdef _XMPP_CAPABILITIES_H +/** + * If an XMPPCapabilites instance is used we want to advertise our support for ping. +**/ +- (void)xmppCapabilities:(XMPPCapabilities *)sender collectingMyCapabilities:(NSXMLElement *)query +{ + // This method is invoked on the moduleQueue. + + if (respondsToQueries) + { + // + // ... + // + // ... + // + + NSXMLElement *feature = [NSXMLElement elementWithName:@"feature"]; + [feature addAttributeWithName:@"var" stringValue:@"urn:xmpp:ping"]; + + [query addChild:feature]; + } +} +#endif + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPPingInfo + +@synthesize timeSent; + +- (id)initWithTarget:(id)aTarget selector:(SEL)aSelector timeout:(NSTimeInterval)aTimeout +{ + if ((self = [super initWithTarget:aTarget selector:aSelector timeout:aTimeout])) + { + timeSent = [[NSDate alloc] init]; + } + return self; +} + +- (NSTimeInterval)rtt +{ + return [timeSent timeIntervalSinceNow] * -1.0; +} + + +@end diff --git a/Extensions/XEP-0202/XMPPAutoTime.h b/Extensions/XEP-0202/XMPPAutoTime.h new file mode 100644 index 0000000..7bd7c72 --- /dev/null +++ b/Extensions/XEP-0202/XMPPAutoTime.h @@ -0,0 +1,124 @@ +#import "XMPP.h" +#import "XMPPTime.h" + +#define _XMPP_AUTO_TIME_H + +@class XMPPJID; + +/** + * The XMPPAutoTime module monitors the time difference between our machine and the target. + * The target may simply be the server, or a specific resource. + * + * The module works by sending time queries to the target, and tracking the responses. + * The module will automatically send multiple queuries, and take into account the average RTT. + * It will also automatically update itself on a customizable interval, and whenever the machine's clock changes. + * + * This module is helpful when you are using timestamps from the target. + * For example, you may be receiving offline messages from your server. + * However, all these offline messages are timestamped from the server's clock. + * And the current machine's clock may vary considerably from the server's clock. + * Timezone differences don't matter as UTC is always used in XMPP, but clocks can easily differ. + * This may cause the user some confusion as server timestamps may reflect a time in the future, + * or much longer ago than in reality. +**/ +@interface XMPPAutoTime : XMPPModule +{ + NSTimeInterval recalibrationInterval; + XMPPJID *targetJID; + NSTimeInterval timeDifference; + + dispatch_time_t lastCalibrationTime; + dispatch_source_t recalibrationTimer; + + BOOL awaitingQueryResponse; + XMPPTime *xmppTime; + + NSData *lastServerAddress; + + NSDate *systemUptimeChecked; + NSTimeInterval systemUptime; +} + +/** + * How often to recalibrate the time difference. + * + * The module will automatically calculate the time difference when it is activated, + * or when it first sees the xmppStream become authenticated (whichever occurs first). + * After that first calculation, it will update itself according to this interval. + * + * To temporarily disable recalibration, set the interval to zero. + * + * The default recalibrationInterval is 24 hours. +**/ +@property (readwrite) NSTimeInterval recalibrationInterval; + +/** + * The target to query. + * + * If the targetJID is nil, this implies the target is the xmpp server we're connected to. + * If the targetJID is non-nil, it must be a full JID (user@domain.tld/rsrc). + * + * The default targetJID is nil. +**/ +@property (readwrite, strong) XMPPJID *targetJID; + +/** + * Returns the calculated time difference between our machine and the target. + * + * This is NOT a reference to the difference in time zones. + * Time zone differences generally shouldn't matter as xmpp standards mandate the use of UTC. + * + * Rather this is the difference between our UTC time, and the remote party's UTC time. + * If the two clocks are not synchronized, then the result represents the approximate difference. + * + * If our clock is earlier than the remote clock, then the value will be negative. + * If our clock is ahead of the remote clock, then the value will be positive. + * + * If you later receive a timestamp from the remote party, you can simply add the diff. + * For example: + * + * myTime = [givenTimeFromRemoteParty dateByAddingTimeInterval:diff]; +**/ +@property (readonly) NSTimeInterval timeDifference; + +/** + * Returns the date of the target based on the time difference. +**/ +@property (readonly) NSDate *date; + +/** + * The last time we've completed a calibration. +**/ +@property (readonly) dispatch_time_t lastCalibrationTime; + +/** + * XMPPAutoTime is used to automatically query a target for its time (and calculate the difference). + * Sometimes the target is also sending time requests to us as well. + * If so, you may optionally set respondsToQueries to YES to allow the module to respond to incoming time queries. + * + * If you create multiple instances of XMPPAutoTime or XMPPTime, + * then only one instance should respond to queries. + * + * The default value is NO. +**/ +@property (readwrite) BOOL respondsToQueries; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPAutoTimeDelegate +@optional + +- (void)xmppAutoTime:(XMPPAutoTime *)sender didUpdateTimeDifference:(NSTimeInterval)timeDifference; + +@end + +@interface XMPPStream (XMPPAutoTime) + +- (NSTimeInterval)xmppAutoTime_timeDifferenceForTargetJID:(XMPPJID *)targetJID; +- (NSDate *)xmppAutoTime_dateForTargetJID:(XMPPJID *)targetJID; + +@end diff --git a/Extensions/XEP-0202/XMPPAutoTime.m b/Extensions/XEP-0202/XMPPAutoTime.m new file mode 100644 index 0000000..86037d6 --- /dev/null +++ b/Extensions/XEP-0202/XMPPAutoTime.m @@ -0,0 +1,488 @@ +#import "XMPPAutoTime.h" +#import "XMPP.h" +#import "XMPPLogging.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; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +@interface XMPPAutoTime () + +@property (nonatomic, strong) NSData *lastServerAddress; +@property (nonatomic, strong) NSDate *systemUptimeChecked; + +- (void)updateRecalibrationTimer; +- (void)startRecalibrationTimer; +- (void)stopRecalibrationTimer; +@end + +#pragma mark - + +@implementation XMPPAutoTime + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + if ((self = [super initWithDispatchQueue:queue])) + { + recalibrationInterval = (60 * 60 * 24); + + lastCalibrationTime = DISPATCH_TIME_FOREVER; + + xmppTime = [[XMPPTime alloc] initWithDispatchQueue:queue]; + xmppTime.respondsToQueries = NO; + + [xmppTime addDelegate:self delegateQueue:moduleQueue]; + } + return self; +} + +- (BOOL)activate:(XMPPStream *)aXmppStream +{ + if ([super activate:aXmppStream]) + { + [xmppTime activate:aXmppStream]; + + self.systemUptimeChecked = [NSDate date]; + systemUptime = [[NSProcessInfo processInfo] systemUptime]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(systemClockDidChange:) + name:NSSystemClockDidChangeNotification + object:nil]; + + return YES; + } + + return NO; +} + +- (void)deactivate +{ + dispatch_block_t block = ^{ @autoreleasepool { + + [self stopRecalibrationTimer]; + + [xmppTime deactivate]; + awaitingQueryResponse = NO; + + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + [super deactivate]; + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); +} + +- (void)dealloc +{ + // recalibrationTimer released in [self deactivate] + + [xmppTime removeDelegate:self]; + xmppTime = nil; // Might be referenced via [super dealloc] -> [self deactivate] +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Properties +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@synthesize lastServerAddress; +@synthesize systemUptimeChecked; + +- (NSTimeInterval)recalibrationInterval +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return recalibrationInterval; + } + else + { + __block NSTimeInterval result; + + dispatch_sync(moduleQueue, ^{ + result = recalibrationInterval; + }); + return result; + } +} + +- (void)setRecalibrationInterval:(NSTimeInterval)interval +{ + dispatch_block_t block = ^{ + + if (recalibrationInterval != interval) + { + recalibrationInterval = interval; + + // Update the recalibrationTimer. + // + // Depending on new value and current state of the recalibrationTimer, + // this may mean starting, stoping, or simply updating the timer. + + if (recalibrationInterval > 0) + { + // Remember: Only start the timer after the xmpp stream is up and authenticated + if ([xmppStream isAuthenticated]) + [self startRecalibrationTimer]; + } + else + { + [self stopRecalibrationTimer]; + } + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (XMPPJID *)targetJID +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return targetJID; + } + else + { + __block XMPPJID *result; + + dispatch_sync(moduleQueue, ^{ + result = targetJID; + }); + return result; + } +} + +- (void)setTargetJID:(XMPPJID *)jid +{ + dispatch_block_t block = ^{ + + if (![targetJID isEqualToJID:jid]) + { + targetJID = jid; + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (NSTimeInterval)timeDifference +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return timeDifference; + } + else + { + __block NSTimeInterval result; + + dispatch_sync(moduleQueue, ^{ + result = timeDifference; + }); + + return result; + } +} + +- (NSDate *)date +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return [[NSDate date] dateByAddingTimeInterval:-timeDifference]; + } + else + { + __block NSDate *result; + + dispatch_sync(moduleQueue, ^{ + result = [[NSDate date] dateByAddingTimeInterval:-timeDifference]; + }); + + return result; + } +} + +- (dispatch_time_t)lastCalibrationTime +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return lastCalibrationTime; + } + else + { + __block dispatch_time_t result; + + dispatch_sync(moduleQueue, ^{ + result = lastCalibrationTime; + }); + + return result; + } +} + +- (BOOL)respondsToQueries +{ + return xmppTime.respondsToQueries; +} + +- (void)setRespondsToQueries:(BOOL)respondsToQueries +{ + xmppTime.respondsToQueries = respondsToQueries; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Notifications +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)systemClockDidChange:(NSNotification *)notification +{ + XMPPLogTrace(); + XMPPLogVerbose(@"NSSystemClockDidChangeNotification: %@", notification); + + if (lastCalibrationTime == DISPATCH_TIME_FOREVER) + { + // Doesn't matter, we haven't done a calibration yet. + return; + } + + // When the system clock changes, this affects our timeDifference. + // However, the notification doesn't tell us by how much the system clock has changed. + // So here's how we figure it out: + // + // The systemUptime isn't affected by the system clock. + // We previously recorded the system uptime, and simultaneously recoded the system clock time. + // We can now grab the current system uptime and current system clock time. + // Using the four data points we can calculate how much the system clock has changed. + + NSDate *now = [NSDate date]; + NSTimeInterval sysUptime = [[NSProcessInfo processInfo] systemUptime]; + + dispatch_async(moduleQueue, ^{ @autoreleasepool { + + // Calculate system clock change + + NSDate *oldSysTime = systemUptimeChecked; + NSDate *newSysTime = now; + + NSTimeInterval oldSysUptime = systemUptime; + NSTimeInterval newSysUptime = sysUptime; + + NSTimeInterval sysTimeDiff = [newSysTime timeIntervalSinceDate:oldSysTime]; + NSTimeInterval sysUptimeDiff = newSysUptime - oldSysUptime; + + NSTimeInterval sysClockChange = sysTimeDiff - sysUptimeDiff; + + // Modify timeDifference & notify delegate + + timeDifference += sysClockChange; + [multicastDelegate xmppAutoTime:self didUpdateTimeDifference:timeDifference]; + + // Dont forget to update our variables + + self.systemUptimeChecked = now; + systemUptime = sysUptime; + + }}); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Recalibration Timer +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)handleRecalibrationTimerFire +{ + XMPPLogTrace(); + + if (awaitingQueryResponse) return; + + awaitingQueryResponse = YES; + + if (targetJID) + [xmppTime sendQueryToJID:targetJID]; + else + [xmppTime sendQueryToServer]; +} + +- (void)updateRecalibrationTimer +{ + XMPPLogTrace(); + + NSAssert(recalibrationTimer != NULL, @"Broken logic (1)"); + NSAssert(recalibrationInterval > 0, @"Broken logic (2)"); + + + uint64_t interval = (recalibrationInterval * NSEC_PER_SEC); + dispatch_time_t tt; + + if (lastCalibrationTime == DISPATCH_TIME_FOREVER) + tt = dispatch_time(DISPATCH_TIME_NOW, 0); // First timer fire at (NOW) + else + tt = dispatch_time(lastCalibrationTime, interval); // First timer fire at (lastCalibrationTime + interval) + + dispatch_source_set_timer(recalibrationTimer, tt, interval, 0); +} + +- (void)startRecalibrationTimer +{ + XMPPLogTrace(); + + if (recalibrationInterval <= 0) + { + // Timer is disabled + return; + } + + BOOL newTimer = NO; + + if (recalibrationTimer == NULL) + { + newTimer = YES; + recalibrationTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, moduleQueue); + + dispatch_source_set_event_handler(recalibrationTimer, ^{ @autoreleasepool { + + [self handleRecalibrationTimerFire]; + + }}); + } + + [self updateRecalibrationTimer]; + + if (newTimer) + { + dispatch_resume(recalibrationTimer); + } +} + +- (void)stopRecalibrationTimer +{ + XMPPLogTrace(); + + if (recalibrationTimer) + { + #if !OS_OBJECT_USE_OBJC + dispatch_release(recalibrationTimer); + #endif + recalibrationTimer = NULL; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPTime Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)xmppTime:(XMPPTime *)sender didReceiveResponse:(XMPPIQ *)iq withRTT:(NSTimeInterval)rtt +{ + XMPPLogTrace(); + + awaitingQueryResponse = NO; + + lastCalibrationTime = dispatch_time(DISPATCH_TIME_NOW, 0); + timeDifference = [XMPPTime approximateTimeDifferenceFromResponse:iq andRTT:rtt]; + + [multicastDelegate xmppAutoTime:self didUpdateTimeDifference:timeDifference]; +} + +- (void)xmppTime:(XMPPTime *)sender didNotReceiveResponse:(NSString *)queryID dueToTimeout:(NSTimeInterval)timeout +{ + XMPPLogTrace(); + + awaitingQueryResponse = NO; + + // Nothing to do here really. Most likely the server doesn't support XEP-0202. +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPStream Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)xmppStream:(XMPPStream *)sender socketDidConnect:(GCDAsyncSocket *)socket +{ + NSData *currentServerAddress = [socket connectedAddress]; + + if (lastServerAddress == nil) + { + self.lastServerAddress = currentServerAddress; + } + else if (![lastServerAddress isEqualToData:currentServerAddress]) + { + XMPPLogInfo(@"%@: Connected to a different server. Resetting calibration info.", [self class]); + + lastCalibrationTime = DISPATCH_TIME_FOREVER; + timeDifference = 0.0; + + self.lastServerAddress = currentServerAddress; + } +} + +- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender +{ + [self startRecalibrationTimer]; +} + +- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error +{ + [self stopRecalibrationTimer]; + + awaitingQueryResponse = NO; + + // We do NOT reset the lastCalibrationTime here. + // If we reconnect to the same server, the lastCalibrationTime remains valid. +} + +@end + +@implementation XMPPStream (XMPPAutoTime) + +- (NSTimeInterval)xmppAutoTime_timeDifferenceForTargetJID:(XMPPJID *)targetJID +{ + __block NSTimeInterval timeDifference = 0.0; + + [self enumerateModulesOfClass:[XMPPAutoTime class] withBlock:^(XMPPModule *module, NSUInteger idx, BOOL *stop) { + + XMPPAutoTime *autoTime = (XMPPAutoTime *)module; + + if([targetJID isEqualToJID:autoTime.targetJID] || (!targetJID && !autoTime.targetJID)) + { + timeDifference = autoTime.timeDifference; + *stop = YES; + } + }]; + + return timeDifference; +} + +- (NSDate *)xmppAutoTime_dateForTargetJID:(XMPPJID *)targetJID +{ + __block NSDate *date = [NSDate date]; + + [self enumerateModulesOfClass:[XMPPAutoTime class] withBlock:^(XMPPModule *module, NSUInteger idx, BOOL *stop) { + + XMPPAutoTime *autoTime = (XMPPAutoTime *)module; + + if([targetJID isEqualToJID:autoTime.targetJID] || (!targetJID && !autoTime.targetJID)) + { + date = autoTime.date; + *stop = YES; + } + }]; + + return date; +} + +@end diff --git a/Extensions/XEP-0202/XMPPTime.h b/Extensions/XEP-0202/XMPPTime.h new file mode 100644 index 0000000..4d8799b --- /dev/null +++ b/Extensions/XEP-0202/XMPPTime.h @@ -0,0 +1,90 @@ +#import +#import "XMPP.h" + +#define _XMPP_TIME_H + +@class XMPPIDTracker; +@protocol XMPPTimeDelegate; + + +@interface XMPPTime : XMPPModule +{ + BOOL respondsToQueries; + XMPPIDTracker *queryTracker; +} + +/** + * Whether or not the module should respond to incoming time queries. + * It you create multiple instances of this module, only one instance should respond to queries. + * + * It is recommended you set this (if needed) before you activate the module. + * The default value is YES. +**/ +@property (readwrite) BOOL respondsToQueries; + +/** + * Send query to the server or a specific JID. + * The disco module may be used to detect if the target supports this XEP. + * + * The returned string is the queryID (the elementID of the query that was sent). + * In other words: + * + * SEND: + * RECV: + * + * This may be helpful if you are sending multiple simultaneous queries to the same target. +**/ +- (NSString *)sendQueryToServer; +- (NSString *)sendQueryToServerWithTimeout:(NSTimeInterval)timeout; +- (NSString *)sendQueryToJID:(XMPPJID *)jid; +- (NSString *)sendQueryToJID:(XMPPJID *)jid withTimeout:(NSTimeInterval)timeout; + +/** + * Extracts the utc date from the given response/time element, + * and returns an NSDate representation of the time in the local time zone. + * Since the returned date is in the local time zone, it is suitable for presentation. +**/ ++ (NSDate *)dateFromResponse:(XMPPIQ *)iq; + +/** + * Extracts the time zone offset from the given response/time element. +**/ ++ (NSTimeZone *)timeZoneOffsetFromResponse:(XMPPIQ *)iq; + +/** + * Given the returned time response from a remote party, and the approximate round trip time, + * calculates the difference between our clock and the remote party's clock. + * + * This is NOT a reference to the difference in time zones. + * Time zone differences generally shouldn't matter as xmpp standards mandate the use of UTC. + * + * Rather this is the difference between our UTC time, and the remote party's UTC time. + * If the two clocks are not synchronized, then the result represents the approximate difference. + * + * If our clock is earlier than the remote clock, then the result will be negative. + * If our clock is ahead of the remote clock, then the result will be positive. + * + * If you later receive a timestamp from the remote party, you could add the diff. + * For example: + * + * myTime = [givenTimeFromRemoteParty dateByAddingTimeInterval:diff]; +**/ ++ (NSTimeInterval)approximateTimeDifferenceFromResponse:(XMPPIQ *)iq andRTT:(NSTimeInterval)rtt; + +/** + * Creates and returns a time element. +**/ ++ (NSXMLElement *)timeElement; ++ (NSXMLElement *)timeElementFromDate:(NSDate *)date; + +@end + +@protocol XMPPTimeDelegate +@optional + +- (void)xmppTime:(XMPPTime *)sender didReceiveResponse:(XMPPIQ *)iq withRTT:(NSTimeInterval)rtt; +- (void)xmppTime:(XMPPTime *)sender didNotReceiveResponse:(NSString *)queryID dueToTimeout:(NSTimeInterval)timeout; + +// Note: If the xmpp stream is disconnected, no delegate methods will be called, and outstanding queries are forgotten. + +@end diff --git a/Extensions/XEP-0202/XMPPTime.m b/Extensions/XEP-0202/XMPPTime.m new file mode 100644 index 0000000..3e09cd5 --- /dev/null +++ b/Extensions/XEP-0202/XMPPTime.m @@ -0,0 +1,505 @@ +#import "XMPPTime.h" +#import "XMPPIDTracker.h" +#import "XMPPDateTimeProfiles.h" +#import "XMPPFramework.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#define DEFAULT_TIMEOUT 30.0 // seconds + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPTimeQueryInfo : XMPPBasicTrackingInfo +{ + NSDate *timeSent; +} + +@property (nonatomic, readonly) NSDate *timeSent; + +- (NSTimeInterval)rtt; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPTime + +- (id)init +{ + return [self initWithDispatchQueue:NULL]; +} + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + if ((self = [super initWithDispatchQueue:queue])) + { + respondsToQueries = YES; + } + return self; +} + +- (BOOL)activate:(XMPPStream *)aXmppStream +{ + if ([super activate:aXmppStream]) + { + #ifdef _XMPP_CAPABILITIES_H + [xmppStream autoAddDelegate:self delegateQueue:moduleQueue toModulesOfClass:[XMPPCapabilities class]]; + #endif + + queryTracker = [[XMPPIDTracker alloc] initWithDispatchQueue:moduleQueue]; + + return YES; + } + + return NO; +} + +- (void)deactivate +{ +#ifdef _XMPP_CAPABILITIES_H + [xmppStream removeAutoDelegate:self delegateQueue:moduleQueue fromModulesOfClass:[XMPPCapabilities class]]; +#endif + + dispatch_block_t block = ^{ @autoreleasepool { + + [queryTracker removeAllIDs]; + queryTracker = nil; + + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + [super deactivate]; +} + + +- (BOOL)respondsToQueries +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return respondsToQueries; + } + else + { + __block BOOL result; + + dispatch_sync(moduleQueue, ^{ + result = respondsToQueries; + }); + return result; + } +} + +- (void)setRespondsToQueries:(BOOL)flag +{ + dispatch_block_t block = ^{ + + if (respondsToQueries != flag) + { + respondsToQueries = flag; + + #ifdef _XMPP_CAPABILITIES_H + @autoreleasepool { + // Capabilities may have changed, need to notify others. + [xmppStream resendMyPresence]; + } + #endif + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (NSString *)generateQueryIDWithTimeout:(NSTimeInterval)timeout +{ + // This method may be invoked on any thread/queue. + + // Generate unique ID for query. + // It's important the ID be unique as the ID is the + // only thing that distinguishes multiple queries from each other. + + NSString *queryID = [xmppStream generateUUID]; + + dispatch_async(moduleQueue, ^{ @autoreleasepool { + + XMPPTimeQueryInfo *queryInfo = [[XMPPTimeQueryInfo alloc] initWithTarget:self + selector:@selector(handleResponse:withInfo:) + timeout:timeout]; + + [queryTracker addID:queryID trackingInfo:queryInfo]; + + }}); + + return queryID; +} + +- (NSString *)sendQueryToServer +{ + // This is a public method. + // It may be invoked on any thread/queue. + + return [self sendQueryToServerWithTimeout:DEFAULT_TIMEOUT]; +} + +- (NSString *)sendQueryToServerWithTimeout:(NSTimeInterval)timeout +{ + // This is a public method. + // It may be invoked on any thread/queue. + + NSString *queryID = [self generateQueryIDWithTimeout:timeout]; + + // Send ping packet + // + // + // + // + // Note: Sometimes the to attribute is required. (ejabberd) + + NSXMLElement *time = [NSXMLElement elementWithName:@"time" xmlns:@"urn:xmpp:time"]; + XMPPJID *domainJID = [[xmppStream myJID] domainJID]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:domainJID elementID:queryID child:time]; + + [xmppStream sendElement:iq]; + + return queryID; +} + +- (NSString *)sendQueryToJID:(XMPPJID *)jid +{ + // This is a public method. + // It may be invoked on any thread/queue. + + return [self sendQueryToJID:jid withTimeout:DEFAULT_TIMEOUT]; +} + +- (NSString *)sendQueryToJID:(XMPPJID *)jid withTimeout:(NSTimeInterval)timeout +{ + // This is a public method. + // It may be invoked on any thread/queue. + + NSString *queryID = [self generateQueryIDWithTimeout:timeout]; + + // Send ping element + // + // + // + + NSXMLElement *time = [NSXMLElement elementWithName:@"time" xmlns:@"urn:xmpp:time"]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:jid elementID:queryID child:time]; + + [xmppStream sendElement:iq]; + + return queryID; +} + +- (void)handleResponse:(XMPPIQ *)iq withInfo:(XMPPTimeQueryInfo *)queryInfo +{ + if (iq) + { + if ([[iq type] isEqualToString:@"result"]) + [multicastDelegate xmppTime:self didReceiveResponse:iq withRTT:[queryInfo rtt]]; + else + [multicastDelegate xmppTime:self didNotReceiveResponse:[queryInfo elementID] dueToTimeout:-1.0]; + } + else + { + [multicastDelegate xmppTime:self didNotReceiveResponse:[queryInfo elementID] dueToTimeout:[queryInfo timeout]]; + } +} + +- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq +{ + // This method is invoked on the moduleQueue. + + NSString *type = [iq type]; + + if ([type isEqualToString:@"result"] || [type isEqualToString:@"error"]) + { + // Examples: + // + // + // + // + // + // + // + + return [queryTracker invokeForID:[iq elementID] withObject:iq]; + } + else if (respondsToQueries && [type isEqualToString:@"get"]) + { + // Example: + // + // + // + + NSXMLElement *time = [iq elementForName:@"time" xmlns:@"urn:xmpp:time"]; + if (time) + { + NSXMLElement *currentTime = [[self class] timeElement]; + + XMPPIQ *response = [XMPPIQ iqWithType:@"result" to:[iq from] elementID:[iq elementID]]; + [response addChild:currentTime]; + + [sender sendElement:response]; + + return YES; + } + } + + return NO; +} + +- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error +{ + [queryTracker removeAllIDs]; +} + +#ifdef _XMPP_CAPABILITIES_H +/** + * If an XMPPCapabilites instance is used we want to advertise our support for XEP-0202. +**/ +- (void)xmppCapabilities:(XMPPCapabilities *)sender collectingMyCapabilities:(NSXMLElement *)query +{ + // This method is invoked on the moduleQueue. + + if (respondsToQueries) + { + // + // ... + // + // ... + // + + NSXMLElement *feature = [NSXMLElement elementWithName:@"feature"]; + [feature addAttributeWithName:@"var" stringValue:@"urn:xmpp:time"]; + + [query addChild:feature]; + } +} +#endif + ++ (NSDate *)dateFromResponse:(XMPPIQ *)iq +{ + // + // + // + + NSXMLElement *time = [iq elementForName:@"time" xmlns:@"urn:xmpp:time"]; + if (time == nil) return nil; + + NSString *utc = [[time elementForName:@"utc"] stringValue]; + if (utc == nil) return nil; + + // Note: + // + // NSDate is a very simple class, but can be confusing at times. + // NSDate simply stores an NSTimeInterval internally, + // which is just a double representing the number of seconds since the reference date. + // Since it's a double, it can yield sub-millisecond precision. + // + // In addition to this, it stores the values in UTC. + // However, if you print the value using NSLog via "%@", + // it will automatically print the date in the local timezone: + // + // NSDate *refDate = [NSDate dateWithTimeIntervalSinceReferenceDate:0.0]; + // + // NSLog(@"%f", [refDate timeIntervalSinceReferenceDate]); // Prints: 0.0 + // NSLog(@"%@", refDate); // Prints: 2000-12-31 19:00:00 -05:00 + // NSLog(@"%@", [utcDateFormatter stringFromDate:refDate]); // Prints: 2001-01-01 00:00:00 +00:00 + // + // Now the value we've received from XMPPDateTimeProfiles is correct. + // If we print it out using a utcDateFormatter we would see it is correct. + // If we printed it out generically using NSLog, then we would see it converted into our local time zone. + + return [XMPPDateTimeProfiles parseDateTime:utc]; +} + ++ (NSTimeZone *)timeZoneOffsetFromResponse:(XMPPIQ *)iq +{ + // + // + // + + NSXMLElement *time = [iq elementForName:@"time" xmlns:@"urn:xmpp:time"]; + if (time == nil) return 0; + + NSString *tzo = [[time elementForName:@"tzo"] stringValue]; + if (tzo == nil) return 0; + + return [XMPPDateTimeProfiles parseTimeZoneOffset:tzo]; +} + ++ (NSTimeInterval)approximateTimeDifferenceFromResponse:(XMPPIQ *)iq andRTT:(NSTimeInterval)rtt +{ + // First things first, get the current date and time + + NSDate *localDate = [NSDate date]; + + // Then worry about the calculations + + NSXMLElement *time = [iq elementForName:@"time" xmlns:@"urn:xmpp:time"]; + if (time == nil) return 0.0; + + NSString *utc = [[time elementForName:@"utc"] stringValue]; + if (utc == nil) return 0.0; + + NSDate *remoteDate = [XMPPDateTimeProfiles parseDateTime:utc]; + if (remoteDate == nil) return 0.0; + + NSTimeInterval localTI = [localDate timeIntervalSinceReferenceDate]; + NSTimeInterval remoteTI = [remoteDate timeIntervalSinceReferenceDate] - (rtt / 2.0); + + // Did the response contain millisecond precision? + // This is an important consideration. + // Imagine if both computers are perfectly synced, + // but the remote response doesn't contain milliseconds. + // This could possibly cause us to think the difference is close to a full second. + // + // DateTime examples (from XMPPDateTimeProfiles documentation): + // + // 1969-07-21T02:56:15 + // 1969-07-21T02:56:15Z + // 1969-07-20T21:56:15-05:00 + // 1969-07-21T02:56:15.123 + // 1969-07-21T02:56:15.123Z + // 1969-07-20T21:56:15.123-05:00 + + BOOL hasMilliseconds = ([utc length] > 19) && ([utc characterAtIndex:19] == '.'); + + if (hasMilliseconds) + { + return remoteTI - localTI; + } + else + { + // No milliseconds. What to do? + // + // We could simply truncate the milliseconds from our time... + // But this could make things much worse. + // For example: + // + // local = 14:22:36.750 + // remote = 14:22:37 + // + // If we truncate the result now we calculate a diff of 1.000 (a full second). + // Considering the remote's milliseconds could have been anything from 000 to 999, + // this means our calculations are: + // + // perfect : 0.1% chance + // diff too big : 75.0% chance + // diff too small : 24.9% chance + // + // Perhaps a better solution would give us a more even spread. + // We can do this by calculating the range: + // + // 37.000 - 36.750 = 0.25 + // 37.999 - 36.750 = 1.249 + // + // So a better guess of the diff is 0.750 (3/4 of a second): + // + // perfect : 0.1% chance + // diff too big : 50.0% chance + // diff too small : 49.9% chance + + NSTimeInterval diff1 = localTI - (remoteTI + 0.000); + NSTimeInterval diff2 = localTI - (remoteTI + 0.999); + + return ((diff1 + diff2) / 2.0); + } +} + ++ (NSXMLElement *)timeElement +{ + return [self timeElementFromDate:[NSDate date]]; +} + ++ (NSXMLElement *)timeElementFromDate:(NSDate *)date +{ + // + + NSDateFormatter *df = [[NSDateFormatter alloc] init]; + [df setFormatterBehavior:NSDateFormatterBehavior10_4]; // Use unicode patterns (as opposed to 10_3) + [df setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]]; + [df setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss'Z'"]; + [df setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; + + NSString *utcValue = [df stringFromDate:date]; + + + NSInteger tzoInSeconds = [[NSTimeZone systemTimeZone] secondsFromGMTForDate:date]; + + NSInteger tzoH = tzoInSeconds / (60 * 60); + NSInteger tzoS = tzoInSeconds % (60 * 60); + + NSString *tzoValue = [NSString stringWithFormat:@"%+03li:%02li", (long)tzoH, (long)tzoS]; + + NSXMLElement *tzo = [NSXMLElement elementWithName:@"tzo" stringValue:tzoValue]; + NSXMLElement *utc = [NSXMLElement elementWithName:@"utc" stringValue:utcValue]; + + NSXMLElement *time = [NSXMLElement elementWithName:@"time" xmlns:@"urn:xmpp:time"]; + [time addChild:tzo]; + [time addChild:utc]; + + return time; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPTimeQueryInfo + +@synthesize timeSent; + +- (id)initWithTarget:(id)aTarget selector:(SEL)aSelector timeout:(NSTimeInterval)aTimeout +{ + if ((self = [super initWithTarget:aTarget selector:aSelector timeout:aTimeout])) + { + timeSent = [[NSDate alloc] init]; + } + return self; +} + +- (NSTimeInterval)rtt +{ + return [timeSent timeIntervalSinceNow] * -1.0; +} + + +@end diff --git a/Extensions/XEP-0203/NSXMLElement+XEP_0203.h b/Extensions/XEP-0203/NSXMLElement+XEP_0203.h new file mode 100644 index 0000000..27db923 --- /dev/null +++ b/Extensions/XEP-0203/NSXMLElement+XEP_0203.h @@ -0,0 +1,12 @@ +#import +#if TARGET_OS_IPHONE +#import "DDXML.h" +#endif + + +@interface NSXMLElement (XEP_0203) + +- (BOOL)wasDelayed; +- (NSDate *)delayedDeliveryDate; + +@end diff --git a/Extensions/XEP-0203/NSXMLElement+XEP_0203.m b/Extensions/XEP-0203/NSXMLElement+XEP_0203.m new file mode 100644 index 0000000..ee404d3 --- /dev/null +++ b/Extensions/XEP-0203/NSXMLElement+XEP_0203.m @@ -0,0 +1,84 @@ +#import "NSXMLElement+XEP_0203.h" +#import "XMPPDateTimeProfiles.h" +#import "NSXMLElement+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 + +@implementation NSXMLElement (XEP_0203) + +- (BOOL)wasDelayed +{ + NSXMLElement *delay; + + delay = [self elementForName:@"delay" xmlns:@"urn:xmpp:delay"]; + if (delay) + { + return YES; + } + + delay = [self elementForName:@"x" xmlns:@"jabber:x:delay"]; + if (delay) + { + return YES; + } + + return NO; +} + +- (NSDate *)delayedDeliveryDate +{ + NSXMLElement *delay; + + // From XEP-0203 (Delayed Delivery) + // + // + // + // The format [of the stamp attribute] MUST adhere to the dateTime format + // specified in XEP-0082 and MUST be expressed in UTC. + + delay = [self elementForName:@"delay" xmlns:@"urn:xmpp:delay"]; + if (delay) + { + NSString *stampValue = [delay attributeStringValueForName:@"stamp"]; + + // There are other considerations concerning XEP-0082. + // For example, it may optionally contain milliseconds. + // And it may possibly express UTC as "+00:00" instead of "Z". + // + // Thankfully there is already an implementation that takes into account all these possibilities. + + return [XMPPDateTimeProfiles parseDateTime:stampValue]; + } + + // From XEP-0091 (Legacy Delayed Delivery) + // + // + + delay = [self elementForName:@"x" xmlns:@"jabber:x:delay"]; + if (delay) + { + NSDate *stamp; + + NSString *stampValue = [delay attributeStringValueForName:@"stamp"]; + + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; + [dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]]; + [dateFormatter setDateFormat:@"yyyyMMdd'T'HH:mm:ss"]; + [dateFormatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"GMT"]]; + + stamp = [dateFormatter dateFromString:stampValue]; + + return stamp; + } + + return nil; +} + +@end diff --git a/Extensions/XEP-0223/XEP_0223.h b/Extensions/XEP-0223/XEP_0223.h new file mode 100644 index 0000000..aa0670f --- /dev/null +++ b/Extensions/XEP-0223/XEP_0223.h @@ -0,0 +1,21 @@ +/** + * XEP-0223 : Persistent Storage of Private Data via PubSub + * + * This specification defines best practices for using the XMPP publish-subscribe extension to + * persistently store private information such as bookmarks and client configuration options. + * + * http://xmpp.org/extensions/xep-0223.html +**/ + +#import + + +@interface XEP_0223 : NSObject + +/** + * This method returns the recommended configuration options to configure a pubsub node for storing private data. + * It may be passed directly to the publishToNoe:::: method of XMPPPubSub. +**/ ++ (NSDictionary *)privateStoragePubSubOptions; + +@end diff --git a/Extensions/XEP-0223/XEP_0223.m b/Extensions/XEP-0223/XEP_0223.m new file mode 100644 index 0000000..129ce35 --- /dev/null +++ b/Extensions/XEP-0223/XEP_0223.m @@ -0,0 +1,20 @@ +/** + * XEP-0223 : Persistent Storage of Private Data via PubSub + * + * This specification defines best practices for using the XMPP publish-subscribe extension to + * persistently store private information such as bookmarks and client configuration options. + * + * http://xmpp.org/extensions/xep-0223.html +**/ + +#import "XEP_0223.h" + +@implementation XEP_0223 + ++ (NSDictionary *)privateStoragePubSubOptions +{ + return @{ @"pubsub#persist_items" : @(YES), + @"pubsub#access_model" : @"whitelist" }; +} + +@end diff --git a/Extensions/XEP-0224/XMPPAttentionModule.h b/Extensions/XEP-0224/XMPPAttentionModule.h new file mode 100644 index 0000000..c003ee7 --- /dev/null +++ b/Extensions/XEP-0224/XMPPAttentionModule.h @@ -0,0 +1,24 @@ +#import "XMPPModule.h" +#import "XMPPMessage+XEP_0224.h" + +#define _XMPP_ATTENTION_MODULE_H + +@interface XMPPAttentionModule : XMPPModule { + BOOL respondsToQueries; +} + +/** + * Whether or not the module should respond to incoming attention request queries. + * It you create multiple instances of this module, only one instance should respond to queries. + * + * It is recommended you set this (if needed) before you activate the module. + * The default value is YES. + **/ +@property (readwrite) BOOL respondsToQueries; + +@end + +@protocol XMPPAttentionDelegate +@optional +- (void)xmppAttention:(XMPPAttentionModule *)sender didReceiveAttentionHeadlineMessage:(XMPPMessage *)attentionRequest; +@end diff --git a/Extensions/XEP-0224/XMPPAttentionModule.m b/Extensions/XEP-0224/XMPPAttentionModule.m new file mode 100644 index 0000000..439152e --- /dev/null +++ b/Extensions/XEP-0224/XMPPAttentionModule.m @@ -0,0 +1,127 @@ +#import "XMPPAttentionModule.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +@implementation XMPPAttentionModule + +- (id)init +{ + return [self initWithDispatchQueue:NULL]; +} + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + if ((self = [super initWithDispatchQueue:queue])) + { + respondsToQueries = YES; + } + return self; +} + +- (BOOL)activate:(XMPPStream *)aXmppStream +{ + if ([super activate:aXmppStream]) + { +#ifdef _XMPP_CAPABILITIES_H + [xmppStream autoAddDelegate:self delegateQueue:moduleQueue toModulesOfClass:[XMPPCapabilities class]]; +#endif + return YES; + } + + return NO; +} + +- (void)deactivate +{ +#ifdef _XMPP_CAPABILITIES_H + [xmppStream removeAutoDelegate:self delegateQueue:moduleQueue fromModulesOfClass:[XMPPCapabilities class]]; +#endif + + [super deactivate]; +} + +- (BOOL)respondsToQueries +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return respondsToQueries; + } + else + { + __block BOOL result; + + dispatch_sync(moduleQueue, ^{ + result = respondsToQueries; + }); + return result; + } +} + +- (void)setRespondsToQueries:(BOOL)flag +{ + dispatch_block_t block = ^{ + + if (respondsToQueries != flag) + { + respondsToQueries = flag; + +#ifdef _XMPP_CAPABILITIES_H + @autoreleasepool { + // Capabilities may have changed, need to notify others. + [xmppStream resendMyPresence]; + } +#endif + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + + +- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message +{ + // This method is invoked on the moduleQueue. + + // Format of an attention message. Body is optional and not used by clients like Pidgin + // + // + // Why don't you answer, Herbie? + // + + if ([message isAttentionMessage]) + { + [multicastDelegate xmppAttention:self didReceiveAttentionHeadlineMessage:message]; + } +} + + +#ifdef _XMPP_CAPABILITIES_H +/** + * If an XMPPCapabilites instance is used we want to advertise our support for attention requests. + **/ +- (void)xmppCapabilities:(XMPPCapabilities *)sender collectingMyCapabilities:(NSXMLElement *)query +{ + // This method is invoked on the moduleQueue. + + if (respondsToQueries) + { + // + // ... + // + // ... + // + + NSXMLElement *feature = [NSXMLElement elementWithName:@"feature"]; + [feature addAttributeWithName:@"var" stringValue:XMLNS_ATTENTION]; + + [query addChild:feature]; + } +} +#endif + +@end diff --git a/Extensions/XEP-0224/XMPPMessage+XEP_0224.h b/Extensions/XEP-0224/XMPPMessage+XEP_0224.h new file mode 100644 index 0000000..45e81a2 --- /dev/null +++ b/Extensions/XEP-0224/XMPPMessage+XEP_0224.h @@ -0,0 +1,8 @@ +#import "XMPPMessage.h" +#define XMLNS_ATTENTION @"urn:xmpp:attention:0" + +@interface XMPPMessage (XEP_0224) +- (BOOL)isHeadLineMessage; +- (BOOL)isAttentionMessage; +- (BOOL)isAttentionMessageWithBody; +@end diff --git a/Extensions/XEP-0224/XMPPMessage+XEP_0224.m b/Extensions/XEP-0224/XMPPMessage+XEP_0224.m new file mode 100644 index 0000000..91bc941 --- /dev/null +++ b/Extensions/XEP-0224/XMPPMessage+XEP_0224.m @@ -0,0 +1,24 @@ +#import "XMPPMessage+XEP_0224.h" +#import "NSXMLElement+XMPP.h" + +@implementation XMPPMessage (XEP_0224) + +- (BOOL)isHeadLineMessage { + return [[[self attributeForName:@"type"] stringValue] isEqualToString:@"headline"]; +} + +- (BOOL)isAttentionMessage +{ + return [self isHeadLineMessage] && [self elementForName:@"attention" xmlns:XMLNS_ATTENTION]; +} + +- (BOOL)isAttentionMessageWithBody +{ + if([self isAttentionMessage]) + { + return [self isMessageWithBody]; + } + return NO; +} + +@end diff --git a/Extensions/XEP-0280/XMPPMessage+XEP_0280.h b/Extensions/XEP-0280/XMPPMessage+XEP_0280.h new file mode 100644 index 0000000..7f76965 --- /dev/null +++ b/Extensions/XEP-0280/XMPPMessage+XEP_0280.h @@ -0,0 +1,19 @@ +#import "XMPPMessage.h" +@class XMPPJID; + +@interface XMPPMessage (XEP_0280) + +- (NSXMLElement *)receivedMessageCarbon; +- (NSXMLElement *)sentMessageCarbon; + +- (BOOL)isMessageCarbon; +- (BOOL)isReceivedMessageCarbon; +- (BOOL)isSentMessageCarbon; +- (BOOL)isTrustedMessageCarbon; +- (BOOL)isTrustedMessageCarbonForMyJID:(XMPPJID *)jid; + +- (XMPPMessage *)messageCarbonForwardedMessage; + +- (void)addPrivateMessageCarbons; + +@end diff --git a/Extensions/XEP-0280/XMPPMessage+XEP_0280.m b/Extensions/XEP-0280/XMPPMessage+XEP_0280.m new file mode 100644 index 0000000..4229e3f --- /dev/null +++ b/Extensions/XEP-0280/XMPPMessage+XEP_0280.m @@ -0,0 +1,118 @@ +#import "XMPPMessage+XEP_0280.h" +#import "XMPPJID.h" +#import "NSXMLElement+XMPP.h" +#import "NSXMLElement+XEP_0297.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#define XMLNS_XMPP_MESSAGE_CARBONS @"urn:xmpp:carbons:2" + +@implementation XMPPMessage (XEP_0280) + +- (NSXMLElement *)receivedMessageCarbon +{ + return [self elementForName:@"received" xmlns:XMLNS_XMPP_MESSAGE_CARBONS]; +} + +- (NSXMLElement *)sentMessageCarbon +{ + return [self elementForName:@"sent" xmlns:XMLNS_XMPP_MESSAGE_CARBONS]; +} + + +- (BOOL)isMessageCarbon +{ + if([self isReceivedMessageCarbon] || [self isSentMessageCarbon]) + { + return YES; + } + else + { + return NO; + } +} + +- (BOOL)isReceivedMessageCarbon +{ + if([self receivedMessageCarbon]) + { + return YES; + } + else + { + return NO; + } +} + +- (BOOL)isSentMessageCarbon +{ + if([self sentMessageCarbon]) + { + return YES; + } + else + { + return NO; + } +} + +- (BOOL)isTrustedMessageCarbon +{ + BOOL isTrustedMessageCarbon = NO; + XMPPMessage *messageCarbonForwardedMessage = [self messageCarbonForwardedMessage]; + + if([self isSentMessageCarbon]) + { + if([[self from] isEqualToJID:[messageCarbonForwardedMessage from] options:XMPPJIDCompareBare]) + { + isTrustedMessageCarbon = YES; + } + + } + else if([self isReceivedMessageCarbon]) + { + if([[self from] isEqualToJID:[messageCarbonForwardedMessage to] options:XMPPJIDCompareBare]) + { + isTrustedMessageCarbon = YES; + } + } + + return isTrustedMessageCarbon; +} + +- (BOOL)isTrustedMessageCarbonForMyJID:(XMPPJID *)jid +{ + if([self isTrustedMessageCarbon] && [[jid bareJID] isEqualToJID:self.from]) + { + return YES; + } + else + { + return NO; + } +} + +- (XMPPMessage *)messageCarbonForwardedMessage +{ + NSXMLElement *carbon = nil; + + if([self receivedMessageCarbon]) + { + carbon = [self receivedMessageCarbon]; + } + else if([self sentMessageCarbon]) + { + carbon = [self sentMessageCarbon]; + } + + return [carbon forwardedMessage]; +} + +- (void)addPrivateMessageCarbons +{ + [self addChild:[NSXMLElement elementWithName:@"private" xmlns:XMLNS_XMPP_MESSAGE_CARBONS]]; +} + +@end diff --git a/Extensions/XEP-0280/XMPPMessageCarbons.h b/Extensions/XEP-0280/XMPPMessageCarbons.h new file mode 100644 index 0000000..6696085 --- /dev/null +++ b/Extensions/XEP-0280/XMPPMessageCarbons.h @@ -0,0 +1,65 @@ +#import "XMPPModule.h" +@class XMPPMessage; +@class XMPPIDTracker; + +#define _XMPP_MESSAGE_CARBONS_H + +@interface XMPPMessageCarbons : XMPPModule +{ + BOOL autoEnableMessageCarbons; + BOOL allowsUntrustedMessageCarbons; + BOOL messageCarbonsEnabled; + + XMPPIDTracker *xmppIDTracker; +} + +/** + * Wether or not to automatically enable Message Carbons. + * + * Default YES +**/ +@property (assign) BOOL autoEnableMessageCarbons; + +/** + * Wether Message Carbons is currently enabled or not. + * + * @see enableMessageCarbons + * @see disableMessageCarbons +**/ +@property (assign, getter = isMessageCarbonsEnabled,readonly) BOOL messageCarbonsEnabled; + +/** + * Whether Message Carbons are validated before calling the delegate methods. + * + * @see xmppMessageCarbons:willReceiveMessage:outgoing: + * @see xmppMessageCarbons:didReceiveMessage:outgoing: + * + * A Message Carbon is Trusted if: + * + * - It is from the Stream's Bare JID + * - Sent Forward Messages are from the Stream's JID + * - Received Forward Messages are to the Stream's JID + * + * Default is NO +**/ +@property (assign) BOOL allowsUntrustedMessageCarbons; + +/** + * Enable Message Carbons +**/ +- (void)enableMessageCarbons; + +/** + * Disable Message Carbons +**/ +- (void)disableMessageCarbons; + +@end + +@protocol XMPPMessageCarbonsDelegate + +- (void)xmppMessageCarbons:(XMPPMessageCarbons *)xmppMessageCarbons willReceiveMessage:(XMPPMessage *)message outgoing:(BOOL)isOutgoing; + +- (void)xmppMessageCarbons:(XMPPMessageCarbons *)xmppMessageCarbons didReceiveMessage:(XMPPMessage *)message outgoing:(BOOL)isOutgoing; + +@end \ No newline at end of file diff --git a/Extensions/XEP-0280/XMPPMessageCarbons.m b/Extensions/XEP-0280/XMPPMessageCarbons.m new file mode 100644 index 0000000..6111bfa --- /dev/null +++ b/Extensions/XEP-0280/XMPPMessageCarbons.m @@ -0,0 +1,293 @@ +#import "XMPPMessageCarbons.h" +#import "XMPP.h" +#import "XMPPFramework.h" +#import "XMPPLogging.h" +#import "XMPPIDTracker.h" +#import "NSXMLElement+XEP_0297.h" +#import "XMPPMessage+XEP_0280.h" +#import "XMPPInternal.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_VERBOSE; // | XMPP_LOG_FLAG_TRACE; +#else +static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + + +#define XMLNS_XMPP_MESSAGE_CARBONS @"urn:xmpp:carbons:2" + +@implementation XMPPMessageCarbons + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + if((self = [super initWithDispatchQueue:queue])) + { + autoEnableMessageCarbons = YES; + } + return self; +} + +- (BOOL)activate:(XMPPStream *)aXmppStream +{ + XMPPLogTrace(); + + if ([super activate:aXmppStream]) + { + XMPPLogVerbose(@"%@: Activated", THIS_FILE); + + xmppIDTracker = [[XMPPIDTracker alloc] initWithDispatchQueue:moduleQueue]; + + return YES; + } + + return NO; +} + +- (void)deactivate +{ + XMPPLogTrace(); + + dispatch_block_t block = ^{ @autoreleasepool { + + [xmppIDTracker removeAllIDs]; + xmppIDTracker = nil; + + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + [super deactivate]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Properties +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)autoEnableMessageCarbons +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = autoEnableMessageCarbons; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (void)setAutoEnableMessageCarbons:(BOOL)flag +{ + dispatch_block_t block = ^{ + autoEnableMessageCarbons = flag; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (BOOL)isMessageCarbonsEnabled +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = messageCarbonsEnabled; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (BOOL)allowsUntrustedMessageCarbons +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = allowsUntrustedMessageCarbons; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (void)setAllowsUntrustedMessageCarbons:(BOOL)flag +{ + dispatch_block_t block = ^{ + allowsUntrustedMessageCarbons = flag; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (void)enableMessageCarbons +{ + dispatch_block_t block = ^{ + + if(!messageCarbonsEnabled && [xmppIDTracker numberOfIDs] == 0) + { + NSString *elementID = [XMPPStream generateUUID]; + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" elementID:elementID]; + [iq setXmlns:@"jabber:client"]; + + NSXMLElement *enable = [NSXMLElement elementWithName:@"enable" xmlns:XMLNS_XMPP_MESSAGE_CARBONS]; + [iq addChild:enable]; + + [xmppIDTracker addElement:iq + target:self + selector:@selector(enableMessageCarbonsIQ:withInfo:) + timeout:XMPPIDTrackerTimeoutNone]; + + [xmppStream sendElement:iq]; + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); +} + +- (void)disableMessageCarbons +{ + dispatch_block_t block = ^{ + + if(messageCarbonsEnabled && [xmppIDTracker numberOfIDs] == 0) + { + NSString *elementID = [XMPPStream generateUUID]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" elementID:elementID]; + [iq setXmlns:@"jabber:client"]; + + NSXMLElement *enable = [NSXMLElement elementWithName:@"disable" xmlns:XMLNS_XMPP_MESSAGE_CARBONS]; + [iq addChild:enable]; + + [xmppIDTracker addElement:iq + target:self + selector:@selector(disableMessageCarbonsIQ:withInfo:) + timeout:XMPPIDTrackerTimeoutNone]; + + [xmppStream sendElement:iq]; + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPStream Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender +{ + XMPPLogTrace(); + + messageCarbonsEnabled = NO; + + if(self.autoEnableMessageCarbons) + { + [self enableMessageCarbons]; + } +} + +- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error +{ + messageCarbonsEnabled = NO; +} + +- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq +{ + [xmppIDTracker invokeForID:[iq elementID] withObject:iq]; + + return NO; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPStream Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)enableMessageCarbonsIQ:(XMPPIQ *)iq withInfo:(XMPPBasicTrackingInfo *)trackerInfo +{ + XMPPLogTrace(); + + if([iq isResultIQ]) + { + messageCarbonsEnabled = YES; + } +} + +- (void)disableMessageCarbonsIQ:(XMPPIQ *)iq withInfo:(XMPPBasicTrackingInfo *)trackerInfo +{ + XMPPLogTrace(); + + if([iq isResultIQ]) + { + messageCarbonsEnabled = NO; + } +} + +- (XMPPMessage *)xmppStream:(XMPPStream *)sender willReceiveMessage:(XMPPMessage *)message +{ + XMPPLogTrace(); + + if([message isTrustedMessageCarbonForMyJID:sender.myJID] || + ([message isMessageCarbon] && allowsUntrustedMessageCarbons)) + { + BOOL outgoing = [message isSentMessageCarbon]; + + XMPPMessage *messageCarbonForwardedMessage = [message messageCarbonForwardedMessage]; + + [multicastDelegate xmppMessageCarbons:self + willReceiveMessage:messageCarbonForwardedMessage + outgoing:outgoing]; + } + + return message; +} + +- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message +{ + XMPPLogTrace(); + + if([message isTrustedMessageCarbonForMyJID:sender.myJID] || + ([message isMessageCarbon] && allowsUntrustedMessageCarbons)) + { + BOOL outgoing = [message isSentMessageCarbon]; + + XMPPMessage *messageCarbonForwardedMessage = [message messageCarbonForwardedMessage]; + + [multicastDelegate xmppMessageCarbons:self + didReceiveMessage:messageCarbonForwardedMessage + outgoing:outgoing]; + } +} + + +@end diff --git a/Extensions/XEP-0297/NSXMLElement+XEP_0297.h b/Extensions/XEP-0297/NSXMLElement+XEP_0297.h new file mode 100644 index 0000000..cebec07 --- /dev/null +++ b/Extensions/XEP-0297/NSXMLElement+XEP_0297.h @@ -0,0 +1,39 @@ +#import + +#if TARGET_OS_IPHONE + #import "DDXML.h" +#endif + +@class XMPPIQ; +@class XMPPMessage; +@class XMPPPresence; + +@interface NSXMLElement (XEP_0297) + +#pragma mark Forwarded Stanza + +- (NSXMLElement *)forwardedStanza; + +- (BOOL)hasForwardedStanza; + +- (BOOL)isForwardedStanza; + +#pragma mark Delayed Delivery Date + +- (NSDate *)forwardedStanzaDelayedDeliveryDate; + +#pragma mark XMPPElement + +- (XMPPIQ *)forwardedIQ; + +- (BOOL)hasForwardedIQ; + +- (XMPPMessage *)forwardedMessage; + +- (BOOL)hasForwardedMessage; + +- (XMPPPresence *)forwardedPresence; + +- (BOOL)hasForwardedPresence; + +@end diff --git a/Extensions/XEP-0297/NSXMLElement+XEP_0297.m b/Extensions/XEP-0297/NSXMLElement+XEP_0297.m new file mode 100644 index 0000000..bda7682 --- /dev/null +++ b/Extensions/XEP-0297/NSXMLElement+XEP_0297.m @@ -0,0 +1,138 @@ +#import "NSXMLElement+XEP_0297.h" +#import "NSXMLElement+XMPP.h" +#import "NSXMLElement+XEP_0203.h" +#import "XMPPIQ.h" +#import "XMPPMessage.h" +#import "XMPPPresence.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#define NAME_XMPP_STANZA_FORWARDING @"forwarded" +#define XMLNS_XMPP_STANZA_FORWARDING @"urn:xmpp:forward:0" + +@implementation NSXMLElement (XEP_0297) + +#pragma mark Forwarded Stanza + +- (NSXMLElement *)forwardedStanza +{ + return [self elementForName:NAME_XMPP_STANZA_FORWARDING xmlns:XMLNS_XMPP_STANZA_FORWARDING]; +} + +- (BOOL)hasForwardedStanza +{ + if([self forwardedStanza]) + { + return YES; + } + else + { + return NO; + } +} + +- (BOOL)isForwardedStanza +{ + if([[self name] isEqualToString:NAME_XMPP_STANZA_FORWARDING] && [[self xmlns] isEqualToString:XMLNS_XMPP_STANZA_FORWARDING]) + { + return YES; + } + else + { + return NO; + } +} + +#pragma mark Delayed Delivery Date + +- (NSDate *)forwardedStanzaDelayedDeliveryDate +{ + if([self isForwardedStanza]) + { + return [self delayedDeliveryDate]; + } + else + { + return [[self forwardedStanza] delayedDeliveryDate]; + } +} + + +#pragma mark XMPPElement + +- (XMPPIQ *)forwardedIQ +{ + if([self isForwardedStanza]) + { + return [XMPPIQ iqFromElement:[self elementForName:@"iq"]]; + } + else + { + return [XMPPIQ iqFromElement:[[self forwardedStanza] elementForName:@"iq"]]; + } +} + +- (BOOL)hasForwardedIQ +{ + if([self forwardedIQ]) + { + return YES; + } + else + { + return NO; + } +} + +- (XMPPMessage *)forwardedMessage +{ + if([self isForwardedStanza]) + { + return [XMPPMessage messageFromElement:[self elementForName:@"message"]]; + } + else + { + return [XMPPMessage messageFromElement:[[self forwardedStanza] elementForName:@"message"]]; + } +} + +- (BOOL)hasForwardedMessage +{ + if([self forwardedMessage]) + { + return YES; + } + else + { + return NO; + } +} + + +- (XMPPPresence *)forwardedPresence +{ + if([self isForwardedStanza]) + { + return [XMPPPresence presenceFromElement:[self elementForName:@"presence"]]; + } + else + { + return [XMPPPresence presenceFromElement:[[self forwardedStanza] elementForName:@"presence"]]; + } +} + +- (BOOL)hasForwardedPresence +{ + if([self forwardedPresence]) + { + return YES; + } + else + { + return NO; + } +} + +@end diff --git a/Extensions/XEP-0308/XMPPMessage+XEP_0308.h b/Extensions/XEP-0308/XMPPMessage+XEP_0308.h new file mode 100644 index 0000000..eec8267 --- /dev/null +++ b/Extensions/XEP-0308/XMPPMessage+XEP_0308.h @@ -0,0 +1,14 @@ +#import "XMPPMessage.h" + +@interface XMPPMessage (XEP_0308) + +- (BOOL)isMessageCorrection; + +- (NSString *)correctedMessageID; + +- (void)addMessageCorrectionWithID:(NSString *)messageCorrectionID; + +- (XMPPMessage *)generateCorrectionMessageWithID:(NSString *)elementID; +- (XMPPMessage *)generateCorrectionMessageWithID:(NSString *)elementID body:(NSString *)body; + +@end diff --git a/Extensions/XEP-0308/XMPPMessage+XEP_0308.m b/Extensions/XEP-0308/XMPPMessage+XEP_0308.m new file mode 100644 index 0000000..d34c8db --- /dev/null +++ b/Extensions/XEP-0308/XMPPMessage+XEP_0308.m @@ -0,0 +1,88 @@ +#import "XMPPMessage+XEP_0308.h" +#import "NSXMLElement+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 + +#define NAME_XMPP_MESSAGE_CORRECT @"replace" +#define XMLNS_XMPP_MESSAGE_CORRECT @"urn:xmpp:message-correct:0" + +@implementation XMPPMessage (XEP_0308) + +- (BOOL)isMessageCorrection +{ + if([[self correctedMessageID] length]) + { + return YES; + } + else + { + return NO; + } +} + +- (NSString *)correctedMessageID +{ + return [[self elementForName:NAME_XMPP_MESSAGE_CORRECT xmlns:XMLNS_XMPP_MESSAGE_CORRECT] attributeStringValueForName:@"id"]; +} + +- (void)addMessageCorrectionWithID:(NSString *)messageCorrectionID +{ + NSXMLElement *replace = [NSXMLElement elementWithName:NAME_XMPP_MESSAGE_CORRECT stringValue:XMLNS_XMPP_MESSAGE_CORRECT]; + [replace addAttributeWithName:@"id" stringValue:messageCorrectionID]; + [self addChild:replace]; +} + +- (XMPPMessage *)generateCorrectionMessageWithID:(NSString *)elementID +{ + XMPPMessage *correctionMessage = nil; + + if([[self elementID] length] && ![self isMessageCorrection]) + { + correctionMessage = [self copy]; + + [correctionMessage removeAttributeForName:@"id"]; + + if([elementID length]) + { + [correctionMessage addAttributeWithName:@"id" stringValue:elementID]; + } + + [correctionMessage addMessageCorrectionWithID:[self elementID]]; + } + + return correctionMessage; +} + +- (XMPPMessage *)generateCorrectionMessageWithID:(NSString *)elementID body:(NSString *)body +{ + XMPPMessage *correctionMessage = nil; + + if([[self elementID] length] && ![self isMessageCorrection]) + { + correctionMessage = [self copy]; + + [correctionMessage removeAttributeForName:@"id"]; + + if([elementID length]) + { + [correctionMessage addAttributeWithName:@"id" stringValue:elementID]; + } + + NSXMLElement *bodyElement = [correctionMessage elementForName:@"body"]; + + if(bodyElement) + { + [correctionMessage removeChildAtIndex:[[correctionMessage children] indexOfObject:bodyElement]]; + } + + [self addBody:body]; + + [correctionMessage addMessageCorrectionWithID:[self elementID]]; + } + + return correctionMessage; +} + +@end diff --git a/Extensions/XEP-0333/XMPPMessage+XEP_0333.h b/Extensions/XEP-0333/XMPPMessage+XEP_0333.h new file mode 100644 index 0000000..e7e70e6 --- /dev/null +++ b/Extensions/XEP-0333/XMPPMessage+XEP_0333.h @@ -0,0 +1,28 @@ +#import "XMPPMessage.h" + +@interface XMPPMessage (XEP_0333) + +- (BOOL)hasChatMarker; + +- (BOOL)hasMarkableChatMarker; +- (BOOL)hasReceivedChatMarker; +- (BOOL)hasDisplayedChatMarker; +- (BOOL)hasAcknowledgedChatMarker; + +- (NSString *)chatMarker; +- (NSString *)chatMarkerID; + +- (void)addMarkableChatMarker; +- (void)addReceivedChatMarkerWithID:(NSString *)elementID; +- (void)addDisplayedChatMarkerWithID:(NSString *)elementID; +- (void)addAcknowledgedChatMarkerWithID:(NSString *)elementID; + +- (XMPPMessage *)generateReceivedChatMarker; +- (XMPPMessage *)generateDisplayedChatMarker; +- (XMPPMessage *)generateAcknowledgedChatMarker; + +- (XMPPMessage *)generateReceivedChatMarkerIncludingThread:(BOOL)includingThread; +- (XMPPMessage *)generateDisplayedChatMarkerIncludingThread:(BOOL)includingThread; +- (XMPPMessage *)generateAcknowledgedChatMarkerIncludingThread:(BOOL)includingThread; + +@end diff --git a/Extensions/XEP-0333/XMPPMessage+XEP_0333.m b/Extensions/XEP-0333/XMPPMessage+XEP_0333.m new file mode 100644 index 0000000..6159142 --- /dev/null +++ b/Extensions/XEP-0333/XMPPMessage+XEP_0333.m @@ -0,0 +1,142 @@ +#import "XMPPMessage+XEP_0333.h" +#import "NSXMLElement+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 + +#define XMLNS_CHAT_MARKERS @"urn:xmpp:chat-markers:0" + +#define MARKABLE_NAME @"markable" +#define RECEIVED_NAME @"received" +#define DISPLAYED_NAME @"displayed" +#define ACKNOWLEDGED_NAME @"acknowledged" + +@implementation XMPPMessage (XEP_0333) + +- (BOOL)hasChatMarker +{ + return ([[self elementsForXmlns:XMLNS_CHAT_MARKERS] count] > 0); +} + +- (BOOL)hasMarkableChatMarker +{ + return ([self elementForName:MARKABLE_NAME xmlns:XMLNS_CHAT_MARKERS] != nil); +} + +- (BOOL)hasReceivedChatMarker +{ + return ([self elementForName:RECEIVED_NAME xmlns:XMLNS_CHAT_MARKERS] != nil); +} + +- (BOOL)hasDisplayedChatMarker +{ + return ([self elementForName:DISPLAYED_NAME xmlns:XMLNS_CHAT_MARKERS] != nil); +} + +- (BOOL)hasAcknowledgedChatMarker +{ + return ([self elementForName:ACKNOWLEDGED_NAME xmlns:XMLNS_CHAT_MARKERS] != nil); +} + +- (NSString *)chatMarker +{ + return [[[self elementsForXmlns:XMLNS_CHAT_MARKERS] lastObject] name]; +} + +- (NSString *)chatMarkerID +{ + return [[[self elementsForXmlns:XMLNS_CHAT_MARKERS] lastObject] attributeStringValueForName:@"id"]; +} + +- (NSString *)chatMarkerThread +{ + return [[[self elementsForXmlns:XMLNS_CHAT_MARKERS] lastObject] attributeStringValueForName:@"thread"]; +} + +- (void)addMarkableChatMarker +{ + NSXMLElement *markableDisplayedMarker = [[NSXMLElement alloc] initWithName:MARKABLE_NAME xmlns:XMLNS_CHAT_MARKERS]; + [self addChild:markableDisplayedMarker]; +} + +- (void)addReceivedChatMarkerWithID:(NSString *)elementID +{ + NSXMLElement *receivedChatMarker = [[NSXMLElement alloc] initWithName:RECEIVED_NAME xmlns:XMLNS_CHAT_MARKERS]; + [receivedChatMarker addAttributeWithName:@"id" stringValue:elementID]; + + [self addChild:receivedChatMarker]; +} + +- (void)addDisplayedChatMarkerWithID:(NSString *)elementID +{ + NSXMLElement *readDisplayedMarker = [[NSXMLElement alloc] initWithName:DISPLAYED_NAME xmlns:XMLNS_CHAT_MARKERS]; + [readDisplayedMarker addAttributeWithName:@"id" stringValue:elementID]; + + [self addChild:readDisplayedMarker]; +} + +- (void)addAcknowledgedChatMarkerWithID:(NSString *)elementID +{ + NSXMLElement *acknowledgedChatMarker = [[NSXMLElement alloc] initWithName:ACKNOWLEDGED_NAME xmlns:XMLNS_CHAT_MARKERS]; + [acknowledgedChatMarker addAttributeWithName:@"id" stringValue:elementID]; + + [self addChild:acknowledgedChatMarker]; +} + +- (XMPPMessage *)generateReceivedChatMarker +{ + return [self generateReceivedChatMarkerIncludingThread:NO]; +} + +- (XMPPMessage *)generateDisplayedChatMarker +{ + return [self generateDisplayedChatMarkerIncludingThread:NO]; +} + +- (XMPPMessage *)generateAcknowledgedChatMarker +{ + return [self generateAcknowledgedChatMarkerIncludingThread:NO]; +} + +- (XMPPMessage *)generateReceivedChatMarkerIncludingThread:(BOOL)includingThread +{ + XMPPMessage *message = [XMPPMessage message]; + [message addAttributeWithName:@"to" stringValue:[self fromStr]]; + + if(includingThread && [self thread]) + { + [message addThread:[self thread]]; + } + + [message addReceivedChatMarkerWithID:[self elementID]]; + + return message; +} +- (XMPPMessage *)generateDisplayedChatMarkerIncludingThread:(BOOL)includingThread +{ + XMPPMessage *message = [XMPPMessage message]; + [message addAttributeWithName:@"to" stringValue:[self fromStr]]; + + if(includingThread && [self thread]) + { + [message addThread:[self thread]]; + } + + [message addDisplayedChatMarkerWithID:[self elementID]]; + return message; +} +- (XMPPMessage *)generateAcknowledgedChatMarkerIncludingThread:(BOOL)includingThread +{ + XMPPMessage *message = [XMPPMessage message]; + [message addAttributeWithName:@"to" stringValue:[self fromStr]]; + + if(includingThread && [self thread]) + { + [message addThread:[self thread]]; + } + + [message addAcknowledgedChatMarkerWithID:[self elementID]]; + return message; +} +@end diff --git a/Extensions/XEP-0335/NSXMLElement+XEP_0335.h b/Extensions/XEP-0335/NSXMLElement+XEP_0335.h new file mode 100644 index 0000000..334e511 --- /dev/null +++ b/Extensions/XEP-0335/NSXMLElement+XEP_0335.h @@ -0,0 +1,21 @@ +#import +#if TARGET_OS_IPHONE +#import "DDXML.h" +#endif + +@interface NSXMLElement (XEP_0335) + +- (NSXMLElement *)JSONContainer; + +- (BOOL)isJSONContainer; +- (BOOL)hasJSONContainer; + +- (NSString *)JSONContainerString; +- (NSData *)JSONContainerData; +- (id)JSONContainerObject; + +- (void)addJSONContainerWithString:(NSString *)JSONContainerString; +- (void)addJSONContainerWithData:(NSData *)JSONContainerData; +- (void)addJSONContainerWithObject:(id)JSONContainerObject; + +@end diff --git a/Extensions/XEP-0335/NSXMLElement+XEP_0335.m b/Extensions/XEP-0335/NSXMLElement+XEP_0335.m new file mode 100644 index 0000000..7c4a534 --- /dev/null +++ b/Extensions/XEP-0335/NSXMLElement+XEP_0335.m @@ -0,0 +1,87 @@ +#import "NSXMLElement+XEP_0335.h" +#import "NSXMLElement+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 + +#define XEP_0335_NAME @"json" +#define XEP_0335_XMLNS @"urn:xmpp:json:0" + +@implementation NSXMLElement (XEP_0335) + +- (NSXMLElement *)JSONContainer +{ + if([self isJSONContainer]) + { + return self; + } + else + { + return [self elementForName:XEP_0335_NAME xmlns:XEP_0335_XMLNS]; + } +} + +- (BOOL)isJSONContainer +{ + if([[self name] isEqualToString:XEP_0335_NAME] && [[self xmlns] isEqualToString:XEP_0335_XMLNS]) + { + return YES; + } + else + { + return NO; + } +} + +- (BOOL)hasJSONContainer +{ + return [self elementForName:XEP_0335_NAME xmlns:XEP_0335_XMLNS] != nil; +} + +- (NSString *)JSONContainerString +{ + return [[self JSONContainer] stringValue]; +} + +- (NSData *)JSONContainerData +{ + NSString *JSONContainerString = [self JSONContainerString]; + return [JSONContainerString dataUsingEncoding:NSUTF8StringEncoding]; +} + +- (id)JSONContainerObject +{ + NSData *JSONData = [self JSONContainerData]; + return [NSJSONSerialization JSONObjectWithData:JSONData options:0 error:nil]; +} + +- (void)addJSONContainerWithString:(NSString *)JSONContainerString +{ + if([JSONContainerString length]) + { + NSXMLElement *container = [NSXMLElement elementWithName:XEP_0335_NAME xmlns:XEP_0335_XMLNS]; + [container setStringValue:JSONContainerString]; + [self addChild:container]; + } +} + +- (void)addJSONContainerWithData:(NSData *)JSONContainerData +{ + if([JSONContainerData length]) + { + NSString *JSONContainerString = [[NSString alloc] initWithData:JSONContainerData encoding:NSUTF8StringEncoding]; + [self addJSONContainerWithString:JSONContainerString]; + } +} + +- (void)addJSONContainerWithObject:(id)JSONContainerObject +{ + if([NSJSONSerialization isValidJSONObject:JSONContainerObject]) + { + NSData *JSONContainerData = [NSJSONSerialization dataWithJSONObject:JSONContainerObject options:0 error:nil]; + [self addJSONContainerWithData:JSONContainerData]; + } +} + +@end diff --git a/Extensions/XEP-0352/NSXMLElement+XEP_0352.h b/Extensions/XEP-0352/NSXMLElement+XEP_0352.h new file mode 100644 index 0000000..7c360fc --- /dev/null +++ b/Extensions/XEP-0352/NSXMLElement+XEP_0352.h @@ -0,0 +1,9 @@ + +#import "NSXMLElement+XMPP.h" + +@interface NSXMLElement (XEP0352) + ++ (instancetype)indicateInactiveElement; ++ (instancetype)indicateActiveElement; + +@end diff --git a/Extensions/XEP-0352/NSXMLElement+XEP_0352.m b/Extensions/XEP-0352/NSXMLElement+XEP_0352.m new file mode 100644 index 0000000..1e02012 --- /dev/null +++ b/Extensions/XEP-0352/NSXMLElement+XEP_0352.m @@ -0,0 +1,18 @@ + +#import "NSXMLElement+XEP_0352.h" + +#define XMLNS_XMPP_CLIENT_STATE_INDICATION @"urn:xmpp:csi:0" + +@implementation NSXMLElement (XEP0352) + ++ (instancetype)indicateActiveElement +{ + return [NSXMLElement elementWithName:@"active" xmlns:XMLNS_XMPP_CLIENT_STATE_INDICATION]; +} + ++ (instancetype)indicateInactiveElement +{ + return [NSXMLElement elementWithName:@"inactive" xmlns:XMLNS_XMPP_CLIENT_STATE_INDICATION]; +} + +@end diff --git a/PNXMPPFramework.podspec b/PNXMPPFramework.podspec index 3ec68f5..60404f9 100644 --- a/PNXMPPFramework.podspec +++ b/PNXMPPFramework.podspec @@ -18,17 +18,18 @@ Pod::Spec.new do |s| # * Finally, don't worry about the indent, CocoaPods strips it! #s.description = <<-DESC DESC - s.homepage = "https://github.com//PNXMPPFramework" + s.homepage = "https://github.com/giuseppenucifora/PNXMPPFramework" # s.screenshots = "www.example.com/screenshots_1", "www.example.com/screenshots_2" s.license = 'MIT' s.author = { "Giuseppe Nucifora" => "me@giuseppenucifora.com" } - s.source = { :git => "https://github.com//PNXMPPFramework.git", :tag => s.version.to_s } + s.source = { :git => "https://github.com/giuseppenucifora/PNXMPPFramework.git", :tag => s.version.to_s } # s.social_media_url = 'https://twitter.com/' s.platform = :ios, '7.0' s.requires_arc = true s.source_files = 'Pod/Classes/**/*' + s.resources = [ '**/*.{xcdatamodel,xcdatamodeld}'] s.resource_bundles = { 'PNXMPPFramework' => ['Pod/Assets/*.png'] } @@ -36,7 +37,273 @@ Pod::Spec.new do |s| # s.public_header_files = 'Pod/Classes/**/*.h' # s.frameworks = 'UIKit', 'MapKit' # s.dependency 'AFNetworking', '~> 2.3' - s.dependency 'CocoaLumberjack' - s.dependency 'CocoaAsyncSocket' - s.ios.dependency 'KissXML' + + +s.preserve_path = 'PNXMPPFramework/module/module.modulemap' +#s.module_map = 'module/module.modulemap' + +s.subspec 'Core' do |core| +core.source_files = ['XMPPFramework.h', 'Core/**/*.{h,m}', 'Vendor/libidn/*.h', 'Authentication/**/*.{h,m}', 'Categories/**/*.{h,m}', 'Utilities/**/*.{h,m}'] +core.vendored_libraries = 'Vendor/libidn/libidn.a' +core.libraries = 'xml2', 'resolv' +core.xcconfig = { 'HEADER_SEARCH_PATHS' => '$(inherited) $(SDKROOT)/usr/include/libxml2 $(PODS_ROOT)/XMPPFramework/module $(SDKROOT)/usr/include/libresolv', 'LIBRARY_SEARCH_PATHS' => '"$(PODS_ROOT)/Vendor/libidn"', 'CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES' => 'YES', 'OTHER_LDFLAGS' => '"-lxml2"', 'ENABLE_BITCODE' => 'NO' +} +core.dependency 'CocoaLumberjack','~>1.9' +core.dependency 'CocoaAsyncSocket' +core.dependency 'KissXML' +end + +s.subspec 'Authentication' do |ss| +ss.dependency 'PNXMPPFramework/Core' +end + +s.subspec 'Categories' do |ss| +ss.dependency 'PNXMPPFramework/Core' +end + +s.subspec 'Utilities' do |ss| +ss.dependency 'PNXMPPFramework/Core' +end + +s.subspec 'BandwidthMonitor' do |ss| +ss.source_files = 'Extensions/BandwidthMonitor/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'CoreDataStorage' do |ss| +ss.source_files = ['Extensions/CoreDataStorage/**/*.{h,m}', 'Extensions/XEP-0203/NSXMLElement+XEP_0203.h'] +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +ss.framework = 'CoreData' +end + +s.subspec 'GoogleSharedStatus' do |ss| +ss.source_files = 'Extensions/GoogleSharedStatus/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'ProcessOne' do |ss| +ss.source_files = 'Extensions/ProcessOne/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'Reconnect' do |ss| +ss.source_files = 'Extensions/Reconnect/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +ss.framework = 'SystemConfiguration' +end + +s.subspec 'Roster' do |ss| +ss.source_files = 'Extensions/Roster/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.dependency 'PNXMPPFramework/CoreDataStorage' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'SystemInputActivityMonitor' do |ss| +ss.source_files = ['Extensions/SystemInputActivityMonitor/**/*.{h,m}', 'Utilities/GCDMulticastDelegate.h'] +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0009' do |ss| +ss.source_files = 'Extensions/XEP-0009/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0012' do |ss| +ss.source_files = 'Extensions/XEP-0012/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0016' do |ss| +ss.source_files = 'Extensions/XEP-0016/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0045' do |ss| +ss.source_files = 'Extensions/XEP-0045/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.dependency 'PNXMPPFramework/CoreDataStorage' +ss.dependency 'PNXMPPFramework/XEP-0203' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0054' do |ss| +ss.source_files = ['Extensions/XEP-0054/**/*.{h,m}', 'Extensions/XEP-0153/XMPPvCardAvatarModule.h', 'Extensions/XEP-0082/XMPPDateTimeProfiles.h', 'Extensions/XEP-0082/NSDate+XMPPDateTimeProfiles.h'] +ss.dependency 'PNXMPPFramework/Core' +ss.dependency 'PNXMPPFramework/Roster' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +ss.framework = 'CoreLocation' +end + +s.subspec 'XEP-0059' do |ss| +ss.source_files = 'Extensions/XEP-0059/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0060' do |ss| +ss.source_files = 'Extensions/XEP-0060/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0065' do |ss| +ss.source_files = 'Extensions/XEP-0065/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0066' do |ss| +ss.source_files = 'Extensions/XEP-0066/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0082' do |ss| +ss.source_files = 'Extensions/XEP-0082/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0085' do |ss| +ss.source_files = 'Extensions/XEP-0085/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0092' do |ss| +ss.source_files = 'Extensions/XEP-0092/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0100' do |ss| +ss.source_files = 'Extensions/XEP-0100/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0106' do |ss| +ss.source_files = 'Extensions/XEP-0106/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0115' do |ss| +ss.source_files = 'Extensions/XEP-0115/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.dependency 'PNXMPPFramework/CoreDataStorage' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0136' do |ss| +ss.source_files = 'Extensions/XEP-0136/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/CoreDataStorage' +ss.dependency 'PNXMPPFramework/XEP-0203' +ss.dependency 'PNXMPPFramework/XEP-0085' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0153' do |ss| +ss.source_files = ['Extensions/XEP-0153/**/*.{h,m}', 'Extensions/XEP-0082/NSDate+XMPPDateTimeProfiles.h'] +ss.dependency 'PNXMPPFramework/Core' +ss.dependency 'PNXMPPFramework/XEP-0054' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0172' do |ss| +ss.source_files = 'Extensions/XEP-0172/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0184' do |ss| +ss.source_files = 'Extensions/XEP-0184/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0191' do |ss| +ss.source_files = 'Extensions/XEP-0191/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0198' do |ss| +ss.source_files = 'Extensions/XEP-0198/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0199' do |ss| +ss.source_files = 'Extensions/XEP-0199/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0202' do |ss| +ss.source_files = 'Extensions/XEP-0202/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.dependency 'PNXMPPFramework/XEP-0082' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0203' do |ss| +ss.source_files = 'Extensions/XEP-0203/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.dependency 'PNXMPPFramework/XEP-0082' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0223' do |ss| +ss.source_files = 'Extensions/XEP-0223/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0224' do |ss| +ss.source_files = 'Extensions/XEP-0224/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0280' do |ss| +ss.source_files = ['Extensions/XEP-0280/**/*.{h,m}', 'Extensions/XEP-0297/NSXMLElement+XEP_0297.h'] +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0297' do |ss| +ss.source_files = ['Extensions/XEP-0297/**/*.{h,m}', 'Extensions/XEP-0203/**/*.h'] +ss.dependency 'PNXMPPFramework/Core' +ss.dependency 'PNXMPPFramework/XEP-0203' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0308' do |ss| +ss.source_files = 'Extensions/XEP-0308/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0333' do |ss| +ss.source_files = 'Extensions/XEP-0333/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0335' do |ss| +ss.source_files = 'Extensions/XEP-0335/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end end diff --git a/Utilities/DDList.h b/Utilities/DDList.h new file mode 100644 index 0000000..0f741fc --- /dev/null +++ b/Utilities/DDList.h @@ -0,0 +1,61 @@ +#import + +@class DDListEnumerator; + +struct DDListNode { + void * element; + struct DDListNode * prev; + struct DDListNode * next; +}; +typedef struct DDListNode DDListNode; + + +/** + * The DDList class is designed as a simple list class. + * It can store objective-c objects as well as non-objective-c pointers. + * It does not retain objective-c objects as it treats all elements as simple pointers. + * + * Example usages: + * - Storing a list of delegates, where there is a desire to not retain the individual delegates. + * - Storing a list of dispatch timers, which are not NSObjects, and cannot be stored in NSCollection classes. + * + * This class is NOT thread-safe. + * It is designed to be used within a thread-safe context (e.g. within a single dispatch_queue). +**/ +@interface DDList : NSObject +{ + DDListNode *listHead; + DDListNode *listTail; +} + +- (void)add:(void *)element; +- (void)remove:(void *)element; +- (void)removeAll:(void *)element; +- (void)removeAllElements; + +- (BOOL)contains:(void *)element; + +- (NSUInteger)count; + +/** + * The enumerators return a snapshot of the list that can be enumerated. + * The list can later be altered (elements added/removed) without affecting enumerator snapshots. +**/ +- (DDListEnumerator *)listEnumerator; +- (DDListEnumerator *)reverseListEnumerator; + +@end + +@interface DDListEnumerator : NSObject +{ + NSUInteger numElements; + NSUInteger currentElementIndex; + void **elements; +} + +- (NSUInteger)numTotal; +- (NSUInteger)numLeft; + +- (void *)nextElement; + +@end diff --git a/Utilities/DDList.m b/Utilities/DDList.m new file mode 100644 index 0000000..0ddd4ae --- /dev/null +++ b/Utilities/DDList.m @@ -0,0 +1,297 @@ +#import "DDList.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@interface DDListEnumerator (PrivateAPI) + +- (id)initWithList:(DDListNode *)list reverse:(BOOL)reverse; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation DDList + +- (id)init +{ + if ((self = [super init])) + { + listHead = NULL; + listTail = NULL; + } + return self; +} + +- (void)add:(void *)element +{ + if(element == NULL) return; + + DDListNode *node = malloc(sizeof(DDListNode)); + + node->element = element; + + // Remember: The list is a linked list of DDListNode objects. + // Each node object is allocated and placed in the list. + // It is not deallocated until it is later removed from the linked list. + + if (listTail == NULL) + { + node->next = NULL; + node->prev = NULL; + } + else + { + node->next = NULL; + node->prev = listTail; + node->prev->next = node; + } + + listTail = node; + + if (listHead == NULL) + listHead = node; +} + +- (void)remove:(void *)element allInstances:(BOOL)allInstances +{ + if(element == NULL) return; + + DDListNode *node = listHead; + while (node != NULL) + { + DDListNode *nextNode = node->next; + if (element == node->element) + { + // Remove the node from the list. + // This is done by editing the pointers of the node's neighbors to skip it. + // + // In other words: + // node->prev->next = node->next + // node->next->prev = node->prev + // + // We also want to properly update our list pointer, + // which always points to the "first" element in the list. (Most recently added.) + + if (node->prev != NULL) + node->prev->next = node->next; + else + listHead = node->next; + + if (node->next != NULL) + node->next->prev = node->prev; + else + listTail = node->prev; + + free(node); + + if (!allInstances) break; + } + node = nextNode; + } +} + +- (void)remove:(void *)element +{ + [self remove:element allInstances:NO]; +} + +- (void)removeAll:(void *)element +{ + [self remove:element allInstances:YES]; +} + +- (void)removeAllElements +{ + DDListNode *node = listHead; + while (node != NULL) + { + DDListNode *next = node->next; + + free(node); + node = next; + } + + listHead = NULL; + listTail = NULL; +} + +- (BOOL)contains:(void *)element +{ + DDListNode *node; + for (node = listHead; node != NULL; node = node->next) + { + if (node->element == element) + { + return YES; + } + } + + return NO; +} + +- (NSUInteger)count +{ + NSUInteger count = 0; + + DDListNode *node; + for (node = listHead; node != NULL; node = node->next) + { + count++; + } + + return count; +} + +- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state + objects:(id __unsafe_unretained [])buffer + count:(NSUInteger)len +{ + DDListNode *currentNode; + + if (state->extra[0] == 1) + return 0; + + if (state->state == 0) + currentNode = listHead; + else + currentNode = (DDListNode *)state->state; + + NSUInteger batchCount = 0; + while (currentNode != NULL && batchCount < len) + { + buffer[batchCount] = (__bridge id)currentNode->element; + currentNode = currentNode->next; + batchCount++; + } + + state->state = (unsigned long)currentNode; + state->itemsPtr = buffer; + state->mutationsPtr = (__bridge void *)self; + + if (currentNode == NULL) + state->extra[0] = 1; + else + state->extra[0] = 0; + + return batchCount; +} + +- (DDListEnumerator *)listEnumerator +{ + return [[DDListEnumerator alloc] initWithList:listHead reverse:NO]; +} + +- (DDListEnumerator *)reverseListEnumerator +{ + return [[DDListEnumerator alloc] initWithList:listTail reverse:YES]; +} + +- (void)dealloc +{ + [self removeAllElements]; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation DDListEnumerator + +- (id)initWithList:(DDListNode *)list reverse:(BOOL)reverse +{ + if ((self = [super init])) + { + numElements = 0; + currentElementIndex = 0; + + // First get a count of the number of elements in the given list. + + if (reverse) + { + for (DDListNode *node = list; node != NULL; node = node->prev) + { + numElements++; + } + } + else + { + for (DDListNode *node = list; node != NULL; node = node->next) + { + numElements++; + } + + } + + // Now copy the list into a C array. + + if (numElements > 0) + { + elements = malloc(numElements * sizeof(void *)); + + DDListNode *node = list; + + if (reverse) + { + for (NSUInteger i = 0; i < numElements; i++) + { + elements[i] = node->element; + node = node->prev; + } + } + else + { + for (NSUInteger i = 0; i < numElements; i++) + { + elements[i] = node->element; + node = node->next; + } + } + } + } + return self; +} + +- (NSUInteger)numTotal +{ + return numElements; +} + +- (NSUInteger)numLeft +{ + if (currentElementIndex < numElements) + return numElements - currentElementIndex; + else + return 0; +} + +- (void *)nextElement +{ + if (currentElementIndex < numElements) + { + void *element = elements[currentElementIndex]; + currentElementIndex++; + + return element; + } + else + { + return NULL; + } +} + +- (void)dealloc +{ + if (elements) + { + free(elements); + } +} + +@end diff --git a/Utilities/GCDMulticastDelegate.h b/Utilities/GCDMulticastDelegate.h new file mode 100644 index 0000000..81976f6 --- /dev/null +++ b/Utilities/GCDMulticastDelegate.h @@ -0,0 +1,56 @@ +#import + +@class GCDMulticastDelegateEnumerator; + +/** + * This class provides multicast delegate functionality. That is: + * - it provides a means for managing a list of delegates + * - any method invocations to an instance of this class are automatically forwarded to all delegates + * + * For example: + * + * // Make this method call on every added delegate (there may be several) + * [multicastDelegate cog:self didFindThing:thing]; + * + * This allows multiple delegates to be added to an xmpp stream or any xmpp module, + * which in turn makes development easier as there can be proper separation of logically different code sections. + * + * In addition, this makes module development easier, + * as multiple delegates can usually be handled in a manner similar to the traditional single delegate paradigm. + * + * This class also provides proper support for GCD queues. + * So each delegate specifies which queue they would like their delegate invocations to be dispatched onto. + * + * All delegate dispatching is done asynchronously (which is a critically important architectural design). +**/ + +@interface GCDMulticastDelegate : NSObject + +- (void)addDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; +- (void)removeDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; +- (void)removeDelegate:(id)delegate; + +- (void)removeAllDelegates; + +- (NSUInteger)count; +- (NSUInteger)countOfClass:(Class)aClass; +- (NSUInteger)countForSelector:(SEL)aSelector; + +- (BOOL)hasDelegateThatRespondsToSelector:(SEL)aSelector; + +- (GCDMulticastDelegateEnumerator *)delegateEnumerator; + +@end + + +@interface GCDMulticastDelegateEnumerator : NSObject + +- (NSUInteger)count; +- (NSUInteger)countOfClass:(Class)aClass; +- (NSUInteger)countForSelector:(SEL)aSelector; + +- (BOOL)getNextDelegate:(id *)delPtr delegateQueue:(dispatch_queue_t *)dqPtr; +- (BOOL)getNextDelegate:(id *)delPtr delegateQueue:(dispatch_queue_t *)dqPtr ofClass:(Class)aClass; +- (BOOL)getNextDelegate:(id *)delPtr delegateQueue:(dispatch_queue_t *)dqPtr forSelector:(SEL)aSelector; + +@end diff --git a/Utilities/GCDMulticastDelegate.m b/Utilities/GCDMulticastDelegate.m new file mode 100644 index 0000000..1785c27 --- /dev/null +++ b/Utilities/GCDMulticastDelegate.m @@ -0,0 +1,654 @@ +#import "GCDMulticastDelegate.h" +#import + +#if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE +#import +#endif + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +/** + * How does this class work? + * + * In theory, this class is very straight-forward. + * It provides a way for multiple delegates to be called, each on its own delegate queue. + * + * In other words, any delegate method call to this class + * will get forwarded (dispatch_async'd) to each added delegate. + * + * Important note concerning thread-safety: + * + * This class is designed to be used from within a single dispatch queue. + * In other words, it is NOT thread-safe, and should only be used from within the external dedicated dispatch_queue. +**/ + +@interface GCDMulticastDelegateNode : NSObject { +@private + + #if __has_feature(objc_arc_weak) + __weak id delegate; + #if !TARGET_OS_IPHONE + __unsafe_unretained id unsafeDelegate; // Some classes don't support weak references yet (e.g. NSWindowController) + #endif + #else + __unsafe_unretained id delegate; + #endif + + dispatch_queue_t delegateQueue; +} + +- (id)initWithDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; + +#if __has_feature(objc_arc_weak) +@property (/* atomic */ readwrite, weak) id delegate; +#if !TARGET_OS_IPHONE +@property (/* atomic */ readwrite, unsafe_unretained) id unsafeDelegate; +#endif +#else +@property (/* atomic */ readwrite, unsafe_unretained) id delegate; +#endif + +@property (nonatomic, readonly) dispatch_queue_t delegateQueue; + +@end + + +@interface GCDMulticastDelegate () +{ + NSMutableArray *delegateNodes; +} + +- (NSInvocation *)duplicateInvocation:(NSInvocation *)origInvocation; + +@end + + +@interface GCDMulticastDelegateEnumerator () +{ + NSUInteger numNodes; + NSUInteger currentNodeIndex; + NSArray *delegateNodes; +} + +- (id)initFromDelegateNodes:(NSMutableArray *)inDelegateNodes; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation GCDMulticastDelegate + +- (id)init +{ + if ((self = [super init])) + { + delegateNodes = [[NSMutableArray alloc] init]; + } + return self; +} + +- (void)addDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue +{ + if (delegate == nil) return; + if (delegateQueue == NULL) return; + + GCDMulticastDelegateNode *node = + [[GCDMulticastDelegateNode alloc] initWithDelegate:delegate delegateQueue:delegateQueue]; + + [delegateNodes addObject:node]; +} + +- (void)removeDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue +{ + if (delegate == nil) return; + + NSUInteger i; + for (i = [delegateNodes count]; i > 0; i--) + { + GCDMulticastDelegateNode *node = delegateNodes[i - 1]; + + id nodeDelegate = node.delegate; + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + if (nodeDelegate == [NSNull null]) + nodeDelegate = node.unsafeDelegate; + #endif + + if (delegate == nodeDelegate) + { + if ((delegateQueue == NULL) || (delegateQueue == node.delegateQueue)) + { + // Recall that this node may be retained by a GCDMulticastDelegateEnumerator. + // The enumerator is a thread-safe snapshot of the delegate list at the moment it was created. + // To properly remove this node from list, and from the list(s) of any enumerators, + // we nullify the delegate via the atomic property. + // + // However, the delegateQueue is not modified. + // The thread-safety is hinged on the atomic delegate property. + // The delegateQueue is expected to properly exist until the node is deallocated. + + node.delegate = nil; + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + node.unsafeDelegate = nil; + #endif + + [delegateNodes removeObjectAtIndex:(i-1)]; + } + } + } +} + +- (void)removeDelegate:(id)delegate +{ + [self removeDelegate:delegate delegateQueue:NULL]; +} + +- (void)removeAllDelegates +{ + for (GCDMulticastDelegateNode *node in delegateNodes) + { + node.delegate = nil; + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + node.unsafeDelegate = nil; + #endif + } + + [delegateNodes removeAllObjects]; +} + +- (NSUInteger)count +{ + return [delegateNodes count]; +} + +- (NSUInteger)countOfClass:(Class)aClass +{ + NSUInteger count = 0; + + for (GCDMulticastDelegateNode *node in delegateNodes) + { + id nodeDelegate = node.delegate; + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + if (nodeDelegate == [NSNull null]) + nodeDelegate = node.unsafeDelegate; + #endif + + if ([nodeDelegate isKindOfClass:aClass]) + { + count++; + } + } + + return count; +} + +- (NSUInteger)countForSelector:(SEL)aSelector +{ + NSUInteger count = 0; + + for (GCDMulticastDelegateNode *node in delegateNodes) + { + id nodeDelegate = node.delegate; + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + if (nodeDelegate == [NSNull null]) + nodeDelegate = node.unsafeDelegate; + #endif + + if ([nodeDelegate respondsToSelector:aSelector]) + { + count++; + } + } + + return count; +} + +- (BOOL)hasDelegateThatRespondsToSelector:(SEL)aSelector +{ + for (GCDMulticastDelegateNode *node in delegateNodes) + { + id nodeDelegate = node.delegate; + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + if (nodeDelegate == [NSNull null]) + nodeDelegate = node.unsafeDelegate; + #endif + + if ([nodeDelegate respondsToSelector:aSelector]) + { + return YES; + } + } + + return NO; +} + +- (GCDMulticastDelegateEnumerator *)delegateEnumerator +{ + return [[GCDMulticastDelegateEnumerator alloc] initFromDelegateNodes:delegateNodes]; +} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector +{ + for (GCDMulticastDelegateNode *node in delegateNodes) + { + id nodeDelegate = node.delegate; + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + if (nodeDelegate == [NSNull null]) + nodeDelegate = node.unsafeDelegate; + #endif + + NSMethodSignature *result = [nodeDelegate methodSignatureForSelector:aSelector]; + + if (result != nil) + { + return result; + } + } + + // This causes a crash... + // return [super methodSignatureForSelector:aSelector]; + + // This also causes a crash... + // return nil; + + return [[self class] instanceMethodSignatureForSelector:@selector(doNothing)]; +} + +- (void)forwardInvocation:(NSInvocation *)origInvocation +{ + SEL selector = [origInvocation selector]; + BOOL foundNilDelegate = NO; + + for (GCDMulticastDelegateNode *node in delegateNodes) + { + id nodeDelegate = node.delegate; + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + if (nodeDelegate == [NSNull null]) + nodeDelegate = node.unsafeDelegate; + #endif + + if ([nodeDelegate respondsToSelector:selector]) + { + // All delegates MUST be invoked ASYNCHRONOUSLY. + + NSInvocation *dupInvocation = [self duplicateInvocation:origInvocation]; + + dispatch_async(node.delegateQueue, ^{ @autoreleasepool { + + [dupInvocation invokeWithTarget:nodeDelegate]; + + }}); + } + else if (nodeDelegate == nil) + { + foundNilDelegate = YES; + } + } + + if (foundNilDelegate) + { + // At lease one weak delegate reference disappeared. + // Remove nil delegate nodes from the list. + // + // This is expected to happen very infrequently. + // This is why we handle it separately (as it requires allocating an indexSet). + + NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] init]; + + NSUInteger i = 0; + for (GCDMulticastDelegateNode *node in delegateNodes) + { + id nodeDelegate = node.delegate; + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + if (nodeDelegate == [NSNull null]) + nodeDelegate = node.unsafeDelegate; + #endif + + if (nodeDelegate == nil) + { + [indexSet addIndex:i]; + } + i++; + } + + [delegateNodes removeObjectsAtIndexes:indexSet]; + } +} + +- (void)doesNotRecognizeSelector:(SEL)aSelector +{ + // Prevent NSInvalidArgumentException +} + +- (void)doNothing {} + +- (void)dealloc +{ + [self removeAllDelegates]; +} + +- (NSInvocation *)duplicateInvocation:(NSInvocation *)origInvocation +{ + NSMethodSignature *methodSignature = [origInvocation methodSignature]; + + NSInvocation *dupInvocation = [NSInvocation invocationWithMethodSignature:methodSignature]; + [dupInvocation setSelector:[origInvocation selector]]; + + NSUInteger i, count = [methodSignature numberOfArguments]; + for (i = 2; i < count; i++) + { + const char *type = [methodSignature getArgumentTypeAtIndex:i]; + + if (*type == *@encode(BOOL)) + { + BOOL value; + [origInvocation getArgument:&value atIndex:i]; + [dupInvocation setArgument:&value atIndex:i]; + } + else if (*type == *@encode(char) || *type == *@encode(unsigned char)) + { + char value; + [origInvocation getArgument:&value atIndex:i]; + [dupInvocation setArgument:&value atIndex:i]; + } + else if (*type == *@encode(short) || *type == *@encode(unsigned short)) + { + short value; + [origInvocation getArgument:&value atIndex:i]; + [dupInvocation setArgument:&value atIndex:i]; + } + else if (*type == *@encode(int) || *type == *@encode(unsigned int)) + { + int value; + [origInvocation getArgument:&value atIndex:i]; + [dupInvocation setArgument:&value atIndex:i]; + } + else if (*type == *@encode(long) || *type == *@encode(unsigned long)) + { + long value; + [origInvocation getArgument:&value atIndex:i]; + [dupInvocation setArgument:&value atIndex:i]; + } + else if (*type == *@encode(long long) || *type == *@encode(unsigned long long)) + { + long long value; + [origInvocation getArgument:&value atIndex:i]; + [dupInvocation setArgument:&value atIndex:i]; + } + else if (*type == *@encode(double)) + { + double value; + [origInvocation getArgument:&value atIndex:i]; + [dupInvocation setArgument:&value atIndex:i]; + } + else if (*type == *@encode(float)) + { + float value; + [origInvocation getArgument:&value atIndex:i]; + [dupInvocation setArgument:&value atIndex:i]; + } + else if (*type == '@') + { + void *value; + [origInvocation getArgument:&value atIndex:i]; + [dupInvocation setArgument:&value atIndex:i]; + } + else if (*type == '^') + { + void *block; + [origInvocation getArgument:&block atIndex:i]; + [dupInvocation setArgument:&block atIndex:i]; + } + else + { + NSString *selectorStr = NSStringFromSelector([origInvocation selector]); + + NSString *format = @"Argument %lu to method %@ - Type(%c) not supported"; + NSString *reason = [NSString stringWithFormat:format, (unsigned long)(i - 2), selectorStr, *type]; + + [[NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil] raise]; + } + } + + [dupInvocation retainArguments]; + + return dupInvocation; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation GCDMulticastDelegateNode + +@synthesize delegate; // atomic +#if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE +@synthesize unsafeDelegate; // atomic +#endif +@synthesize delegateQueue; // non-atomic + +#if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE +static BOOL SupportsWeakReferences(id delegate) +{ + // From Apple's documentation: + // + // > Which classes don’t support weak references? + // > + // > You cannot currently create weak references to instances of the following classes: + // > + // > NSATSTypesetter, NSColorSpace, NSFont, NSFontManager, NSFontPanel, NSImage, NSMenuView, + // > NSParagraphStyle, NSSimpleHorizontalTypesetter, NSTableCellView, NSTextView, NSViewController, + // > NSWindow, and NSWindowController. + // > + // > In addition, in OS X no classes in the AV Foundation framework support weak references. + // + // NSMenuView is deprecated (and not available to 64-bit applications). + // NSSimpleHorizontalTypesetter is an internal class. + + if ([delegate isKindOfClass:[NSATSTypesetter class]]) return NO; + if ([delegate isKindOfClass:[NSColorSpace class]]) return NO; + if ([delegate isKindOfClass:[NSFont class]]) return NO; + if ([delegate isKindOfClass:[NSFontManager class]]) return NO; + if ([delegate isKindOfClass:[NSFontPanel class]]) return NO; + if ([delegate isKindOfClass:[NSImage class]]) return NO; + if ([delegate isKindOfClass:[NSParagraphStyle class]]) return NO; + if ([delegate isKindOfClass:[NSTableCellView class]]) return NO; + if ([delegate isKindOfClass:[NSTextView class]]) return NO; + if ([delegate isKindOfClass:[NSViewController class]]) return NO; + if ([delegate isKindOfClass:[NSWindow class]]) return NO; + if ([delegate isKindOfClass:[NSWindowController class]]) return NO; + + return YES; +} +#endif + +- (id)initWithDelegate:(id)inDelegate delegateQueue:(dispatch_queue_t)inDelegateQueue +{ + if ((self = [super init])) + { + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + { + if (SupportsWeakReferences(inDelegate)) + { + delegate = inDelegate; + delegateQueue = inDelegateQueue; + } + else + { + delegate = [NSNull null]; + + unsafeDelegate = inDelegate; + delegateQueue = inDelegateQueue; + } + } + #else + { + delegate = inDelegate; + delegateQueue = inDelegateQueue; + } + #endif + + #if !OS_OBJECT_USE_OBJC + if (delegateQueue) + dispatch_retain(delegateQueue); + #endif + } + return self; +} + +- (void)dealloc +{ + #if !OS_OBJECT_USE_OBJC + if (delegateQueue) + dispatch_release(delegateQueue); + #endif +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation GCDMulticastDelegateEnumerator + +- (id)initFromDelegateNodes:(NSMutableArray *)inDelegateNodes +{ + if ((self = [super init])) + { + delegateNodes = [inDelegateNodes copy]; + + numNodes = [delegateNodes count]; + currentNodeIndex = 0; + } + return self; +} + +- (NSUInteger)count +{ + return numNodes; +} + +- (NSUInteger)countOfClass:(Class)aClass +{ + NSUInteger count = 0; + + for (GCDMulticastDelegateNode *node in delegateNodes) + { + id nodeDelegate = node.delegate; + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + if (nodeDelegate == [NSNull null]) + nodeDelegate = node.unsafeDelegate; + #endif + + if ([nodeDelegate isKindOfClass:aClass]) + { + count++; + } + } + + return count; +} + +- (NSUInteger)countForSelector:(SEL)aSelector +{ + NSUInteger count = 0; + + for (GCDMulticastDelegateNode *node in delegateNodes) + { + id nodeDelegate = node.delegate; + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + if (nodeDelegate == [NSNull null]) + nodeDelegate = node.unsafeDelegate; + #endif + + if ([nodeDelegate respondsToSelector:aSelector]) + { + count++; + } + } + + return count; +} + +- (BOOL)getNextDelegate:(id *)delPtr delegateQueue:(dispatch_queue_t *)dqPtr +{ + while (currentNodeIndex < numNodes) + { + GCDMulticastDelegateNode *node = delegateNodes[currentNodeIndex]; + currentNodeIndex++; + + id nodeDelegate = node.delegate; // snapshot atomic property + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + if (nodeDelegate == [NSNull null]) + nodeDelegate = node.unsafeDelegate; + #endif + + if (nodeDelegate) + { + if (delPtr) *delPtr = nodeDelegate; + if (dqPtr) *dqPtr = node.delegateQueue; + + return YES; + } + } + + return NO; +} + +- (BOOL)getNextDelegate:(id *)delPtr delegateQueue:(dispatch_queue_t *)dqPtr ofClass:(Class)aClass +{ + while (currentNodeIndex < numNodes) + { + GCDMulticastDelegateNode *node = delegateNodes[currentNodeIndex]; + currentNodeIndex++; + + id nodeDelegate = node.delegate; // snapshot atomic property + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + if (nodeDelegate == [NSNull null]) + nodeDelegate = node.unsafeDelegate; + #endif + + if ([nodeDelegate isKindOfClass:aClass]) + { + if (delPtr) *delPtr = nodeDelegate; + if (dqPtr) *dqPtr = node.delegateQueue; + + return YES; + } + } + + return NO; +} + +- (BOOL)getNextDelegate:(id *)delPtr delegateQueue:(dispatch_queue_t *)dqPtr forSelector:(SEL)aSelector +{ + while (currentNodeIndex < numNodes) + { + GCDMulticastDelegateNode *node = delegateNodes[currentNodeIndex]; + currentNodeIndex++; + + id nodeDelegate = node.delegate; // snapshot atomic property + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + if (nodeDelegate == [NSNull null]) + nodeDelegate = node.unsafeDelegate; + #endif + + if ([nodeDelegate respondsToSelector:aSelector]) + { + if (delPtr) *delPtr = nodeDelegate; + if (dqPtr) *dqPtr = node.delegateQueue; + + return YES; + } + } + + return NO; +} + +@end diff --git a/Utilities/RFImageToDataTransformer.h b/Utilities/RFImageToDataTransformer.h new file mode 100644 index 0000000..579ab4b --- /dev/null +++ b/Utilities/RFImageToDataTransformer.h @@ -0,0 +1,19 @@ + +/* + File: RFImageToDataTransformer.h + Abstract: A value transformer, which transforms a UIImage or NSImage object into an NSData object. + + Based on Apple's UIImageToDataTransformer + + Copyright (C) 2010 Apple Inc. All Rights Reserved. + Copyright (C) 2011 RF.com All Rights Reserved. + */ + +#import + + +@interface RFImageToDataTransformer : NSValueTransformer { + +} + +@end diff --git a/Utilities/RFImageToDataTransformer.m b/Utilities/RFImageToDataTransformer.m new file mode 100644 index 0000000..a7df718 --- /dev/null +++ b/Utilities/RFImageToDataTransformer.m @@ -0,0 +1,48 @@ + +/* + File: RFImageToDataTransformer.m + Abstract: A value transformer, which transforms a UIImage or NSImage object into an NSData object. + + Based on Apple's UIImageToDataTransformer + + Copyright (C) 2010 Apple Inc. All Rights Reserved. + Copyright (C) 2011 RF.com All Rights Reserved. + */ + +#import "RFImageToDataTransformer.h" + +#if TARGET_OS_IPHONE +#import +#else + +#endif + + +@implementation RFImageToDataTransformer + ++ (BOOL)allowsReverseTransformation { + return YES; +} + ++ (Class)transformedValueClass { + return [NSData class]; +} + +- (id)transformedValue:(id)value { +#if TARGET_OS_IPHONE + return UIImagePNGRepresentation(value); +#else + return [(NSImage *)value TIFFRepresentation]; +#endif +} + +- (id)reverseTransformedValue:(id)value { +#if TARGET_OS_IPHONE + return [[UIImage alloc] initWithData:value]; +#else + return [[NSImage alloc] initWithData:value]; +#endif +} + +@end + diff --git a/Utilities/XMPPIDTracker.h b/Utilities/XMPPIDTracker.h new file mode 100644 index 0000000..ea5b58f --- /dev/null +++ b/Utilities/XMPPIDTracker.h @@ -0,0 +1,210 @@ +#import + +@protocol XMPPTrackingInfo; + +@class XMPPElement; + +@class XMPPStream; + +extern const NSTimeInterval XMPPIDTrackerTimeoutNone; + +/** + * A common operation in XMPP is to send some kind of request with a unique id, + * and wait for the response to come back. + * The most common example is sending an IQ of type='get' with a unique id, and then awaiting the response. + * + * In order to properly handle the response, the id must be stored. + * If there are multiple queries going out and/or different kinds of queries, + * then information about the appropriate handling of the response must also be stored. + * This may be accomplished by storing the appropriate selector, or perhaps a block handler. + * Additionally one may need to setup timeouts and handle those properly as well. + * + * This class provides the scaffolding to simplify the tasks associated with this common operation. + * Essentially, it provides the following: + * - a dictionary where the unique id is the key, and the needed tracking info is the object + * - an optional timer to fire upon a timeout + * + * The class is designed to be flexible. + * You can provide a target/selector or a block handler to be invoked. + * Additionally, you can use the basic tracking info, or you can extend it to suit your needs. + * + * It is best illustrated with a few examples. + * + * ---- EXAMPLE 1 - SIMPLE TRACKING WITH TARGET / SELECTOR ---- + * + * XMPPIQ *iq = ... + * [iqTracker addID:[iq elementID] target:self selector:@selector(processBookQuery:withInfo:) timeout:15.0]; + * + * - (void)processBookQueury:(XMPPIQ *)iq withInfo:(id ) = ^(XMPPIQ *iq, id info) { + * ... + * }; + * [iqTracker addID:[iq elementID] block:blockHandler timeout:15.0]; + * + * // Same xmppStream:didReceiveIQ: as example 1 + * + * ---- EXAMPLE 3 - ADVANCED TRACKING ---- + * + * @interface PingTrackingInfo : XMPPBasicTrackingInfo + * ... + * @end + * + * XMPPIQ *ping = ... + * PingTrackingInfo *pingInfo = ... + * + * [iqTracker addID:[ping elementID] trackingInfo:pingInfo]; + * + * - (void)handlePong:(XMPPIQ *)iq withInfo:(PingTrackingInfo *)info { + * ... + * } + * + * // Same xmppStream:didReceiveIQ: as example 1 + * + * + * ---- Validating Responses ---- + * + * XMPPIDTracker can also be used to validate that the response was from the expected jid. + * To do this you need to initalize XMPPIDTracker with the stream where the request/response is going to be tracked. + * + * xmppIDTracker = [[XMPPIDTracker alloc] initWithStream:stream dispatchQueue:queue]; + * + * You also need to supply the element (not just the ID) to the add an invoke methods. + * + * ---- EXAMPLE 1 - SIMPLE TRACKING WITH TARGET / SELECTOR AND VALIDATION ---- + * + * XMPPIQ *iq = ... + * [iqTracker addElement:iq target:self selector:@selector(processBookQuery:withInfo:) timeout:15.0]; + * + * - (void)processBookQueury:(XMPPIQ *)iq withInfo:(id info))block + timeout:(NSTimeInterval)timeout; + +- (void)addElement:(XMPPElement *)element + block:(void (^)(id obj, id info))block + timeout:(NSTimeInterval)timeout; + +- (void)addID:(NSString *)elementID trackingInfo:(id )trackingInfo; + +- (void)addElement:(XMPPElement *)element trackingInfo:(id )trackingInfo; + +- (BOOL)invokeForID:(NSString *)elementID withObject:(id)obj; + +- (BOOL)invokeForElement:(XMPPElement *)element withObject:(id)obj; + +- (NSUInteger)numberOfIDs; + +- (void)removeID:(NSString *)elementID; +- (void)removeAllIDs; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPTrackingInfo + +@property (nonatomic, readonly) NSTimeInterval timeout; + +@property (nonatomic, readwrite, copy) NSString *elementID; + +@property (nonatomic, readwrite, copy) XMPPElement *element; + +- (void)createTimerWithDispatchQueue:(dispatch_queue_t)queue; +- (void)cancelTimer; + +- (void)invokeWithObject:(id)obj; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPBasicTrackingInfo : NSObject +{ + id target; + SEL selector; + + void (^block)(id obj, id info); + + NSTimeInterval timeout; + + NSString *elementID; + XMPPElement *element; + dispatch_source_t timer; +} + +- (id)initWithTarget:(id)target selector:(SEL)selector timeout:(NSTimeInterval)timeout; +- (id)initWithBlock:(void (^)(id obj, id info))block timeout:(NSTimeInterval)timeout; + +@property (nonatomic, readonly) NSTimeInterval timeout; + +@property (nonatomic, readwrite, copy) NSString *elementID; + +@property (nonatomic, readwrite, copy) XMPPElement *element; + +- (void)createTimerWithDispatchQueue:(dispatch_queue_t)queue; +- (void)cancelTimer; + +- (void)invokeWithObject:(id)obj; + +@end diff --git a/Utilities/XMPPIDTracker.m b/Utilities/XMPPIDTracker.m new file mode 100644 index 0000000..2bd62db --- /dev/null +++ b/Utilities/XMPPIDTracker.m @@ -0,0 +1,347 @@ +#import "XMPPIDTracker.h" +#import "XMPP.h" +#import "XMPPLogging.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_VERBOSE; // | XMPP_LOG_FLAG_TRACE; +#else +static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +#define AssertProperQueue() NSAssert(dispatch_get_specific(queueTag), @"Invoked on incorrect queue") + +const NSTimeInterval XMPPIDTrackerTimeoutNone = -1; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPIDTracker () +{ + void *queueTag; +} + +@end + +@implementation XMPPIDTracker + +- (id)init +{ + // You must use initWithDispatchQueue or initWithStream:dispatchQueue: + return nil; +} + +- (id)initWithDispatchQueue:(dispatch_queue_t)aQueue +{ + return [self initWithStream:nil dispatchQueue:aQueue]; +} + +- (id)initWithStream:(XMPPStream *)stream dispatchQueue:(dispatch_queue_t)aQueue +{ + NSParameterAssert(aQueue != NULL); + + if ((self = [super init])) + { + xmppStream = stream; + + queue = aQueue; + + queueTag = &queueTag; + dispatch_queue_set_specific(queue, queueTag, queueTag, NULL); + +#if !OS_OBJECT_USE_OBJC + dispatch_retain(queue); +#endif + + dict = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (void)dealloc +{ + // We don't call [self removeAllIDs] because dealloc might not be invoked on queue + + for (id info in [dict objectEnumerator]) + { + [info cancelTimer]; + } + [dict removeAllObjects]; + + #if !OS_OBJECT_USE_OBJC + dispatch_release(queue); + #endif +} + +- (void)addID:(NSString *)elementID target:(id)target selector:(SEL)selector timeout:(NSTimeInterval)timeout +{ + AssertProperQueue(); + + XMPPBasicTrackingInfo *trackingInfo; + trackingInfo = [[XMPPBasicTrackingInfo alloc] initWithTarget:target selector:selector timeout:timeout]; + + [self addID:elementID trackingInfo:trackingInfo]; +} + +- (void)addElement:(XMPPElement *)element target:(id)target selector:(SEL)selector timeout:(NSTimeInterval)timeout +{ + AssertProperQueue(); + + XMPPBasicTrackingInfo *trackingInfo; + trackingInfo = [[XMPPBasicTrackingInfo alloc] initWithTarget:target selector:selector timeout:timeout]; + + [self addElement:element trackingInfo:trackingInfo]; +} + +- (void)addID:(NSString *)elementID + block:(void (^)(id obj, id info))block + timeout:(NSTimeInterval)timeout +{ + AssertProperQueue(); + + XMPPBasicTrackingInfo *trackingInfo; + trackingInfo = [[XMPPBasicTrackingInfo alloc] initWithBlock:block timeout:timeout]; + + [self addID:elementID trackingInfo:trackingInfo]; +} + + +- (void)addElement:(XMPPElement *)element + block:(void (^)(id obj, id info))block + timeout:(NSTimeInterval)timeout +{ + AssertProperQueue(); + + XMPPBasicTrackingInfo *trackingInfo; + trackingInfo = [[XMPPBasicTrackingInfo alloc] initWithBlock:block timeout:timeout]; + + [self addElement:element trackingInfo:trackingInfo]; +} + +- (void)addID:(NSString *)elementID trackingInfo:(id )trackingInfo +{ + AssertProperQueue(); + + dict[elementID] = trackingInfo; + + [trackingInfo setElementID:elementID]; + [trackingInfo createTimerWithDispatchQueue:queue]; +} + +- (void)addElement:(XMPPElement *)element trackingInfo:(id )trackingInfo +{ + AssertProperQueue(); + + if([[element elementID] length] == 0) return; + + dict[[element elementID]] = trackingInfo; + + [trackingInfo setElementID:[element elementID]]; + [trackingInfo setElement:element]; + [trackingInfo createTimerWithDispatchQueue:queue]; +} + +- (BOOL)invokeForID:(NSString *)elementID withObject:(id)obj +{ + AssertProperQueue(); + + if([elementID length] == 0) return NO; + + id info = dict[elementID]; + + if (info) + { + [info invokeWithObject:obj]; + [info cancelTimer]; + [dict removeObjectForKey:elementID]; + + return YES; + } + + return NO; +} + +- (BOOL)invokeForElement:(XMPPElement *)element withObject:(id)obj +{ + AssertProperQueue(); + + NSString *elementID = [element elementID]; + + if ([elementID length] == 0) return NO; + + id info = dict[elementID]; + if(info) + { + BOOL valid = YES; + + if(xmppStream && [element isKindOfClass:[XMPPIQ class]] && [[info element] isKindOfClass:[XMPPIQ class]]) + { + XMPPIQ *iq = (XMPPIQ *)element; + + if([iq isResultIQ] || [iq isErrorIQ]) + { + valid = [xmppStream isValidResponseElement:iq forRequestElement:[info element]]; + } + } + + if(!valid) + { + XMPPLogError(@"%s: Element with ID %@ cannot be validated.", __FILE__ , [element elementID]); + } + + if (valid) + { + [info invokeWithObject:obj]; + [info cancelTimer]; + [dict removeObjectForKey:[element elementID]]; + + return YES; + } + } + + return NO; +} + +- (NSUInteger)numberOfIDs +{ + AssertProperQueue(); + + return [[dict allKeys] count]; +} + +- (void)removeID:(NSString *)elementID +{ + AssertProperQueue(); + + id info = dict[elementID]; + if (info) + { + [info cancelTimer]; + [dict removeObjectForKey:elementID]; + } +} + +- (void)removeAllIDs +{ + AssertProperQueue(); + + for (id info in [dict objectEnumerator]) + { + [info cancelTimer]; + } + [dict removeAllObjects]; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPBasicTrackingInfo + +@synthesize timeout; +@synthesize elementID; +@synthesize element; + +- (id)init +{ + // Use initWithTarget:selector:timeout: or initWithBlock:timeout: + + return nil; +} + +- (id)initWithTarget:(id)aTarget selector:(SEL)aSelector timeout:(NSTimeInterval)aTimeout +{ + if(target || selector) + { + NSParameterAssert(aTarget); + NSParameterAssert(aSelector); + } + + if ((self = [super init])) + { + target = aTarget; + selector = aSelector; + timeout = aTimeout; + } + return self; +} + +- (id)initWithBlock:(void (^)(id obj, id info))aBlock timeout:(NSTimeInterval)aTimeout +{ + NSParameterAssert(aBlock); + + if ((self = [super init])) + { + block = [aBlock copy]; + timeout = aTimeout; + } + return self; +} + +- (void)dealloc +{ + [self cancelTimer]; + + target = nil; + selector = NULL; +} + +- (void)createTimerWithDispatchQueue:(dispatch_queue_t)queue +{ + NSAssert(queue != NULL, @"Method invoked with NULL queue"); + NSAssert(timer == NULL, @"Method invoked multiple times"); + + if (timeout > 0.0) + { + timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); + + dispatch_source_set_event_handler(timer, ^{ @autoreleasepool { + + [self invokeWithObject:nil]; + [self cancelTimer]; + + }}); + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (timeout * NSEC_PER_SEC)); + + dispatch_source_set_timer(timer, tt, DISPATCH_TIME_FOREVER, 0); + dispatch_resume(timer); + } +} + +- (void)cancelTimer +{ + if (timer) + { + dispatch_source_cancel(timer); + #if !OS_OBJECT_USE_OBJC + dispatch_release(timer); + #endif + timer = NULL; + } +} + +- (void)invokeWithObject:(id)obj +{ + if (block) + { + block(obj, self); + } + else if(target && selector) + { + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Warc-performSelector-leaks" + [target performSelector:selector withObject:obj withObject:self]; + #pragma clang diagnostic pop + } +} + +@end diff --git a/Utilities/XMPPSRVResolver.h b/Utilities/XMPPSRVResolver.h new file mode 100644 index 0000000..571e696 --- /dev/null +++ b/Utilities/XMPPSRVResolver.h @@ -0,0 +1,86 @@ +// +// XMPPSRVResolver.h +// +// Originally created by Eric Chamberlain on 6/15/10. +// Based on SRVResolver by Apple, Inc. +// + +#import +#include + +extern NSString *const XMPPSRVResolverErrorDomain; + + +@interface XMPPSRVResolver : NSObject +{ + __unsafe_unretained id delegate; + dispatch_queue_t delegateQueue; + + dispatch_queue_t resolverQueue; + void *resolverQueueTag; + + __strong NSString *srvName; + NSTimeInterval timeout; + + BOOL resolveInProgress; + + NSMutableArray *results; + DNSServiceRef sdRef; + + int sdFd; + dispatch_source_t sdReadSource; + dispatch_source_t timeoutTimer; +} + +/** + * The delegate & delegateQueue are mandatory. + * The resolverQueue is optional. If NULL, it will automatically create it's own internal queue. +**/ +- (id)initWithdDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq resolverQueue:(dispatch_queue_t)rq; + +@property (strong, readonly) NSString *srvName; +@property (readonly) NSTimeInterval timeout; + +- (void)startWithSRVName:(NSString *)aSRVName timeout:(NSTimeInterval)aTimeout; +- (void)stop; + ++ (NSString *)srvNameFromXMPPDomain:(NSString *)xmppDomain; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPSRVResolverDelegate + +- (void)xmppSRVResolver:(XMPPSRVResolver *)sender didResolveRecords:(NSArray *)records; +- (void)xmppSRVResolver:(XMPPSRVResolver *)sender didNotResolveDueToError:(NSError *)error; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPSRVRecord : NSObject +{ + UInt16 priority; + UInt16 weight; + UInt16 port; + NSString *target; + + NSUInteger sum; + NSUInteger srvResultsIndex; +} + ++ (XMPPSRVRecord *)recordWithPriority:(UInt16)priority weight:(UInt16)weight port:(UInt16)port target:(NSString *)target; + +- (id)initWithPriority:(UInt16)priority weight:(UInt16)weight port:(UInt16)port target:(NSString *)target; + +@property (nonatomic, readonly) UInt16 priority; +@property (nonatomic, readonly) UInt16 weight; +@property (nonatomic, readonly) UInt16 port; +@property (nonatomic, readonly) NSString *target; + +@end diff --git a/Utilities/XMPPSRVResolver.m b/Utilities/XMPPSRVResolver.m new file mode 100644 index 0000000..7a4c543 --- /dev/null +++ b/Utilities/XMPPSRVResolver.m @@ -0,0 +1,687 @@ +// +// XMPPSRVResolver.m +// +// Originally created by Eric Chamberlain on 6/15/10. +// Based on SRVResolver by Apple, Inc. +// + +#import "XMPPSRVResolver.h" +#import "XMPPLogging.h" + +//#warning Fix "dns.h" issue without resorting to this ugly hack. +// This is a hack to prevent OnionKit's clobbering of the actual system's +//#include "/usr/include/dns.h" + +#include +#include + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +NSString *const XMPPSRVResolverErrorDomain = @"XMPPSRVResolverErrorDomain"; + +// Log levels: off, error, warn, info, verbose +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPSRVRecord () + +@property(nonatomic, assign) NSUInteger srvResultsIndex; +@property(nonatomic, assign) NSUInteger sum; + +- (NSComparisonResult)compareByPriority:(XMPPSRVRecord *)aRecord; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPSRVResolver + +- (id)initWithdDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq resolverQueue:(dispatch_queue_t)rq +{ + NSParameterAssert(aDelegate != nil); + NSParameterAssert(dq != NULL); + + if ((self = [super init])) + { + XMPPLogTrace(); + + delegate = aDelegate; + delegateQueue = dq; + + #if !OS_OBJECT_USE_OBJC + dispatch_retain(delegateQueue); + #endif + + if (rq) + { + resolverQueue = rq; + #if !OS_OBJECT_USE_OBJC + dispatch_retain(resolverQueue); + #endif + } + else + { + resolverQueue = dispatch_queue_create("XMPPSRVResolver", NULL); + } + + resolverQueueTag = &resolverQueueTag; + dispatch_queue_set_specific(resolverQueue, resolverQueueTag, resolverQueueTag, NULL); + + results = [[NSMutableArray alloc] initWithCapacity:2]; + } + return self; +} + +- (void)dealloc +{ + XMPPLogTrace(); + + [self stop]; + + #if !OS_OBJECT_USE_OBJC + if (resolverQueue) + dispatch_release(resolverQueue); + #endif +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Properties +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@dynamic srvName; +@dynamic timeout; + +- (NSString *)srvName +{ + __block NSString *result = nil; + + dispatch_block_t block = ^{ + result = [srvName copy]; + }; + + if (dispatch_get_specific(resolverQueueTag)) + block(); + else + dispatch_sync(resolverQueue, block); + + return result; +} + +- (NSTimeInterval)timeout +{ + __block NSTimeInterval result = 0.0; + + dispatch_block_t block = ^{ + result = timeout; + }; + + if (dispatch_get_specific(resolverQueueTag)) + block(); + else + dispatch_sync(resolverQueue, block); + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Private Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)sortResults +{ + NSAssert(dispatch_get_specific(resolverQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + // Sort results + NSMutableArray *sortedResults = [NSMutableArray arrayWithCapacity:[results count]]; + + // Sort the list by priority (lowest number first) + [results sortUsingSelector:@selector(compareByPriority:)]; + + /* From RFC 2782 + * + * For each distinct priority level + * While there are still elements left at this priority level + * + * Select an element as specified above, in the + * description of Weight in "The format of the SRV + * RR" Section, and move it to the tail of the new + * list. + * + * The following algorithm SHOULD be used to order + * the SRV RRs of the same priority: + */ + + NSUInteger srvResultsCount; + + while ([results count] > 0) + { + srvResultsCount = [results count]; + + if (srvResultsCount == 1) + { + XMPPSRVRecord *srvRecord = results[0]; + + [sortedResults addObject:srvRecord]; + [results removeObjectAtIndex:0]; + } + else // (srvResultsCount > 1) + { + // more than two records so we need to sort + + /* To select a target to be contacted next, arrange all SRV RRs + * (that have not been ordered yet) in any order, except that all + * those with weight 0 are placed at the beginning of the list. + * + * Compute the sum of the weights of those RRs, and with each RR + * associate the running sum in the selected order. + */ + + NSUInteger runningSum = 0; + NSMutableArray *samePriorityRecords = [NSMutableArray arrayWithCapacity:srvResultsCount]; + + XMPPSRVRecord *srvRecord = results[0]; + + NSUInteger initialPriority = srvRecord.priority; + NSUInteger index = 0; + + do + { + if (srvRecord.weight == 0) + { + // add to front of array + [samePriorityRecords insertObject:srvRecord atIndex:0]; + + srvRecord.srvResultsIndex = index; + srvRecord.sum = 0; + } + else + { + // add to end of array and update the running sum + [samePriorityRecords addObject:srvRecord]; + + runningSum += srvRecord.weight; + + srvRecord.srvResultsIndex = index; + srvRecord.sum = runningSum; + } + + if (++index < srvResultsCount) + { + srvRecord = results[index]; + } + else + { + srvRecord = nil; + } + + } while(srvRecord && (srvRecord.priority == initialPriority)); + + /* Then choose a uniform random number between 0 and the sum computed + * (inclusive), and select the RR whose running sum value is the + * first in the selected order which is greater than or equal to + * the random number selected. + */ + + NSUInteger randomIndex = arc4random() % (runningSum + 1); + + for (srvRecord in samePriorityRecords) + { + if (srvRecord.sum >= randomIndex) + { + /* The target host specified in the + * selected SRV RR is the next one to be contacted by the client. + * Remove this SRV RR from the set of the unordered SRV RRs and + * apply the described algorithm to the unordered SRV RRs to select + * the next target host. Continue the ordering process until there + * are no unordered SRV RRs. This process is repeated for each + * Priority. + */ + + [sortedResults addObject:srvRecord]; + [results removeObjectAtIndex:srvRecord.srvResultsIndex]; + + break; + } + } + } + } + + results = sortedResults; + + XMPPLogVerbose(@"%@: Sorted results:\n%@", THIS_FILE, results); +} + +- (void)succeed +{ + NSAssert(dispatch_get_specific(resolverQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + [self sortResults]; + + id theDelegate = delegate; + NSArray *records = [results copy]; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + SEL selector = @selector(xmppSRVResolver:didResolveRecords:); + + if ([theDelegate respondsToSelector:selector]) + { + [theDelegate xmppSRVResolver:self didResolveRecords:records]; + } + else + { + XMPPLogWarn(@"%@: delegate doesn't implement %@", THIS_FILE, NSStringFromSelector(selector)); + } + + }}); + + [self stop]; +} + +- (void)failWithError:(NSError *)error +{ + NSAssert(dispatch_get_specific(resolverQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace2(@"%@: %@ %@", THIS_FILE, THIS_METHOD, error); + + id theDelegate = delegate; + + if (delegateQueue != NULL) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + SEL selector = @selector(xmppSRVResolver:didNotResolveDueToError:); + + if ([theDelegate respondsToSelector:selector]) + { + [theDelegate xmppSRVResolver:self didNotResolveDueToError:error]; + } + else + { + XMPPLogWarn(@"%@: delegate doesn't implement %@", THIS_FILE, NSStringFromSelector(selector)); + } + + }}); + } + + [self stop]; +} + +- (void)failWithDNSError:(DNSServiceErrorType)sdErr +{ + XMPPLogTrace2(@"%@: %@ %i", THIS_FILE, THIS_METHOD, (int)sdErr); + + [self failWithError:[NSError errorWithDomain:XMPPSRVResolverErrorDomain code:sdErr userInfo:nil]]; +} + +- (XMPPSRVRecord *)processRecord:(const void *)rdata length:(uint16_t)rdlen +{ + XMPPLogTrace(); + + // Note: This method is almost entirely from Apple's sample code. + // + // Otherwise there would be a lot more comments and explanation... + + if (rdata == NULL) + { + XMPPLogWarn(@"%@: %@ - rdata == NULL", THIS_FILE, THIS_METHOD); + return nil; + } + + // Rather than write a whole bunch of icky parsing code, I just synthesise + // a resource record and use . + + XMPPSRVRecord *result = nil; + + NSMutableData * rrData; + dns_resource_record_t * rr; + uint8_t u8; // 1 byte + uint16_t u16; // 2 bytes + uint32_t u32; // 4 bytes + + rrData = [NSMutableData dataWithCapacity:(1 + 2 + 2 + 4 + 2 + rdlen)]; + + u8 = 0; + [rrData appendBytes:&u8 length:sizeof(u8)]; + u16 = htons(kDNSServiceType_SRV); + [rrData appendBytes:&u16 length:sizeof(u16)]; + u16 = htons(kDNSServiceClass_IN); + [rrData appendBytes:&u16 length:sizeof(u16)]; + u32 = htonl(666); + [rrData appendBytes:&u32 length:sizeof(u32)]; + u16 = htons(rdlen); + [rrData appendBytes:&u16 length:sizeof(u16)]; + [rrData appendBytes:rdata length:rdlen]; + + // Parse the record. + + rr = dns_parse_resource_record([rrData bytes], (uint32_t) [rrData length]); + if (rr != NULL) + { + NSString *target; + + target = [NSString stringWithCString:rr->data.SRV->target encoding:NSASCIIStringEncoding]; + if (target != nil) + { + UInt16 priority = rr->data.SRV->priority; + UInt16 weight = rr->data.SRV->weight; + UInt16 port = rr->data.SRV->port; + + result = [XMPPSRVRecord recordWithPriority:priority weight:weight port:port target:target]; + } + + dns_free_resource_record(rr); + } + + return result; +} + +static void QueryRecordCallback(DNSServiceRef sdRef, + DNSServiceFlags flags, + uint32_t interfaceIndex, + DNSServiceErrorType errorCode, + const char * fullname, + uint16_t rrtype, + uint16_t rrclass, + uint16_t rdlen, + const void * rdata, + uint32_t ttl, + void * context) +{ + // Called when we get a response to our query. + // It does some preliminary work, but the bulk of the interesting stuff + // is done in the processRecord:length: method. + + XMPPSRVResolver *resolver = (__bridge XMPPSRVResolver *)context; + + NSCAssert(dispatch_get_specific(resolver->resolverQueueTag), @"Invoked on incorrect queue"); + + XMPPLogCTrace(); + + if (!(flags & kDNSServiceFlagsAdd)) + { + // If the kDNSServiceFlagsAdd flag is not set, the domain information is not valid. + return; + } + + if (errorCode == kDNSServiceErr_NoError && + rrtype == kDNSServiceType_SRV) + { + XMPPSRVRecord *record = [resolver processRecord:rdata length:rdlen]; + if (record) + { + [resolver->results addObject:record]; + } + + if ( ! (flags & kDNSServiceFlagsMoreComing) ) + { + [resolver succeed]; + } + } + else + { + [resolver failWithDNSError:errorCode]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Public Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)startWithSRVName:(NSString *)aSRVName timeout:(NSTimeInterval)aTimeout +{ + dispatch_block_t block = ^{ @autoreleasepool { + + if (resolveInProgress) + { + return; + } + + XMPPLogTrace2(@"%@: startWithSRVName:%@ timeout:%f", THIS_FILE, aSRVName, aTimeout); + + // Save parameters + + srvName = [aSRVName copy]; + + timeout = aTimeout; + + // Check parameters + + const char *srvNameCStr = [srvName cStringUsingEncoding:NSASCIIStringEncoding]; + if (srvNameCStr == NULL) + { + [self failWithDNSError:kDNSServiceErr_BadParam]; + return; + + } + + // Create DNS Service + + DNSServiceErrorType sdErr; + sdErr = DNSServiceQueryRecord(&sdRef, // Pointer to unitialized DNSServiceRef + kDNSServiceFlagsReturnIntermediates, // Flags + kDNSServiceInterfaceIndexAny, // Interface index + srvNameCStr, // Full domain name + kDNSServiceType_SRV, // rrtype + kDNSServiceClass_IN, // rrclass + QueryRecordCallback, // Callback method + (__bridge void *)self); // Context pointer + + if (sdErr != kDNSServiceErr_NoError) + { + [self failWithDNSError:sdErr]; + return; + } + + // Extract unix socket (so we can poll for events) + + sdFd = DNSServiceRefSockFD(sdRef); + if (sdFd < 0) + { + // Todo... + } + + // Create GCD read source for sd file descriptor + + sdReadSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, sdFd, 0, resolverQueue); + + dispatch_source_set_event_handler(sdReadSource, ^{ @autoreleasepool { + + XMPPLogVerbose(@"%@: sdReadSource_eventHandler", THIS_FILE); + + // There is data to be read on the socket (or an error occurred). + // + // Invoking DNSServiceProcessResult will invoke our QueryRecordCallback, + // the callback we set when we created the sdRef. + + DNSServiceErrorType dnsErr = DNSServiceProcessResult(sdRef); + if (dnsErr != kDNSServiceErr_NoError) + { + [self failWithDNSError:dnsErr]; + } + + }}); + + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theSdReadSource = sdReadSource; + #endif + DNSServiceRef theSdRef = sdRef; + + dispatch_source_set_cancel_handler(sdReadSource, ^{ @autoreleasepool { + + XMPPLogVerbose(@"%@: sdReadSource_cancelHandler", THIS_FILE); + + #if !OS_OBJECT_USE_OBJC + dispatch_release(theSdReadSource); + #endif + DNSServiceRefDeallocate(theSdRef); + + }}); + + dispatch_resume(sdReadSource); + + // Create timer (if requested timeout > 0) + + if (timeout > 0.0) + { + timeoutTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, resolverQueue); + + dispatch_source_set_event_handler(timeoutTimer, ^{ @autoreleasepool { + + NSString *errMsg = @"Operation timed out"; + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + NSError *err = [NSError errorWithDomain:XMPPSRVResolverErrorDomain code:0 userInfo:userInfo]; + + [self failWithError:err]; + + }}); + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (timeout * NSEC_PER_SEC)); + + dispatch_source_set_timer(timeoutTimer, tt, DISPATCH_TIME_FOREVER, 0); + dispatch_resume(timeoutTimer); + } + + resolveInProgress = YES; + }}; + + if (dispatch_get_specific(resolverQueueTag)) + block(); + else + dispatch_async(resolverQueue, block); +} + +- (void)stop +{ + dispatch_block_t block = ^{ @autoreleasepool { + + XMPPLogTrace(); + + delegate = nil; + if (delegateQueue) + { + #if !OS_OBJECT_USE_OBJC + dispatch_release(delegateQueue); + #endif + delegateQueue = NULL; + } + + [results removeAllObjects]; + + if (sdReadSource) + { + // Cancel the readSource. + // It will be released from within the cancel handler. + dispatch_source_cancel(sdReadSource); + sdReadSource = NULL; + sdFd = -1; + + // The sdRef will be deallocated from within the cancel handler too. + sdRef = NULL; + } + + if (timeoutTimer) + { + dispatch_source_cancel(timeoutTimer); + #if !OS_OBJECT_USE_OBJC + dispatch_release(timeoutTimer); + #endif + timeoutTimer = NULL; + } + + resolveInProgress = NO; + }}; + + if (dispatch_get_specific(resolverQueueTag)) + block(); + else + dispatch_sync(resolverQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Utility Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ++ (NSString *)srvNameFromXMPPDomain:(NSString *)xmppDomain +{ + if (xmppDomain == nil) + return nil; + else + return [NSString stringWithFormat:@"_xmpp-client._tcp.%@", xmppDomain]; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPSRVRecord + +@synthesize priority; +@synthesize weight; +@synthesize port; +@synthesize target; + +@synthesize sum; +@synthesize srvResultsIndex; + + ++ (XMPPSRVRecord *)recordWithPriority:(UInt16)p1 weight:(UInt16)w port:(UInt16)p2 target:(NSString *)t +{ + return [[XMPPSRVRecord alloc] initWithPriority:p1 weight:w port:p2 target:t]; +} + +- (id)initWithPriority:(UInt16)p1 weight:(UInt16)w port:(UInt16)p2 target:(NSString *)t +{ + if ((self = [super init])) + { + priority = p1; + weight = w; + port = p2; + target = [t copy]; + + sum = 0; + srvResultsIndex = 0; + } + return self; +} + + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@:%p target(%@) port(%hu) priority(%hu) weight(%hu)>", + NSStringFromClass([self class]), self, target, port, priority, weight]; +} + +- (NSComparisonResult)compareByPriority:(XMPPSRVRecord *)aRecord +{ + UInt16 mPriority = self.priority; + UInt16 aPriority = aRecord.priority; + + if (mPriority < aPriority) + return NSOrderedAscending; + + if (mPriority > aPriority) + return NSOrderedDescending; + + return NSOrderedSame; +} + +@end diff --git a/Utilities/XMPPStringPrep.h b/Utilities/XMPPStringPrep.h new file mode 100644 index 0000000..c1d41a8 --- /dev/null +++ b/Utilities/XMPPStringPrep.h @@ -0,0 +1,41 @@ +#import + + +@interface XMPPStringPrep : NSObject + +/** + * Preps a node identifier for use in a JID. + * If the given node is invalid, this method returns nil. + * + * See the XMPP RFC (6120) for details. + * + * Note: The prep properly converts the string to lowercase, as per the RFC. +**/ ++ (NSString *)prepNode:(NSString *)node; + +/** + * Preps a domain name for use in a JID. + * If the given domain is invalid, this method returns nil. + * + * See the XMPP RFC (6120) for details. +**/ ++ (NSString *)prepDomain:(NSString *)domain; + +/** + * Preps a resource identifier for use in a JID. + * If the given node is invalid, this method returns nil. + * + * See the XMPP RFC (6120) for details. + **/ ++ (NSString *)prepResource:(NSString *)resource; + +/** + * Preps a password with SASLprep profile. + * If the given string is invalid, this method returns nil. + * + * See the SCRAM RFC (5802) for details. + **/ + ++ (NSString *) prepPassword:(NSString *)password; + +@end diff --git a/Utilities/XMPPStringPrep.m b/Utilities/XMPPStringPrep.m new file mode 100644 index 0000000..5831efd --- /dev/null +++ b/Utilities/XMPPStringPrep.m @@ -0,0 +1,67 @@ +#import "XMPPStringPrep.h" +#import "stringprep.h" + + +@implementation XMPPStringPrep + ++ (NSString *)prepNode:(NSString *)node +{ + if(node == nil) return nil; + + // Each allowable portion of a JID MUST NOT be more than 1023 bytes in length. + // We make the buffer just big enough to hold a null-terminated string of this length. + char buf[1024]; + + strncpy(buf, [node UTF8String], sizeof(buf)); + + if(stringprep_xmpp_nodeprep(buf, sizeof(buf)) != 0) return nil; + + return [NSString stringWithUTF8String:buf]; +} + ++ (NSString *)prepDomain:(NSString *)domain +{ + if(domain == nil) return nil; + + // Each allowable portion of a JID MUST NOT be more than 1023 bytes in length. + // We make the buffer just big enough to hold a null-terminated string of this length. + char buf[1024]; + + strncpy(buf, [domain UTF8String], sizeof(buf)); + + if(stringprep_nameprep(buf, sizeof(buf)) != 0) return nil; + + return [NSString stringWithUTF8String:buf]; +} + ++ (NSString *)prepResource:(NSString *)resource +{ + if(resource == nil) return nil; + + // Each allowable portion of a JID MUST NOT be more than 1023 bytes in length. + // We make the buffer just big enough to hold a null-terminated string of this length. + char buf[1024]; + + strncpy(buf, [resource UTF8String], sizeof(buf)); + + if(stringprep_xmpp_resourceprep(buf, sizeof(buf)) != 0) return nil; + + return [NSString stringWithUTF8String:buf]; +} + ++ (NSString *)prepPassword:(NSString *)password +{ + if(password == nil) return nil; + + // Each allowable portion of a JID MUST NOT be more than 1023 bytes in length. + // We make the buffer just big enough to hold a null-terminated string of this length. + char buf[1024]; + + strncpy(buf, [password UTF8String], sizeof(buf)); + + if(stringprep(buf, sizeof(buf), 0, stringprep_saslprep) != 0) return nil; + + return [NSString stringWithUTF8String:buf]; +} + +@end diff --git a/Utilities/XMPPTimer.h b/Utilities/XMPPTimer.h new file mode 100644 index 0000000..64f15c1 --- /dev/null +++ b/Utilities/XMPPTimer.h @@ -0,0 +1,41 @@ +#import + +/** + * This class is a simple wrapper around dispatch_source_t timers. + * + * The primary motivation for this is to allow timers to be stored in collections. + * But the class also makes it easier to code timers, as it simplifies the API. +**/ +@interface XMPPTimer : NSObject + +/** + * Creates an instance of a timer that will fire on the given queue. + * It will invoke the given event handler block when it fires. +**/ +- (instancetype)initWithQueue:(dispatch_queue_t)queue eventHandler:(dispatch_block_t)block; + +/** + * Starts the timer. + * It will first fire after the timeout. + * After that, it will continue to fire every interval. + * + * The interval is optional. + * If interval is zero (or negative), it will not use an interval (will only fire once after the timeout). + * + * This method can only be called once. +**/ +- (void)startWithTimeout:(NSTimeInterval)timeout interval:(NSTimeInterval)interval; + +/** + * Allows you to update an already started timer. + * + * The new timeout that you pass can be applied to 'now' or to the original start time of the timer. +**/ +- (void)updateTimeout:(NSTimeInterval)timeout fromOriginalStartTime:(BOOL)useOriginalStartTime; + +/** + * Cancels the timer so that it won't fire. +**/ +- (void)cancel; + +@end diff --git a/Utilities/XMPPTimer.m b/Utilities/XMPPTimer.m new file mode 100644 index 0000000..f3b6d0c --- /dev/null +++ b/Utilities/XMPPTimer.m @@ -0,0 +1,86 @@ +#import "XMPPTimer.h" +#import "XMPPLogging.h" + +// 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 + +@implementation XMPPTimer +{ + BOOL isStarted; + + dispatch_time_t start; + uint64_t timeout; + uint64_t interval; + + dispatch_source_t timer; +} + +- (instancetype)initWithQueue:(dispatch_queue_t)queue eventHandler:(dispatch_block_t)block +{ + if ((self = [super init])) + { + timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); + dispatch_source_set_event_handler(timer, block); + + isStarted = NO; + } + return self; +} + +- (void)dealloc +{ + [self cancel]; +} + +- (void)startWithTimeout:(NSTimeInterval)inTimeout interval:(NSTimeInterval)inInterval +{ + if (isStarted) + { + XMPPLogWarn(@"Unable to start timer - already started"); + return; + } + + start = dispatch_time(DISPATCH_TIME_NOW, 0); + timeout = (inTimeout * NSEC_PER_SEC); + interval = (inInterval > 0.0) ? (inInterval * NSEC_PER_SEC) : DISPATCH_TIME_FOREVER; + + dispatch_source_set_timer(timer, dispatch_time(start, timeout), interval, 0); + dispatch_resume(timer); + + isStarted = YES; +} + +- (void)updateTimeout:(NSTimeInterval)inTimeout fromOriginalStartTime:(BOOL)useOriginalStartTime +{ + if (!isStarted) + { + XMPPLogWarn(@"Unable to update timer - not yet started"); + return; + } + + if (!useOriginalStartTime) { + start = dispatch_time(DISPATCH_TIME_NOW, 0); + } + timeout = (inTimeout * NSEC_PER_SEC); + + dispatch_source_set_timer(timer, dispatch_time(start, timeout), interval, 0); +} + +- (void)cancel +{ + if (timer) + { + dispatch_source_cancel(timer); + #if !OS_OBJECT_USE_OBJC + dispatch_release(timer); + #endif + timer = NULL; + } +} + +@end diff --git a/Vendor/CocoaAsyncSocket/GCDAsyncSocket.h b/Vendor/CocoaAsyncSocket/GCDAsyncSocket.h new file mode 100644 index 0000000..374bcdd --- /dev/null +++ b/Vendor/CocoaAsyncSocket/GCDAsyncSocket.h @@ -0,0 +1,1179 @@ +// +// GCDAsyncSocket.h +// +// This class is in the public domain. +// Originally created by Robbie Hanson in Q3 2010. +// Updated and maintained by Deusty LLC and the Apple development community. +// +// https://github.com/robbiehanson/CocoaAsyncSocket +// + +#import +#import +#import +#import +#import + +#include // AF_INET, AF_INET6 + +@class GCDAsyncReadPacket; +@class GCDAsyncWritePacket; +@class GCDAsyncSocketPreBuffer; + +extern NSString *const GCDAsyncSocketException; +extern NSString *const GCDAsyncSocketErrorDomain; + +extern NSString *const GCDAsyncSocketQueueName; +extern NSString *const GCDAsyncSocketThreadName; + +extern NSString *const GCDAsyncSocketManuallyEvaluateTrust; +#if TARGET_OS_IPHONE +extern NSString *const GCDAsyncSocketUseCFStreamForTLS; +#endif +#define GCDAsyncSocketSSLPeerName (NSString *)kCFStreamSSLPeerName +#define GCDAsyncSocketSSLCertificates (NSString *)kCFStreamSSLCertificates +#define GCDAsyncSocketSSLIsServer (NSString *)kCFStreamSSLIsServer +extern NSString *const GCDAsyncSocketSSLPeerID; +extern NSString *const GCDAsyncSocketSSLProtocolVersionMin; +extern NSString *const GCDAsyncSocketSSLProtocolVersionMax; +extern NSString *const GCDAsyncSocketSSLSessionOptionFalseStart; +extern NSString *const GCDAsyncSocketSSLSessionOptionSendOneByteRecord; +extern NSString *const GCDAsyncSocketSSLCipherSuites; +#if !TARGET_OS_IPHONE +extern NSString *const GCDAsyncSocketSSLDiffieHellmanParameters; +#endif + +#define GCDAsyncSocketLoggingContext 65535 + + +enum GCDAsyncSocketError +{ + GCDAsyncSocketNoError = 0, // Never used + GCDAsyncSocketBadConfigError, // Invalid configuration + GCDAsyncSocketBadParamError, // Invalid parameter was passed + GCDAsyncSocketConnectTimeoutError, // A connect operation timed out + GCDAsyncSocketReadTimeoutError, // A read operation timed out + GCDAsyncSocketWriteTimeoutError, // A write operation timed out + GCDAsyncSocketReadMaxedOutError, // Reached set maxLength without completing + GCDAsyncSocketClosedError, // The remote peer closed the connection + GCDAsyncSocketOtherError, // Description provided in userInfo +}; +typedef enum GCDAsyncSocketError GCDAsyncSocketError; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface GCDAsyncSocket : NSObject + +/** + * GCDAsyncSocket uses the standard delegate paradigm, + * but executes all delegate callbacks on a given delegate dispatch queue. + * This allows for maximum concurrency, while at the same time providing easy thread safety. + * + * You MUST set a delegate AND delegate dispatch queue before attempting to + * use the socket, or you will get an error. + * + * The socket queue is optional. + * If you pass NULL, GCDAsyncSocket will automatically create it's own socket queue. + * If you choose to provide a socket queue, the socket queue must not be a concurrent queue. + * If you choose to provide a socket queue, and the socket queue has a configured target queue, + * then please see the discussion for the method markSocketQueueTargetQueue. + * + * The delegate queue and socket queue can optionally be the same. +**/ +- (id)init; +- (id)initWithSocketQueue:(dispatch_queue_t)sq; +- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq; +- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq; + +#pragma mark Configuration + +@property (atomic, weak, readwrite) id delegate; +#if OS_OBJECT_USE_OBJC +@property (atomic, strong, readwrite) dispatch_queue_t delegateQueue; +#else +@property (atomic, assign, readwrite) dispatch_queue_t delegateQueue; +#endif + +- (void)getDelegate:(id *)delegatePtr delegateQueue:(dispatch_queue_t *)delegateQueuePtr; +- (void)setDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; + +/** + * If you are setting the delegate to nil within the delegate's dealloc method, + * you may need to use the synchronous versions below. +**/ +- (void)synchronouslySetDelegate:(id)delegate; +- (void)synchronouslySetDelegateQueue:(dispatch_queue_t)delegateQueue; +- (void)synchronouslySetDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; + +/** + * By default, both IPv4 and IPv6 are enabled. + * + * For accepting incoming connections, this means GCDAsyncSocket automatically supports both protocols, + * and can simulataneously accept incoming connections on either protocol. + * + * For outgoing connections, this means GCDAsyncSocket can connect to remote hosts running either protocol. + * If a DNS lookup returns only IPv4 results, GCDAsyncSocket will automatically use IPv4. + * If a DNS lookup returns only IPv6 results, GCDAsyncSocket will automatically use IPv6. + * If a DNS lookup returns both IPv4 and IPv6 results, the preferred protocol will be chosen. + * By default, the preferred protocol is IPv4, but may be configured as desired. +**/ + +@property (atomic, assign, readwrite, getter=isIPv4Enabled) BOOL IPv4Enabled; +@property (atomic, assign, readwrite, getter=isIPv6Enabled) BOOL IPv6Enabled; + +@property (atomic, assign, readwrite, getter=isIPv4PreferredOverIPv6) BOOL IPv4PreferredOverIPv6; + +/** + * User data allows you to associate arbitrary information with the socket. + * This data is not used internally by socket in any way. +**/ +@property (atomic, strong, readwrite) id userData; + +#pragma mark Accepting + +/** + * Tells the socket to begin listening and accepting connections on the given port. + * When a connection is accepted, a new instance of GCDAsyncSocket will be spawned to handle it, + * and the socket:didAcceptNewSocket: delegate method will be invoked. + * + * The socket will listen on all available interfaces (e.g. wifi, ethernet, etc) +**/ +- (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr; + +/** + * This method is the same as acceptOnPort:error: with the + * additional option of specifying which interface to listen on. + * + * For example, you could specify that the socket should only accept connections over ethernet, + * and not other interfaces such as wifi. + * + * The interface may be specified by name (e.g. "en1" or "lo0") or by IP address (e.g. "192.168.4.34"). + * You may also use the special strings "localhost" or "loopback" to specify that + * the socket only accept connections from the local machine. + * + * You can see the list of interfaces via the command line utility "ifconfig", + * or programmatically via the getifaddrs() function. + * + * To accept connections on any interface pass nil, or simply use the acceptOnPort:error: method. +**/ +- (BOOL)acceptOnInterface:(NSString *)interface port:(uint16_t)port error:(NSError **)errPtr; + +#pragma mark Connecting + +/** + * Connects to the given host and port. + * + * This method invokes connectToHost:onPort:viaInterface:withTimeout:error: + * and uses the default interface, and no timeout. +**/ +- (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)errPtr; + +/** + * Connects to the given host and port with an optional timeout. + * + * This method invokes connectToHost:onPort:viaInterface:withTimeout:error: and uses the default interface. +**/ +- (BOOL)connectToHost:(NSString *)host + onPort:(uint16_t)port + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; + +/** + * Connects to the given host & port, via the optional interface, with an optional timeout. + * + * The host may be a domain name (e.g. "deusty.com") or an IP address string (e.g. "192.168.0.2"). + * The host may also be the special strings "localhost" or "loopback" to specify connecting + * to a service on the local machine. + * + * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). + * The interface may also be used to specify the local port (see below). + * + * To not time out use a negative time interval. + * + * This method will return NO if an error is detected, and set the error pointer (if one was given). + * Possible errors would be a nil host, invalid interface, or socket is already connected. + * + * If no errors are detected, this method will start a background connect operation and immediately return YES. + * The delegate callbacks are used to notify you when the socket connects, or if the host was unreachable. + * + * Since this class supports queued reads and writes, you can immediately start reading and/or writing. + * All read/write operations will be queued, and upon socket connection, + * the operations will be dequeued and processed in order. + * + * The interface may optionally contain a port number at the end of the string, separated by a colon. + * This allows you to specify the local port that should be used for the outgoing connection. (read paragraph to end) + * To specify both interface and local port: "en1:8082" or "192.168.4.35:2424". + * To specify only local port: ":8082". + * Please note this is an advanced feature, and is somewhat hidden on purpose. + * You should understand that 99.999% of the time you should NOT specify the local port for an outgoing connection. + * If you think you need to, there is a very good chance you have a fundamental misunderstanding somewhere. + * Local ports do NOT need to match remote ports. In fact, they almost never do. + * This feature is here for networking professionals using very advanced techniques. +**/ +- (BOOL)connectToHost:(NSString *)host + onPort:(uint16_t)port + viaInterface:(NSString *)interface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; + +/** + * Connects to the given address, specified as a sockaddr structure wrapped in a NSData object. + * For example, a NSData object returned from NSNetService's addresses method. + * + * If you have an existing struct sockaddr you can convert it to a NSData object like so: + * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; + * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; + * + * This method invokes connectToAdd +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr; + +/** + * This method is the same as connectToAddress:error: with an additional timeout option. + * To not time out use a negative time interval, or simply use the connectToAddress:error: method. +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; + +/** + * Connects to the given address, using the specified interface and timeout. + * + * The address is specified as a sockaddr structure wrapped in a NSData object. + * For example, a NSData object returned from NSNetService's addresses method. + * + * If you have an existing struct sockaddr you can convert it to a NSData object like so: + * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; + * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; + * + * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). + * The interface may also be used to specify the local port (see below). + * + * The timeout is optional. To not time out use a negative time interval. + * + * This method will return NO if an error is detected, and set the error pointer (if one was given). + * Possible errors would be a nil host, invalid interface, or socket is already connected. + * + * If no errors are detected, this method will start a background connect operation and immediately return YES. + * The delegate callbacks are used to notify you when the socket connects, or if the host was unreachable. + * + * Since this class supports queued reads and writes, you can immediately start reading and/or writing. + * All read/write operations will be queued, and upon socket connection, + * the operations will be dequeued and processed in order. + * + * The interface may optionally contain a port number at the end of the string, separated by a colon. + * This allows you to specify the local port that should be used for the outgoing connection. (read paragraph to end) + * To specify both interface and local port: "en1:8082" or "192.168.4.35:2424". + * To specify only local port: ":8082". + * Please note this is an advanced feature, and is somewhat hidden on purpose. + * You should understand that 99.999% of the time you should NOT specify the local port for an outgoing connection. + * If you think you need to, there is a very good chance you have a fundamental misunderstanding somewhere. + * Local ports do NOT need to match remote ports. In fact, they almost never do. + * This feature is here for networking professionals using very advanced techniques. +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr + viaInterface:(NSString *)interface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; + +#pragma mark Disconnecting + +/** + * Disconnects immediately (synchronously). Any pending reads or writes are dropped. + * + * If the socket is not already disconnected, an invocation to the socketDidDisconnect:withError: delegate method + * will be queued onto the delegateQueue asynchronously (behind any previously queued delegate methods). + * In other words, the disconnected delegate method will be invoked sometime shortly after this method returns. + * + * Please note the recommended way of releasing a GCDAsyncSocket instance (e.g. in a dealloc method) + * [asyncSocket setDelegate:nil]; + * [asyncSocket disconnect]; + * [asyncSocket release]; + * + * If you plan on disconnecting the socket, and then immediately asking it to connect again, + * you'll likely want to do so like this: + * [asyncSocket setDelegate:nil]; + * [asyncSocket disconnect]; + * [asyncSocket setDelegate:self]; + * [asyncSocket connect...]; +**/ +- (void)disconnect; + +/** + * Disconnects after all pending reads have completed. + * After calling this, the read and write methods will do nothing. + * The socket will disconnect even if there are still pending writes. +**/ +- (void)disconnectAfterReading; + +/** + * Disconnects after all pending writes have completed. + * After calling this, the read and write methods will do nothing. + * The socket will disconnect even if there are still pending reads. +**/ +- (void)disconnectAfterWriting; + +/** + * Disconnects after all pending reads and writes have completed. + * After calling this, the read and write methods will do nothing. +**/ +- (void)disconnectAfterReadingAndWriting; + +#pragma mark Diagnostics + +/** + * Returns whether the socket is disconnected or connected. + * + * A disconnected socket may be recycled. + * That is, it can used again for connecting or listening. + * + * If a socket is in the process of connecting, it may be neither disconnected nor connected. +**/ +@property (atomic, readonly) BOOL isDisconnected; +@property (atomic, readonly) BOOL isConnected; + +/** + * Returns the local or remote host and port to which this socket is connected, or nil and 0 if not connected. + * The host will be an IP address. +**/ +@property (atomic, readonly) NSString *connectedHost; +@property (atomic, readonly) uint16_t connectedPort; + +@property (atomic, readonly) NSString *localHost; +@property (atomic, readonly) uint16_t localPort; + +/** + * Returns the local or remote address to which this socket is connected, + * specified as a sockaddr structure wrapped in a NSData object. + * + * @seealso connectedHost + * @seealso connectedPort + * @seealso localHost + * @seealso localPort +**/ +@property (atomic, readonly) NSData *connectedAddress; +@property (atomic, readonly) NSData *localAddress; + +/** + * Returns whether the socket is IPv4 or IPv6. + * An accepting socket may be both. +**/ +@property (atomic, readonly) BOOL isIPv4; +@property (atomic, readonly) BOOL isIPv6; + +/** + * Returns whether or not the socket has been secured via SSL/TLS. + * + * See also the startTLS method. +**/ +@property (atomic, readonly) BOOL isSecure; + +#pragma mark Reading + +// The readData and writeData methods won't block (they are asynchronous). +// +// When a read is complete the socket:didReadData:withTag: delegate method is dispatched on the delegateQueue. +// When a write is complete the socket:didWriteDataWithTag: delegate method is dispatched on the delegateQueue. +// +// You may optionally set a timeout for any read/write operation. (To not timeout, use a negative time interval.) +// If a read/write opertion times out, the corresponding "socket:shouldTimeout..." delegate method +// is called to optionally allow you to extend the timeout. +// Upon a timeout, the "socket:didDisconnectWithError:" method is called +// +// The tag is for your convenience. +// You can use it as an array index, step number, state id, pointer, etc. + +/** + * Reads the first available bytes that become available on the socket. + * + * If the timeout value is negative, the read operation will not use a timeout. +**/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads the first available bytes that become available on the socket. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, the socket will create a buffer for you. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. +**/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads the first available bytes that become available on the socket. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * A maximum of length bytes will be read. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, a buffer will automatically be created for you. + * If maxLength is zero, no length restriction is enforced. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. +**/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag; + +/** + * Reads the given number of bytes. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If the length is 0, this method does nothing and the delegate is not called. +**/ +- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads the given number of bytes. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, a buffer will automatically be created for you. + * + * If the length is 0, this method does nothing and the delegate is not called. + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while AsyncSocket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. +**/ +- (void)readDataToLength:(NSUInteger)length + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If you pass nil or zero-length data as the "data" parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, a buffer will automatically be created for you. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If maxLength is zero, no length restriction is enforced. + * Otherwise if maxLength bytes are read without completing the read, + * it is treated similarly to a timeout - the socket is closed with a GCDAsyncSocketReadMaxedOutError. + * The read will complete successfully if exactly maxLength bytes are read and the given data is found at the end. + * + * If you pass nil or zero-length data as the "data" parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * If you pass a maxLength parameter that is less than the length of the data parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(NSUInteger)length tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, a buffer will automatically be created for you. + * + * If maxLength is zero, no length restriction is enforced. + * Otherwise if maxLength bytes are read without completing the read, + * it is treated similarly to a timeout - the socket is closed with a GCDAsyncSocketReadMaxedOutError. + * The read will complete successfully if exactly maxLength bytes are read and the given data is found at the end. + * + * If you pass a maxLength parameter that is less than the length of the data (separator) parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag; + +/** + * Returns progress of the current read, from 0.0 to 1.0, or NaN if no current read (use isnan() to check). + * The parameters "tag", "done" and "total" will be filled in if they aren't NULL. +**/ +- (float)progressOfReadReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr; + +#pragma mark Writing + +/** + * Writes data to the socket, and calls the delegate when finished. + * + * If you pass in nil or zero-length data, this method does nothing and the delegate will not be called. + * If the timeout value is negative, the write operation will not use a timeout. + * + * Thread-Safety Note: + * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while + * the socket is writing it. In other words, it's not safe to alter the data until after the delegate method + * socket:didWriteDataWithTag: is invoked signifying that this particular write operation has completed. + * This is due to the fact that GCDAsyncSocket does NOT copy the data. It simply retains it. + * This is for performance reasons. Often times, if NSMutableData is passed, it is because + * a request/response was built up in memory. Copying this data adds an unwanted/unneeded overhead. + * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket + * completes writing the bytes (which is NOT immediately after this method returns, but rather at a later time + * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method. +**/ +- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Returns progress of the current write, from 0.0 to 1.0, or NaN if no current write (use isnan() to check). + * The parameters "tag", "done" and "total" will be filled in if they aren't NULL. +**/ +- (float)progressOfWriteReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr; + +#pragma mark Security + +/** + * Secures the connection using SSL/TLS. + * + * This method may be called at any time, and the TLS handshake will occur after all pending reads and writes + * are finished. This allows one the option of sending a protocol dependent StartTLS message, and queuing + * the upgrade to TLS at the same time, without having to wait for the write to finish. + * Any reads or writes scheduled after this method is called will occur over the secured connection. + * + * ==== The available TOP-LEVEL KEYS are: + * + * - GCDAsyncSocketManuallyEvaluateTrust + * The value must be of type NSNumber, encapsulating a BOOL value. + * If you set this to YES, then the underlying SecureTransport system will not evaluate the SecTrustRef of the peer. + * Instead it will pause at the moment evaulation would typically occur, + * and allow us to handle the security evaluation however we see fit. + * So GCDAsyncSocket will invoke the delegate method socket:shouldTrustPeer: passing the SecTrustRef. + * + * Note that if you set this option, then all other configuration keys are ignored. + * Evaluation will be completely up to you during the socket:didReceiveTrust:completionHandler: delegate method. + * + * For more information on trust evaluation see: + * Apple's Technical Note TN2232 - HTTPS Server Trust Evaluation + * https://developer.apple.com/library/ios/technotes/tn2232/_index.html + * + * If unspecified, the default value is NO. + * + * - GCDAsyncSocketUseCFStreamForTLS (iOS only) + * The value must be of type NSNumber, encapsulating a BOOL value. + * By default GCDAsyncSocket will use the SecureTransport layer to perform encryption. + * This gives us more control over the security protocol (many more configuration options), + * plus it allows us to optimize things like sys calls and buffer allocation. + * + * However, if you absolutely must, you can instruct GCDAsyncSocket to use the old-fashioned encryption + * technique by going through the CFStream instead. So instead of using SecureTransport, GCDAsyncSocket + * will instead setup a CFRead/CFWriteStream. And then set the kCFStreamPropertySSLSettings property + * (via CFReadStreamSetProperty / CFWriteStreamSetProperty) and will pass the given options to this method. + * + * Thus all the other keys in the given dictionary will be ignored by GCDAsyncSocket, + * and will passed directly CFReadStreamSetProperty / CFWriteStreamSetProperty. + * For more infomation on these keys, please see the documentation for kCFStreamPropertySSLSettings. + * + * If unspecified, the default value is NO. + * + * ==== The available CONFIGURATION KEYS are: + * + * - kCFStreamSSLPeerName + * The value must be of type NSString. + * It should match the name in the X.509 certificate given by the remote party. + * See Apple's documentation for SSLSetPeerDomainName. + * + * - kCFStreamSSLCertificates + * The value must be of type NSArray. + * See Apple's documentation for SSLSetCertificate. + * + * - kCFStreamSSLIsServer + * The value must be of type NSNumber, encapsulationg a BOOL value. + * See Apple's documentation for SSLCreateContext for iOS. + * This is optional for iOS. If not supplied, a NO value is the default. + * This is not needed for Mac OS X, and the value is ignored. + * + * - GCDAsyncSocketSSLPeerID + * The value must be of type NSData. + * You must set this value if you want to use TLS session resumption. + * See Apple's documentation for SSLSetPeerID. + * + * - GCDAsyncSocketSSLProtocolVersionMin + * - GCDAsyncSocketSSLProtocolVersionMax + * The value(s) must be of type NSNumber, encapsulting a SSLProtocol value. + * See Apple's documentation for SSLSetProtocolVersionMin & SSLSetProtocolVersionMax. + * See also the SSLProtocol typedef. + * + * - GCDAsyncSocketSSLSessionOptionFalseStart + * The value must be of type NSNumber, encapsulating a BOOL value. + * See Apple's documentation for kSSLSessionOptionFalseStart. + * + * - GCDAsyncSocketSSLSessionOptionSendOneByteRecord + * The value must be of type NSNumber, encapsulating a BOOL value. + * See Apple's documentation for kSSLSessionOptionSendOneByteRecord. + * + * - GCDAsyncSocketSSLCipherSuites + * The values must be of type NSArray. + * Each item within the array must be a NSNumber, encapsulating + * See Apple's documentation for SSLSetEnabledCiphers. + * See also the SSLCipherSuite typedef. + * + * - GCDAsyncSocketSSLDiffieHellmanParameters (Mac OS X only) + * The value must be of type NSData. + * See Apple's documentation for SSLSetDiffieHellmanParams. + * + * ==== The following UNAVAILABLE KEYS are: (with throw an exception) + * + * - kCFStreamSSLAllowsAnyRoot (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetAllowsAnyRoot + * + * - kCFStreamSSLAllowsExpiredRoots (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetAllowsExpiredRoots + * + * - kCFStreamSSLAllowsExpiredCertificates (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetAllowsExpiredCerts + * + * - kCFStreamSSLValidatesCertificateChain (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetEnableCertVerify + * + * - kCFStreamSSLLevel (UNAVAILABLE) + * You MUST use GCDAsyncSocketSSLProtocolVersionMin & GCDAsyncSocketSSLProtocolVersionMin instead. + * Corresponding deprecated method: SSLSetProtocolVersionEnabled + * + * + * Please refer to Apple's documentation for corresponding SSLFunctions. + * + * If you pass in nil or an empty dictionary, the default settings will be used. + * + * IMPORTANT SECURITY NOTE: + * The default settings will check to make sure the remote party's certificate is signed by a + * trusted 3rd party certificate agency (e.g. verisign) and that the certificate is not expired. + * However it will not verify the name on the certificate unless you + * give it a name to verify against via the kCFStreamSSLPeerName key. + * The security implications of this are important to understand. + * Imagine you are attempting to create a secure connection to MySecureServer.com, + * but your socket gets directed to MaliciousServer.com because of a hacked DNS server. + * If you simply use the default settings, and MaliciousServer.com has a valid certificate, + * the default settings will not detect any problems since the certificate is valid. + * To properly secure your connection in this particular scenario you + * should set the kCFStreamSSLPeerName property to "MySecureServer.com". + * + * You can also perform additional validation in socketDidSecure. +**/ +- (void)startTLS:(NSDictionary *)tlsSettings; + +#pragma mark Advanced + +/** + * Traditionally sockets are not closed until the conversation is over. + * However, it is technically possible for the remote enpoint to close its write stream. + * Our socket would then be notified that there is no more data to be read, + * but our socket would still be writeable and the remote endpoint could continue to receive our data. + * + * The argument for this confusing functionality stems from the idea that a client could shut down its + * write stream after sending a request to the server, thus notifying the server there are to be no further requests. + * In practice, however, this technique did little to help server developers. + * + * To make matters worse, from a TCP perspective there is no way to tell the difference from a read stream close + * and a full socket close. They both result in the TCP stack receiving a FIN packet. The only way to tell + * is by continuing to write to the socket. If it was only a read stream close, then writes will continue to work. + * Otherwise an error will be occur shortly (when the remote end sends us a RST packet). + * + * In addition to the technical challenges and confusion, many high level socket/stream API's provide + * no support for dealing with the problem. If the read stream is closed, the API immediately declares the + * socket to be closed, and shuts down the write stream as well. In fact, this is what Apple's CFStream API does. + * It might sound like poor design at first, but in fact it simplifies development. + * + * The vast majority of the time if the read stream is closed it's because the remote endpoint closed its socket. + * Thus it actually makes sense to close the socket at this point. + * And in fact this is what most networking developers want and expect to happen. + * However, if you are writing a server that interacts with a plethora of clients, + * you might encounter a client that uses the discouraged technique of shutting down its write stream. + * If this is the case, you can set this property to NO, + * and make use of the socketDidCloseReadStream delegate method. + * + * The default value is YES. +**/ +@property (atomic, assign, readwrite) BOOL autoDisconnectOnClosedReadStream; + +/** + * GCDAsyncSocket maintains thread safety by using an internal serial dispatch_queue. + * In most cases, the instance creates this queue itself. + * However, to allow for maximum flexibility, the internal queue may be passed in the init method. + * This allows for some advanced options such as controlling socket priority via target queues. + * However, when one begins to use target queues like this, they open the door to some specific deadlock issues. + * + * For example, imagine there are 2 queues: + * dispatch_queue_t socketQueue; + * dispatch_queue_t socketTargetQueue; + * + * If you do this (pseudo-code): + * socketQueue.targetQueue = socketTargetQueue; + * + * Then all socketQueue operations will actually get run on the given socketTargetQueue. + * This is fine and works great in most situations. + * But if you run code directly from within the socketTargetQueue that accesses the socket, + * you could potentially get deadlock. Imagine the following code: + * + * - (BOOL)socketHasSomething + * { + * __block BOOL result = NO; + * dispatch_block_t block = ^{ + * result = [self someInternalMethodToBeRunOnlyOnSocketQueue]; + * } + * if (is_executing_on_queue(socketQueue)) + * block(); + * else + * dispatch_sync(socketQueue, block); + * + * return result; + * } + * + * What happens if you call this method from the socketTargetQueue? The result is deadlock. + * This is because the GCD API offers no mechanism to discover a queue's targetQueue. + * Thus we have no idea if our socketQueue is configured with a targetQueue. + * If we had this information, we could easily avoid deadlock. + * But, since these API's are missing or unfeasible, you'll have to explicitly set it. + * + * IF you pass a socketQueue via the init method, + * AND you've configured the passed socketQueue with a targetQueue, + * THEN you should pass the end queue in the target hierarchy. + * + * For example, consider the following queue hierarchy: + * socketQueue -> ipQueue -> moduleQueue + * + * This example demonstrates priority shaping within some server. + * All incoming client connections from the same IP address are executed on the same target queue. + * And all connections for a particular module are executed on the same target queue. + * Thus, the priority of all networking for the entire module can be changed on the fly. + * Additionally, networking traffic from a single IP cannot monopolize the module. + * + * Here's how you would accomplish something like that: + * - (dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock + * { + * dispatch_queue_t socketQueue = dispatch_queue_create("", NULL); + * dispatch_queue_t ipQueue = [self ipQueueForAddress:address]; + * + * dispatch_set_target_queue(socketQueue, ipQueue); + * dispatch_set_target_queue(iqQueue, moduleQueue); + * + * return socketQueue; + * } + * - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket + * { + * [clientConnections addObject:newSocket]; + * [newSocket markSocketQueueTargetQueue:moduleQueue]; + * } + * + * Note: This workaround is ONLY needed if you intend to execute code directly on the ipQueue or moduleQueue. + * This is often NOT the case, as such queues are used solely for execution shaping. +**/ +- (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreConfiguredTargetQueue; +- (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreviouslyConfiguredTargetQueue; + +/** + * It's not thread-safe to access certain variables from outside the socket's internal queue. + * + * For example, the socket file descriptor. + * File descriptors are simply integers which reference an index in the per-process file table. + * However, when one requests a new file descriptor (by opening a file or socket), + * the file descriptor returned is guaranteed to be the lowest numbered unused descriptor. + * So if we're not careful, the following could be possible: + * + * - Thread A invokes a method which returns the socket's file descriptor. + * - The socket is closed via the socket's internal queue on thread B. + * - Thread C opens a file, and subsequently receives the file descriptor that was previously the socket's FD. + * - Thread A is now accessing/altering the file instead of the socket. + * + * In addition to this, other variables are not actually objects, + * and thus cannot be retained/released or even autoreleased. + * An example is the sslContext, of type SSLContextRef, which is actually a malloc'd struct. + * + * Although there are internal variables that make it difficult to maintain thread-safety, + * it is important to provide access to these variables + * to ensure this class can be used in a wide array of environments. + * This method helps to accomplish this by invoking the current block on the socket's internal queue. + * The methods below can be invoked from within the block to access + * those generally thread-unsafe internal variables in a thread-safe manner. + * The given block will be invoked synchronously on the socket's internal queue. + * + * If you save references to any protected variables and use them outside the block, you do so at your own peril. +**/ +- (void)performBlock:(dispatch_block_t)block; + +/** + * These methods are only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's file descriptor(s). + * If the socket is a server socket (is accepting incoming connections), + * it might actually have multiple internal socket file descriptors - one for IPv4 and one for IPv6. +**/ +- (int)socketFD; +- (int)socket4FD; +- (int)socket6FD; + +#if TARGET_OS_IPHONE + +/** + * These methods are only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's internal CFReadStream/CFWriteStream. + * + * These streams are only used as workarounds for specific iOS shortcomings: + * + * - Apple has decided to keep the SecureTransport framework private is iOS. + * This means the only supplied way to do SSL/TLS is via CFStream or some other API layered on top of it. + * Thus, in order to provide SSL/TLS support on iOS we are forced to rely on CFStream, + * instead of the preferred and faster and more powerful SecureTransport. + * + * - If a socket doesn't have backgrounding enabled, and that socket is closed while the app is backgrounded, + * Apple only bothers to notify us via the CFStream API. + * The faster and more powerful GCD API isn't notified properly in this case. + * + * See also: (BOOL)enableBackgroundingOnSocket +**/ +- (CFReadStreamRef)readStream; +- (CFWriteStreamRef)writeStream; + +/** + * This method is only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Configures the socket to allow it to operate when the iOS application has been backgrounded. + * In other words, this method creates a read & write stream, and invokes: + * + * CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + * CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + * + * Returns YES if successful, NO otherwise. + * + * Note: Apple does not officially support backgrounding server sockets. + * That is, if your socket is accepting incoming connections, Apple does not officially support + * allowing iOS applications to accept incoming connections while an app is backgrounded. + * + * Example usage: + * + * - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port + * { + * [asyncSocket performBlock:^{ + * [asyncSocket enableBackgroundingOnSocket]; + * }]; + * } +**/ +- (BOOL)enableBackgroundingOnSocket; + +#endif + +/** + * This method is only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's SSLContext, if SSL/TLS has been started on the socket. +**/ +- (SSLContextRef)sslContext; + +#pragma mark Utilities + +/** + * The address lookup utility used by the class. + * This method is synchronous, so it's recommended you use it on a background thread/queue. + * + * The special strings "localhost" and "loopback" return the loopback address for IPv4 and IPv6. + * + * @returns + * A mutable array with all IPv4 and IPv6 addresses returned by getaddrinfo. + * The addresses are specifically for TCP connections. + * You can filter the addresses, if needed, using the other utility methods provided by the class. +**/ ++ (NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSError **)errPtr; + +/** + * Extracting host and port information from raw address data. +**/ + ++ (NSString *)hostFromAddress:(NSData *)address; ++ (uint16_t)portFromAddress:(NSData *)address; + ++ (BOOL)isIPv4Address:(NSData *)address; ++ (BOOL)isIPv6Address:(NSData *)address; + ++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr fromAddress:(NSData *)address; + ++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr family:(sa_family_t *)afPtr fromAddress:(NSData *)address; + +/** + * A few common line separators, for use with the readDataToData:... methods. +**/ ++ (NSData *)CRLFData; // 0x0D0A ++ (NSData *)CRData; // 0x0D ++ (NSData *)LFData; // 0x0A ++ (NSData *)ZeroData; // 0x00 + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol GCDAsyncSocketDelegate +@optional + +/** + * This method is called immediately prior to socket:didAcceptNewSocket:. + * It optionally allows a listening socket to specify the socketQueue for a new accepted socket. + * If this method is not implemented, or returns NULL, the new accepted socket will create its own default queue. + * + * Since you cannot autorelease a dispatch_queue, + * this method uses the "new" prefix in its name to specify that the returned queue has been retained. + * + * Thus you could do something like this in the implementation: + * return dispatch_queue_create("MyQueue", NULL); + * + * If you are placing multiple sockets on the same queue, + * then care should be taken to increment the retain count each time this method is invoked. + * + * For example, your implementation might look something like this: + * dispatch_retain(myExistingQueue); + * return myExistingQueue; +**/ +- (dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock; + +/** + * Called when a socket accepts a connection. + * Another socket is automatically spawned to handle it. + * + * You must retain the newSocket if you wish to handle the connection. + * Otherwise the newSocket instance will be released and the spawned connection will be closed. + * + * By default the new socket will have the same delegate and delegateQueue. + * You may, of course, change this at any time. +**/ +- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket; + +/** + * Called when a socket connects and is ready for reading and writing. + * The host parameter will be an IP address, not a DNS name. +**/ +- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port; + +/** + * Called when a socket has completed reading the requested data into memory. + * Not called if there is an error. +**/ +- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag; + +/** + * Called when a socket has read in data, but has not yet completed the read. + * This would occur if using readToData: or readToLength: methods. + * It may be used to for things such as updating progress bars. +**/ +- (void)socket:(GCDAsyncSocket *)sock didReadPartialDataOfLength:(NSUInteger)partialLength tag:(long)tag; + +/** + * Called when a socket has completed writing the requested data. Not called if there is an error. +**/ +- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag; + +/** + * Called when a socket has written some data, but has not yet completed the entire write. + * It may be used to for things such as updating progress bars. +**/ +- (void)socket:(GCDAsyncSocket *)sock didWritePartialDataOfLength:(NSUInteger)partialLength tag:(long)tag; + +/** + * Called if a read operation has reached its timeout without completing. + * This method allows you to optionally extend the timeout. + * If you return a positive time interval (> 0) the read's timeout will be extended by the given amount. + * If you don't implement this method, or return a non-positive time interval (<= 0) the read will timeout as usual. + * + * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method. + * The length parameter is the number of bytes that have been read so far for the read operation. + * + * Note that this method may be called multiple times for a single read if you return positive numbers. +**/ +- (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutReadWithTag:(long)tag + elapsed:(NSTimeInterval)elapsed + bytesDone:(NSUInteger)length; + +/** + * Called if a write operation has reached its timeout without completing. + * This method allows you to optionally extend the timeout. + * If you return a positive time interval (> 0) the write's timeout will be extended by the given amount. + * If you don't implement this method, or return a non-positive time interval (<= 0) the write will timeout as usual. + * + * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method. + * The length parameter is the number of bytes that have been written so far for the write operation. + * + * Note that this method may be called multiple times for a single write if you return positive numbers. +**/ +- (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutWriteWithTag:(long)tag + elapsed:(NSTimeInterval)elapsed + bytesDone:(NSUInteger)length; + +/** + * Conditionally called if the read stream closes, but the write stream may still be writeable. + * + * This delegate method is only called if autoDisconnectOnClosedReadStream has been set to NO. + * See the discussion on the autoDisconnectOnClosedReadStream method for more information. +**/ +- (void)socketDidCloseReadStream:(GCDAsyncSocket *)sock; + +/** + * Called when a socket disconnects with or without error. + * + * If you call the disconnect method, and the socket wasn't already disconnected, + * then an invocation of this delegate method will be enqueued on the delegateQueue + * before the disconnect method returns. + * + * Note: If the GCDAsyncSocket instance is deallocated while it is still connected, + * and the delegate is not also deallocated, then this method will be invoked, + * but the sock parameter will be nil. (It must necessarily be nil since it is no longer available.) + * This is a generally rare, but is possible if one writes code like this: + * + * asyncSocket = nil; // I'm implicitly disconnecting the socket + * + * In this case it may preferrable to nil the delegate beforehand, like this: + * + * asyncSocket.delegate = nil; // Don't invoke my delegate method + * asyncSocket = nil; // I'm implicitly disconnecting the socket + * + * Of course, this depends on how your state machine is configured. +**/ +- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err; + +/** + * Called after the socket has successfully completed SSL/TLS negotiation. + * This method is not called unless you use the provided startTLS method. + * + * If a SSL/TLS negotiation fails (invalid certificate, etc) then the socket will immediately close, + * and the socketDidDisconnect:withError: delegate method will be called with the specific SSL error code. +**/ +- (void)socketDidSecure:(GCDAsyncSocket *)sock; + +/** + * Allows a socket delegate to hook into the TLS handshake and manually validate the peer it's connecting to. + * + * This is only called if startTLS is invoked with options that include: + * - GCDAsyncSocketManuallyEvaluateTrust == YES + * + * Typically the delegate will use SecTrustEvaluate (and related functions) to properly validate the peer. + * + * Note from Apple's documentation: + * Because [SecTrustEvaluate] might look on the network for certificates in the certificate chain, + * [it] might block while attempting network access. You should never call it from your main thread; + * call it only from within a function running on a dispatch queue or on a separate thread. + * + * Thus this method uses a completionHandler block rather than a normal return value. + * The completionHandler block is thread-safe, and may be invoked from a background queue/thread. + * It is safe to invoke the completionHandler block even if the socket has been closed. +**/ +- (void)socket:(GCDAsyncSocket *)sock didReceiveTrust:(SecTrustRef)trust + completionHandler:(void (^)(BOOL shouldTrustPeer))completionHandler; + +@end diff --git a/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m b/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m new file mode 100644 index 0000000..531a29d --- /dev/null +++ b/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m @@ -0,0 +1,7719 @@ +// +// GCDAsyncSocket.m +// +// This class is in the public domain. +// Originally created by Robbie Hanson in Q4 2010. +// Updated and maintained by Deusty LLC and the Apple development community. +// +// https://github.com/robbiehanson/CocoaAsyncSocket +// + +#import "GCDAsyncSocket.h" + +#if TARGET_OS_IPHONE +#import +#endif + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +// For more information see: https://github.com/robbiehanson/CocoaAsyncSocket/wiki/ARC +#endif + + +#ifndef GCDAsyncSocketLoggingEnabled +#define GCDAsyncSocketLoggingEnabled 0 +#endif + +#if GCDAsyncSocketLoggingEnabled + +// Logging Enabled - See log level below + +// Logging uses the CocoaLumberjack framework (which is also GCD based). +// https://github.com/robbiehanson/CocoaLumberjack +// +// It allows us to do a lot of logging without significantly slowing down the code. +#import "DDLog.h" + +#define LogAsync YES +#define LogContext GCDAsyncSocketLoggingContext + +#define LogObjc(flg, frmt, ...) LOG_OBJC_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__) +#define LogC(flg, frmt, ...) LOG_C_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__) + +#define LogError(frmt, ...) LogObjc(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogWarn(frmt, ...) LogObjc(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogInfo(frmt, ...) LogObjc(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogVerbose(frmt, ...) LogObjc(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) + +#define LogCError(frmt, ...) LogC(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogCWarn(frmt, ...) LogC(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogCInfo(frmt, ...) LogC(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogCVerbose(frmt, ...) LogC(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) + +#define LogTrace() LogObjc(LOG_FLAG_VERBOSE, @"%@: %@", THIS_FILE, THIS_METHOD) +#define LogCTrace() LogC(LOG_FLAG_VERBOSE, @"%@: %s", THIS_FILE, __FUNCTION__) + +#ifndef GCDAsyncSocketLogLevel +#define GCDAsyncSocketLogLevel LOG_LEVEL_VERBOSE +#endif + +// Log levels : off, error, warn, info, verbose +static const int logLevel = GCDAsyncSocketLogLevel; + +#else + +// Logging Disabled + +#define LogError(frmt, ...) {} +#define LogWarn(frmt, ...) {} +#define LogInfo(frmt, ...) {} +#define LogVerbose(frmt, ...) {} + +#define LogCError(frmt, ...) {} +#define LogCWarn(frmt, ...) {} +#define LogCInfo(frmt, ...) {} +#define LogCVerbose(frmt, ...) {} + +#define LogTrace() {} +#define LogCTrace(frmt, ...) {} + +#endif + +/** + * Seeing a return statements within an inner block + * can sometimes be mistaken for a return point of the enclosing method. + * This makes inline blocks a bit easier to read. +**/ +#define return_from_block return + +/** + * A socket file descriptor is really just an integer. + * It represents the index of the socket within the kernel. + * This makes invalid file descriptor comparisons easier to read. +**/ +#define SOCKET_NULL -1 + + +NSString *const GCDAsyncSocketException = @"GCDAsyncSocketException"; +NSString *const GCDAsyncSocketErrorDomain = @"GCDAsyncSocketErrorDomain"; + +NSString *const GCDAsyncSocketQueueName = @"GCDAsyncSocket"; +NSString *const GCDAsyncSocketThreadName = @"GCDAsyncSocket-CFStream"; + +NSString *const GCDAsyncSocketManuallyEvaluateTrust = @"GCDAsyncSocketManuallyEvaluateTrust"; +#if TARGET_OS_IPHONE +NSString *const GCDAsyncSocketUseCFStreamForTLS = @"GCDAsyncSocketUseCFStreamForTLS"; +#endif +NSString *const GCDAsyncSocketSSLPeerID = @"GCDAsyncSocketSSLPeerID"; +NSString *const GCDAsyncSocketSSLProtocolVersionMin = @"GCDAsyncSocketSSLProtocolVersionMin"; +NSString *const GCDAsyncSocketSSLProtocolVersionMax = @"GCDAsyncSocketSSLProtocolVersionMax"; +NSString *const GCDAsyncSocketSSLSessionOptionFalseStart = @"GCDAsyncSocketSSLSessionOptionFalseStart"; +NSString *const GCDAsyncSocketSSLSessionOptionSendOneByteRecord = @"GCDAsyncSocketSSLSessionOptionSendOneByteRecord"; +NSString *const GCDAsyncSocketSSLCipherSuites = @"GCDAsyncSocketSSLCipherSuites"; +#if !TARGET_OS_IPHONE +NSString *const GCDAsyncSocketSSLDiffieHellmanParameters = @"GCDAsyncSocketSSLDiffieHellmanParameters"; +#endif + +enum GCDAsyncSocketFlags +{ + kSocketStarted = 1 << 0, // If set, socket has been started (accepting/connecting) + kConnected = 1 << 1, // If set, the socket is connected + kForbidReadsWrites = 1 << 2, // If set, no new reads or writes are allowed + kReadsPaused = 1 << 3, // If set, reads are paused due to possible timeout + kWritesPaused = 1 << 4, // If set, writes are paused due to possible timeout + kDisconnectAfterReads = 1 << 5, // If set, disconnect after no more reads are queued + kDisconnectAfterWrites = 1 << 6, // If set, disconnect after no more writes are queued + kSocketCanAcceptBytes = 1 << 7, // If set, we know socket can accept bytes. If unset, it's unknown. + kReadSourceSuspended = 1 << 8, // If set, the read source is suspended + kWriteSourceSuspended = 1 << 9, // If set, the write source is suspended + kQueuedTLS = 1 << 10, // If set, we've queued an upgrade to TLS + kStartingReadTLS = 1 << 11, // If set, we're waiting for TLS negotiation to complete + kStartingWriteTLS = 1 << 12, // If set, we're waiting for TLS negotiation to complete + kSocketSecure = 1 << 13, // If set, socket is using secure communication via SSL/TLS + kSocketHasReadEOF = 1 << 14, // If set, we have read EOF from socket + kReadStreamClosed = 1 << 15, // If set, we've read EOF plus prebuffer has been drained + kDealloc = 1 << 16, // If set, the socket is being deallocated +#if TARGET_OS_IPHONE + kAddedStreamsToRunLoop = 1 << 17, // If set, CFStreams have been added to listener thread + kUsingCFStreamForTLS = 1 << 18, // If set, we're forced to use CFStream instead of SecureTransport + kSecureSocketHasBytesAvailable = 1 << 19, // If set, CFReadStream has notified us of bytes available +#endif +}; + +enum GCDAsyncSocketConfig +{ + kIPv4Disabled = 1 << 0, // If set, IPv4 is disabled + kIPv6Disabled = 1 << 1, // If set, IPv6 is disabled + kPreferIPv6 = 1 << 2, // If set, IPv6 is preferred over IPv4 + kAllowHalfDuplexConnection = 1 << 3, // If set, the socket will stay open even if the read stream closes +}; + +#if TARGET_OS_IPHONE + static NSThread *cfstreamThread; // Used for CFStreams + + static uint64_t cfstreamThreadRetainCount; // setup & teardown + static dispatch_queue_t cfstreamThreadSetupQueue; // setup & teardown +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * A PreBuffer is used when there is more data available on the socket + * than is being requested by current read request. + * In this case we slurp up all data from the socket (to minimize sys calls), + * and store additional yet unread data in a "prebuffer". + * + * The prebuffer is entirely drained before we read from the socket again. + * In other words, a large chunk of data is written is written to the prebuffer. + * The prebuffer is then drained via a series of one or more reads (for subsequent read request(s)). + * + * A ring buffer was once used for this purpose. + * But a ring buffer takes up twice as much memory as needed (double the size for mirroring). + * In fact, it generally takes up more than twice the needed size as everything has to be rounded up to vm_page_size. + * And since the prebuffer is always completely drained after being written to, a full ring buffer isn't needed. + * + * The current design is very simple and straight-forward, while also keeping memory requirements lower. +**/ + +@interface GCDAsyncSocketPreBuffer : NSObject +{ + uint8_t *preBuffer; + size_t preBufferSize; + + uint8_t *readPointer; + uint8_t *writePointer; +} + +- (id)initWithCapacity:(size_t)numBytes; + +- (void)ensureCapacityForWrite:(size_t)numBytes; + +- (size_t)availableBytes; +- (uint8_t *)readBuffer; + +- (void)getReadBuffer:(uint8_t **)bufferPtr availableBytes:(size_t *)availableBytesPtr; + +- (size_t)availableSpace; +- (uint8_t *)writeBuffer; + +- (void)getWriteBuffer:(uint8_t **)bufferPtr availableSpace:(size_t *)availableSpacePtr; + +- (void)didRead:(size_t)bytesRead; +- (void)didWrite:(size_t)bytesWritten; + +- (void)reset; + +@end + +@implementation GCDAsyncSocketPreBuffer + +- (id)initWithCapacity:(size_t)numBytes +{ + if ((self = [super init])) + { + preBufferSize = numBytes; + preBuffer = malloc(preBufferSize); + + readPointer = preBuffer; + writePointer = preBuffer; + } + return self; +} + +- (void)dealloc +{ + if (preBuffer) + free(preBuffer); +} + +- (void)ensureCapacityForWrite:(size_t)numBytes +{ + size_t availableSpace = [self availableSpace]; + + if (numBytes > availableSpace) + { + size_t additionalBytes = numBytes - availableSpace; + + size_t newPreBufferSize = preBufferSize + additionalBytes; + uint8_t *newPreBuffer = realloc(preBuffer, newPreBufferSize); + + size_t readPointerOffset = readPointer - preBuffer; + size_t writePointerOffset = writePointer - preBuffer; + + preBuffer = newPreBuffer; + preBufferSize = newPreBufferSize; + + readPointer = preBuffer + readPointerOffset; + writePointer = preBuffer + writePointerOffset; + } +} + +- (size_t)availableBytes +{ + return writePointer - readPointer; +} + +- (uint8_t *)readBuffer +{ + return readPointer; +} + +- (void)getReadBuffer:(uint8_t **)bufferPtr availableBytes:(size_t *)availableBytesPtr +{ + if (bufferPtr) *bufferPtr = readPointer; + if (availableBytesPtr) *availableBytesPtr = [self availableBytes]; +} + +- (void)didRead:(size_t)bytesRead +{ + readPointer += bytesRead; + + if (readPointer == writePointer) + { + // The prebuffer has been drained. Reset pointers. + readPointer = preBuffer; + writePointer = preBuffer; + } +} + +- (size_t)availableSpace +{ + return preBufferSize - (writePointer - preBuffer); +} + +- (uint8_t *)writeBuffer +{ + return writePointer; +} + +- (void)getWriteBuffer:(uint8_t **)bufferPtr availableSpace:(size_t *)availableSpacePtr +{ + if (bufferPtr) *bufferPtr = writePointer; + if (availableSpacePtr) *availableSpacePtr = [self availableSpace]; +} + +- (void)didWrite:(size_t)bytesWritten +{ + writePointer += bytesWritten; +} + +- (void)reset +{ + readPointer = preBuffer; + writePointer = preBuffer; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The GCDAsyncReadPacket encompasses the instructions for any given read. + * The content of a read packet allows the code to determine if we're: + * - reading to a certain length + * - reading to a certain separator + * - or simply reading the first chunk of available data +**/ +@interface GCDAsyncReadPacket : NSObject +{ + @public + NSMutableData *buffer; + NSUInteger startOffset; + NSUInteger bytesDone; + NSUInteger maxLength; + NSTimeInterval timeout; + NSUInteger readLength; + NSData *term; + BOOL bufferOwner; + NSUInteger originalBufferLength; + long tag; +} +- (id)initWithData:(NSMutableData *)d + startOffset:(NSUInteger)s + maxLength:(NSUInteger)m + timeout:(NSTimeInterval)t + readLength:(NSUInteger)l + terminator:(NSData *)e + tag:(long)i; + +- (void)ensureCapacityForAdditionalDataOfLength:(NSUInteger)bytesToRead; + +- (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr; + +- (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable; +- (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuffer:(BOOL *)shouldPreBufferPtr; +- (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffer found:(BOOL *)foundPtr; + +- (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes; + +@end + +@implementation GCDAsyncReadPacket + +- (id)initWithData:(NSMutableData *)d + startOffset:(NSUInteger)s + maxLength:(NSUInteger)m + timeout:(NSTimeInterval)t + readLength:(NSUInteger)l + terminator:(NSData *)e + tag:(long)i +{ + if((self = [super init])) + { + bytesDone = 0; + maxLength = m; + timeout = t; + readLength = l; + term = [e copy]; + tag = i; + + if (d) + { + buffer = d; + startOffset = s; + bufferOwner = NO; + originalBufferLength = [d length]; + } + else + { + if (readLength > 0) + buffer = [[NSMutableData alloc] initWithLength:readLength]; + else + buffer = [[NSMutableData alloc] initWithLength:0]; + + startOffset = 0; + bufferOwner = YES; + originalBufferLength = 0; + } + } + return self; +} + +/** + * Increases the length of the buffer (if needed) to ensure a read of the given size will fit. +**/ +- (void)ensureCapacityForAdditionalDataOfLength:(NSUInteger)bytesToRead +{ + NSUInteger buffSize = [buffer length]; + NSUInteger buffUsed = startOffset + bytesDone; + + NSUInteger buffSpace = buffSize - buffUsed; + + if (bytesToRead > buffSpace) + { + NSUInteger buffInc = bytesToRead - buffSpace; + + [buffer increaseLengthBy:buffInc]; + } +} + +/** + * This method is used when we do NOT know how much data is available to be read from the socket. + * This method returns the default value unless it exceeds the specified readLength or maxLength. + * + * Furthermore, the shouldPreBuffer decision is based upon the packet type, + * and whether the returned value would fit in the current buffer without requiring a resize of the buffer. +**/ +- (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr +{ + NSUInteger result; + + if (readLength > 0) + { + // Read a specific length of data + + result = MIN(defaultValue, (readLength - bytesDone)); + + // There is no need to prebuffer since we know exactly how much data we need to read. + // Even if the buffer isn't currently big enough to fit this amount of data, + // it would have to be resized eventually anyway. + + if (shouldPreBufferPtr) + *shouldPreBufferPtr = NO; + } + else + { + // Either reading until we find a specified terminator, + // or we're simply reading all available data. + // + // In other words, one of: + // + // - readDataToData packet + // - readDataWithTimeout packet + + if (maxLength > 0) + result = MIN(defaultValue, (maxLength - bytesDone)); + else + result = defaultValue; + + // Since we don't know the size of the read in advance, + // the shouldPreBuffer decision is based upon whether the returned value would fit + // in the current buffer without requiring a resize of the buffer. + // + // This is because, in all likelyhood, the amount read from the socket will be less than the default value. + // Thus we should avoid over-allocating the read buffer when we can simply use the pre-buffer instead. + + if (shouldPreBufferPtr) + { + NSUInteger buffSize = [buffer length]; + NSUInteger buffUsed = startOffset + bytesDone; + + NSUInteger buffSpace = buffSize - buffUsed; + + if (buffSpace >= result) + *shouldPreBufferPtr = NO; + else + *shouldPreBufferPtr = YES; + } + } + + return result; +} + +/** + * For read packets without a set terminator, returns the amount of data + * that can be read without exceeding the readLength or maxLength. + * + * The given parameter indicates the number of bytes estimated to be available on the socket, + * which is taken into consideration during the calculation. + * + * The given hint MUST be greater than zero. +**/ +- (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable +{ + NSAssert(term == nil, @"This method does not apply to term reads"); + NSAssert(bytesAvailable > 0, @"Invalid parameter: bytesAvailable"); + + if (readLength > 0) + { + // Read a specific length of data + + return MIN(bytesAvailable, (readLength - bytesDone)); + + // No need to avoid resizing the buffer. + // If the user provided their own buffer, + // and told us to read a certain length of data that exceeds the size of the buffer, + // then it is clear that our code will resize the buffer during the read operation. + // + // This method does not actually do any resizing. + // The resizing will happen elsewhere if needed. + } + else + { + // Read all available data + + NSUInteger result = bytesAvailable; + + if (maxLength > 0) + { + result = MIN(result, (maxLength - bytesDone)); + } + + // No need to avoid resizing the buffer. + // If the user provided their own buffer, + // and told us to read all available data without giving us a maxLength, + // then it is clear that our code might resize the buffer during the read operation. + // + // This method does not actually do any resizing. + // The resizing will happen elsewhere if needed. + + return result; + } +} + +/** + * For read packets with a set terminator, returns the amount of data + * that can be read without exceeding the maxLength. + * + * The given parameter indicates the number of bytes estimated to be available on the socket, + * which is taken into consideration during the calculation. + * + * To optimize memory allocations, mem copies, and mem moves + * the shouldPreBuffer boolean value will indicate if the data should be read into a prebuffer first, + * or if the data can be read directly into the read packet's buffer. +**/ +- (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuffer:(BOOL *)shouldPreBufferPtr +{ + NSAssert(term != nil, @"This method does not apply to non-term reads"); + NSAssert(bytesAvailable > 0, @"Invalid parameter: bytesAvailable"); + + + NSUInteger result = bytesAvailable; + + if (maxLength > 0) + { + result = MIN(result, (maxLength - bytesDone)); + } + + // Should the data be read into the read packet's buffer, or into a pre-buffer first? + // + // One would imagine the preferred option is the faster one. + // So which one is faster? + // + // Reading directly into the packet's buffer requires: + // 1. Possibly resizing packet buffer (malloc/realloc) + // 2. Filling buffer (read) + // 3. Searching for term (memcmp) + // 4. Possibly copying overflow into prebuffer (malloc/realloc, memcpy) + // + // Reading into prebuffer first: + // 1. Possibly resizing prebuffer (malloc/realloc) + // 2. Filling buffer (read) + // 3. Searching for term (memcmp) + // 4. Copying underflow into packet buffer (malloc/realloc, memcpy) + // 5. Removing underflow from prebuffer (memmove) + // + // Comparing the performance of the two we can see that reading + // data into the prebuffer first is slower due to the extra memove. + // + // However: + // The implementation of NSMutableData is open source via core foundation's CFMutableData. + // Decreasing the length of a mutable data object doesn't cause a realloc. + // In other words, the capacity of a mutable data object can grow, but doesn't shrink. + // + // This means the prebuffer will rarely need a realloc. + // The packet buffer, on the other hand, may often need a realloc. + // This is especially true if we are the buffer owner. + // Furthermore, if we are constantly realloc'ing the packet buffer, + // and then moving the overflow into the prebuffer, + // then we're consistently over-allocating memory for each term read. + // And now we get into a bit of a tradeoff between speed and memory utilization. + // + // The end result is that the two perform very similarly. + // And we can answer the original question very simply by another means. + // + // If we can read all the data directly into the packet's buffer without resizing it first, + // then we do so. Otherwise we use the prebuffer. + + if (shouldPreBufferPtr) + { + NSUInteger buffSize = [buffer length]; + NSUInteger buffUsed = startOffset + bytesDone; + + if ((buffSize - buffUsed) >= result) + *shouldPreBufferPtr = NO; + else + *shouldPreBufferPtr = YES; + } + + return result; +} + +/** + * For read packets with a set terminator, + * returns the amount of data that can be read from the given preBuffer, + * without going over a terminator or the maxLength. + * + * It is assumed the terminator has not already been read. +**/ +- (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffer found:(BOOL *)foundPtr +{ + NSAssert(term != nil, @"This method does not apply to non-term reads"); + NSAssert([preBuffer availableBytes] > 0, @"Invoked with empty pre buffer!"); + + // We know that the terminator, as a whole, doesn't exist in our own buffer. + // But it is possible that a _portion_ of it exists in our buffer. + // So we're going to look for the terminator starting with a portion of our own buffer. + // + // Example: + // + // term length = 3 bytes + // bytesDone = 5 bytes + // preBuffer length = 5 bytes + // + // If we append the preBuffer to our buffer, + // it would look like this: + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // --------------------- + // + // So we start our search here: + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // -------^-^-^--------- + // + // And move forwards... + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // ---------^-^-^------- + // + // Until we find the terminator or reach the end. + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // ---------------^-^-^- + + BOOL found = NO; + + NSUInteger termLength = [term length]; + NSUInteger preBufferLength = [preBuffer availableBytes]; + + if ((bytesDone + preBufferLength) < termLength) + { + // Not enough data for a full term sequence yet + return preBufferLength; + } + + NSUInteger maxPreBufferLength; + if (maxLength > 0) { + maxPreBufferLength = MIN(preBufferLength, (maxLength - bytesDone)); + + // Note: maxLength >= termLength + } + else { + maxPreBufferLength = preBufferLength; + } + + uint8_t seq[termLength]; + const void *termBuf = [term bytes]; + + NSUInteger bufLen = MIN(bytesDone, (termLength - 1)); + uint8_t *buf = (uint8_t *)[buffer mutableBytes] + startOffset + bytesDone - bufLen; + + NSUInteger preLen = termLength - bufLen; + const uint8_t *pre = [preBuffer readBuffer]; + + NSUInteger loopCount = bufLen + maxPreBufferLength - termLength + 1; // Plus one. See example above. + + NSUInteger result = maxPreBufferLength; + + NSUInteger i; + for (i = 0; i < loopCount; i++) + { + if (bufLen > 0) + { + // Combining bytes from buffer and preBuffer + + memcpy(seq, buf, bufLen); + memcpy(seq + bufLen, pre, preLen); + + if (memcmp(seq, termBuf, termLength) == 0) + { + result = preLen; + found = YES; + break; + } + + buf++; + bufLen--; + preLen++; + } + else + { + // Comparing directly from preBuffer + + if (memcmp(pre, termBuf, termLength) == 0) + { + NSUInteger preOffset = pre - [preBuffer readBuffer]; // pointer arithmetic + + result = preOffset + termLength; + found = YES; + break; + } + + pre++; + } + } + + // There is no need to avoid resizing the buffer in this particular situation. + + if (foundPtr) *foundPtr = found; + return result; +} + +/** + * For read packets with a set terminator, scans the packet buffer for the term. + * It is assumed the terminator had not been fully read prior to the new bytes. + * + * If the term is found, the number of excess bytes after the term are returned. + * If the term is not found, this method will return -1. + * + * Note: A return value of zero means the term was found at the very end. + * + * Prerequisites: + * The given number of bytes have been added to the end of our buffer. + * Our bytesDone variable has NOT been changed due to the prebuffered bytes. +**/ +- (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes +{ + NSAssert(term != nil, @"This method does not apply to non-term reads"); + + // The implementation of this method is very similar to the above method. + // See the above method for a discussion of the algorithm used here. + + uint8_t *buff = [buffer mutableBytes]; + NSUInteger buffLength = bytesDone + numBytes; + + const void *termBuff = [term bytes]; + NSUInteger termLength = [term length]; + + // Note: We are dealing with unsigned integers, + // so make sure the math doesn't go below zero. + + NSUInteger i = ((buffLength - numBytes) >= termLength) ? (buffLength - numBytes - termLength + 1) : 0; + + while (i + termLength <= buffLength) + { + uint8_t *subBuffer = buff + startOffset + i; + + if (memcmp(subBuffer, termBuff, termLength) == 0) + { + return buffLength - (i + termLength); + } + + i++; + } + + return -1; +} + + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The GCDAsyncWritePacket encompasses the instructions for any given write. +**/ +@interface GCDAsyncWritePacket : NSObject +{ + @public + NSData *buffer; + NSUInteger bytesDone; + long tag; + NSTimeInterval timeout; +} +- (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i; +@end + +@implementation GCDAsyncWritePacket + +- (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i +{ + if((self = [super init])) + { + buffer = d; // Retain not copy. For performance as documented in header file. + bytesDone = 0; + timeout = t; + tag = i; + } + return self; +} + + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The GCDAsyncSpecialPacket encompasses special instructions for interruptions in the read/write queues. + * This class my be altered to support more than just TLS in the future. +**/ +@interface GCDAsyncSpecialPacket : NSObject +{ + @public + NSDictionary *tlsSettings; +} +- (id)initWithTLSSettings:(NSDictionary *)settings; +@end + +@implementation GCDAsyncSpecialPacket + +- (id)initWithTLSSettings:(NSDictionary *)settings +{ + if((self = [super init])) + { + tlsSettings = [settings copy]; + } + return self; +} + + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation GCDAsyncSocket +{ + uint32_t flags; + uint16_t config; + + __weak id delegate; + dispatch_queue_t delegateQueue; + + int socket4FD; + int socket6FD; + int stateIndex; + NSData * connectInterface4; + NSData * connectInterface6; + + dispatch_queue_t socketQueue; + + dispatch_source_t accept4Source; + dispatch_source_t accept6Source; + dispatch_source_t connectTimer; + dispatch_source_t readSource; + dispatch_source_t writeSource; + dispatch_source_t readTimer; + dispatch_source_t writeTimer; + + NSMutableArray *readQueue; + NSMutableArray *writeQueue; + + GCDAsyncReadPacket *currentRead; + GCDAsyncWritePacket *currentWrite; + + unsigned long socketFDBytesAvailable; + + GCDAsyncSocketPreBuffer *preBuffer; + +#if TARGET_OS_IPHONE + CFStreamClientContext streamContext; + CFReadStreamRef readStream; + CFWriteStreamRef writeStream; +#endif + SSLContextRef sslContext; + GCDAsyncSocketPreBuffer *sslPreBuffer; + size_t sslWriteCachedLength; + OSStatus sslErrCode; + + void *IsOnSocketQueueOrTargetQueueKey; + + id userData; +} + +- (id)init +{ + return [self initWithDelegate:nil delegateQueue:NULL socketQueue:NULL]; +} + +- (id)initWithSocketQueue:(dispatch_queue_t)sq +{ + return [self initWithDelegate:nil delegateQueue:NULL socketQueue:sq]; +} + +- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq +{ + return [self initWithDelegate:aDelegate delegateQueue:dq socketQueue:NULL]; +} + +- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq +{ + if((self = [super init])) + { + delegate = aDelegate; + delegateQueue = dq; + + #if !OS_OBJECT_USE_OBJC + if (dq) dispatch_retain(dq); + #endif + + socket4FD = SOCKET_NULL; + socket6FD = SOCKET_NULL; + stateIndex = 0; + + if (sq) + { + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + + socketQueue = sq; + #if !OS_OBJECT_USE_OBJC + dispatch_retain(sq); + #endif + } + else + { + socketQueue = dispatch_queue_create([GCDAsyncSocketQueueName UTF8String], NULL); + } + + // The dispatch_queue_set_specific() and dispatch_get_specific() functions take a "void *key" parameter. + // From the documentation: + // + // > Keys are only compared as pointers and are never dereferenced. + // > Thus, you can use a pointer to a static variable for a specific subsystem or + // > any other value that allows you to identify the value uniquely. + // + // We're just going to use the memory address of an ivar. + // Specifically an ivar that is explicitly named for our purpose to make the code more readable. + // + // However, it feels tedious (and less readable) to include the "&" all the time: + // dispatch_get_specific(&IsOnSocketQueueOrTargetQueueKey) + // + // So we're going to make it so it doesn't matter if we use the '&' or not, + // by assigning the value of the ivar to the address of the ivar. + // Thus: IsOnSocketQueueOrTargetQueueKey == &IsOnSocketQueueOrTargetQueueKey; + + IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey; + + void *nonNullUnusedPointer = (__bridge void *)self; + dispatch_queue_set_specific(socketQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); + + readQueue = [[NSMutableArray alloc] initWithCapacity:5]; + currentRead = nil; + + writeQueue = [[NSMutableArray alloc] initWithCapacity:5]; + currentWrite = nil; + + preBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)]; + } + return self; +} + +- (void)dealloc +{ + LogInfo(@"%@ - %@ (start)", THIS_METHOD, self); + + // Set dealloc flag. + // This is used by closeWithError to ensure we don't accidentally retain ourself. + flags |= kDealloc; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + [self closeWithError:nil]; + } + else + { + dispatch_sync(socketQueue, ^{ + [self closeWithError:nil]; + }); + } + + delegate = nil; + + #if !OS_OBJECT_USE_OBJC + if (delegateQueue) dispatch_release(delegateQueue); + #endif + delegateQueue = NULL; + + #if !OS_OBJECT_USE_OBJC + if (socketQueue) dispatch_release(socketQueue); + #endif + socketQueue = NULL; + + LogInfo(@"%@ - %@ (finish)", THIS_METHOD, self); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (id)delegate +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return delegate; + } + else + { + __block id result; + + dispatch_sync(socketQueue, ^{ + result = delegate; + }); + + return result; + } +} + +- (void)setDelegate:(id)newDelegate synchronously:(BOOL)synchronously +{ + dispatch_block_t block = ^{ + delegate = newDelegate; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } +} + +- (void)setDelegate:(id)newDelegate +{ + [self setDelegate:newDelegate synchronously:NO]; +} + +- (void)synchronouslySetDelegate:(id)newDelegate +{ + [self setDelegate:newDelegate synchronously:YES]; +} + +- (dispatch_queue_t)delegateQueue +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return delegateQueue; + } + else + { + __block dispatch_queue_t result; + + dispatch_sync(socketQueue, ^{ + result = delegateQueue; + }); + + return result; + } +} + +- (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously +{ + dispatch_block_t block = ^{ + + #if !OS_OBJECT_USE_OBJC + if (delegateQueue) dispatch_release(delegateQueue); + if (newDelegateQueue) dispatch_retain(newDelegateQueue); + #endif + + delegateQueue = newDelegateQueue; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } +} + +- (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegateQueue:newDelegateQueue synchronously:NO]; +} + +- (void)synchronouslySetDelegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegateQueue:newDelegateQueue synchronously:YES]; +} + +- (void)getDelegate:(id *)delegatePtr delegateQueue:(dispatch_queue_t *)delegateQueuePtr +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (delegatePtr) *delegatePtr = delegate; + if (delegateQueuePtr) *delegateQueuePtr = delegateQueue; + } + else + { + __block id dPtr = NULL; + __block dispatch_queue_t dqPtr = NULL; + + dispatch_sync(socketQueue, ^{ + dPtr = delegate; + dqPtr = delegateQueue; + }); + + if (delegatePtr) *delegatePtr = dPtr; + if (delegateQueuePtr) *delegateQueuePtr = dqPtr; + } +} + +- (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously +{ + dispatch_block_t block = ^{ + + delegate = newDelegate; + + #if !OS_OBJECT_USE_OBJC + if (delegateQueue) dispatch_release(delegateQueue); + if (newDelegateQueue) dispatch_retain(newDelegateQueue); + #endif + + delegateQueue = newDelegateQueue; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } +} + +- (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:NO]; +} + +- (void)synchronouslySetDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:YES]; +} + +- (BOOL)isIPv4Enabled +{ + // Note: YES means kIPv4Disabled is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kIPv4Disabled) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((config & kIPv4Disabled) == 0); + }); + + return result; + } +} + +- (void)setIPv4Enabled:(BOOL)flag +{ + // Note: YES means kIPv4Disabled is OFF + + dispatch_block_t block = ^{ + + if (flag) + config &= ~kIPv4Disabled; + else + config |= kIPv4Disabled; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (BOOL)isIPv6Enabled +{ + // Note: YES means kIPv6Disabled is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kIPv6Disabled) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((config & kIPv6Disabled) == 0); + }); + + return result; + } +} + +- (void)setIPv6Enabled:(BOOL)flag +{ + // Note: YES means kIPv6Disabled is OFF + + dispatch_block_t block = ^{ + + if (flag) + config &= ~kIPv6Disabled; + else + config |= kIPv6Disabled; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (BOOL)isIPv4PreferredOverIPv6 +{ + // Note: YES means kPreferIPv6 is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kPreferIPv6) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((config & kPreferIPv6) == 0); + }); + + return result; + } +} + +- (void)setIPv4PreferredOverIPv6:(BOOL)flag +{ + // Note: YES means kPreferIPv6 is OFF + + dispatch_block_t block = ^{ + + if (flag) + config &= ~kPreferIPv6; + else + config |= kPreferIPv6; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (id)userData +{ + __block id result = nil; + + dispatch_block_t block = ^{ + + result = userData; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (void)setUserData:(id)arbitraryUserData +{ + dispatch_block_t block = ^{ + + if (userData != arbitraryUserData) + { + userData = arbitraryUserData; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Accepting +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr +{ + return [self acceptOnInterface:nil port:port error:errPtr]; +} + +- (BOOL)acceptOnInterface:(NSString *)inInterface port:(uint16_t)port error:(NSError **)errPtr +{ + LogTrace(); + + // Just in-case interface parameter is immutable. + NSString *interface = [inInterface copy]; + + __block BOOL result = NO; + __block NSError *err = nil; + + // CreateSocket Block + // This block will be invoked within the dispatch block below. + + int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) { + + int socketFD = socket(domain, SOCK_STREAM, 0); + + if (socketFD == SOCKET_NULL) + { + NSString *reason = @"Error in socket() function"; + err = [self errnoErrorWithReason:reason]; + + return SOCKET_NULL; + } + + int status; + + // Set socket options + + status = fcntl(socketFD, F_SETFL, O_NONBLOCK); + if (status == -1) + { + NSString *reason = @"Error enabling non-blocking IO on socket (fcntl)"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + int reuseOn = 1; + status = setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + if (status == -1) + { + NSString *reason = @"Error enabling address reuse (setsockopt)"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + // Bind socket + + status = bind(socketFD, (const struct sockaddr *)[interfaceAddr bytes], (socklen_t)[interfaceAddr length]); + if (status == -1) + { + NSString *reason = @"Error in bind() function"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + // Listen + + status = listen(socketFD, 1024); + if (status == -1) + { + NSString *reason = @"Error in listen() function"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + return socketFD; + }; + + // Create dispatch block and run on socketQueue + + dispatch_block_t block = ^{ @autoreleasepool { + + if (delegate == nil) // Must have delegate set + { + NSString *msg = @"Attempting to accept without a delegate. Set a delegate first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + if (delegateQueue == NULL) // Must have delegate queue set + { + NSString *msg = @"Attempting to accept without a delegate queue. Set a delegate queue first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled + { + NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + if (![self isDisconnected]) // Must be disconnected + { + NSString *msg = @"Attempting to accept while connected or accepting connections. Disconnect first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + // Clear queues (spurious read/write requests post disconnect) + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + // Resolve interface from description + + NSMutableData *interface4 = nil; + NSMutableData *interface6 = nil; + + [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:port]; + + if ((interface4 == nil) && (interface6 == nil)) + { + NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; + err = [self badParamError:msg]; + + return_from_block; + } + + if (isIPv4Disabled && (interface6 == nil)) + { + NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; + err = [self badParamError:msg]; + + return_from_block; + } + + if (isIPv6Disabled && (interface4 == nil)) + { + NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; + err = [self badParamError:msg]; + + return_from_block; + } + + BOOL enableIPv4 = !isIPv4Disabled && (interface4 != nil); + BOOL enableIPv6 = !isIPv6Disabled && (interface6 != nil); + + // Create sockets, configure, bind, and listen + + if (enableIPv4) + { + LogVerbose(@"Creating IPv4 socket"); + socket4FD = createSocket(AF_INET, interface4); + + if (socket4FD == SOCKET_NULL) + { + return_from_block; + } + } + + if (enableIPv6) + { + LogVerbose(@"Creating IPv6 socket"); + + if (enableIPv4 && (port == 0)) + { + // No specific port was specified, so we allowed the OS to pick an available port for us. + // Now we need to make sure the IPv6 socket listens on the same port as the IPv4 socket. + + struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)[interface6 mutableBytes]; + addr6->sin6_port = htons([self localPort4]); + } + + socket6FD = createSocket(AF_INET6, interface6); + + if (socket6FD == SOCKET_NULL) + { + if (socket4FD != SOCKET_NULL) + { + LogVerbose(@"close(socket4FD)"); + close(socket4FD); + } + + return_from_block; + } + } + + // Create accept sources + + if (enableIPv4) + { + accept4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket4FD, 0, socketQueue); + + int socketFD = socket4FD; + dispatch_source_t acceptSource = accept4Source; + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(accept4Source, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + LogVerbose(@"event4Block"); + + unsigned long i = 0; + unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); + + LogVerbose(@"numPendingConnections: %lu", numPendingConnections); + + while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections)); + + #pragma clang diagnostic pop + }}); + + + dispatch_source_set_cancel_handler(accept4Source, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + #if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(accept4Source)"); + dispatch_release(acceptSource); + #endif + + LogVerbose(@"close(socket4FD)"); + close(socketFD); + + #pragma clang diagnostic pop + }); + + LogVerbose(@"dispatch_resume(accept4Source)"); + dispatch_resume(accept4Source); + } + + if (enableIPv6) + { + accept6Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket6FD, 0, socketQueue); + + int socketFD = socket6FD; + dispatch_source_t acceptSource = accept6Source; + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(accept6Source, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + LogVerbose(@"event6Block"); + + unsigned long i = 0; + unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); + + LogVerbose(@"numPendingConnections: %lu", numPendingConnections); + + while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections)); + + #pragma clang diagnostic pop + }}); + + dispatch_source_set_cancel_handler(accept6Source, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + #if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(accept6Source)"); + dispatch_release(acceptSource); + #endif + + LogVerbose(@"close(socket6FD)"); + close(socketFD); + + #pragma clang diagnostic pop + }); + + LogVerbose(@"dispatch_resume(accept6Source)"); + dispatch_resume(accept6Source); + } + + flags |= kSocketStarted; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (result == NO) + { + LogInfo(@"Error in accept: %@", err); + + if (errPtr) + *errPtr = err; + } + + return result; +} + +- (BOOL)doAccept:(int)parentSocketFD +{ + LogTrace(); + + BOOL isIPv4; + int childSocketFD; + NSData *childSocketAddress; + + if (parentSocketFD == socket4FD) + { + isIPv4 = YES; + + struct sockaddr_in addr; + socklen_t addrLen = sizeof(addr); + + childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); + + if (childSocketFD == -1) + { + LogWarn(@"Accept failed with error: %@", [self errnoError]); + return NO; + } + + childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; + } + else // if (parentSocketFD == socket6FD) + { + isIPv4 = NO; + + struct sockaddr_in6 addr; + socklen_t addrLen = sizeof(addr); + + childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); + + if (childSocketFD == -1) + { + LogWarn(@"Accept failed with error: %@", [self errnoError]); + return NO; + } + + childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; + } + + // Enable non-blocking IO on the socket + + int result = fcntl(childSocketFD, F_SETFL, O_NONBLOCK); + if (result == -1) + { + LogWarn(@"Error enabling non-blocking IO on accepted socket (fcntl)"); + return NO; + } + + // Prevent SIGPIPE signals + + int nosigpipe = 1; + setsockopt(childSocketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); + + // Notify delegate + + if (delegateQueue) + { + __strong id theDelegate = delegate; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + // Query delegate for custom socket queue + + dispatch_queue_t childSocketQueue = NULL; + + if ([theDelegate respondsToSelector:@selector(newSocketQueueForConnectionFromAddress:onSocket:)]) + { + childSocketQueue = [theDelegate newSocketQueueForConnectionFromAddress:childSocketAddress + onSocket:self]; + } + + // Create GCDAsyncSocket instance for accepted socket + + GCDAsyncSocket *acceptedSocket = [[GCDAsyncSocket alloc] initWithDelegate:theDelegate + delegateQueue:delegateQueue + socketQueue:childSocketQueue]; + + if (isIPv4) + acceptedSocket->socket4FD = childSocketFD; + else + acceptedSocket->socket6FD = childSocketFD; + + acceptedSocket->flags = (kSocketStarted | kConnected); + + // Setup read and write sources for accepted socket + + dispatch_async(acceptedSocket->socketQueue, ^{ @autoreleasepool { + + [acceptedSocket setupReadAndWriteSourcesForNewlyConnectedSocket:childSocketFD]; + }}); + + // Notify delegate + + if ([theDelegate respondsToSelector:@selector(socket:didAcceptNewSocket:)]) + { + [theDelegate socket:self didAcceptNewSocket:acceptedSocket]; + } + + // Release the socket queue returned from the delegate (it was retained by acceptedSocket) + #if !OS_OBJECT_USE_OBJC + if (childSocketQueue) dispatch_release(childSocketQueue); + #endif + + // The accepted socket should have been retained by the delegate. + // Otherwise it gets properly released when exiting the block. + }}); + } + + return YES; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Connecting +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method runs through the various checks required prior to a connection attempt. + * It is shared between the connectToHost and connectToAddress methods. + * +**/ +- (BOOL)preConnectWithInterface:(NSString *)interface error:(NSError **)errPtr +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + if (delegate == nil) // Must have delegate set + { + if (errPtr) + { + NSString *msg = @"Attempting to connect without a delegate. Set a delegate first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (delegateQueue == NULL) // Must have delegate queue set + { + if (errPtr) + { + NSString *msg = @"Attempting to connect without a delegate queue. Set a delegate queue first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (![self isDisconnected]) // Must be disconnected + { + if (errPtr) + { + NSString *msg = @"Attempting to connect while connected or accepting connections. Disconnect first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled + { + if (errPtr) + { + NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (interface) + { + NSMutableData *interface4 = nil; + NSMutableData *interface6 = nil; + + [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:0]; + + if ((interface4 == nil) && (interface6 == nil)) + { + if (errPtr) + { + NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; + *errPtr = [self badParamError:msg]; + } + return NO; + } + + if (isIPv4Disabled && (interface6 == nil)) + { + if (errPtr) + { + NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; + *errPtr = [self badParamError:msg]; + } + return NO; + } + + if (isIPv6Disabled && (interface4 == nil)) + { + if (errPtr) + { + NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; + *errPtr = [self badParamError:msg]; + } + return NO; + } + + connectInterface4 = interface4; + connectInterface6 = interface6; + } + + // Clear queues (spurious read/write requests post disconnect) + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + return YES; +} + +- (BOOL)connectToHost:(NSString*)host onPort:(uint16_t)port error:(NSError **)errPtr +{ + return [self connectToHost:host onPort:port withTimeout:-1 error:errPtr]; +} + +- (BOOL)connectToHost:(NSString *)host + onPort:(uint16_t)port + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr +{ + return [self connectToHost:host onPort:port viaInterface:nil withTimeout:timeout error:errPtr]; +} + +- (BOOL)connectToHost:(NSString *)inHost + onPort:(uint16_t)port + viaInterface:(NSString *)inInterface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr +{ + LogTrace(); + + // Just in case immutable objects were passed + NSString *host = [inHost copy]; + NSString *interface = [inInterface copy]; + + __block BOOL result = NO; + __block NSError *preConnectErr = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Check for problems with host parameter + + if ([host length] == 0) + { + NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string."; + preConnectErr = [self badParamError:msg]; + + return_from_block; + } + + // Run through standard pre-connect checks + + if (![self preConnectWithInterface:interface error:&preConnectErr]) + { + return_from_block; + } + + // We've made it past all the checks. + // It's time to start the connection process. + + flags |= kSocketStarted; + + LogVerbose(@"Dispatching DNS lookup..."); + + // It's possible that the given host parameter is actually a NSMutableString. + // So we want to copy it now, within this block that will be executed synchronously. + // This way the asynchronous lookup block below doesn't have to worry about it changing. + + NSString *hostCpy = [host copy]; + + int aStateIndex = stateIndex; + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(globalConcurrentQueue, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + NSError *lookupErr = nil; + NSMutableArray *addresses = [GCDAsyncSocket lookupHost:hostCpy port:port error:&lookupErr]; + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + if (lookupErr) + { + dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { + + [strongSelf lookup:aStateIndex didFail:lookupErr]; + }}); + } + else + { + NSData *address4 = nil; + NSData *address6 = nil; + + for (NSData *address in addresses) + { + if (!address4 && [GCDAsyncSocket isIPv4Address:address]) + { + address4 = address; + } + else if (!address6 && [GCDAsyncSocket isIPv6Address:address]) + { + address6 = address; + } + } + + dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { + + [strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6]; + }}); + } + + #pragma clang diagnostic pop + }}); + + [self startConnectTimeout:timeout]; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + + if (errPtr) *errPtr = preConnectErr; + return result; +} + +- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr +{ + return [self connectToAddress:remoteAddr viaInterface:nil withTimeout:-1 error:errPtr]; +} + +- (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr +{ + return [self connectToAddress:remoteAddr viaInterface:nil withTimeout:timeout error:errPtr]; +} + +- (BOOL)connectToAddress:(NSData *)inRemoteAddr + viaInterface:(NSString *)inInterface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr +{ + LogTrace(); + + // Just in case immutable objects were passed + NSData *remoteAddr = [inRemoteAddr copy]; + NSString *interface = [inInterface copy]; + + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Check for problems with remoteAddr parameter + + NSData *address4 = nil; + NSData *address6 = nil; + + if ([remoteAddr length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddr = (const struct sockaddr *)[remoteAddr bytes]; + + if (sockaddr->sa_family == AF_INET) + { + if ([remoteAddr length] == sizeof(struct sockaddr_in)) + { + address4 = remoteAddr; + } + } + else if (sockaddr->sa_family == AF_INET6) + { + if ([remoteAddr length] == sizeof(struct sockaddr_in6)) + { + address6 = remoteAddr; + } + } + } + + if ((address4 == nil) && (address6 == nil)) + { + NSString *msg = @"A valid IPv4 or IPv6 address was not given"; + err = [self badParamError:msg]; + + return_from_block; + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && (address4 != nil)) + { + NSString *msg = @"IPv4 has been disabled and an IPv4 address was passed."; + err = [self badParamError:msg]; + + return_from_block; + } + + if (isIPv6Disabled && (address6 != nil)) + { + NSString *msg = @"IPv6 has been disabled and an IPv6 address was passed."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Run through standard pre-connect checks + + if (![self preConnectWithInterface:interface error:&err]) + { + return_from_block; + } + + // We've made it past all the checks. + // It's time to start the connection process. + + if (![self connectWithAddress4:address4 address6:address6 error:&err]) + { + return_from_block; + } + + flags |= kSocketStarted; + + [self startConnectTimeout:timeout]; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (result == NO) + { + if (errPtr) + *errPtr = err; + } + + return result; +} + +- (void)lookup:(int)aStateIndex didSucceedWithAddress4:(NSData *)address4 address6:(NSData *)address6 +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert(address4 || address6, @"Expected at least one valid address"); + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring lookupDidSucceed, already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + // Check for problems + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && (address6 == nil)) + { + NSString *msg = @"IPv4 has been disabled and DNS lookup found no IPv6 address."; + + [self closeWithError:[self otherError:msg]]; + return; + } + + if (isIPv6Disabled && (address4 == nil)) + { + NSString *msg = @"IPv6 has been disabled and DNS lookup found no IPv4 address."; + + [self closeWithError:[self otherError:msg]]; + return; + } + + // Start the normal connection process + + NSError *err = nil; + if (![self connectWithAddress4:address4 address6:address6 error:&err]) + { + [self closeWithError:err]; + } +} + +/** + * This method is called if the DNS lookup fails. + * This method is executed on the socketQueue. + * + * Since the DNS lookup executed synchronously on a global concurrent queue, + * the original connection request may have already been cancelled or timed-out by the time this method is invoked. + * The lookupIndex tells us whether the lookup is still valid or not. +**/ +- (void)lookup:(int)aStateIndex didFail:(NSError *)error +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring lookup:didFail: - already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + [self endConnectTimeout]; + [self closeWithError:error]; +} + +- (BOOL)connectWithAddress4:(NSData *)address4 address6:(NSData *)address6 error:(NSError **)errPtr +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + LogVerbose(@"IPv4: %@:%hu", [[self class] hostFromAddress:address4], [[self class] portFromAddress:address4]); + LogVerbose(@"IPv6: %@:%hu", [[self class] hostFromAddress:address6], [[self class] portFromAddress:address6]); + + // Determine socket type + + BOOL preferIPv6 = (config & kPreferIPv6) ? YES : NO; + + BOOL useIPv6 = ((preferIPv6 && address6) || (address4 == nil)); + + // Create the socket + + int socketFD; + NSData *address; + NSData *connectInterface; + + if (useIPv6) + { + LogVerbose(@"Creating IPv6 socket"); + + socket6FD = socket(AF_INET6, SOCK_STREAM, 0); + + socketFD = socket6FD; + address = address6; + connectInterface = connectInterface6; + } + else + { + LogVerbose(@"Creating IPv4 socket"); + + socket4FD = socket(AF_INET, SOCK_STREAM, 0); + + socketFD = socket4FD; + address = address4; + connectInterface = connectInterface4; + } + + if (socketFD == SOCKET_NULL) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error in socket() function"]; + + return NO; + } + + // Bind the socket to the desired interface (if needed) + + if (connectInterface) + { + LogVerbose(@"Binding socket..."); + + if ([[self class] portFromAddress:connectInterface] > 0) + { + // Since we're going to be binding to a specific port, + // we should turn on reuseaddr to allow us to override sockets in time_wait. + + int reuseOn = 1; + setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + } + + const struct sockaddr *interfaceAddr = (const struct sockaddr *)[connectInterface bytes]; + + int result = bind(socketFD, interfaceAddr, (socklen_t)[connectInterface length]); + if (result != 0) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error in bind() function"]; + + return NO; + } + } + + // Prevent SIGPIPE signals + + int nosigpipe = 1; + setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); + + // Start the connection process in a background queue + + int aStateIndex = stateIndex; + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(globalConcurrentQueue, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + int result = connect(socketFD, (const struct sockaddr *)[address bytes], (socklen_t)[address length]); + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + if (result == 0) + { + dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { + + [strongSelf didConnect:aStateIndex]; + }}); + } + else + { + NSError *error = [strongSelf errnoErrorWithReason:@"Error in connect() function"]; + + dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { + + [strongSelf didNotConnect:aStateIndex error:error]; + }}); + } + + #pragma clang diagnostic pop + }); + + LogVerbose(@"Connecting..."); + + return YES; +} + +- (void)didConnect:(int)aStateIndex +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring didConnect, already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + flags |= kConnected; + + [self endConnectTimeout]; + + #if TARGET_OS_IPHONE + // The endConnectTimeout method executed above incremented the stateIndex. + aStateIndex = stateIndex; + #endif + + // Setup read/write streams (as workaround for specific shortcomings in the iOS platform) + // + // Note: + // There may be configuration options that must be set by the delegate before opening the streams. + // The primary example is the kCFStreamNetworkServiceTypeVoIP flag, which only works on an unopened stream. + // + // Thus we wait until after the socket:didConnectToHost:port: delegate method has completed. + // This gives the delegate time to properly configure the streams if needed. + + dispatch_block_t SetupStreamsPart1 = ^{ + #if TARGET_OS_IPHONE + + if (![self createReadAndWriteStream]) + { + [self closeWithError:[self otherError:@"Error creating CFStreams"]]; + return; + } + + if (![self registerForStreamCallbacksIncludingReadWrite:NO]) + { + [self closeWithError:[self otherError:@"Error in CFStreamSetClient"]]; + return; + } + + #endif + }; + dispatch_block_t SetupStreamsPart2 = ^{ + #if TARGET_OS_IPHONE + + if (aStateIndex != stateIndex) + { + // The socket has been disconnected. + return; + } + + if (![self addStreamsToRunLoop]) + { + [self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]]; + return; + } + + if (![self openStreams]) + { + [self closeWithError:[self otherError:@"Error creating CFStreams"]]; + return; + } + + #endif + }; + + // Notify delegate + + NSString *host = [self connectedHost]; + uint16_t port = [self connectedPort]; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didConnectToHost:port:)]) + { + SetupStreamsPart1(); + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didConnectToHost:host port:port]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + SetupStreamsPart2(); + }}); + }}); + } + else + { + SetupStreamsPart1(); + SetupStreamsPart2(); + } + + // Get the connected socket + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : socket6FD; + + // Enable non-blocking IO on the socket + + int result = fcntl(socketFD, F_SETFL, O_NONBLOCK); + if (result == -1) + { + NSString *errMsg = @"Error enabling non-blocking IO on socket (fcntl)"; + [self closeWithError:[self otherError:errMsg]]; + + return; + } + + // Setup our read/write sources + + [self setupReadAndWriteSourcesForNewlyConnectedSocket:socketFD]; + + // Dequeue any pending read/write requests + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; +} + +- (void)didNotConnect:(int)aStateIndex error:(NSError *)error +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring didNotConnect, already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + [self closeWithError:error]; +} + +- (void)startConnectTimeout:(NSTimeInterval)timeout +{ + if (timeout >= 0.0) + { + connectTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(connectTimer, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + [strongSelf doConnectTimeout]; + + #pragma clang diagnostic pop + }}); + + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theConnectTimer = connectTimer; + dispatch_source_set_cancel_handler(connectTimer, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"dispatch_release(connectTimer)"); + dispatch_release(theConnectTimer); + + #pragma clang diagnostic pop + }); + #endif + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); + dispatch_source_set_timer(connectTimer, tt, DISPATCH_TIME_FOREVER, 0); + + dispatch_resume(connectTimer); + } +} + +- (void)endConnectTimeout +{ + LogTrace(); + + if (connectTimer) + { + dispatch_source_cancel(connectTimer); + connectTimer = NULL; + } + + // Increment stateIndex. + // This will prevent us from processing results from any related background asynchronous operations. + // + // Note: This should be called from close method even if connectTimer is NULL. + // This is because one might disconnect a socket prior to a successful connection which had no timeout. + + stateIndex++; + + if (connectInterface4) + { + connectInterface4 = nil; + } + if (connectInterface6) + { + connectInterface6 = nil; + } +} + +- (void)doConnectTimeout +{ + LogTrace(); + + [self endConnectTimeout]; + [self closeWithError:[self connectTimeoutError]]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Disconnecting +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)closeWithError:(NSError *)error +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + [self endConnectTimeout]; + + if (currentRead != nil) [self endCurrentRead]; + if (currentWrite != nil) [self endCurrentWrite]; + + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + [preBuffer reset]; + + #if TARGET_OS_IPHONE + { + if (readStream || writeStream) + { + [self removeStreamsFromRunLoop]; + + if (readStream) + { + CFReadStreamSetClient(readStream, kCFStreamEventNone, NULL, NULL); + CFReadStreamClose(readStream); + CFRelease(readStream); + readStream = NULL; + } + if (writeStream) + { + CFWriteStreamSetClient(writeStream, kCFStreamEventNone, NULL, NULL); + CFWriteStreamClose(writeStream); + CFRelease(writeStream); + writeStream = NULL; + } + } + } + #endif + + [sslPreBuffer reset]; + sslErrCode = noErr; + + if (sslContext) + { + // Getting a linker error here about the SSLx() functions? + // You need to add the Security Framework to your application. + + SSLClose(sslContext); + + #if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1080) + CFRelease(sslContext); + #else + SSLDisposeContext(sslContext); + #endif + + sslContext = NULL; + } + + // For some crazy reason (in my opinion), cancelling a dispatch source doesn't + // invoke the cancel handler if the dispatch source is paused. + // So we have to unpause the source if needed. + // This allows the cancel handler to be run, which in turn releases the source and closes the socket. + + if (!accept4Source && !accept6Source && !readSource && !writeSource) + { + LogVerbose(@"manually closing close"); + + if (socket4FD != SOCKET_NULL) + { + LogVerbose(@"close(socket4FD)"); + close(socket4FD); + socket4FD = SOCKET_NULL; + } + + if (socket6FD != SOCKET_NULL) + { + LogVerbose(@"close(socket6FD)"); + close(socket6FD); + socket6FD = SOCKET_NULL; + } + } + else + { + if (accept4Source) + { + LogVerbose(@"dispatch_source_cancel(accept4Source)"); + dispatch_source_cancel(accept4Source); + + // We never suspend accept4Source + + accept4Source = NULL; + } + + if (accept6Source) + { + LogVerbose(@"dispatch_source_cancel(accept6Source)"); + dispatch_source_cancel(accept6Source); + + // We never suspend accept6Source + + accept6Source = NULL; + } + + if (readSource) + { + LogVerbose(@"dispatch_source_cancel(readSource)"); + dispatch_source_cancel(readSource); + + [self resumeReadSource]; + + readSource = NULL; + } + + if (writeSource) + { + LogVerbose(@"dispatch_source_cancel(writeSource)"); + dispatch_source_cancel(writeSource); + + [self resumeWriteSource]; + + writeSource = NULL; + } + + // The sockets will be closed by the cancel handlers of the corresponding source + + socket4FD = SOCKET_NULL; + socket6FD = SOCKET_NULL; + } + + // If the client has passed the connect/accept method, then the connection has at least begun. + // Notify delegate that it is now ending. + BOOL shouldCallDelegate = (flags & kSocketStarted) ? YES : NO; + BOOL isDeallocating = (flags & kDealloc) ? YES : NO; + + // Clear stored socket info and all flags (config remains as is) + socketFDBytesAvailable = 0; + flags = 0; + + if (shouldCallDelegate) + { + __strong id theDelegate = delegate; + __strong id theSelf = isDeallocating ? nil : self; + + if (delegateQueue && [theDelegate respondsToSelector: @selector(socketDidDisconnect:withError:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidDisconnect:theSelf withError:error]; + }}); + } + } +} + +- (void)disconnect +{ + dispatch_block_t block = ^{ @autoreleasepool { + + if (flags & kSocketStarted) + { + [self closeWithError:nil]; + } + }}; + + // Synchronous disconnection, as documented in the header file + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); +} + +- (void)disconnectAfterReading +{ + dispatch_async(socketQueue, ^{ @autoreleasepool { + + if (flags & kSocketStarted) + { + flags |= (kForbidReadsWrites | kDisconnectAfterReads); + [self maybeClose]; + } + }}); +} + +- (void)disconnectAfterWriting +{ + dispatch_async(socketQueue, ^{ @autoreleasepool { + + if (flags & kSocketStarted) + { + flags |= (kForbidReadsWrites | kDisconnectAfterWrites); + [self maybeClose]; + } + }}); +} + +- (void)disconnectAfterReadingAndWriting +{ + dispatch_async(socketQueue, ^{ @autoreleasepool { + + if (flags & kSocketStarted) + { + flags |= (kForbidReadsWrites | kDisconnectAfterReads | kDisconnectAfterWrites); + [self maybeClose]; + } + }}); +} + +/** + * Closes the socket if possible. + * That is, if all writes have completed, and we're set to disconnect after writing, + * or if all reads have completed, and we're set to disconnect after reading. +**/ +- (void)maybeClose +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + BOOL shouldClose = NO; + + if (flags & kDisconnectAfterReads) + { + if (([readQueue count] == 0) && (currentRead == nil)) + { + if (flags & kDisconnectAfterWrites) + { + if (([writeQueue count] == 0) && (currentWrite == nil)) + { + shouldClose = YES; + } + } + else + { + shouldClose = YES; + } + } + } + else if (flags & kDisconnectAfterWrites) + { + if (([writeQueue count] == 0) && (currentWrite == nil)) + { + shouldClose = YES; + } + } + + if (shouldClose) + { + [self closeWithError:nil]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Errors +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSError *)badConfigError:(NSString *)errMsg +{ + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadConfigError userInfo:userInfo]; +} + +- (NSError *)badParamError:(NSString *)errMsg +{ + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadParamError userInfo:userInfo]; +} + ++ (NSError *)gaiError:(int)gai_error +{ + NSString *errMsg = [NSString stringWithCString:gai_strerror(gai_error) encoding:NSASCIIStringEncoding]; + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:@"kCFStreamErrorDomainNetDB" code:gai_error userInfo:userInfo]; +} + +- (NSError *)errnoErrorWithReason:(NSString *)reason +{ + NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)]; + NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:errMsg, NSLocalizedDescriptionKey, + reason, NSLocalizedFailureReasonErrorKey, nil]; + + return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; +} + +- (NSError *)errnoError +{ + NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)]; + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; +} + +- (NSError *)sslError:(OSStatus)ssl_error +{ + NSString *msg = @"Error code definition can be found in Apple's SecureTransport.h"; + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:msg forKey:NSLocalizedRecoverySuggestionErrorKey]; + + return [NSError errorWithDomain:@"kCFStreamErrorDomainSSL" code:ssl_error userInfo:userInfo]; +} + +- (NSError *)connectTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketConnectTimeoutError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Attempt to connect to host timed out", nil); + + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketConnectTimeoutError userInfo:userInfo]; +} + +/** + * Returns a standard AsyncSocket maxed out error. +**/ +- (NSError *)readMaxedOutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketReadMaxedOutError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Read operation reached set maximum length", nil); + + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketReadMaxedOutError userInfo:info]; +} + +/** + * Returns a standard AsyncSocket write timeout error. +**/ +- (NSError *)readTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketReadTimeoutError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Read operation timed out", nil); + + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketReadTimeoutError userInfo:userInfo]; +} + +/** + * Returns a standard AsyncSocket write timeout error. +**/ +- (NSError *)writeTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketWriteTimeoutError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Write operation timed out", nil); + + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketWriteTimeoutError userInfo:userInfo]; +} + +- (NSError *)connectionClosedError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketClosedError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Socket closed by remote peer", nil); + + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketClosedError userInfo:userInfo]; +} + +- (NSError *)otherError:(NSString *)errMsg +{ + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketOtherError userInfo:userInfo]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Diagnostics +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)isDisconnected +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (flags & kSocketStarted) ? NO : YES; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (BOOL)isConnected +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (flags & kConnected) ? YES : NO; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (NSString *)connectedHost +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self connectedHostFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self connectedHostFromSocket6:socket6FD]; + + return nil; + } + else + { + __block NSString *result = nil; + + dispatch_sync(socketQueue, ^{ @autoreleasepool { + + if (socket4FD != SOCKET_NULL) + result = [self connectedHostFromSocket4:socket4FD]; + else if (socket6FD != SOCKET_NULL) + result = [self connectedHostFromSocket6:socket6FD]; + }}); + + return result; + } +} + +- (uint16_t)connectedPort +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self connectedPortFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self connectedPortFromSocket6:socket6FD]; + + return 0; + } + else + { + __block uint16_t result = 0; + + dispatch_sync(socketQueue, ^{ + // No need for autorelease pool + + if (socket4FD != SOCKET_NULL) + result = [self connectedPortFromSocket4:socket4FD]; + else if (socket6FD != SOCKET_NULL) + result = [self connectedPortFromSocket6:socket6FD]; + }); + + return result; + } +} + +- (NSString *)localHost +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self localHostFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self localHostFromSocket6:socket6FD]; + + return nil; + } + else + { + __block NSString *result = nil; + + dispatch_sync(socketQueue, ^{ @autoreleasepool { + + if (socket4FD != SOCKET_NULL) + result = [self localHostFromSocket4:socket4FD]; + else if (socket6FD != SOCKET_NULL) + result = [self localHostFromSocket6:socket6FD]; + }}); + + return result; + } +} + +- (uint16_t)localPort +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self localPortFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self localPortFromSocket6:socket6FD]; + + return 0; + } + else + { + __block uint16_t result = 0; + + dispatch_sync(socketQueue, ^{ + // No need for autorelease pool + + if (socket4FD != SOCKET_NULL) + result = [self localPortFromSocket4:socket4FD]; + else if (socket6FD != SOCKET_NULL) + result = [self localPortFromSocket6:socket6FD]; + }); + + return result; + } +} + +- (NSString *)connectedHost4 +{ + if (socket4FD != SOCKET_NULL) + return [self connectedHostFromSocket4:socket4FD]; + + return nil; +} + +- (NSString *)connectedHost6 +{ + if (socket6FD != SOCKET_NULL) + return [self connectedHostFromSocket6:socket6FD]; + + return nil; +} + +- (uint16_t)connectedPort4 +{ + if (socket4FD != SOCKET_NULL) + return [self connectedPortFromSocket4:socket4FD]; + + return 0; +} + +- (uint16_t)connectedPort6 +{ + if (socket6FD != SOCKET_NULL) + return [self connectedPortFromSocket6:socket6FD]; + + return 0; +} + +- (NSString *)localHost4 +{ + if (socket4FD != SOCKET_NULL) + return [self localHostFromSocket4:socket4FD]; + + return nil; +} + +- (NSString *)localHost6 +{ + if (socket6FD != SOCKET_NULL) + return [self localHostFromSocket6:socket6FD]; + + return nil; +} + +- (uint16_t)localPort4 +{ + if (socket4FD != SOCKET_NULL) + return [self localPortFromSocket4:socket4FD]; + + return 0; +} + +- (uint16_t)localPort6 +{ + if (socket6FD != SOCKET_NULL) + return [self localPortFromSocket6:socket6FD]; + + return 0; +} + +- (NSString *)connectedHostFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr4:&sockaddr4]; +} + +- (NSString *)connectedHostFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr6:&sockaddr6]; +} + +- (uint16_t)connectedPortFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr4:&sockaddr4]; +} + +- (uint16_t)connectedPortFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr6:&sockaddr6]; +} + +- (NSString *)localHostFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr4:&sockaddr4]; +} + +- (NSString *)localHostFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr6:&sockaddr6]; +} + +- (uint16_t)localPortFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr4:&sockaddr4]; +} + +- (uint16_t)localPortFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr6:&sockaddr6]; +} + +- (NSData *)connectedAddress +{ + __block NSData *result = nil; + + dispatch_block_t block = ^{ + if (socket4FD != SOCKET_NULL) + { + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getpeername(socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr4 length:sockaddr4len]; + } + } + + if (socket6FD != SOCKET_NULL) + { + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getpeername(socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr6 length:sockaddr6len]; + } + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (NSData *)localAddress +{ + __block NSData *result = nil; + + dispatch_block_t block = ^{ + if (socket4FD != SOCKET_NULL) + { + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getsockname(socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr4 length:sockaddr4len]; + } + } + + if (socket6FD != SOCKET_NULL) + { + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getsockname(socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr6 length:sockaddr6len]; + } + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (BOOL)isIPv4 +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return (socket4FD != SOCKET_NULL); + } + else + { + __block BOOL result = NO; + + dispatch_sync(socketQueue, ^{ + result = (socket4FD != SOCKET_NULL); + }); + + return result; + } +} + +- (BOOL)isIPv6 +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return (socket6FD != SOCKET_NULL); + } + else + { + __block BOOL result = NO; + + dispatch_sync(socketQueue, ^{ + result = (socket6FD != SOCKET_NULL); + }); + + return result; + } +} + +- (BOOL)isSecure +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return (flags & kSocketSecure) ? YES : NO; + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = (flags & kSocketSecure) ? YES : NO; + }); + + return result; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Utilities +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Finds the address of an interface description. + * An inteface description may be an interface name (en0, en1, lo0) or corresponding IP (192.168.4.34). + * + * The interface description may optionally contain a port number at the end, separated by a colon. + * If a non-zero port parameter is provided, any port number in the interface description is ignored. + * + * The returned value is a 'struct sockaddr' wrapped in an NSMutableData object. +**/ +- (void)getInterfaceAddress4:(NSMutableData **)interfaceAddr4Ptr + address6:(NSMutableData **)interfaceAddr6Ptr + fromDescription:(NSString *)interfaceDescription + port:(uint16_t)port +{ + NSMutableData *addr4 = nil; + NSMutableData *addr6 = nil; + + NSString *interface = nil; + + NSArray *components = [interfaceDescription componentsSeparatedByString:@":"]; + if ([components count] > 0) + { + NSString *temp = [components objectAtIndex:0]; + if ([temp length] > 0) + { + interface = temp; + } + } + if ([components count] > 1 && port == 0) + { + long portL = strtol([[components objectAtIndex:1] UTF8String], NULL, 10); + + if (portL > 0 && portL <= UINT16_MAX) + { + port = (uint16_t)portL; + } + } + + if (interface == nil) + { + // ANY address + + struct sockaddr_in sockaddr4; + memset(&sockaddr4, 0, sizeof(sockaddr4)); + + sockaddr4.sin_len = sizeof(sockaddr4); + sockaddr4.sin_family = AF_INET; + sockaddr4.sin_port = htons(port); + sockaddr4.sin_addr.s_addr = htonl(INADDR_ANY); + + struct sockaddr_in6 sockaddr6; + memset(&sockaddr6, 0, sizeof(sockaddr6)); + + sockaddr6.sin6_len = sizeof(sockaddr6); + sockaddr6.sin6_family = AF_INET6; + sockaddr6.sin6_port = htons(port); + sockaddr6.sin6_addr = in6addr_any; + + addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; + addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; + } + else if ([interface isEqualToString:@"localhost"] || [interface isEqualToString:@"loopback"]) + { + // LOOPBACK address + + struct sockaddr_in sockaddr4; + memset(&sockaddr4, 0, sizeof(sockaddr4)); + + sockaddr4.sin_len = sizeof(sockaddr4); + sockaddr4.sin_family = AF_INET; + sockaddr4.sin_port = htons(port); + sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + + struct sockaddr_in6 sockaddr6; + memset(&sockaddr6, 0, sizeof(sockaddr6)); + + sockaddr6.sin6_len = sizeof(sockaddr6); + sockaddr6.sin6_family = AF_INET6; + sockaddr6.sin6_port = htons(port); + sockaddr6.sin6_addr = in6addr_loopback; + + addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; + addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; + } + else + { + const char *iface = [interface UTF8String]; + + struct ifaddrs *addrs; + const struct ifaddrs *cursor; + + if ((getifaddrs(&addrs) == 0)) + { + cursor = addrs; + while (cursor != NULL) + { + if ((addr4 == nil) && (cursor->ifa_addr->sa_family == AF_INET)) + { + // IPv4 + + struct sockaddr_in nativeAddr4; + memcpy(&nativeAddr4, cursor->ifa_addr, sizeof(nativeAddr4)); + + if (strcmp(cursor->ifa_name, iface) == 0) + { + // Name match + + nativeAddr4.sin_port = htons(port); + + addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + } + else + { + char ip[INET_ADDRSTRLEN]; + + const char *conversion = inet_ntop(AF_INET, &nativeAddr4.sin_addr, ip, sizeof(ip)); + + if ((conversion != NULL) && (strcmp(ip, iface) == 0)) + { + // IP match + + nativeAddr4.sin_port = htons(port); + + addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + } + } + } + else if ((addr6 == nil) && (cursor->ifa_addr->sa_family == AF_INET6)) + { + // IPv6 + + struct sockaddr_in6 nativeAddr6; + memcpy(&nativeAddr6, cursor->ifa_addr, sizeof(nativeAddr6)); + + if (strcmp(cursor->ifa_name, iface) == 0) + { + // Name match + + nativeAddr6.sin6_port = htons(port); + + addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + } + else + { + char ip[INET6_ADDRSTRLEN]; + + const char *conversion = inet_ntop(AF_INET6, &nativeAddr6.sin6_addr, ip, sizeof(ip)); + + if ((conversion != NULL) && (strcmp(ip, iface) == 0)) + { + // IP match + + nativeAddr6.sin6_port = htons(port); + + addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + } + } + } + + cursor = cursor->ifa_next; + } + + freeifaddrs(addrs); + } + } + + if (interfaceAddr4Ptr) *interfaceAddr4Ptr = addr4; + if (interfaceAddr6Ptr) *interfaceAddr6Ptr = addr6; +} + +- (void)setupReadAndWriteSourcesForNewlyConnectedSocket:(int)socketFD +{ + readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socketFD, 0, socketQueue); + writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socketFD, 0, socketQueue); + + // Setup event handlers + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(readSource, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + LogVerbose(@"readEventBlock"); + + strongSelf->socketFDBytesAvailable = dispatch_source_get_data(strongSelf->readSource); + LogVerbose(@"socketFDBytesAvailable: %lu", strongSelf->socketFDBytesAvailable); + + if (strongSelf->socketFDBytesAvailable > 0) + [strongSelf doReadData]; + else + [strongSelf doReadEOF]; + + #pragma clang diagnostic pop + }}); + + dispatch_source_set_event_handler(writeSource, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + LogVerbose(@"writeEventBlock"); + + strongSelf->flags |= kSocketCanAcceptBytes; + [strongSelf doWriteData]; + + #pragma clang diagnostic pop + }}); + + // Setup cancel handlers + + __block int socketFDRefCount = 2; + + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theReadSource = readSource; + dispatch_source_t theWriteSource = writeSource; + #endif + + dispatch_source_set_cancel_handler(readSource, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"readCancelBlock"); + + #if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(readSource)"); + dispatch_release(theReadSource); + #endif + + if (--socketFDRefCount == 0) + { + LogVerbose(@"close(socketFD)"); + close(socketFD); + } + + #pragma clang diagnostic pop + }); + + dispatch_source_set_cancel_handler(writeSource, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"writeCancelBlock"); + + #if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(writeSource)"); + dispatch_release(theWriteSource); + #endif + + if (--socketFDRefCount == 0) + { + LogVerbose(@"close(socketFD)"); + close(socketFD); + } + + #pragma clang diagnostic pop + }); + + // We will not be able to read until data arrives. + // But we should be able to write immediately. + + socketFDBytesAvailable = 0; + flags &= ~kReadSourceSuspended; + + LogVerbose(@"dispatch_resume(readSource)"); + dispatch_resume(readSource); + + flags |= kSocketCanAcceptBytes; + flags |= kWriteSourceSuspended; +} + +- (BOOL)usingCFStreamForTLS +{ + #if TARGET_OS_IPHONE + + if ((flags & kSocketSecure) && (flags & kUsingCFStreamForTLS)) + { + // The startTLS method was given the GCDAsyncSocketUseCFStreamForTLS flag. + + return YES; + } + + #endif + + return NO; +} + +- (BOOL)usingSecureTransportForTLS +{ + // Invoking this method is equivalent to ![self usingCFStreamForTLS] (just more readable) + + #if TARGET_OS_IPHONE + + if ((flags & kSocketSecure) && (flags & kUsingCFStreamForTLS)) + { + // The startTLS method was given the GCDAsyncSocketUseCFStreamForTLS flag. + + return NO; + } + + #endif + + return YES; +} + +- (void)suspendReadSource +{ + if (!(flags & kReadSourceSuspended)) + { + LogVerbose(@"dispatch_suspend(readSource)"); + + dispatch_suspend(readSource); + flags |= kReadSourceSuspended; + } +} + +- (void)resumeReadSource +{ + if (flags & kReadSourceSuspended) + { + LogVerbose(@"dispatch_resume(readSource)"); + + dispatch_resume(readSource); + flags &= ~kReadSourceSuspended; + } +} + +- (void)suspendWriteSource +{ + if (!(flags & kWriteSourceSuspended)) + { + LogVerbose(@"dispatch_suspend(writeSource)"); + + dispatch_suspend(writeSource); + flags |= kWriteSourceSuspended; + } +} + +- (void)resumeWriteSource +{ + if (flags & kWriteSourceSuspended) + { + LogVerbose(@"dispatch_resume(writeSource)"); + + dispatch_resume(writeSource); + flags &= ~kWriteSourceSuspended; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Reading +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataWithTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag]; +} + +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag +{ + [self readDataWithTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag]; +} + +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag +{ + if (offset > [buffer length]) { + LogWarn(@"Cannot read: offset > [buffer length]"); + return; + } + + GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer + startOffset:offset + maxLength:length + timeout:timeout + readLength:0 + terminator:nil + tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)) + { + [readQueue addObject:packet]; + [self maybeDequeueRead]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataToLength:length withTimeout:timeout buffer:nil bufferOffset:0 tag:tag]; +} + +- (void)readDataToLength:(NSUInteger)length + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag +{ + if (length == 0) { + LogWarn(@"Cannot read: length == 0"); + return; + } + if (offset > [buffer length]) { + LogWarn(@"Cannot read: offset > [buffer length]"); + return; + } + + GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer + startOffset:offset + maxLength:0 + timeout:timeout + readLength:length + terminator:nil + tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)) + { + [readQueue addObject:packet]; + [self maybeDequeueRead]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataToData:data withTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag]; +} + +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag +{ + [self readDataToData:data withTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag]; +} + +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(NSUInteger)length tag:(long)tag +{ + [self readDataToData:data withTimeout:timeout buffer:nil bufferOffset:0 maxLength:length tag:tag]; +} + +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)maxLength + tag:(long)tag +{ + if ([data length] == 0) { + LogWarn(@"Cannot read: [data length] == 0"); + return; + } + if (offset > [buffer length]) { + LogWarn(@"Cannot read: offset > [buffer length]"); + return; + } + if (maxLength > 0 && maxLength < [data length]) { + LogWarn(@"Cannot read: maxLength > 0 && maxLength < [data length]"); + return; + } + + GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer + startOffset:offset + maxLength:maxLength + timeout:timeout + readLength:0 + terminator:data + tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)) + { + [readQueue addObject:packet]; + [self maybeDequeueRead]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (float)progressOfReadReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr +{ + __block float result = 0.0F; + + dispatch_block_t block = ^{ + + if (!currentRead || ![currentRead isKindOfClass:[GCDAsyncReadPacket class]]) + { + // We're not reading anything right now. + + if (tagPtr != NULL) *tagPtr = 0; + if (donePtr != NULL) *donePtr = 0; + if (totalPtr != NULL) *totalPtr = 0; + + result = NAN; + } + else + { + // It's only possible to know the progress of our read if we're reading to a certain length. + // If we're reading to data, we of course have no idea when the data will arrive. + // If we're reading to timeout, then we have no idea when the next chunk of data will arrive. + + NSUInteger done = currentRead->bytesDone; + NSUInteger total = currentRead->readLength; + + if (tagPtr != NULL) *tagPtr = currentRead->tag; + if (donePtr != NULL) *donePtr = done; + if (totalPtr != NULL) *totalPtr = total; + + if (total > 0) + result = (float)done / (float)total; + else + result = 1.0F; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +/** + * This method starts a new read, if needed. + * + * It is called when: + * - a user requests a read + * - after a read request has finished (to handle the next request) + * - immediately after the socket opens to handle any pending requests + * + * This method also handles auto-disconnect post read/write completion. +**/ +- (void)maybeDequeueRead +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + // If we're not currently processing a read AND we have an available read stream + if ((currentRead == nil) && (flags & kConnected)) + { + if ([readQueue count] > 0) + { + // Dequeue the next object in the write queue + currentRead = [readQueue objectAtIndex:0]; + [readQueue removeObjectAtIndex:0]; + + + if ([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]]) + { + LogVerbose(@"Dequeued GCDAsyncSpecialPacket"); + + // Attempt to start TLS + flags |= kStartingReadTLS; + + // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set + [self maybeStartTLS]; + } + else + { + LogVerbose(@"Dequeued GCDAsyncReadPacket"); + + // Setup read timer (if needed) + [self setupReadTimerWithTimeout:currentRead->timeout]; + + // Immediately read, if possible + [self doReadData]; + } + } + else if (flags & kDisconnectAfterReads) + { + if (flags & kDisconnectAfterWrites) + { + if (([writeQueue count] == 0) && (currentWrite == nil)) + { + [self closeWithError:nil]; + } + } + else + { + [self closeWithError:nil]; + } + } + else if (flags & kSocketSecure) + { + [self flushSSLBuffers]; + + // Edge case: + // + // We just drained all data from the ssl buffers, + // and all known data from the socket (socketFDBytesAvailable). + // + // If we didn't get any data from this process, + // then we may have reached the end of the TCP stream. + // + // Be sure callbacks are enabled so we're notified about a disconnection. + + if ([preBuffer availableBytes] == 0) + { + if ([self usingCFStreamForTLS]) { + // Callbacks never disabled + } + else { + [self resumeReadSource]; + } + } + } + } +} + +- (void)flushSSLBuffers +{ + LogTrace(); + + NSAssert((flags & kSocketSecure), @"Cannot flush ssl buffers on non-secure socket"); + + if ([preBuffer availableBytes] > 0) + { + // Only flush the ssl buffers if the prebuffer is empty. + // This is to avoid growing the prebuffer inifinitely large. + + return; + } + + #if TARGET_OS_IPHONE + + if ([self usingCFStreamForTLS]) + { + if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream)) + { + LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD); + + CFIndex defaultBytesToRead = (1024 * 4); + + [preBuffer ensureCapacityForWrite:defaultBytesToRead]; + + uint8_t *buffer = [preBuffer writeBuffer]; + + CFIndex result = CFReadStreamRead(readStream, buffer, defaultBytesToRead); + LogVerbose(@"%@ - CFReadStreamRead(): result = %i", THIS_METHOD, (int)result); + + if (result > 0) + { + [preBuffer didWrite:result]; + } + + flags &= ~kSecureSocketHasBytesAvailable; + } + + return; + } + + #endif + + __block NSUInteger estimatedBytesAvailable = 0; + + dispatch_block_t updateEstimatedBytesAvailable = ^{ + + // Figure out if there is any data available to be read + // + // socketFDBytesAvailable <- Number of encrypted bytes we haven't read from the bsd socket + // [sslPreBuffer availableBytes] <- Number of encrypted bytes we've buffered from bsd socket + // sslInternalBufSize <- Number of decrypted bytes SecureTransport has buffered + // + // We call the variable "estimated" because we don't know how many decrypted bytes we'll get + // from the encrypted bytes in the sslPreBuffer. + // However, we do know this is an upper bound on the estimation. + + estimatedBytesAvailable = socketFDBytesAvailable + [sslPreBuffer availableBytes]; + + size_t sslInternalBufSize = 0; + SSLGetBufferedReadSize(sslContext, &sslInternalBufSize); + + estimatedBytesAvailable += sslInternalBufSize; + }; + + updateEstimatedBytesAvailable(); + + if (estimatedBytesAvailable > 0) + { + LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD); + + BOOL done = NO; + do + { + LogVerbose(@"%@ - estimatedBytesAvailable = %lu", THIS_METHOD, (unsigned long)estimatedBytesAvailable); + + // Make sure there's enough room in the prebuffer + + [preBuffer ensureCapacityForWrite:estimatedBytesAvailable]; + + // Read data into prebuffer + + uint8_t *buffer = [preBuffer writeBuffer]; + size_t bytesRead = 0; + + OSStatus result = SSLRead(sslContext, buffer, (size_t)estimatedBytesAvailable, &bytesRead); + LogVerbose(@"%@ - read from secure socket = %u", THIS_METHOD, (unsigned)bytesRead); + + if (bytesRead > 0) + { + [preBuffer didWrite:bytesRead]; + } + + LogVerbose(@"%@ - prebuffer.length = %zu", THIS_METHOD, [preBuffer availableBytes]); + + if (result != noErr) + { + done = YES; + } + else + { + updateEstimatedBytesAvailable(); + } + + } while (!done && estimatedBytesAvailable > 0); + } +} + +- (void)doReadData +{ + LogTrace(); + + // This method is called on the socketQueue. + // It might be called directly, or via the readSource when data is available to be read. + + if ((currentRead == nil) || (flags & kReadsPaused)) + { + LogVerbose(@"No currentRead or kReadsPaused"); + + // Unable to read at this time + + if (flags & kSocketSecure) + { + // Here's the situation: + // + // We have an established secure connection. + // There may not be a currentRead, but there might be encrypted data sitting around for us. + // When the user does get around to issuing a read, that encrypted data will need to be decrypted. + // + // So why make the user wait? + // We might as well get a head start on decrypting some data now. + // + // The other reason we do this has to do with detecting a socket disconnection. + // The SSL/TLS protocol has it's own disconnection handshake. + // So when a secure socket is closed, a "goodbye" packet comes across the wire. + // We want to make sure we read the "goodbye" packet so we can properly detect the TCP disconnection. + + [self flushSSLBuffers]; + } + + if ([self usingCFStreamForTLS]) + { + // CFReadStream only fires once when there is available data. + // It won't fire again until we've invoked CFReadStreamRead. + } + else + { + // If the readSource is firing, we need to pause it + // or else it will continue to fire over and over again. + // + // If the readSource is not firing, + // we want it to continue monitoring the socket. + + if (socketFDBytesAvailable > 0) + { + [self suspendReadSource]; + } + } + return; + } + + BOOL hasBytesAvailable = NO; + unsigned long estimatedBytesAvailable = 0; + + if ([self usingCFStreamForTLS]) + { + #if TARGET_OS_IPHONE + + // Requested CFStream, rather than SecureTransport, for TLS (via GCDAsyncSocketUseCFStreamForTLS) + + estimatedBytesAvailable = 0; + if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream)) + hasBytesAvailable = YES; + else + hasBytesAvailable = NO; + + #endif + } + else + { + estimatedBytesAvailable = socketFDBytesAvailable; + + if (flags & kSocketSecure) + { + // There are 2 buffers to be aware of here. + // + // We are using SecureTransport, a TLS/SSL security layer which sits atop TCP. + // We issue a read to the SecureTranport API, which in turn issues a read to our SSLReadFunction. + // Our SSLReadFunction then reads from the BSD socket and returns the encrypted data to SecureTransport. + // SecureTransport then decrypts the data, and finally returns the decrypted data back to us. + // + // The first buffer is one we create. + // SecureTransport often requests small amounts of data. + // This has to do with the encypted packets that are coming across the TCP stream. + // But it's non-optimal to do a bunch of small reads from the BSD socket. + // So our SSLReadFunction reads all available data from the socket (optimizing the sys call) + // and may store excess in the sslPreBuffer. + + estimatedBytesAvailable += [sslPreBuffer availableBytes]; + + // The second buffer is within SecureTransport. + // As mentioned earlier, there are encrypted packets coming across the TCP stream. + // SecureTransport needs the entire packet to decrypt it. + // But if the entire packet produces X bytes of decrypted data, + // and we only asked SecureTransport for X/2 bytes of data, + // it must store the extra X/2 bytes of decrypted data for the next read. + // + // The SSLGetBufferedReadSize function will tell us the size of this internal buffer. + // From the documentation: + // + // "This function does not block or cause any low-level read operations to occur." + + size_t sslInternalBufSize = 0; + SSLGetBufferedReadSize(sslContext, &sslInternalBufSize); + + estimatedBytesAvailable += sslInternalBufSize; + } + + hasBytesAvailable = (estimatedBytesAvailable > 0); + } + + if ((hasBytesAvailable == NO) && ([preBuffer availableBytes] == 0)) + { + LogVerbose(@"No data available to read..."); + + // No data available to read. + + if (![self usingCFStreamForTLS]) + { + // Need to wait for readSource to fire and notify us of + // available data in the socket's internal read buffer. + + [self resumeReadSource]; + } + return; + } + + if (flags & kStartingReadTLS) + { + LogVerbose(@"Waiting for SSL/TLS handshake to complete"); + + // The readQueue is waiting for SSL/TLS handshake to complete. + + if (flags & kStartingWriteTLS) + { + if ([self usingSecureTransportForTLS]) + { + // We are in the process of a SSL Handshake. + // We were waiting for incoming data which has just arrived. + + [self ssl_continueSSLHandshake]; + } + } + else + { + // We are still waiting for the writeQueue to drain and start the SSL/TLS process. + // We now know data is available to read. + + if (![self usingCFStreamForTLS]) + { + // Suspend the read source or else it will continue to fire nonstop. + + [self suspendReadSource]; + } + } + + return; + } + + BOOL done = NO; // Completed read operation + NSError *error = nil; // Error occured + + NSUInteger totalBytesReadForCurrentRead = 0; + + // + // STEP 1 - READ FROM PREBUFFER + // + + if ([preBuffer availableBytes] > 0) + { + // There are 3 types of read packets: + // + // 1) Read all available data. + // 2) Read a specific length of data. + // 3) Read up to a particular terminator. + + NSUInteger bytesToCopy; + + if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done]; + } + else + { + // Read type #1 or #2 + + bytesToCopy = [currentRead readLengthForNonTermWithHint:[preBuffer availableBytes]]; + } + + // Make sure we have enough room in the buffer for our read. + + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy]; + + // Copy bytes from prebuffer into packet buffer + + uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + + currentRead->bytesDone; + + memcpy(buffer, [preBuffer readBuffer], bytesToCopy); + + // Remove the copied bytes from the preBuffer + [preBuffer didRead:bytesToCopy]; + + LogVerbose(@"copied(%lu) preBufferLength(%zu)", (unsigned long)bytesToCopy, [preBuffer availableBytes]); + + // Update totals + + currentRead->bytesDone += bytesToCopy; + totalBytesReadForCurrentRead += bytesToCopy; + + // Check to see if the read operation is done + + if (currentRead->readLength > 0) + { + // Read type #2 - read a specific length of data + + done = (currentRead->bytesDone == currentRead->readLength); + } + else if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method + + if (!done && currentRead->maxLength > 0) + { + // We're not done and there's a set maxLength. + // Have we reached that maxLength yet? + + if (currentRead->bytesDone >= currentRead->maxLength) + { + error = [self readMaxedOutError]; + } + } + } + else + { + // Read type #1 - read all available data + // + // We're done as soon as + // - we've read all available data (in prebuffer and socket) + // - we've read the maxLength of read packet. + + done = ((currentRead->maxLength > 0) && (currentRead->bytesDone == currentRead->maxLength)); + } + + } + + // + // STEP 2 - READ FROM SOCKET + // + + BOOL socketEOF = (flags & kSocketHasReadEOF) ? YES : NO; // Nothing more to read via socket (end of file) + BOOL waiting = !done && !error && !socketEOF && !hasBytesAvailable; // Ran out of data, waiting for more + + if (!done && !error && !socketEOF && hasBytesAvailable) + { + NSAssert(([preBuffer availableBytes] == 0), @"Invalid logic"); + + BOOL readIntoPreBuffer = NO; + uint8_t *buffer = NULL; + size_t bytesRead = 0; + + if (flags & kSocketSecure) + { + if ([self usingCFStreamForTLS]) + { + #if TARGET_OS_IPHONE + + // Using CFStream, rather than SecureTransport, for TLS + + NSUInteger defaultReadLength = (1024 * 32); + + NSUInteger bytesToRead = [currentRead optimalReadLengthWithDefault:defaultReadLength + shouldPreBuffer:&readIntoPreBuffer]; + + // Make sure we have enough room in the buffer for our read. + // + // We are either reading directly into the currentRead->buffer, + // or we're reading into the temporary preBuffer. + + if (readIntoPreBuffer) + { + [preBuffer ensureCapacityForWrite:bytesToRead]; + + buffer = [preBuffer writeBuffer]; + } + else + { + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; + + buffer = (uint8_t *)[currentRead->buffer mutableBytes] + + currentRead->startOffset + + currentRead->bytesDone; + } + + // Read data into buffer + + CFIndex result = CFReadStreamRead(readStream, buffer, (CFIndex)bytesToRead); + LogVerbose(@"CFReadStreamRead(): result = %i", (int)result); + + if (result < 0) + { + error = (__bridge_transfer NSError *)CFReadStreamCopyError(readStream); + } + else if (result == 0) + { + socketEOF = YES; + } + else + { + waiting = YES; + bytesRead = (size_t)result; + } + + // We only know how many decrypted bytes were read. + // The actual number of bytes read was likely more due to the overhead of the encryption. + // So we reset our flag, and rely on the next callback to alert us of more data. + flags &= ~kSecureSocketHasBytesAvailable; + + #endif + } + else + { + // Using SecureTransport for TLS + // + // We know: + // - how many bytes are available on the socket + // - how many encrypted bytes are sitting in the sslPreBuffer + // - how many decypted bytes are sitting in the sslContext + // + // But we do NOT know: + // - how many encypted bytes are sitting in the sslContext + // + // So we play the regular game of using an upper bound instead. + + NSUInteger defaultReadLength = (1024 * 32); + + if (defaultReadLength < estimatedBytesAvailable) { + defaultReadLength = estimatedBytesAvailable + (1024 * 16); + } + + NSUInteger bytesToRead = [currentRead optimalReadLengthWithDefault:defaultReadLength + shouldPreBuffer:&readIntoPreBuffer]; + + if (bytesToRead > SIZE_MAX) { // NSUInteger may be bigger than size_t + bytesToRead = SIZE_MAX; + } + + // Make sure we have enough room in the buffer for our read. + // + // We are either reading directly into the currentRead->buffer, + // or we're reading into the temporary preBuffer. + + if (readIntoPreBuffer) + { + [preBuffer ensureCapacityForWrite:bytesToRead]; + + buffer = [preBuffer writeBuffer]; + } + else + { + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; + + buffer = (uint8_t *)[currentRead->buffer mutableBytes] + + currentRead->startOffset + + currentRead->bytesDone; + } + + // The documentation from Apple states: + // + // "a read operation might return errSSLWouldBlock, + // indicating that less data than requested was actually transferred" + // + // However, starting around 10.7, the function will sometimes return noErr, + // even if it didn't read as much data as requested. So we need to watch out for that. + + OSStatus result; + do + { + void *loop_buffer = buffer + bytesRead; + size_t loop_bytesToRead = (size_t)bytesToRead - bytesRead; + size_t loop_bytesRead = 0; + + result = SSLRead(sslContext, loop_buffer, loop_bytesToRead, &loop_bytesRead); + LogVerbose(@"read from secure socket = %u", (unsigned)loop_bytesRead); + + bytesRead += loop_bytesRead; + + } while ((result == noErr) && (bytesRead < bytesToRead)); + + + if (result != noErr) + { + if (result == errSSLWouldBlock) + waiting = YES; + else + { + if (result == errSSLClosedGraceful || result == errSSLClosedAbort) + { + // We've reached the end of the stream. + // Handle this the same way we would an EOF from the socket. + socketEOF = YES; + sslErrCode = result; + } + else + { + error = [self sslError:result]; + } + } + // It's possible that bytesRead > 0, even if the result was errSSLWouldBlock. + // This happens when the SSLRead function is able to read some data, + // but not the entire amount we requested. + + if (bytesRead <= 0) + { + bytesRead = 0; + } + } + + // Do not modify socketFDBytesAvailable. + // It will be updated via the SSLReadFunction(). + } + } + else + { + // Normal socket operation + + NSUInteger bytesToRead; + + // There are 3 types of read packets: + // + // 1) Read all available data. + // 2) Read a specific length of data. + // 3) Read up to a particular terminator. + + if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + bytesToRead = [currentRead readLengthForTermWithHint:estimatedBytesAvailable + shouldPreBuffer:&readIntoPreBuffer]; + } + else + { + // Read type #1 or #2 + + bytesToRead = [currentRead readLengthForNonTermWithHint:estimatedBytesAvailable]; + } + + if (bytesToRead > SIZE_MAX) { // NSUInteger may be bigger than size_t (read param 3) + bytesToRead = SIZE_MAX; + } + + // Make sure we have enough room in the buffer for our read. + // + // We are either reading directly into the currentRead->buffer, + // or we're reading into the temporary preBuffer. + + if (readIntoPreBuffer) + { + [preBuffer ensureCapacityForWrite:bytesToRead]; + + buffer = [preBuffer writeBuffer]; + } + else + { + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; + + buffer = (uint8_t *)[currentRead->buffer mutableBytes] + + currentRead->startOffset + + currentRead->bytesDone; + } + + // Read data into buffer + + int socketFD = (socket4FD == SOCKET_NULL) ? socket6FD : socket4FD; + + ssize_t result = read(socketFD, buffer, (size_t)bytesToRead); + LogVerbose(@"read from socket = %i", (int)result); + + if (result < 0) + { + if (errno == EWOULDBLOCK) + waiting = YES; + else + error = [self errnoErrorWithReason:@"Error in read() function"]; + + socketFDBytesAvailable = 0; + } + else if (result == 0) + { + socketEOF = YES; + socketFDBytesAvailable = 0; + } + else + { + bytesRead = result; + + if (bytesRead < bytesToRead) + { + // The read returned less data than requested. + // This means socketFDBytesAvailable was a bit off due to timing, + // because we read from the socket right when the readSource event was firing. + socketFDBytesAvailable = 0; + } + else + { + if (socketFDBytesAvailable <= bytesRead) + socketFDBytesAvailable = 0; + else + socketFDBytesAvailable -= bytesRead; + } + + if (socketFDBytesAvailable == 0) + { + waiting = YES; + } + } + } + + if (bytesRead > 0) + { + // Check to see if the read operation is done + + if (currentRead->readLength > 0) + { + // Read type #2 - read a specific length of data + // + // Note: We should never be using a prebuffer when we're reading a specific length of data. + + NSAssert(readIntoPreBuffer == NO, @"Invalid logic"); + + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + + done = (currentRead->bytesDone == currentRead->readLength); + } + else if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + if (readIntoPreBuffer) + { + // We just read a big chunk of data into the preBuffer + + [preBuffer didWrite:bytesRead]; + LogVerbose(@"read data into preBuffer - preBuffer.length = %zu", [preBuffer availableBytes]); + + // Search for the terminating sequence + + NSUInteger bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done]; + LogVerbose(@"copying %lu bytes from preBuffer", (unsigned long)bytesToCopy); + + // Ensure there's room on the read packet's buffer + + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy]; + + // Copy bytes from prebuffer into read buffer + + uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + + currentRead->bytesDone; + + memcpy(readBuf, [preBuffer readBuffer], bytesToCopy); + + // Remove the copied bytes from the prebuffer + [preBuffer didRead:bytesToCopy]; + LogVerbose(@"preBuffer.length = %zu", [preBuffer availableBytes]); + + // Update totals + currentRead->bytesDone += bytesToCopy; + totalBytesReadForCurrentRead += bytesToCopy; + + // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method above + } + else + { + // We just read a big chunk of data directly into the packet's buffer. + // We need to move any overflow into the prebuffer. + + NSInteger overflow = [currentRead searchForTermAfterPreBuffering:bytesRead]; + + if (overflow == 0) + { + // Perfect match! + // Every byte we read stays in the read buffer, + // and the last byte we read was the last byte of the term. + + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + done = YES; + } + else if (overflow > 0) + { + // The term was found within the data that we read, + // and there are extra bytes that extend past the end of the term. + // We need to move these excess bytes out of the read packet and into the prebuffer. + + NSInteger underflow = bytesRead - overflow; + + // Copy excess data into preBuffer + + LogVerbose(@"copying %ld overflow bytes into preBuffer", (long)overflow); + [preBuffer ensureCapacityForWrite:overflow]; + + uint8_t *overflowBuffer = buffer + underflow; + memcpy([preBuffer writeBuffer], overflowBuffer, overflow); + + [preBuffer didWrite:overflow]; + LogVerbose(@"preBuffer.length = %zu", [preBuffer availableBytes]); + + // Note: The completeCurrentRead method will trim the buffer for us. + + currentRead->bytesDone += underflow; + totalBytesReadForCurrentRead += underflow; + done = YES; + } + else + { + // The term was not found within the data that we read. + + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + done = NO; + } + } + + if (!done && currentRead->maxLength > 0) + { + // We're not done and there's a set maxLength. + // Have we reached that maxLength yet? + + if (currentRead->bytesDone >= currentRead->maxLength) + { + error = [self readMaxedOutError]; + } + } + } + else + { + // Read type #1 - read all available data + + if (readIntoPreBuffer) + { + // We just read a chunk of data into the preBuffer + + [preBuffer didWrite:bytesRead]; + + // Now copy the data into the read packet. + // + // Recall that we didn't read directly into the packet's buffer to avoid + // over-allocating memory since we had no clue how much data was available to be read. + // + // Ensure there's room on the read packet's buffer + + [currentRead ensureCapacityForAdditionalDataOfLength:bytesRead]; + + // Copy bytes from prebuffer into read buffer + + uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + + currentRead->bytesDone; + + memcpy(readBuf, [preBuffer readBuffer], bytesRead); + + // Remove the copied bytes from the prebuffer + [preBuffer didRead:bytesRead]; + + // Update totals + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + } + else + { + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + } + + done = YES; + } + + } // if (bytesRead > 0) + + } // if (!done && !error && !socketEOF && hasBytesAvailable) + + + if (!done && currentRead->readLength == 0 && currentRead->term == nil) + { + // Read type #1 - read all available data + // + // We might arrive here if we read data from the prebuffer but not from the socket. + + done = (totalBytesReadForCurrentRead > 0); + } + + // Check to see if we're done, or if we've made progress + + if (done) + { + [self completeCurrentRead]; + + if (!error && (!socketEOF || [preBuffer availableBytes] > 0)) + { + [self maybeDequeueRead]; + } + } + else if (totalBytesReadForCurrentRead > 0) + { + // We're not done read type #2 or #3 yet, but we have read in some bytes + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReadPartialDataOfLength:tag:)]) + { + long theReadTag = currentRead->tag; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didReadPartialDataOfLength:totalBytesReadForCurrentRead tag:theReadTag]; + }}); + } + } + + // Check for errors + + if (error) + { + [self closeWithError:error]; + } + else if (socketEOF) + { + [self doReadEOF]; + } + else if (waiting) + { + if (![self usingCFStreamForTLS]) + { + // Monitor the socket for readability (if we're not already doing so) + [self resumeReadSource]; + } + } + + // Do not add any code here without first adding return statements in the error cases above. +} + +- (void)doReadEOF +{ + LogTrace(); + + // This method may be called more than once. + // If the EOF is read while there is still data in the preBuffer, + // then this method may be called continually after invocations of doReadData to see if it's time to disconnect. + + flags |= kSocketHasReadEOF; + + if (flags & kSocketSecure) + { + // If the SSL layer has any buffered data, flush it into the preBuffer now. + + [self flushSSLBuffers]; + } + + BOOL shouldDisconnect = NO; + NSError *error = nil; + + if ((flags & kStartingReadTLS) || (flags & kStartingWriteTLS)) + { + // We received an EOF during or prior to startTLS. + // The SSL/TLS handshake is now impossible, so this is an unrecoverable situation. + + shouldDisconnect = YES; + + if ([self usingSecureTransportForTLS]) + { + error = [self sslError:errSSLClosedAbort]; + } + } + else if (flags & kReadStreamClosed) + { + // The preBuffer has already been drained. + // The config allows half-duplex connections. + // We've previously checked the socket, and it appeared writeable. + // So we marked the read stream as closed and notified the delegate. + // + // As per the half-duplex contract, the socket will be closed when a write fails, + // or when the socket is manually closed. + + shouldDisconnect = NO; + } + else if ([preBuffer availableBytes] > 0) + { + LogVerbose(@"Socket reached EOF, but there is still data available in prebuffer"); + + // Although we won't be able to read any more data from the socket, + // there is existing data that has been prebuffered that we can read. + + shouldDisconnect = NO; + } + else if (config & kAllowHalfDuplexConnection) + { + // We just received an EOF (end of file) from the socket's read stream. + // This means the remote end of the socket (the peer we're connected to) + // has explicitly stated that it will not be sending us any more data. + // + // Query the socket to see if it is still writeable. (Perhaps the peer will continue reading data from us) + + int socketFD = (socket4FD == SOCKET_NULL) ? socket6FD : socket4FD; + + struct pollfd pfd[1]; + pfd[0].fd = socketFD; + pfd[0].events = POLLOUT; + pfd[0].revents = 0; + + poll(pfd, 1, 0); + + if (pfd[0].revents & POLLOUT) + { + // Socket appears to still be writeable + + shouldDisconnect = NO; + flags |= kReadStreamClosed; + + // Notify the delegate that we're going half-duplex + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidCloseReadStream:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidCloseReadStream:self]; + }}); + } + } + else + { + shouldDisconnect = YES; + } + } + else + { + shouldDisconnect = YES; + } + + + if (shouldDisconnect) + { + if (error == nil) + { + if ([self usingSecureTransportForTLS]) + { + if (sslErrCode != noErr && sslErrCode != errSSLClosedGraceful) + { + error = [self sslError:sslErrCode]; + } + else + { + error = [self connectionClosedError]; + } + } + else + { + error = [self connectionClosedError]; + } + } + [self closeWithError:error]; + } + else + { + if (![self usingCFStreamForTLS]) + { + // Suspend the read source (if needed) + + [self suspendReadSource]; + } + } +} + +- (void)completeCurrentRead +{ + LogTrace(); + + NSAssert(currentRead, @"Trying to complete current read when there is no current read."); + + + NSData *result = nil; + + if (currentRead->bufferOwner) + { + // We created the buffer on behalf of the user. + // Trim our buffer to be the proper size. + [currentRead->buffer setLength:currentRead->bytesDone]; + + result = currentRead->buffer; + } + else + { + // We did NOT create the buffer. + // The buffer is owned by the caller. + // Only trim the buffer if we had to increase its size. + + if ([currentRead->buffer length] > currentRead->originalBufferLength) + { + NSUInteger readSize = currentRead->startOffset + currentRead->bytesDone; + NSUInteger origSize = currentRead->originalBufferLength; + + NSUInteger buffSize = MAX(readSize, origSize); + + [currentRead->buffer setLength:buffSize]; + } + + uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset; + + result = [NSData dataWithBytesNoCopy:buffer length:currentRead->bytesDone freeWhenDone:NO]; + } + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReadData:withTag:)]) + { + GCDAsyncReadPacket *theRead = currentRead; // Ensure currentRead retained since result may not own buffer + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didReadData:result withTag:theRead->tag]; + }}); + } + + [self endCurrentRead]; +} + +- (void)endCurrentRead +{ + if (readTimer) + { + dispatch_source_cancel(readTimer); + readTimer = NULL; + } + + currentRead = nil; +} + +- (void)setupReadTimerWithTimeout:(NSTimeInterval)timeout +{ + if (timeout >= 0.0) + { + readTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(readTimer, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + [strongSelf doReadTimeout]; + + #pragma clang diagnostic pop + }}); + + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theReadTimer = readTimer; + dispatch_source_set_cancel_handler(readTimer, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"dispatch_release(readTimer)"); + dispatch_release(theReadTimer); + + #pragma clang diagnostic pop + }); + #endif + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); + + dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0); + dispatch_resume(readTimer); + } +} + +- (void)doReadTimeout +{ + // This is a little bit tricky. + // Ideally we'd like to synchronously query the delegate about a timeout extension. + // But if we do so synchronously we risk a possible deadlock. + // So instead we have to do so asynchronously, and callback to ourselves from within the delegate block. + + flags |= kReadsPaused; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:shouldTimeoutReadWithTag:elapsed:bytesDone:)]) + { + GCDAsyncReadPacket *theRead = currentRead; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + NSTimeInterval timeoutExtension = 0.0; + + timeoutExtension = [theDelegate socket:self shouldTimeoutReadWithTag:theRead->tag + elapsed:theRead->timeout + bytesDone:theRead->bytesDone]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self doReadTimeoutWithExtension:timeoutExtension]; + }}); + }}); + } + else + { + [self doReadTimeoutWithExtension:0.0]; + } +} + +- (void)doReadTimeoutWithExtension:(NSTimeInterval)timeoutExtension +{ + if (currentRead) + { + if (timeoutExtension > 0.0) + { + currentRead->timeout += timeoutExtension; + + // Reschedule the timer + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeoutExtension * NSEC_PER_SEC)); + dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0); + + // Unpause reads, and continue + flags &= ~kReadsPaused; + [self doReadData]; + } + else + { + LogVerbose(@"ReadTimeout"); + + [self closeWithError:[self readTimeoutError]]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Writing +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + if ([data length] == 0) return; + + GCDAsyncWritePacket *packet = [[GCDAsyncWritePacket alloc] initWithData:data timeout:timeout tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)) + { + [writeQueue addObject:packet]; + [self maybeDequeueWrite]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (float)progressOfWriteReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr +{ + __block float result = 0.0F; + + dispatch_block_t block = ^{ + + if (!currentWrite || ![currentWrite isKindOfClass:[GCDAsyncWritePacket class]]) + { + // We're not writing anything right now. + + if (tagPtr != NULL) *tagPtr = 0; + if (donePtr != NULL) *donePtr = 0; + if (totalPtr != NULL) *totalPtr = 0; + + result = NAN; + } + else + { + NSUInteger done = currentWrite->bytesDone; + NSUInteger total = [currentWrite->buffer length]; + + if (tagPtr != NULL) *tagPtr = currentWrite->tag; + if (donePtr != NULL) *donePtr = done; + if (totalPtr != NULL) *totalPtr = total; + + result = (float)done / (float)total; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +/** + * Conditionally starts a new write. + * + * It is called when: + * - a user requests a write + * - after a write request has finished (to handle the next request) + * - immediately after the socket opens to handle any pending requests + * + * This method also handles auto-disconnect post read/write completion. +**/ +- (void)maybeDequeueWrite +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + // If we're not currently processing a write AND we have an available write stream + if ((currentWrite == nil) && (flags & kConnected)) + { + if ([writeQueue count] > 0) + { + // Dequeue the next object in the write queue + currentWrite = [writeQueue objectAtIndex:0]; + [writeQueue removeObjectAtIndex:0]; + + + if ([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]]) + { + LogVerbose(@"Dequeued GCDAsyncSpecialPacket"); + + // Attempt to start TLS + flags |= kStartingWriteTLS; + + // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set + [self maybeStartTLS]; + } + else + { + LogVerbose(@"Dequeued GCDAsyncWritePacket"); + + // Setup write timer (if needed) + [self setupWriteTimerWithTimeout:currentWrite->timeout]; + + // Immediately write, if possible + [self doWriteData]; + } + } + else if (flags & kDisconnectAfterWrites) + { + if (flags & kDisconnectAfterReads) + { + if (([readQueue count] == 0) && (currentRead == nil)) + { + [self closeWithError:nil]; + } + } + else + { + [self closeWithError:nil]; + } + } + } +} + +- (void)doWriteData +{ + LogTrace(); + + // This method is called by the writeSource via the socketQueue + + if ((currentWrite == nil) || (flags & kWritesPaused)) + { + LogVerbose(@"No currentWrite or kWritesPaused"); + + // Unable to write at this time + + if ([self usingCFStreamForTLS]) + { + // CFWriteStream only fires once when there is available data. + // It won't fire again until we've invoked CFWriteStreamWrite. + } + else + { + // If the writeSource is firing, we need to pause it + // or else it will continue to fire over and over again. + + if (flags & kSocketCanAcceptBytes) + { + [self suspendWriteSource]; + } + } + return; + } + + if (!(flags & kSocketCanAcceptBytes)) + { + LogVerbose(@"No space available to write..."); + + // No space available to write. + + if (![self usingCFStreamForTLS]) + { + // Need to wait for writeSource to fire and notify us of + // available space in the socket's internal write buffer. + + [self resumeWriteSource]; + } + return; + } + + if (flags & kStartingWriteTLS) + { + LogVerbose(@"Waiting for SSL/TLS handshake to complete"); + + // The writeQueue is waiting for SSL/TLS handshake to complete. + + if (flags & kStartingReadTLS) + { + if ([self usingSecureTransportForTLS]) + { + // We are in the process of a SSL Handshake. + // We were waiting for available space in the socket's internal OS buffer to continue writing. + + [self ssl_continueSSLHandshake]; + } + } + else + { + // We are still waiting for the readQueue to drain and start the SSL/TLS process. + // We now know we can write to the socket. + + if (![self usingCFStreamForTLS]) + { + // Suspend the write source or else it will continue to fire nonstop. + + [self suspendWriteSource]; + } + } + + return; + } + + // Note: This method is not called if currentWrite is a GCDAsyncSpecialPacket (startTLS packet) + + BOOL waiting = NO; + NSError *error = nil; + size_t bytesWritten = 0; + + if (flags & kSocketSecure) + { + if ([self usingCFStreamForTLS]) + { + #if TARGET_OS_IPHONE + + // + // Writing data using CFStream (over internal TLS) + // + + const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone; + + NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone; + + if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) + { + bytesToWrite = SIZE_MAX; + } + + CFIndex result = CFWriteStreamWrite(writeStream, buffer, (CFIndex)bytesToWrite); + LogVerbose(@"CFWriteStreamWrite(%lu) = %li", (unsigned long)bytesToWrite, result); + + if (result < 0) + { + error = (__bridge_transfer NSError *)CFWriteStreamCopyError(writeStream); + } + else + { + bytesWritten = (size_t)result; + + // We always set waiting to true in this scenario. + // CFStream may have altered our underlying socket to non-blocking. + // Thus if we attempt to write without a callback, we may end up blocking our queue. + waiting = YES; + } + + #endif + } + else + { + // We're going to use the SSLWrite function. + // + // OSStatus SSLWrite(SSLContextRef context, const void *data, size_t dataLength, size_t *processed) + // + // Parameters: + // context - An SSL session context reference. + // data - A pointer to the buffer of data to write. + // dataLength - The amount, in bytes, of data to write. + // processed - On return, the length, in bytes, of the data actually written. + // + // It sounds pretty straight-forward, + // but there are a few caveats you should be aware of. + // + // The SSLWrite method operates in a non-obvious (and rather annoying) manner. + // According to the documentation: + // + // Because you may configure the underlying connection to operate in a non-blocking manner, + // a write operation might return errSSLWouldBlock, indicating that less data than requested + // was actually transferred. In this case, you should repeat the call to SSLWrite until some + // other result is returned. + // + // This sounds perfect, but when our SSLWriteFunction returns errSSLWouldBlock, + // then the SSLWrite method returns (with the proper errSSLWouldBlock return value), + // but it sets processed to dataLength !! + // + // In other words, if the SSLWrite function doesn't completely write all the data we tell it to, + // then it doesn't tell us how many bytes were actually written. So, for example, if we tell it to + // write 256 bytes then it might actually write 128 bytes, but then report 0 bytes written. + // + // You might be wondering: + // If the SSLWrite function doesn't tell us how many bytes were written, + // then how in the world are we supposed to update our parameters (buffer & bytesToWrite) + // for the next time we invoke SSLWrite? + // + // The answer is that SSLWrite cached all the data we told it to write, + // and it will push out that data next time we call SSLWrite. + // If we call SSLWrite with new data, it will push out the cached data first, and then the new data. + // If we call SSLWrite with empty data, then it will simply push out the cached data. + // + // For this purpose we're going to break large writes into a series of smaller writes. + // This allows us to report progress back to the delegate. + + OSStatus result; + + BOOL hasCachedDataToWrite = (sslWriteCachedLength > 0); + BOOL hasNewDataToWrite = YES; + + if (hasCachedDataToWrite) + { + size_t processed = 0; + + result = SSLWrite(sslContext, NULL, 0, &processed); + + if (result == noErr) + { + bytesWritten = sslWriteCachedLength; + sslWriteCachedLength = 0; + + if ([currentWrite->buffer length] == (currentWrite->bytesDone + bytesWritten)) + { + // We've written all data for the current write. + hasNewDataToWrite = NO; + } + } + else + { + if (result == errSSLWouldBlock) + { + waiting = YES; + } + else + { + error = [self sslError:result]; + } + + // Can't write any new data since we were unable to write the cached data. + hasNewDataToWrite = NO; + } + } + + if (hasNewDataToWrite) + { + const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + + currentWrite->bytesDone + + bytesWritten; + + NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone - bytesWritten; + + if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) + { + bytesToWrite = SIZE_MAX; + } + + size_t bytesRemaining = bytesToWrite; + + BOOL keepLooping = YES; + while (keepLooping) + { + const size_t sslMaxBytesToWrite = 32768; + size_t sslBytesToWrite = MIN(bytesRemaining, sslMaxBytesToWrite); + size_t sslBytesWritten = 0; + + result = SSLWrite(sslContext, buffer, sslBytesToWrite, &sslBytesWritten); + + if (result == noErr) + { + buffer += sslBytesWritten; + bytesWritten += sslBytesWritten; + bytesRemaining -= sslBytesWritten; + + keepLooping = (bytesRemaining > 0); + } + else + { + if (result == errSSLWouldBlock) + { + waiting = YES; + sslWriteCachedLength = sslBytesToWrite; + } + else + { + error = [self sslError:result]; + } + + keepLooping = NO; + } + + } // while (keepLooping) + + } // if (hasNewDataToWrite) + } + } + else + { + // + // Writing data directly over raw socket + // + + int socketFD = (socket4FD == SOCKET_NULL) ? socket6FD : socket4FD; + + const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone; + + NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone; + + if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) + { + bytesToWrite = SIZE_MAX; + } + + ssize_t result = write(socketFD, buffer, (size_t)bytesToWrite); + LogVerbose(@"wrote to socket = %zd", result); + + // Check results + if (result < 0) + { + if (errno == EWOULDBLOCK) + { + waiting = YES; + } + else + { + error = [self errnoErrorWithReason:@"Error in write() function"]; + } + } + else + { + bytesWritten = result; + } + } + + // We're done with our writing. + // If we explictly ran into a situation where the socket told us there was no room in the buffer, + // then we immediately resume listening for notifications. + // + // We must do this before we dequeue another write, + // as that may in turn invoke this method again. + // + // Note that if CFStream is involved, it may have maliciously put our socket in blocking mode. + + if (waiting) + { + flags &= ~kSocketCanAcceptBytes; + + if (![self usingCFStreamForTLS]) + { + [self resumeWriteSource]; + } + } + + // Check our results + + BOOL done = NO; + + if (bytesWritten > 0) + { + // Update total amount read for the current write + currentWrite->bytesDone += bytesWritten; + LogVerbose(@"currentWrite->bytesDone = %lu", (unsigned long)currentWrite->bytesDone); + + // Is packet done? + done = (currentWrite->bytesDone == [currentWrite->buffer length]); + } + + if (done) + { + [self completeCurrentWrite]; + + if (!error) + { + dispatch_async(socketQueue, ^{ @autoreleasepool{ + + [self maybeDequeueWrite]; + }}); + } + } + else + { + // We were unable to finish writing the data, + // so we're waiting for another callback to notify us of available space in the lower-level output buffer. + + if (!waiting && !error) + { + // This would be the case if our write was able to accept some data, but not all of it. + + flags &= ~kSocketCanAcceptBytes; + + if (![self usingCFStreamForTLS]) + { + [self resumeWriteSource]; + } + } + + if (bytesWritten > 0) + { + // We're not done with the entire write, but we have written some bytes + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didWritePartialDataOfLength:tag:)]) + { + long theWriteTag = currentWrite->tag; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didWritePartialDataOfLength:bytesWritten tag:theWriteTag]; + }}); + } + } + } + + // Check for errors + + if (error) + { + [self closeWithError:[self errnoErrorWithReason:@"Error in write() function"]]; + } + + // Do not add any code here without first adding a return statement in the error case above. +} + +- (void)completeCurrentWrite +{ + LogTrace(); + + NSAssert(currentWrite, @"Trying to complete current write when there is no current write."); + + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didWriteDataWithTag:)]) + { + long theWriteTag = currentWrite->tag; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didWriteDataWithTag:theWriteTag]; + }}); + } + + [self endCurrentWrite]; +} + +- (void)endCurrentWrite +{ + if (writeTimer) + { + dispatch_source_cancel(writeTimer); + writeTimer = NULL; + } + + currentWrite = nil; +} + +- (void)setupWriteTimerWithTimeout:(NSTimeInterval)timeout +{ + if (timeout >= 0.0) + { + writeTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(writeTimer, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + [strongSelf doWriteTimeout]; + + #pragma clang diagnostic pop + }}); + + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theWriteTimer = writeTimer; + dispatch_source_set_cancel_handler(writeTimer, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"dispatch_release(writeTimer)"); + dispatch_release(theWriteTimer); + + #pragma clang diagnostic pop + }); + #endif + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); + + dispatch_source_set_timer(writeTimer, tt, DISPATCH_TIME_FOREVER, 0); + dispatch_resume(writeTimer); + } +} + +- (void)doWriteTimeout +{ + // This is a little bit tricky. + // Ideally we'd like to synchronously query the delegate about a timeout extension. + // But if we do so synchronously we risk a possible deadlock. + // So instead we have to do so asynchronously, and callback to ourselves from within the delegate block. + + flags |= kWritesPaused; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:shouldTimeoutWriteWithTag:elapsed:bytesDone:)]) + { + GCDAsyncWritePacket *theWrite = currentWrite; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + NSTimeInterval timeoutExtension = 0.0; + + timeoutExtension = [theDelegate socket:self shouldTimeoutWriteWithTag:theWrite->tag + elapsed:theWrite->timeout + bytesDone:theWrite->bytesDone]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self doWriteTimeoutWithExtension:timeoutExtension]; + }}); + }}); + } + else + { + [self doWriteTimeoutWithExtension:0.0]; + } +} + +- (void)doWriteTimeoutWithExtension:(NSTimeInterval)timeoutExtension +{ + if (currentWrite) + { + if (timeoutExtension > 0.0) + { + currentWrite->timeout += timeoutExtension; + + // Reschedule the timer + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeoutExtension * NSEC_PER_SEC)); + dispatch_source_set_timer(writeTimer, tt, DISPATCH_TIME_FOREVER, 0); + + // Unpause writes, and continue + flags &= ~kWritesPaused; + [self doWriteData]; + } + else + { + LogVerbose(@"WriteTimeout"); + + [self closeWithError:[self writeTimeoutError]]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Security +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)startTLS:(NSDictionary *)tlsSettings +{ + LogTrace(); + + if (tlsSettings == nil) + { + // Passing nil/NULL to CFReadStreamSetProperty will appear to work the same as passing an empty dictionary, + // but causes problems if we later try to fetch the remote host's certificate. + // + // To be exact, it causes the following to return NULL instead of the normal result: + // CFReadStreamCopyProperty(readStream, kCFStreamPropertySSLPeerCertificates) + // + // So we use an empty dictionary instead, which works perfectly. + + tlsSettings = [NSDictionary dictionary]; + } + + GCDAsyncSpecialPacket *packet = [[GCDAsyncSpecialPacket alloc] initWithTLSSettings:tlsSettings]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + if ((flags & kSocketStarted) && !(flags & kQueuedTLS) && !(flags & kForbidReadsWrites)) + { + [readQueue addObject:packet]; + [writeQueue addObject:packet]; + + flags |= kQueuedTLS; + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; + } + }}); + +} + +- (void)maybeStartTLS +{ + // We can't start TLS until: + // - All queued reads prior to the user calling startTLS are complete + // - All queued writes prior to the user calling startTLS are complete + // + // We'll know these conditions are met when both kStartingReadTLS and kStartingWriteTLS are set + + if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) + { + BOOL useSecureTransport = YES; + + #if TARGET_OS_IPHONE + { + GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; + NSDictionary *tlsSettings = tlsPacket->tlsSettings; + + NSNumber *value = [tlsSettings objectForKey:GCDAsyncSocketUseCFStreamForTLS]; + if (value && [value boolValue] == YES) + useSecureTransport = NO; + } + #endif + + if (useSecureTransport) + { + [self ssl_startTLS]; + } + else + { + #if TARGET_OS_IPHONE + [self cf_startTLS]; + #endif + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Security via SecureTransport +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (OSStatus)sslReadWithBuffer:(void *)buffer length:(size_t *)bufferLength +{ + LogVerbose(@"sslReadWithBuffer:%p length:%lu", buffer, (unsigned long)*bufferLength); + + if ((socketFDBytesAvailable == 0) && ([sslPreBuffer availableBytes] == 0)) + { + LogVerbose(@"%@ - No data available to read...", THIS_METHOD); + + // No data available to read. + // + // Need to wait for readSource to fire and notify us of + // available data in the socket's internal read buffer. + + [self resumeReadSource]; + + *bufferLength = 0; + return errSSLWouldBlock; + } + + size_t totalBytesRead = 0; + size_t totalBytesLeftToBeRead = *bufferLength; + + BOOL done = NO; + BOOL socketError = NO; + + // + // STEP 1 : READ FROM SSL PRE BUFFER + // + + size_t sslPreBufferLength = [sslPreBuffer availableBytes]; + + if (sslPreBufferLength > 0) + { + LogVerbose(@"%@: Reading from SSL pre buffer...", THIS_METHOD); + + size_t bytesToCopy; + if (sslPreBufferLength > totalBytesLeftToBeRead) + bytesToCopy = totalBytesLeftToBeRead; + else + bytesToCopy = sslPreBufferLength; + + LogVerbose(@"%@: Copying %zu bytes from sslPreBuffer", THIS_METHOD, bytesToCopy); + + memcpy(buffer, [sslPreBuffer readBuffer], bytesToCopy); + [sslPreBuffer didRead:bytesToCopy]; + + LogVerbose(@"%@: sslPreBuffer.length = %zu", THIS_METHOD, [sslPreBuffer availableBytes]); + + totalBytesRead += bytesToCopy; + totalBytesLeftToBeRead -= bytesToCopy; + + done = (totalBytesLeftToBeRead == 0); + + if (done) LogVerbose(@"%@: Complete", THIS_METHOD); + } + + // + // STEP 2 : READ FROM SOCKET + // + + if (!done && (socketFDBytesAvailable > 0)) + { + LogVerbose(@"%@: Reading from socket...", THIS_METHOD); + + int socketFD = (socket6FD == SOCKET_NULL) ? socket4FD : socket6FD; + + BOOL readIntoPreBuffer; + size_t bytesToRead; + uint8_t *buf; + + if (socketFDBytesAvailable > totalBytesLeftToBeRead) + { + // Read all available data from socket into sslPreBuffer. + // Then copy requested amount into dataBuffer. + + LogVerbose(@"%@: Reading into sslPreBuffer...", THIS_METHOD); + + [sslPreBuffer ensureCapacityForWrite:socketFDBytesAvailable]; + + readIntoPreBuffer = YES; + bytesToRead = (size_t)socketFDBytesAvailable; + buf = [sslPreBuffer writeBuffer]; + } + else + { + // Read available data from socket directly into dataBuffer. + + LogVerbose(@"%@: Reading directly into dataBuffer...", THIS_METHOD); + + readIntoPreBuffer = NO; + bytesToRead = totalBytesLeftToBeRead; + buf = (uint8_t *)buffer + totalBytesRead; + } + + ssize_t result = read(socketFD, buf, bytesToRead); + LogVerbose(@"%@: read from socket = %zd", THIS_METHOD, result); + + if (result < 0) + { + LogVerbose(@"%@: read errno = %i", THIS_METHOD, errno); + + if (errno != EWOULDBLOCK) + { + socketError = YES; + } + + socketFDBytesAvailable = 0; + } + else if (result == 0) + { + LogVerbose(@"%@: read EOF", THIS_METHOD); + + socketError = YES; + socketFDBytesAvailable = 0; + } + else + { + size_t bytesReadFromSocket = result; + + if (socketFDBytesAvailable > bytesReadFromSocket) + socketFDBytesAvailable -= bytesReadFromSocket; + else + socketFDBytesAvailable = 0; + + if (readIntoPreBuffer) + { + [sslPreBuffer didWrite:bytesReadFromSocket]; + + size_t bytesToCopy = MIN(totalBytesLeftToBeRead, bytesReadFromSocket); + + LogVerbose(@"%@: Copying %zu bytes out of sslPreBuffer", THIS_METHOD, bytesToCopy); + + memcpy((uint8_t *)buffer + totalBytesRead, [sslPreBuffer readBuffer], bytesToCopy); + [sslPreBuffer didRead:bytesToCopy]; + + totalBytesRead += bytesToCopy; + totalBytesLeftToBeRead -= bytesToCopy; + + LogVerbose(@"%@: sslPreBuffer.length = %zu", THIS_METHOD, [sslPreBuffer availableBytes]); + } + else + { + totalBytesRead += bytesReadFromSocket; + totalBytesLeftToBeRead -= bytesReadFromSocket; + } + + done = (totalBytesLeftToBeRead == 0); + + if (done) LogVerbose(@"%@: Complete", THIS_METHOD); + } + } + + *bufferLength = totalBytesRead; + + if (done) + return noErr; + + if (socketError) + return errSSLClosedAbort; + + return errSSLWouldBlock; +} + +- (OSStatus)sslWriteWithBuffer:(const void *)buffer length:(size_t *)bufferLength +{ + if (!(flags & kSocketCanAcceptBytes)) + { + // Unable to write. + // + // Need to wait for writeSource to fire and notify us of + // available space in the socket's internal write buffer. + + [self resumeWriteSource]; + + *bufferLength = 0; + return errSSLWouldBlock; + } + + size_t bytesToWrite = *bufferLength; + size_t bytesWritten = 0; + + BOOL done = NO; + BOOL socketError = NO; + + int socketFD = (socket4FD == SOCKET_NULL) ? socket6FD : socket4FD; + + ssize_t result = write(socketFD, buffer, bytesToWrite); + + if (result < 0) + { + if (errno != EWOULDBLOCK) + { + socketError = YES; + } + + flags &= ~kSocketCanAcceptBytes; + } + else if (result == 0) + { + flags &= ~kSocketCanAcceptBytes; + } + else + { + bytesWritten = result; + + done = (bytesWritten == bytesToWrite); + } + + *bufferLength = bytesWritten; + + if (done) + return noErr; + + if (socketError) + return errSSLClosedAbort; + + return errSSLWouldBlock; +} + +static OSStatus SSLReadFunction(SSLConnectionRef connection, void *data, size_t *dataLength) +{ + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection; + + NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?"); + + return [asyncSocket sslReadWithBuffer:data length:dataLength]; +} + +static OSStatus SSLWriteFunction(SSLConnectionRef connection, const void *data, size_t *dataLength) +{ + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection; + + NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?"); + + return [asyncSocket sslWriteWithBuffer:data length:dataLength]; +} + +- (void)ssl_startTLS +{ + LogTrace(); + + LogVerbose(@"Starting TLS (via SecureTransport)..."); + + OSStatus status; + + GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; + if (tlsPacket == nil) // Code to quiet the analyzer + { + NSAssert(NO, @"Logic error"); + + [self closeWithError:[self otherError:@"Logic error"]]; + return; + } + NSDictionary *tlsSettings = tlsPacket->tlsSettings; + + // Create SSLContext, and setup IO callbacks and connection ref + + BOOL isServer = [[tlsSettings objectForKey:(NSString *)kCFStreamSSLIsServer] boolValue]; + + #if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1080) + { + if (isServer) + sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLServerSide, kSSLStreamType); + else + sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType); + + if (sslContext == NULL) + { + [self closeWithError:[self otherError:@"Error in SSLCreateContext"]]; + return; + } + } + #else // (__MAC_OS_X_VERSION_MIN_REQUIRED < 1080) + { + status = SSLNewContext(isServer, &sslContext); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLNewContext"]]; + return; + } + } + #endif + + status = SSLSetIOFuncs(sslContext, &SSLReadFunction, &SSLWriteFunction); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetIOFuncs"]]; + return; + } + + status = SSLSetConnection(sslContext, (__bridge SSLConnectionRef)self); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetConnection"]]; + return; + } + + + BOOL shouldManuallyEvaluateTrust = [[tlsSettings objectForKey:GCDAsyncSocketManuallyEvaluateTrust] boolValue]; + if (shouldManuallyEvaluateTrust) + { + if (isServer) + { + [self closeWithError:[self otherError:@"Manual trust validation is not supported for server sockets"]]; + return; + } + + status = SSLSetSessionOption(sslContext, kSSLSessionOptionBreakOnServerAuth, true); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetSessionOption"]]; + return; + } + + #if !TARGET_OS_IPHONE && (__MAC_OS_X_VERSION_MIN_REQUIRED < 1080) + + // Note from Apple's documentation: + // + // It is only necessary to call SSLSetEnableCertVerify on the Mac prior to OS X 10.8. + // On OS X 10.8 and later setting kSSLSessionOptionBreakOnServerAuth always disables the + // built-in trust evaluation. All versions of iOS behave like OS X 10.8 and thus + // SSLSetEnableCertVerify is not available on that platform at all. + + status = SSLSetEnableCertVerify(sslContext, NO); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetEnableCertVerify"]]; + return; + } + + #endif + } + + // Configure SSLContext from given settings + // + // Checklist: + // 1. kCFStreamSSLPeerName + // 2. kCFStreamSSLCertificates + // 3. GCDAsyncSocketSSLPeerID + // 4. GCDAsyncSocketSSLProtocolVersionMin + // 5. GCDAsyncSocketSSLProtocolVersionMax + // 6. GCDAsyncSocketSSLSessionOptionFalseStart + // 7. GCDAsyncSocketSSLSessionOptionSendOneByteRecord + // 8. GCDAsyncSocketSSLCipherSuites + // 9. GCDAsyncSocketSSLDiffieHellmanParameters (Mac) + // + // Deprecated (throw error): + // 10. kCFStreamSSLAllowsAnyRoot + // 11. kCFStreamSSLAllowsExpiredRoots + // 12. kCFStreamSSLAllowsExpiredCertificates + // 13. kCFStreamSSLValidatesCertificateChain + // 14. kCFStreamSSLLevel + + id value; + + // 1. kCFStreamSSLPeerName + + value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLPeerName]; + if ([value isKindOfClass:[NSString class]]) + { + NSString *peerName = (NSString *)value; + + const char *peer = [peerName UTF8String]; + size_t peerLen = strlen(peer); + + status = SSLSetPeerDomainName(sslContext, peer, peerLen); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetPeerDomainName"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for kCFStreamSSLPeerName. Value must be of type NSString."); + + [self closeWithError:[self otherError:@"Invalid value for kCFStreamSSLPeerName."]]; + return; + } + + // 2. kCFStreamSSLCertificates + + value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLCertificates]; + if ([value isKindOfClass:[NSArray class]]) + { + CFArrayRef certs = (__bridge CFArrayRef)value; + + status = SSLSetCertificate(sslContext, certs); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetCertificate"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for kCFStreamSSLCertificates. Value must be of type NSArray."); + + [self closeWithError:[self otherError:@"Invalid value for kCFStreamSSLCertificates."]]; + return; + } + + // 3. GCDAsyncSocketSSLPeerID + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLPeerID]; + if ([value isKindOfClass:[NSData class]]) + { + NSData *peerIdData = (NSData *)value; + + status = SSLSetPeerID(sslContext, [peerIdData bytes], [peerIdData length]); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetPeerID"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLPeerID. Value must be of type NSData." + @" (You can convert strings to data using a method like" + @" [string dataUsingEncoding:NSUTF8StringEncoding])"); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLPeerID."]]; + return; + } + + // 4. GCDAsyncSocketSSLProtocolVersionMin + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLProtocolVersionMin]; + if ([value isKindOfClass:[NSNumber class]]) + { + SSLProtocol minProtocol = (SSLProtocol)[(NSNumber *)value intValue]; + if (minProtocol != kSSLProtocolUnknown) + { + status = SSLSetProtocolVersionMin(sslContext, minProtocol); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetProtocolVersionMin"]]; + return; + } + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLProtocolVersionMin. Value must be of type NSNumber."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLProtocolVersionMin."]]; + return; + } + + // 5. GCDAsyncSocketSSLProtocolVersionMax + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLProtocolVersionMax]; + if ([value isKindOfClass:[NSNumber class]]) + { + SSLProtocol maxProtocol = (SSLProtocol)[(NSNumber *)value intValue]; + if (maxProtocol != kSSLProtocolUnknown) + { + status = SSLSetProtocolVersionMax(sslContext, maxProtocol); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetProtocolVersionMax"]]; + return; + } + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLProtocolVersionMax. Value must be of type NSNumber."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLProtocolVersionMax."]]; + return; + } + + // 6. GCDAsyncSocketSSLSessionOptionFalseStart + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLSessionOptionFalseStart]; + if ([value isKindOfClass:[NSNumber class]]) + { + status = SSLSetSessionOption(sslContext, kSSLSessionOptionFalseStart, [value boolValue]); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetSessionOption (kSSLSessionOptionFalseStart)"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLSessionOptionFalseStart. Value must be of type NSNumber."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLSessionOptionFalseStart."]]; + return; + } + + // 7. GCDAsyncSocketSSLSessionOptionSendOneByteRecord + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLSessionOptionSendOneByteRecord]; + if ([value isKindOfClass:[NSNumber class]]) + { + status = SSLSetSessionOption(sslContext, kSSLSessionOptionSendOneByteRecord, [value boolValue]); + if (status != noErr) + { + [self closeWithError: + [self otherError:@"Error in SSLSetSessionOption (kSSLSessionOptionSendOneByteRecord)"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLSessionOptionSendOneByteRecord." + @" Value must be of type NSNumber."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLSessionOptionSendOneByteRecord."]]; + return; + } + + // 8. GCDAsyncSocketSSLCipherSuites + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLCipherSuites]; + if ([value isKindOfClass:[NSArray class]]) + { + NSArray *cipherSuites = (NSArray *)value; + NSUInteger numberCiphers = [cipherSuites count]; + SSLCipherSuite ciphers[numberCiphers]; + + NSUInteger cipherIndex; + for (cipherIndex = 0; cipherIndex < numberCiphers; cipherIndex++) + { + NSNumber *cipherObject = [cipherSuites objectAtIndex:cipherIndex]; + ciphers[cipherIndex] = [cipherObject shortValue]; + } + + status = SSLSetEnabledCiphers(sslContext, ciphers, numberCiphers); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetEnabledCiphers"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLCipherSuites. Value must be of type NSArray."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLCipherSuites."]]; + return; + } + + // 9. GCDAsyncSocketSSLDiffieHellmanParameters + + #if !TARGET_OS_IPHONE + value = [tlsSettings objectForKey:GCDAsyncSocketSSLDiffieHellmanParameters]; + if ([value isKindOfClass:[NSData class]]) + { + NSData *diffieHellmanData = (NSData *)value; + + status = SSLSetDiffieHellmanParams(sslContext, [diffieHellmanData bytes], [diffieHellmanData length]); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetDiffieHellmanParams"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLDiffieHellmanParameters. Value must be of type NSData."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLDiffieHellmanParameters."]]; + return; + } + #endif + + // DEPRECATED checks + + // 10. kCFStreamSSLAllowsAnyRoot + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLAllowsAnyRoot]; + #pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsAnyRoot" + @" - You must use manual trust evaluation"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsAnyRoot"]]; + return; + } + + // 11. kCFStreamSSLAllowsExpiredRoots + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLAllowsExpiredRoots]; + #pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsExpiredRoots" + @" - You must use manual trust evaluation"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsExpiredRoots"]]; + return; + } + + // 12. kCFStreamSSLValidatesCertificateChain + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLValidatesCertificateChain]; + #pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLValidatesCertificateChain" + @" - You must use manual trust evaluation"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLValidatesCertificateChain"]]; + return; + } + + // 13. kCFStreamSSLAllowsExpiredCertificates + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLAllowsExpiredCertificates]; + #pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsExpiredCertificates" + @" - You must use manual trust evaluation"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsExpiredCertificates"]]; + return; + } + + // 14. kCFStreamSSLLevel + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLLevel]; + #pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLLevel" + @" - You must use GCDAsyncSocketSSLProtocolVersionMin & GCDAsyncSocketSSLProtocolVersionMax"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLLevel"]]; + return; + } + + // Setup the sslPreBuffer + // + // Any data in the preBuffer needs to be moved into the sslPreBuffer, + // as this data is now part of the secure read stream. + + sslPreBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)]; + + size_t preBufferLength = [preBuffer availableBytes]; + + if (preBufferLength > 0) + { + [sslPreBuffer ensureCapacityForWrite:preBufferLength]; + + memcpy([sslPreBuffer writeBuffer], [preBuffer readBuffer], preBufferLength); + [preBuffer didRead:preBufferLength]; + [sslPreBuffer didWrite:preBufferLength]; + } + + sslErrCode = noErr; + + // Start the SSL Handshake process + + [self ssl_continueSSLHandshake]; +} + +- (void)ssl_continueSSLHandshake +{ + LogTrace(); + + // If the return value is noErr, the session is ready for normal secure communication. + // If the return value is errSSLWouldBlock, the SSLHandshake function must be called again. + // If the return value is errSSLServerAuthCompleted, we ask delegate if we should trust the + // server and then call SSLHandshake again to resume the handshake or close the connection + // errSSLPeerBadCert SSL error. + // Otherwise, the return value indicates an error code. + + OSStatus status = SSLHandshake(sslContext); + + if (status == noErr) + { + LogVerbose(@"SSLHandshake complete"); + + flags &= ~kStartingReadTLS; + flags &= ~kStartingWriteTLS; + + flags |= kSocketSecure; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidSecure:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidSecure:self]; + }}); + } + + [self endCurrentRead]; + [self endCurrentWrite]; + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; + } + else if (status == errSSLPeerAuthCompleted) + { + LogVerbose(@"SSLHandshake peerAuthCompleted - awaiting delegate approval"); + + __block SecTrustRef trust = NULL; + status = SSLCopyPeerTrust(sslContext, &trust); + if (status != noErr) + { + [self closeWithError:[self sslError:status]]; + return; + } + + int aStateIndex = stateIndex; + dispatch_queue_t theSocketQueue = socketQueue; + + __weak GCDAsyncSocket *weakSelf = self; + + void (^comletionHandler)(BOOL) = ^(BOOL shouldTrust){ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + dispatch_async(theSocketQueue, ^{ @autoreleasepool { + + if (trust) { + CFRelease(trust); + trust = NULL; + } + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf) + { + [strongSelf ssl_shouldTrustPeer:shouldTrust stateIndex:aStateIndex]; + } + }}); + + #pragma clang diagnostic pop + }}; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReceiveTrust:completionHandler:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didReceiveTrust:trust completionHandler:comletionHandler]; + }}); + } + else + { + if (trust) { + CFRelease(trust); + trust = NULL; + } + + NSString *msg = @"GCDAsyncSocketManuallyEvaluateTrust specified in tlsSettings," + @" but delegate doesn't implement socket:shouldTrustPeer:"; + + [self closeWithError:[self otherError:msg]]; + return; + } + } + else if (status == errSSLWouldBlock) + { + LogVerbose(@"SSLHandshake continues..."); + + // Handshake continues... + // + // This method will be called again from doReadData or doWriteData. + } + else + { + [self closeWithError:[self sslError:status]]; + } +} + +- (void)ssl_shouldTrustPeer:(BOOL)shouldTrust stateIndex:(int)aStateIndex +{ + LogTrace(); + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring ssl_shouldTrustPeer - invalid state (maybe disconnected)"); + + // One of the following is true + // - the socket was disconnected + // - the startTLS operation timed out + // - the completionHandler was already invoked once + + return; + } + + // Increment stateIndex to ensure completionHandler can only be called once. + stateIndex++; + + if (shouldTrust) + { + [self ssl_continueSSLHandshake]; + } + else + { + [self closeWithError:[self sslError:errSSLPeerBadCert]]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Security via CFStream +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if TARGET_OS_IPHONE + +- (void)cf_finishSSLHandshake +{ + LogTrace(); + + if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) + { + flags &= ~kStartingReadTLS; + flags &= ~kStartingWriteTLS; + + flags |= kSocketSecure; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidSecure:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidSecure:self]; + }}); + } + + [self endCurrentRead]; + [self endCurrentWrite]; + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; + } +} + +- (void)cf_abortSSLHandshake:(NSError *)error +{ + LogTrace(); + + if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) + { + flags &= ~kStartingReadTLS; + flags &= ~kStartingWriteTLS; + + [self closeWithError:error]; + } +} + +- (void)cf_startTLS +{ + LogTrace(); + + LogVerbose(@"Starting TLS (via CFStream)..."); + + if ([preBuffer availableBytes] > 0) + { + NSString *msg = @"Invalid TLS transition. Handshake has already been read from socket."; + + [self closeWithError:[self otherError:msg]]; + return; + } + + [self suspendReadSource]; + [self suspendWriteSource]; + + socketFDBytesAvailable = 0; + flags &= ~kSocketCanAcceptBytes; + flags &= ~kSecureSocketHasBytesAvailable; + + flags |= kUsingCFStreamForTLS; + + if (![self createReadAndWriteStream]) + { + [self closeWithError:[self otherError:@"Error in CFStreamCreatePairWithSocket"]]; + return; + } + + if (![self registerForStreamCallbacksIncludingReadWrite:YES]) + { + [self closeWithError:[self otherError:@"Error in CFStreamSetClient"]]; + return; + } + + if (![self addStreamsToRunLoop]) + { + [self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]]; + return; + } + + NSAssert([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid read packet for startTLS"); + NSAssert([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid write packet for startTLS"); + + GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; + CFDictionaryRef tlsSettings = (__bridge CFDictionaryRef)tlsPacket->tlsSettings; + + // Getting an error concerning kCFStreamPropertySSLSettings ? + // You need to add the CFNetwork framework to your iOS application. + + BOOL r1 = CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, tlsSettings); + BOOL r2 = CFWriteStreamSetProperty(writeStream, kCFStreamPropertySSLSettings, tlsSettings); + + // For some reason, starting around the time of iOS 4.3, + // the first call to set the kCFStreamPropertySSLSettings will return true, + // but the second will return false. + // + // Order doesn't seem to matter. + // So you could call CFReadStreamSetProperty and then CFWriteStreamSetProperty, or you could reverse the order. + // Either way, the first call will return true, and the second returns false. + // + // Interestingly, this doesn't seem to affect anything. + // Which is not altogether unusual, as the documentation seems to suggest that (for many settings) + // setting it on one side of the stream automatically sets it for the other side of the stream. + // + // Although there isn't anything in the documentation to suggest that the second attempt would fail. + // + // Furthermore, this only seems to affect streams that are negotiating a security upgrade. + // In other words, the socket gets connected, there is some back-and-forth communication over the unsecure + // connection, and then a startTLS is issued. + // So this mostly affects newer protocols (XMPP, IMAP) as opposed to older protocols (HTTPS). + + if (!r1 && !r2) // Yes, the && is correct - workaround for apple bug. + { + [self closeWithError:[self otherError:@"Error in CFStreamSetProperty"]]; + return; + } + + if (![self openStreams]) + { + [self closeWithError:[self otherError:@"Error in CFStreamOpen"]]; + return; + } + + LogVerbose(@"Waiting for SSL Handshake to complete..."); +} + +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark CFStream +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if TARGET_OS_IPHONE + ++ (void)ignore:(id)_ +{} + ++ (void)startCFStreamThreadIfNeeded +{ + LogTrace(); + + static dispatch_once_t predicate; + dispatch_once(&predicate, ^{ + + cfstreamThreadRetainCount = 0; + cfstreamThreadSetupQueue = dispatch_queue_create("GCDAsyncSocket-CFStreamThreadSetup", DISPATCH_QUEUE_SERIAL); + }); + + dispatch_sync(cfstreamThreadSetupQueue, ^{ @autoreleasepool { + + if (++cfstreamThreadRetainCount == 1) + { + cfstreamThread = [[NSThread alloc] initWithTarget:self + selector:@selector(cfstreamThread) + object:nil]; + [cfstreamThread start]; + } + }}); +} + ++ (void)stopCFStreamThreadIfNeeded +{ + LogTrace(); + + // The creation of the cfstreamThread is relatively expensive. + // So we'd like to keep it available for recycling. + // However, there's a tradeoff here, because it shouldn't remain alive forever. + // So what we're going to do is use a little delay before taking it down. + // This way it can be reused properly in situations where multiple sockets are continually in flux. + + int delayInSeconds = 30; + dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); + dispatch_after(when, cfstreamThreadSetupQueue, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + if (cfstreamThreadRetainCount == 0) + { + LogWarn(@"Logic error concerning cfstreamThread start / stop"); + return_from_block; + } + + if (--cfstreamThreadRetainCount == 0) + { + [cfstreamThread cancel]; // set isCancelled flag + + // wake up the thread + [GCDAsyncSocket performSelector:@selector(ignore:) + onThread:cfstreamThread + withObject:[NSNull null] + waitUntilDone:NO]; + + cfstreamThread = nil; + } + + #pragma clang diagnostic pop + }}); +} + ++ (void)cfstreamThread { @autoreleasepool +{ + [[NSThread currentThread] setName:GCDAsyncSocketThreadName]; + + LogInfo(@"CFStreamThread: Started"); + + // We can't run the run loop unless it has an associated input source or a timer. + // So we'll just create a timer that will never fire - unless the server runs for decades. + [NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow] + target:self + selector:@selector(ignore:) + userInfo:nil + repeats:YES]; + + NSThread *currentThread = [NSThread currentThread]; + NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop]; + + BOOL isCancelled = [currentThread isCancelled]; + + while (!isCancelled && [currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) + { + isCancelled = [currentThread isCancelled]; + } + + LogInfo(@"CFStreamThread: Stopped"); +}} + ++ (void)scheduleCFStreams:(GCDAsyncSocket *)asyncSocket +{ + LogTrace(); + NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread"); + + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); + + if (asyncSocket->readStream) + CFReadStreamScheduleWithRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode); + + if (asyncSocket->writeStream) + CFWriteStreamScheduleWithRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode); +} + ++ (void)unscheduleCFStreams:(GCDAsyncSocket *)asyncSocket +{ + LogTrace(); + NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread"); + + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); + + if (asyncSocket->readStream) + CFReadStreamUnscheduleFromRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode); + + if (asyncSocket->writeStream) + CFWriteStreamUnscheduleFromRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode); +} + +static void CFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type, void *pInfo) +{ + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)pInfo; + + switch(type) + { + case kCFStreamEventHasBytesAvailable: + { + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFReadStreamCallback - HasBytesAvailable"); + + if (asyncSocket->readStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + // If we set kCFStreamPropertySSLSettings before we opened the streams, this might be a lie. + // (A callback related to the tcp stream, but not to the SSL layer). + + if (CFReadStreamHasBytesAvailable(asyncSocket->readStream)) + { + asyncSocket->flags |= kSecureSocketHasBytesAvailable; + [asyncSocket cf_finishSSLHandshake]; + } + } + else + { + asyncSocket->flags |= kSecureSocketHasBytesAvailable; + [asyncSocket doReadData]; + } + }}); + + break; + } + default: + { + NSError *error = (__bridge_transfer NSError *)CFReadStreamCopyError(stream); + + if (error == nil && type == kCFStreamEventEndEncountered) + { + error = [asyncSocket connectionClosedError]; + } + + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFReadStreamCallback - Other"); + + if (asyncSocket->readStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + [asyncSocket cf_abortSSLHandshake:error]; + } + else + { + [asyncSocket closeWithError:error]; + } + }}); + + break; + } + } + +} + +static void CFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType type, void *pInfo) +{ + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)pInfo; + + switch(type) + { + case kCFStreamEventCanAcceptBytes: + { + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFWriteStreamCallback - CanAcceptBytes"); + + if (asyncSocket->writeStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + // If we set kCFStreamPropertySSLSettings before we opened the streams, this might be a lie. + // (A callback related to the tcp stream, but not to the SSL layer). + + if (CFWriteStreamCanAcceptBytes(asyncSocket->writeStream)) + { + asyncSocket->flags |= kSocketCanAcceptBytes; + [asyncSocket cf_finishSSLHandshake]; + } + } + else + { + asyncSocket->flags |= kSocketCanAcceptBytes; + [asyncSocket doWriteData]; + } + }}); + + break; + } + default: + { + NSError *error = (__bridge_transfer NSError *)CFWriteStreamCopyError(stream); + + if (error == nil && type == kCFStreamEventEndEncountered) + { + error = [asyncSocket connectionClosedError]; + } + + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFWriteStreamCallback - Other"); + + if (asyncSocket->writeStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + [asyncSocket cf_abortSSLHandshake:error]; + } + else + { + [asyncSocket closeWithError:error]; + } + }}); + + break; + } + } + +} + +- (BOOL)createReadAndWriteStream +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + if (readStream || writeStream) + { + // Streams already created + return YES; + } + + int socketFD = (socket6FD == SOCKET_NULL) ? socket4FD : socket6FD; + + if (socketFD == SOCKET_NULL) + { + // Cannot create streams without a file descriptor + return NO; + } + + if (![self isConnected]) + { + // Cannot create streams until file descriptor is connected + return NO; + } + + LogVerbose(@"Creating read and write stream..."); + + CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socketFD, &readStream, &writeStream); + + // The kCFStreamPropertyShouldCloseNativeSocket property should be false by default (for our case). + // But let's not take any chances. + + if (readStream) + CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); + if (writeStream) + CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); + + if ((readStream == NULL) || (writeStream == NULL)) + { + LogWarn(@"Unable to create read and write stream..."); + + if (readStream) + { + CFReadStreamClose(readStream); + CFRelease(readStream); + readStream = NULL; + } + if (writeStream) + { + CFWriteStreamClose(writeStream); + CFRelease(writeStream); + writeStream = NULL; + } + + return NO; + } + + return YES; +} + +- (BOOL)registerForStreamCallbacksIncludingReadWrite:(BOOL)includeReadWrite +{ + LogVerbose(@"%@ %@", THIS_METHOD, (includeReadWrite ? @"YES" : @"NO")); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); + + streamContext.version = 0; + streamContext.info = (__bridge void *)(self); + streamContext.retain = nil; + streamContext.release = nil; + streamContext.copyDescription = nil; + + CFOptionFlags readStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; + if (includeReadWrite) + readStreamEvents |= kCFStreamEventHasBytesAvailable; + + if (!CFReadStreamSetClient(readStream, readStreamEvents, &CFReadStreamCallback, &streamContext)) + { + return NO; + } + + CFOptionFlags writeStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; + if (includeReadWrite) + writeStreamEvents |= kCFStreamEventCanAcceptBytes; + + if (!CFWriteStreamSetClient(writeStream, writeStreamEvents, &CFWriteStreamCallback, &streamContext)) + { + return NO; + } + + return YES; +} + +- (BOOL)addStreamsToRunLoop +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); + + if (!(flags & kAddedStreamsToRunLoop)) + { + LogVerbose(@"Adding streams to runloop..."); + + [[self class] startCFStreamThreadIfNeeded]; + [[self class] performSelector:@selector(scheduleCFStreams:) + onThread:cfstreamThread + withObject:self + waitUntilDone:YES]; + + flags |= kAddedStreamsToRunLoop; + } + + return YES; +} + +- (void)removeStreamsFromRunLoop +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); + + if (flags & kAddedStreamsToRunLoop) + { + LogVerbose(@"Removing streams from runloop..."); + + [[self class] performSelector:@selector(unscheduleCFStreams:) + onThread:cfstreamThread + withObject:self + waitUntilDone:YES]; + [[self class] stopCFStreamThreadIfNeeded]; + + flags &= ~kAddedStreamsToRunLoop; + } +} + +- (BOOL)openStreams +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); + + CFStreamStatus readStatus = CFReadStreamGetStatus(readStream); + CFStreamStatus writeStatus = CFWriteStreamGetStatus(writeStream); + + if ((readStatus == kCFStreamStatusNotOpen) || (writeStatus == kCFStreamStatusNotOpen)) + { + LogVerbose(@"Opening read and write stream..."); + + BOOL r1 = CFReadStreamOpen(readStream); + BOOL r2 = CFWriteStreamOpen(writeStream); + + if (!r1 || !r2) + { + LogError(@"Error in CFStreamOpen"); + return NO; + } + } + + return YES; +} + +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Advanced +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * See header file for big discussion of this method. +**/ +- (BOOL)autoDisconnectOnClosedReadStream +{ + // Note: YES means kAllowHalfDuplexConnection is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kAllowHalfDuplexConnection) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((config & kAllowHalfDuplexConnection) == 0); + }); + + return result; + } +} + +/** + * See header file for big discussion of this method. +**/ +- (void)setAutoDisconnectOnClosedReadStream:(BOOL)flag +{ + // Note: YES means kAllowHalfDuplexConnection is OFF + + dispatch_block_t block = ^{ + + if (flag) + config &= ~kAllowHalfDuplexConnection; + else + config |= kAllowHalfDuplexConnection; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + + +/** + * See header file for big discussion of this method. +**/ +- (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketNewTargetQueue +{ + void *nonNullUnusedPointer = (__bridge void *)self; + dispatch_queue_set_specific(socketNewTargetQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); +} + +/** + * See header file for big discussion of this method. +**/ +- (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketOldTargetQueue +{ + dispatch_queue_set_specific(socketOldTargetQueue, IsOnSocketQueueOrTargetQueueKey, NULL, NULL); +} + +/** + * See header file for big discussion of this method. +**/ +- (void)performBlock:(dispatch_block_t)block +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); +} + +/** + * Questions? Have you read the header file? +**/ +- (int)socketFD +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return SOCKET_NULL; + } + + if (socket4FD != SOCKET_NULL) + return socket4FD; + else + return socket6FD; +} + +/** + * Questions? Have you read the header file? +**/ +- (int)socket4FD +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return SOCKET_NULL; + } + + return socket4FD; +} + +/** + * Questions? Have you read the header file? +**/ +- (int)socket6FD +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return SOCKET_NULL; + } + + return socket6FD; +} + +#if TARGET_OS_IPHONE + +/** + * Questions? Have you read the header file? +**/ +- (CFReadStreamRef)readStream +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NULL; + } + + if (readStream == NULL) + [self createReadAndWriteStream]; + + return readStream; +} + +/** + * Questions? Have you read the header file? +**/ +- (CFWriteStreamRef)writeStream +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NULL; + } + + if (writeStream == NULL) + [self createReadAndWriteStream]; + + return writeStream; +} + +- (BOOL)enableBackgroundingOnSocketWithCaveat:(BOOL)caveat +{ + if (![self createReadAndWriteStream]) + { + // Error occured creating streams (perhaps socket isn't open) + return NO; + } + + BOOL r1, r2; + + LogVerbose(@"Enabling backgrouding on socket"); + + r1 = CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + r2 = CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + + if (!r1 || !r2) + { + return NO; + } + + if (!caveat) + { + if (![self openStreams]) + { + return NO; + } + } + + return YES; +} + +/** + * Questions? Have you read the header file? +**/ +- (BOOL)enableBackgroundingOnSocket +{ + LogTrace(); + + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NO; + } + + return [self enableBackgroundingOnSocketWithCaveat:NO]; +} + +- (BOOL)enableBackgroundingOnSocketWithCaveat // Deprecated in iOS 4.??? +{ + // This method was created as a workaround for a bug in iOS. + // Apple has since fixed this bug. + // I'm not entirely sure which version of iOS they fixed it in... + + LogTrace(); + + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NO; + } + + return [self enableBackgroundingOnSocketWithCaveat:YES]; +} + +#endif + +- (SSLContextRef)sslContext +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NULL; + } + + return sslContext; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Class Utilities +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ++ (NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSError **)errPtr +{ + LogTrace(); + + NSMutableArray *addresses = nil; + NSError *error = nil; + + if ([host isEqualToString:@"localhost"] || [host isEqualToString:@"loopback"]) + { + // Use LOOPBACK address + struct sockaddr_in nativeAddr4; + nativeAddr4.sin_len = sizeof(struct sockaddr_in); + nativeAddr4.sin_family = AF_INET; + nativeAddr4.sin_port = htons(port); + nativeAddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + memset(&(nativeAddr4.sin_zero), 0, sizeof(nativeAddr4.sin_zero)); + + struct sockaddr_in6 nativeAddr6; + nativeAddr6.sin6_len = sizeof(struct sockaddr_in6); + nativeAddr6.sin6_family = AF_INET6; + nativeAddr6.sin6_port = htons(port); + nativeAddr6.sin6_flowinfo = 0; + nativeAddr6.sin6_addr = in6addr_loopback; + nativeAddr6.sin6_scope_id = 0; + + // Wrap the native address structures + + NSData *address4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + NSData *address6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + + addresses = [NSMutableArray arrayWithCapacity:2]; + [addresses addObject:address4]; + [addresses addObject:address6]; + } + else + { + NSString *portStr = [NSString stringWithFormat:@"%hu", port]; + + struct addrinfo hints, *res, *res0; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + int gai_error = getaddrinfo([host UTF8String], [portStr UTF8String], &hints, &res0); + + if (gai_error) + { + error = [self gaiError:gai_error]; + } + else + { + NSUInteger capacity = 0; + for (res = res0; res; res = res->ai_next) + { + if (res->ai_family == AF_INET || res->ai_family == AF_INET6) { + capacity++; + } + } + + addresses = [NSMutableArray arrayWithCapacity:capacity]; + + for (res = res0; res; res = res->ai_next) + { + if (res->ai_family == AF_INET) + { + // Found IPv4 address. + // Wrap the native address structure, and add to results. + + NSData *address4 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + [addresses addObject:address4]; + } + else if (res->ai_family == AF_INET6) + { + // Found IPv6 address. + // Wrap the native address structure, and add to results. + + NSData *address6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + [addresses addObject:address6]; + } + } + freeaddrinfo(res0); + + if ([addresses count] == 0) + { + error = [self gaiError:EAI_FAIL]; + } + } + } + + if (errPtr) *errPtr = error; + return addresses; +} + ++ (NSString *)hostFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 +{ + char addrBuf[INET_ADDRSTRLEN]; + + if (inet_ntop(AF_INET, &pSockaddr4->sin_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) + { + addrBuf[0] = '\0'; + } + + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; +} + ++ (NSString *)hostFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 +{ + char addrBuf[INET6_ADDRSTRLEN]; + + if (inet_ntop(AF_INET6, &pSockaddr6->sin6_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) + { + addrBuf[0] = '\0'; + } + + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; +} + ++ (uint16_t)portFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 +{ + return ntohs(pSockaddr4->sin_port); +} + ++ (uint16_t)portFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 +{ + return ntohs(pSockaddr6->sin6_port); +} + ++ (NSString *)hostFromAddress:(NSData *)address +{ + NSString *host; + + if ([self getHost:&host port:NULL fromAddress:address]) + return host; + else + return nil; +} + ++ (uint16_t)portFromAddress:(NSData *)address +{ + uint16_t port; + + if ([self getHost:NULL port:&port fromAddress:address]) + return port; + else + return 0; +} + ++ (BOOL)isIPv4Address:(NSData *)address +{ + if ([address length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddrX = [address bytes]; + + if (sockaddrX->sa_family == AF_INET) { + return YES; + } + } + + return NO; +} + ++ (BOOL)isIPv6Address:(NSData *)address +{ + if ([address length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddrX = [address bytes]; + + if (sockaddrX->sa_family == AF_INET6) { + return YES; + } + } + + return NO; +} + ++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr fromAddress:(NSData *)address +{ + return [self getHost:hostPtr port:portPtr family:NULL fromAddress:address]; +} + ++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr family:(sa_family_t *)afPtr fromAddress:(NSData *)address +{ + if ([address length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddrX = [address bytes]; + + if (sockaddrX->sa_family == AF_INET) + { + if ([address length] >= sizeof(struct sockaddr_in)) + { + struct sockaddr_in sockaddr4; + memcpy(&sockaddr4, sockaddrX, sizeof(sockaddr4)); + + if (hostPtr) *hostPtr = [self hostFromSockaddr4:&sockaddr4]; + if (portPtr) *portPtr = [self portFromSockaddr4:&sockaddr4]; + if (afPtr) *afPtr = AF_INET; + + return YES; + } + } + else if (sockaddrX->sa_family == AF_INET6) + { + if ([address length] >= sizeof(struct sockaddr_in6)) + { + struct sockaddr_in6 sockaddr6; + memcpy(&sockaddr6, sockaddrX, sizeof(sockaddr6)); + + if (hostPtr) *hostPtr = [self hostFromSockaddr6:&sockaddr6]; + if (portPtr) *portPtr = [self portFromSockaddr6:&sockaddr6]; + if (afPtr) *afPtr = AF_INET6; + + return YES; + } + } + } + + return NO; +} + ++ (NSData *)CRLFData +{ + return [NSData dataWithBytes:"\x0D\x0A" length:2]; +} + ++ (NSData *)CRData +{ + return [NSData dataWithBytes:"\x0D" length:1]; +} + ++ (NSData *)LFData +{ + return [NSData dataWithBytes:"\x0A" length:1]; +} + ++ (NSData *)ZeroData +{ + return [NSData dataWithBytes:"" length:1]; +} + +@end diff --git a/Vendor/CocoaLumberjack/DDASLLogger.h b/Vendor/CocoaLumberjack/DDASLLogger.h new file mode 100755 index 0000000..2aaf4e3 --- /dev/null +++ b/Vendor/CocoaLumberjack/DDASLLogger.h @@ -0,0 +1,41 @@ +#import +#import + +#import "DDLog.h" + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/CocoaLumberjack/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/CocoaLumberjack/CocoaLumberjack/wiki/GettingStarted + * + * + * This class provides a logger for the Apple System Log facility. + * + * As described in the "Getting Started" page, + * the traditional NSLog() function directs it's output to two places: + * + * - Apple System Log + * - StdErr (if stderr is a TTY) so log statements show up in Xcode console + * + * To duplicate NSLog() functionality you can simply add this logger and a tty logger. + * However, if you instead choose to use file logging (for faster performance), + * you may choose to use a file logger and a tty logger. +**/ + +@interface DDASLLogger : DDAbstractLogger +{ + aslclient client; +} + ++ (instancetype)sharedInstance; + +// Inherited from DDAbstractLogger + +// - (id )logFormatter; +// - (void)setLogFormatter:(id )formatter; + +@end diff --git a/Vendor/CocoaLumberjack/DDASLLogger.m b/Vendor/CocoaLumberjack/DDASLLogger.m new file mode 100755 index 0000000..90beff1 --- /dev/null +++ b/Vendor/CocoaLumberjack/DDASLLogger.m @@ -0,0 +1,100 @@ +#import "DDASLLogger.h" + +#import + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/CocoaLumberjack/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/CocoaLumberjack/CocoaLumberjack/wiki/GettingStarted +**/ + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@implementation DDASLLogger + +static DDASLLogger *sharedInstance; + +/** + * The runtime sends initialize to each class in a program exactly one time just before the class, + * or any class that inherits from it, is sent its first message from within the program. (Thus the + * method may never be invoked if the class is not used.) The runtime sends the initialize message to + * classes in a thread-safe manner. Superclasses receive this message before their subclasses. + * + * This method may also be called directly (assumably by accident), hence the safety mechanism. +**/ ++ (void)initialize +{ + static BOOL initialized = NO; + if (!initialized) + { + initialized = YES; + + sharedInstance = [[[self class] alloc] init]; + } +} + ++ (instancetype)sharedInstance +{ + return sharedInstance; +} + +- (id)init +{ + if (sharedInstance != nil) + { + return nil; + } + + if ((self = [super init])) + { + // A default asl client is provided for the main thread, + // but background threads need to create their own client. + + client = asl_open(NULL, "com.apple.console", 0); + } + return self; +} + +- (void)logMessage:(DDLogMessage *)logMessage +{ + NSString *logMsg = logMessage->logMsg; + + if (formatter) + { + logMsg = [formatter formatLogMessage:logMessage]; + } + + if (logMsg) + { + const char *msg = [logMsg UTF8String]; + + int aslLogLevel; + switch (logMessage->logFlag) + { + // Note: By default ASL will filter anything above level 5 (Notice). + // So our mappings shouldn't go above that level. + + case LOG_FLAG_ERROR : aslLogLevel = ASL_LEVEL_ALERT; break; + case LOG_FLAG_WARN : aslLogLevel = ASL_LEVEL_CRIT; break; + case LOG_FLAG_INFO : aslLogLevel = ASL_LEVEL_ERR; break; + case LOG_FLAG_DEBUG : aslLogLevel = ASL_LEVEL_WARNING; break; + default : aslLogLevel = ASL_LEVEL_NOTICE; break; + } + + asl_log(client, NULL, aslLogLevel, "%s", msg); + } +} + +- (NSString *)loggerName +{ + return @"cocoa.lumberjack.aslLogger"; +} + +@end diff --git a/Vendor/CocoaLumberjack/DDAbstractDatabaseLogger.h b/Vendor/CocoaLumberjack/DDAbstractDatabaseLogger.h new file mode 100755 index 0000000..4e0c33c --- /dev/null +++ b/Vendor/CocoaLumberjack/DDAbstractDatabaseLogger.h @@ -0,0 +1,102 @@ +#import + +#import "DDLog.h" + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/CocoaLumberjack/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/CocoaLumberjack/CocoaLumberjack/wiki/GettingStarted + * + * + * This class provides an abstract implementation of a database logger. + * + * That is, it provides the base implementation for a database logger to build atop of. + * All that is needed for a concrete database logger is to extend this class + * and override the methods in the implementation file that are prefixed with "db_". +**/ + +@interface DDAbstractDatabaseLogger : DDAbstractLogger { +@protected + NSUInteger saveThreshold; + NSTimeInterval saveInterval; + NSTimeInterval maxAge; + NSTimeInterval deleteInterval; + BOOL deleteOnEverySave; + + BOOL saveTimerSuspended; + NSUInteger unsavedCount; + dispatch_time_t unsavedTime; + dispatch_source_t saveTimer; + dispatch_time_t lastDeleteTime; + dispatch_source_t deleteTimer; +} + +/** + * Specifies how often to save the data to disk. + * Since saving is an expensive operation (disk io) it is not done after every log statement. + * These properties allow you to configure how/when the logger saves to disk. + * + * A save is done when either (whichever happens first): + * + * - The number of unsaved log entries reaches saveThreshold + * - The amount of time since the oldest unsaved log entry was created reaches saveInterval + * + * You can optionally disable the saveThreshold by setting it to zero. + * If you disable the saveThreshold you are entirely dependent on the saveInterval. + * + * You can optionally disable the saveInterval by setting it to zero (or a negative value). + * If you disable the saveInterval you are entirely dependent on the saveThreshold. + * + * It's not wise to disable both saveThreshold and saveInterval. + * + * The default saveThreshold is 500. + * The default saveInterval is 60 seconds. +**/ +@property (assign, readwrite) NSUInteger saveThreshold; +@property (assign, readwrite) NSTimeInterval saveInterval; + +/** + * It is likely you don't want the log entries to persist forever. + * Doing so would allow the database to grow infinitely large over time. + * + * The maxAge property provides a way to specify how old a log statement can get + * before it should get deleted from the database. + * + * The deleteInterval specifies how often to sweep for old log entries. + * Since deleting is an expensive operation (disk io) is is done on a fixed interval. + * + * An alternative to the deleteInterval is the deleteOnEverySave option. + * This specifies that old log entries should be deleted during every save operation. + * + * You can optionally disable the maxAge by setting it to zero (or a negative value). + * If you disable the maxAge then old log statements are not deleted. + * + * You can optionally disable the deleteInterval by setting it to zero (or a negative value). + * + * If you disable both deleteInterval and deleteOnEverySave then old log statements are not deleted. + * + * It's not wise to enable both deleteInterval and deleteOnEverySave. + * + * The default maxAge is 7 days. + * The default deleteInterval is 5 minutes. + * The default deleteOnEverySave is NO. +**/ +@property (assign, readwrite) NSTimeInterval maxAge; +@property (assign, readwrite) NSTimeInterval deleteInterval; +@property (assign, readwrite) BOOL deleteOnEverySave; + +/** + * Forces a save of any pending log entries (flushes log entries to disk). +**/ +- (void)savePendingLogEntries; + +/** + * Removes any log entries that are older than maxAge. +**/ +- (void)deleteOldLogEntries; + +@end diff --git a/Vendor/CocoaLumberjack/DDAbstractDatabaseLogger.m b/Vendor/CocoaLumberjack/DDAbstractDatabaseLogger.m new file mode 100755 index 0000000..1410f7a --- /dev/null +++ b/Vendor/CocoaLumberjack/DDAbstractDatabaseLogger.m @@ -0,0 +1,727 @@ +#import "DDAbstractDatabaseLogger.h" +#import + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/CocoaLumberjack/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/CocoaLumberjack/CocoaLumberjack/wiki/GettingStarted +**/ + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +@interface DDAbstractDatabaseLogger () +- (void)destroySaveTimer; +- (void)destroyDeleteTimer; +@end + +#pragma mark - + +@implementation DDAbstractDatabaseLogger + +- (id)init +{ + if ((self = [super init])) + { + saveThreshold = 500; + saveInterval = 60; // 60 seconds + maxAge = (60 * 60 * 24 * 7); // 7 days + deleteInterval = (60 * 5); // 5 minutes + } + return self; +} + +- (void)dealloc +{ + [self destroySaveTimer]; + [self destroyDeleteTimer]; + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Override Me +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)db_log:(DDLogMessage *)logMessage +{ + // Override me and add your implementation. + // + // Return YES if an item was added to the buffer. + // Return NO if the logMessage was ignored. + + return NO; +} + +- (void)db_save +{ + // Override me and add your implementation. +} + +- (void)db_delete +{ + // Override me and add your implementation. +} + +- (void)db_saveAndDelete +{ + // Override me and add your implementation. +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Private API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)performSaveAndSuspendSaveTimer +{ + if (unsavedCount > 0) + { + if (deleteOnEverySave) + [self db_saveAndDelete]; + else + [self db_save]; + } + + unsavedCount = 0; + unsavedTime = 0; + + if (saveTimer && !saveTimerSuspended) + { + dispatch_suspend(saveTimer); + saveTimerSuspended = YES; + } +} + +- (void)performDelete +{ + if (maxAge > 0.0) + { + [self db_delete]; + + lastDeleteTime = dispatch_time(DISPATCH_TIME_NOW, 0); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Timers +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)destroySaveTimer +{ + if (saveTimer) + { + dispatch_source_cancel(saveTimer); + if (saveTimerSuspended) + { + // Must resume a timer before releasing it (or it will crash) + dispatch_resume(saveTimer); + saveTimerSuspended = NO; + } + #if !OS_OBJECT_USE_OBJC + dispatch_release(saveTimer); + #endif + saveTimer = NULL; + } +} + +- (void)updateAndResumeSaveTimer +{ + if ((saveTimer != NULL) && (saveInterval > 0.0) && (unsavedTime > 0.0)) + { + uint64_t interval = (uint64_t)(saveInterval * NSEC_PER_SEC); + dispatch_time_t startTime = dispatch_time(unsavedTime, interval); + + dispatch_source_set_timer(saveTimer, startTime, interval, 1.0); + + if (saveTimerSuspended) + { + dispatch_resume(saveTimer); + saveTimerSuspended = NO; + } + } +} + +- (void)createSuspendedSaveTimer +{ + if ((saveTimer == NULL) && (saveInterval > 0.0)) + { + saveTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, loggerQueue); + + dispatch_source_set_event_handler(saveTimer, ^{ @autoreleasepool { + + [self performSaveAndSuspendSaveTimer]; + + }}); + + saveTimerSuspended = YES; + } +} + +- (void)destroyDeleteTimer +{ + if (deleteTimer) + { + dispatch_source_cancel(deleteTimer); + #if !OS_OBJECT_USE_OBJC + dispatch_release(deleteTimer); + #endif + deleteTimer = NULL; + } +} + +- (void)updateDeleteTimer +{ + if ((deleteTimer != NULL) && (deleteInterval > 0.0) && (maxAge > 0.0)) + { + uint64_t interval = (uint64_t)(deleteInterval * NSEC_PER_SEC); + dispatch_time_t startTime; + + if (lastDeleteTime > 0) + startTime = dispatch_time(lastDeleteTime, interval); + else + startTime = dispatch_time(DISPATCH_TIME_NOW, interval); + + dispatch_source_set_timer(deleteTimer, startTime, interval, 1.0); + } +} + +- (void)createAndStartDeleteTimer +{ + if ((deleteTimer == NULL) && (deleteInterval > 0.0) && (maxAge > 0.0)) + { + deleteTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, loggerQueue); + + if (deleteTimer != NULL) { + dispatch_source_set_event_handler(deleteTimer, ^{ @autoreleasepool { + + [self performDelete]; + + }}); + + [self updateDeleteTimer]; + + if (deleteTimer != NULL) dispatch_resume(deleteTimer); + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSUInteger)saveThreshold +{ + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block NSUInteger result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(loggerQueue, ^{ + result = saveThreshold; + }); + }); + + return result; +} + +- (void)setSaveThreshold:(NSUInteger)threshold +{ + dispatch_block_t block = ^{ @autoreleasepool { + + if (saveThreshold != threshold) + { + saveThreshold = threshold; + + // Since the saveThreshold has changed, + // we check to see if the current unsavedCount has surpassed the new threshold. + // + // If it has, we immediately save the log. + + if ((unsavedCount >= saveThreshold) && (saveThreshold > 0)) + { + [self performSaveAndSuspendSaveTimer]; + } + } + }}; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +- (NSTimeInterval)saveInterval +{ + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block NSTimeInterval result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(loggerQueue, ^{ + result = saveInterval; + }); + }); + + return result; +} + +- (void)setSaveInterval:(NSTimeInterval)interval +{ + dispatch_block_t block = ^{ @autoreleasepool { + + // C99 recommended floating point comparison macro + // Read: isLessThanOrGreaterThan(floatA, floatB) + + if (/* saveInterval != interval */ islessgreater(saveInterval, interval)) + { + saveInterval = interval; + + // There are several cases we need to handle here. + // + // 1. If the saveInterval was previously enabled and it just got disabled, + // then we need to stop the saveTimer. (And we might as well release it.) + // + // 2. If the saveInterval was previously disabled and it just got enabled, + // then we need to setup the saveTimer. (Plus we might need to do an immediate save.) + // + // 3. If the saveInterval increased, then we need to reset the timer so that it fires at the later date. + // + // 4. If the saveInterval decreased, then we need to reset the timer so that it fires at an earlier date. + // (Plus we might need to do an immediate save.) + + if (saveInterval > 0.0) + { + if (saveTimer == NULL) + { + // Handles #2 + // + // Since the saveTimer uses the unsavedTime to calculate it's first fireDate, + // if a save is needed the timer will fire immediately. + + [self createSuspendedSaveTimer]; + [self updateAndResumeSaveTimer]; + } + else + { + // Handles #3 + // Handles #4 + // + // Since the saveTimer uses the unsavedTime to calculate it's first fireDate, + // if a save is needed the timer will fire immediately. + + [self updateAndResumeSaveTimer]; + } + } + else if (saveTimer) + { + // Handles #1 + + [self destroySaveTimer]; + } + } + }}; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +- (NSTimeInterval)maxAge +{ + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block NSTimeInterval result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(loggerQueue, ^{ + result = maxAge; + }); + }); + + return result; +} + +- (void)setMaxAge:(NSTimeInterval)interval +{ + dispatch_block_t block = ^{ @autoreleasepool { + + // C99 recommended floating point comparison macro + // Read: isLessThanOrGreaterThan(floatA, floatB) + + if (/* maxAge != interval */ islessgreater(maxAge, interval)) + { + NSTimeInterval oldMaxAge = maxAge; + NSTimeInterval newMaxAge = interval; + + maxAge = interval; + + // There are several cases we need to handle here. + // + // 1. If the maxAge was previously enabled and it just got disabled, + // then we need to stop the deleteTimer. (And we might as well release it.) + // + // 2. If the maxAge was previously disabled and it just got enabled, + // then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.) + // + // 3. If the maxAge was increased, + // then we don't need to do anything. + // + // 4. If the maxAge was decreased, + // then we should do an immediate delete. + + BOOL shouldDeleteNow = NO; + + if (oldMaxAge > 0.0) + { + if (newMaxAge <= 0.0) + { + // Handles #1 + + [self destroyDeleteTimer]; + } + else if (oldMaxAge > newMaxAge) + { + // Handles #4 + shouldDeleteNow = YES; + } + } + else if (newMaxAge > 0.0) + { + // Handles #2 + shouldDeleteNow = YES; + } + + if (shouldDeleteNow) + { + [self performDelete]; + + if (deleteTimer) + [self updateDeleteTimer]; + else + [self createAndStartDeleteTimer]; + } + } + }}; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +- (NSTimeInterval)deleteInterval +{ + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block NSTimeInterval result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(loggerQueue, ^{ + result = deleteInterval; + }); + }); + + return result; +} + +- (void)setDeleteInterval:(NSTimeInterval)interval +{ + dispatch_block_t block = ^{ @autoreleasepool { + + // C99 recommended floating point comparison macro + // Read: isLessThanOrGreaterThan(floatA, floatB) + + if (/* deleteInterval != interval */ islessgreater(deleteInterval, interval)) + { + deleteInterval = interval; + + // There are several cases we need to handle here. + // + // 1. If the deleteInterval was previously enabled and it just got disabled, + // then we need to stop the deleteTimer. (And we might as well release it.) + // + // 2. If the deleteInterval was previously disabled and it just got enabled, + // then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.) + // + // 3. If the deleteInterval increased, then we need to reset the timer so that it fires at the later date. + // + // 4. If the deleteInterval decreased, then we need to reset the timer so that it fires at an earlier date. + // (Plus we might need to do an immediate delete.) + + if (deleteInterval > 0.0) + { + if (deleteTimer == NULL) + { + // Handles #2 + // + // Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate, + // if a delete is needed the timer will fire immediately. + + [self createAndStartDeleteTimer]; + } + else + { + // Handles #3 + // Handles #4 + // + // Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate, + // if a save is needed the timer will fire immediately. + + [self updateDeleteTimer]; + } + } + else if (deleteTimer) + { + // Handles #1 + + [self destroyDeleteTimer]; + } + } + }}; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +- (BOOL)deleteOnEverySave +{ + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block BOOL result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(loggerQueue, ^{ + result = deleteOnEverySave; + }); + }); + + return result; +} + +- (void)setDeleteOnEverySave:(BOOL)flag +{ + dispatch_block_t block = ^{ + + deleteOnEverySave = flag; + }; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Public API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)savePendingLogEntries +{ + dispatch_block_t block = ^{ @autoreleasepool { + + [self performSaveAndSuspendSaveTimer]; + }}; + + if ([self isOnInternalLoggerQueue]) + block(); + else + dispatch_async(loggerQueue, block); +} + +- (void)deleteOldLogEntries +{ + dispatch_block_t block = ^{ @autoreleasepool { + + [self performDelete]; + }}; + + if ([self isOnInternalLoggerQueue]) + block(); + else + dispatch_async(loggerQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark DDLogger +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)didAddLogger +{ + // If you override me be sure to invoke [super didAddLogger]; + + [self createSuspendedSaveTimer]; + + [self createAndStartDeleteTimer]; +} + +- (void)willRemoveLogger +{ + // If you override me be sure to invoke [super willRemoveLogger]; + + [self performSaveAndSuspendSaveTimer]; + + [self destroySaveTimer]; + [self destroyDeleteTimer]; +} + +- (void)logMessage:(DDLogMessage *)logMessage +{ + if ([self db_log:logMessage]) + { + BOOL firstUnsavedEntry = (++unsavedCount == 1); + + if ((unsavedCount >= saveThreshold) && (saveThreshold > 0)) + { + [self performSaveAndSuspendSaveTimer]; + } + else if (firstUnsavedEntry) + { + unsavedTime = dispatch_time(DISPATCH_TIME_NOW, 0); + [self updateAndResumeSaveTimer]; + } + } +} + +- (void)flush +{ + // This method is invoked by DDLog's flushLog method. + // + // It is called automatically when the application quits, + // or if the developer invokes DDLog's flushLog method prior to crashing or something. + + [self performSaveAndSuspendSaveTimer]; +} + +@end diff --git a/Vendor/CocoaLumberjack/DDFileLogger.h b/Vendor/CocoaLumberjack/DDFileLogger.h new file mode 100755 index 0000000..e5f20dc --- /dev/null +++ b/Vendor/CocoaLumberjack/DDFileLogger.h @@ -0,0 +1,369 @@ +#import +#import "DDLog.h" + +@class DDLogFileInfo; + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/CocoaLumberjack/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/CocoaLumberjack/CocoaLumberjack/wiki/GettingStarted + * + * + * This class provides a logger to write log statements to a file. +**/ + + +// Default configuration and safety/sanity values. +// +// maximumFileSize -> DEFAULT_LOG_MAX_FILE_SIZE +// rollingFrequency -> DEFAULT_LOG_ROLLING_FREQUENCY +// maximumNumberOfLogFiles -> DEFAULT_LOG_MAX_NUM_LOG_FILES +// +// You should carefully consider the proper configuration values for your application. + +#define DEFAULT_LOG_MAX_FILE_SIZE (1024 * 1024) // 1 MB +#define DEFAULT_LOG_ROLLING_FREQUENCY (60 * 60 * 24) // 24 Hours +#define DEFAULT_LOG_MAX_NUM_LOG_FILES (5) // 5 Files + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// The LogFileManager protocol is designed to allow you to control all aspects of your log files. +// +// The primary purpose of this is to allow you to do something with the log files after they have been rolled. +// Perhaps you want to compress them to save disk space. +// Perhaps you want to upload them to an FTP server. +// Perhaps you want to run some analytics on the file. +// +// A default LogFileManager is, of course, provided. +// The default LogFileManager simply deletes old log files according to the maximumNumberOfLogFiles property. +// +// This protocol provides various methods to fetch the list of log files. +// +// There are two variants: sorted and unsorted. +// If sorting is not necessary, the unsorted variant is obviously faster. +// The sorted variant will return an array sorted by when the log files were created, +// with the most recently created log file at index 0, and the oldest log file at the end of the array. +// +// You can fetch only the log file paths (full path including name), log file names (name only), +// or an array of DDLogFileInfo objects. +// The DDLogFileInfo class is documented below, and provides a handy wrapper that +// gives you easy access to various file attributes such as the creation date or the file size. + +@protocol DDLogFileManager +@required + +// Public properties + +/** + * The maximum number of archived log files to keep on disk. + * For example, if this property is set to 3, + * then the LogFileManager will only keep 3 archived log files (plus the current active log file) on disk. + * Once the active log file is rolled/archived, then the oldest of the existing 3 rolled/archived log files is deleted. + * + * You may optionally disable deleting old/rolled/archived log files by setting this property to zero. +**/ +@property (readwrite, assign) NSUInteger maximumNumberOfLogFiles; + +// Public methods + +- (NSString *)logsDirectory; + +- (NSArray *)unsortedLogFilePaths; +- (NSArray *)unsortedLogFileNames; +- (NSArray *)unsortedLogFileInfos; + +- (NSArray *)sortedLogFilePaths; +- (NSArray *)sortedLogFileNames; +- (NSArray *)sortedLogFileInfos; + +// Private methods (only to be used by DDFileLogger) + +- (NSString *)createNewLogFile; + +@optional + +// Notifications from DDFileLogger + +- (void)didArchiveLogFile:(NSString *)logFilePath; +- (void)didRollAndArchiveLogFile:(NSString *)logFilePath; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Default log file manager. + * + * All log files are placed inside the logsDirectory. + * If a specific logsDirectory isn't specified, the default directory is used. + * On Mac, this is in ~/Library/Logs/. + * On iPhone, this is in ~/Library/Caches/Logs. + * + * Log files are named "