Skip to content

Feat/hs #35

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
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
57 changes: 48 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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.

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

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

Expand Down
Binary file modified android/libs/sifir_android.aar
Binary file not shown.
2 changes: 1 addition & 1 deletion android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.reactnativetor">

<uses-permission android:name="android.permission.INTERNET" />
</manifest>
4 changes: 2 additions & 2 deletions android/src/main/java/com/reactnativetor/TorBridgeRequest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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") {
Expand Down
143 changes: 132 additions & 11 deletions android/src/main/java/com/reactnativetor/TorModule.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.*

Expand Down Expand Up @@ -52,13 +55,35 @@ class DataObserverEmitter(
}
}

class HttpHandlerObserverEmitter(
private val handlerId: String,
private val reactContext: ReactApplicationContext,
private val serviceHandlers: HashMap<String, HiddenServiceHandler>
) : 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<String, TcpSocksStream> = HashMap();
// private val executorService: ExecutorService = Executors.newFixedThreadPool(4)
private val executorService : ThreadPoolExecutor = ThreadPoolExecutor(4,4, 0L, TimeUnit.MILLISECONDS, LinkedBlockingQueue<Runnable>());
private val _serviceHandlers: HashMap<String, HiddenServiceHandler> = HashMap();

// private val executorService: ExecutorService = Executors.newFixedThreadPool(4)
private val executorService: ThreadPoolExecutor =
ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, LinkedBlockingQueue<Runnable>(50));


/**
Expand Down Expand Up @@ -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")
Expand All @@ -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 {
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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<Any?, Any?>();

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)
}

}

}
}
3 changes: 2 additions & 1 deletion android/src/main/java/com/reactnativetor/TorPackage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ class TorPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
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<NativeModule>(TorModule(reactContext))
}

Expand Down
36 changes: 18 additions & 18 deletions example/ios/Podfile
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading