termbridge 0.1.0 → 0.2.0
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 +5 -1
- package/dist/bin.js +60 -9
- package/dist/bin.js.map +1 -1
- package/package.json +2 -2
- package/ui/dist/assets/index-C9vi5hfL.css +1 -0
- package/ui/dist/assets/{index-BH9gzDdw.js → index-Cix4a2YI.js} +19 -19
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-B5s06ZPI.css +0 -1
package/README.md
CHANGED
|
@@ -17,10 +17,14 @@ npx termbridge
|
|
|
17
17
|
|
|
18
18
|
Scan the QR code and open the URL on your phone. The CLI stays running while the tunnel is active.
|
|
19
19
|
|
|
20
|
+
## Documentation
|
|
21
|
+
|
|
22
|
+
Visit `https://termbridge.dev` for the full docs, architecture notes, and troubleshooting.
|
|
23
|
+
|
|
20
24
|
## CLI usage
|
|
21
25
|
|
|
22
26
|
```bash
|
|
23
|
-
termbridge --port
|
|
27
|
+
termbridge --port 8080 --session dev --kill-on-exit --no-qr --tunnel cloudflare
|
|
24
28
|
```
|
|
25
29
|
|
|
26
30
|
Flags:
|
package/dist/bin.js
CHANGED
|
@@ -2089,6 +2089,7 @@ import { randomBytes, createHash } from "crypto";
|
|
|
2089
2089
|
var SESSION_COOKIE_NAME = "termbridge_session";
|
|
2090
2090
|
var hashToken = (token) => createHash("sha256").update(token).digest("hex");
|
|
2091
2091
|
var createSessionId = () => randomBytes(18).toString("base64url");
|
|
2092
|
+
var createCsrfToken = () => randomBytes(24).toString("base64url");
|
|
2092
2093
|
var parseCookies = (cookieHeader) => {
|
|
2093
2094
|
const cookies = {};
|
|
2094
2095
|
if (!cookieHeader) {
|
|
@@ -2136,6 +2137,7 @@ var createAuth = ({
|
|
|
2136
2137
|
record.consumed = true;
|
|
2137
2138
|
const session = {
|
|
2138
2139
|
id: createSessionId(),
|
|
2140
|
+
csrfToken: createCsrfToken(),
|
|
2139
2141
|
createdAt: clock(),
|
|
2140
2142
|
lastSeen: clock()
|
|
2141
2143
|
};
|
|
@@ -2179,12 +2181,20 @@ var createAuth = ({
|
|
|
2179
2181
|
}
|
|
2180
2182
|
return parts.join("; ");
|
|
2181
2183
|
};
|
|
2184
|
+
const verifyCsrfToken = (sessionId, csrfToken) => {
|
|
2185
|
+
const session = sessions.get(sessionId);
|
|
2186
|
+
if (!session) {
|
|
2187
|
+
return false;
|
|
2188
|
+
}
|
|
2189
|
+
return session.csrfToken === csrfToken;
|
|
2190
|
+
};
|
|
2182
2191
|
return {
|
|
2183
2192
|
issueToken,
|
|
2184
2193
|
redeemToken,
|
|
2185
2194
|
getSession,
|
|
2186
2195
|
getSessionFromRequest,
|
|
2187
|
-
createSessionCookie
|
|
2196
|
+
createSessionCookie,
|
|
2197
|
+
verifyCsrfToken
|
|
2188
2198
|
};
|
|
2189
2199
|
};
|
|
2190
2200
|
|
|
@@ -2303,6 +2313,9 @@ var createStaticHandler = (uiDistPath, basePath) => {
|
|
|
2303
2313
|
};
|
|
2304
2314
|
|
|
2305
2315
|
// src/server/server.ts
|
|
2316
|
+
var MAX_HTTP_BODY_SIZE = 64 * 1024;
|
|
2317
|
+
var MAX_WS_MESSAGE_SIZE = 1024 * 1024;
|
|
2318
|
+
var MAX_INPUT_LENGTH = 64 * 1024;
|
|
2306
2319
|
var jsonResponse = (response, status, payload) => {
|
|
2307
2320
|
const body = JSON.stringify(payload);
|
|
2308
2321
|
response.statusCode = status;
|
|
@@ -2312,7 +2325,12 @@ var jsonResponse = (response, status, payload) => {
|
|
|
2312
2325
|
};
|
|
2313
2326
|
var readJsonBody = async (request) => {
|
|
2314
2327
|
const chunks = [];
|
|
2328
|
+
let totalSize = 0;
|
|
2315
2329
|
for await (const chunk of request) {
|
|
2330
|
+
totalSize += chunk.length;
|
|
2331
|
+
if (totalSize > MAX_HTTP_BODY_SIZE) {
|
|
2332
|
+
return "too_large";
|
|
2333
|
+
}
|
|
2316
2334
|
chunks.push(chunk);
|
|
2317
2335
|
}
|
|
2318
2336
|
const body = Buffer.concat(chunks).toString("utf8").trim();
|
|
@@ -2338,21 +2356,28 @@ var isAllowedOrigin = (origin, host) => {
|
|
|
2338
2356
|
};
|
|
2339
2357
|
var allowedControlKeys = new Set(TERMINAL_CONTROL_KEYS);
|
|
2340
2358
|
var parseClientMessage = (payload) => {
|
|
2359
|
+
const size = typeof payload === "string" ? payload.length : payload.byteLength;
|
|
2360
|
+
if (size > MAX_WS_MESSAGE_SIZE) {
|
|
2361
|
+
return { ok: false, error: "too_large" };
|
|
2362
|
+
}
|
|
2341
2363
|
const text = typeof payload === "string" ? payload : payload.toString();
|
|
2342
2364
|
try {
|
|
2343
2365
|
const parsed = JSON.parse(text);
|
|
2344
2366
|
if (parsed.type === "input" && typeof parsed.data === "string") {
|
|
2345
|
-
|
|
2367
|
+
if (parsed.data.length > MAX_INPUT_LENGTH) {
|
|
2368
|
+
return { ok: false, error: "too_large" };
|
|
2369
|
+
}
|
|
2370
|
+
return { ok: true, message: parsed };
|
|
2346
2371
|
}
|
|
2347
2372
|
if (parsed.type === "resize" && typeof parsed.cols === "number" && typeof parsed.rows === "number") {
|
|
2348
|
-
return parsed;
|
|
2373
|
+
return { ok: true, message: parsed };
|
|
2349
2374
|
}
|
|
2350
2375
|
if (parsed.type === "control" && allowedControlKeys.has(parsed.key)) {
|
|
2351
|
-
return parsed;
|
|
2376
|
+
return { ok: true, message: parsed };
|
|
2352
2377
|
}
|
|
2353
|
-
return
|
|
2378
|
+
return { ok: false, error: "invalid" };
|
|
2354
2379
|
} catch {
|
|
2355
|
-
return
|
|
2380
|
+
return { ok: false, error: "invalid" };
|
|
2356
2381
|
}
|
|
2357
2382
|
};
|
|
2358
2383
|
var sendWsMessage = (socket, message) => {
|
|
@@ -2396,6 +2421,21 @@ var createAppServer = (deps) => {
|
|
|
2396
2421
|
response.end();
|
|
2397
2422
|
return;
|
|
2398
2423
|
}
|
|
2424
|
+
if (url.pathname === "/api/csrf") {
|
|
2425
|
+
const session = deps.auth.getSessionFromRequest(request);
|
|
2426
|
+
if (!session) {
|
|
2427
|
+
response.statusCode = 401;
|
|
2428
|
+
response.end("unauthorized");
|
|
2429
|
+
return;
|
|
2430
|
+
}
|
|
2431
|
+
if (request.method === "GET") {
|
|
2432
|
+
jsonResponse(response, 200, { csrfToken: session.csrfToken });
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2435
|
+
response.statusCode = 404;
|
|
2436
|
+
response.end("not found");
|
|
2437
|
+
return;
|
|
2438
|
+
}
|
|
2399
2439
|
if (url.pathname === "/api/terminals") {
|
|
2400
2440
|
const session = deps.auth.getSessionFromRequest(request);
|
|
2401
2441
|
if (!session) {
|
|
@@ -2410,6 +2450,11 @@ var createAppServer = (deps) => {
|
|
|
2410
2450
|
}
|
|
2411
2451
|
if (request.method === "POST") {
|
|
2412
2452
|
const body = await readJsonBody(request);
|
|
2453
|
+
if (body === "too_large") {
|
|
2454
|
+
response.statusCode = 413;
|
|
2455
|
+
response.end("request body too large");
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2413
2458
|
if (body && typeof body !== "object") {
|
|
2414
2459
|
response.statusCode = 400;
|
|
2415
2460
|
response.end("invalid body");
|
|
@@ -2453,6 +2498,11 @@ var createAppServer = (deps) => {
|
|
|
2453
2498
|
socket.destroy();
|
|
2454
2499
|
return;
|
|
2455
2500
|
}
|
|
2501
|
+
const csrfToken = url.searchParams.get("csrf");
|
|
2502
|
+
if (!csrfToken || !deps.auth.verifyCsrfToken(session.id, csrfToken)) {
|
|
2503
|
+
socket.destroy();
|
|
2504
|
+
return;
|
|
2505
|
+
}
|
|
2456
2506
|
const record = deps.terminalRegistry.get(terminalId);
|
|
2457
2507
|
if (!record) {
|
|
2458
2508
|
socket.destroy();
|
|
@@ -2470,15 +2520,16 @@ var createAppServer = (deps) => {
|
|
|
2470
2520
|
sendWsMessage(socket, { type: "output", data });
|
|
2471
2521
|
});
|
|
2472
2522
|
socket.on("message", (payload) => {
|
|
2473
|
-
const
|
|
2474
|
-
if (!
|
|
2523
|
+
const result = parseClientMessage(payload);
|
|
2524
|
+
if (!result.ok) {
|
|
2475
2525
|
sendWsMessage(socket, {
|
|
2476
2526
|
type: "status",
|
|
2477
2527
|
state: "error",
|
|
2478
|
-
message: "invalid payload"
|
|
2528
|
+
message: result.error === "too_large" ? "message too large" : "invalid payload"
|
|
2479
2529
|
});
|
|
2480
2530
|
return;
|
|
2481
2531
|
}
|
|
2532
|
+
const message = result.message;
|
|
2482
2533
|
if (message.type === "input") {
|
|
2483
2534
|
void deps.terminalBackend.write(info.sessionName, message.data);
|
|
2484
2535
|
return;
|