-
Notifications
You must be signed in to change notification settings - Fork 17
/
index.tsx
522 lines (497 loc) · 15.9 KB
/
index.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
import {
NativeModules,
DeviceEventEmitter,
NativeEventEmitter,
AppState,
AppStateStatus,
Platform,
EmitterSubscription,
} from 'react-native';
import { queue } from 'async';
type SocksPortNumber = number;
export type RequestHeaders = { [header: string]: string } | {};
export type ResponseHeaders = { [header: string]: string | string[] };
/**
* Supported Request types
* @todo PUT
*/
export enum RequestMethod {
'GET' = 'get',
'POST' = 'post',
'DELETE' = 'delete',
}
/**
* Supported Body Payloads for the respective RequestMethod
*/
export interface RequestBody {
[RequestMethod.GET]: undefined;
[RequestMethod.POST]: string;
[RequestMethod.DELETE]: string | undefined;
}
/**
* Response returned from a successfully executed request
*/
export interface RequestResponse<T = any> {
/**
* Content mimeType returned by server
*/
mimeType: string;
/**
* Base64 encoded string of data returned by server
*/
b64Data: string;
/**
* String indexed object for headers returned by Server
*/
headers: ResponseHeaders;
/**
* The response code for the request as returned by the server
* Note: a respCode > 299 is considered an error by the client and throws
*/
respCode: number;
/**
* If the mimeType of the payload is valid JSON then this field will
* be populated with parsed JSON (object)
*/
json?: T;
}
interface ProcessedRequestResponse extends RequestResponse {}
/**
* Native module interface
* Used internally, public calls should be made on the returned TorType
*/
interface NativeTor {
startDaemon(
timeoutMs: number,
clientTimeoutSeconds: number
): Promise<SocksPortNumber>;
stopDaemon(): Promise<void>;
getDaemonStatus(): Promise<string>;
request<T extends RequestMethod>(
url: string,
method: T,
data: string, // native side expects string for body
headers: RequestHeaders,
trustInvalidSSL: boolean
): Promise<RequestResponse>;
startTcpConn(target: string, timeoutMs: number): Promise<string>;
sendTcpConnMsg(
target: string,
msg: string,
timeoutSeconds: number
): Promise<boolean>;
stopTcpConn(target: string): Promise<boolean>;
}
/**
* Tcpstream data handler.
* If err is populated then there was an error
*/
type TcpConnDatahandler = (data?: string, err?: string) => void;
/**
* Interface returned by createTcpConnection factory
*/
interface TcpStream {
/**
* Called to close and end the Tcp connection
*/
close(): Promise<boolean>;
/**
* Send a message (write on socket)
* @param msg
*/
write(msg: string): Promise<boolean>;
}
/**
* /**
* Factory function to create a persistent TcpStream connection to a target.
* Wraps the native side emitter and subscribes to the targets data messages (string).
* The TcpStream currently emits per line of data received . That is it reads data from the socket until a new line is reached, at which time
* it will emit the data read (by calling onData(data,null). If an error is received or the connection is dropped it onData will be called
* with the second parameter containing the error string (ie onData(null,'some error');
* Note: Receiving an 'EOF' error from the target we're connected to signifies the end of a stream or the target dropped the connection.
* 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.
* @param param {target: string, writeTimeout: number, connectionTimeout: number } :
* `target` onion to connect to (ex: kciybn4d4vuqvobdl2kdp3r2rudqbqvsymqwg4jomzft6m6gaibaf6yd.onion:50001)
* 'writeTimeout' in seconds to wait before timing out on writing to the socket (Defaults to 7)
* 'connectionTimeout' in MilliSeconds to wait before timing out on connecting to the Target (Defaults to 15000 = 15 seconds)
* 'numberConcurrentWrites' Number of maximum messages to write concurrently on the Tcp socket. Defaults to 4. If more than numberConcurrentWrites messages are recieved they are placed on queue to be dispatched as soon as a previous message write resolves.
* @param onData TcpConnDatahandler node style callback called when data or an error is received for this connection
* @returns TcpStream
*/
const _createTcpConnection = async (
param: {
target: string;
connectionTimeout?: number;
writeTimeout?: number;
numberConcurrentWrites?: number;
},
onData: TcpConnDatahandler
): Promise<TcpStream> => {
const { target } = param;
const connectionTimeout = param.connectionTimeout || 15000;
const writeQueueConcurrency = param.numberConcurrentWrites || 4;
const connId = await NativeModules.TorBridge.startTcpConn(
target,
connectionTimeout
);
let lsnr_handle: EmitterSubscription[] = [];
/**
* Handles errors from Tcp Connection
* Mainly check for EOF (connection closed/end of stream) and removes lnsers
*/
const onError = async (event: string) => {
if (event.toLowerCase() === 'eof') {
console.warn(
`Got to end of stream on TcpStream to ${target} having connection Id ${connId}. Removing listners`
);
try {
await close();
} catch (err) {
console.warn('RnTor: onError close execution error', err);
}
}
};
if (Platform.OS === 'android') {
lsnr_handle.push(
DeviceEventEmitter.addListener(`${connId}-data`, (event) => {
onData(event);
})
);
lsnr_handle.push(
DeviceEventEmitter.addListener(`${connId}-error`, async (event) => {
await onError(event);
await onData(undefined, event);
})
);
} else if (Platform.OS === 'ios') {
const emitter = new NativeEventEmitter(NativeModules.TorBridge);
lsnr_handle.push(
emitter.addListener(`torTcpStreamData`, (event) => {
const [uuid, data] = event.split('||', 2);
if (connId === uuid) {
onData(data);
}
})
);
lsnr_handle.push(
emitter.addListener(`torTcpStreamError`, async (event) => {
const [uuid, data] = event.split('||', 2);
if (connId === uuid) {
await onError(data);
await onData(undefined, data);
}
})
);
}
const writeTimeout = param.writeTimeout || 7;
const write = (msg: string) =>
NativeModules.TorBridge.sendTcpConnMsg(connId, msg, writeTimeout);
const close = () => {
lsnr_handle.map((e) => e.remove());
return NativeModules.TorBridge.stopTcpConn(connId);
};
/**
* Wrap write with a JS queue for non V8 engines
*/
const writeMessageQueue = queue<{
msg: string;
res: (x: any) => void;
rej: (x: any) => void;
}>(async (payload, cb) => {
const { msg, res, rej } = payload;
try {
const result = await write(msg);
res(result);
} catch (err) {
rej(err);
} finally {
cb();
}
}, writeQueueConcurrency);
writeMessageQueue.drain(() =>
console.log(
'notice: All tcpConnection write messages requests have been disptached..'
)
);
return {
close,
write: (msg: string) =>
new Promise((res, rej) => writeMessageQueue.push({ msg, res, rej })),
};
};
const createTcpConnQueue = queue<{
param: Parameters<typeof _createTcpConnection>;
res: (x: any) => void;
rej: (x: any) => void;
}>(async (payload, cb) => {
const { param, res, rej } = payload;
try {
const result = await _createTcpConnection(...param);
res(result);
} catch (err) {
console.error('error creating tcp conn', err);
rej(err);
} finally {
cb();
}
}, 1);
createTcpConnQueue.drain(() =>
console.log(
'notice: All requested TcpConnections requests have been dispatched..'
)
);
/**
* We expose _createTcpConnection publicly as a wrapped queue to avoid JS->Native bridge hang issue for non V8 engines
*/
const createTcpConnection = (
...param: Parameters<typeof _createTcpConnection>
): ReturnType<typeof _createTcpConnection> =>
new Promise((res, rej) => {
createTcpConnQueue.push({ param, res, rej });
});
type TorType = {
/**
* Send a GET request routed through the SOCKS proxy on the native side
* Starts the Tor Daemon automatically if not already started
* @param url
* @param headers
* @param trustSSL
*/
get(
url: string,
headers?: RequestHeaders,
trustSSL?: boolean
): Promise<ProcessedRequestResponse>;
/**
* Send a POST request routed through the SOCKS proxy on the native side
* Starts the Tor Daemon automatically if not already started
* @param url
* @param body
* @param headers
* @param trustSSL
*/
post(
url: string,
body: RequestBody[RequestMethod.POST],
headers?: RequestHeaders,
trustSSL?: boolean
): Promise<ProcessedRequestResponse>;
/**
* Send a DELETE request routed through the SOCKS proxy on the native side
* Starts the Tor Daemon automatically if not already started
* @param url
* @param headers
* @param trustSSL
*/
delete(
url: string,
body?: RequestBody[RequestMethod.DELETE],
headers?: RequestHeaders,
trustSSL?: boolean
): Promise<ProcessedRequestResponse>;
/** Starts the Tor Daemon if not started and returns a promise that fullfills with the socks port number when boostraping is complete.
* If the function was previously called it will return the promise without attempting to start the daemon again.
* Useful when used as a guard in your transport or action layer
*/
startIfNotStarted(): Promise<SocksPortNumber>;
/**
* Stops a running Tor Daemon
*/
stopIfRunning(): Promise<void>;
/**
* Returns the current status of the Daemon
* Some :
* NOTINIT - Not initialized or run (call startIfNotStarted to the startDaemon)
* STARTING - Daemon is starting and bootsraping
* DONE - Daemon has completed boostraing and socks proxy is ready to be used to route traffic.
* <other> - A status returned directly by the Daemon that can indicate a transient state or error.
*/
getDaemonStatus(): Promise<string>;
/**
* Accessor the Native request function
* Should not be used unless you know what you are doing.
*/
request: NativeTor['request'];
/**
* Factory function for creating a peristant Tcp connection to a target
* See createTcpConnectio;
*/
createTcpConnection: typeof createTcpConnection;
};
const TorBridge: NativeTor = NativeModules.TorBridge;
/**
* Tor module factory function
* @param stopDaemonOnBackground
* @default true
* When set to true will shutdown the Tor daemon when the application is backgrounded preventing pre-emitive shutdowns by the OS
* @param startDaemonOnActive
* @default false
* When set to true will automatically start/restart the Tor daemon when the application is bought back to the foreground (from the background)
* @param numberConcurrentRequests If sent to > 0 this will instruct the module to queue requests on the JS side before sending them over the Native bridge. Requests will get exectued with numberConcurrentRequests concurent requests. Note setting this to 0 disables JS sided queueing and sends requests directly to Native bridge as they are recieved. This is useful if you're running the stock/hermes RN JS engine that has a tendency off breaking under heavy multithreaded work. If you using V8 you can set this to 0 to disable JS sided queueing and thus get maximum performance.
* @default 4
* @param os The OS the module is running on (Set automatically and is provided as an injectable for testing purposes)
* @default The os the module is running on.
*/
export default ({
stopDaemonOnBackground = true,
startDaemonOnActive = false,
bootstrapTimeoutMs = 25000,
numberConcurrentRequests = 4,
clientTimeoutSeconds = 60,
os = Platform.OS,
} = {}): TorType => {
let bootstrapPromise: Promise<number> | undefined;
let lastAppState: AppStateStatus = 'active';
let _appStateLsnerSet: boolean = false;
let requestQueue: ReturnType<typeof queue> | undefined;
if (numberConcurrentRequests > 0) {
requestQueue = queue<{
res: (x: any) => void;
rej: (x: any) => void;
request: () => Promise<RequestResponse>;
}>(async (task, cb) => {
const { res, rej, request } = task;
try {
const result = await request();
res(result);
} catch (err) {
rej(err);
} finally {
cb();
}
}, numberConcurrentRequests);
requestQueue.drain(() =>
console.log('notice: Request queue has been processed')
);
}
const _handleAppStateChange = async (nextAppState: AppStateStatus) => {
if (
startDaemonOnActive &&
lastAppState.match(/background/) &&
nextAppState === 'active'
) {
const status = NativeModules.TorBridge.getDaemonStatus();
// Daemon should be in NOTINIT status if coming from background and this is enabled, so if not shutodwn and start again
if (status !== 'NOTINIT') {
await stopIfRunning();
}
startIfNotStarted();
}
if (
stopDaemonOnBackground &&
lastAppState.match(/active/) &&
nextAppState === 'background'
) {
const status = NativeModules.TorBridge.getDaemonStatus();
if (status !== 'NOTINIT') {
await stopIfRunning();
}
}
lastAppState = nextAppState;
};
const startIfNotStarted = () => {
if (!bootstrapPromise) {
bootstrapPromise = NativeModules.TorBridge.startDaemon(
bootstrapTimeoutMs,
clientTimeoutSeconds
);
}
return bootstrapPromise;
};
const stopIfRunning = async () => {
console.warn('Stopping Tor daemon.');
bootstrapPromise = undefined;
await NativeModules.TorBridge.stopDaemon();
};
/**
* Post process request result
*/
const onAfterRequest = async (
res: RequestResponse
): Promise<RequestResponse> => {
if (os === 'android') {
// Mapping JSONObject to ReadableMap for the bridge is a bit of a manual shitshow
// so android JSON will be returned as string from the other side and we parse it here
//
if (res?.json) {
const json = JSON.parse(res.json);
return {
...res,
json,
};
}
}
return res;
};
// Register app state lsner only once
if (!_appStateLsnerSet) {
AppState.addEventListener('change', _handleAppStateChange);
}
/**
* Wraps requests to be queued or executed directly.
* numberConcurrentRequests > 0 will cause tasks to be wrapped in a JS side queue
*/
const requestQueueWrapper = (
request: () => Promise<RequestResponse>
): Promise<RequestResponse> => {
return new Promise((res, rej) =>
numberConcurrentRequests > 0
? requestQueue?.push({ request, res, rej })
: request().then(res).catch(rej)
);
};
return {
async get(url: string, headers?: Headers, trustSSL: boolean = true) {
await startIfNotStarted();
return await onAfterRequest(
await requestQueueWrapper(() =>
TorBridge.request(url, RequestMethod.GET, '', headers || {}, trustSSL)
)
);
},
async post(
url: string,
body: RequestBody[RequestMethod.POST],
headers?: RequestHeaders,
trustSSL: boolean = true
) {
await startIfNotStarted();
return await onAfterRequest(
await requestQueueWrapper(() =>
TorBridge.request(
url,
RequestMethod.POST,
body,
headers || {},
trustSSL
)
)
);
},
async delete(
url: string,
body: RequestBody[RequestMethod.DELETE],
headers?: RequestHeaders,
trustSSL: boolean = true
) {
await startIfNotStarted();
return await onAfterRequest(
await requestQueueWrapper(() =>
TorBridge.request(
url,
RequestMethod.DELETE,
body || '',
headers || {},
trustSSL
)
)
);
},
startIfNotStarted,
stopIfRunning,
request: TorBridge.request,
getDaemonStatus: TorBridge.getDaemonStatus,
createTcpConnection,
} as TorType;
};