fied 0.1.3 → 0.1.4
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 +99 -72
- 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,54 @@ async function createSession(relay) {
|
|
|
168
175
|
const data = await res.json();
|
|
169
176
|
return data.sessionId;
|
|
170
177
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
178
|
+
var RelayBridge = class {
|
|
179
|
+
constructor(relay, key, keyFragment, pty) {
|
|
180
|
+
this.relay = relay;
|
|
181
|
+
this.key = key;
|
|
182
|
+
this.keyFragment = keyFragment;
|
|
183
|
+
this.pty = pty;
|
|
184
|
+
}
|
|
185
|
+
ws = null;
|
|
186
|
+
destroyed = false;
|
|
187
|
+
backoff = RECONNECT_BASE_MS;
|
|
188
|
+
reconnectTimer = null;
|
|
189
|
+
encoder = new TextEncoder();
|
|
190
|
+
decoder = new TextDecoder();
|
|
191
|
+
async connect() {
|
|
192
|
+
if (this.destroyed) return;
|
|
193
|
+
let sessionId;
|
|
194
|
+
try {
|
|
195
|
+
sessionId = await createSession(this.relay);
|
|
196
|
+
} catch {
|
|
197
|
+
console.error(" \x1B[33mRelay unreachable, retrying...\x1B[0m");
|
|
198
|
+
this.scheduleReconnect();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const url = `${this.relay}/s/${sessionId}#${this.keyFragment}`;
|
|
202
|
+
console.log(` \x1B[1mShare this link:\x1B[0m`);
|
|
203
|
+
console.log(` \x1B[4m\x1B[36m${url}\x1B[0m`);
|
|
204
|
+
console.log("");
|
|
205
|
+
console.log(" \x1B[2mThe encryption key is in the URL fragment (#) \u2014 the server never sees it.\x1B[0m");
|
|
206
|
+
console.log(" \x1B[2mPress Ctrl+C to stop sharing.\x1B[0m");
|
|
207
|
+
console.log("");
|
|
208
|
+
const wsUrl = this.relay.replace(/^http/, "ws") + `/api/sessions/${sessionId}/ws?role=host`;
|
|
174
209
|
const ws = new WebSocket(wsUrl);
|
|
175
210
|
ws.binaryType = "arraybuffer";
|
|
176
|
-
|
|
177
|
-
let heartbeatTimer;
|
|
211
|
+
this.ws = ws;
|
|
178
212
|
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));
|
|
213
|
+
this.backoff = RECONNECT_BASE_MS;
|
|
214
|
+
const resizePayload = JSON.stringify({ cols: this.pty.cols, rows: this.pty.rows });
|
|
215
|
+
this.sendEncrypted(MSG_RESIZE, this.encoder.encode(resizePayload));
|
|
189
216
|
});
|
|
190
|
-
pty.onData((data) => {
|
|
191
|
-
if (ws
|
|
192
|
-
sendEncrypted(
|
|
217
|
+
this.pty.onData((data) => {
|
|
218
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
219
|
+
this.sendEncrypted(MSG_TERMINAL_OUTPUT, this.encoder.encode(data));
|
|
193
220
|
}
|
|
194
221
|
});
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
resolve();
|
|
200
|
-
});
|
|
201
|
-
ws.on("message", async (raw) => {
|
|
202
|
-
if (typeof raw === "string") {
|
|
203
|
-
if (raw === "__fied_ping__") {
|
|
222
|
+
ws.on("message", async (raw, isBinary) => {
|
|
223
|
+
if (!isBinary) {
|
|
224
|
+
const text = this.decoder.decode(raw);
|
|
225
|
+
if (text === "__fied_ping__") {
|
|
204
226
|
ws.send("__fied_pong__");
|
|
205
227
|
}
|
|
206
228
|
return;
|
|
@@ -209,50 +231,55 @@ async function connectToRelay(relay, sessionId, key, pty) {
|
|
|
209
231
|
const data = new Uint8Array(raw);
|
|
210
232
|
const frame = parseFrame(data);
|
|
211
233
|
if (frame.type === MSG_TERMINAL_INPUT) {
|
|
212
|
-
const plaintext = await decrypt(key, frame.iv, frame.ciphertext);
|
|
213
|
-
pty.write(
|
|
234
|
+
const plaintext = await decrypt(this.key, frame.iv, frame.ciphertext);
|
|
235
|
+
this.pty.write(this.decoder.decode(plaintext));
|
|
214
236
|
} 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);
|
|
237
|
+
const plaintext = await decrypt(this.key, frame.iv, frame.ciphertext);
|
|
238
|
+
const { cols, rows } = JSON.parse(this.decoder.decode(plaintext));
|
|
239
|
+
this.pty.resize(cols, rows);
|
|
218
240
|
}
|
|
219
|
-
} catch
|
|
220
|
-
console.error("Failed to process incoming message:", err);
|
|
241
|
+
} catch {
|
|
221
242
|
}
|
|
222
243
|
});
|
|
223
244
|
ws.on("close", () => {
|
|
224
|
-
|
|
225
|
-
if (
|
|
226
|
-
|
|
245
|
+
this.ws = null;
|
|
246
|
+
if (!this.destroyed) {
|
|
247
|
+
console.error(" \x1B[33mConnection lost, reconnecting...\x1B[0m");
|
|
248
|
+
this.scheduleReconnect();
|
|
227
249
|
}
|
|
228
|
-
resolve();
|
|
229
250
|
});
|
|
230
|
-
ws.on("error", (
|
|
231
|
-
console.error("WebSocket error:", err.message);
|
|
251
|
+
ws.on("error", () => {
|
|
232
252
|
});
|
|
233
|
-
process.on("SIGINT", () => {
|
|
234
|
-
alive = false;
|
|
235
|
-
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
236
|
-
ws.close();
|
|
237
|
-
pty.kill();
|
|
238
|
-
});
|
|
239
|
-
process.on("SIGTERM", () => {
|
|
240
|
-
alive = false;
|
|
241
|
-
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
242
|
-
ws.close();
|
|
243
|
-
pty.kill();
|
|
244
|
-
});
|
|
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
253
|
}
|
|
255
|
-
|
|
254
|
+
destroy() {
|
|
255
|
+
this.destroyed = true;
|
|
256
|
+
if (this.reconnectTimer) {
|
|
257
|
+
clearTimeout(this.reconnectTimer);
|
|
258
|
+
this.reconnectTimer = null;
|
|
259
|
+
}
|
|
260
|
+
if (this.ws) {
|
|
261
|
+
this.ws.close();
|
|
262
|
+
this.ws = null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
scheduleReconnect() {
|
|
266
|
+
if (this.destroyed || this.reconnectTimer) return;
|
|
267
|
+
this.reconnectTimer = setTimeout(() => {
|
|
268
|
+
this.reconnectTimer = null;
|
|
269
|
+
this.connect();
|
|
270
|
+
}, this.backoff);
|
|
271
|
+
this.backoff = Math.min(this.backoff * 2, RECONNECT_MAX_MS);
|
|
272
|
+
}
|
|
273
|
+
async sendEncrypted(type, plaintext) {
|
|
274
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
275
|
+
try {
|
|
276
|
+
const { iv, ciphertext } = await encrypt(this.key, plaintext);
|
|
277
|
+
const frame = frameMessage(type, iv, ciphertext);
|
|
278
|
+
this.ws.send(frame);
|
|
279
|
+
} catch {
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
};
|
|
256
283
|
|
|
257
284
|
// src/bin.ts
|
|
258
285
|
var args = process.argv.slice(2);
|