Skip to content

Commit b617483

Browse files
authored
feat: SSL/TLS support (#40)
1 parent e6a3d30 commit b617483

File tree

18 files changed

+459
-212
lines changed

18 files changed

+459
-212
lines changed

.eslintrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"ignoreReadBeforeAssign": false
2424
}
2525
],
26-
"curly": ["error", "multi", "consistent"],
26+
"curly": ["error", "multi-line", "consistent"],
2727
"no-var": "error",
2828
"prefer-template": 2,
2929
"require-atomic-updates": "off",

README.md

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
![](https://github.com/Rapsssito/react-native-tcp-socket/workflows/tests/badge.svg)
33

44

5-
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.
5+
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).
66

77
## Table of Contents
88

99
- [Getting started](#getting-started)
10+
- [Self-Signed SSL](#self-signed-ssl-only-available-for-react-native--060)
1011
- [Compatibility](#react-native-compatibility)
1112
- [Usage](#usage)
12-
- [API](#icon-component)
13+
- [API](#api)
1314
- [Client](#client)
1415
- [Server](#server)
1516
- [Maintainers](#maintainers)
@@ -48,11 +49,23 @@ Linking the package manually is not required anymore with [Autolinking](https://
4849
}
4950
```
5051

51-
Modify your **`android/app/src/main/AndroidManifest.xml`** and add the following:
52-
```
53-
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
54-
```
55-
52+
#### Self-Signed SSL (only available for React Native > 0.60)
53+
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.
54+
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.
55+
56+
If you want to support `.pem` files (plus all the already supported files), your `metro.config.js` would like like this:
57+
```javascript
58+
const {getDefaultConfig} = require('metro-config');
59+
const defaultConfig = getDefaultConfig.getDefaultValues(__dirname);
60+
61+
module.exports = {
62+
resolver: {
63+
assetExts: [...defaultConfig.resolver.assetExts, 'pem'],
64+
},
65+
// ...
66+
};
67+
```
68+
5669

5770
#### Using React Native < 0.60
5871

@@ -105,7 +118,7 @@ import TcpSocket from 'react-native-tcp-socket';
105118
### Client
106119
```javascript
107120
// Create socket
108-
var client = TcpSocket.createConnection(options);
121+
const client = TcpSocket.createConnection(options);
109122

110123
client.on('data', function(data) {
111124
console.log('message was received', data);
@@ -125,9 +138,10 @@ client.write('Hello server!');
125138
// Close socket
126139
client.destroy();
127140
```
141+
128142
### Server
129143
```javascript
130-
var server = TcpSocket.createServer(function(socket) {
144+
const server = TcpSocket.createServer(function(socket) {
131145
socket.on('data', (data) => {
132146
socket.write('Echo server', data);
133147
});
@@ -150,6 +164,20 @@ server.on('close', () => {
150164
});
151165
```
152166

167+
### SSL Client
168+
```javascript
169+
const client = TcpSocket.createConnection({
170+
port: 8443,
171+
host: "example.com",
172+
tls: true,
173+
// tlsCheckValidity: false, // Disable validity checking
174+
// tlsCert: require('./selfmade.pem') // Self-signed certificate
175+
});
176+
177+
// ...
178+
```
179+
_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)._
180+
153181
## API
154182
### Client
155183
* **Methods:**
@@ -170,6 +198,9 @@ server.on('close', () => {
170198
| `localPort` | `<number>` ||| Local port the socket should connect from. If not specified, the OS will decide. |
171199
| `interface`| `<string>` ||| Interface the socket should connect from. If not specified, it will use the current active connection. The options are: `'wifi', 'ethernet', 'cellular'`. |
172200
| `reuseAddress`| `<boolean>` ||| Enable/disable the reuseAddress socket option. **Default**: `true`. |
201+
| `tls`| `<boolean>` ||| Enable/disable SSL/TLS socket creation. **Default**: `false`. |
202+
| `tlsCheckValidity`| `<boolean>` ||| Enable/disable SSL/TLS certificate validity check. **Default**: `true`. |
203+
| `tlsCert`| `<any>` ||| 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`. |
173204

174205
**Note**: The platforms marked as ❌ use the default value.
175206

@@ -201,7 +232,6 @@ server.on('close', () => {
201232
**Note**: The platforms marked as ❌ use the default value.
202233

203234
## Maintainers
204-
Looking for maintainers!
205235

206236
* [Rapsssito](https://github.com/rapsssito)
207237

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package com.asterinet.react.tcpsocket;
2+
3+
import android.annotation.SuppressLint;
4+
import android.content.Context;
5+
6+
import java.io.IOException;
7+
import java.io.InputStream;
8+
import java.net.URI;
9+
import java.security.GeneralSecurityException;
10+
import java.security.KeyStore;
11+
import java.security.SecureRandom;
12+
import java.security.cert.Certificate;
13+
import java.security.cert.CertificateFactory;
14+
import java.security.cert.X509Certificate;
15+
16+
import javax.net.ssl.SSLContext;
17+
import javax.net.ssl.SSLSocketFactory;
18+
import javax.net.ssl.TrustManager;
19+
import javax.net.ssl.TrustManagerFactory;
20+
import javax.net.ssl.X509TrustManager;
21+
22+
import androidx.annotation.NonNull;
23+
import androidx.annotation.RawRes;
24+
25+
final class SSLCertificateHelper {
26+
/**
27+
* Creates an SSLSocketFactory instance for use with all CAs provided.
28+
*
29+
* @return An SSLSocketFactory which trusts all CAs when provided to network clients
30+
*/
31+
static SSLSocketFactory createBlindSocketFactory() throws GeneralSecurityException {
32+
SSLContext ctx = SSLContext.getInstance("TLS");
33+
ctx.init(null, new TrustManager[]{new BlindTrustManager()}, null);
34+
return ctx.getSocketFactory();
35+
}
36+
37+
/**
38+
* Creates an SSLSocketFactory instance for use with the CA provided in the resource file.
39+
*
40+
* @param context Context used to open up the CA file
41+
* @param rawResourceUri Raw resource file to the CA (in .crt or .cer format, for instance)
42+
* @return An SSLSocketFactory which trusts the provided CA when provided to network clients
43+
*/
44+
static SSLSocketFactory createCustomTrustedSocketFactory(@NonNull final Context context, @NonNull final String rawResourceUri) throws IOException, GeneralSecurityException {
45+
InputStream caInput = getRawResourceStream(context, rawResourceUri);
46+
// Generate the CA Certificate from the raw resource file
47+
Certificate ca = CertificateFactory.getInstance("X.509").generateCertificate(caInput);
48+
caInput.close();
49+
// Load the key store using the CA
50+
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
51+
keyStore.load(null, null);
52+
keyStore.setCertificateEntry("ca", ca);
53+
54+
// Initialize the TrustManager with this CA
55+
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
56+
tmf.init(keyStore);
57+
58+
// Create an SSL context that uses the created trust manager
59+
SSLContext sslContext = SSLContext.getInstance("TLS");
60+
sslContext.init(null, tmf.getTrustManagers(), new SecureRandom());
61+
return sslContext.getSocketFactory();
62+
}
63+
64+
private static InputStream getRawResourceStream(@NonNull final Context context, @NonNull final String resourceUri) throws IOException {
65+
final int resId = getResourceId(context, resourceUri);
66+
if (resId == 0)
67+
return URI.create(resourceUri).toURL().openStream(); // From metro on development
68+
else return context.getResources().openRawResource(resId); // From bundle in production
69+
}
70+
71+
@RawRes
72+
private static int getResourceId(@NonNull final Context context, @NonNull final String resourceUri) {
73+
String name = resourceUri.toLowerCase().replace("-", "_");
74+
try {
75+
return Integer.parseInt(name);
76+
} catch (NumberFormatException ex) {
77+
return context.getResources().getIdentifier(name, "raw", context.getPackageName());
78+
}
79+
}
80+
81+
private static class BlindTrustManager implements X509TrustManager {
82+
public X509Certificate[] getAcceptedIssuers() {
83+
return null;
84+
}
85+
86+
@SuppressLint("TrustAllX509TrustManager")
87+
public void checkClientTrusted(X509Certificate[] chain, String authType) {
88+
}
89+
90+
@SuppressLint("TrustAllX509TrustManager")
91+
public void checkServerTrusted(X509Certificate[] chain, String authType) {
92+
}
93+
}
94+
}

android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketClient.java

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.asterinet.react.tcpsocket;
22

3+
import android.content.Context;
34
import android.net.Network;
45
import android.os.AsyncTask;
56
import android.util.Pair;
@@ -11,17 +12,21 @@
1112
import java.net.InetAddress;
1213
import java.net.InetSocketAddress;
1314
import java.net.Socket;
15+
import java.security.GeneralSecurityException;
16+
17+
import javax.net.SocketFactory;
18+
import javax.net.ssl.SSLSocket;
19+
import javax.net.ssl.SSLSocketFactory;
1420

1521
import androidx.annotation.NonNull;
1622
import androidx.annotation.Nullable;
1723

1824
class TcpSocketClient {
25+
private final int id;
1926
private TcpReceiverTask receiverTask;
2027
private Socket socket;
2128
private TcpReceiverTask.OnDataReceivedListener mReceiverListener;
2229

23-
private final int id;
24-
2530
TcpSocketClient(final int id) {
2631
this.id = id;
2732
}
@@ -42,9 +47,23 @@ public Socket getSocket() {
4247
return socket;
4348
}
4449

45-
public void connect(@NonNull final String address, @NonNull final Integer port, @NonNull final ReadableMap options, @Nullable final Network network) throws IOException {
50+
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 {
4651
if (socket != null) throw new IOException("Already connected");
47-
socket = new Socket();
52+
final boolean isTls = options.hasKey("tls") && options.getBoolean("tls");
53+
if (isTls) {
54+
SocketFactory sf;
55+
if (options.hasKey("tlsCheckValidity") && !options.getBoolean("tlsCheckValidity")){
56+
sf = SSLCertificateHelper.createBlindSocketFactory();
57+
} else {
58+
final String customTlsCert = options.hasKey("tlsCert") ? options.getString("tlsCert") : null;
59+
sf = customTlsCert != null ? SSLCertificateHelper.createCustomTrustedSocketFactory(context, customTlsCert) : SSLSocketFactory.getDefault();
60+
}
61+
final SSLSocket sslSocket = (SSLSocket) sf.createSocket();
62+
sslSocket.setUseClientMode(true);
63+
socket = sslSocket;
64+
} else {
65+
socket = new Socket();
66+
}
4867
// Get the addresses
4968
final String localAddress = options.hasKey("localAddress") ? options.getString("localAddress") : "0.0.0.0";
5069
final InetAddress localInetAddress = InetAddress.getByName(localAddress);
@@ -63,9 +82,11 @@ public void connect(@NonNull final String address, @NonNull final Integer port,
6382
// bind
6483
socket.bind(new InetSocketAddress(localInetAddress, localPort));
6584
socket.connect(new InetSocketAddress(remoteInetAddress, port));
85+
if (isTls) ((SSLSocket) socket).startHandshake();
6686
startListening();
6787
}
6888

89+
@SuppressWarnings("WeakerAccess")
6990
public void startListening() {
7091
//noinspection unchecked
7192
receiverTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Pair<>(this, mReceiverListener));
@@ -77,10 +98,11 @@ public void startListening() {
7798
* @param data data to be sent
7899
*/
79100
public void write(final byte[] data) throws IOException {
80-
if (socket != null && !socket.isClosed()) {
81-
OutputStream output = socket.getOutputStream();
82-
output.write(data);
101+
if (socket == null) {
102+
throw new IOException("Socket is not connected.");
83103
}
104+
OutputStream output = socket.getOutputStream();
105+
output.write(data);
84106
}
85107

86108
/**

android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketModule.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ protected void doInBackgroundGuarded(Void... params) {
8383
selectNetwork(iface, localAddress);
8484
client = new TcpSocketClient(TcpSocketModule.this, cId, null);
8585
socketClients.put(cId, client);
86-
client.connect(host, port, options, currentNetwork.getNetwork());
86+
client.connect(mReactContext, host, port, options, currentNetwork.getNetwork());
8787
onConnect(cId, host, port);
8888
} catch (Exception e) {
8989
onError(cId, e.getMessage());
@@ -288,7 +288,7 @@ public void onConnection(Integer serverId, Integer clientId, InetSocketAddress s
288288
sendEvent("connection", eventParams);
289289
}
290290

291-
private class CurrentNetwork {
291+
private static class CurrentNetwork {
292292
@Nullable
293293
Network network = null;
294294

examples/tcpsockets/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,6 @@ buck-out/
5959

6060
# CocoaPods
6161
/ios/Pods/
62+
63+
# TLS Cert
64+
*.pem

examples/tcpsockets/android/app/src/main/AndroidManifest.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
package="com.tcpsockets">
33

44
<uses-permission android:name="android.permission.INTERNET" />
5-
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
65

76
<application
87
android:name=".MainApplication"

examples/tcpsockets/ios/Podfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ PODS:
233233
- React-cxxreact (= 0.62.1)
234234
- React-jsi (= 0.62.1)
235235
- React-jsinspector (0.62.1)
236-
- react-native-tcp-socket (3.4.1):
236+
- react-native-tcp-socket (3.4.2):
237237
- CocoaAsyncSocket
238238
- React
239239
- React-RCTActionSheet (0.62.1):
@@ -430,7 +430,7 @@ SPEC CHECKSUMS:
430430
React-jsi: 600d8e42510c3254fd2abd702f4b9d3f598d8f52
431431
React-jsiexecutor: e9698dee4fd43ceb44832baf15d5745f455b0157
432432
React-jsinspector: f74a62727e5604119abd4a1eda52c0a12144bcd5
433-
react-native-tcp-socket: 8399fdf6128c294e52654d50f45649df1fde6cfb
433+
react-native-tcp-socket: a100fffbe7c96f92f457fa7fad6a29ec24bb8187
434434
React-RCTActionSheet: af8f28dd82fec89b8fe29637b8c779829e016a88
435435
React-RCTAnimation: 0d21fff7c20fb8ee41de5f2ebb63221127febd96
436436
React-RCTBlob: 9496bd93130b22069bfbc5d35e98653dae7c35c6

examples/tcpsockets/metro.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@
44
*
55
* @format
66
*/
7+
const {getDefaultConfig} = require('metro-config');
8+
const defaultConfig = getDefaultConfig.getDefaultValues(__dirname);
79

810
module.exports = {
11+
resolver: {
12+
assetExts: [...defaultConfig.resolver.assetExts, 'pem'],
13+
},
914
transformer: {
1015
getTransformOptions: async () => ({
1116
transform: {

0 commit comments

Comments
 (0)