fied 0.2.4 → 0.2.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 +110 -15
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -34,13 +34,21 @@ async function importKey(rawKey) {
|
|
|
34
34
|
function generateIV() {
|
|
35
35
|
return getRandomValues(new Uint8Array(IV_LENGTH));
|
|
36
36
|
}
|
|
37
|
-
async function encrypt(key, plaintext) {
|
|
37
|
+
async function encrypt(key, plaintext, additionalData) {
|
|
38
38
|
const iv = generateIV();
|
|
39
|
-
const
|
|
39
|
+
const algorithm = { name: ALGORITHM, iv };
|
|
40
|
+
if (additionalData) {
|
|
41
|
+
algorithm.additionalData = additionalData;
|
|
42
|
+
}
|
|
43
|
+
const encrypted = await getSubtleCrypto().encrypt(algorithm, key, plaintext);
|
|
40
44
|
return { iv, ciphertext: new Uint8Array(encrypted) };
|
|
41
45
|
}
|
|
42
|
-
async function decrypt(key, iv, ciphertext) {
|
|
43
|
-
const
|
|
46
|
+
async function decrypt(key, iv, ciphertext, additionalData) {
|
|
47
|
+
const algorithm = { name: ALGORITHM, iv };
|
|
48
|
+
if (additionalData) {
|
|
49
|
+
algorithm.additionalData = additionalData;
|
|
50
|
+
}
|
|
51
|
+
const decrypted = await getSubtleCrypto().decrypt(algorithm, key, ciphertext);
|
|
44
52
|
return new Uint8Array(decrypted);
|
|
45
53
|
}
|
|
46
54
|
function toBase64Url(bytes) {
|
|
@@ -124,13 +132,14 @@ function attachSession(sessionName, cols, rows) {
|
|
|
124
132
|
}
|
|
125
133
|
|
|
126
134
|
// src/store.ts
|
|
127
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
135
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
128
136
|
import { join } from "node:path";
|
|
129
137
|
import { homedir } from "node:os";
|
|
130
138
|
var STORE_DIR = join(homedir(), ".fied");
|
|
131
139
|
var STORE_FILE = join(STORE_DIR, "sessions.json");
|
|
132
140
|
function ensureDir() {
|
|
133
|
-
mkdirSync(STORE_DIR, { recursive: true });
|
|
141
|
+
mkdirSync(STORE_DIR, { recursive: true, mode: 448 });
|
|
142
|
+
chmodSync(STORE_DIR, 448);
|
|
134
143
|
}
|
|
135
144
|
function isAlive(pid) {
|
|
136
145
|
try {
|
|
@@ -144,8 +153,29 @@ function loadSessions() {
|
|
|
144
153
|
try {
|
|
145
154
|
const raw = readFileSync(STORE_FILE, "utf-8");
|
|
146
155
|
const entries = JSON.parse(raw);
|
|
147
|
-
const
|
|
148
|
-
|
|
156
|
+
const normalized = [];
|
|
157
|
+
let needsRewrite = false;
|
|
158
|
+
for (const entry of entries) {
|
|
159
|
+
if (!entry || typeof entry !== "object") continue;
|
|
160
|
+
if (typeof entry.pid !== "number") continue;
|
|
161
|
+
if (typeof entry.tmuxSession !== "string") continue;
|
|
162
|
+
if (typeof entry.relay !== "string") continue;
|
|
163
|
+
if (typeof entry.startedAt !== "string") continue;
|
|
164
|
+
const sessionId = typeof entry.sessionId === "string" ? entry.sessionId : extractSessionIdFromUrl(entry.url);
|
|
165
|
+
if (typeof entry.sessionId !== "string" || typeof entry.url === "string") {
|
|
166
|
+
needsRewrite = true;
|
|
167
|
+
}
|
|
168
|
+
if (!sessionId) continue;
|
|
169
|
+
normalized.push({
|
|
170
|
+
pid: entry.pid,
|
|
171
|
+
tmuxSession: entry.tmuxSession,
|
|
172
|
+
sessionId,
|
|
173
|
+
relay: entry.relay,
|
|
174
|
+
startedAt: entry.startedAt
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
const alive = normalized.filter((e) => isAlive(e.pid));
|
|
178
|
+
if (alive.length !== entries.length || needsRewrite) {
|
|
149
179
|
saveSessions(alive);
|
|
150
180
|
}
|
|
151
181
|
return alive;
|
|
@@ -155,7 +185,8 @@ function loadSessions() {
|
|
|
155
185
|
}
|
|
156
186
|
function saveSessions(entries) {
|
|
157
187
|
ensureDir();
|
|
158
|
-
writeFileSync(STORE_FILE, JSON.stringify(entries, null, 2));
|
|
188
|
+
writeFileSync(STORE_FILE, JSON.stringify(entries, null, 2), { mode: 384 });
|
|
189
|
+
chmodSync(STORE_FILE, 384);
|
|
159
190
|
}
|
|
160
191
|
function addSession(entry) {
|
|
161
192
|
const entries = loadSessions();
|
|
@@ -176,6 +207,16 @@ function stopSession(pid) {
|
|
|
176
207
|
return false;
|
|
177
208
|
}
|
|
178
209
|
}
|
|
210
|
+
function extractSessionIdFromUrl(url) {
|
|
211
|
+
if (typeof url !== "string") return null;
|
|
212
|
+
try {
|
|
213
|
+
const parsed = new URL(url);
|
|
214
|
+
const match = parsed.pathname.match(/^\/s\/([A-Za-z0-9_-]{8,64})$/);
|
|
215
|
+
return match ? match[1] : null;
|
|
216
|
+
} catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
179
220
|
|
|
180
221
|
// src/index.ts
|
|
181
222
|
var DEFAULT_RELAY = "https://fied.app";
|
|
@@ -188,6 +229,8 @@ var RESIZE_MAX_COLS = 1e3;
|
|
|
188
229
|
var RESIZE_MIN_ROWS = 5;
|
|
189
230
|
var RESIZE_MAX_ROWS = 300;
|
|
190
231
|
var MAX_INVALID_RESIZE_FRAMES = 5;
|
|
232
|
+
var MAX_INVALID_INPUT_FRAMES = 5;
|
|
233
|
+
var MAX_RECENT_INPUT_NONCES = 2048;
|
|
191
234
|
var RECONNECT_BASE_MS = 1e3;
|
|
192
235
|
var RECONNECT_MAX_MS = 3e4;
|
|
193
236
|
async function share(options) {
|
|
@@ -233,10 +276,14 @@ async function share(options) {
|
|
|
233
276
|
const bridge = new RelayBridge(relayTarget, cryptoKey, keyFragment, pty, options.background, options.sessionId);
|
|
234
277
|
const onUrl = (url) => {
|
|
235
278
|
if (options.background) {
|
|
279
|
+
const sessionId = bridge.getSessionId();
|
|
280
|
+
if (!sessionId) {
|
|
281
|
+
throw new Error("missing session id for background mode");
|
|
282
|
+
}
|
|
236
283
|
addSession({
|
|
237
284
|
pid: process.pid,
|
|
238
285
|
tmuxSession: targetSession,
|
|
239
|
-
|
|
286
|
+
sessionId,
|
|
240
287
|
relay: relayTarget.httpBase.toString(),
|
|
241
288
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
242
289
|
});
|
|
@@ -307,6 +354,9 @@ async function createSession(relayHttpBase) {
|
|
|
307
354
|
return data.sessionId;
|
|
308
355
|
}
|
|
309
356
|
var WS_CONNECT_TIMEOUT_MS = 1e4;
|
|
357
|
+
function typeAAD(type) {
|
|
358
|
+
return new Uint8Array([type & 255]);
|
|
359
|
+
}
|
|
310
360
|
var RelayBridge = class {
|
|
311
361
|
constructor(relayTarget, key, keyFragment, pty, silent = false, sessionId) {
|
|
312
362
|
this.relayTarget = relayTarget;
|
|
@@ -331,6 +381,12 @@ var RelayBridge = class {
|
|
|
331
381
|
sessionId = null;
|
|
332
382
|
onUrl = null;
|
|
333
383
|
invalidResizeFrames = 0;
|
|
384
|
+
invalidInputFrames = 0;
|
|
385
|
+
seenInputNonces = /* @__PURE__ */ new Set();
|
|
386
|
+
inputNonceOrder = [];
|
|
387
|
+
getSessionId() {
|
|
388
|
+
return this.sessionId;
|
|
389
|
+
}
|
|
334
390
|
async connect(onUrl) {
|
|
335
391
|
if (this.destroyed) return;
|
|
336
392
|
if (onUrl) {
|
|
@@ -387,10 +443,20 @@ var RelayBridge = class {
|
|
|
387
443
|
const data = new Uint8Array(raw);
|
|
388
444
|
const frame = parseFrame(data);
|
|
389
445
|
if (frame.type === MSG_TERMINAL_INPUT) {
|
|
390
|
-
const plaintext = await decrypt(this.key, frame.iv, frame.ciphertext);
|
|
391
|
-
|
|
446
|
+
const plaintext = await decrypt(this.key, frame.iv, frame.ciphertext, typeAAD(frame.type));
|
|
447
|
+
const input = parseInputPayload(this.decoder.decode(plaintext));
|
|
448
|
+
if (!input || this.isReplayNonce(input.nonce)) {
|
|
449
|
+
this.invalidInputFrames += 1;
|
|
450
|
+
if (this.invalidInputFrames >= MAX_INVALID_INPUT_FRAMES) {
|
|
451
|
+
ws.close(1008, "invalid input frames");
|
|
452
|
+
}
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
this.invalidInputFrames = 0;
|
|
456
|
+
this.rememberInputNonce(input.nonce);
|
|
457
|
+
this.pty.write(input.data);
|
|
392
458
|
} else if (frame.type === MSG_RESIZE) {
|
|
393
|
-
const plaintext = await decrypt(this.key, frame.iv, frame.ciphertext);
|
|
459
|
+
const plaintext = await decrypt(this.key, frame.iv, frame.ciphertext, typeAAD(frame.type));
|
|
394
460
|
const resize = parseResizePayload(this.decoder.decode(plaintext));
|
|
395
461
|
if (!resize) {
|
|
396
462
|
this.invalidResizeFrames += 1;
|
|
@@ -449,7 +515,7 @@ var RelayBridge = class {
|
|
|
449
515
|
async sendEncrypted(type, plaintext) {
|
|
450
516
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
451
517
|
try {
|
|
452
|
-
const { iv, ciphertext } = await encrypt(this.key, plaintext);
|
|
518
|
+
const { iv, ciphertext } = await encrypt(this.key, plaintext, typeAAD(type));
|
|
453
519
|
const frame = frameMessage(type, iv, ciphertext);
|
|
454
520
|
this.ws.send(frame);
|
|
455
521
|
} catch (err) {
|
|
@@ -459,7 +525,34 @@ var RelayBridge = class {
|
|
|
459
525
|
}
|
|
460
526
|
}
|
|
461
527
|
}
|
|
528
|
+
isReplayNonce(nonce) {
|
|
529
|
+
return this.seenInputNonces.has(nonce);
|
|
530
|
+
}
|
|
531
|
+
rememberInputNonce(nonce) {
|
|
532
|
+
this.seenInputNonces.add(nonce);
|
|
533
|
+
this.inputNonceOrder.push(nonce);
|
|
534
|
+
while (this.inputNonceOrder.length > MAX_RECENT_INPUT_NONCES) {
|
|
535
|
+
const dropped = this.inputNonceOrder.shift();
|
|
536
|
+
if (!dropped) break;
|
|
537
|
+
this.seenInputNonces.delete(dropped);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
462
540
|
};
|
|
541
|
+
function parseInputPayload(payload) {
|
|
542
|
+
let parsed;
|
|
543
|
+
try {
|
|
544
|
+
parsed = JSON.parse(payload);
|
|
545
|
+
} catch {
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
549
|
+
const typed = parsed;
|
|
550
|
+
if (typeof typed.nonce !== "string" || typed.nonce.length < 8 || typed.nonce.length > 64) {
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
if (typeof typed.data !== "string") return null;
|
|
554
|
+
return { nonce: typed.nonce, data: typed.data };
|
|
555
|
+
}
|
|
463
556
|
function parseResizePayload(payload) {
|
|
464
557
|
let parsed;
|
|
465
558
|
try {
|
|
@@ -469,6 +562,7 @@ function parseResizePayload(payload) {
|
|
|
469
562
|
}
|
|
470
563
|
if (!parsed || typeof parsed !== "object") return null;
|
|
471
564
|
const typed = parsed;
|
|
565
|
+
if (typeof typed.nonce !== "string" || typed.nonce.length < 8 || typed.nonce.length > 64) return null;
|
|
472
566
|
if (!Number.isInteger(typed.cols) || !Number.isInteger(typed.rows)) return null;
|
|
473
567
|
const cols = typed.cols;
|
|
474
568
|
const rows = typed.rows;
|
|
@@ -604,7 +698,8 @@ async function main() {
|
|
|
604
698
|
const s = active[i];
|
|
605
699
|
const age = timeSince(new Date(s.startedAt));
|
|
606
700
|
console.error(` \x1B[36m${i + 1}\x1B[0m) \x1B[1m${s.tmuxSession}\x1B[0m ${age} ago`);
|
|
607
|
-
console.error(`
|
|
701
|
+
console.error(` relay: ${s.relay}`);
|
|
702
|
+
console.error(` session: ${s.sessionId}`);
|
|
608
703
|
}
|
|
609
704
|
const action = await pickManageAction(active.length);
|
|
610
705
|
if (action === "q") {
|