Просмотр исходного кода

Make most protocol methods optional

Adam Pritchard 9 лет назад
Родитель
Сommit
3de57769c9

+ 39 - 23
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/PsiphonTunnel.h

@@ -35,7 +35,12 @@ FOUNDATION_EXPORT const unsigned char PsiphonTunnelVersionString[];
  Used to communicate with the application that is using the PsiphonTunnel framework,
  and retrieve config info from it.
  */
-@protocol TunneledAppDelegate
+@protocol TunneledAppDelegate <NSObject>
+
+//
+// Required delegate methods
+//
+@required
 
 /*!
  Called when tunnel is started to get the library consumer's desired configuration.
@@ -85,30 +90,36 @@ FOUNDATION_EXPORT const unsigned char PsiphonTunnelVersionString[];
  */
 - (NSString * _Nullable)getPsiphonConfig;
 
+//
+// Optional delegate methods. Note that some of these are probably necessary for
+// for a functioning app to implement, for example `onConnected`.
+//
+@optional
+
 /*!
  Gets runtime errors info that may be useful for debugging.
  @param message  The diagnostic message string.
  Swift: @code func onDiagnosticMessage(_ message: String) @endcode
  */
-- (void) onDiagnosticMessage: (NSString * _Nonnull)message;
+- (void)onDiagnosticMessage:(NSString * _Nonnull)message;
 
 /*! 
  Called when the tunnel is in the process of connecting.
  Swift: @code func onConnecting() @endcode
  */
-- (void) onConnecting;
+- (void)onConnecting;
 /*!
  Called when the tunnel has successfully connected.
  Swift: @code func onConnected() @endcode
  */
-- (void) onConnected;
+- (void)onConnected;
 
 /*!
  Called to indicate that tunnel-core is exiting imminently (usually do to
  a `stop()` call, but could be due to an unexpected error).
  Swift: @code func onExiting() @endcode
  */
-- (void) onExiting;
+- (void)onExiting;
 
 /*!
  Called when tunnel-core determines which server egress regions are available
@@ -117,7 +128,7 @@ FOUNDATION_EXPORT const unsigned char PsiphonTunnelVersionString[];
  @param regions  A string array containing the available egress region country codes.
  Swift: @code func onAvailableEgressRegions(_ regions: [Any]) @endcode
  */
-- (void) onAvailableEgressRegions: (NSArray * _Nonnull)regions;
+- (void)onAvailableEgressRegions:(NSArray * _Nonnull)regions;
 
 /*!
  If the tunnel is started with a fixed SOCKS proxy port, and that port is
@@ -125,48 +136,48 @@ FOUNDATION_EXPORT const unsigned char PsiphonTunnelVersionString[];
  @param port  The port number.
  Swift: @code func onSocksProxyPort(inUse port: Int) @endcode
  */
-- (void) onSocksProxyPortInUse: (NSInteger)port;
+- (void)onSocksProxyPortInUse:(NSInteger)port;
 /*!
  If the tunnel is started with a fixed HTTP proxy port, and that port is
  already in use, this will be called.
  @param port  The port number.
  Swift: @code func onHttpProxyPort(inUse port: Int) @endcode
  */
-- (void) onHttpProxyPortInUse: (NSInteger)port;
+- (void)onHttpProxyPortInUse:(NSInteger)port;
 
 /*!
  Called when tunnel-core determines what port will be used for the local SOCKS proxy.
  @param port  The port number.
  Swift: @code func onListeningSocksProxyPort(_ port: Int) @endcode
  */
-- (void) onListeningSocksProxyPort: (NSInteger)port;
+- (void)onListeningSocksProxyPort:(NSInteger)port;
 /*!
  Called when tunnel-core determines what port will be used for the local HTTP proxy.
  @param port  The port number.
  Swift: @code func onListeningHttpProxyPort(_ port: Int) @endcode
  */
-- (void) onListeningHttpProxyPort: (NSInteger)port;
+- (void)onListeningHttpProxyPort:(NSInteger)port;
 
 /*!
  Called when a error occurs when trying to utilize a configured upstream proxy.
  @param message  A message giving additional info about the error.
  Swift: @code func onUpstreamProxyError(_ message: String) @endcode
  */
