From 2acc16f46d1db2ac212deca85c2d3e64650effb4 Mon Sep 17 00:00:00 2001 From: Peter Date: Fri, 8 Jun 2018 19:28:52 +0300 Subject: [PATCH] no message --- MTConnectionProbing.h | 11 + MTConnectionProbing.m | 142 +++++ MTProtoKit/MTHttpTransport.m | 12 +- MTProtoKit/MTProto.h | 2 + MTProtoKit/MTProto.m | 73 ++- MTProtoKit/MTRequestMessageService.m | 3 +- MTProtoKit/MTTcpTransport.m | 20 +- MTProtoKit/MTTransport.h | 3 +- MTProxyConnectivity.m | 1 - MtProtoKit.xcodeproj/project.pbxproj | 32 ++ PingFoundation.h | 233 ++++++++ PingFoundation.m | 774 +++++++++++++++++++++++++++ 12 files changed, 1282 insertions(+), 24 deletions(-) create mode 100644 MTConnectionProbing.h create mode 100644 MTConnectionProbing.m create mode 100644 PingFoundation.h create mode 100644 PingFoundation.m diff --git a/MTConnectionProbing.h b/MTConnectionProbing.h new file mode 100644 index 0000000000..644862f8fd --- /dev/null +++ b/MTConnectionProbing.h @@ -0,0 +1,11 @@ +#import + +@class MTSignal; +@class MTContext; +@class MTSocksProxySettings; + +@interface MTConnectionProbing : NSObject + ++ (MTSignal *)probeProxyWithContext:(MTContext *)context datacenterId:(NSInteger)datacenterId settings:(MTSocksProxySettings *)settings; + +@end diff --git a/MTConnectionProbing.m b/MTConnectionProbing.m new file mode 100644 index 0000000000..f279f1a0be --- /dev/null +++ b/MTConnectionProbing.m @@ -0,0 +1,142 @@ +#import "MTConnectionProbing.h" + +#if defined(MtProtoKitDynamicFramework) +# import +# import +# import +# import +# import +# import +# import +# import +# import +# import +# import +# import +# import +# import +# import +#elif defined(MtProtoKitMacFramework) +# import +# import +# import +# import +# import +# import +# import +# import +# import +# import +# import +# import +# import +# import +# import +#else +# import +# import +# import +# import +# import +# import +# import +# import +# import +# import +# import +# import +# import +# import +# import +#endif + +#import "PingFoundation.h" + +@interface MTPingHelper : NSObject { + void (^_success)(); + PingFoundation *_ping; +} + +@end + +@implementation MTPingHelper + +- (instancetype)initWithSuccess:(void (^)())success { + self = [super init]; + if (self != nil) { + _success = [success copy]; + + NSArray *hosts = @[ + @"google.com", + @"8.8.8.8" + ]; + + NSString *host = hosts[(int)(arc4random_uniform((uint32_t)hosts.count))]; + + _ping = [[PingFoundation alloc] initWithHostName:host]; + _ping.delegate = self; + [_ping start]; + } + return self; +} + +- (void)dealloc { + _ping.delegate = nil; + [_ping stop]; +} + +- (void)stop { +} + +- (void)pingFoundation:(PingFoundation *)pinger didReceivePingResponsePacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber { + if (_success) { + _success(); + } +} + +- (void)pingFoundation:(PingFoundation *)pinger didStartWithAddress:(NSData *)__unused address { + [pinger sendPingWithData:nil]; +} + +@end + +@implementation MTConnectionProbing + ++ (MTSignal *)pingAddress { + return [[MTSignal alloc] initWithGenerator:^id(MTSubscriber *subscriber) { + MTQueue *queue = [MTQueue mainQueue]; + MTMetaDisposable *disposable = [[MTMetaDisposable alloc] init]; + + [queue dispatchOnQueue:^{ + MTPingHelper *helper = [[MTPingHelper alloc] initWithSuccess:^{ + [subscriber putNext:@true]; + [subscriber putCompletion]; + }]; + + [disposable setDisposable:[[MTBlockDisposable alloc] initWithBlock:^{ + [helper stop]; + }]]; + }]; + + return disposable; + }]; +} + ++ (MTSignal *)probeProxyWithContext:(MTContext *)context datacenterId:(NSInteger)datacenterId settings:(MTSocksProxySettings *)settings { + MTSignal *proxyAvailable = [[[MTProxyConnectivity pingProxyWithContext:context datacenterId:datacenterId settings:settings] map:^id(MTProxyConnectivityStatus *status) { + return @(status.reachable); + }] timeout:10.0 onQueue:[MTQueue concurrentDefaultQueue] orSignal:[MTSignal single:@false]]; + MTSignal *referenceAvailable = [[self pingAddress] timeout:10.0 onQueue:[MTQueue concurrentDefaultQueue] orSignal:[MTSignal single:@false]]; + MTSignal *combined = [[MTSignal combineSignals:@[proxyAvailable, referenceAvailable]] map:^id(NSArray *values) { + NSNumber *proxy = values[0]; + NSNumber *ping = values[1]; + if (![proxy boolValue] && [ping boolValue]) { + return @true; + } else { + return @false; + } + }]; + return combined; +} + +@end diff --git a/MTProtoKit/MTHttpTransport.m b/MTProtoKit/MTHttpTransport.m index efa0e93708..48428063df 100644 --- a/MTProtoKit/MTHttpTransport.m +++ b/MTProtoKit/MTHttpTransport.m @@ -150,8 +150,8 @@ id delegate = self.delegate; if ([delegate respondsToSelector:@selector(transportNetworkAvailabilityChanged:isNetworkAvailable:)]) [delegate transportNetworkAvailabilityChanged:self isNetworkAvailable:_isNetworkAvailable]; - if ([delegate respondsToSelector:@selector(transportConnectionStateChanged:isConnected:proxyAddress:)]) - [delegate transportConnectionStateChanged:self isConnected:_isConnected proxyAddress:nil]; + if ([delegate respondsToSelector:@selector(transportConnectionStateChanged:isConnected:proxySettings:)]) + [delegate transportConnectionStateChanged:self isConnected:_isConnected proxySettings:nil]; if ([delegate respondsToSelector:@selector(transportConnectionContextUpdateStateChanged:isUpdatingConnectionContext:)]) [delegate transportConnectionContextUpdateStateChanged:self isUpdatingConnectionContext:_currentActualizationPingId != 0]; }]; @@ -199,8 +199,8 @@ _isConnected = false; id delegate = self.delegate; - if ([delegate respondsToSelector:@selector(transportConnectionStateChanged:isConnected:proxyAddress:)]) - [delegate transportConnectionStateChanged:self isConnected:_isConnected proxyAddress:nil]; + if ([delegate respondsToSelector:@selector(transportConnectionStateChanged:isConnected:proxySettings:)]) + [delegate transportConnectionStateChanged:self isConnected:_isConnected proxySettings:nil]; } }]; } @@ -281,8 +281,8 @@ _isConnected = true; id delegate = self.delegate; - if ([delegate respondsToSelector:@selector(transportConnectionStateChanged:isConnected:proxyAddress:)]) - [delegate transportConnectionStateChanged:self isConnected:_isConnected proxyAddress:nil]; + if ([delegate respondsToSelector:@selector(transportConnectionStateChanged:isConnected:proxySettings:)]) + [delegate transportConnectionStateChanged:self isConnected:_isConnected proxySettings:nil]; } [self stopConnectionWatchdogTimer]; diff --git a/MTProtoKit/MTProto.h b/MTProtoKit/MTProto.h index 10e5d3be7b..4321a200d3 100644 --- a/MTProtoKit/MTProto.h +++ b/MTProtoKit/MTProto.h @@ -21,6 +21,7 @@ @property (nonatomic, readonly) bool isConnected; @property (nonatomic, readonly) NSString *proxyAddress; +@property (nonatomic, readonly) bool proxyHasConnectionIssues; @end @@ -49,6 +50,7 @@ @property (nonatomic) bool media; @property (nonatomic) bool enforceMedia; @property (nonatomic) bool cdn; +@property (nonatomic) bool checkForProxyConnectionIssues; @property (nonatomic) id requiredAuthToken; @property (nonatomic) NSInteger authTokenMasterDatacenterId; diff --git a/MTProtoKit/MTProto.m b/MTProtoKit/MTProto.m index 899aeb2514..87bcde7cb3 100644 --- a/MTProtoKit/MTProto.m +++ b/MTProtoKit/MTProto.m @@ -57,10 +57,23 @@ #import "MTRpcResultMessage.h" #import "MTRpcError.h" +#import "MTConnectionProbing.h" + #import "MTApiEnvironment.h" #import "MTTime.h" +#if defined(MtProtoKitDynamicFramework) +# import +# import +#elif defined(MtProtoKitMacFramework) +# import +# import +#else +# import +# import +#endif + #define MTProtoV2 1 typedef enum { @@ -81,11 +94,12 @@ static const NSUInteger MTMaxUnacknowledgedMessageCount = 64; @implementation MTProtoConnectionState -- (instancetype)initWithIsConnected:(bool)isConnected proxyAddress:(NSString *)proxyAddress { +- (instancetype)initWithIsConnected:(bool)isConnected proxyAddress:(NSString *)proxyAddress proxyHasConnectionIssues:(bool)proxyHasConnectionIssues { self = [super init]; if (self != nil) { _isConnected = isConnected; _proxyAddress = proxyAddress; + _proxyHasConnectionIssues = proxyHasConnectionIssues; } return self; } @@ -111,6 +125,12 @@ static const NSUInteger MTMaxUnacknowledgedMessageCount = 64; bool _willRequestTransactionOnNextQueuePass; MTNetworkUsageCalculationInfo *_usageCalculationInfo; + + MTProtoConnectionState *_connectionState; + + bool _isProbing; + MTMetaDisposable *_probingDisposable; + NSNumber *_probingStatus; } @end @@ -161,10 +181,12 @@ static const NSUInteger MTMaxUnacknowledgedMessageCount = 64; MTTransport *transport = _transport; _transport.delegate = nil; _transport = nil; + id probingDisposable = _probingDisposable; [[MTProto managerQueue] dispatchOnQueue:^ { [transport stop]; + [probingDisposable dispose]; }]; } @@ -240,7 +262,7 @@ static const NSUInteger MTMaxUnacknowledgedMessageCount = 64; id delegate = _delegate; if ([delegate respondsToSelector:@selector(mtProtoNetworkAvailabilityChanged:isNetworkAvailable:)]) [delegate mtProtoNetworkAvailabilityChanged:self isNetworkAvailable:false]; - if ([delegate respondsToSelector:@selector(mtProtoConnectionStateChanged:isConnected:)]) + if ([delegate respondsToSelector:@selector(mtProtoConnectionStateChanged:state:)]) [delegate mtProtoConnectionStateChanged:self state:nil]; if ([delegate respondsToSelector:@selector(mtProtoConnectionContextUpdateStateChanged:isUpdatingConnectionContext:)]) [delegate mtProtoConnectionContextUpdateStateChanged:self isUpdatingConnectionContext:false]; @@ -734,7 +756,7 @@ static const NSUInteger MTMaxUnacknowledgedMessageCount = 64; }]; } -- (void)transportConnectionStateChanged:(MTTransport *)transport isConnected:(bool)isConnected proxyAddress:(NSString *)proxyAddress +- (void)transportConnectionStateChanged:(MTTransport *)transport isConnected:(bool)isConnected proxySettings:(MTSocksProxySettings *)proxySettings { [[MTProto managerQueue] dispatchOnQueue:^ { @@ -751,9 +773,50 @@ static const NSUInteger MTMaxUnacknowledgedMessageCount = 64; [messageService mtProtoConnectionStateChanged:self isConnected:isConnected]; } + if (isConnected || proxySettings == nil) { + _probingStatus = nil; + } + + MTProtoConnectionState *connectionState = [[MTProtoConnectionState alloc] initWithIsConnected:isConnected proxyAddress:proxySettings.ip proxyHasConnectionIssues:[_probingStatus boolValue]]; + _connectionState = connectionState; + id delegate = _delegate; - if ([delegate respondsToSelector:@selector(mtProtoConnectionStateChanged:state:)]) - [delegate mtProtoConnectionStateChanged:self state:[[MTProtoConnectionState alloc] initWithIsConnected:isConnected proxyAddress:proxyAddress]]; + if ([delegate respondsToSelector:@selector(mtProtoConnectionStateChanged:state:)]) { + [delegate mtProtoConnectionStateChanged:self state:connectionState]; + } + + if (isConnected || proxySettings == nil || !_checkForProxyConnectionIssues) { + if (_isProbing) { + _isProbing = false; + [_probingDisposable setDisposable:nil]; + _probingStatus = nil; + } + } else { + if (!_isProbing) { + _isProbing = true; + __weak MTProto *weakSelf = self; + MTSignal *checkSignal = [[MTConnectionProbing probeProxyWithContext:_context datacenterId:_datacenterId settings:proxySettings] delay:5.0 onQueue:[MTQueue concurrentDefaultQueue]]; + checkSignal = [[checkSignal then:[[MTSignal complete] delay:20.0 onQueue:[MTQueue concurrentDefaultQueue]]] restart]; + [_probingDisposable setDisposable:[checkSignal startWithNext:^(NSNumber *next) { + [[MTProto managerQueue] dispatchOnQueue:^{ + __strong MTProto *strongSelf = weakSelf; + if (strongSelf == nil) { + return; + } + if (strongSelf->_isProbing) { + strongSelf->_probingStatus = next; + if (strongSelf->_connectionState != nil) { + strongSelf->_connectionState = [[MTProtoConnectionState alloc] initWithIsConnected:strongSelf->_connectionState.isConnected proxyAddress:strongSelf->_connectionState.proxyAddress proxyHasConnectionIssues:[strongSelf->_probingStatus boolValue]]; + id delegate = strongSelf->_delegate; + if ([delegate respondsToSelector:@selector(mtProtoConnectionStateChanged:state:)]) { + [delegate mtProtoConnectionStateChanged:self state:connectionState]; + } + } + } + }]; + }]]; + } + } }]; } diff --git a/MTProtoKit/MTRequestMessageService.m b/MTProtoKit/MTRequestMessageService.m index 1ee5c6d7cb..6b065c9e0d 100644 --- a/MTProtoKit/MTRequestMessageService.m +++ b/MTProtoKit/MTRequestMessageService.m @@ -563,7 +563,8 @@ rpcError = maybeInternalMessage; else { - rpcResult = request.responseParser([MTInternalMessageParser unwrapMessage:rpcResultMessage.data]); + NSData *unwrappedData = [MTInternalMessageParser unwrapMessage:rpcResultMessage.data]; + rpcResult = request.responseParser(unwrappedData); if (rpcResult == nil) { rpcError = [[MTRpcError alloc] initWithErrorCode:500 errorDescription:@"TL_PARSING_ERROR"]; diff --git a/MTProtoKit/MTTcpTransport.m b/MTProtoKit/MTTcpTransport.m index 110a96b840..2e1bcd6db2 100644 --- a/MTProtoKit/MTTcpTransport.m +++ b/MTProtoKit/MTTcpTransport.m @@ -37,7 +37,7 @@ static const NSTimeInterval MTTcpTransportSleepWatchdogTimeout = 60.0; @property (nonatomic, strong) MTDatacenterAddress *address; @property (nonatomic, strong) MTTcpConnection *connection; -@property (nonatomic, strong) NSString *proxyAddress; +@property (nonatomic, strong) MTSocksProxySettings *proxySettings; @property (nonatomic) bool connectionConnected; @property (nonatomic) bool connectionIsValid; @@ -117,7 +117,7 @@ static const NSTimeInterval MTTcpTransportSleepWatchdogTimeout = 60.0; transportContext.isNetworkAvailable = true; - transportContext.proxyAddress = context.apiEnvironment.socksProxySettings.ip; + transportContext.proxySettings = context.apiEnvironment.socksProxySettings; }]; } return self; @@ -163,8 +163,8 @@ static const NSTimeInterval MTTcpTransportSleepWatchdogTimeout = 60.0; id delegate = self.delegate; if ([delegate respondsToSelector:@selector(transportNetworkAvailabilityChanged:isNetworkAvailable:)]) [delegate transportNetworkAvailabilityChanged:self isNetworkAvailable:transportContext.isNetworkAvailable]; - if ([delegate respondsToSelector:@selector(transportConnectionStateChanged:isConnected:proxyAddress:)]) - [delegate transportConnectionStateChanged:self isConnected:transportContext.connectionConnected proxyAddress:transportContext.proxyAddress]; + if ([delegate respondsToSelector:@selector(transportConnectionStateChanged:isConnected:proxySettings:)]) + [delegate transportConnectionStateChanged:self isConnected:transportContext.connectionConnected proxySettings:transportContext.proxySettings]; if ([delegate respondsToSelector:@selector(transportConnectionContextUpdateStateChanged:isUpdatingConnectionContext:)]) [delegate transportConnectionContextUpdateStateChanged:self isUpdatingConnectionContext:transportContext.currentActualizationPingMessageId != 0]; }]; @@ -236,8 +236,8 @@ static const NSTimeInterval MTTcpTransportSleepWatchdogTimeout = 60.0; transportContext.connectionIsValid = false; id delegate = self.delegate; - if ([delegate respondsToSelector:@selector(transportConnectionStateChanged:isConnected:proxyAddress:)]) - [delegate transportConnectionStateChanged:self isConnected:false proxyAddress:transportContext.proxyAddress]; + if ([delegate respondsToSelector:@selector(transportConnectionStateChanged:isConnected:proxySettings:)]) + [delegate transportConnectionStateChanged:self isConnected:false proxySettings:transportContext.proxySettings]; transportContext.connectionBehaviour.needsReconnection = false; @@ -407,8 +407,8 @@ static const NSTimeInterval MTTcpTransportSleepWatchdogTimeout = 60.0; [transportContext.connectionBehaviour connectionOpened]; id delegate = self.delegate; - if ([delegate respondsToSelector:@selector(transportConnectionStateChanged:isConnected:proxyAddress:)]) - [delegate transportConnectionStateChanged:self isConnected:true proxyAddress:transportContext.proxyAddress]; + if ([delegate respondsToSelector:@selector(transportConnectionStateChanged:isConnected:proxySettings:)]) + [delegate transportConnectionStateChanged:self isConnected:true proxySettings:transportContext.proxySettings]; transportContext.didSendActualizationPingAfterConnection = false; transportContext.currentActualizationPingMessageId = 0; @@ -438,8 +438,8 @@ static const NSTimeInterval MTTcpTransportSleepWatchdogTimeout = 60.0; [self restartSleepWatchdogTimer]; id delegate = self.delegate; - if ([delegate respondsToSelector:@selector(transportConnectionStateChanged:isConnected:proxyAddress:)]) - [delegate transportConnectionStateChanged:self isConnected:false proxyAddress:transportContext.proxyAddress]; + if ([delegate respondsToSelector:@selector(transportConnectionStateChanged:isConnected:proxySettings:)]) + [delegate transportConnectionStateChanged:self isConnected:false proxySettings:transportContext.proxySettings]; if ([delegate respondsToSelector:@selector(transportTransactionsMayHaveFailed:transactionIds:)]) [delegate transportTransactionsMayHaveFailed:self transactionIds:@[connection.internalId]]; diff --git a/MTProtoKit/MTTransport.h b/MTProtoKit/MTTransport.h index ce8951594c..a54687f790 100644 --- a/MTProtoKit/MTTransport.h +++ b/MTProtoKit/MTTransport.h @@ -16,6 +16,7 @@ @class MTIncomingMessage; @class MTMessageTransaction; @class MTNetworkUsageCalculationInfo; +@class MTSocksProxySettings; #if defined(MtProtoKitDynamicFramework) # import @@ -30,7 +31,7 @@ @optional - (void)transportNetworkAvailabilityChanged:(MTTransport *)transport isNetworkAvailable:(bool)isNetworkAvailable; -- (void)transportConnectionStateChanged:(MTTransport *)transport isConnected:(bool)isConnected proxyAddress:(NSString *)proxyAddress; +- (void)transportConnectionStateChanged:(MTTransport *)transport isConnected:(bool)isConnected proxySettings:(MTSocksProxySettings *)proxySettings; - (void)transportConnectionContextUpdateStateChanged:(MTTransport *)transport isUpdatingConnectionContext:(bool)isUpdatingConnectionContext; - (void)transportConnectionProblemsStatusChanged:(MTTransport *)transport hasConnectionProblems:(bool)hasConnectionProblems isProbablyHttp:(bool)isProbablyHttp; diff --git a/MTProxyConnectivity.m b/MTProxyConnectivity.m index b40a631c03..affa7ea89a 100644 --- a/MTProxyConnectivity.m +++ b/MTProxyConnectivity.m @@ -154,7 +154,6 @@ typedef struct { + (MTSignal *)pingProxyWithContext:(MTContext *)context datacenterId:(NSInteger)datacenterId settings:(MTSocksProxySettings *)settings { return [[MTSignal alloc] initWithGenerator:^id(MTSubscriber *subscriber) { - MTTransportScheme *transportScheme = [context transportSchemeForDatacenterWithId:datacenterId media:false isProxy:true]; MTDatacenterAddressSet *addressSet = [context addressSetForDatacenterWithId:datacenterId]; NSMutableArray *signals = [[NSMutableArray alloc] init]; for (MTDatacenterAddress *address in addressSet.addressList) { diff --git a/MtProtoKit.xcodeproj/project.pbxproj b/MtProtoKit.xcodeproj/project.pbxproj index 30b770bef6..c6def13123 100644 --- a/MtProtoKit.xcodeproj/project.pbxproj +++ b/MtProtoKit.xcodeproj/project.pbxproj @@ -16,6 +16,18 @@ D010DB7E1D70ABEE0012AD96 /* MTRsa.m in Sources */ = {isa = PBXBuildFile; fileRef = D010DB7C1D70ABEE0012AD96 /* MTRsa.m */; }; D010DB811D70B3B90012AD96 /* MTAes.h in Headers */ = {isa = PBXBuildFile; fileRef = D010DB7F1D70B3B90012AD96 /* MTAes.h */; }; D010DB821D70B3B90012AD96 /* MTAes.m in Sources */ = {isa = PBXBuildFile; fileRef = D010DB801D70B3B90012AD96 /* MTAes.m */; }; + D0119CC120CAD34800895300 /* MTConnectionProbing.h in Headers */ = {isa = PBXBuildFile; fileRef = D0119CBF20CAD34800895300 /* MTConnectionProbing.h */; }; + D0119CC220CAD34800895300 /* MTConnectionProbing.h in Headers */ = {isa = PBXBuildFile; fileRef = D0119CBF20CAD34800895300 /* MTConnectionProbing.h */; }; + D0119CC320CAD34800895300 /* MTConnectionProbing.h in Headers */ = {isa = PBXBuildFile; fileRef = D0119CBF20CAD34800895300 /* MTConnectionProbing.h */; }; + D0119CC420CAD34800895300 /* MTConnectionProbing.m in Sources */ = {isa = PBXBuildFile; fileRef = D0119CC020CAD34800895300 /* MTConnectionProbing.m */; }; + D0119CC520CAD34800895300 /* MTConnectionProbing.m in Sources */ = {isa = PBXBuildFile; fileRef = D0119CC020CAD34800895300 /* MTConnectionProbing.m */; }; + D0119CC620CAD34800895300 /* MTConnectionProbing.m in Sources */ = {isa = PBXBuildFile; fileRef = D0119CC020CAD34800895300 /* MTConnectionProbing.m */; }; + D0119CC920CAD65D00895300 /* PingFoundation.h in Headers */ = {isa = PBXBuildFile; fileRef = D0119CC720CAD65D00895300 /* PingFoundation.h */; }; + D0119CCA20CAD65D00895300 /* PingFoundation.h in Headers */ = {isa = PBXBuildFile; fileRef = D0119CC720CAD65D00895300 /* PingFoundation.h */; }; + D0119CCB20CAD65D00895300 /* PingFoundation.h in Headers */ = {isa = PBXBuildFile; fileRef = D0119CC720CAD65D00895300 /* PingFoundation.h */; }; + D0119CCC20CAD65D00895300 /* PingFoundation.m in Sources */ = {isa = PBXBuildFile; fileRef = D0119CC820CAD65D00895300 /* PingFoundation.m */; }; + D0119CCD20CAD65D00895300 /* PingFoundation.m in Sources */ = {isa = PBXBuildFile; fileRef = D0119CC820CAD65D00895300 /* PingFoundation.m */; }; + D0119CCE20CAD65D00895300 /* PingFoundation.m in Sources */ = {isa = PBXBuildFile; fileRef = D0119CC820CAD65D00895300 /* PingFoundation.m */; }; D0185E722089D265005E1A6C /* MTProxyConnectivity.h in Headers */ = {isa = PBXBuildFile; fileRef = D0185E702089D265005E1A6C /* MTProxyConnectivity.h */; settings = {ATTRIBUTES = (Public, ); }; }; D0185E732089D265005E1A6C /* MTProxyConnectivity.h in Headers */ = {isa = PBXBuildFile; fileRef = D0185E702089D265005E1A6C /* MTProxyConnectivity.h */; settings = {ATTRIBUTES = (Public, ); }; }; D0185E742089D265005E1A6C /* MTProxyConnectivity.h in Headers */ = {isa = PBXBuildFile; fileRef = D0185E702089D265005E1A6C /* MTProxyConnectivity.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -607,6 +619,10 @@ D010DB7C1D70ABEE0012AD96 /* MTRsa.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTRsa.m; sourceTree = ""; }; D010DB7F1D70B3B90012AD96 /* MTAes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTAes.h; sourceTree = ""; }; D010DB801D70B3B90012AD96 /* MTAes.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTAes.m; sourceTree = ""; }; + D0119CBF20CAD34800895300 /* MTConnectionProbing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MTConnectionProbing.h; sourceTree = ""; }; + D0119CC020CAD34800895300 /* MTConnectionProbing.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MTConnectionProbing.m; sourceTree = ""; }; + D0119CC720CAD65D00895300 /* PingFoundation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PingFoundation.h; sourceTree = ""; }; + D0119CC820CAD65D00895300 /* PingFoundation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PingFoundation.m; sourceTree = ""; }; D0185E702089D265005E1A6C /* MTProxyConnectivity.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MTProxyConnectivity.h; sourceTree = ""; }; D0185E712089D265005E1A6C /* MTProxyConnectivity.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MTProxyConnectivity.m; sourceTree = ""; }; D020FAF81D994E3100F279AA /* MTHttpRequestOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MTHttpRequestOperation.h; path = MTProtoKit/MTHttpRequestOperation.h; sourceTree = ""; }; @@ -1071,6 +1087,10 @@ D0E2E17620866780005737E8 /* MTDNS.m */, D0185E702089D265005E1A6C /* MTProxyConnectivity.h */, D0185E712089D265005E1A6C /* MTProxyConnectivity.m */, + D0119CBF20CAD34800895300 /* MTConnectionProbing.h */, + D0119CC020CAD34800895300 /* MTConnectionProbing.m */, + D0119CC720CAD65D00895300 /* PingFoundation.h */, + D0119CC820CAD65D00895300 /* PingFoundation.m */, ); name = Utils; sourceTree = ""; @@ -1555,6 +1575,7 @@ D0CD98161D74B99400F41187 /* MTMsgContainerMessage.h in Headers */, D0CD981A1D74B99400F41187 /* MTMsgsStateInfoMessage.h in Headers */, D0CD98191D74B99400F41187 /* MTMsgsAckMessage.h in Headers */, + D0119CC220CAD34800895300 /* MTConnectionProbing.h in Headers */, D0CD98FA1D75C0E400F41187 /* MTApiEnvironment.h in Headers */, D0CD98881D74BA5100F41187 /* MTDatacenterAddressListData.h in Headers */, D0CD98591D74B9BF00F41187 /* MTServerDhInnerDataMessage.h in Headers */, @@ -1591,6 +1612,7 @@ D0CFBB8B1FD718C500B65C0D /* AFHTTPRequestOperation.h in Headers */, D0CD98841D74BA5100F41187 /* MTDatacenterAddress.h in Headers */, D0CD97EA1D74B94300F41187 /* MTInternalId.h in Headers */, + D0119CCA20CAD65D00895300 /* PingFoundation.h in Headers */, D0CAF2DB1D75E31A0011F558 /* MTDisposable.h in Headers */, D0CD98851D74BA5100F41187 /* MTDatacenterAddressSet.h in Headers */, D0CD98B61D74BA7500F41187 /* MTTransportTransaction.h in Headers */, @@ -1632,6 +1654,7 @@ D0CB06321ADC4583005E298F /* MTDatacenterAuthInfo.h in Headers */, D0D1A0541ADD983C007D9ED6 /* MTMsgsStateReqMessage.h in Headers */, D0CB06201ADC454C005E298F /* MTSerialization.h in Headers */, + D0119CC920CAD65D00895300 /* PingFoundation.h in Headers */, D0D1A0401ADD983C007D9ED6 /* MTExportedAuthorizationData.h in Headers */, D0CB063C1ADC4591005E298F /* MTOutgoingMessage.h in Headers */, D0D1A0361ADD983C007D9ED6 /* MTBadMsgNotificationMessage.h in Headers */, @@ -1661,6 +1684,7 @@ D0CB06251ADC4562005E298F /* MTTransportScheme.h in Headers */, D0D1A05C1ADD983C007D9ED6 /* MTResPqMessage.h in Headers */, D0D1A03E1ADD983C007D9ED6 /* MTDropRpcResultMessage.h in Headers */, + D0119CC120CAD34800895300 /* MTConnectionProbing.h in Headers */, D0185E722089D265005E1A6C /* MTProxyConnectivity.h in Headers */, D0D1A05E1ADD983C007D9ED6 /* MTRpcError.h in Headers */, D0E2E17720866780005737E8 /* MTDNS.h in Headers */, @@ -1753,6 +1777,7 @@ D0CD98241D74B99500F41187 /* MTMsgContainerMessage.h in Headers */, D0CD98281D74B99500F41187 /* MTMsgsStateInfoMessage.h in Headers */, D0CD98271D74B99500F41187 /* MTMsgsAckMessage.h in Headers */, + D0119CC320CAD34800895300 /* MTConnectionProbing.h in Headers */, D0CD98FF1D75C0E500F41187 /* MTApiEnvironment.h in Headers */, D0CD988D1D74BA5200F41187 /* MTDatacenterAddressListData.h in Headers */, D0CD985D1D74B9BF00F41187 /* MTServerDhInnerDataMessage.h in Headers */, @@ -1789,6 +1814,7 @@ D0CFBB8C1FD718C600B65C0D /* AFHTTPRequestOperation.h in Headers */, D0CD98891D74BA5200F41187 /* MTDatacenterAddress.h in Headers */, D0CD97F01D74B94300F41187 /* MTInternalId.h in Headers */, + D0119CCB20CAD65D00895300 /* PingFoundation.h in Headers */, D0CAF2DC1D75E31B0011F558 /* MTDisposable.h in Headers */, D0CD988A1D74BA5200F41187 /* MTDatacenterAddressSet.h in Headers */, D0CD98B91D74BA7500F41187 /* MTTransportTransaction.h in Headers */, @@ -2017,6 +2043,7 @@ D0CD987E1D74BA4900F41187 /* MTDatacenterTransferAuthAction.m in Sources */, D0CD988F1D74BA5900F41187 /* MTDatacenterAddressSet.m in Sources */, D0CD98091D74B96C00F41187 /* MTDestroySessionResponseMessage.m in Sources */, + D0119CC520CAD34800895300 /* MTConnectionProbing.m in Sources */, D0CD982E1D74B9AA00F41187 /* MTExportedAuthorizationData.m in Sources */, D0CD986E1D74B9F400F41187 /* MTTransportScheme.m in Sources */, D0CD98D81D74BAAD00F41187 /* MTHttpWorkerBehaviour.m in Sources */, @@ -2033,6 +2060,7 @@ D0CD98081D74B96C00F41187 /* MTBufferReader.m in Sources */, D0CD98341D74B9AA00F41187 /* MTMsgDetailedInfoMessage.m in Sources */, D0CD984F1D74B9B700F41187 /* MTRpcResultMessage.m in Sources */, + D0119CCD20CAD65D00895300 /* PingFoundation.m in Sources */, D020FAFE1D994E3100F279AA /* MTHttpRequestOperation.m in Sources */, D0CD98391D74B9AA00F41187 /* MTNewSessionCreatedMessage.m in Sources */, D0CD98381D74B9AA00F41187 /* MTMsgsStateReqMessage.m in Sources */, @@ -2115,6 +2143,7 @@ D0CB066F1ADC49FF005E298F /* AFHTTPRequestOperation.m in Sources */, D0D1A06C1ADD987A007D9ED6 /* MTDatacenterAddressListData.m in Sources */, D09A59591B582EFF00FC3724 /* MTFileBasedKeychain.m in Sources */, + D0119CC420CAD34800895300 /* MTConnectionProbing.m in Sources */, D0CB06681ADC45DA005E298F /* MTHttpWorker.m in Sources */, D0CB06561ADC45BA005E298F /* MTApiEnvironment.m in Sources */, D0D1A05F1ADD983C007D9ED6 /* MTRpcError.m in Sources */, @@ -2131,6 +2160,7 @@ D0D1A0391ADD983C007D9ED6 /* MTBuffer.m in Sources */, D0D1A0591ADD983C007D9ED6 /* MTPingMessage.m in Sources */, D0CB06361ADC4588005E298F /* MTDatacenterAuthInfo.m in Sources */, + D0119CCC20CAD65D00895300 /* PingFoundation.m in Sources */, D020FAFD1D994E3100F279AA /* MTHttpRequestOperation.m in Sources */, D0CB062D1ADC457B005E298F /* MTDatacenterTransferAuthAction.m in Sources */, D0D1A04F1ADD983C007D9ED6 /* MTMsgResendReqMessage.m in Sources */, @@ -2213,6 +2243,7 @@ D0CD98811D74BA4900F41187 /* MTDatacenterTransferAuthAction.m in Sources */, D0CD98941D74BA5A00F41187 /* MTDatacenterAddressSet.m in Sources */, D0CD980E1D74B96C00F41187 /* MTDestroySessionResponseMessage.m in Sources */, + D0119CC620CAD34800895300 /* MTConnectionProbing.m in Sources */, D0CD983D1D74B9AA00F41187 /* MTExportedAuthorizationData.m in Sources */, D0CD986F1D74B9F500F41187 /* MTTransportScheme.m in Sources */, D0CD98D91D74BAAD00F41187 /* MTHttpWorkerBehaviour.m in Sources */, @@ -2229,6 +2260,7 @@ D0CD980D1D74B96C00F41187 /* MTBufferReader.m in Sources */, D0CD98431D74B9AA00F41187 /* MTMsgDetailedInfoMessage.m in Sources */, D0CD98541D74B9B700F41187 /* MTRpcResultMessage.m in Sources */, + D0119CCE20CAD65D00895300 /* PingFoundation.m in Sources */, D020FAFF1D994E3100F279AA /* MTHttpRequestOperation.m in Sources */, D0CD98481D74B9AA00F41187 /* MTNewSessionCreatedMessage.m in Sources */, D0CD98471D74B9AA00F41187 /* MTMsgsStateReqMessage.m in Sources */, diff --git a/PingFoundation.h b/PingFoundation.h new file mode 100644 index 0000000000..ffb54db275 --- /dev/null +++ b/PingFoundation.h @@ -0,0 +1,233 @@ +#import + +#if TARGET_OS_EMBEDDED || TARGET_IPHONE_SIMULATOR +#import +#else +#import +#endif + +#include + +#pragma mark * PingFoundation + +@protocol PingFoundationDelegate; + +typedef NS_ENUM(NSInteger, PingFoundationAddressStyle) { + PingFoundationAddressStyleAny, ///< Use the first IPv4 or IPv6 address found; the default. + PingFoundationAddressStyleICMPv4, ///< Use the first IPv4 address found. + PingFoundationAddressStyleICMPv6 ///< Use the first IPv6 address found. +}; + +@interface PingFoundation : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +/*! Initialise the object to ping the specified host. + * \param hostName The DNS name of the host to ping; an IPv4 or IPv6 address in string form will + * work here. + * \returns The initialised object. + */ + +- (instancetype)initWithHostName:(NSString *)hostName NS_DESIGNATED_INITIALIZER; + +/*! A copy of the value passed to `-initWithHostName:`. + */ + +@property (nonatomic, copy, readonly) NSString * hostName; + +/*! The delegate for this object. + * \details Delegate callbacks are schedule in the default run loop mode of the run loop of the + * thread that calls `-start`. + */ + +@property (nonatomic, weak, readwrite) id delegate; + +/*! Controls the IP address version used by the object. + * \details You should set this value before starting the object. + */ + +@property (nonatomic, assign, readwrite) PingFoundationAddressStyle addressStyle; + +/*! The address being pinged. + * \details The contents of the NSData is a (struct sockaddr) of some form. The + * value is nil while the object is stopped and remains nil on start until + * `-pingFoundation:didStartWithAddress:` is called. + */ + +@property (nonatomic, copy, readonly) NSData * hostAddress; + +/*! The address family for `hostAddress`, or `AF_UNSPEC` if that's nil. + */ + +@property (nonatomic, assign, readonly) sa_family_t hostAddressFamily; + +/*! The identifier used by pings by this object. + * \details When you create an instance of this object it generates a random identifier + * that it uses to identify its own pings. + */ + +@property (nonatomic, assign, readonly) uint16_t identifier; + +/*! The next sequence number to be used by this object. + * \details This value starts at zero and increments each time you send a ping (safely + * wrapping back to zero if necessary). The sequence number is included in the ping, + * allowing you to match up requests and responses, and thus calculate ping times and + * so on. + */ + +@property (nonatomic, assign, readonly) uint16_t nextSequenceNumber; + +- (void)start; +// Starts the pinger object pinging. You should call this after +// you've setup the delegate and any ping parameters. + +- (void)sendPingWithData:(NSData *)data; +// Sends an actual ping. Pass nil for data to use a standard 56 byte payload (resulting in a +// standard 64 byte ping). Otherwise pass a non-nil value and it will be appended to the +// ICMP header. +// +// Do not try to send a ping before you receive the -PingFoundation:didStartWithAddress: delegate +// callback. + +- (void)stop; +// Stops the pinger object. You should call this when you're done +// pinging. + +@end + +@protocol PingFoundationDelegate + +@optional + +/*! A PingFoundation delegate callback, called once the object has started up. + * \details This is called shortly after you start the object to tell you that the + * object has successfully started. On receiving this callback, you can call + * `-sendPingWithData:` to send pings. + * + * If the object didn't start, `-pingFoundation:didFailWithError:` is called instead. + * \param pinger The object issuing the callback. + * \param address The address that's being pinged; at the time this delegate callback + * is made, this will have the same value as the `hostAddress` property. + */ + +- (void)pingFoundation:(PingFoundation *)pinger didStartWithAddress:(NSData *)address; + +/*! A PingFoundation delegate callback, called if the object fails to start up. + * \details This is called shortly after you start the object to tell you that the + * object has failed to start. The most likely cause of failure is a problem + * resolving `hostName`. + * + * By the time this callback is called, the object has stopped (that is, you don't + * need to call `-stop` yourself). + * \param pinger The object issuing the callback. + * \param error Describes the failure. + */ + +- (void)pingFoundation:(PingFoundation *)pinger didFailWithError:(NSError *)error; + +/*! A PingFoundation delegate callback, called when the object has successfully sent a ping packet. + * \details Each call to `-sendPingWithData:` will result in either a + * `-pingFoundation:didSendPacket:sequenceNumber:` delegate callback or a + * `-pingFoundation:didFailToSendPacket:sequenceNumber:error:` delegate callback (unless you + * stop the object before you get the callback). These callbacks are currently delivered + * synchronously from within `-sendPingWithData:`, but this synchronous behaviour is not + * considered API. + * \param pinger The object issuing the callback. + * \param packet The packet that was sent; this includes the ICMP header (`ICMPHeader`) and the + * data you passed to `-sendPingWithData:` but does not include any IP-level headers. + * \param sequenceNumber The ICMP sequence number of that packet. + */ + +- (void)pingFoundation:(PingFoundation *)pinger didSendPacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber; + +/*! A PingFoundation delegate callback, called when the object fails to send a ping packet. + * \details Each call to `-sendPingWithData:` will result in either a + * `-pingFoundation:didSendPacket:sequenceNumber:` delegate callback or a + * `-pingFoundation:didFailToSendPacket:sequenceNumber:error:` delegate callback (unless you + * stop the object before you get the callback). These callbacks are currently delivered + * synchronously from within `-sendPingWithData:`, but this synchronous behaviour is not + * considered API. + * \param pinger The object issuing the callback. + * \param packet The packet that was not sent; see `-pingFoundation:didSendPacket:sequenceNumber:` + * for details. + * \param sequenceNumber The ICMP sequence number of that packet. + * \param error Describes the failure. + */ + +- (void)pingFoundation:(PingFoundation *)pinger didFailToSendPacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber error:(NSError *)error; + +/*! A PingFoundation delegate callback, called when the object receives a ping response. + * \details If the object receives an ping response that matches a ping request that it + * sent, it informs the delegate via this callback. Matching is primarily done based on + * the ICMP identifier, although other criteria are used as well. + * \param pinger The object issuing the callback. + * \param packet The packet received; this includes the ICMP header (`ICMPHeader`) and any data that + * follows that in the ICMP message but does not include any IP-level headers. + * \param sequenceNumber The ICMP sequence number of that packet. + */ + +- (void)pingFoundation:(PingFoundation *)pinger didReceivePingResponsePacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber; + +/*! A PingFoundation delegate callback, called when the object receives an unmatched ICMP message. + * \details If the object receives an ICMP message that does not match a ping request that it + * sent, it informs the delegate via this callback. The nature of ICMP handling in a + * BSD kernel makes this a common event because, when an ICMP message arrives, it is + * delivered to all ICMP sockets. + * + * IMPORTANT: This callback is especially common when using IPv6 because IPv6 uses ICMP + * for important network management functions. For example, IPv6 routers periodically + * send out Router Advertisement (RA) packets via Neighbor Discovery Protocol (NDP), which + * is implemented on top of ICMP. + * + * For more on matching, see the discussion associated with + * `-pingFoundation:didReceivePingResponsePacket:sequenceNumber:`. + * \param pinger The object issuing the callback. + * \param packet The packet received; this includes the ICMP header (`ICMPHeader`) and any data that + * follows that in the ICMP message but does not include any IP-level headers. + */ + +- (void)pingFoundation:(PingFoundation *)pinger didReceiveUnexpectedPacket:(NSData *)packet; + +@end + +#pragma mark * IP and ICMP On-The-Wire Format + +/*! Describes the on-the-wire header format for an ICMP ping. + * \details This defines the header structure of ping packets on the wire. Both IPv4 and + * IPv6 use the same basic structure. + * + * This is declared in the header because clients of PingFoundation might want to use + * it parse received ping packets. + */ + +// ICMP type and code combinations: + +enum { + ICMPv4TypeEchoRequest = 8, ///< The ICMP `type` for a ping request; in this case `code` is always 0. + ICMPv4TypeEchoReply = 0 ///< The ICMP `type` for a ping response; in this case `code` is always 0. +}; + +enum { + ICMPv6TypeEchoRequest = 128, ///< The ICMP `type` for a ping request; in this case `code` is always 0. + ICMPv6TypeEchoReply = 129 ///< The ICMP `type` for a ping response; in this case `code` is always 0. +}; + +// ICMP header structure: + +struct ICMPHeader +{ + uint8_t type; + uint8_t code; + uint16_t checksum; + uint16_t identifier; + uint16_t sequenceNumber; + // data... +}; +typedef struct ICMPHeader ICMPHeader; + +__Check_Compile_Time(sizeof(ICMPHeader) == 8); +__Check_Compile_Time(offsetof(ICMPHeader, type) == 0); +__Check_Compile_Time(offsetof(ICMPHeader, code) == 1); +__Check_Compile_Time(offsetof(ICMPHeader, checksum) == 2); +__Check_Compile_Time(offsetof(ICMPHeader, identifier) == 4); +__Check_Compile_Time(offsetof(ICMPHeader, sequenceNumber) == 6); diff --git a/PingFoundation.m b/PingFoundation.m new file mode 100644 index 0000000000..f605c272e6 --- /dev/null +++ b/PingFoundation.m @@ -0,0 +1,774 @@ +#import "PingFoundation.h" + +#include +#include +#include + +#pragma mark * IPv4 and ICMPv4 On-The-Wire Format + +/*! Describes the on-the-wire header format for an IPv4 packet. + * \details This defines the header structure of IPv4 packets on the wire. We need + * this in order to skip this header in the IPv4 case, where the kernel passes + * it to us for no obvious reason. + */ + +struct IPv4Header { + uint8_t versionAndHeaderLength; + uint8_t differentiatedServices; + uint16_t totalLength; + uint16_t identification; + uint16_t flagsAndFragmentOffset; + uint8_t timeToLive; + uint8_t protocol; + uint16_t headerChecksum; + uint8_t sourceAddress[4]; + uint8_t destinationAddress[4]; + // options... + // data... +}; +typedef struct IPv4Header IPv4Header; + +__Check_Compile_Time(sizeof(IPv4Header) == 20); +__Check_Compile_Time(offsetof(IPv4Header, versionAndHeaderLength) == 0); +__Check_Compile_Time(offsetof(IPv4Header, differentiatedServices) == 1); +__Check_Compile_Time(offsetof(IPv4Header, totalLength) == 2); +__Check_Compile_Time(offsetof(IPv4Header, identification) == 4); +__Check_Compile_Time(offsetof(IPv4Header, flagsAndFragmentOffset) == 6); +__Check_Compile_Time(offsetof(IPv4Header, timeToLive) == 8); +__Check_Compile_Time(offsetof(IPv4Header, protocol) == 9); +__Check_Compile_Time(offsetof(IPv4Header, headerChecksum) == 10); +__Check_Compile_Time(offsetof(IPv4Header, sourceAddress) == 12); +__Check_Compile_Time(offsetof(IPv4Header, destinationAddress) == 16); + +/*! Calculates an IP checksum. + * \details This is the standard BSD checksum code, modified to use modern types. + * \param buffer A pointer to the data to checksum. + * \param bufferLen The length of that data. + * \returns The checksum value, in network byte order. + */ + +static uint16_t in_cksum(const void *buffer, size_t bufferLen) { + // + size_t bytesLeft; + int32_t sum; + const uint16_t * cursor; + union { + uint16_t us; + uint8_t uc[2]; + } last; + uint16_t answer; + + bytesLeft = bufferLen; + sum = 0; + cursor = buffer; + + /* + * Our algorithm is simple, using a 32 bit accumulator (sum), we add + * sequential 16 bit words to it, and at the end, fold back all the + * carry bits from the top 16 bits into the lower 16 bits. + */ + while (bytesLeft > 1) { + sum += *cursor; + cursor += 1; + bytesLeft -= 2; + } + + /* mop up an odd byte, if necessary */ + if (bytesLeft == 1) { + last.uc[0] = * (const uint8_t *) cursor; + last.uc[1] = 0; + sum += last.us; + } + + /* add back carry outs from top 16 bits to low 16 bits */ + sum = (sum >> 16) + (sum & 0xffff); /* add hi 16 to low 16 */ + sum += (sum >> 16); /* add carry */ + answer = (uint16_t) ~sum; /* truncate to 16 bits */ + + return answer; +} + +#pragma mark * PingFoundation + +@interface PingFoundation () + +// read/write versions of public properties + +@property (nonatomic, copy, readwrite, nullable) NSData * hostAddress; +@property (nonatomic, assign, readwrite ) uint16_t nextSequenceNumber; + +// private properties + +/*! True if nextSequenceNumber has wrapped from 65535 to 0. + */ + +@property (nonatomic, assign, readwrite) BOOL nextSequenceNumberHasWrapped; + +/*! A host object for name-to-address resolution. + */ + +@property (nonatomic, strong, readwrite, nullable) CFHostRef host __attribute__ ((NSObject)); + +/*! A socket object for ICMP send and receive. + */ + +@property (nonatomic, strong, readwrite, nullable) CFSocketRef socket __attribute__ ((NSObject)); + +@end + +@implementation PingFoundation + +- (instancetype)initWithHostName:(NSString *)hostName +{ + if ([hostName length] <= 0) + { + return nil; + } + + self = [super init]; + if (self != nil) { + self->_hostName = [hostName copy]; + self->_identifier = (uint16_t) arc4random(); + } + return self; +} + +- (void)dealloc +{ + [self stop]; +} + +- (sa_family_t)hostAddressFamily { + sa_family_t result; + + result = AF_UNSPEC; + if ( (self.hostAddress != nil) && (self.hostAddress.length >= sizeof(struct sockaddr)) ) + { + result = ((const struct sockaddr *) self.hostAddress.bytes)->sa_family; + } + return result; +} + +/*! Shuts down the pinger object and tell the delegate about the error. + * \param error Describes the failure. + */ + +- (void)didFailWithError:(NSError *)error { + id strongDelegate; + + // We retain ourselves temporarily because it's common for the delegate method + // to release its last reference to us, which causes -dealloc to be called here. + // If we then reference self on the return path, things go badly. I don't think + // that happens currently, but I've got into the habit of doing this as a + // defensive measure. + + CFAutorelease(CFBridgingRetain(self)); + + [self stop]; + + strongDelegate = self.delegate; + if ((strongDelegate != nil) && [strongDelegate respondsToSelector:@selector(pingFoundation:didFailWithError:)]) + { + [strongDelegate pingFoundation:self didFailWithError:error]; + } +} + +/*! Shuts down the pinger object and tell the delegate about the error. + * \details This converts the CFStreamError to an NSError and then call through to + * -didFailWithError: to do the real work. + * \param streamError Describes the failure. + */ + +- (void)didFailWithHostStreamError:(CFStreamError)streamError { + NSDictionary * userInfo; + NSError * error; + + if (streamError.domain == kCFStreamErrorDomainNetDB) { + userInfo = @{(id) kCFGetAddrInfoFailureKey: @(streamError.error)}; + } else { + userInfo = nil; + } + error = [NSError errorWithDomain:(NSString *) kCFErrorDomainCFNetwork code:kCFHostErrorUnknown userInfo:userInfo]; + + [self didFailWithError:error]; +} + +/*! Builds a ping packet from the supplied parameters. + * \param type The packet type, which is different for IPv4 and IPv6. + * \param payload Data to place after the ICMP header. + * \param requiresChecksum Determines whether a checksum is calculated (IPv4) or not (IPv6). + * \returns A ping packet suitable to be passed to the kernel. + */ + +- (NSData *)pingPacketWithType:(uint8_t)type payload:(NSData *)payload requiresChecksum:(BOOL)requiresChecksum { + NSMutableData * packet; + ICMPHeader * icmpPtr; + + packet = [NSMutableData dataWithLength:sizeof(*icmpPtr) + payload.length]; + + icmpPtr = packet.mutableBytes; + icmpPtr->type = type; + icmpPtr->code = 0; + icmpPtr->checksum = 0; + icmpPtr->identifier = OSSwapHostToBigInt16(self.identifier); + icmpPtr->sequenceNumber = OSSwapHostToBigInt16(self.nextSequenceNumber); + memcpy(&icmpPtr[1], [payload bytes], [payload length]); + + if (requiresChecksum) { + // The IP checksum routine returns a 16-bit number that's already in correct byte order + // (due to wacky 1's complement maths), so we just put it into the packet as a 16-bit unit. + + icmpPtr->checksum = in_cksum(packet.bytes, packet.length); + } + + return packet; +} + +- (void)sendPingWithData:(NSData *)data { + int err; + NSData * payload; + NSData * packet; + ssize_t bytesSent; + id strongDelegate; + + // data may be nil + + // Construct the ping packet. + + payload = data; + if (payload == nil) + { + payload = [[NSString stringWithFormat:@"%28zd bottles of beer on the wall", (ssize_t) 99 - (size_t) (self.nextSequenceNumber % 100) ] dataUsingEncoding:NSASCIIStringEncoding]; + // Our dummy payload is sized so that the resulting ICMP packet, including the ICMPHeader, is + // 64-bytes, which makes it easier to recognise our packets on the wire. + } + + switch (self.hostAddressFamily) { + case AF_INET: { + packet = [self pingPacketWithType:ICMPv4TypeEchoRequest payload:payload requiresChecksum:YES]; + } break; + case AF_INET6: { + packet = [self pingPacketWithType:ICMPv6TypeEchoRequest payload:payload requiresChecksum:NO]; + } break; + default: { + assert(NO); + } break; + } + + // Send the packet. + + if (self.socket == NULL) { + bytesSent = -1; + err = EBADF; + } else { + bytesSent = sendto( + CFSocketGetNative(self.socket), + packet.bytes, + packet.length, + 0, + self.hostAddress.bytes, + (socklen_t) self.hostAddress.length + ); + err = 0; + if (bytesSent < 0) { + err = errno; + } + } + + // Handle the results of the send. + + strongDelegate = self.delegate; + if ( (bytesSent > 0) && (((NSUInteger) bytesSent) == packet.length) ) { + + // Complete success. Tell the client. + + if ( (strongDelegate != nil) && [strongDelegate respondsToSelector:@selector(pingFoundation:didSendPacket:sequenceNumber:)] ) { + [strongDelegate pingFoundation:self didSendPacket:packet sequenceNumber:self.nextSequenceNumber]; + } + } else { + NSError * error; + + // Some sort of failure. Tell the client. + + if (err == 0) { + err = ENOBUFS; // This is not a hugely descriptor error, alas. + } + error = [NSError errorWithDomain:NSPOSIXErrorDomain code:err userInfo:nil]; + if ( (strongDelegate != nil) && [strongDelegate respondsToSelector:@selector(pingFoundation:didFailToSendPacket:sequenceNumber:error:)] ) { + [strongDelegate pingFoundation:self didFailToSendPacket:packet sequenceNumber:self.nextSequenceNumber error:error]; + } + } + + self.nextSequenceNumber += 1; + if (self.nextSequenceNumber == 0) { + self.nextSequenceNumberHasWrapped = YES; + } +} + +/*! Calculates the offset of the ICMP header within an IPv4 packet. + * \details In the IPv4 case the kernel returns us a buffer that includes the + * IPv4 header. We're not interested in that, so we have to skip over it. + * This code does a rough check of the IPv4 header and, if it looks OK, + * returns the offset of the ICMP header. + * \param packet The IPv4 packet, as returned to us by the kernel. + * \returns The offset of the ICMP header, or NSNotFound. + */ + ++ (NSUInteger)icmpHeaderOffsetInIPv4Packet:(NSData *)packet { + // Returns the offset of the ICMPv4Header within an IP packet. + NSUInteger result; + const struct IPv4Header * ipPtr; + size_t ipHeaderLength; + + result = NSNotFound; + if (packet.length >= (sizeof(IPv4Header) + sizeof(ICMPHeader))) { + ipPtr = (const IPv4Header *) packet.bytes; + if ( ((ipPtr->versionAndHeaderLength & 0xF0) == 0x40) && // IPv4 + ( ipPtr->protocol == IPPROTO_ICMP ) ) { + ipHeaderLength = (ipPtr->versionAndHeaderLength & 0x0F) * sizeof(uint32_t); + if (packet.length >= (ipHeaderLength + sizeof(ICMPHeader))) { + result = ipHeaderLength; + } + } + } + return result; +} + +/*! Checks whether the specified sequence number is one we sent. + * \param sequenceNumber The incoming sequence number. + * \returns YES if the sequence number looks like one we sent. + */ + +- (BOOL)validateSequenceNumber:(uint16_t)sequenceNumber { + if (self.nextSequenceNumberHasWrapped) { + // If the sequence numbers have wrapped that we can't reliably check + // whether this is a sequence number we sent. Rather, we check to see + // whether the sequence number is within the last 120 sequence numbers + // we sent. Note that the uint16_t subtraction here does the right + // thing regardless of the wrapping. + // + // Why 120? Well, if we send one ping per second, 120 is 2 minutes, which + // is the standard "max time a packet can bounce around the Internet" value. + return ((uint16_t) (self.nextSequenceNumber - sequenceNumber)) < (uint16_t) 120; + } else { + return sequenceNumber < self.nextSequenceNumber; + } +} + +/*! Checks whether an incoming IPv4 packet looks like a ping response. + * \details This routine modifies this `packet` data! It does this for two reasons: + * + * * It needs to zero out the `checksum` field of the ICMPHeader in order to do + * its checksum calculation. + * + * * It removes the IPv4 header from the front of the packet. + * \param packet The IPv4 packet, as returned to us by the kernel. + * \param sequenceNumberPtr A pointer to a place to start the ICMP sequence number. + * \returns YES if the packet looks like a reasonable IPv4 ping response. + */ + +- (BOOL)validatePing4ResponsePacket:(NSMutableData *)packet sequenceNumber:(uint16_t *)sequenceNumberPtr { + BOOL result; + NSUInteger icmpHeaderOffset; + ICMPHeader * icmpPtr; + uint16_t receivedChecksum; + uint16_t calculatedChecksum; + + result = NO; + + icmpHeaderOffset = [[self class] icmpHeaderOffsetInIPv4Packet:packet]; + if (icmpHeaderOffset != NSNotFound) { + icmpPtr = (struct ICMPHeader *) (((uint8_t *) packet.mutableBytes) + icmpHeaderOffset); + + receivedChecksum = icmpPtr->checksum; + icmpPtr->checksum = 0; + calculatedChecksum = in_cksum(icmpPtr, packet.length - icmpHeaderOffset); + icmpPtr->checksum = receivedChecksum; + + if (receivedChecksum == calculatedChecksum) { + if ( (icmpPtr->type == ICMPv4TypeEchoReply) && (icmpPtr->code == 0) ) { + if ( OSSwapBigToHostInt16(icmpPtr->identifier) == self.identifier ) { + uint16_t sequenceNumber; + + sequenceNumber = OSSwapBigToHostInt16(icmpPtr->sequenceNumber); + if ([self validateSequenceNumber:sequenceNumber]) { + + // Remove the IPv4 header off the front of the data we received, leaving us with + // just the ICMP header and the ping payload. + [packet replaceBytesInRange:NSMakeRange(0, icmpHeaderOffset) withBytes:NULL length:0]; + + *sequenceNumberPtr = sequenceNumber; + result = YES; + } + } + } + } + } + + return result; +} + +/*! Checks whether an incoming IPv6 packet looks like a ping response. + * \param packet The IPv6 packet, as returned to us by the kernel; note that this routine + * could modify this data but does not need to in the IPv6 case. + * \param sequenceNumberPtr A pointer to a place to start the ICMP sequence number. + * \returns YES if the packet looks like a reasonable IPv4 ping response. + */ + +- (BOOL)validatePing6ResponsePacket:(NSMutableData *)packet sequenceNumber:(uint16_t *)sequenceNumberPtr { + BOOL result; + const ICMPHeader * icmpPtr; + + result = NO; + + if (packet.length >= sizeof(*icmpPtr)) { + icmpPtr = packet.bytes; + + // In the IPv6 case we don't check the checksum because that's hard (we need to + // cook up an IPv6 pseudo header and we don't have the ingredients) and unnecessary + // (the kernel has already done this check). + + if ( (icmpPtr->type == ICMPv6TypeEchoReply) && (icmpPtr->code == 0) ) { + if ( OSSwapBigToHostInt16(icmpPtr->identifier) == self.identifier ) { + uint16_t sequenceNumber; + + sequenceNumber = OSSwapBigToHostInt16(icmpPtr->sequenceNumber); + if ([self validateSequenceNumber:sequenceNumber]) { + *sequenceNumberPtr = sequenceNumber; + result = YES; + } + } + } + } + return result; +} + +/*! Checks whether an incoming packet looks like a ping response. + * \param packet The packet, as returned to us by the kernel; note that may end up modifying + * this data. + * \param sequenceNumberPtr A pointer to a place to start the ICMP sequence number. + * \returns YES if the packet looks like a reasonable IPv4 ping response. + */ + +- (BOOL)validatePingResponsePacket:(NSMutableData *)packet sequenceNumber:(uint16_t *)sequenceNumberPtr { + BOOL result; + + switch (self.hostAddressFamily) { + case AF_INET: { + result = [self validatePing4ResponsePacket:packet sequenceNumber:sequenceNumberPtr]; + } break; + case AF_INET6: { + result = [self validatePing6ResponsePacket:packet sequenceNumber:sequenceNumberPtr]; + } break; + default: { + assert(NO); + result = NO; + } break; + } + return result; +} + +/*! Reads data from the ICMP socket. + * \details Called by the socket handling code (SocketReadCallback) to process an ICMP + * message waiting on the socket. + */ + +- (void)readData { + int err; + struct sockaddr_storage addr; + socklen_t addrLen; + ssize_t bytesRead; + void * buffer; + enum { kBufferSize = 65535 }; + + // 65535 is the maximum IP packet size, which seems like a reasonable bound + // here (plus it's what uses). + + buffer = malloc(kBufferSize); + if (buffer == NULL) + { + return; + } + + // Actually read the data. We use recvfrom(), and thus get back the source address, + // but we don't actually do anything with it. It would be trivial to pass it to + // the delegate but we don't need it in this example. + + addrLen = sizeof(addr); + bytesRead = recvfrom(CFSocketGetNative(self.socket), buffer, kBufferSize, 0, (struct sockaddr *) &addr, &addrLen); + err = 0; + if (bytesRead < 0) { + err = errno; + } + + // Process the data we read. + + if (bytesRead > 0) { + NSMutableData * packet; + id strongDelegate; + uint16_t sequenceNumber; + + packet = [NSMutableData dataWithBytes:buffer length:(NSUInteger) bytesRead]; + // We got some data, pass it up to our client. + + strongDelegate = self.delegate; + if ( [self validatePingResponsePacket:packet sequenceNumber:&sequenceNumber] ) { + if ( (strongDelegate != nil) && [strongDelegate respondsToSelector:@selector(pingFoundation:didReceivePingResponsePacket:sequenceNumber:)] ) { + [strongDelegate pingFoundation:self didReceivePingResponsePacket:packet sequenceNumber:sequenceNumber]; + } + } else { + if ( (strongDelegate != nil) && [strongDelegate respondsToSelector:@selector(pingFoundation:didReceiveUnexpectedPacket:)] ) { + [strongDelegate pingFoundation:self didReceiveUnexpectedPacket:packet]; + } + } + } else { + + // We failed to read the data, so shut everything down. + + if (err == 0) { + err = EPIPE; + } + [self didFailWithError:[NSError errorWithDomain:NSPOSIXErrorDomain code:err userInfo:nil]]; + } + + free(buffer); + + // Note that we don't loop back trying to read more data. Rather, we just + // let CFSocket call us again. +} + +/*! The callback for our CFSocket object. + * \details This simply routes the call to our `-readData` method. + * \param s See the documentation for CFSocketCallBack. + * \param type See the documentation for CFSocketCallBack. + * \param address See the documentation for CFSocketCallBack. + * \param data See the documentation for CFSocketCallBack. + * \param info See the documentation for CFSocketCallBack; this is actually a pointer to the + * 'owning' object. + */ + +static void SocketReadCallback(CFSocketRef s, CFSocketCallBackType type, CFDataRef address, const void *data, void *info) { + // This C routine is called by CFSocket when there's data waiting on our + // ICMP socket. It just redirects the call to Objective-C code. + PingFoundation * obj; + + obj = (__bridge PingFoundation *) info; + + [obj readData]; +} + +/*! Starts the send and receive infrastructure. + * \details This is called once we've successfully resolved `hostName` in to + * `hostAddress`. It's responsible for setting up the socket for sending and + * receiving pings. + */ + +- (void)startWithHostAddress +{ + if (self.hostAddress == nil) + { + return; + } + + int err; + int fd; + + // Open the socket. + + fd = -1; + err = 0; + switch (self.hostAddressFamily) { + case AF_INET: { + fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP); + if (fd < 0) { + err = errno; + } + } break; + case AF_INET6: { + fd = socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6); + if (fd < 0) { + err = errno; + } + } break; + default: { + err = EPROTONOSUPPORT; + } break; + } + + if (err != 0) { + [self didFailWithError:[NSError errorWithDomain:NSPOSIXErrorDomain code:err userInfo:nil]]; + } else { + CFSocketContext context = {0, (__bridge void *)(self), NULL, NULL, NULL}; + CFRunLoopSourceRef rls; + id strongDelegate; + + // Wrap it in a CFSocket and schedule it on the runloop. + + self.socket = (CFSocketRef) CFAutorelease( CFSocketCreateWithNative(NULL, fd, kCFSocketReadCallBack, SocketReadCallback, &context) ); + + // The socket will now take care of cleaning up our file descriptor. + + fd = -1; + + rls = CFSocketCreateRunLoopSource(NULL, self.socket, 0); + + CFRunLoopAddSource(CFRunLoopGetCurrent(), rls, kCFRunLoopDefaultMode); + + CFRelease(rls); + + strongDelegate = self.delegate; + if ( (strongDelegate != nil) && [strongDelegate respondsToSelector:@selector(pingFoundation:didStartWithAddress:)] ) { + [strongDelegate pingFoundation:self didStartWithAddress:self.hostAddress]; + } + } +} + +/*! Processes the results of our name-to-address resolution. + * \details Called by our CFHost resolution callback (HostResolveCallback) when host + * resolution is complete. We just latch the first appropriate address and kick + * off the send and receive infrastructure. + */ + +- (void)hostResolutionDone { + Boolean resolved; + NSArray * addresses; + + // Find the first appropriate address. + + addresses = (__bridge NSArray *) CFHostGetAddressing(self.host, &resolved); + if ( resolved && (addresses != nil) ) { + resolved = false; + for (NSData * address in addresses) { + const struct sockaddr * addrPtr; + + addrPtr = (const struct sockaddr *) address.bytes; + if ( address.length >= sizeof(struct sockaddr) ) { + switch (addrPtr->sa_family) { + case AF_INET: { + if (self.addressStyle != PingFoundationAddressStyleICMPv6) { + self.hostAddress = address; + resolved = true; + } + } break; + case AF_INET6: { + if (self.addressStyle != PingFoundationAddressStyleICMPv4) { + self.hostAddress = address; + resolved = true; + } + } break; + } + } + if (resolved) { + break; + } + } + } + + // We're done resolving, so shut that down. + + [self stopHostResolution]; + + // If all is OK, start the send and receive infrastructure, otherwise stop. + + if (resolved) { + [self startWithHostAddress]; + } else { + [self didFailWithError:[NSError errorWithDomain:(NSString *)kCFErrorDomainCFNetwork code:kCFHostErrorHostNotFound userInfo:nil]]; + } +} + +/*! The callback for our CFHost object. + * \details This simply routes the call to our `-hostResolutionDone` or + * `-didFailWithHostStreamError:` methods. + * \param theHost See the documentation for CFHostClientCallBack. + * \param typeInfo See the documentation for CFHostClientCallBack. + * \param error See the documentation for CFHostClientCallBack. + * \param info See the documentation for CFHostClientCallBack; this is actually a pointer to + * the 'owning' object. + */ + +static void HostResolveCallback(CFHostRef theHost, CFHostInfoType typeInfo, const CFStreamError *error, void *info) { + // This C routine is called by CFHost when the host resolution is complete. + // It just redirects the call to the appropriate Objective-C method. + PingFoundation * obj; + + obj = (__bridge PingFoundation *) info; + + if (([obj isKindOfClass:[PingFoundation class]] && typeInfo == kCFHostAddresses)) + { + if (theHost != obj->_host) + { + // unmatched, do nothing. + return; + } + + if ((error != NULL) && (error->domain != 0)) + { + [obj didFailWithHostStreamError:*error]; + } + else + { + [obj hostResolutionDone]; + } + } +} + +- (void)start +{ + // If the user supplied us with an address, just start pinging that. Otherwise + // start a host resolution. + + Boolean success; + CFHostClientContext context = {0, (__bridge void *)(self), NULL, NULL, NULL}; + CFStreamError streamError; + + self.host = (CFHostRef)CFAutorelease(CFHostCreateWithName(NULL, (__bridge CFStringRef) self.hostName)); + + if (self.host == NULL) + { + // host NULL; do nothing. + return; + } + + CFHostSetClient(self.host, HostResolveCallback, &context); + CFHostScheduleWithRunLoop(self.host, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); + success = CFHostStartInfoResolution(self.host, kCFHostAddresses, &streamError); + if (!success) + { + [self didFailWithHostStreamError:streamError]; + } +} + +/*! Stops the name-to-address resolution infrastructure. + */ + +- (void)stopHostResolution { + // Shut down the CFHost. + if (self.host != NULL) { + CFHostSetClient(self.host, NULL, NULL); + CFHostUnscheduleFromRunLoop(self.host, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); + self.host = NULL; + } +} + +/*! Stops the send and receive infrastructure. + */ + +- (void)stopSocket { + if (self.socket != NULL) { + CFSocketInvalidate(self.socket); + self.socket = NULL; + } +} + +- (void)stop { + [self stopHostResolution]; + [self stopSocket]; + + // Junk the host address on stop. If the client calls -start again, we'll + // re-resolve the host name. + + self.hostAddress = NULL; +} + +@end