fied 0.1.2 → 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 +101 -73
- package/package.json +2 -2
package/dist/bin.js
CHANGED
|
@@ -94,11 +94,12 @@ function listSessions() {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
function attachSession(sessionName, cols, rows) {
|
|
97
|
+
const { TMUX: _, TMUX_PANE: __, ...env } = process.env;
|
|
97
98
|
return spawn("tmux", ["attach-session", "-t", sessionName], {
|
|
98
99
|
name: "xterm-256color",
|
|
99
100
|
cols,
|
|
100
101
|
rows,
|
|
101
|
-
env
|
|
102
|
+
env
|
|
102
103
|
});
|
|
103
104
|
}
|
|
104
105
|
|
|
@@ -107,6 +108,8 @@ var DEFAULT_RELAY = "https://fied.app";
|
|
|
107
108
|
var MSG_TERMINAL_OUTPUT = 1;
|
|
108
109
|
var MSG_TERMINAL_INPUT = 2;
|
|
109
110
|
var MSG_RESIZE = 3;
|
|
111
|
+
var RECONNECT_BASE_MS = 1e3;
|
|
112
|
+
var RECONNECT_MAX_MS = 3e4;
|
|
110
113
|
async function share(options2) {
|
|
111
114
|
const relay = options2.relay ?? DEFAULT_RELAY;
|
|
112
115
|
const sessions = listSessions();
|
|
@@ -142,22 +145,27 @@ async function share(options2) {
|
|
|
142
145
|
const rawKey = await generateKey();
|
|
143
146
|
const cryptoKey = await importKey(rawKey);
|
|
144
147
|
const keyFragment = toBase64Url(rawKey);
|
|
145
|
-
const
|
|
146
|
-
const url = `${relay}/s/${sessionId}#${keyFragment}`;
|
|
148
|
+
const pty = attachSession(targetSession, cols, rows);
|
|
147
149
|
console.log("");
|
|
148
150
|
console.log(" \x1B[1m\x1B[32mfied\x1B[0m \u2014 encrypted terminal sharing");
|
|
149
151
|
console.log("");
|
|
150
152
|
console.log(` Session: ${targetSession}`);
|
|
151
153
|
console.log(` Size: ${cols}x${rows}`);
|
|
152
154
|
console.log("");
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
+
});
|
|
161
169
|
}
|
|
162
170
|
async function createSession(relay) {
|
|
163
171
|
const res = await fetch(`${relay}/api/sessions`, { method: "POST" });
|
|
@@ -167,39 +175,54 @@ async function createSession(relay) {
|
|
|
167
175
|
const data = await res.json();
|
|
168
176
|
return data.sessionId;
|
|
169
177
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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`;
|
|
173
209
|
const ws = new WebSocket(wsUrl);
|
|
174
210
|
ws.binaryType = "arraybuffer";
|
|
175
|
-
|
|
176
|
-
let heartbeatTimer;
|
|
211
|
+
this.ws = ws;
|
|
177
212
|
ws.on("open", () => {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
}, 3e4);
|
|
183
|
-
const resizePayload = JSON.stringify({
|
|
184
|
-
cols: pty.cols,
|
|
185
|
-
rows: pty.rows
|
|
186
|
-
});
|
|
187
|
-
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));
|
|
188
216
|
});
|
|
189
|
-
pty.onData((data) => {
|
|
190
|
-
if (ws
|
|
191
|
-
sendEncrypted(
|
|
217
|
+
this.pty.onData((data) => {
|
|
218
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
219
|
+
this.sendEncrypted(MSG_TERMINAL_OUTPUT, this.encoder.encode(data));
|
|
192
220
|
}
|
|
193
221
|
});
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
resolve();
|
|
199
|
-
});
|
|
200
|
-
ws.on("message", async (raw) => {
|
|
201
|
-
if (typeof raw === "string") {
|
|
202
|
-
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__") {
|
|
203
226
|
ws.send("__fied_pong__");
|
|
204
227
|
}
|
|
205
228
|
return;
|
|
@@ -208,50 +231,55 @@ async function connectToRelay(relay, sessionId, key, pty) {
|
|
|
208
231
|
const data = new Uint8Array(raw);
|
|
209
232
|
const frame = parseFrame(data);
|
|
210
233
|
if (frame.type === MSG_TERMINAL_INPUT) {
|
|
211
|
-
const plaintext = await decrypt(key, frame.iv, frame.ciphertext);
|
|
212
|
-
pty.write(
|
|
234
|
+
const plaintext = await decrypt(this.key, frame.iv, frame.ciphertext);
|
|
235
|
+
this.pty.write(this.decoder.decode(plaintext));
|
|
213
236
|
} else if (frame.type === MSG_RESIZE) {
|
|
214
|
-
const plaintext = await decrypt(key, frame.iv, frame.ciphertext);
|
|
215
|
-
const { cols, rows } = JSON.parse(
|
|
216
|
-
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);
|
|
217
240
|
}
|
|
218
|
-
} catch
|
|
219
|
-
console.error("Failed to process incoming message:", err);
|
|
241
|
+
} catch {
|
|
220
242
|
}
|
|
221
243
|
});
|
|
222
244
|
ws.on("close", () => {
|
|
223
|
-
|
|
224
|
-
if (
|
|
225
|
-
|
|
245
|
+
this.ws = null;
|
|
246
|
+
if (!this.destroyed) {
|
|
247
|
+
console.error(" \x1B[33mConnection lost, reconnecting...\x1B[0m");
|
|
248
|
+
this.scheduleReconnect();
|
|
226
249
|
}
|
|
227
|
-
resolve();
|
|
228
250
|
});
|
|
229
|
-
ws.on("error", (
|
|
230
|
-
console.error("WebSocket error:", err.message);
|
|
251
|
+
ws.on("error", () => {
|
|
231
252
|
});
|
|
232
|
-
process.on("SIGINT", () => {
|
|
233
|
-
alive = false;
|
|
234
|
-
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
235
|
-
ws.close();
|
|
236
|
-
pty.kill();
|
|
237
|
-
});
|
|
238
|
-
process.on("SIGTERM", () => {
|
|
239
|
-
alive = false;
|
|
240
|
-
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
241
|
-
ws.close();
|
|
242
|
-
pty.kill();
|
|
243
|
-
});
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
async function sendEncrypted(ws, key, type, plaintext) {
|
|
247
|
-
try {
|
|
248
|
-
const { iv, ciphertext } = await encrypt(key, plaintext);
|
|
249
|
-
const frame = frameMessage(type, iv, ciphertext);
|
|
250
|
-
ws.send(frame);
|
|
251
|
-
} catch (err) {
|
|
252
|
-
console.error("Encryption failed:", err);
|
|
253
253
|
}
|
|
254
|
-
|
|
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
|
+
};
|
|
255
283
|
|
|
256
284
|
// src/bin.ts
|
|
257
285
|
var args = process.argv.slice(2);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fied",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Share your tmux session in the browser with end-to-end encryption",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"dev": "tsc --watch"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"node-pty": "^1.0.
|
|
17
|
+
"node-pty": "^1.2.0-beta.11",
|
|
18
18
|
"ws": "^8.18.0"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|