-- (void) onUpstreamProxyError: (NSString * _Nonnull)message;
+- (void)onUpstreamProxyError:(NSString * _Nonnull)message;
 
 /*!
  Called after the handshake with the Psiphon server, with the client region as determined by the server.
  @param region  The country code of the client, as determined by the server.
  Swift: @code func onClientRegion(_ region: String) @endcode
  */
-- (void) onClientRegion: (NSString * _Nonnull)region;
+- (void)onClientRegion:(NSString * _Nonnull)region;
 
 /*!
  Called to report that split tunnel is on for the given region.
  @param region  The region split tunnel is on for.
  Swift: @code func onSplitTunnelRegion(_ region: String) @endcode
  */
-- (void) onSplitTunnelRegion: (NSString * _Nonnull)region;
+- (void)onSplitTunnelRegion:(NSString * _Nonnull)region;
 
 /*!
  Called to indicate that an address has been classified as being within the
@@ -176,7 +187,7 @@ FOUNDATION_EXPORT const unsigned char PsiphonTunnelVersionString[];
  @param address  The IP or hostname that is not being tunneled.
  Swift: @code func onUntunneledAddress(_ address: String) @endcode
  */
-- (void) onUntunneledAddress: (NSString * _Nonnull) address;
+- (void)onUntunneledAddress:(NSString * _Nonnull)address;
 
 /*!
  Called to report how many bytes have been transferred since the last time
@@ -185,7 +196,7 @@ FOUNDATION_EXPORT const unsigned char PsiphonTunnelVersionString[];
  @param received  The number of bytes received.
  Swift: @code func onBytesTransferred(_ sent: Int64, _ received: Int64) @endcode
  */
-- (void) onBytesTransferred: (int64_t)sent : (int64_t)received;
+- (void)onBytesTransferred:(int64_t)sent :(int64_t)received;
 
 // TODO: Only applicable to Psiphon proper?
 /*!
@@ -195,14 +206,14 @@ FOUNDATION_EXPORT const unsigned char PsiphonTunnelVersionString[];
  @param url  The URL of the home page.
  Swift: @code func onHomepage(_ url: String) @endcode
  */
-- (void) onHomepage: (NSString * _Nonnull)url;
+- (void)onHomepage:(NSString * _Nonnull)url;
 
 // TODO: Only applicable to Psiphon proper?
 /*!
  Called if the current version of the client is the latest (i.e., there is no upgrade available).
  Swift: @code func onClientIsLatestVersion() @endcode
  */
-- (void) onClientIsLatestVersion;
+- (void)onClientIsLatestVersion;
 
 // TODO: Only applicable to Psiphon proper?
 /*!
@@ -210,7 +221,7 @@ FOUNDATION_EXPORT const unsigned char PsiphonTunnelVersionString[];
  @param filename  The name of the file containing the upgrade.
  Swift: @code func onClientUpgradeDownloaded(_ filename: String) @endcode
  */
-- (void) onClientUpgradeDownloaded: (NSString * _Nonnull)filename;
+- (void)onClientUpgradeDownloaded:(NSString * _Nonnull)filename;
 
 // TODO: Applies to iOS?
 //func onClientVerificationRequired(nonce: String, ttlSeconds: Int, resetCache: Bool)
@@ -227,30 +238,35 @@ FOUNDATION_EXPORT const unsigned char PsiphonTunnelVersionString[];
  @param tunneledAppDelegate  The delegate implementation to use for callbacks.
  @return  The PsiphonTunnel instance.
  */
-+(PsiphonTunnel * _Nonnull) newPsiphonTunnel:(id<TunneledAppDelegate> _Nonnull)tunneledAppDelegate;
++ (PsiphonTunnel * _Nonnull)newPsiphonTunnel:(id<TunneledAppDelegate> _Nonnull)tunneledAppDelegate;
 
 /*!
  Start connecting the PsiphonTunnel. Returns before connection is complete -- delegate callbacks (such as `onConnected`) are used to indicate progress and state.
  @param embeddedServerEntries  Pre-existing server entries to use when attempting to connect to a server. May be null if there are no embedded server entries.
  @return TRUE if the connection start was successful, FALSE otherwise.
  */
--(BOOL) start:(NSString * _Nullable)embeddedServerEntries;
+- (BOOL)start:(NSString * _Nullable)embeddedServerEntries;
 
 /*!
  Stop the tunnel (regardless of its current connection state). Returns before full stop is complete -- `TunneledAppDelegate::onExiting` is called when complete.
  */
