@dutodev/cli 0.5.0 → 0.6.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 -0
- package/dist/{fullscreen-DKIFHB6J.js → fullscreen-7P5GBCQX.js} +21 -8
- package/dist/index.js +114 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -145,9 +145,14 @@ Com os túneis no ar, o terminal mostra cada túnel (URL, destino local, requisi
|
|
|
145
145
|
| `m` | Mostra o QR code do túnel selecionado, para abrir no celular |
|
|
146
146
|
| `f` | Filtra por túnel, método, caminho ou status |
|
|
147
147
|
| `↵` | Detalhes da requisição: cabeçalhos e o começo dos corpos |
|
|
148
|
+
| `r` | No detalhe, reenvia a requisição ao serviço local |
|
|
148
149
|
| `tab`, `↑↓` | Alterna entre túneis e requisições e move a seleção |
|
|
149
150
|
| `q` ou Ctrl+C | Fecha os túneis e sai |
|
|
150
151
|
|
|
152
|
+
**Reenviar uma requisição.** No detalhe (`↵`), a tecla `r` manda de novo ao serviço local o mesmo método, caminho, cabeçalhos e corpo que o app recebeu, já com as regras `host:` e `headers:` do túnel aplicadas e sem as credenciais do próprio Duto. O reenvio entra na lista marcado com `↻`, a tela pula para o detalhe dele (`↻ reenvio de #N`) e `esc` volta. Serve para depurar webhooks sem esperar o serviço externo mandar de novo; em especial, uma requisição que ficou com `503` porque o serviço local estava fora do ar pode ser reenviada assim que ele subir. Como os bytes são idênticos, assinaturas de webhook continuam válidas. Já apps que recusam eventos antigos (o Stripe tolera 5 minutos por padrão) vão rejeitar o reenvio depois dessa janela.
|
|
153
|
+
|
|
154
|
+
Não dá para reenviar WebSocket, requisições barradas pela senha do túnel, corpos binários, corpos acima de 64 KB, requisições cujo corpo não terminou de chegar ou quando o túnel já não está aberto; nesses casos uma linha explica o motivo. O reenvio não existe no log simples (sem terminal interativo).
|
|
155
|
+
|
|
151
156
|
Se o túnel está no ar mas nada responde na porta local, quem abrir o endereço no navegador vê uma página do Duto que volta sozinha quando o serviço sobe. Ela espera por uma conexão só (SSE): a CLI checa a porta na sua máquina e avisa a página assim que o serviço responde, sem recarregar de tempos em tempos. Clientes que não são navegador (`curl`, APIs) recebem a mesma resposta em texto. Em ambos os casos o status é `503` com `Retry-After: 3`. Os caminhos `/__duto/…` são reservados para isso e nunca chegam ao seu app.
|
|
152
157
|
|
|
153
158
|
Fora de um terminal interativo (pipe, CI) ou com `--no-ui`, a CLI imprime um log simples. No Windows a tela cheia vem desligada; use `--ui` para tentar.
|
|
@@ -84,12 +84,16 @@ function Detail({ record }) {
|
|
|
84
84
|
record.tunnel
|
|
85
85
|
] })
|
|
86
86
|
] }),
|
|
87
|
+
record.replayOf !== void 0 && /* @__PURE__ */ jsxs(Text, { color: C.muted, children: [
|
|
88
|
+
"\u21BB reenvio de #",
|
|
89
|
+
record.replayOf
|
|
90
|
+
] }),
|
|
87
91
|
record.error && /* @__PURE__ */ jsx(Text, { color: C.err, children: record.error }),
|
|
88
|
-
/* @__PURE__ */ jsx(Section, { title: "Requisi\xE7\xE3o", headers: record.reqHeaders, body: record.reqBody, truncated: record.
|
|
89
|
-
/* @__PURE__ */ jsx(Section, { title: "Resposta", headers: record.resHeaders, body: record.resBody, truncated: record.
|
|
92
|
+
/* @__PURE__ */ jsx(Section, { title: "Requisi\xE7\xE3o", headers: record.reqHeaders, body: record.reqBody, truncated: record.reqTruncated }),
|
|
93
|
+
/* @__PURE__ */ jsx(Section, { title: "Resposta", headers: record.resHeaders, body: record.resBody, truncated: record.resTruncated }),
|
|
90
94
|
/* @__PURE__ */ jsxs(Text, { color: C.muted, children: [
|
|
91
95
|
"\n",
|
|
92
|
-
"esc volta para a lista"
|
|
96
|
+
"r reenvia \xB7 esc volta para a lista"
|
|
93
97
|
] })
|
|
94
98
|
] });
|
|
95
99
|
}
|
|
@@ -189,7 +193,7 @@ function RequestList(props) {
|
|
|
189
193
|
const start = Math.max(0, Math.min(selected - Math.floor(height / 2), records.length - height));
|
|
190
194
|
const visible = records.slice(start, start + height);
|
|
191
195
|
const accessW = Math.max(0, ...visible.map((r) => (accessLabel(r.access)?.text.length ?? -1) + 1));
|
|
192
|
-
const pathW = Math.max(10, width -
|
|
196
|
+
const pathW = Math.max(10, width - 54 - accessW);
|
|
193
197
|
const title = props.filter ? `REQUISI\xC7\xD5ES \xB7 filtro: ${props.filter} ` : "REQUISI\xC7\xD5ES ";
|
|
194
198
|
return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", children: [
|
|
195
199
|
/* @__PURE__ */ jsx4(Text4, { color: C.muted, children: title + "\u2500".repeat(Math.max(0, width - title.length)) }),
|
|
@@ -204,6 +208,7 @@ function RequestList(props) {
|
|
|
204
208
|
" ",
|
|
205
209
|
/* @__PURE__ */ jsx4(Text4, { color: tunnelColor(props.tunnelIndex(r.tunnel)), children: fit(r.tunnel, 9) }),
|
|
206
210
|
" ",
|
|
211
|
+
/* @__PURE__ */ jsx4(Text4, { color: C.muted, children: r.replayOf !== void 0 ? "\u21BB " : " " }),
|
|
207
212
|
/* @__PURE__ */ jsx4(Text4, { bold: true, children: fit(method, 5) }),
|
|
208
213
|
fit(r.path, pathW),
|
|
209
214
|
" ",
|
|
@@ -316,6 +321,9 @@ function App({ session, version, system, onExit }) {
|
|
|
316
321
|
useEffect(() => {
|
|
317
322
|
if (selReq >= records.length) setSelReq(Math.max(0, records.length - 1));
|
|
318
323
|
}, [records.length, selReq]);
|
|
324
|
+
useEffect(() => {
|
|
325
|
+
if (mode.kind === "detail" && !all.some((r) => r.id === mode.id)) setMode({ kind: "list" });
|
|
326
|
+
}, [mode, all]);
|
|
319
327
|
useEffect(() => {
|
|
320
328
|
if (!message) return;
|
|
321
329
|
const t = setTimeout(() => setMessage(null), 4e3);
|
|
@@ -396,7 +404,12 @@ function App({ session, version, system, onExit }) {
|
|
|
396
404
|
return;
|
|
397
405
|
}
|
|
398
406
|
if (mode.kind === "detail") {
|
|
399
|
-
if (key.escape || key.return || input === "q") setMode({ kind: "list" });
|
|
407
|
+
if (key.escape || key.return || input === "q") return setMode({ kind: "list" });
|
|
408
|
+
if (input === "r") {
|
|
409
|
+
const res = session.replay(mode.id);
|
|
410
|
+
if (res.ok) setMode({ kind: "detail", id: res.id });
|
|
411
|
+
else setMessage({ text: res.reason, tone: "warn" });
|
|
412
|
+
}
|
|
400
413
|
return;
|
|
401
414
|
}
|
|
402
415
|
if (mode.kind === "qr") {
|
|
@@ -419,7 +432,7 @@ function App({ session, version, system, onExit }) {
|
|
|
419
432
|
else setSelReq((i) => Math.min(Math.max(0, i + d), Math.max(0, records.length - 1)));
|
|
420
433
|
return;
|
|
421
434
|
}
|
|
422
|
-
if (key.return && focus === "requests" && records[selReq]) return setMode({ kind: "detail" });
|
|
435
|
+
if (key.return && focus === "requests" && records[selReq]) return setMode({ kind: "detail", id: records[selReq].id });
|
|
423
436
|
if (key.escape && filter) return setFilter("");
|
|
424
437
|
if (input === "c") return void act("copy");
|
|
425
438
|
if (input === "o") return void act("open");
|
|
@@ -430,7 +443,7 @@ function App({ session, version, system, onExit }) {
|
|
|
430
443
|
const live = state.tunnels.filter((t) => t.status === "live").length;
|
|
431
444
|
const conn = state.connection === "online" ? { text: "conectado \u25CF", color: C.signal } : state.connection === "connecting" ? { text: "conectando\u2026", color: C.muted } : { text: "reconectando\u2026", color: C.warn };
|
|
432
445
|
const listHeight = Math.max(3, rows - state.tunnels.length - 9);
|
|
433
|
-
const
|
|
446
|
+
const detailRecord = mode.kind === "detail" ? all.find((r) => r.id === mode.id) : void 0;
|
|
434
447
|
const qrTunnel = mode.kind === "qr" ? state.tunnels.find((t) => t.key === mode.key) : void 0;
|
|
435
448
|
return /* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", width, children: [
|
|
436
449
|
/* @__PURE__ */ jsxs6(Box5, { justifyContent: "space-between", children: [
|
|
@@ -457,7 +470,7 @@ function App({ session, version, system, onExit }) {
|
|
|
457
470
|
/* @__PURE__ */ jsx6(Text6, { children: " " }),
|
|
458
471
|
/* @__PURE__ */ jsx6(TunnelTable, { tunnels: state.tunnels, selected: selTun, focused: focus === "tunnels" }),
|
|
459
472
|
/* @__PURE__ */ jsx6(Text6, { children: " " }),
|
|
460
|
-
mode.kind === "detail" &&
|
|
473
|
+
mode.kind === "detail" && detailRecord ? /* @__PURE__ */ jsx6(Detail, { record: detailRecord }) : mode.kind === "qr" && qrTunnel ? /* @__PURE__ */ jsx6(QrView, { tunnel: qrTunnel, link: mode.link, now }) : /* @__PURE__ */ jsx6(RequestList, { records, tunnelIndex, selected: selReq, focused: focus === "requests", height: listHeight, width, filter }),
|
|
461
474
|
/* @__PURE__ */ jsx6(Text6, { children: " " }),
|
|
462
475
|
mode.kind === "filter" && /* @__PURE__ */ jsx6(Prompt, { label: "Filtrar (t\xFAnel, m\xE9todo, caminho ou status):", value: mode.value, hint: "\u21B5 aplica \xB7 esc cancela" }),
|
|
463
476
|
mode.kind === "port" && /* @__PURE__ */ jsx6(Prompt, { label: "Novo t\xFAnel \u2014 porta local:", value: mode.value, hint: "\u21B5 continua \xB7 esc cancela" }),
|
package/dist/index.js
CHANGED
|
@@ -787,7 +787,29 @@ ${o.error ? `<p class="error" role="alert">Usu\xE1rio ou senha incorretos.</p>
|
|
|
787
787
|
);
|
|
788
788
|
}
|
|
789
789
|
|
|
790
|
+
// src/session/replay.ts
|
|
791
|
+
function replayBlock(r) {
|
|
792
|
+
if (!r) return "Essa requisi\xE7\xE3o j\xE1 saiu da lista.";
|
|
793
|
+
if (r.websocket) return "WebSocket n\xE3o d\xE1 para reenviar.";
|
|
794
|
+
if (r.noReplay) return r.noReplay;
|
|
795
|
+
if (r.access?.kind === "denied") return "Barrada pela senha do t\xFAnel; n\xE3o d\xE1 para reenviar.";
|
|
796
|
+
if (!r.reqComplete) return "A requisi\xE7\xE3o ainda n\xE3o chegou inteira.";
|
|
797
|
+
if (r.reqTruncated) return "Corpo acima de 64 KB; n\xE3o d\xE1 para reenviar.";
|
|
798
|
+
if (r.reqBodyBytes > 0 && r.reqRaw === null) return "Corpo bin\xE1rio n\xE3o \xE9 guardado; n\xE3o d\xE1 para reenviar.";
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
var DROP = /* @__PURE__ */ new Set(["connection", "keep-alive", "transfer-encoding", "expect", "content-length", "upgrade", "te", "trailer", "proxy-connection"]);
|
|
802
|
+
function replayHeaders(r) {
|
|
803
|
+
const out = {};
|
|
804
|
+
for (const [k, v] of Object.entries(r.reqHeaders)) if (!DROP.has(k.toLowerCase())) out[k] = v;
|
|
805
|
+
const hadLength = Object.keys(r.reqHeaders).some((k) => k.toLowerCase() === "content-length");
|
|
806
|
+
if (r.reqBodyBytes > 0 || hadLength) out["content-length"] = String(r.reqRaw?.length ?? 0);
|
|
807
|
+
return out;
|
|
808
|
+
}
|
|
809
|
+
|
|
790
810
|
// src/session/proxy.ts
|
|
811
|
+
var DENIED_NO_REPLAY = "Barrada pela senha do t\xFAnel; n\xE3o d\xE1 para reenviar.";
|
|
812
|
+
var PREFLIGHT_NO_REPLAY = "Preflight de CORS n\xE3o d\xE1 para reenviar.";
|
|
791
813
|
var TEXT = /^(text\/|application\/(json|xml|javascript|x-www-form-urlencoded|graphql)|[^;]*\+(json|xml))/i;
|
|
792
814
|
var MAX_BODY = 64 * 1024;
|
|
793
815
|
var LOCAL_AGENT = new Agent({ keepAlive: false });
|
|
@@ -805,8 +827,11 @@ var Capture = class {
|
|
|
805
827
|
max;
|
|
806
828
|
chunks = [];
|
|
807
829
|
size = 0;
|
|
830
|
+
/** Bytes vistos, capturados ou não. */
|
|
831
|
+
total = 0;
|
|
808
832
|
truncated = false;
|
|
809
833
|
push(c) {
|
|
834
|
+
this.total += c.length;
|
|
810
835
|
if (!this.enabled) return;
|
|
811
836
|
const room = this.max - this.size;
|
|
812
837
|
if (room <= 0) {
|
|
@@ -818,6 +843,9 @@ var Capture = class {
|
|
|
818
843
|
this.chunks.push(part);
|
|
819
844
|
this.size += part.length;
|
|
820
845
|
}
|
|
846
|
+
raw() {
|
|
847
|
+
return this.enabled && this.size > 0 ? Buffer.concat(this.chunks) : null;
|
|
848
|
+
}
|
|
821
849
|
text() {
|
|
822
850
|
return this.enabled && this.size > 0 ? Buffer.concat(this.chunks).toString("utf8") : null;
|
|
823
851
|
}
|
|
@@ -986,7 +1014,7 @@ async function startProxy(o) {
|
|
|
986
1014
|
await new Promise((r) => setTimeout(r, 50));
|
|
987
1015
|
}
|
|
988
1016
|
}
|
|
989
|
-
function record(req, route, headers, websocket, access) {
|
|
1017
|
+
function record(req, route, headers, websocket, access, noReplay) {
|
|
990
1018
|
const id = nextId++;
|
|
991
1019
|
const started = Date.now();
|
|
992
1020
|
o.store.add({
|
|
@@ -1003,11 +1031,77 @@ async function startProxy(o) {
|
|
|
1003
1031
|
resHeaders: {},
|
|
1004
1032
|
reqBody: null,
|
|
1005
1033
|
resBody: null,
|
|
1034
|
+
reqRaw: null,
|
|
1035
|
+
reqBodyBytes: 0,
|
|
1036
|
+
reqTruncated: false,
|
|
1037
|
+
reqComplete: false,
|
|
1006
1038
|
truncated: false,
|
|
1007
|
-
|
|
1039
|
+
resTruncated: false,
|
|
1040
|
+
...access && { access },
|
|
1041
|
+
...noReplay && { noReplay }
|
|
1008
1042
|
});
|
|
1009
1043
|
return { id, started };
|
|
1010
1044
|
}
|
|
1045
|
+
function trackResponse(id, started, up, reqCap) {
|
|
1046
|
+
const resCap = new Capture(TEXT.test(up.headers["content-type"] ?? ""), max);
|
|
1047
|
+
let bytes = 0;
|
|
1048
|
+
up.on("data", (c) => {
|
|
1049
|
+
bytes += c.length;
|
|
1050
|
+
resCap.push(c);
|
|
1051
|
+
});
|
|
1052
|
+
up.on(
|
|
1053
|
+
"end",
|
|
1054
|
+
() => o.store.update(id, {
|
|
1055
|
+
status: up.statusCode ?? null,
|
|
1056
|
+
durationMs: Date.now() - started,
|
|
1057
|
+
bytes,
|
|
1058
|
+
resHeaders: flat(up.headers),
|
|
1059
|
+
resBody: resCap.text(),
|
|
1060
|
+
truncated: !!reqCap?.truncated || resCap.truncated,
|
|
1061
|
+
resTruncated: resCap.truncated,
|
|
1062
|
+
...reqCap && { reqBody: reqCap.text(), reqRaw: reqCap.truncated ? null : reqCap.raw(), reqBodyBytes: reqCap.total, reqTruncated: reqCap.truncated }
|
|
1063
|
+
})
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
function replay(id) {
|
|
1067
|
+
const r = o.store.get(id);
|
|
1068
|
+
const blocked = replayBlock(r);
|
|
1069
|
+
if (blocked || !r) return { ok: false, reason: blocked ?? "Essa requisi\xE7\xE3o j\xE1 saiu da lista." };
|
|
1070
|
+
const route = [...o.routes.values()].find((x) => x.tunnel === r.tunnel);
|
|
1071
|
+
if (!route) return { ok: false, reason: `O t\xFAnel ${r.tunnel} n\xE3o est\xE1 aberto nesta sess\xE3o.` };
|
|
1072
|
+
const newId = nextId++;
|
|
1073
|
+
const started = Date.now();
|
|
1074
|
+
const headers = replayHeaders(r);
|
|
1075
|
+
o.store.add({
|
|
1076
|
+
id: newId,
|
|
1077
|
+
tunnel: r.tunnel,
|
|
1078
|
+
method: r.method,
|
|
1079
|
+
path: r.path,
|
|
1080
|
+
status: null,
|
|
1081
|
+
durationMs: null,
|
|
1082
|
+
bytes: 0,
|
|
1083
|
+
startedAt: started,
|
|
1084
|
+
websocket: false,
|
|
1085
|
+
reqHeaders: headers,
|
|
1086
|
+
resHeaders: {},
|
|
1087
|
+
reqBody: r.reqBody,
|
|
1088
|
+
resBody: null,
|
|
1089
|
+
reqRaw: r.reqRaw,
|
|
1090
|
+
reqBodyBytes: r.reqBodyBytes,
|
|
1091
|
+
reqTruncated: false,
|
|
1092
|
+
reqComplete: true,
|
|
1093
|
+
truncated: false,
|
|
1094
|
+
resTruncated: false,
|
|
1095
|
+
replayOf: r.id
|
|
1096
|
+
});
|
|
1097
|
+
void (async () => {
|
|
1098
|
+
const host = await localHost(route.port);
|
|
1099
|
+
const upstream = request({ host, port: route.port, agent: LOCAL_AGENT, method: r.method, path: r.path, headers }, (up) => trackResponse(newId, started, up));
|
|
1100
|
+
upstream.on("error", (err) => o.store.update(newId, { status: 503, durationMs: Date.now() - started, error: err.message || err.code || err.name }));
|
|
1101
|
+
upstream.end(r.reqRaw ?? void 0);
|
|
1102
|
+
})();
|
|
1103
|
+
return { ok: true, id: newId };
|
|
1104
|
+
}
|
|
1011
1105
|
const server = createServer(async (req, res) => {
|
|
1012
1106
|
req.pause();
|
|
1013
1107
|
const route = await routeFor(req);
|
|
@@ -1031,12 +1125,12 @@ async function startProxy(o) {
|
|
|
1031
1125
|
const headers = delivered(req.headers, route);
|
|
1032
1126
|
if (access?.kind === "denied") {
|
|
1033
1127
|
const html = wantsHtml(req.headers.accept);
|
|
1034
|
-
const { id: id2, started: started2 } = record(req, route, headers, false, html ? void 0 : access);
|
|
1128
|
+
const { id: id2, started: started2 } = record(req, route, headers, false, html ? void 0 : access, DENIED_NO_REPLAY);
|
|
1035
1129
|
const { status, out, body } = deny(req, res, html);
|
|
1036
1130
|
o.store.update(id2, { status, durationMs: Date.now() - started2, bytes: Buffer.byteLength(body), resHeaders: out });
|
|
1037
1131
|
return;
|
|
1038
1132
|
}
|
|
1039
|
-
const { id, started } = record(req, route, headers, false, access);
|
|
1133
|
+
const { id, started } = record(req, route, headers, false, access, preflight ? PREFLIGHT_NO_REPLAY : void 0);
|
|
1040
1134
|
const reqCap = new Capture(TEXT.test(req.headers["content-type"] ?? ""), max);
|
|
1041
1135
|
req.pause();
|
|
1042
1136
|
const host = await localHost(route.port);
|
|
@@ -1051,35 +1145,19 @@ async function startProxy(o) {
|
|
|
1051
1145
|
up.resume();
|
|
1052
1146
|
return;
|
|
1053
1147
|
}
|
|
1054
|
-
const resCap = new Capture(TEXT.test(up.headers["content-type"] ?? ""), max);
|
|
1055
|
-
let bytes = 0;
|
|
1056
1148
|
if (route.host && route.rewriteRedirects !== false && typeof up.headers.location === "string") {
|
|
1057
1149
|
up.headers.location = rewriteLocation(up.headers.location, route.host, hostOf(req));
|
|
1058
1150
|
}
|
|
1059
1151
|
res.writeHead(up.statusCode ?? 502, up.statusMessage, up.headers);
|
|
1060
|
-
|
|
1061
|
-
bytes += c.length;
|
|
1062
|
-
resCap.push(c);
|
|
1063
|
-
});
|
|
1152
|
+
trackResponse(id, started, up, reqCap);
|
|
1064
1153
|
up.pipe(res);
|
|
1065
|
-
up.on(
|
|
1066
|
-
"end",
|
|
1067
|
-
() => o.store.update(id, {
|
|
1068
|
-
status: up.statusCode ?? null,
|
|
1069
|
-
durationMs: Date.now() - started,
|
|
1070
|
-
bytes,
|
|
1071
|
-
resHeaders: flat(up.headers),
|
|
1072
|
-
reqBody: reqCap.text(),
|
|
1073
|
-
resBody: resCap.text(),
|
|
1074
|
-
truncated: reqCap.truncated || resCap.truncated
|
|
1075
|
-
})
|
|
1076
|
-
);
|
|
1077
1154
|
}
|
|
1078
1155
|
);
|
|
1079
1156
|
res.on("close", () => {
|
|
1080
1157
|
if (!res.writableFinished) upstream.destroy();
|
|
1081
1158
|
});
|
|
1082
1159
|
upstream.on("error", (err) => {
|
|
1160
|
+
req.resume();
|
|
1083
1161
|
if (res.destroyed && res.writableFinished === false && !res.headersSent) return;
|
|
1084
1162
|
const html = wantsHtml(req.headers.accept);
|
|
1085
1163
|
const msg = html ? unavailablePage({ tunnel: route.tunnel, port: route.port, retry: req.method === "GET" }) : `Nada escutando em localhost:${route.port}. Suba o servidor e tente de novo.`;
|
|
@@ -1089,6 +1167,7 @@ async function startProxy(o) {
|
|
|
1089
1167
|
} else res.destroy();
|
|
1090
1168
|
o.store.update(id, { status: 503, durationMs: Date.now() - started, bytes: res.headersSent ? 0 : Buffer.byteLength(msg), error: err.message || err.code || err.name, reqBody: reqCap.text() });
|
|
1091
1169
|
});
|
|
1170
|
+
req.on("end", () => o.store.update(id, { reqComplete: true, reqRaw: reqCap.truncated ? null : reqCap.raw(), reqBodyBytes: reqCap.total, reqTruncated: reqCap.truncated, reqBody: reqCap.text() }));
|
|
1092
1171
|
if (preflight) {
|
|
1093
1172
|
req.resume();
|
|
1094
1173
|
upstream.end();
|
|
@@ -1103,7 +1182,7 @@ async function startProxy(o) {
|
|
|
1103
1182
|
if (!route) return void client.end("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
|
|
1104
1183
|
const access = route.auth?.access(req.headers);
|
|
1105
1184
|
const headers = delivered(req.headers, route);
|
|
1106
|
-
const { id, started } = record(req, route, headers, true, access);
|
|
1185
|
+
const { id, started } = record(req, route, headers, true, access, access?.kind === "denied" ? DENIED_NO_REPLAY : void 0);
|
|
1107
1186
|
if (access?.kind === "denied") {
|
|
1108
1187
|
client.end(WS_DENIED);
|
|
1109
1188
|
o.store.update(id, { status: 401, durationMs: Date.now() - started });
|
|
@@ -1146,6 +1225,7 @@ async function startProxy(o) {
|
|
|
1146
1225
|
});
|
|
1147
1226
|
return {
|
|
1148
1227
|
port: server.address().port,
|
|
1228
|
+
replay,
|
|
1149
1229
|
close: () => new Promise((resolve) => {
|
|
1150
1230
|
for (const s of sockets) s.destroy();
|
|
1151
1231
|
server.close(() => resolve());
|
|
@@ -1241,6 +1321,9 @@ var RecordStore = class {
|
|
|
1241
1321
|
Object.assign(r, patch);
|
|
1242
1322
|
this.emit();
|
|
1243
1323
|
}
|
|
1324
|
+
get(id) {
|
|
1325
|
+
return this.items.find((x) => x.id === id);
|
|
1326
|
+
}
|
|
1244
1327
|
/** Mais recentes primeiro. Mesma referência até a próxima mudança (serve de snapshot para a tela). */
|
|
1245
1328
|
list() {
|
|
1246
1329
|
this.snapshot ??= [...this.items].reverse();
|
|
@@ -1348,6 +1431,10 @@ var Session = class {
|
|
|
1348
1431
|
this.auths.set(key, auth);
|
|
1349
1432
|
return auth;
|
|
1350
1433
|
}
|
|
1434
|
+
/** Reenvia uma requisição da lista ao serviço local. */
|
|
1435
|
+
replay(id) {
|
|
1436
|
+
return this.proxy ? this.proxy.replay(id) : { ok: false, reason: "Sess\xE3o ainda n\xE3o come\xE7ou." };
|
|
1437
|
+
}
|
|
1351
1438
|
async stop() {
|
|
1352
1439
|
if (this.stopped) return;
|
|
1353
1440
|
this.stopped = true;
|
|
@@ -1601,10 +1688,10 @@ async function runSession(specs, source, flags) {
|
|
|
1601
1688
|
}
|
|
1602
1689
|
const session = new Session({ api, deviceId: config.deviceId, deviceName: hostname(), cloudflaredPath, source, runDir: join6(dutoHome(), "run") });
|
|
1603
1690
|
if (wantsUi(flags)) {
|
|
1604
|
-
const { runFullscreen } = await import("./fullscreen-
|
|
1605
|
-
process.exit(await runFullscreen(session, specs, "0.
|
|
1691
|
+
const { runFullscreen } = await import("./fullscreen-7P5GBCQX.js");
|
|
1692
|
+
process.exit(await runFullscreen(session, specs, "0.6.0"));
|
|
1606
1693
|
}
|
|
1607
|
-
console.log(`${mark()} ${paint("muted", `${"0.
|
|
1694
|
+
console.log(`${mark()} ${paint("muted", `${"0.6.0"} \xB7 ${source} \xB7 Ctrl+C para encerrar`)}`);
|
|
1608
1695
|
const stopLog = runPlain(session, (l) => console.log(l));
|
|
1609
1696
|
const stopped = new Promise((resolve) => {
|
|
1610
1697
|
const onSignal = () => {
|
|
@@ -1698,7 +1785,7 @@ cli.help(
|
|
|
1698
1785
|
body: s.body.replace(/^\s*For more info, run any command with the `--help` flag:/m, " Mais detalhes de um comando: duto <comando> --help").replace("Display this message", "Mostra esta ajuda").replace("Display version number", "Mostra a vers\xE3o")
|
|
1699
1786
|
}))
|
|
1700
1787
|
);
|
|
1701
|
-
cli.version("0.
|
|
1788
|
+
cli.version("0.6.0");
|
|
1702
1789
|
try {
|
|
1703
1790
|
cli.parse(process.argv, { run: false });
|
|
1704
1791
|
await cli.runMatchedCommand();
|