506 lines
14 KiB
Objective-C
506 lines
14 KiB
Objective-C
#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
|
|
//
|
|
// <iq type="get" to="domain" id="queryID">
|
|
// <time xmlns="urn:xmpp:time"/>
|
|
// </iq>
|
|
//
|
|
// 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
|
|
//
|
|
// <iq type="get" to="fullJID" id="abc123">
|
|
// <time xmlns="urn:xmpp:time"/>
|
|
// </iq>
|
|
|
|
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:
|
|
//
|
|
// <iq type="result" from="robbie@voalte.com/office" to="robbie@deusty.com/home" id="abc123">
|
|
// <time xmlns="urn:xmpp:time">
|
|
// <tzo>-06:00</tzo>
|
|
// <utc>2006-12-19T17:58:35Z</utc>
|
|
// </time>
|
|
// </iq>
|
|
//
|
|
// <iq type="error" from="robbie@voalte.com/office" to="robbie@deusty.com/home" id="abc123">
|
|
// <time xmlns="urn:xmpp:time"/>
|
|
// <error code="501" type="cancel">
|
|
// <feature-not-implemented xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
|
|
// </error>
|
|
// </iq>
|
|
|
|
return [queryTracker invokeForID:[iq elementID] withObject:iq];
|
|
}
|
|
else if (respondsToQueries && [type isEqualToString:@"get"])
|
|
{
|
|
// Example:
|
|
//
|
|
// <iq type="get" from="robbie@deusty.com/home" to="robbie@voalte.com/office" id="abc123">
|
|
// <time xmlns="urn:xmpp:time"/>
|
|
// </iq>
|
|
|
|
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)
|
|
{
|
|
// <query xmlns="http://jabber.org/protocol/disco#info">
|
|
// ...
|
|
// <feature var="urn:xmpp:time"/>
|
|
// ...
|
|
// </query>
|
|
|
|
NSXMLElement *feature = [NSXMLElement elementWithName:@"feature"];
|
|
[feature addAttributeWithName:@"var" stringValue:@"urn:xmpp:time"];
|
|
|
|
[query addChild:feature];
|
|
}
|
|
}
|
|
#endif
|
|
|
|
+ (NSDate *)dateFromResponse:(XMPPIQ *)iq
|
|
{
|
|
// <iq type="result" from="robbie@voalte.com/office" to="robbie@deusty.com/home" id="abc123">
|
|
// <time xmlns="urn:xmpp:time">
|
|
// <tzo>-06:00</tzo>
|
|
// <utc>2006-12-19T17:58:35Z</utc>
|
|
// </time>
|
|
// </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
|
|
{
|
|
// <iq type="result" from="robbie@voalte.com/office" to="robbie@deusty.com/home" id="abc123">
|
|
// <time xmlns="urn:xmpp:time">
|
|
// <tzo>-06:00</tzo>
|
|
// <utc>2006-12-19T17:58:35Z</utc>
|
|
// </time>
|
|
// </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
|
|
{
|
|
// <time xmlns="urn:xmpp:time">
|
|
// <tzo>-06:00</tzo>
|
|
// <utc>2006-12-19T17:58:35Z</utc>
|
|
// </time>
|
|
|
|
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
|