--(void) stop;
+- (void)stop;
 
 /*!
  Upload a feedback package to Psiphon Inc. The app collects feedback and diagnostics information in a particular format, then calls this function to upload it for later investigation.
  @note The key, server, path, and headers must be provided by Psiphon Inc.
+ @param feedbackJson  The feedback and diagnostics data to upload.
  @param connectionConfigJson  This function may create a tunnel to perform the upload, and this configuration is used to create that tunnel.
- @param diagnosticsJson  The feedback and diagnostics data to upload.
  @param b64EncodedPublicKey  The key that will be used to encrypt the payload before uploading.
  @param uploadServer  The server to which the data will be uploaded.
  @param uploadPath  The path on the server to which the data will be loaded.
  @param uploadServerHeaders  The request headers that will be used when uploading.
  */
-+ (void)sendFeedback:(NSString * _Nonnull)connectionConfigJson diagnostics:(NSString * _Nonnull)diagnosticsJson publicKey:(NSString * _Nonnull)b64EncodedPublicKey uploadServer:(NSString * _Nonnull)uploadServer uploadPath:(NSString * _Nonnull)uploadPath uploadServerHeaders:(NSString * _Nonnull)uploadServerHeaders;
++ (void)sendFeedback:(NSString * _Nonnull)feedbackJson
+    connectionConfig:(NSString * _Nonnull)connectionConfigJson
+           publicKey:(NSString * _Nonnull)b64EncodedPublicKey
+        uploadServer:(NSString * _Nonnull)uploadServer
+          uploadPath:(NSString * _Nonnull)uploadPath
+ uploadServerHeaders:(NSString * _Nonnull)uploadServerHeaders;
 
 @end

+ 100 - 57
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/PsiphonTunnel.m

@@ -58,7 +58,7 @@
 -(BOOL) start:(NSString * _Nullable)embeddedServerEntries {
     @synchronized (PsiphonTunnel.self) {
         [self stop];
-        [self.tunneledAppDelegate onDiagnosticMessage:@"Starting Psiphon library"];
+        [self logMessage:@"Starting Psiphon library"];
 
         // Not supported on iOS.
         const BOOL useDeviceBinder = FALSE;
@@ -78,17 +78,17 @@
                            useDeviceBinder,
                            &e);
             
-            [self.tunneledAppDelegate onDiagnosticMessage:[NSString stringWithFormat: @"GoPsiStart: %@", res ? @"TRUE" : @"FALSE"]];
+            [self logMessage:[NSString stringWithFormat: @"GoPsiStart: %@", res ? @"TRUE" : @"FALSE"]];
             
             if (e != nil) {
-                [self.tunneledAppDelegate onDiagnosticMessage:[NSString stringWithFormat: @"Psiphon tunnel start failed: %@", e.localizedDescription]];
+                [self logMessage:[NSString stringWithFormat: @"Psiphon tunnel start failed: %@", e.localizedDescription]];
                 return FALSE;
             }
         }
         @catch(NSException *exception) {
-            [self.tunneledAppDelegate onDiagnosticMessage:[NSString stringWithFormat: @"Failed to start Psiphon library: %@", exception.reason]];
+            [self logMessage:[NSString stringWithFormat: @"Failed to start Psiphon library: %@", exception.reason]];
         }
-        [self.tunneledAppDelegate onDiagnosticMessage:@"Psiphon tunnel started"];
+        [self logMessage:@"Psiphon tunnel started"];
         
         return TRUE;
     }
@@ -97,15 +97,20 @@
 // See comment in header.
 -(void) stop {
     @synchronized (PsiphonTunnel.self) {
-        [self.tunneledAppDelegate onDiagnosticMessage: @"Stopping Psiphon library"];
+        [self logMessage: @"Stopping Psiphon library"];
         GoPsiStop();
-        [self.tunneledAppDelegate onDiagnosticMessage: @"Psiphon library stopped"];
+        [self logMessage: @"Psiphon library stopped"];
     }
 }
 
 // See comment in header.
