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 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 7000 --session dev --kill-on-exit --no-qr --tunnel cloudflare
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
- return parsed;
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 null;
2378
+ return { ok: false, error: "invalid" };
2354
2379
  } catch {
2355
- return null;
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 message = parseClientMessage(payload);
2474
- if (!message) {
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;