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 */}