@rubytech/create-maxy 1.0.680 → 1.0.682

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.
@@ -1,4 +1,5 @@
1
1
  import {
2
+ LOG_DIR,
2
3
  canAccessAdmin,
3
4
  describeRemoteSession,
4
5
  newCorrId,
@@ -9,7 +10,7 @@ import {
9
10
 
10
11
  // server/maxy-edge.ts
11
12
  import { createServer, request as httpRequest } from "http";
12
- import { createConnection as createConnection2 } from "net";
13
+ import { createConnection as createConnection3 } from "net";
13
14
  import { readFileSync, existsSync, watchFile } from "fs";
14
15
  import { homedir } from "os";
15
16
  import { join } from "path";
@@ -275,6 +276,358 @@ Content-Length: 0\r
275
276
  socket.destroy();
276
277
  }
277
278
 
279
+ // server/ws-proxy-ttyd.ts
280
+ import { createConnection as createConnection2 } from "net";
281
+
282
+ // app/lib/ttyd-logger.ts
283
+ import { appendFileSync, mkdirSync } from "fs";
284
+ import { resolve } from "path";
285
+ var EDGE_LOG_FILE = resolve(LOG_DIR, "edge-boot.log");
286
+ try {
287
+ mkdirSync(LOG_DIR, { recursive: true });
288
+ } catch (err) {
289
+ console.error(`[ttyd-log-fail] mkdir ${LOG_DIR} failed: ${err.message}`);
290
+ }
291
+ function ttydLog(phase, fields = {}) {
292
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
293
+ const kv = Object.entries(fields).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(" ");
294
+ const line = kv.length > 0 ? `[${ts}] [${phase}] ${kv}
295
+ ` : `[${ts}] [${phase}]
296
+ `;
297
+ try {
298
+ appendFileSync(EDGE_LOG_FILE, line);
299
+ } catch (err) {
300
+ console.error(`[ttyd-log-fail] ${err.message} \u2014 dropped: ${line.slice(0, 300).trim()}`);
301
+ }
302
+ }
303
+
304
+ // server/ws-proxy-ttyd.ts
305
+ var WS_PATH2 = "/ttyd";
306
+ var UPSTREAM_WS_PATH = "/ws";
307
+ var UPSTREAM_TIMEOUT_MS2 = 5e3;
308
+ var FLOW_ACTIVE_INTERVAL_MS = 5e3;
309
+ var FLOW_IDLE_INTERVAL_MS = 3e4;
310
+ var CHUNK_THROTTLE_MS = 1e3;
311
+ var CHUNK_UNTHROTTLED_COUNT = 5;
312
+ var HOP_BY_HOP2 = /* @__PURE__ */ new Set([
313
+ "connection",
314
+ "keep-alive",
315
+ "proxy-authenticate",
316
+ "proxy-authorization",
317
+ "te",
318
+ "trailer",
319
+ "transfer-encoding",
320
+ "upgrade"
321
+ ]);
322
+ function attachTtydWsProxy(server2, opts) {
323
+ const upstreamHost = opts.upstreamHost ?? "127.0.0.1";
324
+ const upstreamPort = opts.upstreamPort ?? 7681;
325
+ const now = opts.now ?? (() => Date.now());
326
+ server2.on("upgrade", (req, clientSocket, head) => {
327
+ try {
328
+ handleUpgrade2(req, clientSocket, head, {
329
+ isPublicHost: opts.isPublicHost,
330
+ upstreamHost,
331
+ upstreamPort,
332
+ now
333
+ });
334
+ } catch (err) {
335
+ ttydLog("ttyd-ws-upgrade", {
336
+ decision: "rejected",
337
+ reason: "handler-exception",
338
+ err: err.message
339
+ });
340
+ clientSocket.destroy();
341
+ }
342
+ });
343
+ }
344
+ function handleUpgrade2(req, clientSocket, head, opts) {
345
+ const url = req.url ?? "";
346
+ const qsIndex = url.indexOf("?");
347
+ const pathname = qsIndex === -1 ? url : url.slice(0, qsIndex);
348
+ if (pathname !== WS_PATH2) return;
349
+ const corrId = newCorrId();
350
+ const query = qsIndex === -1 ? "" : url.slice(qsIndex + 1);
351
+ const clientCorrId = sanitizeClientCorrId(parseQueryParam2(query, "corrId"));
352
+ const hostHeader = (req.headers.host ?? "").split(":")[0];
353
+ const originHeader = headerString2(req.headers.origin);
354
+ const remote = req.socket.remoteAddress;
355
+ const xff = headerString2(req.headers["x-forwarded-for"]);
356
+ const cookieHeader = headerString2(req.headers.cookie);
357
+ const decision = canAccessAdmin({
358
+ host: hostHeader,
359
+ remoteAddress: remote,
360
+ xForwardedFor: xff,
361
+ cookieHeader,
362
+ isPublicHost: opts.isPublicHost
363
+ });
364
+ if (!decision.allow) {
365
+ const status = decision.reason === "public-host" ? 404 : 401;
366
+ const rawToken = parseCookieValue(cookieHeader, "__remote_session");
367
+ const tokenInfo = describeRemoteSession(rawToken);
368
+ ttydLog("ttyd-ws-upgrade", {
369
+ corrId,
370
+ clientCorrId: clientCorrId ?? null,
371
+ decision: "rejected",
372
+ reason: decision.reason,
373
+ ip: remote,
374
+ xff: xff ?? null,
375
+ origin: originHeader ?? null,
376
+ host: hostHeader,
377
+ cookieHeaderPresent: cookieHeader != null && cookieHeader.length > 0,
378
+ tokenPresent: tokenInfo.present,
379
+ tokenExpired: tokenInfo.expired
380
+ });
381
+ writeStatusAndDestroy2(clientSocket, status, decision.reason === "public-host" ? "Not Found" : "Unauthorized");
382
+ return;
383
+ }
384
+ const originHost = parseOriginHost2(originHeader);
385
+ if (!originHost) {
386
+ ttydLog("ttyd-ws-upgrade", {
387
+ corrId,
388
+ clientCorrId: clientCorrId ?? null,
389
+ decision: "rejected",
390
+ reason: "origin-missing-or-invalid",
391
+ origin: originHeader ?? null,
392
+ host: hostHeader,
393
+ ip: remote
394
+ });
395
+ writeStatusAndDestroy2(clientSocket, 403, "Forbidden");
396
+ return;
397
+ }
398
+ if (originHost !== hostHeader) {
399
+ ttydLog("ttyd-ws-upgrade", {
400
+ corrId,
401
+ clientCorrId: clientCorrId ?? null,
402
+ decision: "rejected",
403
+ reason: "origin-mismatch",
404
+ origin_host: originHost,
405
+ host: hostHeader,
406
+ ip: remote
407
+ });
408
+ writeStatusAndDestroy2(clientSocket, 403, "Forbidden");
409
+ return;
410
+ }
411
+ if (opts.isPublicHost(originHost)) {
412
+ ttydLog("ttyd-ws-upgrade", {
413
+ corrId,
414
+ clientCorrId: clientCorrId ?? null,
415
+ decision: "rejected",
416
+ reason: "origin-public-host",
417
+ origin_host: originHost,
418
+ host: hostHeader,
419
+ ip: remote
420
+ });
421
+ writeStatusAndDestroy2(clientSocket, 403, "Forbidden");
422
+ return;
423
+ }
424
+ ttydLog("ttyd-ws-upgrade", {
425
+ corrId,
426
+ clientCorrId: clientCorrId ?? null,
427
+ decision: "accepted",
428
+ ip: remote,
429
+ xff: xff ?? null,
430
+ origin: originHeader ?? null,
431
+ host: hostHeader,
432
+ sec_ws_version: headerString2(req.headers["sec-websocket-version"]) ?? null,
433
+ sec_ws_protocol: headerString2(req.headers["sec-websocket-protocol"]) ?? null
434
+ });
435
+ const connectStart = opts.now();
436
+ const upstream = createConnection2({ host: opts.upstreamHost, port: opts.upstreamPort });
437
+ upstream.setTimeout(UPSTREAM_TIMEOUT_MS2);
438
+ let bytesClientToUpstream = 0;
439
+ let bytesUpstreamToClient = 0;
440
+ let lastFlowBytesClient = 0;
441
+ let lastFlowBytesUpstream = 0;
442
+ let lastActivityAt = opts.now();
443
+ let lastChunkLogAtClient = 0;
444
+ let lastChunkLogAtUpstream = 0;
445
+ let chunkCountClient = 0;
446
+ let chunkCountUpstream = 0;
447
+ let closedBy = null;
448
+ let proxyOpened = false;
449
+ const sessionStart = opts.now();
450
+ let flowHandle = null;
451
+ let currentFlowInterval = FLOW_ACTIVE_INTERVAL_MS;
452
+ const scheduleFlow = (intervalMs) => {
453
+ if (flowHandle) clearInterval(flowHandle);
454
+ currentFlowInterval = intervalMs;
455
+ flowHandle = setInterval(emitFlow, intervalMs);
456
+ };
457
+ const emitFlow = () => {
458
+ if (!proxyOpened || closedBy) return;
459
+ const deltaClient = bytesClientToUpstream - lastFlowBytesClient;
460
+ const deltaUpstream = bytesUpstreamToClient - lastFlowBytesUpstream;
461
+ lastFlowBytesClient = bytesClientToUpstream;
462
+ lastFlowBytesUpstream = bytesUpstreamToClient;
463
+ const idleMs = opts.now() - lastActivityAt;
464
+ ttydLog("ttyd-proxy-flow", {
465
+ corrId,
466
+ clientBytes: bytesClientToUpstream,
467
+ upstreamBytes: bytesUpstreamToClient,
468
+ clientBytesDelta: deltaClient,
469
+ upstreamBytesDelta: deltaUpstream,
470
+ idleMs
471
+ });
472
+ const active = deltaClient > 0 || deltaUpstream > 0;
473
+ const desiredInterval = active ? FLOW_ACTIVE_INTERVAL_MS : FLOW_IDLE_INTERVAL_MS;
474
+ if (desiredInterval !== currentFlowInterval) scheduleFlow(desiredInterval);
475
+ };
476
+ const finish = (side, reason) => {
477
+ if (closedBy) return;
478
+ closedBy = side;
479
+ if (flowHandle) {
480
+ clearInterval(flowHandle);
481
+ flowHandle = null;
482
+ }
483
+ if (proxyOpened) {
484
+ ttydLog("ttyd-proxy-close", {
485
+ corrId,
486
+ closedBy: side,
487
+ reason,
488
+ clientBytes: bytesClientToUpstream,
489
+ upstreamBytes: bytesUpstreamToClient,
490
+ durationMs: opts.now() - sessionStart
491
+ });
492
+ }
493
+ clientSocket.destroy();
494
+ upstream.destroy();
495
+ };
496
+ upstream.once("connect", () => {
497
+ upstream.setTimeout(0);
498
+ proxyOpened = true;
499
+ ttydLog("ttyd-proxy-open", {
500
+ corrId,
501
+ upstream: `${opts.upstreamHost}:${opts.upstreamPort}`,
502
+ connect_ms: opts.now() - connectStart
503
+ });
504
+ const lines = [];
505
+ lines.push(`${req.method ?? "GET"} ${UPSTREAM_WS_PATH} HTTP/${req.httpVersion}`);
506
+ lines.push(`host: ${opts.upstreamHost}:${opts.upstreamPort}`);
507
+ for (const [name, value] of Object.entries(req.headers)) {
508
+ if (name === "host") continue;
509
+ if (HOP_BY_HOP2.has(name)) continue;
510
+ if (value == null) continue;
511
+ if (Array.isArray(value)) {
512
+ for (const v of value) lines.push(`${name}: ${v}`);
513
+ } else {
514
+ lines.push(`${name}: ${value}`);
515
+ }
516
+ }
517
+ const upgradeHeader = headerString2(req.headers.upgrade);
518
+ const connectionHeader = headerString2(req.headers.connection);
519
+ if (upgradeHeader) lines.push(`upgrade: ${upgradeHeader}`);
520
+ if (connectionHeader) lines.push(`connection: ${connectionHeader}`);
521
+ upstream.write(lines.join("\r\n") + "\r\n\r\n");
522
+ if (head && head.length > 0) upstream.write(head);
523
+ const logChunk = (dir, bytes) => {
524
+ const now = opts.now();
525
+ if (dir === "client\u2192upstream") {
526
+ chunkCountClient += 1;
527
+ const sinceLastMs = lastChunkLogAtClient === 0 ? 0 : now - lastChunkLogAtClient;
528
+ if (chunkCountClient <= CHUNK_UNTHROTTLED_COUNT || sinceLastMs >= CHUNK_THROTTLE_MS) {
529
+ ttydLog("ttyd-proxy-chunk", { corrId, dir, bytes, sinceLastMs });
530
+ lastChunkLogAtClient = now;
531
+ }
532
+ } else {
533
+ chunkCountUpstream += 1;
534
+ const sinceLastMs = lastChunkLogAtUpstream === 0 ? 0 : now - lastChunkLogAtUpstream;
535
+ if (chunkCountUpstream <= CHUNK_UNTHROTTLED_COUNT || sinceLastMs >= CHUNK_THROTTLE_MS) {
536
+ ttydLog("ttyd-proxy-chunk", { corrId, dir, bytes, sinceLastMs });
537
+ lastChunkLogAtUpstream = now;
538
+ }
539
+ }
540
+ lastActivityAt = now;
541
+ };
542
+ clientSocket.on("data", (chunk) => {
543
+ bytesClientToUpstream += chunk.length;
544
+ logChunk("client\u2192upstream", chunk.length);
545
+ });
546
+ upstream.on("data", (chunk) => {
547
+ bytesUpstreamToClient += chunk.length;
548
+ logChunk("upstream\u2192client", chunk.length);
549
+ });
550
+ clientSocket.pipe(upstream);
551
+ upstream.pipe(clientSocket);
552
+ scheduleFlow(FLOW_ACTIVE_INTERVAL_MS);
553
+ clientSocket.once("close", (hadError) => {
554
+ finish("client", hadError ? "error" : "normal");
555
+ });
556
+ upstream.once("close", (hadError) => {
557
+ finish("upstream", hadError ? "error" : "normal");
558
+ });
559
+ clientSocket.once("error", (err) => {
560
+ ttydLog("ttyd-proxy-error", { corrId, side: "client", err: err.message });
561
+ finish("client", "error");
562
+ });
563
+ upstream.once("error", (err) => {
564
+ ttydLog("ttyd-proxy-error", { corrId, side: "upstream", err: err.message });
565
+ finish("upstream", "error");
566
+ });
567
+ });
568
+ upstream.once("timeout", () => {
569
+ if (proxyOpened) return;
570
+ ttydLog("ttyd-proxy-error", {
571
+ corrId,
572
+ side: "upstream-connect",
573
+ err: "timeout",
574
+ timeout_ms: UPSTREAM_TIMEOUT_MS2
575
+ });
576
+ writeStatusAndDestroy2(clientSocket, 504, "Gateway Timeout");
577
+ upstream.destroy();
578
+ });
579
+ upstream.once("error", (err) => {
580
+ if (proxyOpened) return;
581
+ ttydLog("ttyd-proxy-error", {
582
+ corrId,
583
+ side: "upstream-connect",
584
+ err: err.message
585
+ });
586
+ writeStatusAndDestroy2(clientSocket, 502, "Bad Gateway");
587
+ upstream.destroy();
588
+ });
589
+ }
590
+ function parseQueryParam2(query, key) {
591
+ if (!query) return null;
592
+ for (const pair of query.split("&")) {
593
+ const eq = pair.indexOf("=");
594
+ const k = eq === -1 ? pair : pair.slice(0, eq);
595
+ if (k !== key) continue;
596
+ const v = eq === -1 ? "" : pair.slice(eq + 1);
597
+ try {
598
+ return decodeURIComponent(v);
599
+ } catch {
600
+ return null;
601
+ }
602
+ }
603
+ return null;
604
+ }
605
+ function headerString2(value) {
606
+ if (value == null) return void 0;
607
+ return Array.isArray(value) ? value[0] : value;
608
+ }
609
+ function parseOriginHost2(origin) {
610
+ if (!origin) return null;
611
+ try {
612
+ return new URL(origin).hostname;
613
+ } catch {
614
+ return null;
615
+ }
616
+ }
617
+ function writeStatusAndDestroy2(socket, status, statusText) {
618
+ try {
619
+ socket.write(
620
+ `HTTP/1.1 ${status} ${statusText}\r
621
+ Connection: close\r
622
+ Content-Length: 0\r
623
+ \r
624
+ `
625
+ );
626
+ } catch {
627
+ }
628
+ socket.destroy();
629
+ }
630
+
278
631
  // server/maxy-edge.ts
279
632
  var PLATFORM_ROOT = process.env.MAXY_PLATFORM_ROOT || "";
280
633
  var BRAND_JSON_PATH = PLATFORM_ROOT ? join(PLATFORM_ROOT, "config", "brand.json") : "";
@@ -312,7 +665,9 @@ var UPSTREAM_HOST = process.env.MAXY_UI_HOST ?? "127.0.0.1";
312
665
  var UPSTREAM_PORT = parseInt(process.env.MAXY_UI_PORT ?? "19199", 10);
313
666
  var WEBSOCKIFY_HOST = process.env.WEBSOCKIFY_HOST ?? "127.0.0.1";
314
667
  var WEBSOCKIFY_PORT = parseInt(process.env.WEBSOCKIFY_PORT ?? "6080", 10);
315
- var HOP_BY_HOP2 = /* @__PURE__ */ new Set([
668
+ var TTYD_HOST = process.env.TTYD_HOST ?? "127.0.0.1";
669
+ var TTYD_PORT = parseInt(process.env.TTYD_PORT ?? "7681", 10);
670
+ var HOP_BY_HOP3 = /* @__PURE__ */ new Set([
316
671
  "connection",
317
672
  "keep-alive",
318
673
  "proxy-authenticate",
@@ -326,7 +681,7 @@ function forwardHttp(clientReq, clientRes) {
326
681
  const headers = {};
327
682
  for (const [name, value] of Object.entries(clientReq.headers)) {
328
683
  if (value == null) continue;
329
- if (HOP_BY_HOP2.has(name)) continue;
684
+ if (HOP_BY_HOP3.has(name)) continue;
330
685
  headers[name] = value;
331
686
  }
332
687
  const existingXff = headers["x-forwarded-for"];
@@ -356,7 +711,7 @@ function forwardHttp(clientReq, clientRes) {
356
711
  clientReq.pipe(upstream);
357
712
  }
358
713
  function forwardUpgrade(req, clientSocket, head) {
359
- const upstream = createConnection2({ host: UPSTREAM_HOST, port: UPSTREAM_PORT });
714
+ const upstream = createConnection3({ host: UPSTREAM_HOST, port: UPSTREAM_PORT });
360
715
  upstream.setTimeout(5e3);
361
716
  upstream.once("connect", () => {
362
717
  upstream.setTimeout(0);
@@ -365,7 +720,7 @@ function forwardUpgrade(req, clientSocket, head) {
365
720
  lines.push(`host: ${UPSTREAM_HOST}:${UPSTREAM_PORT}`);
366
721
  for (const [name, value] of Object.entries(req.headers)) {
367
722
  if (name === "host") continue;
368
- if (HOP_BY_HOP2.has(name)) continue;
723
+ if (HOP_BY_HOP3.has(name)) continue;
369
724
  if (value == null) continue;
370
725
  if (Array.isArray(value)) {
371
726
  for (const v of value) lines.push(`${name}: ${v}`);
@@ -416,15 +771,22 @@ attachVncWsProxy(server, {
416
771
  upstreamHost: WEBSOCKIFY_HOST,
417
772
  upstreamPort: WEBSOCKIFY_PORT
418
773
  });
774
+ attachTtydWsProxy(server, {
775
+ isPublicHost,
776
+ upstreamHost: TTYD_HOST,
777
+ upstreamPort: TTYD_PORT
778
+ });
419
779
  server.on("upgrade", (req, socket, head) => {
420
780
  const url = req.url ?? "";
421
781
  const qsIndex = url.indexOf("?");
422
782
  const pathname = qsIndex === -1 ? url : url.slice(0, qsIndex);
423
783
  if (pathname === "/websockify") return;
784
+ if (pathname === "/ttyd") return;
424
785
  forwardUpgrade(req, socket, head);
425
786
  });
426
787
  server.listen(EDGE_PORT, EDGE_HOSTNAME, () => {
427
788
  console.log(`[edge] listening on http://${EDGE_HOSTNAME}:${EDGE_PORT}`);
428
789
  console.log(`[edge] /websockify \u2192 ${WEBSOCKIFY_HOST}:${WEBSOCKIFY_PORT}`);
790
+ console.log(`[edge] /ttyd \u2192 ${TTYD_HOST}:${TTYD_PORT}`);
429
791
  console.log(`[edge] everything else \u2192 ${UPSTREAM_HOST}:${UPSTREAM_PORT}`);
430
792
  });