Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
50 changes: 40 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
```
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
```

#### 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

Expand Down Expand Up @@ -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);
Expand All @@ -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);
});
Expand All @@ -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:**
Expand All @@ -170,6 +198,9 @@ server.on('close', () => {
| `localPort` | `<number>` | ✅ | ✅ | Local port the socket should connect from. If not specified, the OS will decide. |
| `interface`| `<string>` | ❌ | ✅ | Interface the socket should connect from. If not specified, it will use the current active connection. The options are: `'wifi', 'ethernet', 'cellular'`. |
| `reuseAddress`| `<boolean>` | ❌ | ✅ | Enable/disable the reuseAddress socket option. **Default**: `true`. |
| `tls`| `<boolean>` | ✅ | ✅ | Enable/disable SSL/TLS socket creation. **Default**: `false`. |
| `tlsCheckValidity`| `<boolean>` | ✅ | ✅ | Enable/disable SSL/TLS certificate validity check. **Default**: `true`. |
| `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`. |

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

Expand Down Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
@@ -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) {
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
}
Expand All @@ -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);
Expand All @@ -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));
Expand All @@ -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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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;

Expand Down
3 changes: 3 additions & 0 deletions examples/tcpsockets/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,6 @@ buck-out/

# CocoaPods
/ios/Pods/

# TLS Cert
*.pem
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
package="com.tcpsockets">

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

<application
android:name=".MainApplication"
Expand Down
4 changes: 2 additions & 2 deletions examples/tcpsockets/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ PODS:
- React-cxxreact (= 0.62.1)
- React-jsi (= 0.62.1)
- React-jsinspector (0.62.1)
- react-native-tcp-socket (3.4.1):
- react-native-tcp-socket (3.4.2):
- CocoaAsyncSocket
- React
- React-RCTActionSheet (0.62.1):
Expand Down Expand Up @@ -430,7 +430,7 @@ SPEC CHECKSUMS:
React-jsi: 600d8e42510c3254fd2abd702f4b9d3f598d8f52
React-jsiexecutor: e9698dee4fd43ceb44832baf15d5745f455b0157
React-jsinspector: f74a62727e5604119abd4a1eda52c0a12144bcd5
react-native-tcp-socket: 8399fdf6128c294e52654d50f45649df1fde6cfb
react-native-tcp-socket: a100fffbe7c96f92f457fa7fad6a29ec24bb8187
React-RCTActionSheet: af8f28dd82fec89b8fe29637b8c779829e016a88
React-RCTAnimation: 0d21fff7c20fb8ee41de5f2ebb63221127febd96
React-RCTBlob: 9496bd93130b22069bfbc5d35e98653dae7c35c6
Expand Down
5 changes: 5 additions & 0 deletions examples/tcpsockets/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@
*
* @format
*/
const {getDefaultConfig} = require('metro-config');
const defaultConfig = getDefaultConfig.getDefaultValues(__dirname);

module.exports = {
resolver: {
assetExts: [...defaultConfig.resolver.assetExts, 'pem'],
},
transformer: {
getTransformOptions: async () => ({
transform: {
Expand Down
2 changes: 1 addition & 1 deletion examples/tcpsockets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading