react-native-tor/ios/Tor.swift
2022-01-30 08:16:27 -05:00

377 lines
17 KiB
Swift

import Foundation;
// Trust SSLs even if invalid
extension Tor:URLSessionDelegate {
public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
//Trust the certificate even if not valid
let urlCredential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
completionHandler(.useCredential, urlCredential)
}
}
extension DispatchQueue {
static func background(delay: Double = 0.0, background: (()->Void)? = nil, completion: (() -> Void)? = nil) {
DispatchQueue.global(qos: .background).async {
background?()
if let completion = completion {
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: {
completion()
})
}
}
}
}
class ObserverSwift {
public let onSuccess: ((String) -> Void)
public let onError: ((String) -> Void)
init(onSuccess: @escaping ((String) -> Void), onError: @escaping ((String) -> Void),target:String) {
self.onSuccess = onSuccess
self.onError = onError
}
}
@objc(Tor)
class Tor: RCTEventEmitter {
var service:Optional<OpaquePointer> = nil;
var proxySocksPort:Optional<UInt16> = nil;
var starting:Bool = false;
var streams:Dictionary<String,OpaquePointer> = [:];
var hasLnser = false;
var clienTimeout:TimeInterval = 60;
func getProxiedClient(headers:Optional<NSDictionary>,socksPort:UInt16,trustInvalidSSL: Bool = false)->URLSession{
let config = URLSessionConfiguration.default;
config.requestCachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData;
config.connectionProxyDictionary = [AnyHashable: Any]();
config.connectionProxyDictionary?[kCFNetworkProxiesHTTPEnable as String] = 1;
config.connectionProxyDictionary?[kCFStreamPropertySOCKSProxyHost as String] = "127.0.0.1";
config.connectionProxyDictionary?[kCFStreamPropertySOCKSProxyPort as String] = socksPort;
config.connectionProxyDictionary?[kCFProxyTypeSOCKS as String] = 1;
config.timeoutIntervalForRequest = clienTimeout;
config.timeoutIntervalForResource = clienTimeout;
if let headersPassed = headers {
config.httpAdditionalHeaders = headersPassed as? [AnyHashable : Any]
}
if(trustInvalidSSL){
return URLSession.init(configuration: config, delegate: self, delegateQueue: nil)
} else {
return URLSession.init(configuration: config, delegate: nil, delegateQueue: OperationQueue.current)
}
}
func resolveObjResp (data:Data,resp:HTTPURLResponse,resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock)->Void{
let jsonObject: NSMutableDictionary = NSMutableDictionary()
jsonObject.setValue(data.base64EncodedString(), forKey: "b64Data")
jsonObject.setValue(resp.mimeType, forKey: "mimeType")
jsonObject.setValue(resp.allHeaderFields, forKey: "headers")
jsonObject.setValue(resp.statusCode, forKey: "respCode")
// parse json if that's what we have
if let mimeType = resp.mimeType {
if mimeType == "application/json" || mimeType == "application/javascript" {
do{
let json = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
jsonObject.setValue(json, forKey: "json")
} catch {
print("prepareObjResp errorParsingJson!",error);
}
}
}
if 200...299 ~= resp.statusCode {
resolve(jsonObject as NSObject )
}else{
var msg:Optional<String> = nil;
if let errorMessage = String(data:data,encoding: .utf8) {
msg = errorMessage
}
reject(
"TOR.REQUEST","Resp Code: \(resp.statusCode) : \(msg)",NSError.init(domain: "TOR.REQUEST", code: resp.statusCode,userInfo:["data" : msg]));
}
}
@objc(request:method:jsonBody:headers:trustInvalidCert:resolver:rejecter:)
func request(url: String, method: String, jsonBody: String, headers: NSDictionary, trustInvalidCert:Bool, resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock){
if service == nil {
reject("TOR.SERVICE","Tor Service NOT Running. Call `startDaemon` first.",NSError.init(domain: "TOR.DAEMON", code: 99));
return;
}
let session = getProxiedClient(headers:headers,socksPort: proxySocksPort!,trustInvalidSSL:trustInvalidCert);
guard let _url = URL(string:url) else {
reject("TOR.URL","Could not parse url",NSError.init(domain: "TOR", code: 404));
return;
}
do{
switch method{
case "get":
session.dataTask(with: _url) { data, resp, error in
guard let dataResp = data , error == nil, let respData = resp else {
reject("TOR.NETWORK.GET",error?.localizedDescription,error);
return;
}
self.resolveObjResp(data:dataResp,resp:respData as! HTTPURLResponse,resolve:resolve,reject:reject);
}.resume();
case "delete":
var request = URLRequest(url:_url);
request.httpMethod = "DELETE";
session.dataTask(with: request) { data, resp, error in
guard let dataResp = data , error == nil, let respData = resp else {
reject("TOR.NETWORK.DELETE",error?.localizedDescription,error);
return;
}
self.resolveObjResp(data:dataResp,resp:respData as! HTTPURLResponse,resolve:resolve,reject:reject);
}.resume();
case "post":
var request = URLRequest(url:_url);
request.httpMethod = "POST";
session.uploadTask(with: request, from: jsonBody.data(using: .utf8)) { data, resp, error in
guard let dataResp = data , let respData = resp , error == nil else {
reject("TOR.NETWORK.POST",error?.localizedDescription,error);
return;
}
self.resolveObjResp(data:dataResp,resp:respData as! HTTPURLResponse,resolve:resolve,reject:reject);
}.resume();
default:
throw NSError.init(domain:"TOR.REQUEST_METHOD",code:400)
}
} catch{
reject("TOR.REQUEST",error.localizedDescription,error);
}
}
@objc(startDaemon:clientTimeoutSec:resolver:rejecter:)
func startDaemon(timeoutMs:NSNumber, clientTimeoutSec:NSNumber, resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock)->Void{
if service != nil || starting {
reject("TOR.START","Tor Service Already Running. Call `stopDaemon` first.",NSError.init(domain: "TOR.START", code: 01));
return;
}
clienTimeout = clientTimeoutSec.doubleValue;
starting = true;
do {
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(),isDirectory: true)
// FIXME pass this and check if avalible
let socksPort:UInt16 = 19032;
// this gives file:///Users/.../tmp/ so we remove the file:// prefix and trailing slash
let path = String(temporaryDirectoryURL.absoluteString.dropFirst(7).dropLast());
// Rust will start Tor daemon thread and block until boostrapped, so run as dispatched async task so not to block this thread
DispatchQueue.background(background: {
defer {
self.starting = false;
}
// FIXME here make the timeout a param
// better way to automatically have the JS promise handle a fail ?
let call_result = get_owned_TorService(path, socksPort,timeoutMs.uint64Value).pointee;
switch(call_result.message.tag){
case Success:
self.service = Optional.some(call_result.result);
self.proxySocksPort = socksPort;
resolve(socksPort);
return;
case Error:
// Convert RustByteSlice to String
if let error_body = call_result.message.error {
let error_string = String.init(cString: error_body);
reject("TOR.START",error_string,NSError.init(domain: "TOR", code: 0))
} else {
reject("TOR.START","Unknown daemon startup error",NSError.init(domain: "TOR", code: 99));
}
return;
default:
reject("TOR.START","unknown startup result",NSError.init(domain: "TOR", code: 99));
return;
}
}, completion:{
})
}
}
@objc(getDaemonStatus:rejecter:)
func getDaemonStatus(resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock)->Void {
guard let daemon = service else {
if(starting)
{
resolve("STARTING")
}else {
resolve("NOTINIT");
}
return;
}
if let status = get_status_of_owned_TorService(daemon) {
defer {
destroy_cstr(status);
}
let status_string = String.init(cString: status);
resolve(status_string)
} else {
reject("TOR.STATUS","UNKNOWN",NSError.init(domain: "TOR", code: 99));
}
}
@objc(stopDaemon:rejecter:)
func stopDaemon( resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock)->Void {
if let hasSevice = service {
// if we have streams, shut them down
for key in streams.keys {
if let stream = streams[key] {
// set it here in case eof callback gets called
// while looping
streams[key] = nil;
tcp_stream_destroy(stream);
}
}
shutdown_owned_TorService(hasSevice);
service = nil
proxySocksPort = nil
}
resolve(true);
}
override func startObserving(){
self.hasLnser = true;
}
override func stopObserving(){
self.hasLnser = false;
}
// FIXME here it needs to support, so i guess we can use
override func supportedEvents() -> [String]! {
["torTcpStreamData","torTcpStreamError"]
}
@objc(startTcpConn:timeoutMs:resolver:rejecter:)
func startTcpConn(target:String,timeoutMs:NSNumber,resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock){
guard let socksProxy = self.proxySocksPort else {
reject("TOR.TCPCONN.startTcpConn","SocksProxy not detected, make sure Tor is started",NSError.init(domain: "TOR", code: 99));
return;
}
let uuid = UUID().uuidString;
let call_result = tcp_stream_start(target, "127.0.0.1:\(socksProxy)",timeoutMs.uint64Value).pointee;
switch(call_result.message.tag){
case Success:
let stream = call_result.result;
self.streams[uuid] = stream;
// Create swift observer wrapper to store context
let observerWrapper = ObserverSwift(onSuccess:{ (data) in
self.sendEvent(withName: "torTcpStreamData", body: "\(uuid)||\(data)")
}, onError:{ (data) in
// On Eof destrory stream and remove from map
// TODO update this when streaming streams
if(data == "EOF"){
guard let stream = self.streams[uuid] else{
print("Note: EOF but stream already destroyed, returning...")
return;
}
tcp_stream_destroy(stream);
self.streams[uuid] = nil;
} else if (data.contains("NotConnected")){
guard self.streams[uuid] != nil else{
print("Note: EOF but stream already destroyed, returning...")
return;
}
// Stream pointer could be already delocated from rust side here
// so don't try to destory it just remove it from map of references.
// Worst case we create a new one and the memory leak gets recycled
// when app restarts
// TODO way to better coordinate this.
// tcp_stream_destroy(stream);
self.streams[uuid] = nil;
} else {
print("Got observerWrapper event but not EOF",data )
}
self.sendEvent(withName: "torTcpStreamError", body: "\(uuid)||\(data)")
},target:target);
// Prepare pointer to context and observer callbacks as Retained
let owner = UnsafeMutableRawPointer(Unmanaged.passRetained(observerWrapper).toOpaque());
let onSuccess:@convention(c) (UnsafeMutablePointer<Int8>?, UnsafeRawPointer?) -> Void = { (data, context) in
// take unretained so we don't clear it
let obv = Unmanaged<ObserverSwift>.fromOpaque(context!).takeUnretainedValue();
obv.onSuccess(String(cString: data!));
destroy_cstr(data);
}
let onError:@convention(c) (UnsafeMutablePointer<Int8>?, UnsafeRawPointer?) -> Void = { (data, context) in
let obv = Unmanaged<ObserverSwift>.fromOpaque(context!).takeUnretainedValue();
obv.onError(String(cString: data!));
destroy_cstr(data);
}
let obv = Observer(context: owner, on_success: onSuccess, on_err:onError);
tcp_stream_on_data(stream,obv);
resolve(uuid);
return;
case Error:
// Convert RustByteSlice to String
if let error_body = call_result.message.error {
let error_string = String.init(cString: error_body);
reject("TOR.TCPCONN.startTcpConn",error_string,NSError.init(domain: "TOR", code: 0))
} else {
reject("TOR.TCPCONN.startTcpConn","Unknown tcpStream startup error",NSError.init(domain: "TOR", code: 99));
}
return;
default:
reject("TOR.startTcpConn","unknown startup result",NSError.init(domain: "TOR", code: 99));
return;
}
}
@objc(sendTcpConnMsg:msg:timeoutSec:resolver:rejecter:)
func sendTcpConnMsg(target:String,msg:String,timeoutSec:NSNumber,resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock){
guard let _ = self.service else {
reject("TOR.TCPCONN.sendTcpConnMsg","Service not detected, make sure Tor is started",NSError.init(domain: "TOR", code: 99));
return;
}
guard let stream = self.streams[target] else{
reject("TOR.TCPCONN.sendTcpConnMsg","Stream not detected",NSError.init(domain: "TOR", code: 99));
return;
}
let result = tcp_stream_send_msg(stream, msg,timeoutSec.uint64Value).pointee;
switch(result.tag){
case Success:
resolve(true);
return;
case Error:
if let error_body = result.error {
let error_string = String.init(cString: error_body);
reject("TOR.TCPCONN.sendTcpConnMsg",error_string,NSError.init(domain: "TOR", code: 0))
} else {
reject("TOR.TCPCONN.sendTcpConnMsg","Unknown tcpStream startup error",NSError.init(domain: "TOR", code: 99));
}
return;
default:
reject("TOR.TCPCONN.sendTcpConnMsg","unknown tcp send message result",NSError.init(domain: "TOR", code: 99));
return;
}
}
@objc(stopTcpConn:resolver:rejecter:)
func stopTcpConn(target:String,resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock){
guard let stream = self.streams[target] else{
reject("TOR.TCPCONN.stopTcpConn","Stream not detected",NSError.init(domain: "TOR", code: 99));
return;
}
self.streams[target] = nil;
tcp_stream_destroy(stream);
resolve(true);
}
}