diff --git a/.eslintrc.json b/.eslintrc.json index 54b3bec..3faa9d8 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -23,7 +23,7 @@ "ignoreReadBeforeAssign": false } ], - "curly": ["error", "multi", "consistent"], + "curly": ["error", "multi-line", "consistent"], "no-var": "error", "prefer-template": 2, "require-atomic-updates": "off", diff --git a/README.md b/README.md index bf570e2..cd6ea10 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,15 @@ ![](https://github.com/Rapsssito/react-native-tcp-socket/workflows/tests/badge.svg) -React Native TCP socket API for Android & iOS. It allows you to create TCP clients and servers sockets, simulating node's [net](https://nodejs.org/api/net.html) API. +React Native TCP socket API for Android & iOS with **client SSL/TLS support**. It allows you to create TCP clients and servers sockets, imitating some of node's [net](https://nodejs.org/api/net.html) API functionalities (check the available [API](#api) for more information). ## Table of Contents - [Getting started](#getting-started) + - [Self-Signed SSL](#self-signed-ssl-only-available-for-react-native--060) - [Compatibility](#react-native-compatibility) - [Usage](#usage) -- [API](#icon-component) +- [API](#api) - [Client](#client) - [Server](#server) - [Maintainers](#maintainers) @@ -48,11 +49,23 @@ Linking the package manually is not required anymore with [Autolinking](https:// } ``` - Modify your **`android/app/src/main/AndroidManifest.xml`** and add the following: - ``` - - ``` - +#### Self-Signed SSL (only available for React Native > 0.60) +You will need a [metro.config.js](https://facebook.github.io/metro/docs/en/configuration.html) file in order to use a self-signed SSL certificate. You should already have this file in your root project directory, but if you don't, create it. +Inside a `module.exports` object, create a key called `resolver` with another object called `assetExts`. The value of `assetExts` should be an array of the resource file extensions you want to support. + +If you want to support `.pem` files (plus all the already supported files), your `metro.config.js` would like like this: +```javascript +const {getDefaultConfig} = require('metro-config'); +const defaultConfig = getDefaultConfig.getDefaultValues(__dirname); + +module.exports = { + resolver: { + assetExts: [...defaultConfig.resolver.assetExts, 'pem'], + }, + // ... +}; +``` + #### Using React Native < 0.60 @@ -105,7 +118,7 @@ import TcpSocket from 'react-native-tcp-socket'; ### Client ```javascript // Create socket -var client = TcpSocket.createConnection(options); +const client = TcpSocket.createConnection(options); client.on('data', function(data) { console.log('message was received', data); @@ -125,9 +138,10 @@ client.write('Hello server!'); // Close socket client.destroy(); ``` + ### Server ```javascript -var server = TcpSocket.createServer(function(socket) { +const server = TcpSocket.createServer(function(socket) { socket.on('data', (data) => { socket.write('Echo server', data); }); @@ -150,6 +164,20 @@ server.on('close', () => { }); ``` +### SSL Client +```javascript +const client = TcpSocket.createConnection({ + port: 8443, + host: "example.com", + tls: true, + // tlsCheckValidity: false, // Disable validity checking + // tlsCert: require('./selfmade.pem') // Self-signed certificate +}); + +// ... +``` +_Note: In order to use self-signed certificates make sure to [update your metro.config.js configuration](#self-signed-ssl-only-available-for-react-native--060)._ + ## API ### Client * **Methods:** @@ -170,6 +198,9 @@ server.on('close', () => { | `localPort` | `` | ✅ | ✅ | Local port the socket should connect from. If not specified, the OS will decide. | | `interface`| `` | ❌ | ✅ | Interface the socket should connect from. If not specified, it will use the current active connection. The options are: `'wifi', 'ethernet', 'cellular'`. | | `reuseAddress`| `` | ❌ | ✅ | Enable/disable the reuseAddress socket option. **Default**: `true`. | +| `tls`| `` | ✅ | ✅ | Enable/disable SSL/TLS socket creation. **Default**: `false`. | +| `tlsCheckValidity`| `` | ✅ | ✅ | Enable/disable SSL/TLS certificate validity check. **Default**: `true`. | +| `tlsCert`| `` | ✅ | ✅ | CA file (.pem format) to trust. If `null`, it will use the device's default SSL trusted list. Useful for self-signed certificates. _See [example](#ssl-client) for more info_. **Default**: `null`. | **Note**: The platforms marked as ❌ use the default value. @@ -201,7 +232,6 @@ server.on('close', () => { **Note**: The platforms marked as ❌ use the default value. ## Maintainers -Looking for maintainers! * [Rapsssito](https://github.com/rapsssito) diff --git a/android/src/main/java/com/asterinet/react/tcpsocket/SSLCertificateHelper.java b/android/src/main/java/com/asterinet/react/tcpsocket/SSLCertificateHelper.java new file mode 100644 index 0000000..b938d75 --- /dev/null +++ b/android/src/main/java/com/asterinet/react/tcpsocket/SSLCertificateHelper.java @@ -0,0 +1,94 @@ +package com.asterinet.react.tcpsocket; + +import android.annotation.SuppressLint; +import android.content.Context; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +import androidx.annotation.NonNull; +import androidx.annotation.RawRes; + +final class SSLCertificateHelper { + /** + * Creates an SSLSocketFactory instance for use with all CAs provided. + * + * @return An SSLSocketFactory which trusts all CAs when provided to network clients + */ + static SSLSocketFactory createBlindSocketFactory() throws GeneralSecurityException { + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(null, new TrustManager[]{new BlindTrustManager()}, null); + return ctx.getSocketFactory(); + } + + /** + * Creates an SSLSocketFactory instance for use with the CA provided in the resource file. + * + * @param context Context used to open up the CA file + * @param rawResourceUri Raw resource file to the CA (in .crt or .cer format, for instance) + * @return An SSLSocketFactory which trusts the provided CA when provided to network clients + */ + static SSLSocketFactory createCustomTrustedSocketFactory(@NonNull final Context context, @NonNull final String rawResourceUri) throws IOException, GeneralSecurityException { + InputStream caInput = getRawResourceStream(context, rawResourceUri); + // Generate the CA Certificate from the raw resource file + Certificate ca = CertificateFactory.getInstance("X.509").generateCertificate(caInput); + caInput.close(); + // Load the key store using the CA + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null, null); + keyStore.setCertificateEntry("ca", ca); + + // Initialize the TrustManager with this CA + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(keyStore); + + // Create an SSL context that uses the created trust manager + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, tmf.getTrustManagers(), new SecureRandom()); + return sslContext.getSocketFactory(); + } + + private static InputStream getRawResourceStream(@NonNull final Context context, @NonNull final String resourceUri) throws IOException { + final int resId = getResourceId(context, resourceUri); + if (resId == 0) + return URI.create(resourceUri).toURL().openStream(); // From metro on development + else return context.getResources().openRawResource(resId); // From bundle in production + } + + @RawRes + private static int getResourceId(@NonNull final Context context, @NonNull final String resourceUri) { + String name = resourceUri.toLowerCase().replace("-", "_"); + try { + return Integer.parseInt(name); + } catch (NumberFormatException ex) { + return context.getResources().getIdentifier(name, "raw", context.getPackageName()); + } + } + + private static class BlindTrustManager implements X509TrustManager { + public X509Certificate[] getAcceptedIssuers() { + return null; + } + + @SuppressLint("TrustAllX509TrustManager") + public void checkClientTrusted(X509Certificate[] chain, String authType) { + } + + @SuppressLint("TrustAllX509TrustManager") + public void checkServerTrusted(X509Certificate[] chain, String authType) { + } + } +} diff --git a/android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketClient.java b/android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketClient.java index 4db6067..ce259e4 100644 --- a/android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketClient.java +++ b/android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketClient.java @@ -1,5 +1,6 @@ package com.asterinet.react.tcpsocket; +import android.content.Context; import android.net.Network; import android.os.AsyncTask; import android.util.Pair; @@ -11,17 +12,21 @@ import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; +import java.security.GeneralSecurityException; + +import javax.net.SocketFactory; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; import androidx.annotation.NonNull; import androidx.annotation.Nullable; class TcpSocketClient { + private final int id; private TcpReceiverTask receiverTask; private Socket socket; private TcpReceiverTask.OnDataReceivedListener mReceiverListener; - private final int id; - TcpSocketClient(final int id) { this.id = id; } @@ -42,9 +47,23 @@ public Socket getSocket() { return socket; } - public void connect(@NonNull final String address, @NonNull final Integer port, @NonNull final ReadableMap options, @Nullable final Network network) throws IOException { + public void connect(@NonNull final Context context, @NonNull final String address, @NonNull final Integer port, @NonNull final ReadableMap options, @Nullable final Network network) throws IOException, GeneralSecurityException { if (socket != null) throw new IOException("Already connected"); - socket = new Socket(); + final boolean isTls = options.hasKey("tls") && options.getBoolean("tls"); + if (isTls) { + SocketFactory sf; + if (options.hasKey("tlsCheckValidity") && !options.getBoolean("tlsCheckValidity")){ + sf = SSLCertificateHelper.createBlindSocketFactory(); + } else { + final String customTlsCert = options.hasKey("tlsCert") ? options.getString("tlsCert") : null; + sf = customTlsCert != null ? SSLCertificateHelper.createCustomTrustedSocketFactory(context, customTlsCert) : SSLSocketFactory.getDefault(); + } + final SSLSocket sslSocket = (SSLSocket) sf.createSocket(); + sslSocket.setUseClientMode(true); + socket = sslSocket; + } else { + socket = new Socket(); + } // Get the addresses final String localAddress = options.hasKey("localAddress") ? options.getString("localAddress") : "0.0.0.0"; final InetAddress localInetAddress = InetAddress.getByName(localAddress); @@ -63,9 +82,11 @@ public void connect(@NonNull final String address, @NonNull final Integer port, // bind socket.bind(new InetSocketAddress(localInetAddress, localPort)); socket.connect(new InetSocketAddress(remoteInetAddress, port)); + if (isTls) ((SSLSocket) socket).startHandshake(); startListening(); } + @SuppressWarnings("WeakerAccess") public void startListening() { //noinspection unchecked receiverTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Pair<>(this, mReceiverListener)); @@ -77,10 +98,11 @@ public void startListening() { * @param data data to be sent */ public void write(final byte[] data) throws IOException { - if (socket != null && !socket.isClosed()) { - OutputStream output = socket.getOutputStream(); - output.write(data); + if (socket == null) { + throw new IOException("Socket is not connected."); } + OutputStream output = socket.getOutputStream(); + output.write(data); } /** diff --git a/android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketModule.java b/android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketModule.java index bce0750..abbccca 100644 --- a/android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketModule.java +++ b/android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketModule.java @@ -83,7 +83,7 @@ protected void doInBackgroundGuarded(Void... params) { selectNetwork(iface, localAddress); client = new TcpSocketClient(TcpSocketModule.this, cId, null); socketClients.put(cId, client); - client.connect(host, port, options, currentNetwork.getNetwork()); + client.connect(mReactContext, host, port, options, currentNetwork.getNetwork()); onConnect(cId, host, port); } catch (Exception e) { onError(cId, e.getMessage()); @@ -288,7 +288,7 @@ public void onConnection(Integer serverId, Integer clientId, InetSocketAddress s sendEvent("connection", eventParams); } - private class CurrentNetwork { + private static class CurrentNetwork { @Nullable Network network = null; diff --git a/examples/tcpsockets/.gitignore b/examples/tcpsockets/.gitignore index 57331d6..ea0571e 100644 --- a/examples/tcpsockets/.gitignore +++ b/examples/tcpsockets/.gitignore @@ -59,3 +59,6 @@ buck-out/ # CocoaPods /ios/Pods/ + +# TLS Cert +*.pem \ No newline at end of file diff --git a/examples/tcpsockets/android/app/src/main/java/com/tcpsockets/ReactNativeFlipper.java b/examples/tcpsockets/android/app/src/debug/java/com/tcpsockets/ReactNativeFlipper.java similarity index 100% rename from examples/tcpsockets/android/app/src/main/java/com/tcpsockets/ReactNativeFlipper.java rename to examples/tcpsockets/android/app/src/debug/java/com/tcpsockets/ReactNativeFlipper.java diff --git a/examples/tcpsockets/android/app/src/main/AndroidManifest.xml b/examples/tcpsockets/android/app/src/main/AndroidManifest.xml index 21fa74a..826a0a2 100644 --- a/examples/tcpsockets/android/app/src/main/AndroidManifest.xml +++ b/examples/tcpsockets/android/app/src/main/AndroidManifest.xml @@ -2,7 +2,6 @@ package="com.tcpsockets"> - ({ transform: { diff --git a/examples/tcpsockets/package.json b/examples/tcpsockets/package.json index 416fb7f..a4d7b29 100644 --- a/examples/tcpsockets/package.json +++ b/examples/tcpsockets/package.json @@ -14,7 +14,7 @@ "dependencies": { "react": "16.11.0", "react-native": "0.62.1", - "react-native-tcp-socket": "^3.4.1" + "react-native-tcp-socket": "https://github.com/Rapsssito/react-native-tcp-socket#tls" }, "devDependencies": { "@babel/core": "^7.7.2", diff --git a/ios/Podfile.lock b/ios/Podfile.lock index bb7c0f4..fc16322 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2,14 +2,14 @@ PODS: - boost-for-react-native (1.63.0) - CocoaAsyncSocket (7.6.3) - DoubleConversion (1.1.6) - - FBLazyVector (0.61.4) - - FBReactNativeSpec (0.61.4): + - FBLazyVector (0.61.5) + - FBReactNativeSpec (0.61.5): - Folly (= 2018.10.22.00) - - RCTRequired (= 0.61.4) - - RCTTypeSafety (= 0.61.4) - - React-Core (= 0.61.4) - - React-jsi (= 0.61.4) - - ReactCommon/turbomodule/core (= 0.61.4) + - RCTRequired (= 0.61.5) + - RCTTypeSafety (= 0.61.5) + - React-Core (= 0.61.5) + - React-jsi (= 0.61.5) + - ReactCommon/turbomodule/core (= 0.61.5) - Folly (2018.10.22.00): - boost-for-react-native - DoubleConversion @@ -20,204 +20,204 @@ PODS: - DoubleConversion - glog - glog (0.3.5) - - RCTRequired (0.61.4) - - RCTTypeSafety (0.61.4): - - FBLazyVector (= 0.61.4) + - RCTRequired (0.61.5) + - RCTTypeSafety (0.61.5): + - FBLazyVector (= 0.61.5) - Folly (= 2018.10.22.00) - - RCTRequired (= 0.61.4) - - React-Core (= 0.61.4) - - React (0.61.4): - - React-Core (= 0.61.4) - - React-Core/DevSupport (= 0.61.4) - - React-Core/RCTWebSocket (= 0.61.4) - - React-RCTActionSheet (= 0.61.4) - - React-RCTAnimation (= 0.61.4) - - React-RCTBlob (= 0.61.4) - - React-RCTImage (= 0.61.4) - - React-RCTLinking (= 0.61.4) - - React-RCTNetwork (= 0.61.4) - - React-RCTSettings (= 0.61.4) - - React-RCTText (= 0.61.4) - - React-RCTVibration (= 0.61.4) - - React-Core (0.61.4): + - RCTRequired (= 0.61.5) + - React-Core (= 0.61.5) + - React (0.61.5): + - React-Core (= 0.61.5) + - React-Core/DevSupport (= 0.61.5) + - React-Core/RCTWebSocket (= 0.61.5) + - React-RCTActionSheet (= 0.61.5) + - React-RCTAnimation (= 0.61.5) + - React-RCTBlob (= 0.61.5) + - React-RCTImage (= 0.61.5) + - React-RCTLinking (= 0.61.5) + - React-RCTNetwork (= 0.61.5) + - React-RCTSettings (= 0.61.5) + - React-RCTText (= 0.61.5) + - React-RCTVibration (= 0.61.5) + - React-Core (0.61.5): - Folly (= 2018.10.22.00) - glog - - React-Core/Default (= 0.61.4) - - React-cxxreact (= 0.61.4) - - React-jsi (= 0.61.4) - - React-jsiexecutor (= 0.61.4) + - React-Core/Default (= 0.61.5) + - React-cxxreact (= 0.61.5) + - React-jsi (= 0.61.5) + - React-jsiexecutor (= 0.61.5) - Yoga - - React-Core/CoreModulesHeaders (0.61.4): + - React-Core/CoreModulesHeaders (0.61.5): - Folly (= 2018.10.22.00) - glog - React-Core/Default - - React-cxxreact (= 0.61.4) - - React-jsi (= 0.61.4) - - React-jsiexecutor (= 0.61.4) + - React-cxxreact (= 0.61.5) + - React-jsi (= 0.61.5) + - React-jsiexecutor (= 0.61.5) - Yoga - - React-Core/Default (0.61.4): + - React-Core/Default (0.61.5): - Folly (= 2018.10.22.00) - glog - - React-cxxreact (= 0.61.4) - - React-jsi (= 0.61.4) - - React-jsiexecutor (= 0.61.4) + - React-cxxreact (= 0.61.5) + - React-jsi (= 0.61.5) + - React-jsiexecutor (= 0.61.5) - Yoga - - React-Core/DevSupport (0.61.4): + - React-Core/DevSupport (0.61.5): - Folly (= 2018.10.22.00) - glog - - React-Core/Default (= 0.61.4) - - React-Core/RCTWebSocket (= 0.61.4) - - React-cxxreact (= 0.61.4) - - React-jsi (= 0.61.4) - - React-jsiexecutor (= 0.61.4) - - React-jsinspector (= 0.61.4) + - React-Core/Default (= 0.61.5) + - React-Core/RCTWebSocket (= 0.61.5) + - React-cxxreact (= 0.61.5) + - React-jsi (= 0.61.5) + - React-jsiexecutor (= 0.61.5) + - React-jsinspector (= 0.61.5) - Yoga - - React-Core/RCTActionSheetHeaders (0.61.4): + - React-Core/RCTActionSheetHeaders (0.61.5): - Folly (= 2018.10.22.00) - glog - React-Core/Default - - React-cxxreact (= 0.61.4) - - React-jsi (= 0.61.4) - - React-jsiexecutor (= 0.61.4) + - React-cxxreact (= 0.61.5) + - React-jsi (= 0.61.5) + - React-jsiexecutor (= 0.61.5) - Yoga - - React-Core/RCTAnimationHeaders (0.61.4): + - React-Core/RCTAnimationHeaders (0.61.5): - Folly (= 2018.10.22.00) - glog - React-Core/Default - - React-cxxreact (= 0.61.4) - - React-jsi (= 0.61.4) - - React-jsiexecutor (= 0.61.4) + - React-cxxreact (= 0.61.5) + - React-jsi (= 0.61.5) + - React-jsiexecutor (= 0.61.5) - Yoga - - React-Core/RCTBlobHeaders (0.61.4): + - React-Core/RCTBlobHeaders (0.61.5): - Folly (= 2018.10.22.00) - glog - React-Core/Default - - React-cxxreact (= 0.61.4) - - React-jsi (= 0.61.4) - - React-jsiexecutor (= 0.61.4) + - React-cxxreact (= 0.61.5) + - React-jsi (= 0.61.5) + - React-jsiexecutor (= 0.61.5) - Yoga - - React-Core/RCTImageHeaders (0.61.4): + - React-Core/RCTImageHeaders (0.61.5): - Folly (= 2018.10.22.00) - glog - React-Core/Default - - React-cxxreact (= 0.61.4) - - React-jsi (= 0.61.4) - - React-jsiexecutor (= 0.61.4) + - React-cxxreact (= 0.61.5) + - React-jsi (= 0.61.5) + - React-jsiexecutor (= 0.61.5) - Yoga - - React-Core/RCTLinkingHeaders (0.61.4): + - React-Core/RCTLinkingHeaders (0.61.5): - Folly (= 2018.10.22.00) - glog - React-Core/Default - - React-cxxreact (= 0.61.4) - - React-jsi (= 0.61.4) - - React-jsiexecutor (= 0.61.4) + - React-cxxreact (= 0.61.5) + - React-jsi (= 0.61.5) + - React-jsiexecutor (= 0.61.5) - Yoga - - React-Core/RCTNetworkHeaders (0.61.4): + - React-Core/RCTNetworkHeaders (0.61.5): - Folly (= 2018.10.22.00) - glog - React-Core/Default - - React-cxxreact (= 0.61.4) - - React-jsi (= 0.61.4) - - React-jsiexecutor (= 0.61.4) + - React-cxxreact (= 0.61.5) + - React-jsi (= 0.61.5) + - React-jsiexecutor (= 0.61.5) - Yoga - - React-Core/RCTSettingsHeaders (0.61.4): + - React-Core/RCTSettingsHeaders (0.61.5): - Folly (= 2018.10.22.00) - glog - React-Core/Default - - React-cxxreact (= 0.61.4) - - React-jsi (= 0.61.4) - - React-jsiexecutor (= 0.61.4) + - React-cxxreact (= 0.61.5) + - React-jsi (= 0.61.5) + - React-jsiexecutor (= 0.61.5) - Yoga - - React-Core/RCTTextHeaders (0.61.4): + - React-Core/RCTTextHeaders (0.61.5): - Folly (= 2018.10.22.00) - glog - React-Core/Default - - React-cxxreact (= 0.61.4) - - React-jsi (= 0.61.4) - - React-jsiexecutor (= 0.61.4) + - React-cxxreact (= 0.61.5) + - React-jsi (= 0.61.5) + - React-jsiexecutor (= 0.61.5) - Yoga - - React-Core/RCTVibrationHeaders (0.61.4): + - React-Core/RCTVibrationHeaders (0.61.5): - Folly (= 2018.10.22.00) - glog - React-Core/Default - - React-cxxreact (= 0.61.4) - - React-jsi (= 0.61.4) - - React-jsiexecutor (= 0.61.4) + - React-cxxreact (= 0.61.5) + - React-jsi (= 0.61.5) + - React-jsiexecutor (= 0.61.5) - Yoga - - React-Core/RCTWebSocket (0.61.4): + - React-Core/RCTWebSocket (0.61.5): - Folly (= 2018.10.22.00) - glog - - React-Core/Default (= 0.61.4) - - React-cxxreact (= 0.61.4) - - React-jsi (= 0.61.4) - - React-jsiexecutor (= 0.61.4) + - React-Core/Default (= 0.61.5) + - React-cxxreact (= 0.61.5) + - React-jsi (= 0.61.5) + - React-jsiexecutor (= 0.61.5) - Yoga - - React-CoreModules (0.61.4): - - FBReactNativeSpec (= 0.61.4) + - React-CoreModules (0.61.5): + - FBReactNativeSpec (= 0.61.5) - Folly (= 2018.10.22.00) - - RCTTypeSafety (= 0.61.4) - - React-Core/CoreModulesHeaders (= 0.61.4) - - React-RCTImage (= 0.61.4) - - ReactCommon/turbomodule/core (= 0.61.4) - - React-cxxreact (0.61.4): + - RCTTypeSafety (= 0.61.5) + - React-Core/CoreModulesHeaders (= 0.61.5) + - React-RCTImage (= 0.61.5) + - ReactCommon/turbomodule/core (= 0.61.5) + - React-cxxreact (0.61.5): - boost-for-react-native (= 1.63.0) - DoubleConversion - Folly (= 2018.10.22.00) - glog - - React-jsinspector (= 0.61.4) - - React-jsi (0.61.4): + - React-jsinspector (= 0.61.5) + - React-jsi (0.61.5): - boost-for-react-native (= 1.63.0) - DoubleConversion - Folly (= 2018.10.22.00) - glog - - React-jsi/Default (= 0.61.4) - - React-jsi/Default (0.61.4): + - React-jsi/Default (= 0.61.5) + - React-jsi/Default (0.61.5): - boost-for-react-native (= 1.63.0) - DoubleConversion - Folly (= 2018.10.22.00) - glog - - React-jsiexecutor (0.61.4): + - React-jsiexecutor (0.61.5): - DoubleConversion - Folly (= 2018.10.22.00) - glog - - React-cxxreact (= 0.61.4) - - React-jsi (= 0.61.4) - - React-jsinspector (0.61.4) - - React-RCTActionSheet (0.61.4): - - React-Core/RCTActionSheetHeaders (= 0.61.4) - - React-RCTAnimation (0.61.4): - - React-Core/RCTAnimationHeaders (= 0.61.4) - - React-RCTBlob (0.61.4): - - React-Core/RCTBlobHeaders (= 0.61.4) - - React-Core/RCTWebSocket (= 0.61.4) - - React-jsi (= 0.61.4) - - React-RCTNetwork (= 0.61.4) - - React-RCTImage (0.61.4): - - React-Core/RCTImageHeaders (= 0.61.4) - - React-RCTNetwork (= 0.61.4) - - React-RCTLinking (0.61.4): - - React-Core/RCTLinkingHeaders (= 0.61.4) - - React-RCTNetwork (0.61.4): - - React-Core/RCTNetworkHeaders (= 0.61.4) - - React-RCTSettings (0.61.4): - - React-Core/RCTSettingsHeaders (= 0.61.4) - - React-RCTText (0.61.4): - - React-Core/RCTTextHeaders (= 0.61.4) - - React-RCTVibration (0.61.4): - - React-Core/RCTVibrationHeaders (= 0.61.4) - - ReactCommon/jscallinvoker (0.61.4): + - React-cxxreact (= 0.61.5) + - React-jsi (= 0.61.5) + - React-jsinspector (0.61.5) + - React-RCTActionSheet (0.61.5): + - React-Core/RCTActionSheetHeaders (= 0.61.5) + - React-RCTAnimation (0.61.5): + - React-Core/RCTAnimationHeaders (= 0.61.5) + - React-RCTBlob (0.61.5): + - React-Core/RCTBlobHeaders (= 0.61.5) + - React-Core/RCTWebSocket (= 0.61.5) + - React-jsi (= 0.61.5) + - React-RCTNetwork (= 0.61.5) + - React-RCTImage (0.61.5): + - React-Core/RCTImageHeaders (= 0.61.5) + - React-RCTNetwork (= 0.61.5) + - React-RCTLinking (0.61.5): + - React-Core/RCTLinkingHeaders (= 0.61.5) + - React-RCTNetwork (0.61.5): + - React-Core/RCTNetworkHeaders (= 0.61.5) + - React-RCTSettings (0.61.5): + - React-Core/RCTSettingsHeaders (= 0.61.5) + - React-RCTText (0.61.5): + - React-Core/RCTTextHeaders (= 0.61.5) + - React-RCTVibration (0.61.5): + - React-Core/RCTVibrationHeaders (= 0.61.5) + - ReactCommon/jscallinvoker (0.61.5): - DoubleConversion - Folly (= 2018.10.22.00) - glog - - React-cxxreact (= 0.61.4) - - ReactCommon/turbomodule/core (0.61.4): + - React-cxxreact (= 0.61.5) + - ReactCommon/turbomodule/core (0.61.5): - DoubleConversion - Folly (= 2018.10.22.00) - glog - - React-Core (= 0.61.4) - - React-cxxreact (= 0.61.4) - - React-jsi (= 0.61.4) - - ReactCommon/jscallinvoker (= 0.61.4) + - React-Core (= 0.61.5) + - React-cxxreact (= 0.61.5) + - React-jsi (= 0.61.5) + - ReactCommon/jscallinvoker (= 0.61.5) - Yoga (1.14.0) DEPENDENCIES: @@ -312,30 +312,30 @@ SPEC CHECKSUMS: boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c CocoaAsyncSocket: eafaa68a7e0ec99ead0a7b35015e0bf25d2c8987 DoubleConversion: 5805e889d232975c086db112ece9ed034df7a0b2 - FBLazyVector: feb35a6b7f7b50f367be07f34012f34a79282fa3 - FBReactNativeSpec: 51477b84b1bf7ab6f9ef307c24e3dd675391be44 + FBLazyVector: aaeaf388755e4f29cd74acbc9e3b8da6d807c37f + FBReactNativeSpec: 118d0d177724c2d67f08a59136eb29ef5943ec75 Folly: 30e7936e1c45c08d884aa59369ed951a8e68cf51 glog: 1f3da668190260b06b429bb211bfbee5cd790c28 - RCTRequired: f3b3fb6f4723e8e52facb229d0c75fdc76773849 - RCTTypeSafety: 2ec60de6abb1db050b56ecc4b60188026078fd10 - React: 10e0130b57e55a7cd8c3dee37c1261102ce295f4 - React-Core: 636212410772d05f3a1eb79d965df2962ca1c70b - React-CoreModules: 6f70d5e41919289c582f88c9ad9923fe5c87400a - React-cxxreact: ddecbe9157ec1743f52ea17bf8d95debc0d6e846 - React-jsi: ca921f4041505f9d5197139b2d09eeb020bb12e8 - React-jsiexecutor: 8dfb73b987afa9324e4009bdce62a18ce23d983c - React-jsinspector: d15478d0a8ada19864aa4d1cc1c697b41b3fa92f - React-RCTActionSheet: 7369b7c85f99b6299491333affd9f01f5a130c22 - React-RCTAnimation: d07be15b2bd1d06d89417eb0343f98ffd2b099a7 - React-RCTBlob: 8e0b23d95c9baa98f6b0e127e07666aaafd96c34 - React-RCTImage: 443050d14a66e8c2332e9c055f45689d23e15cc7 - React-RCTLinking: ce9a90ba155aec41be49e75ec721bbae2d48a47e - React-RCTNetwork: 41fe54bacc67dd00e6e4c4d30dd98a13e4beabc8 - React-RCTSettings: 45e3e0a6470310b2dab2ccc6d1d73121ba3ea936 - React-RCTText: 21934e0a51d522abcd0a275407e80af45d6fd9ec - React-RCTVibration: 0f76400ee3cec6edb9c125da49fed279340d145a - ReactCommon: a6a294e7028ed67b926d29551aa9394fd989c24c - Yoga: ba3d99dbee6c15ea6bbe3783d1f0cb1ffb79af0f + RCTRequired: b153add4da6e7dbc44aebf93f3cf4fcae392ddf1 + RCTTypeSafety: 9aa1b91d7f9310fc6eadc3cf95126ffe818af320 + React: b6a59ef847b2b40bb6e0180a97d0ca716969ac78 + React-Core: 688b451f7d616cc1134ac95295b593d1b5158a04 + React-CoreModules: d04f8494c1a328b69ec11db9d1137d667f916dcb + React-cxxreact: d0f7bcafa196ae410e5300736b424455e7fb7ba7 + React-jsi: cb2cd74d7ccf4cffb071a46833613edc79cdf8f7 + React-jsiexecutor: d5525f9ed5f782fdbacb64b9b01a43a9323d2386 + React-jsinspector: fa0ecc501688c3c4c34f28834a76302233e29dc0 + React-RCTActionSheet: 600b4d10e3aea0913b5a92256d2719c0cdd26d76 + React-RCTAnimation: 791a87558389c80908ed06cc5dfc5e7920dfa360 + React-RCTBlob: d89293cc0236d9cb0933d85e430b0bbe81ad1d72 + React-RCTImage: 6b8e8df449eb7c814c99a92d6b52de6fe39dea4e + React-RCTLinking: 121bb231c7503cf9094f4d8461b96a130fabf4a5 + React-RCTNetwork: fb353640aafcee84ca8b78957297bd395f065c9a + React-RCTSettings: 8db258ea2a5efee381fcf7a6d5044e2f8b68b640 + React-RCTText: 9ccc88273e9a3aacff5094d2175a605efa854dbe + React-RCTVibration: a49a1f42bf8f5acf1c3e297097517c6b3af377ad + ReactCommon: 198c7c8d3591f975e5431bec1b0b3b581aa1c5dd + Yoga: f2a7cd4280bfe2cca5a7aed98ba0eb3d1310f18b PODFILE CHECKSUM: c17fa9414d86be6752251d37471f1e2e374a215b diff --git a/ios/TcpSocketClient.h b/ios/TcpSocketClient.h index 1f760c0..5706f0b 100644 --- a/ios/TcpSocketClient.h +++ b/ios/TcpSocketClient.h @@ -1,8 +1,3 @@ -/** - * Copyright (c) 2015-present, Peel Technologies, Inc. - * All rights reserved. - */ - #import #import "CocoaAsyncSocket/GCDAsyncSocket.h" diff --git a/ios/TcpSocketClient.m b/ios/TcpSocketClient.m index 9f6d1c6..4df8096 100644 --- a/ios/TcpSocketClient.m +++ b/ios/TcpSocketClient.m @@ -1,8 +1,3 @@ -/** - * Copyright (c) 2015-present, Peel Technologies, Inc. - * All rights reserved. - */ - #import #import #import "TcpSocketClient.h" @@ -14,6 +9,9 @@ @interface TcpSocketClient() { @private + BOOL _tls; + BOOL _checkValidity; + NSString *_certPath; GCDAsyncSocket *_tcpSocket; NSMutableDictionary *_pendingSends; NSLock *_lock; @@ -67,8 +65,8 @@ - (BOOL)connect:(NSString *)host port:(int)port withOptions:(NSDictionary *)opti BOOL result = false; - NSString *localAddress = (options?options[@"localAddress"]:nil); - NSNumber *localPort = (options?options[@"localPort"]:nil); + NSString *localAddress = options[@"localAddress"]; + NSNumber *localPort = options[@"localPort"]; if (!localAddress && !localPort) { result = [_tcpSocket connectToHost:host onPort:port error:error]; @@ -84,7 +82,25 @@ - (BOOL)connect:(NSString *)host port:(int)port withOptions:(NSDictionary *)opti withTimeout:-1 error:error]; } - + _tls = (options[@"tls"]?[options[@"tls"] boolValue]:false); + if (result && _tls){ + NSMutableDictionary *settings = [NSMutableDictionary dictionary]; + NSString *certResourcePath = options[@"tlsCert"]; + BOOL checkValidity = (options[@"tlsCheckValidity"]?[options[@"tlsCheckValidity"] boolValue]:true); + if (!checkValidity) { + // Do not validate + _checkValidity = false; + [settings setObject:[NSNumber numberWithBool:YES] forKey:GCDAsyncSocketManuallyEvaluateTrust]; + } else if (certResourcePath != nil) { + // Self-signed certificate + _certPath = certResourcePath; + [settings setObject:[NSNumber numberWithBool:YES] forKey:GCDAsyncSocketManuallyEvaluateTrust]; + } else { + // Default certificates + [settings setObject:host forKey:(NSString *) kCFStreamSSLPeerName]; + } + [_tcpSocket startTLS:settings]; + } return result; } @@ -225,15 +241,84 @@ - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSo [newSocket readDataWithTimeout:-1 tag:inComing.id.longValue]; } -- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port +- (void)socketDidSecure:(GCDAsyncSocket *)sock { + // Only for TLS if (!_clientDelegate) { - RCTLogWarn(@"didConnectToHost with nil clientDelegate for %@", [sock userData]); + RCTLogWarn(@"socketDidSecure with nil clientDelegate for %@", [sock userData]); return; } [_clientDelegate onConnect:self]; +} + +- (void)socket:(GCDAsyncSocket *)sock didReceiveTrust:(SecTrustRef)trust completionHandler:(void (^)(BOOL shouldTrustPeer))completionHandler { + // Check if we should check the validity + if (!_checkValidity) { + completionHandler(YES); + return; + } + + // Server certificate + SecCertificateRef serverCertificate = SecTrustGetCertificateAtIndex(trust, 0); + CFDataRef serverCertificateData = SecCertificateCopyData(serverCertificate); + const UInt8* const serverData = CFDataGetBytePtr(serverCertificateData); + const CFIndex serverDataSize = CFDataGetLength(serverCertificateData); + NSData* cert1 = [NSData dataWithBytes:serverData length:(NSUInteger)serverDataSize]; + + // Local certificate + NSURL *certUrl = [[NSURL alloc] initWithString:_certPath]; + NSString *pem = [[NSString alloc] initWithContentsOfURL:certUrl encoding:NSUTF8StringEncoding error:NULL]; + + // Strip PEM header and footers. We don't support multi-certificate PEM. + NSMutableString *pemMutable = [pem stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet].mutableCopy; + + // Strip PEM header and footer + [pemMutable replaceOccurrencesOfString:@"-----BEGIN CERTIFICATE-----" + withString:@"" + options:(NSStringCompareOptions)(NSAnchoredSearch | NSLiteralSearch) + range:NSMakeRange(0, pemMutable.length)]; + + [pemMutable replaceOccurrencesOfString:@"-----END CERTIFICATE-----" + withString:@"" + options:(NSStringCompareOptions)(NSAnchoredSearch | NSBackwardsSearch | NSLiteralSearch) + range:NSMakeRange(0, pemMutable.length)]; + + NSData *pemData = [[NSData alloc] initWithBase64EncodedString:pemMutable options:NSDataBase64DecodingIgnoreUnknownCharacters]; + SecCertificateRef localCertificate = SecCertificateCreateWithData(NULL, (CFDataRef)pemData); + if (!localCertificate) + { + [NSException raise:@"Configuration invalid" format:@"Failed to parse PEM certificate"]; + } + + CFDataRef myCertData = SecCertificateCopyData(localCertificate); + const UInt8* const localData = CFDataGetBytePtr(myCertData); + const CFIndex localDataSize = CFDataGetLength(myCertData); + NSData* cert2 = [NSData dataWithBytes:localData length:(NSUInteger)localDataSize]; + + if (cert1 == nil || cert2 == nil) { + RCTLogWarn(@"BAD SSL CERTIFICATE"); + completionHandler(NO); + return; + } + if ([cert1 isEqualToData:cert2]) { + completionHandler(YES); + }else { + completionHandler(NO); + } +} +- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port +{ + if (!_clientDelegate) { + RCTLogWarn(@"didConnectToHost with nil clientDelegate for %@", [sock userData]); + return; + } + + // Show up if SSL handsake is done + if (!_tls) { + [_clientDelegate onConnect:self]; + } [sock readDataWithTimeout:-1 tag:_id.longValue]; } diff --git a/ios/TcpSockets.h b/ios/TcpSockets.h index 47ba018..cf63914 100644 --- a/ios/TcpSockets.h +++ b/ios/TcpSockets.h @@ -1,8 +1,3 @@ -/** - * Copyright (c) 2015-present, Peel Technologies, Inc. - * All rights reserved. - */ - #import "TcpSocketClient.h" #import diff --git a/ios/TcpSockets.m b/ios/TcpSockets.m index 1135f51..81473cb 100644 --- a/ios/TcpSockets.m +++ b/ios/TcpSockets.m @@ -1,8 +1,3 @@ -/** - * Copyright (c) 2015-present, Peel Technologies, Inc. - * All rights reserved. - */ - #import #import #import diff --git a/package.json b/package.json index 4fddf6b..5a0dabd 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "react-native-tcp-socket", "title": "React Native Tcp Socket", "version": "3.4.2", - "description": "React Native TCP socket API for Android & iOS", + "description": "React Native TCP socket API for Android & iOS with SSL/TLS support", "main": "src/index.js", "types": "lib/types/index.d.ts", "scripts": { @@ -34,6 +34,8 @@ "iOS", "Android", "tcp-socket", + "tls", + "ssl", "tcp-server", "tcp-client", "tcp", diff --git a/src/TcpSocket.js b/src/TcpSocket.js index 1c585de..a08f818 100644 --- a/src/TcpSocket.js +++ b/src/TcpSocket.js @@ -1,6 +1,6 @@ 'use strict'; -import { NativeModules } from 'react-native'; +import { NativeModules, Image } from 'react-native'; const Buffer = (global.Buffer = global.Buffer || require('buffer').Buffer); const Sockets = NativeModules.TcpSockets; @@ -32,7 +32,18 @@ class RemovableListener { } /** - * @typedef {{ port: number; host?: string; timeout?: number; localAddress?: string, localPort?: number, interface?: 'wifi' | 'cellular' | 'ethernet', reuseAddress?: boolean}} ConnectionOptions + * @typedef {{ + * port: number; + * host?: string; + * timeout?: number; + * localAddress?: string, + * localPort?: number, + * interface?: 'wifi' | 'cellular' | 'ethernet', + * reuseAddress?: boolean, + * tls?: boolean, + * tlsCheckValidity?: boolean, + * tlsCert?: any, + * }} ConnectionOptions */ export default class TcpSocket { /** @@ -121,18 +132,25 @@ export default class TcpSocket { */ connect(options, callback) { this._registerEvents(); + const customOptions = { ...options }; // Normalize args - options.host = options.host || 'localhost'; - options.port = Number(options.port) || 0; + customOptions.host = customOptions.host || 'localhost'; + customOptions.port = Number(customOptions.port) || 0; const connectListener = this.on('connect', (ev) => { connectListener.remove(); if (callback) callback(ev.address); }); - if (options.timeout) this.setTimeout(options.timeout); + // Timeout + if (customOptions.timeout) this.setTimeout(customOptions.timeout); else if (this._timeout) this._activeTimer(this._timeout.msecs); + // TLS Cert + if (customOptions.tlsCert) { + customOptions.tlsCert = Image.resolveAssetSource(customOptions.tlsCert).uri; + } + // console.log(getAndroidResourceIdentifier(customOptions.tlsCert)); this._state = STATE.CONNECTING; this._destroyed = false; - Sockets.connect(this._id, options.host, options.port, options); + Sockets.connect(this._id, customOptions.host, customOptions.port, customOptions); return this; } @@ -293,13 +311,17 @@ export default class TcpSocket { * @param {BufferEncoding} [encoding] */ _generateSendBuffer(buffer, encoding) { - if (typeof buffer === 'string') return Buffer.from(buffer, encoding); - else if (Buffer.isBuffer(buffer)) return buffer; - else if (buffer instanceof Uint8Array || Array.isArray(buffer)) return Buffer.from(buffer); - else + if (typeof buffer === 'string') { + return Buffer.from(buffer, encoding); + } else if (Buffer.isBuffer(buffer)) { + return buffer; + } else if (buffer instanceof Uint8Array || Array.isArray(buffer)) { + return Buffer.from(buffer); + } else { throw new TypeError( `Invalid data, chunk must be a string or buffer, not ${typeof buffer}` ); + } } /**