-+ (void)sendFeedback:(NSString * _Nonnull)connectionConfigJson diagnostics:(NSString * _Nonnull)diagnosticsJson publicKey:(NSString * _Nonnull)b64EncodedPublicKey uploadServer:(NSString * _Nonnull)uploadServer uploadPath:(NSString * _Nonnull)uploadPath uploadServerHeaders:(NSString * _Nonnull)uploadServerHeaders {
-    GoPsiSendFeedback(connectionConfigJson, diagnosticsJson, b64EncodedPublicKey, uploadServer, uploadPath, uploadServerHeaders);
++ (void)sendFeedback:(NSString * _Nonnull)feedbackJson
+    connectionConfig:(NSString * _Nonnull)connectionConfigJson
+           publicKey:(NSString * _Nonnull)b64EncodedPublicKey
+        uploadServer:(NSString * _Nonnull)uploadServer
+          uploadPath:(NSString * _Nonnull)uploadPath
+ uploadServerHeaders:(NSString * _Nonnull)uploadServerHeaders {
+    GoPsiSendFeedback(connectionConfigJson, feedbackJson, b64EncodedPublicKey, uploadServer, uploadPath, uploadServerHeaders);
 }
 
 
@@ -118,13 +123,13 @@
 -(NSString * _Nullable)getConfig {
     // tunneledAppDelegate is a weak reference, so check it.
     if (self.tunneledAppDelegate == nil) {
-        [self.tunneledAppDelegate onDiagnosticMessage:@"tunneledApp delegate lost"];
+        [self logMessage:@"tunneledApp delegate lost"];
         return nil;
     }
     
     NSString *configStr = [self.tunneledAppDelegate getPsiphonConfig];
     if (configStr == nil) {
-        [self.tunneledAppDelegate onDiagnosticMessage:@"Error getting config from delegate"];
+        [self logMessage:@"Error getting config from delegate"];
         return nil;
     }
     
@@ -138,7 +143,7 @@
     
     id eh = ^(NSError *err) {
         initialConfig = nil;
-        [self.tunneledAppDelegate onDiagnosticMessage:[NSString stringWithFormat: @"Config JSON parse failed: %@", err.description]];
+        [self logMessage:[NSString stringWithFormat: @"Config JSON parse failed: %@", err.description]];
     };
     
     id parser = [SBJson4Parser parserWithBlock:block allowMultiRoot:NO unwrapRootArray:NO errorHandler:eh];
@@ -155,12 +160,12 @@
     //
     
     if (config[@"PropagationChannelId"] == nil) {
-        [self.tunneledAppDelegate onDiagnosticMessage:@"Config missing PropagationChannelId"];
+        [self logMessage:@"Config missing PropagationChannelId"];
         return nil;
     }
 
     if (config[@"SponsorId"] == nil) {
-        [self.tunneledAppDelegate onDiagnosticMessage:@"Config missing SponsorId"];
+        [self logMessage:@"Config missing SponsorId"];
         return nil;
     }
     
@@ -174,7 +179,7 @@
     NSURL *libraryURL = [fileManager URLForDirectory:NSLibraryDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:&err];
     
     if (libraryURL == nil) {
-        [self.tunneledAppDelegate onDiagnosticMessage:[NSString stringWithFormat: @"Unable to get Library URL: %@", err.localizedDescription]];
+        [self logMessage:[NSString stringWithFormat: @"Unable to get Library URL: %@", err.localizedDescription]];
         return nil;
     }
     
@@ -183,28 +188,28 @@
     NSURL *defaultDataStoreDirectoryURL = [libraryURL URLByAppendingPathComponent:@"datastore" isDirectory:YES];
     
     if (defaultDataStoreDirectoryURL == nil) {
-        [self.tunneledAppDelegate onDiagnosticMessage:@"Unable to create defaultDataStoreDirectoryURL"];
+        [self logMessage:@"Unable to create defaultDataStoreDirectoryURL"];
         return nil;
     }
     
     if (config[@"DataStoreDirectory"] == nil) {
         [fileManager createDirectoryAtURL:defaultDataStoreDirectoryURL withIntermediateDirectories:YES attributes:nil error:&err];
         if (err != nil) {
-            [self.tunneledAppDelegate onDiagnosticMessage:[NSString stringWithFormat: @"Unable to create defaultDataStoreDirectoryURL: %@", err.localizedDescription]];
+            [self logMessage:[NSString stringWithFormat: @"Unable to create defaultDataStoreDirectoryURL: %@", err.localizedDescription]];
             return nil;
         }
         
         config[@"DataStoreDirectory"] = [defaultDataStoreDirectoryURL path];
     }
     else {
-        [self.tunneledAppDelegate onDiagnosticMessage:[NSString stringWithFormat: @"DataStoreDirectory overridden from '%@' to '%@'", [defaultDataStoreDirectoryURL path], config[@"DataStoreDirectory"]]];
+        [self logMessage:[NSString stringWithFormat: @"DataStoreDirectory overridden from '%@' to '%@'", [defaultDataStoreDirectoryURL path], config[@"DataStoreDirectory"]]];
     }
     
     // See previous comment.
     NSString *defaultRemoteServerListFilename = [[libraryURL URLByAppendingPathComponent:@"remote_server_list" isDirectory:NO] path];
     
     if (defaultRemoteServerListFilename == nil) {
-        [self.tunneledAppDelegate onDiagnosticMessage:@"Unable to create defaultRemoteServerListFilename"];
+        [self logMessage:@"Unable to create defaultRemoteServerListFilename"];
         return nil;
     }
     
@@ -212,14 +217,14 @@
         config[@"RemoteServerListDownloadFilename"] = defaultRemoteServerListFilename;
     }
     else {
-        [self.tunneledAppDelegate onDiagnosticMessage:[NSString stringWithFormat: @"RemoteServerListDownloadFilename overridden from '%@' to '%@'", defaultRemoteServerListFilename, config[@"RemoteServerListDownloadFilename"]]];
+        [self logMessage:[NSString stringWithFormat: @"RemoteServerListDownloadFilename overridden from '%@' to '%@'", defaultRemoteServerListFilename, config[@"RemoteServerListDownloadFilename"]]];
     }
     
     // If RemoteServerListUrl and RemoteServerListSignaturePublicKey are absent,
     // we'll just leave them out, but we'll log about it.
     if (config[@"RemoteServerListUrl"] == nil ||
         config[@"RemoteServerListSignaturePublicKey"] == nil) {
-        [self.tunneledAppDelegate onDiagnosticMessage:@"Remote server list functionality will be disabled"];
+        [self logMessage:@"Remote server list functionality will be disabled"];
     }
 
     // Other optional fields not being altered. If not set, their defaults will be used:
@@ -254,7 +259,7 @@
     if (rootCAsURL == nil ||
         (bundledTrustedCAPath = [rootCAsURL path]) == nil ||
         ![[NSFileManager defaultManager] fileExistsAtPath:bundledTrustedCAPath]) {
-        [self.tunneledAppDelegate onDiagnosticMessage:[NSString stringWithFormat: @"Unable to find Root CAs file in bundle: %@", bundledTrustedCAPath]];
+        [self logMessage:[NSString stringWithFormat: @"Unable to find Root CAs file in bundle: %@", bundledTrustedCAPath]];
         return nil;
     }
     config[@"TrustedCACertificatesFilename"] = bundledTrustedCAPath;
@@ -269,13 +274,13 @@
         config[@"ClientPlatform"] = @"iOS-Library";
     }
     else {
-        [self.tunneledAppDelegate onDiagnosticMessage:[NSString stringWithFormat: @"ClientPlatform overridden from 'iOS-Library' to '%@'", config[@"ClientPlatform"]]];
+        [self logMessage:[NSString stringWithFormat: @"ClientPlatform overridden from 'iOS-Library' to '%@'", config[@"ClientPlatform"]]];
     }
 
     NSString *finalConfigStr = [[[SBJson4Writer alloc] init] stringWithObject:config];
     
     if (finalConfigStr == nil) {
-        [self.tunneledAppDelegate onDiagnosticMessage:@"Failed to convert config to JSON string"];
+        [self logMessage:@"Failed to convert config to JSON string"];
         return nil;
     }
     
