Browse Source

Make most protocol methods optional

Adam Pritchard 9 years ago
parent
commit
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,
  Used to communicate with the application that is using the PsiphonTunnel framework,
  and retrieve config info from it.
  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.
  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;
 - (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.
  Gets runtime errors info that may be useful for debugging.
  @param message  The diagnostic message string.
  @param message  The diagnostic message string.
  Swift: @code func onDiagnosticMessage(_ message: String) @endcode
  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.
  Called when the tunnel is in the process of connecting.
  Swift: @code func onConnecting() @endcode
  Swift: @code func onConnecting() @endcode
  */
  */
-- (void) onConnecting;
+- (void)onConnecting;
 /*!
 /*!
  Called when the tunnel has successfully connected.
  Called when the tunnel has successfully connected.
  Swift: @code func onConnected() @endcode
  Swift: @code func onConnected() @endcode
  */
  */
-- (void) onConnected;
+- (void)onConnected;
 
 
 /*!
 /*!
  Called to indicate that tunnel-core is exiting imminently (usually do to
  Called to indicate that tunnel-core is exiting imminently (usually do to
  a `stop()` call, but could be due to an unexpected error).
  a `stop()` call, but could be due to an unexpected error).
  Swift: @code func onExiting() @endcode
  Swift: @code func onExiting() @endcode
  */
  */
-- (void) onExiting;
+- (void)onExiting;
 
 
 /*!
 /*!
  Called when tunnel-core determines which server egress regions are available
  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.
  @param regions  A string array containing the available egress region country codes.
  Swift: @code func onAvailableEgressRegions(_ regions: [Any]) @endcode
  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
  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.
  @param port  The port number.
  Swift: @code func onSocksProxyPort(inUse port: Int) @endcode
  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
  If the tunnel is started with a fixed HTTP proxy port, and that port is
  already in use, this will be called.
  already in use, this will be called.
  @param port  The port number.
  @param port  The port number.
  Swift: @code func onHttpProxyPort(inUse port: Int) @endcode
  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.
  Called when tunnel-core determines what port will be used for the local SOCKS proxy.
  @param port  The port number.
  @param port  The port number.
  Swift: @code func onListeningSocksProxyPort(_ port: Int) @endcode
  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.
  Called when tunnel-core determines what port will be used for the local HTTP proxy.
  @param port  The port number.
  @param port  The port number.
  Swift: @code func onListeningHttpProxyPort(_ port: Int) @endcode
  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.
  Called when a error occurs when trying to utilize a configured upstream proxy.
  @param message  A message giving additional info about the error.
  @param message  A message giving additional info about the error.
  Swift: @code func onUpstreamProxyError(_ message: String) @endcode
  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.
  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.
  @param region  The country code of the client, as determined by the server.
  Swift: @code func onClientRegion(_ region: String) @endcode
  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.
  Called to report that split tunnel is on for the given region.
  @param region  The region split tunnel is on for.
  @param region  The region split tunnel is on for.
  Swift: @code func onSplitTunnelRegion(_ region: String) @endcode
  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
  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.
  @param address  The IP or hostname that is not being tunneled.
  Swift: @code func onUntunneledAddress(_ address: String) @endcode
  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
  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.
  @param received  The number of bytes received.
  Swift: @code func onBytesTransferred(_ sent: Int64, _ received: Int64) @endcode
  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?
 // TODO: Only applicable to Psiphon proper?
 /*!
 /*!
@@ -195,14 +206,14 @@ FOUNDATION_EXPORT const unsigned char PsiphonTunnelVersionString[];
  @param url  The URL of the home page.
  @param url  The URL of the home page.
  Swift: @code func onHomepage(_ url: String) @endcode
  Swift: @code func onHomepage(_ url: String) @endcode
  */
  */
-- (void) onHomepage: (NSString * _Nonnull)url;
+- (void)onHomepage:(NSString * _Nonnull)url;
 
 
 // TODO: Only applicable to Psiphon proper?
 // TODO: Only applicable to Psiphon proper?
 /*!
 /*!
  Called if the current version of the client is the latest (i.e., there is no upgrade available).
  Called if the current version of the client is the latest (i.e., there is no upgrade available).
  Swift: @code func onClientIsLatestVersion() @endcode
  Swift: @code func onClientIsLatestVersion() @endcode
  */
  */
-- (void) onClientIsLatestVersion;
+- (void)onClientIsLatestVersion;
 
 
 // TODO: Only applicable to Psiphon proper?
 // 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.
  @param filename  The name of the file containing the upgrade.
  Swift: @code func onClientUpgradeDownloaded(_ filename: String) @endcode
  Swift: @code func onClientUpgradeDownloaded(_ filename: String) @endcode
  */
  */
-- (void) onClientUpgradeDownloaded: (NSString * _Nonnull)filename;
+- (void)onClientUpgradeDownloaded:(NSString * _Nonnull)filename;
 
 
 // TODO: Applies to iOS?
 // TODO: Applies to iOS?
 //func onClientVerificationRequired(nonce: String, ttlSeconds: Int, resetCache: Bool)
 //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.
  @param tunneledAppDelegate  The delegate implementation to use for callbacks.
  @return  The PsiphonTunnel instance.
  @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.
  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.
  @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.
  @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.
  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.
  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.
  @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 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 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 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 uploadPath  The path on the server to which the data will be loaded.
  @param uploadServerHeaders  The request headers that will be used when uploading.
  @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
 @end

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

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