@corpus-core/colibri-tor 1.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -0
- package/dist/browser.d.ts +48 -0
- package/dist/browser.js +96 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +24 -0
- package/dist/node.d.ts +34 -0
- package/dist/node.js +275 -0
- package/dist/types.d.ts +39 -0
- package/dist/types.js +23 -0
- package/node_modules/tor-js/README.md +166 -0
- package/node_modules/tor-js/dist/Log.d.ts +24 -0
- package/node_modules/tor-js/dist/Log.d.ts.map +1 -0
- package/node_modules/tor-js/dist/TorClient.d.ts +37 -0
- package/node_modules/tor-js/dist/TorClient.d.ts.map +1 -0
- package/node_modules/tor-js/dist/commonExports.d.ts +6 -0
- package/node_modules/tor-js/dist/commonExports.d.ts.map +1 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-base64/index.d.ts +3 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-base64/index.d.ts.map +1 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-base64/index.js +2139 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-base64/index.js.map +1 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-base64/singleton.d.ts +4 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-base64/singleton.d.ts.map +1 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-base64/singleton.js +2187 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-base64/singleton.js.map +1 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-cdn/index.d.ts +3 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-cdn/index.d.ts.map +1 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-cdn/index.js +2242 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-cdn/index.js.map +1 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-cdn/singleton.d.ts +4 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-cdn/singleton.d.ts.map +1 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-cdn/singleton.js +2290 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-cdn/singleton.js.map +1 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-file/index.d.ts +3 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-file/index.d.ts.map +1 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-file/index.js +2139 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-file/index.js.map +1 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-file/singleton.d.ts +4 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-file/singleton.d.ts.map +1 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-file/singleton.js +2187 -0
- package/node_modules/tor-js/dist/entryPoints/wasm-file/singleton.js.map +1 -0
- package/node_modules/tor-js/dist/helpers.d.ts +7 -0
- package/node_modules/tor-js/dist/helpers.d.ts.map +1 -0
- package/node_modules/tor-js/dist/polyfills.d.ts +1 -0
- package/node_modules/tor-js/dist/polyfills.d.ts.map +1 -0
- package/node_modules/tor-js/dist/singleton.d.ts +24 -0
- package/node_modules/tor-js/dist/singleton.d.ts.map +1 -0
- package/node_modules/tor-js/dist/socketProvider.d.ts +76 -0
- package/node_modules/tor-js/dist/socketProvider.d.ts.map +1 -0
- package/node_modules/tor-js/dist/storage/filesystem.d.ts +18 -0
- package/node_modules/tor-js/dist/storage/filesystem.d.ts.map +1 -0
- package/node_modules/tor-js/dist/storage/index.d.ts +7 -0
- package/node_modules/tor-js/dist/storage/index.d.ts.map +1 -0
- package/node_modules/tor-js/dist/storage/indexeddb.d.ts +14 -0
- package/node_modules/tor-js/dist/storage/indexeddb.d.ts.map +1 -0
- package/node_modules/tor-js/dist/storage/locking.d.ts +15 -0
- package/node_modules/tor-js/dist/storage/locking.d.ts.map +1 -0
- package/node_modules/tor-js/dist/storage/memory.d.ts +13 -0
- package/node_modules/tor-js/dist/storage/memory.d.ts.map +1 -0
- package/node_modules/tor-js/dist/storage/node-deps.d.ts +8 -0
- package/node_modules/tor-js/dist/storage/node-deps.d.ts.map +1 -0
- package/node_modules/tor-js/dist/tor_js_bg.wasm +0 -0
- package/node_modules/tor-js/dist/types.d.ts +41 -0
- package/node_modules/tor-js/dist/types.d.ts.map +1 -0
- package/node_modules/tor-js/dist/wasm-B6es-efC.d.ts +302 -0
- package/node_modules/tor-js/dist/wasm-pkg/tor_js.d.ts +311 -0
- package/node_modules/tor-js/dist/wasm-pkg/tor_js.js +1159 -0
- package/node_modules/tor-js/dist/wasm.d.ts +31 -0
- package/node_modules/tor-js/dist/wasm.d.ts.map +1 -0
- package/node_modules/tor-js/package.json +61 -0
- package/node_modules/tor-js/src/Log.ts +100 -0
- package/node_modules/tor-js/src/TorClient.ts +134 -0
- package/node_modules/tor-js/src/commonExports.ts +7 -0
- package/node_modules/tor-js/src/entryPoints/wasm-base64/index.ts +17 -0
- package/node_modules/tor-js/src/entryPoints/wasm-base64/singleton.ts +7 -0
- package/node_modules/tor-js/src/entryPoints/wasm-cdn/index.ts +155 -0
- package/node_modules/tor-js/src/entryPoints/wasm-cdn/singleton.ts +7 -0
- package/node_modules/tor-js/src/entryPoints/wasm-file/index.ts +19 -0
- package/node_modules/tor-js/src/entryPoints/wasm-file/singleton.ts +7 -0
- package/node_modules/tor-js/src/globals.d.ts +2 -0
- package/node_modules/tor-js/src/helpers.ts +20 -0
- package/node_modules/tor-js/src/polyfills.ts +4 -0
- package/node_modules/tor-js/src/singleton.ts +54 -0
- package/node_modules/tor-js/src/socketProvider.ts +405 -0
- package/node_modules/tor-js/src/storage/filesystem.ts +171 -0
- package/node_modules/tor-js/src/storage/index.ts +21 -0
- package/node_modules/tor-js/src/storage/indexeddb.ts +99 -0
- package/node_modules/tor-js/src/storage/locking.ts +195 -0
- package/node_modules/tor-js/src/storage/memory.ts +42 -0
- package/node_modules/tor-js/src/storage/node-deps.ts +23 -0
- package/node_modules/tor-js/src/types.ts +48 -0
- package/node_modules/tor-js/src/wasm-base64-data.d.ts +3 -0
- package/node_modules/tor-js/src/wasm.ts +135 -0
- package/package.json +67 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Socket provider for connecting to Tor relays via direct TCP, WebSocket,
|
|
3
|
+
* or WebRTC.
|
|
4
|
+
*
|
|
5
|
+
* ArtiSocketProvider auto-detects available strategies based on environment:
|
|
6
|
+
* - Node.js/Deno: tries direct TCP first, then WebSocket/WebRTC if a gateway URL is set
|
|
7
|
+
* - Browsers: tries WebRTC first (if available), then WebSocket (requires gateway URL)
|
|
8
|
+
*
|
|
9
|
+
* Each `connect(target)` call returns an {@link ArtiSocket} — a uniform
|
|
10
|
+
* bidirectional byte pipe regardless of transport.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Environment detection
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
const HAS_DENO = typeof (globalThis as any).Deno !== 'undefined';
|
|
18
|
+
const HAS_NODE = typeof (globalThis as any).process?.versions?.node !== 'undefined';
|
|
19
|
+
const HAS_RTC = typeof (globalThis as any).RTCPeerConnection !== 'undefined';
|
|
20
|
+
const HAS_WS =
|
|
21
|
+
typeof (globalThis as any).WebSocket !== 'undefined' || HAS_DENO || HAS_NODE;
|
|
22
|
+
|
|
23
|
+
function defaultStrategies(hasUrl: boolean): string[] {
|
|
24
|
+
const s: string[] = [];
|
|
25
|
+
if (HAS_DENO || HAS_NODE) s.push('direct');
|
|
26
|
+
if (hasUrl && HAS_RTC) s.push('webrtc');
|
|
27
|
+
if (hasUrl && HAS_WS) s.push('websocket');
|
|
28
|
+
return s;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// ArtiSocket — uniform bidirectional byte pipe
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A bidirectional byte pipe to a Tor relay.
|
|
37
|
+
*
|
|
38
|
+
* Assign `onmessage` and `onclose` after creation.
|
|
39
|
+
* Call `send(data)` with Uint8Array and `close()` when done.
|
|
40
|
+
*/
|
|
41
|
+
export class ArtiSocket {
|
|
42
|
+
#send: (data: Uint8Array) => void;
|
|
43
|
+
#close: () => void;
|
|
44
|
+
#closed = false;
|
|
45
|
+
#onclose: (() => void) | null = null;
|
|
46
|
+
|
|
47
|
+
/** Set by transport on error, before onclose fires. */
|
|
48
|
+
_error: string | null = null;
|
|
49
|
+
|
|
50
|
+
/** Receive callback — transport fires this with each incoming chunk. */
|
|
51
|
+
onmessage: ((data: Uint8Array) => void) | null = null;
|
|
52
|
+
|
|
53
|
+
constructor(
|
|
54
|
+
send: (data: Uint8Array) => void,
|
|
55
|
+
close: () => void,
|
|
56
|
+
) {
|
|
57
|
+
this.#send = send;
|
|
58
|
+
this.#close = close;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Setter that fires immediately if close already happened. */
|
|
62
|
+
set onclose(fn: (() => void) | null) {
|
|
63
|
+
this.#onclose = fn;
|
|
64
|
+
if (this.#closed && fn) queueMicrotask(() => fn());
|
|
65
|
+
}
|
|
66
|
+
get onclose(): (() => void) | null { return this.#onclose; }
|
|
67
|
+
|
|
68
|
+
/** @internal — called by transport wrappers when the underlying connection closes. */
|
|
69
|
+
_notifyClose(): void {
|
|
70
|
+
if (this.#closed) return;
|
|
71
|
+
this.#closed = true;
|
|
72
|
+
this.#onclose?.();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
send(data: Uint8Array): void {
|
|
76
|
+
this.#send(data);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
close(): void {
|
|
80
|
+
this.#close();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// -- Transport factories --------------------------------------------------
|
|
84
|
+
|
|
85
|
+
/** Wrap a browser WebSocket (already open). */
|
|
86
|
+
static fromWebSocket(ws: WebSocket): ArtiSocket {
|
|
87
|
+
const sock = new ArtiSocket(
|
|
88
|
+
(data) => ws.send(data),
|
|
89
|
+
() => ws.close(),
|
|
90
|
+
);
|
|
91
|
+
ws.onmessage = (ev) => sock.onmessage?.(new Uint8Array(ev.data));
|
|
92
|
+
ws.onclose = () => sock._notifyClose();
|
|
93
|
+
return sock;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Wrap a WebRTC data channel (already open). */
|
|
97
|
+
static fromDataChannel(dc: RTCDataChannel): ArtiSocket {
|
|
98
|
+
const sock = new ArtiSocket(
|
|
99
|
+
(data) => dc.send(data),
|
|
100
|
+
() => dc.close(),
|
|
101
|
+
);
|
|
102
|
+
dc.onmessage = (ev) => sock.onmessage?.(new Uint8Array(ev.data));
|
|
103
|
+
dc.onclose = () => sock._notifyClose();
|
|
104
|
+
return sock;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Wrap a Node.js net.Socket (already connected). */
|
|
108
|
+
static fromNodeSocket(socket: any): ArtiSocket {
|
|
109
|
+
const sock = new ArtiSocket(
|
|
110
|
+
(data) => socket.write(data),
|
|
111
|
+
() => socket.destroy(),
|
|
112
|
+
);
|
|
113
|
+
socket.on('data', (buf: Buffer) => sock.onmessage?.(new Uint8Array(buf)));
|
|
114
|
+
socket.on('close', () => sock._notifyClose());
|
|
115
|
+
socket.on('error', () => {});
|
|
116
|
+
return sock;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Wrap a Deno TCP connection. */
|
|
120
|
+
static fromDenoConn(conn: any): ArtiSocket {
|
|
121
|
+
const sock = new ArtiSocket(
|
|
122
|
+
(data) => {
|
|
123
|
+
const writer = conn.writable.getWriter();
|
|
124
|
+
writer.write(data).then(() => writer.releaseLock());
|
|
125
|
+
},
|
|
126
|
+
() => conn.close(),
|
|
127
|
+
);
|
|
128
|
+
(async () => {
|
|
129
|
+
try {
|
|
130
|
+
for await (const chunk of conn.readable) {
|
|
131
|
+
sock.onmessage?.(new Uint8Array(chunk));
|
|
132
|
+
}
|
|
133
|
+
} catch {}
|
|
134
|
+
sock._notifyClose();
|
|
135
|
+
})();
|
|
136
|
+
return sock;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// ArtiSocketProvider — multi-strategy connection manager
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Options for creating an ArtiSocketProvider.
|
|
146
|
+
*/
|
|
147
|
+
export interface ArtiSocketProviderOptions {
|
|
148
|
+
/**
|
|
149
|
+
* Gateway URL (e.g., `"https://tor-js-gateway.example.com"`).
|
|
150
|
+
* Required in browsers for WebRTC/WebSocket relay connections.
|
|
151
|
+
* Optional in Node.js/Deno (enables fast bootstrap when provided).
|
|
152
|
+
*/
|
|
153
|
+
gateway?: string;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Ordered list of strategies to try: `"direct"`, `"webrtc"`, `"websocket"`.
|
|
157
|
+
* Defaults based on environment and whether a gateway URL is provided.
|
|
158
|
+
*/
|
|
159
|
+
strategies?: string[];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
interface TrackedEntry {
|
|
163
|
+
dc: RTCDataChannel;
|
|
164
|
+
sock: ArtiSocket | null;
|
|
165
|
+
reject: ((err: Error) => void) | null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Opens sockets to Tor relays via configurable strategies (direct TCP,
|
|
170
|
+
* WebRTC data channels, WebSocket) with automatic fallback.
|
|
171
|
+
*
|
|
172
|
+
* The gateway URL is optional — without it, only the `direct` strategy is
|
|
173
|
+
* available (Node.js/Deno native TCP). With a gateway URL, WebRTC and
|
|
174
|
+
* WebSocket strategies become available for browser environments.
|
|
175
|
+
*/
|
|
176
|
+
export class ArtiSocketProvider {
|
|
177
|
+
#url: string | null;
|
|
178
|
+
#strategies: string[];
|
|
179
|
+
|
|
180
|
+
// WebRTC state (lazily created, reused across connect() calls)
|
|
181
|
+
#rtcPc: RTCPeerConnection | null = null;
|
|
182
|
+
#rtcAlive = false;
|
|
183
|
+
#signalChannel: RTCDataChannel | null = null;
|
|
184
|
+
// Tracked data channels: before open has reject, after open has sock.
|
|
185
|
+
#tracked: TrackedEntry[] = [];
|
|
186
|
+
|
|
187
|
+
constructor(options: ArtiSocketProviderOptions = {}) {
|
|
188
|
+
this.#url = options.gateway ? options.gateway.replace(/\/+$/, '') : null;
|
|
189
|
+
this.#strategies = options.strategies ?? defaultStrategies(!!this.#url);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Open a relay socket to the given target (e.g. "198.51.100.1:9001").
|
|
194
|
+
* Tries each configured strategy in order until one succeeds.
|
|
195
|
+
*/
|
|
196
|
+
async connect(target: string): Promise<ArtiSocket> {
|
|
197
|
+
const errors: string[] = [];
|
|
198
|
+
|
|
199
|
+
for (const strategy of this.#strategies) {
|
|
200
|
+
try {
|
|
201
|
+
switch (strategy) {
|
|
202
|
+
case 'direct':
|
|
203
|
+
return await this.#connectDirect(target);
|
|
204
|
+
case 'webrtc':
|
|
205
|
+
return await this.#connectWebRTC(target);
|
|
206
|
+
case 'websocket':
|
|
207
|
+
return await this.#connectWebSocket(target);
|
|
208
|
+
default:
|
|
209
|
+
throw new Error(`unknown strategy: ${strategy}`);
|
|
210
|
+
}
|
|
211
|
+
} catch (e: any) {
|
|
212
|
+
errors.push(`${strategy}: ${e.message}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
throw new Error(`all strategies failed for ${target}: ${errors.join('; ')}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Close WebRTC peer connection and release resources. */
|
|
220
|
+
close(): void {
|
|
221
|
+
if (this.#rtcPc) {
|
|
222
|
+
this.#rtcPc.close();
|
|
223
|
+
this.#rtcPc = null;
|
|
224
|
+
this.#rtcAlive = false;
|
|
225
|
+
this.#signalChannel = null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// -- Direct TCP strategy (Node.js / Deno) ---------------------------------
|
|
230
|
+
|
|
231
|
+
async #connectDirect(target: string): Promise<ArtiSocket> {
|
|
232
|
+
const [host, portStr] = target.split(':');
|
|
233
|
+
const port = parseInt(portStr, 10);
|
|
234
|
+
|
|
235
|
+
if (HAS_DENO) {
|
|
236
|
+
const conn = await (globalThis as any).Deno.connect({ hostname: host, port });
|
|
237
|
+
return ArtiSocket.fromDenoConn(conn);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (HAS_NODE) {
|
|
241
|
+
const net = await import('node:net');
|
|
242
|
+
const socket = net.createConnection({ host, port });
|
|
243
|
+
await new Promise<void>((resolve, reject) => {
|
|
244
|
+
socket.once('connect', resolve);
|
|
245
|
+
socket.once('error', reject);
|
|
246
|
+
});
|
|
247
|
+
return ArtiSocket.fromNodeSocket(socket);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
throw new Error('direct TCP not available in this environment');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// -- WebSocket strategy ---------------------------------------------------
|
|
254
|
+
|
|
255
|
+
async #connectWebSocket(target: string): Promise<ArtiSocket> {
|
|
256
|
+
if (!this.#url) throw new Error('websocket strategy requires a gateway URL');
|
|
257
|
+
const wsUrl = `${this.#url.replace(/^http/, 'ws')}/socket/${target}`;
|
|
258
|
+
const ws = new WebSocket(wsUrl);
|
|
259
|
+
ws.binaryType = 'arraybuffer';
|
|
260
|
+
|
|
261
|
+
await new Promise<void>((resolve, reject) => {
|
|
262
|
+
ws.onopen = () => resolve();
|
|
263
|
+
ws.onerror = () => reject(new Error('websocket connection failed'));
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
return ArtiSocket.fromWebSocket(ws);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// -- WebRTC strategy ------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
async #connectWebRTC(target: string): Promise<ArtiSocket> {
|
|
272
|
+
if (!this.#url) throw new Error('webrtc strategy requires a gateway URL');
|
|
273
|
+
if (typeof RTCPeerConnection === 'undefined') {
|
|
274
|
+
throw new Error('RTCPeerConnection not available');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Create or reuse peer connection
|
|
278
|
+
if (!this.#rtcAlive) {
|
|
279
|
+
if (this.#rtcPc) this.#rtcPc.close();
|
|
280
|
+
await this.#setupRtcPeerConnection();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const dc = this.#rtcPc!.createDataChannel(target);
|
|
284
|
+
dc.binaryType = 'arraybuffer';
|
|
285
|
+
|
|
286
|
+
const entry = { dc, sock: null as ArtiSocket | null, reject: null as ((err: Error) => void) | null };
|
|
287
|
+
this.#tracked.push(entry);
|
|
288
|
+
|
|
289
|
+
// Race: channel opens vs server rejects via _signal
|
|
290
|
+
await new Promise<void>((resolve, reject) => {
|
|
291
|
+
entry.reject = reject;
|
|
292
|
+
dc.onopen = () => resolve();
|
|
293
|
+
dc.onerror = (e: any) => {
|
|
294
|
+
this.#removeTracked(entry);
|
|
295
|
+
reject(new Error(`data channel error: ${e.error?.message || e}`));
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Channel is open — dc.id now available
|
|
300
|
+
entry.reject = null;
|
|
301
|
+
const sock = ArtiSocket.fromDataChannel(dc);
|
|
302
|
+
entry.sock = sock;
|
|
303
|
+
dc.onclose = () => {
|
|
304
|
+
this.#removeTracked(entry);
|
|
305
|
+
sock._notifyClose();
|
|
306
|
+
};
|
|
307
|
+
return sock;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async #setupRtcPeerConnection(): Promise<void> {
|
|
311
|
+
const pc = new RTCPeerConnection();
|
|
312
|
+
|
|
313
|
+
// Signal channel for control messages (hello, ping/pong, rejections)
|
|
314
|
+
const signal = pc.createDataChannel('_signal');
|
|
315
|
+
signal.onmessage = (ev) => this.#handleSignalMessage(ev.data);
|
|
316
|
+
|
|
317
|
+
const offer = await pc.createOffer();
|
|
318
|
+
await pc.setLocalDescription(offer);
|
|
319
|
+
|
|
320
|
+
// Wait for ICE gathering to complete
|
|
321
|
+
await new Promise<void>((resolve) => {
|
|
322
|
+
if (pc.iceGatheringState === 'complete') return resolve();
|
|
323
|
+
pc.addEventListener('icegatheringstatechange', () => {
|
|
324
|
+
if (pc.iceGatheringState === 'complete') resolve();
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const res = await fetch(`${this.#url}/rtc/connect`, {
|
|
329
|
+
method: 'POST',
|
|
330
|
+
body: JSON.stringify(pc.localDescription),
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
if (!res.ok) {
|
|
334
|
+
pc.close();
|
|
335
|
+
throw new Error(`rtc signaling failed: ${res.status} ${await res.text()}`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const answer = await res.json();
|
|
339
|
+
await pc.setRemoteDescription(answer);
|
|
340
|
+
|
|
341
|
+
// Wait for connection
|
|
342
|
+
await new Promise<void>((resolve, reject) => {
|
|
343
|
+
if (pc.connectionState === 'connected') return resolve();
|
|
344
|
+
pc.addEventListener('connectionstatechange', () => {
|
|
345
|
+
if (pc.connectionState === 'connected') resolve();
|
|
346
|
+
if (pc.connectionState === 'failed') reject(new Error('WebRTC connection failed'));
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
this.#rtcPc = pc;
|
|
351
|
+
this.#rtcAlive = true;
|
|
352
|
+
this.#signalChannel = signal;
|
|
353
|
+
|
|
354
|
+
pc.addEventListener('connectionstatechange', () => {
|
|
355
|
+
const s = pc.connectionState;
|
|
356
|
+
if (s === 'disconnected' || s === 'closed' || s === 'failed') {
|
|
357
|
+
this.#rtcAlive = false;
|
|
358
|
+
this.#signalChannel = null;
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
#findTracked(sctpId: number | null, label: string) {
|
|
364
|
+
return this.#tracked.find(e => e.dc.id != null && e.dc.id === sctpId)
|
|
365
|
+
?? this.#tracked.find(e => e.dc.label === label);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
#removeTracked(entry: TrackedEntry): void {
|
|
369
|
+
const i = this.#tracked.indexOf(entry);
|
|
370
|
+
if (i !== -1) this.#tracked.splice(i, 1);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
#handleSignalMessage(data: string): void {
|
|
374
|
+
try {
|
|
375
|
+
const msg = JSON.parse(data);
|
|
376
|
+
switch (msg.type) {
|
|
377
|
+
case 'rejected': {
|
|
378
|
+
const entry = this.#findTracked(msg.sctp_id, msg.channel);
|
|
379
|
+
if (entry) {
|
|
380
|
+
this.#removeTracked(entry);
|
|
381
|
+
if (entry.reject) {
|
|
382
|
+
entry.reject(new Error(`rejected: ${msg.reason}`));
|
|
383
|
+
} else if (entry.sock) {
|
|
384
|
+
entry.sock._error = msg.reason;
|
|
385
|
+
entry.sock.close();
|
|
386
|
+
entry.sock._notifyClose();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
case 'closed': {
|
|
392
|
+
const entry = this.#findTracked(msg.sctp_id, msg.channel);
|
|
393
|
+
if (entry) {
|
|
394
|
+
this.#removeTracked(entry);
|
|
395
|
+
if (entry.sock) {
|
|
396
|
+
entry.sock.close();
|
|
397
|
+
entry.sock._notifyClose();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
} catch { /* ignore malformed signal messages */ }
|
|
404
|
+
}
|
|
405
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { TorStorageSimple } from './locking.js';
|
|
2
|
+
import { getNodeDeps } from './node-deps.js';
|
|
3
|
+
|
|
4
|
+
function isNodeError(err: unknown): err is NodeJS.ErrnoException {
|
|
5
|
+
return err instanceof Error && 'code' in err;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Encode a storage key into a filesystem-safe filename.
|
|
10
|
+
* Alphanumeric characters pass through; everything else becomes _XX_ or _XXXX_.
|
|
11
|
+
*/
|
|
12
|
+
function mangleKey(key: string): string {
|
|
13
|
+
let result = '';
|
|
14
|
+
for (let i = 0; i < key.length; i++) {
|
|
15
|
+
const code = key.charCodeAt(i);
|
|
16
|
+
if (
|
|
17
|
+
(code >= 97 && code <= 122) || // a-z
|
|
18
|
+
(code >= 65 && code <= 90) || // A-Z
|
|
19
|
+
(code >= 48 && code <= 57) // 0-9
|
|
20
|
+
) {
|
|
21
|
+
result += key[i];
|
|
22
|
+
} else if (code <= 0xff) {
|
|
23
|
+
result += '_' + code.toString(16).padStart(2, '0') + '_';
|
|
24
|
+
} else {
|
|
25
|
+
result += '_' + code.toString(16).padStart(4, '0') + '_';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Decode a mangled filename back to the original key.
|
|
33
|
+
*/
|
|
34
|
+
function unmangleKey(filename: string): string {
|
|
35
|
+
let result = '';
|
|
36
|
+
let i = 0;
|
|
37
|
+
while (i < filename.length) {
|
|
38
|
+
if (filename[i] === '_') {
|
|
39
|
+
// Try _XXXX_ (4-digit)
|
|
40
|
+
if (i + 5 < filename.length && filename[i + 5] === '_') {
|
|
41
|
+
const hex = filename.slice(i + 1, i + 5);
|
|
42
|
+
if (/^[0-9a-f]{4}$/i.test(hex)) {
|
|
43
|
+
result += String.fromCharCode(parseInt(hex, 16));
|
|
44
|
+
i += 6;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Try _XX_ (2-digit)
|
|
49
|
+
if (i + 3 < filename.length && filename[i + 3] === '_') {
|
|
50
|
+
const hex = filename.slice(i + 1, i + 3);
|
|
51
|
+
if (/^[0-9a-f]{2}$/i.test(hex)) {
|
|
52
|
+
result += String.fromCharCode(parseInt(hex, 16));
|
|
53
|
+
i += 4;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
result += '_';
|
|
58
|
+
i++;
|
|
59
|
+
} else {
|
|
60
|
+
result += filename[i];
|
|
61
|
+
i++;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class FilesystemStorage implements TorStorageSimple {
|
|
68
|
+
private dirPath: string | null;
|
|
69
|
+
private name: string | null;
|
|
70
|
+
private resolvedDirPath: string | null = null;
|
|
71
|
+
private initialized = false;
|
|
72
|
+
|
|
73
|
+
constructor(dirPath: string) {
|
|
74
|
+
this.dirPath = dirPath;
|
|
75
|
+
this.name = null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static localShare(name: string): FilesystemStorage {
|
|
79
|
+
const s = new FilesystemStorage('');
|
|
80
|
+
s.dirPath = null;
|
|
81
|
+
s.name = name;
|
|
82
|
+
return s;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async resolvedDir(): Promise<string> {
|
|
86
|
+
if (!this.resolvedDirPath) {
|
|
87
|
+
if (this.dirPath) {
|
|
88
|
+
this.resolvedDirPath = this.dirPath;
|
|
89
|
+
} else {
|
|
90
|
+
const { os, path } = await getNodeDeps();
|
|
91
|
+
this.resolvedDirPath = path.join(os.homedir(), '.local', 'share', this.name!);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return this.resolvedDirPath!;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private async ensureDir(): Promise<void> {
|
|
98
|
+
if (!this.initialized) {
|
|
99
|
+
const { fs } = await getNodeDeps();
|
|
100
|
+
await fs.mkdir(await this.resolvedDir(), { recursive: true });
|
|
101
|
+
this.initialized = true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private async filePath(key: string): Promise<string> {
|
|
106
|
+
const { path } = await getNodeDeps();
|
|
107
|
+
return path.join(await this.resolvedDir(), mangleKey(key));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async get(key: string): Promise<string | null> {
|
|
111
|
+
const { fs } = await getNodeDeps();
|
|
112
|
+
await this.ensureDir();
|
|
113
|
+
try {
|
|
114
|
+
return await fs.readFile(await this.filePath(key), 'utf-8');
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (isNodeError(err) && err.code === 'ENOENT') return null;
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async set(key: string, value: string): Promise<void> {
|
|
122
|
+
const { fs } = await getNodeDeps();
|
|
123
|
+
await this.ensureDir();
|
|
124
|
+
await fs.writeFile(await this.filePath(key), value, 'utf-8');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async delete(key: string): Promise<void> {
|
|
128
|
+
const { fs } = await getNodeDeps();
|
|
129
|
+
await this.ensureDir();
|
|
130
|
+
try {
|
|
131
|
+
await fs.unlink(await this.filePath(key));
|
|
132
|
+
} catch (err) {
|
|
133
|
+
if (isNodeError(err) && err.code === 'ENOENT') return;
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async keys(prefix: string): Promise<string[]> {
|
|
139
|
+
const { fs } = await getNodeDeps();
|
|
140
|
+
await this.ensureDir();
|
|
141
|
+
try {
|
|
142
|
+
const files = await fs.readdir(await this.resolvedDir());
|
|
143
|
+
return files
|
|
144
|
+
.map(unmangleKey)
|
|
145
|
+
.filter(k => k.startsWith(prefix))
|
|
146
|
+
.sort();
|
|
147
|
+
} catch (err) {
|
|
148
|
+
if (isNodeError(err) && err.code === 'ENOENT') return [];
|
|
149
|
+
throw err;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async getAll(prefix: string): Promise<[string, string][]> {
|
|
154
|
+
const { fs } = await getNodeDeps();
|
|
155
|
+
await this.ensureDir();
|
|
156
|
+
try {
|
|
157
|
+
const files = await fs.readdir(await this.resolvedDir());
|
|
158
|
+
const keys = files.map(unmangleKey).filter(k => k.startsWith(prefix));
|
|
159
|
+
const entries = await Promise.all(
|
|
160
|
+
keys.map(async (key): Promise<[string, string] | null> => {
|
|
161
|
+
const value = await this.get(key);
|
|
162
|
+
return value !== null ? [key, value] : null;
|
|
163
|
+
})
|
|
164
|
+
);
|
|
165
|
+
return entries.filter((e): e is [string, string] => e !== null);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
if (isNodeError(err) && err.code === 'ENOENT') return [];
|
|
168
|
+
throw err;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export { MemoryStorage } from './memory.js';
|
|
2
|
+
export { IndexedDBStorage } from './indexeddb.js';
|
|
3
|
+
export { FilesystemStorage } from './filesystem.js';
|
|
4
|
+
export { addLocking, type TorStorageSimple } from './locking.js';
|
|
5
|
+
|
|
6
|
+
import type { TorStorage } from '#wasm';
|
|
7
|
+
import { IndexedDBStorage } from './indexeddb.js';
|
|
8
|
+
import { FilesystemStorage } from './filesystem.js';
|
|
9
|
+
import { addLocking } from './locking.js';
|
|
10
|
+
|
|
11
|
+
export function createAutoStorage(name: string = 'tor-js'): TorStorage {
|
|
12
|
+
if (typeof globalThis !== 'undefined' && typeof globalThis.indexedDB !== 'undefined') {
|
|
13
|
+
return addLocking(new IndexedDBStorage(name), name);
|
|
14
|
+
}
|
|
15
|
+
if (typeof process !== 'undefined' && process.versions?.node) {
|
|
16
|
+
return addLocking(FilesystemStorage.localShare(name), name);
|
|
17
|
+
}
|
|
18
|
+
throw new Error(
|
|
19
|
+
'No persistent storage available: need IndexedDB (browser) or filesystem (Node.js)',
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { TorStorageSimple } from './locking.js';
|
|
2
|
+
|
|
3
|
+
export class IndexedDBStorage implements TorStorageSimple {
|
|
4
|
+
private dbName: string;
|
|
5
|
+
private storeName = 'keyvalue';
|
|
6
|
+
private dbPromise: Promise<IDBDatabase> | null = null;
|
|
7
|
+
|
|
8
|
+
constructor(name: string = 'tor-js') {
|
|
9
|
+
this.dbName = name;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
private getDB(): Promise<IDBDatabase> {
|
|
13
|
+
if (!this.dbPromise) {
|
|
14
|
+
this.dbPromise = new Promise((resolve, reject) => {
|
|
15
|
+
const request = indexedDB.open(this.dbName, 1);
|
|
16
|
+
request.onerror = () => reject(request.error);
|
|
17
|
+
request.onsuccess = () => resolve(request.result);
|
|
18
|
+
request.onupgradeneeded = (event) => {
|
|
19
|
+
const db = (event.target as IDBOpenDBRequest).result;
|
|
20
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
21
|
+
db.createObjectStore(this.storeName);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return this.dbPromise;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async get(key: string): Promise<string | null> {
|
|
30
|
+
const db = await this.getDB();
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const tx = db.transaction(this.storeName, 'readonly');
|
|
33
|
+
const store = tx.objectStore(this.storeName);
|
|
34
|
+
const request = store.get(key);
|
|
35
|
+
request.onerror = () => reject(request.error);
|
|
36
|
+
request.onsuccess = () => {
|
|
37
|
+
resolve(request.result === undefined ? null : request.result);
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async set(key: string, value: string): Promise<void> {
|
|
43
|
+
const db = await this.getDB();
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
const tx = db.transaction(this.storeName, 'readwrite');
|
|
46
|
+
const store = tx.objectStore(this.storeName);
|
|
47
|
+
const request = store.put(value, key);
|
|
48
|
+
request.onerror = () => reject(request.error);
|
|
49
|
+
request.onsuccess = () => resolve();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async delete(key: string): Promise<void> {
|
|
54
|
+
const db = await this.getDB();
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const tx = db.transaction(this.storeName, 'readwrite');
|
|
57
|
+
const store = tx.objectStore(this.storeName);
|
|
58
|
+
const request = store.delete(key);
|
|
59
|
+
request.onerror = () => reject(request.error);
|
|
60
|
+
request.onsuccess = () => resolve();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async keys(prefix: string): Promise<string[]> {
|
|
65
|
+
const db = await this.getDB();
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const tx = db.transaction(this.storeName, 'readonly');
|
|
68
|
+
const store = tx.objectStore(this.storeName);
|
|
69
|
+
const request = store.getAllKeys();
|
|
70
|
+
request.onerror = () => reject(request.error);
|
|
71
|
+
request.onsuccess = () => {
|
|
72
|
+
const allKeys = request.result as string[];
|
|
73
|
+
resolve(allKeys.filter(k => k.startsWith(prefix)).sort());
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async getAll(prefix: string): Promise<[string, string][]> {
|
|
79
|
+
const db = await this.getDB();
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
const tx = db.transaction(this.storeName, 'readonly');
|
|
82
|
+
const store = tx.objectStore(this.storeName);
|
|
83
|
+
const keysReq = store.getAllKeys();
|
|
84
|
+
const valsReq = store.getAll();
|
|
85
|
+
tx.onerror = () => reject(tx.error);
|
|
86
|
+
tx.oncomplete = () => {
|
|
87
|
+
const keys = keysReq.result as string[];
|
|
88
|
+
const vals = valsReq.result as string[];
|
|
89
|
+
const result: [string, string][] = [];
|
|
90
|
+
for (let i = 0; i < keys.length; i++) {
|
|
91
|
+
if (keys[i].startsWith(prefix)) {
|
|
92
|
+
result.push([keys[i], vals[i]]);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
resolve(result);
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|