@@ -299,7 +304,7 @@
     
     id eh = ^(NSError *err) {
         notice = nil;
-        [self.tunneledAppDelegate onDiagnosticMessage:[NSString stringWithFormat: @"Notice JSON parse failed: %@", err.description]];
+        [self logMessage:[NSString stringWithFormat: @"Notice JSON parse failed: %@", err.description]];
     };
     
     id parser = [SBJson4Parser parserWithBlock:block allowMultiRoot:NO unwrapRootArray:NO errorHandler:eh];
@@ -311,127 +316,157 @@
 
     NSString *noticeType = notice[@"noticeType"];
     if (noticeType == nil) {
-        [self.tunneledAppDelegate onDiagnosticMessage:@"Notice missing noticeType"];
+        [self logMessage:@"Notice missing noticeType"];
         return;
     }
     
     if ([noticeType isEqualToString:@"Tunnels"]) {
         id count = [notice valueForKeyPath:@"data.count"];
         if (![count isKindOfClass:[NSNumber class]]) {
-            [self.tunneledAppDelegate onDiagnosticMessage:[NSString stringWithFormat: @"Tunnels notice missing data.count: %@", noticeJSON]];
+            [self logMessage:[NSString stringWithFormat: @"Tunnels notice missing data.count: %@", noticeJSON]];
             return;
         }
 
         if ([count integerValue] > 0) {
-            [self.tunneledAppDelegate onConnected];
+            if ([self.tunneledAppDelegate respondsToSelector:@selector(onConnected)]) {
+                [self.tunneledAppDelegate onConnected];
+            }
         } else {
-            [self.tunneledAppDelegate onConnecting];
+            if ([self.tunneledAppDelegate respondsToSelector:@selector(onConnecting)]) {
+                [self.tunneledAppDelegate onConnecting];
+            }
         }
     }
     else if ([noticeType isEqualToString:@"Exiting"]) {
-        [self.tunneledAppDelegate onExiting];
+        if ([self.tunneledAppDelegate respondsToSelector:@selector(onExiting)]) {
+            [self.tunneledAppDelegate onExiting];
+        }
     }
     else if ([noticeType isEqualToString:@"AvailableEgressRegions"]) {
         id regions = [notice valueForKeyPath:@"data.regions"];
         if (![regions isKindOfClass:[NSArray class]]) {
-            [self.tunneledAppDelegate onDiagnosticMessage:[NSString stringWithFormat: @"AvailableEgressRegions notice missing data.regions: %@", noticeJSON]];
+            [self logMessage:[NSString stringWithFormat: @"AvailableEgressRegions notice missing data.regions: %@", noticeJSON]];
             return;
         }
 
-        [self.tunneledAppDelegate onAvailableEgressRegions:regions];
+        if ([self.tunneledAppDelegate respondsToSelector:@selector(onAvailableEgressRegions:)]) {
+            [self.tunneledAppDelegate onAvailableEgressRegions:regions];
+        }
     }
     else if ([noticeType isEqualToString:@"SocksProxyPortInUse"]) {
         id port = [notice valueForKeyPath:@"data.port"];
         if (![port isKindOfClass:[NSNumber class]]) {
-            [self.tunneledAppDelegate onDiagnosticMessage:[NSString stringWithFormat: @"SocksProxyPortInUse notice missing data.port: %@", noticeJSON]];
+            [self logMessage:[NSString stringWithFormat: @"SocksProxyPortInUse notice missing data.port: %@", noticeJSON]];
             return;
         }
 
-        [self.tunneledAppDelegate onSocksProxyPortInUse:[port integerValue]];
+        if ([self.tunneledAppDelegate respondsToSelector:@selector(onSocksProxyPortInUse:)]) {
+            [self.tunneledAppDelegate onSocksProxyPortInUse:[port integerValue]];
+        }
     }
     else if ([noticeType isEqualToString:@"HttpProxyPortInUse"]) {
         id port = [notice valueForKeyPath:@"data.port"];
         if (![port isKindOfClass:[NSNumber class]]) {
-            [self.tunneledAppDelegate onDiagnosticMessage:[NSString stringWithFormat: @"HttpProxyPortInUse notice missing data.port: %@", noticeJSON]];
+            [self logMessage:[NSString stringWithFormat: @"HttpProxyPortInUse notice missing data.port: %@", noticeJSON]];
             return;
         }
         
-        [self.tunneledAppDelegate onHttpProxyPortInUse:[port integerValue]];
+        if ([self.tunneledAppDelegate respondsToSelector:@selector(onHttpProxyPortInUse:)]) {
+            [self.tunneledAppDelegate onHttpProxyPortInUse:[port integerValue]];
+        }
     }
     else if ([noticeType isEqualToString:@"ListeningSocksProxyPort"]) {
         id port = [notice valueForKeyPath:@"data.port"];
         if (![port isKindOfClass:[NSNumber class]]) {
-            [self.tunneledAppDelegate onDiagnosticMessage:[NSString stringWithFormat: @"ListeningSocksProxyPort notice missing data.port: %@", noticeJSON]];
+            [self logMessage:[NSString stringWithFormat: @"ListeningSocksProxyPort notice missing data.port: %@", noticeJSON]];
             return;
         }
         
-        [self.tunneledAppDelegate onListeningSocksProxyPort:[port integerValue]];
+        if ([self.tunneledAppDelegate respondsToSelector:@selector(onListeningSocksProxyPort:)]) {
+            [self.tunneledAppDelegate onListeningSocksProxyPort:[port integerValue]];
+        }
     }
     else if ([noticeType isEqualToString:@"ListeningHttpProxyPort"]) {
         id port = [notice valueForKeyPath:@"data.port"];
         if (![port isKindOfClass:[NSNumber class]]) {
-            [self.tunneledAppDelegate onDiagnosticMessage:[NSString stringWithFormat: @"ListeningHttpProxyPort notice missing data.port: %@", noticeJSON]];
+            [self logMessage:[NSString stringWithFormat: @"ListeningHttpProxyPort notice missing data.port: %@", noticeJSON]];
             return;
         }
         
-        [self.tunneledAppDelegate onListeningHttpProxyPort:[port integerValue]];
+        if ([self.tunneledAppDelegate respondsToSelector:@selector(onListeningHttpProxyPort:)]) {
+            [self.tunneledAppDelegate onListeningHttpProxyPort:[port integerValue]];
+        }
     }
     else if ([noticeType isEqualToString:@"UpstreamProxyError"]) {
         id message = [notice valueForKeyPath:@"data.message"];
         if (![message isKindOfClass:[NSString class]]) {
-            [self.tunneledAppDelegate onDiagnosticMessage:[NSString stringWithFormat: @"UpstreamProxyError notice missing data.message: %@", noticeJSON]];
+            [self logMessage:[NSString stringWithFormat: @"UpstreamProxyError notice missing data.message: %@", noticeJSON]];
             return;
         }
         
-        [self.tunneledAppDelegate onUpstreamProxyError:message];
+        if ([self.tunneledAppDelegate respondsToSelector:@selector(onUpstreamProxyError:)]) {
+            [self.tunneledAppDelegate onUpstreamProxyError:message];
+        }
     }
     else if ([noticeType isEqualToString:@"ClientUpgradeDownloaded"]) {
         id filename = [notice valueForKeyPath:@"data.filename"];
         if (![filename isKindOfClass:[NSString class]]) {
-            [self.tunneledAppDelegate onDiagnosticMessage:[NSString stringWithFormat: @"ClientUpgradeDownloaded notice missing data.filename: %@", noticeJSON]];
+            [self logMessage:[NSString stringWithFormat: @"ClientUpgradeDownloaded notice missing data.filename: %@", noticeJSON]];
             return;
         }
         
-        [self.tunneledAppDelegate onClientUpgradeDownloaded:filename];
+        if ([self.tunneledAppDelegate respondsToSelector:@selector(onClientUpgradeDownloaded:)]) {
+            [self.tunneledAppDelegate onClientUpgradeDownloaded:filename];
+        }
     }
     else if ([noticeType isEqualToString:@"ClientIsLatestVersion"]) {
-        [self.tunneledAppDelegate onClientIsLatestVersion];
+        if ([self.tunneledAppDelegate respondsToSelector:@selector(onClientIsLatestVersion)]) {
+            [self.tunneledAppDelegate onClientIsLatestVersion];
+        }
     }
     else if ([noticeType isEqualToString:@"Homepage"]) {
         id url = [notice valueForKeyPath:@"data.url"];
         if (![url isKindOfClass:[NSString class]]) {
-            [self.tunneledAppDelegate onDiagnosticMessage:[NSString stringWithFormat: @"Homepage notice missing data.url: %@", noticeJSON]];
+            [self logMessage:[NSString stringWithFormat: @"Homepage notice missing data.url: %@", noticeJSON]];
             return;
         }
         
-        [self.tunneledAppDelegate onHomepage:url];
+        if ([self.tunneledAppDelegate respondsToSelector:@selector(onHomepage:)]) {
+            [self.tunneledAppDelegate onHomepage:url];
+        }
     }
     else if ([noticeType isEqualToString:@"ClientRegion"]) {
         id region = [notice valueForKeyPath:@"data.region"];
         if (![region isKindOfClass:[NSString class]]) {
-            [self.tunneledAppDelegate onDiagnosticMessage:[NSString stringWithFormat: @"ClientRegion notice missing data.region: %@", noticeJSON]];
+            [self logMessage:[NSString stringWithFormat: @"ClientRegion notice missing data.region: %@", noticeJSON]];
             return;
         }
         
-        [self.tunneledAppDelegate onClientRegion:region];
+        if ([self.tunneledAppDelegate respondsToSelector:@selector(onClientRegion:)]) {
+            [self.tunneledAppDelegate onClientRegion:region];
+        }
     }
     else if ([noticeType isEqualToString:@"SplitTunnelRegion"]) {
         id region = [notice valueForKeyPath:@"data.region"];
         if (![region isKindOfClass:[NSString class]]) {
-            [self.tunneledAppDelegate onDiagnosticMessage:[NSString stringWithFormat: @"SplitTunnelRegion notice missing data.region: %@", noticeJSON]];
+            [self logMessage:[NSString stringWithFormat: @"SplitTunnelRegion notice missing data.region: %@", noticeJSON]];
             return;
         }
         
-        [self.tunneledAppDelegate onSplitTunnelRegion:region];
+        if ([self.tunneledAppDelegate respondsToSelector:@selector(onSplitTunnelRegion:)]) {
+            [self.tunneledAppDelegate onSplitTunnelRegion:region];
+        }
     }
     else if ([noticeType isEqualToString:@"Untunneled"]) {
         id address = [notice valueForKeyPath:@"data.address"];
         if (![address isKindOfClass:[NSString class]]) {
-            [self.tunneledAppDelegate onDiagnosticMessage:[NSString stringWithFormat: @"Untunneled notice missing data.address: %@", noticeJSON]];
+            [self logMessage:[NSString stringWithFormat: @"Untunneled notice missing data.address: %@", noticeJSON]];
             return;
         }
         
-        [self.tunneledAppDelegate onUntunneledAddress:address];
+        if ([self.tunneledAppDelegate respondsToSelector:@selector(onUntunneledAddress:)]) {
+            [self.tunneledAppDelegate onUntunneledAddress:address];
+        }
     }
     else if ([noticeType isEqualToString:@"BytesTransferred"]) {
         diagnostic = FALSE;
@@ -439,11 +474,13 @@
         id sent = [notice valueForKeyPath:@"data.sent"];
         id received = [notice valueForKeyPath:@"data.received"];
         if (![sent isKindOfClass:[NSNumber class]] || ![received isKindOfClass:[NSNumber class]]) {
-            [self.tunneledAppDelegate onDiagnosticMessage:[NSString stringWithFormat: @"BytesTransferred notice missing data.sent or data.received: %@", noticeJSON]];
+            [self logMessage:[NSString stringWithFormat: @"BytesTransferred notice missing data.sent or data.received: %@", noticeJSON]];
             return;
         }
         
-        [self.tunneledAppDelegate onBytesTransferred:[sent longLongValue]:[received longLongValue]];
+        if ([self.tunneledAppDelegate respondsToSelector:@selector(onBytesTransferred::)]) {
+            [self.tunneledAppDelegate onBytesTransferred:[sent longLongValue]:[received longLongValue]];
+        }
     }
     
     // Pass diagnostic messages to onDiagnosticMessage.
@@ -456,7 +493,7 @@
         NSString *dataStr = [[[SBJson4Writer alloc] init] stringWithObject:data];
 
         NSString *diagnosticMessage = [NSString stringWithFormat:@"%@: %@", noticeType, dataStr];
-        [self. tunneledAppDelegate onDiagnosticMessage:diagnosticMessage];
+        [self logMessage:diagnosticMessage];
     }
 }
 
@@ -491,7 +528,13 @@
 
 #pragma mark - Helpers (private)
 
-/*! 
+- (void)logMessage:(NSString *)message {
+    if ([self.tunneledAppDelegate respondsToSelector:@selector(onDiagnosticMessage:)]) {
+        [self.tunneledAppDelegate onDiagnosticMessage:message];
+    }
+}
+
+/*!
  Determine the device's region. Makes a best guess based on available info.
  @returns The two-letter country code that the device is probably located in.
  */

+ 1 - 1
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/psiphon-config.json.stub

@@ -10,4 +10,4 @@ All other values will be provided to you by Psiphon Inc.
   "SponsorId": "...",
   "RemoteServerListSignaturePublicKey": "...",
   "RemoteServerListUrl": "..."
-}
+}