diff --git a/README.md b/README.md
index a117af5..f4a5a23 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
# React Native Tor
-A fully featured Tor Daemon and Onion Routing Client for your React Native iOS and Android Project!
+A fully featured Tor Daemon , Onion Routing Client and Hidden Service HTTP server for your React Native iOS and Android Project!
:calling: :closed_lock_with_key: :globe_with_meridians:
## TL;DR
In your project:
@@ -43,7 +43,7 @@ If you think this is awesome, please consider [contributing to Privacy and Opens
- Embeds a fully functional Tor Daemon, with its own circuit (non exit) removing the dependency on Orbot and allowing Tor usage on IOS.
- Provides a Socks5 proxy enabled REST client to allow you to make Rest calls on Onion URLs directly from JS just as you would with Axios, Frisbee etc..
- Tcp socket support via a event like interface!
-- [WIP] Start a hidden service accessible via an Onion URL directly on your phone (in final test for upcoming 0.0.2 release)
+- Start a hidden service and HTTP server accessible via an Onion URL directly on your phone!
- Provides guard functions and state management options to autostart/stop the daemon when REST calls are initiated and/or the application is backgrounded/foregrounded
- TS Typed API for sanity.
@@ -156,6 +156,51 @@ Note:
- This will cause the module to drop the TcpConnection and remove all data event listeners.
- Should you wish to reconnect to the target you must initiate a new connection by calling createTcpConnection again.
+### Hidden Service (HS) Example (BETA)
+Note: This feature is still experimental *please* do report any bugs and strange behavoir you experience.
+
+```js
+// create a new hidden service
+// Accepting connections on port 20000 and forwarding the request to port 20011
+const hs = await client.createHiddenService(
+ 20000,
+ 20011,
+
+);
+//
+const {secretKey , onionUrl } = hs;
+// Store `secretKey` ESCDA base64 private key securley so you can re-create this HS later
+// secureStorage.save('key',secretKey);
+// Share your OnionURl with people you want to connect to your hidden service
+
+// Start http server with a callback to process data recieved (type HttpServiceRequest)
+// or errors (This example just creates an Alert whenever an HTTP request is recieved)
+const handler = client.startHttpService(
+ hiddenServiceDestinationPort,
+ (d, e) => {
+ Alert.alert(
+ `Got HTTP ${e ? 'Error' : 'Request'} `,
+ `${JSON.stringify(d)} ${JSON.stringify(e)}`
+ );
+ })
+
+
+// .. later on (once finished)
+// Stop the HTTP server
+handler.close()
+// delete hidden service using the onionURL as it's index
+client.deleteHiddenService(onionUrl);
+
+// Restore the hidden service using the secret key you had stored!
+// secretKey = secureStorage.load("key");
+const hs = await client.createHiddenService(
+ 20000,
+ 20011,
+ secretKey
+);
+// ..repeat above steps to attach an HTTP server and callback
+//
+```
## API reference
Please reference [Typescript defs and JSDoc](./src/index.tsx) for details.
@@ -356,23 +401,17 @@ Know someone who want to add a bit more privacy to their Application / Product ?
- Better API Docs
- Event emitter from Rust to Native on Boostrap status
-- Enable secret service API
- - Start new hidden service on phone.
- - Restore hidden service from key.
- Capture daemon logs into files.
### Backlog
+- Add Raw TCP socket listeners to Hidden Services.
- Search for available ports for socks proxy for iOS
-- Return a Context API (status, etc..) as part of the package to make it easier for developers to build reactive components on topof.
- Build on Request capability
- PUT calls
- DELETE
- Add body support
- - ~Sockets~
- Streaming ?
-- ~Investigate stability builds on older mobile API's (Currently minSdk is Android 26 and iOS 10)~
-- Investigate the possibility of creating a NetworkExtension on iOS which act as a VPN for the app which regular REST libaries can be used on.
## License
diff --git a/android/libs/sifir_android.aar b/android/libs/sifir_android.aar
index cd96638..ddc7e26 100644
Binary files a/android/libs/sifir_android.aar and b/android/libs/sifir_android.aar differ
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index ea941a1..1567a9c 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -1,4 +1,4 @@
-
+
diff --git a/android/src/main/java/com/reactnativetor/TorBridgeRequest.kt b/android/src/main/java/com/reactnativetor/TorBridgeRequest.kt
index c05024f..2af3a76 100644
--- a/android/src/main/java/com/reactnativetor/TorBridgeRequest.kt
+++ b/android/src/main/java/com/reactnativetor/TorBridgeRequest.kt
@@ -37,7 +37,7 @@ class TorBridgeRequest constructor(
}
}
is RequestResult.Success -> mPromise!!.resolve(result.result)
- else -> mPromise!!.reject("Unable to process RequestResult: Exhaustive Clause")
+ else -> mPromise!!.reject("Unable to process RequestResult","RequestResult Exhaustive Clause")
}
mPromise = null
}
@@ -47,7 +47,7 @@ class TorBridgeRequest constructor(
val request = when (param.method.toUpperCase()) {
"POST" -> {
// Check Content-Type headers provided
- // Currently only support application/x-www-form-urlencoded
+ // Currently only supports application/x-www-form-urlencoded
// If not provided defaults to application/json
// TODO Expand supported content formats ?
val body = when (param.headers?.get("Content-Type") ?: "application/json") {
diff --git a/android/src/main/java/com/reactnativetor/TorModule.kt b/android/src/main/java/com/reactnativetor/TorModule.kt
index 5c3ef64..37ea6fb 100644
--- a/android/src/main/java/com/reactnativetor/TorModule.kt
+++ b/android/src/main/java/com/reactnativetor/TorModule.kt
@@ -1,10 +1,12 @@
package com.reactnativetor
+import android.content.Context
import android.util.Log
import com.facebook.react.bridge.*
import com.sifir.tor.DataObserver
import com.sifir.tor.OwnedTorService
import com.sifir.tor.TcpSocksStream
+import com.sifir.tor.HiddenServiceHandler
import okhttp3.OkHttpClient
import java.io.IOException
import java.net.InetSocketAddress
@@ -15,6 +17,7 @@ import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter
+import org.json.JSONObject
import java.util.UUID;
import java.util.concurrent.*
@@ -52,13 +55,35 @@ class DataObserverEmitter(
}
}
+class HttpHandlerObserverEmitter(
+ private val handlerId: String,
+ private val reactContext: ReactApplicationContext,
+ private val serviceHandlers: HashMap
+) : DataObserver {
+ override fun onData(p0: String?) {
+ reactContext
+ .getJSModule(RCTDeviceEventEmitter::class.java)
+ .emit("$handlerId-data", p0)
+ }
+
+ override fun onError(p0: String?) {
+ reactContext
+ .getJSModule(RCTDeviceEventEmitter::class.java)
+ .emit("$handlerId-error", p0)
+ }
+}
+
class TorModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
+ private var _client: OkHttpClient? = null;
private var service: OwnedTorService? = null;
private var proxy: Proxy? = null;
private var _starting: Boolean = false;
private var _streams: HashMap = HashMap();
-// private val executorService: ExecutorService = Executors.newFixedThreadPool(4)
- private val executorService : ThreadPoolExecutor = ThreadPoolExecutor(4,4, 0L, TimeUnit.MILLISECONDS, LinkedBlockingQueue());
+ private val _serviceHandlers: HashMap = HashMap();
+
+ // private val executorService: ExecutorService = Executors.newFixedThreadPool(4)
+ private val executorService: ThreadPoolExecutor =
+ ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, LinkedBlockingQueue(50));
/**
@@ -124,24 +149,33 @@ class TorModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMod
method: String,
jsonBody: String,
headers: ReadableMap,
+ // FIXME move this to startDeamon call
trustAllSSl: Boolean,
promise: Promise
) {
if (service == null) {
promise.reject(Throwable("Service Not Initialized!, Call startDaemon first"));
+ return;
}
- var client = (if (trustAllSSl) getUnsafeOkHttpClient() else OkHttpClient().newBuilder())
- .proxy(proxy)
- .connectTimeout(10, TimeUnit.SECONDS)
- .writeTimeout(10, TimeUnit.SECONDS)
- .readTimeout(10, TimeUnit.SECONDS)
- .build()
+// if(_client !is OkHttpClient){
+// _client = (if (trustAllSSl) getUnsafeOkHttpClient() else OkHttpClient().newBuilder())
+// .proxy(proxy)
+// .connectTimeout(10, TimeUnit.SECONDS)
+// .writeTimeout(10, TimeUnit.SECONDS)
+// .readTimeout(10, TimeUnit.SECONDS)
+// .build()
+// }
+
+ if(_client !is OkHttpClient){
+ promise.reject(Throwable("Request http client not Initialized!, Call startDaemon first"));
+ return;
+ }
val param = TaskParam(method, url, jsonBody, headers.toHashMap())
executorService.execute {
try {
- val task = TorBridgeRequest(promise, client, param);
+ val task = TorBridgeRequest(promise, _client!!, param);
task.run()
} catch (e: Exception) {
Log.d("TorBridge", "error on request: $e")
@@ -155,9 +189,11 @@ class TorModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMod
fun startDaemon(timeoutMs: Double, promise: Promise) {
if (service != null) {
promise.reject(Throwable("Service already running, call stopDaemon first"))
+ return;
}
if (this._starting) {
promise.reject(Throwable("Service already starting"))
+ return;
}
_starting = true;
executorService.execute {
@@ -169,10 +205,18 @@ class TorModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMod
service = it
proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress("0.0.0.0", socksPort))
_starting = false;
+
+ _client = getUnsafeOkHttpClient()
+ .proxy(proxy)
+ .connectTimeout(10, TimeUnit.SECONDS)
+ .writeTimeout(10, TimeUnit.SECONDS)
+ .readTimeout(10, TimeUnit.SECONDS)
+ .build();
+
promise.resolve(socksPort);
}, {
_starting = false;
- promise.reject(it);
+ promise.reject("StartDaemon Error", "Error starting Tor Daemon", it);
}).run();
} catch (e: Exception) {
@@ -243,7 +287,7 @@ class TorModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMod
throw Throwable("Tor Service not running, call startDaemon first")
}
var stream = _streams[connId]
- ?: throw Throwable("Stream for connectionId $connId is not initialized, call startTcpConn first");
+ ?: throw Throwable("Stream for connectionId $connId is not initialized, call startTcpConn first");
stream.send_data(msg, timeoutSec.toLong());
promise.resolve(true);
} catch (e: Exception) {
@@ -265,4 +309,81 @@ class TorModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMod
promise.reject(e)
}
}
+
+ @ReactMethod
+ fun createHiddenService(
+ hiddenServicePort: Int,
+ destinationPort: Int,
+ secretKey: String,
+ promise: Promise
+ ) {
+ try {
+ if (service == null) {
+ promise.reject(Throwable("Service Not Initialized!, Call startDaemon first"));
+ return;
+ }
+ val serviceDetails =
+ service!!.create_hidden_service(destinationPort,hiddenServicePort, secretKey);
+
+ val result = HashMap();
+
+ result.set("onionUrl", serviceDetails.get_onion_url());
+ result.set("secretKey", serviceDetails.get_secret_b64());
+
+ promise.resolve(JSONObject(result).toString());
+
+ } catch (e: Exception) {
+ Log.d("TorBridge", "error on createHiddenService : $e")
+ promise.reject(e)
+ }
+ }
+ @ReactMethod
+ fun deleteHiddenService(
+ onion: String,
+ promise: Promise
+ ) {
+ try {
+ if (service == null) {
+ promise.reject(Throwable("Service Not Initialized!, Call startDaemon first"));
+ return;
+ }
+ service!!.delete_hidden_service(onion);
+
+ promise.resolve(true);
+
+ } catch (e: Exception) {
+ Log.d("TorBridge", "error on deleteHiddenService : $e")
+ promise.reject(e)
+ }
+ }
+
+ @ReactMethod
+ fun startHttpHiddenserviceHandler(port: Int, promise: Promise) {
+ try {
+ val uuid = UUID.randomUUID();
+ val serviceId = uuid.toString();
+ val serviceHandler = HiddenServiceHandler(
+ port,
+ HttpHandlerObserverEmitter(serviceId, this.reactApplicationContext, _serviceHandlers)
+ );
+ _serviceHandlers.set(serviceId, serviceHandler);
+ promise.resolve(serviceId);
+ } catch (e: Exception) {
+ Log.d("TorBridge", "error on startHttpHiddenserviceHandler : $e")
+ promise.reject(e)
+ }
+
+ @ReactMethod
+ fun stopHttpHiddenserviceHandler(id: String, promise: Promise) {
+ try {
+ _serviceHandlers.remove(id)?.delete();
+ promise.resolve(true);
+ } catch (e: Exception) {
+ Log.d("TorBridge", "error on stopHttpHiddenserviceHandler for connection Id $id : $e")
+ promise.reject(e)
+ }
+
+ }
+
+ }
}
diff --git a/android/src/main/java/com/reactnativetor/TorPackage.kt b/android/src/main/java/com/reactnativetor/TorPackage.kt
index 3adcf09..a0b41ce 100644
--- a/android/src/main/java/com/reactnativetor/TorPackage.kt
+++ b/android/src/main/java/com/reactnativetor/TorPackage.kt
@@ -15,7 +15,8 @@ class TorPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List {
val manager = reactContext.getPackageManager();
val ai = manager.getApplicationInfo(reactContext.packageName, PackageManager.GET_META_DATA);
- System.load("${ai.nativeLibraryDir}/libsifir_android.so");
+ System.loadLibrary("sifir_android")
+// System.load("${ai.nativeLibraryDir}/libsifir_android.so");
return Arrays.asList(TorModule(reactContext))
}
diff --git a/example/ios/Podfile b/example/ios/Podfile
index 4be5337..a6ccecb 100644
--- a/example/ios/Podfile
+++ b/example/ios/Podfile
@@ -1,24 +1,24 @@
platform :ios, '11.1'
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
-def add_flipper_pods!
- version = '~> 0.33.1'
- pod 'FlipperKit', version, :configuration => 'Debug'
- pod 'FlipperKit/FlipperKitLayoutPlugin', version, :configuration => 'Debug'
- pod 'FlipperKit/SKIOSNetworkPlugin', version, :configuration => 'Debug'
- pod 'FlipperKit/FlipperKitUserDefaultsPlugin', version, :configuration => 'Debug'
- pod 'FlipperKit/FlipperKitReactPlugin', version, :configuration => 'Debug'
-end
-# Post Install processing for Flipper
-def flipper_post_install(installer)
- installer.pods_project.targets.each do |target|
- if target.name == 'YogaKit'
- target.build_configurations.each do |config|
- config.build_settings['SWIFT_VERSION'] = '4.1'
- end
- end
- end
-end
+#def add_flipper_pods!
+# version = '~> 0.33.1'
+# pod 'FlipperKit', version, :configuration => 'Debug'
+# pod 'FlipperKit/FlipperKitLayoutPlugin', version, :configuration => 'Debug'
+# pod 'FlipperKit/SKIOSNetworkPlugin', version, :configuration => 'Debug'
+# pod 'FlipperKit/FlipperKitUserDefaultsPlugin', version, :configuration => 'Debug'
+# pod 'FlipperKit/FlipperKitReactPlugin', version, :configuration => 'Debug'
+#end
+## Post Install processing for Flipper
+#def flipper_post_install(installer)
+# installer.pods_project.targets.each do |target|
+# if target.name == 'YogaKit'
+# target.build_configurations.each do |config|
+# config.build_settings['SWIFT_VERSION'] = '4.1'
+# end
+# end
+# end
+#end
target 'TorExample' do
# Pods for TorExample
diff --git a/example/package.json b/example/package.json
index 1f95a4b..69d0fa9 100644
--- a/example/package.json
+++ b/example/package.json
@@ -10,7 +10,7 @@
},
"dependencies": {
"react": "16.11.0",
- "react-native": "0.62.2"
+ "react-native": "0.62.3"
},
"devDependencies": {
"@babel/core": "^7.9.6",
diff --git a/example/src/App.tsx b/example/src/App.tsx
index 3702297..1f1b7a7 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { StyleSheet, View, Text, Button, TextInput } from 'react-native';
+import { StyleSheet, View, Text, Button, TextInput, Alert } from 'react-native';
import TorBridge from 'react-native-tor';
type Await = T extends PromiseLike ? U : T;
@@ -14,6 +14,13 @@ export default function App() {
const [onion, setOnion] = React.useState(
'http://3g2upl4pq6kufc4m.onion'
);
+ const [hiddenServicePort, setHiddenServicePort] = React.useState(20000);
+ const [
+ hiddenServiceDestinationPort,
+ setHiddenServiceDestinationPort,
+ ] = React.useState(20011);
+ const [hiddenServiceKey, setHiddenServiceKey] = React.useState('');
+ const [hiddenServiceOnion, setHiddenServiceOnion] = React.useState('');
const [hasStream, setHasStream] = React.useState(false);
const [
streamConnectionTimeoutMS,
@@ -140,6 +147,7 @@ export default function App() {
{!!socksPort && (
+ {/* Onion */}
+ {/* HS */}
+ setHiddenServicePort(Number(v))}
+ value={hiddenServicePort.toString()}
+ />
+ setHiddenServiceDestinationPort(Number(v))}
+ value={hiddenServiceDestinationPort.toString()}
+ />
+ setHiddenServiceKey(v)}
+ value={hiddenServiceKey}
+ />
+ setHiddenServiceOnion(v)}
+ value={hiddenServiceOnion}
+ />
+