fied 0.1.3 → 0.1.5
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/dist/bin.js +125 -73
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -108,6 +108,8 @@ var DEFAULT_RELAY = "https://fied.app";
|
|
|
108
108
|
var MSG_TERMINAL_OUTPUT = 1;
|
|
109
109
|
var MSG_TERMINAL_INPUT = 2;
|
|
110
110
|
var MSG_RESIZE = 3;
|
|
111
|
+
var RECONNECT_BASE_MS = 1e3;
|
|
112
|
+
var RECONNECT_MAX_MS = 3e4;
|
|
111
113
|
async function share(options2) {
|
|
112
114
|
const relay = options2.relay ?? DEFAULT_RELAY;
|
|
113
115
|
const sessions = listSessions();
|
|
@@ -143,22 +145,27 @@ async function share(options2) {
|
|
|
143
145
|
const rawKey = await generateKey();
|
|
144
146
|
const cryptoKey = await importKey(rawKey);
|
|
145
147
|
const keyFragment = toBase64Url(rawKey);
|
|
146
|
-
const
|
|
147
|
-
const url = `${relay}/s/${sessionId}#${keyFragment}`;
|
|
148
|
+
const pty = attachSession(targetSession, cols, rows);
|
|
148
149
|
console.log("");
|
|
149
150
|
console.log(" \x1B[1m\x1B[32mfied\x1B[0m \u2014 encrypted terminal sharing");
|
|
150
151
|
console.log("");
|
|
151
152
|
console.log(` Session: ${targetSession}`);
|
|
152
153
|
console.log(` Size: ${cols}x${rows}`);
|
|
153
154
|
console.log("");
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
155
|
+
const bridge = new RelayBridge(relay, cryptoKey, keyFragment, pty);
|
|
156
|
+
await bridge.connect();
|
|
157
|
+
const shutdown = () => {
|
|
158
|
+
bridge.destroy();
|
|
159
|
+
pty.kill();
|
|
160
|
+
};
|
|
161
|
+
process.on("SIGINT", shutdown);
|
|
162
|
+
process.on("SIGTERM", shutdown);
|
|
163
|
+
await new Promise((resolve) => {
|
|
164
|
+
pty.onExit(() => {
|
|
165
|
+
bridge.destroy();
|
|
166
|
+
resolve();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
162
169
|
}
|
|
163
170
|
async function createSession(relay) {
|
|
164
171
|
const res = await fetch(`${relay}/api/sessions`, { method: "POST" });
|
|
@@ -168,39 +175,71 @@ async function createSession(relay) {
|
|
|
168
175
|
const data = await res.json();
|
|
169
176
|
return data.sessionId;
|
|
170
177
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
178
|
+
var WS_CONNECT_TIMEOUT_MS = 1e4;
|
|
179
|
+
var RelayBridge = class {
|
|
180
|
+
constructor(relay, key, keyFragment, pty) {
|
|
181
|
+
this.relay = relay;
|
|
182
|
+
this.key = key;
|
|
183
|
+
this.keyFragment = keyFragment;
|
|
184
|
+
this.pty = pty;
|
|
185
|
+
this.pty.onData((data) => {
|
|
186
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
187
|
+
this.sendEncrypted(MSG_TERMINAL_OUTPUT, this.encoder.encode(data));
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
ws = null;
|
|
192
|
+
destroyed = false;
|
|
193
|
+
backoff = RECONNECT_BASE_MS;
|
|
194
|
+
reconnectTimer = null;
|
|
195
|
+
connectTimeout = null;
|
|
196
|
+
encoder = new TextEncoder();
|
|
197
|
+
decoder = new TextDecoder();
|
|
198
|
+
firstConnect = true;
|
|
199
|
+
async connect() {
|
|
200
|
+
if (this.destroyed) return;
|
|
201
|
+
let sessionId;
|
|
202
|
+
try {
|
|
203
|
+
sessionId = await createSession(this.relay);
|
|
204
|
+
} catch {
|
|
205
|
+
if (this.firstConnect) {
|
|
206
|
+
console.error(" \x1B[31mRelay unreachable, retrying...\x1B[0m");
|
|
207
|
+
}
|
|
208
|
+
this.scheduleReconnect();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const url = `${this.relay}/s/${sessionId}#${this.keyFragment}`;
|
|
212
|
+
if (this.firstConnect) {
|
|
213
|
+
console.log(` \x1B[1mShare this link:\x1B[0m`);
|
|
214
|
+
console.log(` \x1B[4m\x1B[36m${url}\x1B[0m`);
|
|
215
|
+
console.log("");
|
|
216
|
+
console.log(" \x1B[2mThe encryption key is in the URL fragment (#) \u2014 the server never sees it.\x1B[0m");
|
|
217
|
+
console.log(" \x1B[2mPress Ctrl+C to stop sharing.\x1B[0m");
|
|
218
|
+
console.log("");
|
|
219
|
+
} else {
|
|
220
|
+
console.error(` \x1B[32mReconnected.\x1B[0m New link: \x1B[4m\x1B[36m${url}\x1B[0m`);
|
|
221
|
+
}
|
|
222
|
+
this.firstConnect = false;
|
|
223
|
+
const wsUrl = this.relay.replace(/^http/, "ws") + `/api/sessions/${sessionId}/ws?role=host`;
|
|
174
224
|
const ws = new WebSocket(wsUrl);
|
|
175
225
|
ws.binaryType = "arraybuffer";
|
|
176
|
-
|
|
177
|
-
|
|
226
|
+
this.ws = ws;
|
|
227
|
+
this.connectTimeout = setTimeout(() => {
|
|
228
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
229
|
+
ws.terminate();
|
|
230
|
+
}
|
|
231
|
+
}, WS_CONNECT_TIMEOUT_MS);
|
|
178
232
|
ws.on("open", () => {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
}, 3e4);
|
|
184
|
-
const resizePayload = JSON.stringify({
|
|
185
|
-
cols: pty.cols,
|
|
186
|
-
rows: pty.rows
|
|
187
|
-
});
|
|
188
|
-
sendEncrypted(ws, key, MSG_RESIZE, new TextEncoder().encode(resizePayload));
|
|
189
|
-
});
|
|
190
|
-
pty.onData((data) => {
|
|
191
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
192
|
-
sendEncrypted(ws, key, MSG_TERMINAL_OUTPUT, new TextEncoder().encode(data));
|
|
233
|
+
if (this.connectTimeout) {
|
|
234
|
+
clearTimeout(this.connectTimeout);
|
|
235
|
+
this.connectTimeout = null;
|
|
193
236
|
}
|
|
237
|
+
this.backoff = RECONNECT_BASE_MS;
|
|
194
238
|
});
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
resolve();
|
|
200
|
-
});
|
|
201
|
-
ws.on("message", async (raw) => {
|
|
202
|
-
if (typeof raw === "string") {
|
|
203
|
-
if (raw === "__fied_ping__") {
|
|
239
|
+
ws.on("message", async (raw, isBinary) => {
|
|
240
|
+
if (!isBinary) {
|
|
241
|
+
const text = this.decoder.decode(raw);
|
|
242
|
+
if (text === "__fied_ping__") {
|
|
204
243
|
ws.send("__fied_pong__");
|
|
205
244
|
}
|
|
206
245
|
return;
|
|
@@ -209,50 +248,63 @@ async function connectToRelay(relay, sessionId, key, pty) {
|
|
|
209
248
|
const data = new Uint8Array(raw);
|
|
210
249
|
const frame = parseFrame(data);
|
|
211
250
|
if (frame.type === MSG_TERMINAL_INPUT) {
|
|
212
|
-
const plaintext = await decrypt(key, frame.iv, frame.ciphertext);
|
|
213
|
-
pty.write(
|
|
251
|
+
const plaintext = await decrypt(this.key, frame.iv, frame.ciphertext);
|
|
252
|
+
this.pty.write(this.decoder.decode(plaintext));
|
|
214
253
|
} else if (frame.type === MSG_RESIZE) {
|
|
215
|
-
const plaintext = await decrypt(key, frame.iv, frame.ciphertext);
|
|
216
|
-
const { cols, rows } = JSON.parse(
|
|
217
|
-
pty.resize(cols, rows);
|
|
254
|
+
const plaintext = await decrypt(this.key, frame.iv, frame.ciphertext);
|
|
255
|
+
const { cols, rows } = JSON.parse(this.decoder.decode(plaintext));
|
|
256
|
+
this.pty.resize(cols, rows);
|
|
218
257
|
}
|
|
219
|
-
} catch
|
|
220
|
-
console.error("Failed to process incoming message:", err);
|
|
258
|
+
} catch {
|
|
221
259
|
}
|
|
222
260
|
});
|
|
223
261
|
ws.on("close", () => {
|
|
224
|
-
if (
|
|
225
|
-
|
|
226
|
-
|
|
262
|
+
if (this.connectTimeout) {
|
|
263
|
+
clearTimeout(this.connectTimeout);
|
|
264
|
+
this.connectTimeout = null;
|
|
265
|
+
}
|
|
266
|
+
this.ws = null;
|
|
267
|
+
if (!this.destroyed) {
|
|
268
|
+
console.error(" \x1B[33mConnection lost, reconnecting...\x1B[0m");
|
|
269
|
+
this.scheduleReconnect();
|
|
227
270
|
}
|
|
228
|
-
resolve();
|
|
229
|
-
});
|
|
230
|
-
ws.on("error", (err) => {
|
|
231
|
-
console.error("WebSocket error:", err.message);
|
|
232
|
-
});
|
|
233
|
-
process.on("SIGINT", () => {
|
|
234
|
-
alive = false;
|
|
235
|
-
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
236
|
-
ws.close();
|
|
237
|
-
pty.kill();
|
|
238
271
|
});
|
|
239
|
-
|
|
240
|
-
alive = false;
|
|
241
|
-
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
242
|
-
ws.close();
|
|
243
|
-
pty.kill();
|
|
272
|
+
ws.on("error", () => {
|
|
244
273
|
});
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
async function sendEncrypted(ws, key, type, plaintext) {
|
|
248
|
-
try {
|
|
249
|
-
const { iv, ciphertext } = await encrypt(key, plaintext);
|
|
250
|
-
const frame = frameMessage(type, iv, ciphertext);
|
|
251
|
-
ws.send(frame);
|
|
252
|
-
} catch (err) {
|
|
253
|
-
console.error("Encryption failed:", err);
|
|
254
274
|
}
|
|
255
|
-
|
|
275
|
+
destroy() {
|
|
276
|
+
this.destroyed = true;
|
|
277
|
+
if (this.reconnectTimer) {
|
|
278
|
+
clearTimeout(this.reconnectTimer);
|
|
279
|
+
this.reconnectTimer = null;
|
|
280
|
+
}
|
|
281
|
+
if (this.connectTimeout) {
|
|
282
|
+
clearTimeout(this.connectTimeout);
|
|
283
|
+
this.connectTimeout = null;
|
|
284
|
+
}
|
|
285
|
+
if (this.ws) {
|
|
286
|
+
this.ws.close();
|
|
287
|
+
this.ws = null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
scheduleReconnect() {
|
|
291
|
+
if (this.destroyed || this.reconnectTimer) return;
|
|
292
|
+
this.reconnectTimer = setTimeout(() => {
|
|
293
|
+
this.reconnectTimer = null;
|
|
294
|
+
this.connect();
|
|
295
|
+
}, this.backoff);
|
|
296
|
+
this.backoff = Math.min(this.backoff * 2, RECONNECT_MAX_MS);
|
|
297
|
+
}
|
|
298
|
+
async sendEncrypted(type, plaintext) {
|
|
299
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
300
|
+
try {
|
|
301
|
+
const { iv, ciphertext } = await encrypt(this.key, plaintext);
|
|
302
|
+
const frame = frameMessage(type, iv, ciphertext);
|
|
303
|
+
this.ws.send(frame);
|
|
304
|
+
} catch {
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
};
|
|
256
308
|
|
|
257
309
|
// src/bin.ts
|
|
258
310
|
var args = process.argv.slice(2);
|