From fbf7c152a888deb0e085b8622e530361f1f1d20f Mon Sep 17 00:00:00 2001 From: Adam Burdette Date: Wed, 9 Feb 2022 10:57:40 -0500 Subject: [PATCH] fix(ios): avoid memory leak from ssid APIs by adding explicit config (#560) * Fix #420 - Location Permissions Required `CNCopyCurrentNetworkInfo` will leak without location permissions being enabled. Added comments for depreciations & implementation notes that need future work. * 420 - Pass config to native iOS & gate on new prop Added new exported method for passing configuration to native iOS. Storing it there on the instance as a property and gating based on that config. It will not fetch SSIDs if config is nil. Also cleaned up a bit of typing. * 420 - Fix mock * 420 Updated README Co-authored-by: Mike Hardy --- README.md | 8 ++++++-- ios/RNCNetInfo.m | 20 +++++++++++++++++--- src/index.ts | 8 +++++++- src/internal/__mocks__/nativeModule.ts | 1 + src/internal/defaultConfiguration.ts | 7 ++++++- src/internal/defaultConfiguration.web.ts | 7 ++++++- src/internal/nativeModule.web.ts | 4 ++++ src/internal/privateTypes.ts | 3 ++- src/internal/types.ts | 1 + 9 files changed, 50 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 4f8639e5..65b1075d 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,7 @@ The `details` value depends on the `type` value. | Property | Platform | Type | Description | | ----------------------- | --------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------- | | `isConnectionExpensive` | Android, iOS, macOS, Windows, Web | `boolean` | If the network connection is considered "expensive". This could be in either energy or monetary terms. | -| `ssid` | Android, iOS (not tvOS), Windows | `string` | The SSID of the network. May not be present, `null`, or an empty string if it cannot be determined. **On iOS, make sure your app meets at least one of the [following requirements](https://developer.apple.com/documentation/systemconfiguration/1614126-cncopycurrentnetworkinfo?language=objc#discussion). On Android, you need to have the `ACCESS_FINE_LOCATION` permission in your `AndroidManifest.xml` and accepted by the user**. | +| `ssid` | Android, iOS (not tvOS), Windows | `string` | The SSID of the network. May not be present, `null`, or an empty string if it cannot be determined. **On iOS, your app must meet at least one of the [following requirements](https://developer.apple.com/documentation/systemconfiguration/1614126-cncopycurrentnetworkinfo?language=objc#discussion) and you must set the `shouldFetchWiFiSSID` configuration option or no attempt will be made to fetch the SSID. On Android, you need to have the `ACCESS_FINE_LOCATION` permission in your `AndroidManifest.xml` and accepted by the user**. | | `bssid` | Android, iOS (not tvOS), Windows* | `string` | The BSSID of the network. May not be present, `null`, or an empty string if it cannot be determined. **On iOS, make sure your app meets at least one of the [following requirements](https://developer.apple.com/documentation/systemconfiguration/1614126-cncopycurrentnetworkinfo?language=objc#discussion). On Android, you need to have the `ACCESS_FINE_LOCATION` permission in your `AndroidManifest.xml` and accepted by the user**. | | `strength` | Android, Windows | `number` | An integer number from `0` to `100` for the signal strength. May not be present if the signal strength cannot be determined. | | `ipAddress` | Android, iOS, macOS | `string` | The external IP address. Can be in IPv4 or IPv6 format. May not be present if it cannot be determined. | @@ -240,7 +240,9 @@ The configuration options for the library. | `reachabilityShortTimeout` | `number` | 5 seconds | The number of milliseconds between internet reachability checks when the internet was not previously detected. Only used on platforms which do not supply internet reachability natively. | | `reachabilityLongTimeout` | `number` | 60 seconds | The number of milliseconds between internet reachability checks when the internet was previously detected. Only used on platforms which do not supply internet reachability natively. | | `reachabilityRequestTimeout` | `number` | 15 seconds | The number of milliseconds that a reachability check is allowed to take before failing. Only used on platforms which do not supply internet reachability natively. | -| `reachabilityShouldRun` | `() => boolean` | `() => true` | A function which returns a boolean to determine if checkInternetReachability should be run. +| `reachabilityShouldRun` | `() => boolean` | `() => true` | A function which returns a boolean to determine if checkInternetReachability should be run. | +| `shouldFetchWiFiSSID` | `boolean` | `false` | A flag indicating one of the requirements on iOS has been met to retrieve the network (B)SSID, and the native SSID retrieval APIs should be called. This has no effect on Android. + ### Methods @@ -259,6 +261,7 @@ NetInfo.configure({ reachabilityShortTimeout: 5 * 1000, // 5s reachabilityRequestTimeout: 15 * 1000, // 15s reachabilityShouldRun: () => true, + shouldFetchWiFiSSID: true // met iOS requirements to get SSID. Will leak memory if set to true without meeting requirements. }); ``` @@ -313,6 +316,7 @@ const YourComponent = () => { reachabilityShortTimeout: 5 * 1000, // 5s reachabilityRequestTimeout: 15 * 1000, // 15s reachabilityShouldRun: () => true, + shouldFetchWiFiSSID: true // met iOS requirements to get SSID }); // ... diff --git a/ios/RNCNetInfo.m b/ios/RNCNetInfo.m index 732ea0a2..84a0b5e7 100644 --- a/ios/RNCNetInfo.m +++ b/ios/RNCNetInfo.m @@ -25,6 +25,7 @@ @interface RNCNetInfo () @property (nonatomic, strong) RNCConnectionStateWatcher *connectionStateWatcher; @property (nonatomic) BOOL isObserving; +@property (nonatomic) NSDictionary *config; @end @@ -96,6 +97,11 @@ - (void)connectionStateWatcher:(RNCConnectionStateWatcher *)connectionStateWatch resolve([self currentDictionaryFromUpdateState:state withInterface:requestedInterface]); } +RCT_EXPORT_METHOD(configure:(NSDictionary *)config) +{ + self.config = config; +} + #pragma mark - Utilities // Converts the state into a dictionary to send over the bridge @@ -124,9 +130,15 @@ - (NSMutableDictionary *)detailsFromInterface:(nonnull NSString *)requestedInter } else if ([requestedInterface isEqualToString: RNCConnectionTypeWifi] || [requestedInterface isEqualToString: RNCConnectionTypeEthernet]) { details[@"ipAddress"] = [self ipAddress] ?: NSNull.null; details[@"subnet"] = [self subnet] ?: NSNull.null; - #if !TARGET_OS_TV && !TARGET_OS_OSX - details[@"ssid"] = [self ssid] ?: NSNull.null; - details[@"bssid"] = [self bssid] ?: NSNull.null; + #if !TARGET_OS_TV && !TARGET_OS_OSX + /* + Without one of the conditions needed to use CNCopyCurrentNetworkInfo, it will leak memory. + Clients should only set the shouldFetchWiFiSSID to true after ensuring requirements are met to get (B)SSID. + */ + if (self.config && self.config[@"shouldFetchWiFiSSID"]) { + details[@"ssid"] = [self ssid] ?: NSNull.null; + details[@"bssid"] = [self bssid] ?: NSNull.null; + } #endif } return details; @@ -222,6 +234,7 @@ - (NSString *)ssid NSDictionary *SSIDInfo; NSString *SSID = NULL; for (NSString *interfaceName in interfaceNames) { + // CNCopyCurrentNetworkInfo is deprecated for iOS 13+, need to override & use fetchCurrentWithCompletionHandler SSIDInfo = CFBridgingRelease(CNCopyCurrentNetworkInfo((__bridge CFStringRef)interfaceName)); if (SSIDInfo.count > 0) { SSID = SSIDInfo[@"SSID"]; @@ -240,6 +253,7 @@ - (NSString *)bssid NSDictionary *networkDetails; NSString *BSSID = NULL; for (NSString *interfaceName in interfaceNames) { + // CNCopyCurrentNetworkInfo is deprecated for iOS 13+, need to override & use fetchCurrentWithCompletionHandler networkDetails = CFBridgingRelease(CNCopyCurrentNetworkInfo((__bridge CFStringRef)interfaceName)); if (networkDetails.count > 0) { diff --git a/src/index.ts b/src/index.ts index 14618523..d0cd1f76 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,12 +8,14 @@ */ import {useState, useEffect} from 'react'; +import {Platform} from 'react-native'; import DEFAULT_CONFIGURATION from './internal/defaultConfiguration'; +import NativeInterface from './internal/nativeInterface'; import State from './internal/state'; import * as Types from './internal/types'; // Stores the currently used configuration -let _configuration: Types.NetInfoConfiguration = DEFAULT_CONFIGURATION; +let _configuration = DEFAULT_CONFIGURATION; // Stores the singleton reference to the state manager let _state: State | null = null; @@ -40,6 +42,10 @@ export function configure( _state.tearDown(); _state = createState(); } + + if (Platform.OS === 'ios') { + NativeInterface.configure(configuration); + } } /** diff --git a/src/internal/__mocks__/nativeModule.ts b/src/internal/__mocks__/nativeModule.ts index 08993caf..902fe078 100644 --- a/src/internal/__mocks__/nativeModule.ts +++ b/src/internal/__mocks__/nativeModule.ts @@ -4,6 +4,7 @@ /* eslint-env jest */ const RNCNetInfoMock = { + configure: jest.fn(), getCurrentState: jest.fn(), addListener: jest.fn(), removeListeners: jest.fn(), diff --git a/src/internal/defaultConfiguration.ts b/src/internal/defaultConfiguration.ts index 869fb3ba..71404f6f 100644 --- a/src/internal/defaultConfiguration.ts +++ b/src/internal/defaultConfiguration.ts @@ -1,4 +1,6 @@ -export default { +import * as Types from './types'; + +const DEFAULT_CONFIGURATION: Types.NetInfoConfiguration = { reachabilityUrl: 'https://clients3.google.com/generate_204', reachabilityTest: (response: Response): Promise => Promise.resolve(response.status === 204), @@ -6,4 +8,7 @@ export default { reachabilityLongTimeout: 60 * 1000, // 60s reachabilityRequestTimeout: 15 * 1000, // 15s reachabilityShouldRun: (): boolean => true, + shouldFetchWiFiSSID: false }; + +export default DEFAULT_CONFIGURATION; \ No newline at end of file diff --git a/src/internal/defaultConfiguration.web.ts b/src/internal/defaultConfiguration.web.ts index 19076d9f..daafabe1 100644 --- a/src/internal/defaultConfiguration.web.ts +++ b/src/internal/defaultConfiguration.web.ts @@ -1,4 +1,6 @@ -export default { +import * as Types from './types'; + +const DEFAULT_CONFIGURATION: Types.NetInfoConfiguration = { reachabilityUrl: '/', reachabilityTest: (response: Response): Promise => Promise.resolve(response.status === 200), @@ -6,4 +8,7 @@ export default { reachabilityLongTimeout: 60 * 1000, // 60s reachabilityRequestTimeout: 15 * 1000, // 15s reachabilityShouldRun: (): boolean => true, + shouldFetchWiFiSSID: true }; + +export default DEFAULT_CONFIGURATION \ No newline at end of file diff --git a/src/internal/nativeModule.web.ts b/src/internal/nativeModule.web.ts index 392348e9..8c55d907 100644 --- a/src/internal/nativeModule.web.ts +++ b/src/internal/nativeModule.web.ts @@ -285,6 +285,10 @@ const RNCNetInfo: NetInfoNativeModule = { async getCurrentState(requestedInterface): Promise { return getCurrentState(requestedInterface); }, + + configure(): void { + return; + }, }; export default RNCNetInfo; diff --git a/src/internal/privateTypes.ts b/src/internal/privateTypes.ts index a2bd16af..1868c316 100644 --- a/src/internal/privateTypes.ts +++ b/src/internal/privateTypes.ts @@ -7,7 +7,7 @@ * @format */ -import {NetInfoState} from './types'; +import {NetInfoConfiguration, NetInfoState} from './types'; export const DEVICE_CONNECTIVITY_EVENT = 'netInfo.networkStatusDidChange'; @@ -22,6 +22,7 @@ export interface Events { } export interface NetInfoNativeModule { + configure: (config: Partial) => void; getCurrentState: ( requestedInterface?: string, ) => Promise; diff --git a/src/internal/types.ts b/src/internal/types.ts index f21cf00b..aef647ff 100644 --- a/src/internal/types.ts +++ b/src/internal/types.ts @@ -114,4 +114,5 @@ export interface NetInfoConfiguration { reachabilityShortTimeout: number; reachabilityRequestTimeout: number; reachabilityShouldRun: () => boolean; + shouldFetchWiFiSSID: boolean; }