@dutodev/cli 0.5.0 → 0.7.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/dist/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
C,
|
|
4
|
+
TRUNCATED_NOTE,
|
|
5
|
+
bodyNote,
|
|
3
6
|
formatBytes,
|
|
4
7
|
formatDuration,
|
|
5
8
|
formatTime,
|
|
@@ -7,7 +10,7 @@ import {
|
|
|
7
10
|
normalizeName,
|
|
8
11
|
statusColor,
|
|
9
12
|
validateName
|
|
10
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-74IJFYGT.js";
|
|
11
14
|
|
|
12
15
|
// src/ui/brand.ts
|
|
13
16
|
function colorOn(stream = process.stdout) {
|
|
@@ -30,6 +33,7 @@ function paint(tone, text, stream) {
|
|
|
30
33
|
var mark = () => `${paint("signal", "\u25B0")} ${paint("strong", "duto")}`;
|
|
31
34
|
var ok = (msg) => `${paint("signal", "\u2714")} ${msg}`;
|
|
32
35
|
var fail = (msg) => `${paint("err", "\u2716", process.stderr)} ${msg}`;
|
|
36
|
+
var warn = (msg) => `${paint("warn", "\u26A0")} ${msg}`;
|
|
33
37
|
|
|
34
38
|
// src/index.ts
|
|
35
39
|
import { hostname } from "os";
|
|
@@ -517,6 +521,921 @@ function findConfig(cwd) {
|
|
|
517
521
|
}
|
|
518
522
|
}
|
|
519
523
|
|
|
524
|
+
// src/inspector/jsonView.ts
|
|
525
|
+
function jsonSyntaxError(text) {
|
|
526
|
+
try {
|
|
527
|
+
JSON.parse(text);
|
|
528
|
+
return null;
|
|
529
|
+
} catch (e) {
|
|
530
|
+
const message2 = String(e && e.message ? e.message : e);
|
|
531
|
+
const m = /position (\d+)/.exec(message2);
|
|
532
|
+
if (m) return { position: Number(m[1]), message: message2 };
|
|
533
|
+
const lc = /line (\d+) column (\d+)/.exec(message2);
|
|
534
|
+
if (lc) {
|
|
535
|
+
let idx = 0;
|
|
536
|
+
for (let ln = 1; ln < Number(lc[1]); ln++) {
|
|
537
|
+
const nl = text.indexOf("\n", idx);
|
|
538
|
+
if (nl < 0) return { position: text.length, message: message2 };
|
|
539
|
+
idx = nl + 1;
|
|
540
|
+
}
|
|
541
|
+
return { position: Math.min(idx + Number(lc[2]) - 1, text.length), message: message2 };
|
|
542
|
+
}
|
|
543
|
+
return { position: text.length, message: message2 };
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
function jsonTokens(text) {
|
|
547
|
+
const out = [];
|
|
548
|
+
let depth = 0;
|
|
549
|
+
let i = 0;
|
|
550
|
+
while (i < text.length) {
|
|
551
|
+
const c = text.charAt(i);
|
|
552
|
+
let j = i + 1;
|
|
553
|
+
let kind;
|
|
554
|
+
if (c === " " || c === "\n" || c === " " || c === "\r") {
|
|
555
|
+
while (j < text.length && " \n \r".indexOf(text.charAt(j)) >= 0) j++;
|
|
556
|
+
kind = "space";
|
|
557
|
+
} else if (c === '"') {
|
|
558
|
+
while (j < text.length && text.charAt(j) !== '"') j += text.charAt(j) === "\\" ? 2 : 1;
|
|
559
|
+
j = Math.min(j + 1, text.length);
|
|
560
|
+
let k = j;
|
|
561
|
+
while (k < text.length && " \n \r".indexOf(text.charAt(k)) >= 0) k++;
|
|
562
|
+
kind = text.charAt(k) === ":" ? "key" : "string";
|
|
563
|
+
} else if ("{}[],:".indexOf(c) >= 0) {
|
|
564
|
+
kind = "punct";
|
|
565
|
+
if (c === "}" || c === "]") depth = Math.max(0, depth - 1);
|
|
566
|
+
} else {
|
|
567
|
+
while (j < text.length && ' \n \r{}[],:"'.indexOf(text.charAt(j)) < 0) j++;
|
|
568
|
+
const word = text.slice(i, j);
|
|
569
|
+
kind = word === "true" || word === "false" || word === "null" ? "literal" : "number";
|
|
570
|
+
}
|
|
571
|
+
out.push({ kind, text: text.slice(i, j), depth });
|
|
572
|
+
if (kind === "punct" && (c === "{" || c === "[")) depth++;
|
|
573
|
+
i = j;
|
|
574
|
+
}
|
|
575
|
+
return out;
|
|
576
|
+
}
|
|
577
|
+
function jsonIndent(text) {
|
|
578
|
+
let out = "";
|
|
579
|
+
let depth = 0;
|
|
580
|
+
let i = 0;
|
|
581
|
+
const ws = " \n \r";
|
|
582
|
+
while (i < text.length) {
|
|
583
|
+
const c = text.charAt(i);
|
|
584
|
+
if (ws.indexOf(c) >= 0) {
|
|
585
|
+
i++;
|
|
586
|
+
} else if (c === '"') {
|
|
587
|
+
let j = i + 1;
|
|
588
|
+
while (j < text.length && text.charAt(j) !== '"') j += text.charAt(j) === "\\" ? 2 : 1;
|
|
589
|
+
out += text.slice(i, j + 1);
|
|
590
|
+
i = j + 1;
|
|
591
|
+
} else if (c === "{" || c === "[") {
|
|
592
|
+
let j = i + 1;
|
|
593
|
+
while (j < text.length && ws.indexOf(text.charAt(j)) >= 0) j++;
|
|
594
|
+
if (text.charAt(j) === (c === "{" ? "}" : "]")) {
|
|
595
|
+
out += c + text.charAt(j);
|
|
596
|
+
i = j + 1;
|
|
597
|
+
} else {
|
|
598
|
+
depth++;
|
|
599
|
+
out += c + "\n" + " ".repeat(depth);
|
|
600
|
+
i++;
|
|
601
|
+
}
|
|
602
|
+
} else if (c === "}" || c === "]") {
|
|
603
|
+
depth = Math.max(0, depth - 1);
|
|
604
|
+
out += "\n" + " ".repeat(depth) + c;
|
|
605
|
+
i++;
|
|
606
|
+
} else if (c === ",") {
|
|
607
|
+
out += ",\n" + " ".repeat(depth);
|
|
608
|
+
i++;
|
|
609
|
+
} else if (c === ":") {
|
|
610
|
+
out += ": ";
|
|
611
|
+
i++;
|
|
612
|
+
} else {
|
|
613
|
+
let j = i + 1;
|
|
614
|
+
while (j < text.length && (ws + '{}[],:"').indexOf(text.charAt(j)) < 0) j++;
|
|
615
|
+
out += text.slice(i, j);
|
|
616
|
+
i = j;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return out;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// src/inspector/page.ts
|
|
623
|
+
var BG = "#111413";
|
|
624
|
+
var BAR = "#0b0d0c";
|
|
625
|
+
var LINE = "#222826";
|
|
626
|
+
var ROW_LINE = "#1a1f1d";
|
|
627
|
+
var INPUT_LINE = "#2a312e";
|
|
628
|
+
var TEXT = "#e7e9e6";
|
|
629
|
+
var SELECTED = "#16201e";
|
|
630
|
+
var JSON_KEY = "#93c5fd";
|
|
631
|
+
var JSON_STRING = "#fdba74";
|
|
632
|
+
var JSON_VALUE = "#c4b5fd";
|
|
633
|
+
var MONO = '"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace';
|
|
634
|
+
var STYLE = `
|
|
635
|
+
:root { color-scheme: dark; }
|
|
636
|
+
* { box-sizing: border-box; }
|
|
637
|
+
html, body { height: 100%; }
|
|
638
|
+
body { margin: 0; background: ${BG}; color: ${TEXT}; font-family: ${MONO}; font-size: 13px; line-height: 1.45; }
|
|
639
|
+
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
|
|
640
|
+
:focus-visible { outline: 1px solid ${C.signal}; outline-offset: -1px; }
|
|
641
|
+
.app { display: flex; flex-direction: column; height: 100vh; }
|
|
642
|
+
.top { display: flex; justify-content: space-between; gap: 16px; padding: 8px 12px; background: ${BAR}; border-bottom: 1px solid ${LINE}; color: ${C.muted}; }
|
|
643
|
+
.top b { color: ${C.signal}; font-weight: 600; }
|
|
644
|
+
.tunnels { display: flex; flex-wrap: wrap; gap: 4px 14px; justify-content: flex-end; min-width: 0; }
|
|
645
|
+
.tunnels .k { color: ${C.strong}; }
|
|
646
|
+
.banner { padding: 6px 12px; background: ${BAR}; border-bottom: 1px solid ${C.err}; color: ${C.err}; }
|
|
647
|
+
.main { flex: 1; min-height: 0; display: grid; grid-template-columns: 320px 1fr; }
|
|
648
|
+
.side { display: flex; flex-direction: column; min-height: 0; border-right: 1px solid ${LINE}; }
|
|
649
|
+
.search { margin: 8px 12px; padding: 5px 8px; border: 1px solid ${INPUT_LINE}; border-radius: 5px; background: ${BG}; color: ${TEXT}; font: inherit; outline: none; }
|
|
650
|
+
.search:focus { border-color: ${C.signal}; }
|
|
651
|
+
.search::placeholder { color: ${C.muted}; }
|
|
652
|
+
.list { flex: 1; min-height: 0; overflow-y: auto; outline: none; }
|
|
653
|
+
.row { display: grid; grid-template-columns: 14px 42px minmax(0, 1fr) 34px 56px; gap: 8px; padding: 5px 12px; border-bottom: 1px solid ${ROW_LINE}; align-items: center; cursor: pointer; white-space: nowrap; }
|
|
654
|
+
.list.multi .row { grid-template-columns: 14px 56px 42px minmax(0, 1fr) 34px 56px; }
|
|
655
|
+
.row:hover { background: #141917; }
|
|
656
|
+
.row.sel { background: ${SELECTED}; box-shadow: inset 2px 0 0 ${C.signal}; }
|
|
657
|
+
.row span { overflow: hidden; text-overflow: ellipsis; }
|
|
658
|
+
.row .dur { text-align: right; }
|
|
659
|
+
.m { font-weight: 700; }
|
|
660
|
+
.mut { color: ${C.muted}; }
|
|
661
|
+
.empty { padding: 12px; color: ${C.muted}; }
|
|
662
|
+
.detail { display: flex; flex-direction: column; min-height: 0; min-width: 0; }
|
|
663
|
+
.pane { flex: 1; }
|
|
664
|
+
.head { padding: 8px 12px; overflow-wrap: anywhere; }
|
|
665
|
+
.head b { color: ${C.strong}; }
|
|
666
|
+
.tabs { display: flex; gap: 14px; padding: 8px 12px 0; border-bottom: 1px solid ${LINE}; }
|
|
667
|
+
.tab { color: ${C.muted}; padding-bottom: 6px; border-bottom: 2px solid transparent; }
|
|
668
|
+
.tab:hover { color: ${TEXT}; }
|
|
669
|
+
.tab.on { color: ${C.strong}; border-bottom-color: ${C.signal}; }
|
|
670
|
+
.content { flex: 1; min-height: 0; overflow: auto; }
|
|
671
|
+
.content pre { margin: 0; padding: 10px 12px; line-height: 1.55; white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; }
|
|
672
|
+
.note { padding: 10px 12px; color: ${C.muted}; }
|
|
673
|
+
.trunc { padding: 0 12px 10px; color: ${C.warn}; }
|
|
674
|
+
.jerr { padding: 10px 12px 0; color: ${C.err}; }
|
|
675
|
+
.bad { background: ${C.err}40; color: ${C.strong}; }
|
|
676
|
+
.k { color: ${JSON_KEY}; } .s { color: ${JSON_STRING}; } .n { color: ${JSON_VALUE}; }
|
|
677
|
+
.fold.closed > .inner { display: none; }
|
|
678
|
+
.fold > .sum { display: none; color: ${C.muted}; }
|
|
679
|
+
.fold.closed > .sum { display: inline; }
|
|
680
|
+
.open { cursor: pointer; }
|
|
681
|
+
.open:hover { color: ${C.signal}; }
|
|
682
|
+
table { border-collapse: collapse; margin: 6px 12px 10px; }
|
|
683
|
+
th, td { text-align: left; vertical-align: top; padding: 3px 16px 3px 0; border-bottom: 1px solid ${ROW_LINE}; overflow-wrap: anywhere; }
|
|
684
|
+
th { color: ${JSON_KEY}; font-weight: 400; white-space: nowrap; }
|
|
685
|
+
.sect { padding: 10px 12px 0; color: ${C.muted}; }
|
|
686
|
+
.btns { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 8px 12px; border-top: 1px solid ${LINE}; }
|
|
687
|
+
.btn { border: 1px solid ${INPUT_LINE}; border-radius: 5px; padding: 3px 8px; }
|
|
688
|
+
.btn:hover:not(:disabled) { border-color: ${C.muted}; }
|
|
689
|
+
.btn.pri { border-color: ${C.signal}; color: ${C.signal}; }
|
|
690
|
+
.btn:disabled { color: ${C.muted}; border-color: ${ROW_LINE}; cursor: default; }
|
|
691
|
+
.msg { color: ${C.muted}; }
|
|
692
|
+
.msg.err { color: ${C.err}; }
|
|
693
|
+
[hidden] { display: none !important; }
|
|
694
|
+
@media (max-width: 720px) {
|
|
695
|
+
.main { grid-template-columns: 1fr; grid-template-rows: 40vh 1fr; }
|
|
696
|
+
.side { border-right: 0; border-bottom: 1px solid ${LINE}; }
|
|
697
|
+
}
|
|
698
|
+
`;
|
|
699
|
+
var SCRIPT = String.raw`"use strict";
|
|
700
|
+
var jsonSyntaxError = (${jsonSyntaxError.toString()});
|
|
701
|
+
var jsonTokens = (${jsonTokens.toString()});
|
|
702
|
+
var jsonIndent = (${jsonIndent.toString()});
|
|
703
|
+
var COLORS = ${JSON.stringify(C)};
|
|
704
|
+
var TOKEN = new URLSearchParams(location.search).get("t") || "";
|
|
705
|
+
var TABS = ["Corpo", "Cabeçalhos", "Resposta", "Raw"];
|
|
706
|
+
|
|
707
|
+
var state = { items: [], tunnels: [], selected: null, detail: null, detailKey: "", tab: "Corpo", ended: false };
|
|
708
|
+
var listSeq = 0, detailSeq = 0, msgTimer = null;
|
|
709
|
+
var es = null, lost = false, replaying = false;
|
|
710
|
+
function fail(e) { if (!state.ended) console.error(e); }
|
|
711
|
+
|
|
712
|
+
function $(id) { return document.getElementById(id); }
|
|
713
|
+
function el(tag, cls, text) {
|
|
714
|
+
var e = document.createElement(tag);
|
|
715
|
+
if (cls) e.className = cls;
|
|
716
|
+
if (text !== undefined && text !== null) e.textContent = String(text);
|
|
717
|
+
return e;
|
|
718
|
+
}
|
|
719
|
+
function clear(node) { while (node.firstChild) node.removeChild(node.firstChild); }
|
|
720
|
+
|
|
721
|
+
function banner(text) {
|
|
722
|
+
var b = $("banner");
|
|
723
|
+
b.textContent = text || "";
|
|
724
|
+
b.hidden = !text;
|
|
725
|
+
}
|
|
726
|
+
function ended() {
|
|
727
|
+
state.ended = true;
|
|
728
|
+
banner("Sessão encerrada; abra o inspetor de novo pela tecla i.");
|
|
729
|
+
if (es) es.close();
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function api(path, method) {
|
|
733
|
+
return fetch(path, { method: method || "GET", headers: { "X-Duto-Inspetor": TOKEN }, cache: "no-store" }).then(function (r) {
|
|
734
|
+
if (r.status === 403) { ended(); throw new Error("403"); }
|
|
735
|
+
return r.json().then(function (body) { return { status: r.status, body: body }; });
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function statusInfo(it) {
|
|
740
|
+
if (it.status !== null && it.status !== undefined) {
|
|
741
|
+
var s = it.status;
|
|
742
|
+
var color = s >= 500 ? COLORS.err : s >= 400 ? COLORS.warn : s >= 300 ? COLORS.signal : COLORS.ok;
|
|
743
|
+
return { text: String(s), color: color };
|
|
744
|
+
}
|
|
745
|
+
if (it.error) return { text: "erro", color: COLORS.err };
|
|
746
|
+
return { text: "…", color: COLORS.muted };
|
|
747
|
+
}
|
|
748
|
+
function duration(it) { return it.durationMs === null || it.durationMs === undefined ? "" : it.durationMs + " ms"; }
|
|
749
|
+
function size(n) {
|
|
750
|
+
if (n < 1024) return n + " B";
|
|
751
|
+
if (n < 1024 * 1024) return (n / 1024).toFixed(1).replace(".", ",") + " KB";
|
|
752
|
+
return (n / 1024 / 1024).toFixed(1).replace(".", ",") + " MB";
|
|
753
|
+
}
|
|
754
|
+
function methodOf(it) { return it.websocket ? "WS" : it.method; }
|
|
755
|
+
function header(h, name) {
|
|
756
|
+
for (var k in h) if (k.toLowerCase() === name) return h[k];
|
|
757
|
+
return "";
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Mesma regra do filtro da tela do terminal.
|
|
761
|
+
function visible() {
|
|
762
|
+
var f = $("filter").value.trim().toLowerCase();
|
|
763
|
+
if (!f) return state.items;
|
|
764
|
+
return state.items.filter(function (it) {
|
|
765
|
+
return (it.tunnel + " " + (it.websocket ? "ws" : it.method) + " " + it.path + " " + (it.status === null || it.status === undefined ? "" : it.status)).toLowerCase().indexOf(f) >= 0;
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function renderTop() {
|
|
770
|
+
var n = state.items.length;
|
|
771
|
+
$("count").textContent = n + (n === 1 ? " requisição" : " requisições");
|
|
772
|
+
var box = $("tunnels");
|
|
773
|
+
clear(box);
|
|
774
|
+
state.tunnels.forEach(function (t) {
|
|
775
|
+
var s = el("span");
|
|
776
|
+
s.appendChild(el("span", "k", t.key));
|
|
777
|
+
s.appendChild(document.createTextNode(" " + (t.url || "sem endereço ainda")));
|
|
778
|
+
box.appendChild(s);
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function renderList() {
|
|
783
|
+
var list = $("list");
|
|
784
|
+
var multi = state.tunnels.length > 1;
|
|
785
|
+
list.classList.toggle("multi", multi);
|
|
786
|
+
clear(list);
|
|
787
|
+
var items = visible();
|
|
788
|
+
if (items.length === 0) {
|
|
789
|
+
list.appendChild(el("div", "empty", state.items.length === 0 ? "Nenhuma requisição ainda." : "Nada casa com o filtro."));
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
items.forEach(function (it) {
|
|
793
|
+
var row = el("div", "row" + (it.id === state.selected ? " sel" : ""));
|
|
794
|
+
row.setAttribute("role", "option");
|
|
795
|
+
row.setAttribute("aria-selected", it.id === state.selected ? "true" : "false");
|
|
796
|
+
row.dataset.id = String(it.id);
|
|
797
|
+
row.appendChild(el("span", "mut", it.replayOf !== undefined ? "↻" : ""));
|
|
798
|
+
if (multi) row.appendChild(el("span", "mut", it.tunnel));
|
|
799
|
+
row.appendChild(el("span", "m", methodOf(it)));
|
|
800
|
+
var p = el("span", "", it.path);
|
|
801
|
+
p.title = it.path;
|
|
802
|
+
row.appendChild(p);
|
|
803
|
+
var st = statusInfo(it);
|
|
804
|
+
var s = el("span", "", st.text);
|
|
805
|
+
s.style.color = st.color;
|
|
806
|
+
row.appendChild(s);
|
|
807
|
+
row.appendChild(el("span", "mut dur", duration(it)));
|
|
808
|
+
row.addEventListener("click", function () { select(it.id); });
|
|
809
|
+
list.appendChild(row);
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function loadList() {
|
|
814
|
+
var seq = ++listSeq;
|
|
815
|
+
return Promise.all([api("/api/requisicoes"), api("/api/sessao")]).then(function (rs) {
|
|
816
|
+
if (seq !== listSeq) return;
|
|
817
|
+
state.items = rs[0].body.items || [];
|
|
818
|
+
state.tunnels = rs[1].body.tunnels || [];
|
|
819
|
+
renderTop();
|
|
820
|
+
if (state.selected === null && state.items.length > 0) return select(state.items[0].id);
|
|
821
|
+
renderList();
|
|
822
|
+
}).catch(fail);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function select(id) {
|
|
826
|
+
if (state.selected !== id) {
|
|
827
|
+
state.selected = id;
|
|
828
|
+
state.detail = null;
|
|
829
|
+
state.detailKey = "";
|
|
830
|
+
hideMsg();
|
|
831
|
+
}
|
|
832
|
+
renderList();
|
|
833
|
+
var row = document.querySelector('.row[data-id="' + id + '"]');
|
|
834
|
+
if (row) row.scrollIntoView({ block: "nearest" });
|
|
835
|
+
return loadDetail(id);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function loadDetail(id) {
|
|
839
|
+
var seq = ++detailSeq;
|
|
840
|
+
return api("/api/requisicoes/" + id).then(function (r) {
|
|
841
|
+
if (seq !== detailSeq || state.selected !== id) return;
|
|
842
|
+
var key = JSON.stringify(r.body);
|
|
843
|
+
if (key === state.detailKey) return; // nada mudou: mantém dobras e rolagem
|
|
844
|
+
state.detailKey = key;
|
|
845
|
+
state.detail = r.status === 200 ? r.body : null;
|
|
846
|
+
if (r.status !== 200) renderGone(r.body && r.body.error ? r.body.error : "Essa requisição já saiu da lista.");
|
|
847
|
+
else renderDetail();
|
|
848
|
+
}).catch(fail);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function renderGone(text) {
|
|
852
|
+
$("detail").hidden = true;
|
|
853
|
+
$("none").hidden = false;
|
|
854
|
+
$("none").textContent = text;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function renderDetail() {
|
|
858
|
+
var d = state.detail;
|
|
859
|
+
if (!d) return;
|
|
860
|
+
$("none").hidden = true;
|
|
861
|
+
$("detail").hidden = false;
|
|
862
|
+
|
|
863
|
+
var head = $("head");
|
|
864
|
+
clear(head);
|
|
865
|
+
head.appendChild(el("b", "", methodOf(d) + " " + d.path));
|
|
866
|
+
head.appendChild(document.createTextNode(" "));
|
|
867
|
+
var st = statusInfo(d);
|
|
868
|
+
var s = el("span", "", st.text);
|
|
869
|
+
s.style.color = st.color;
|
|
870
|
+
head.appendChild(s);
|
|
871
|
+
var parts = [];
|
|
872
|
+
if (duration(d)) parts.push(duration(d));
|
|
873
|
+
parts.push(size(d.bytes));
|
|
874
|
+
parts.push(d.tunnel);
|
|
875
|
+
if (d.replayOf !== undefined) parts.push("↻ reenvio de #" + d.replayOf);
|
|
876
|
+
head.appendChild(el("span", "mut", " · " + parts.join(" · ")));
|
|
877
|
+
if (d.error) head.appendChild(el("div", "msg err", d.error));
|
|
878
|
+
|
|
879
|
+
var tabs = document.querySelectorAll(".tab");
|
|
880
|
+
for (var i = 0; i < tabs.length; i++) {
|
|
881
|
+
var on = tabs[i].dataset.tab === state.tab;
|
|
882
|
+
tabs[i].classList.toggle("on", on);
|
|
883
|
+
tabs[i].setAttribute("aria-selected", on ? "true" : "false");
|
|
884
|
+
}
|
|
885
|
+
renderContent();
|
|
886
|
+
renderButtons();
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function renderContent() {
|
|
890
|
+
var d = state.detail;
|
|
891
|
+
var box = $("content");
|
|
892
|
+
var top = box.scrollTop;
|
|
893
|
+
clear(box);
|
|
894
|
+
if (state.tab === "Corpo") renderBody(box, d.reqBody, d.reqNote, d.reqTruncatedNote, d.reqHeaders);
|
|
895
|
+
else if (state.tab === "Cabeçalhos") box.appendChild(table(d.reqHeaders));
|
|
896
|
+
else if (state.tab === "Resposta") {
|
|
897
|
+
box.appendChild(el("div", "sect", "Cabeçalhos"));
|
|
898
|
+
box.appendChild(table(d.resHeaders));
|
|
899
|
+
box.appendChild(el("div", "sect", "Corpo"));
|
|
900
|
+
renderBody(box, d.resBody, d.resNote, d.resTruncatedNote, d.resHeaders);
|
|
901
|
+
} else {
|
|
902
|
+
var lines = [d.method + " " + d.path + " HTTP/1.1"];
|
|
903
|
+
for (var k in d.reqHeaders) lines.push(k + ": " + d.reqHeaders[k]);
|
|
904
|
+
lines.push("");
|
|
905
|
+
lines.push(d.reqNote !== null ? d.reqNote : d.reqBody || "");
|
|
906
|
+
box.appendChild(el("pre", "", lines.join("\n")));
|
|
907
|
+
}
|
|
908
|
+
box.scrollTop = top;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function table(h) {
|
|
912
|
+
var keys = Object.keys(h || {});
|
|
913
|
+
if (keys.length === 0) return el("div", "note", "Sem cabeçalhos.");
|
|
914
|
+
var t = el("table");
|
|
915
|
+
keys.forEach(function (k) {
|
|
916
|
+
var tr = el("tr");
|
|
917
|
+
tr.appendChild(el("th", "", k));
|
|
918
|
+
tr.appendChild(el("td", "", h[k]));
|
|
919
|
+
t.appendChild(tr);
|
|
920
|
+
});
|
|
921
|
+
return t;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function renderBody(box, body, note, truncNote, headers) {
|
|
925
|
+
if (note !== null) {
|
|
926
|
+
box.appendChild(el("div", "note", note));
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
if (body === null || body === undefined) {
|
|
930
|
+
box.appendChild(el("div", "note", "Sem corpo."));
|
|
931
|
+
} else if (header(headers, "content-type").toLowerCase().indexOf("json") >= 0) {
|
|
932
|
+
var err = jsonSyntaxError(body);
|
|
933
|
+
if (err === null) box.appendChild(jsonTree(jsonIndent(body)));
|
|
934
|
+
else {
|
|
935
|
+
box.appendChild(el("div", "jerr", err.message));
|
|
936
|
+
var pre = el("pre");
|
|
937
|
+
var at = Math.min(err.position, Math.max(0, body.length - 1));
|
|
938
|
+
pre.appendChild(document.createTextNode(body.slice(0, at)));
|
|
939
|
+
pre.appendChild(el("span", "bad", body.slice(at, at + 1) || " "));
|
|
940
|
+
pre.appendChild(document.createTextNode(body.slice(at + 1)));
|
|
941
|
+
box.appendChild(pre);
|
|
942
|
+
}
|
|
943
|
+
} else {
|
|
944
|
+
box.appendChild(el("pre", "", body));
|
|
945
|
+
}
|
|
946
|
+
if (truncNote !== null) box.appendChild(el("div", "trunc", truncNote));
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
var KIND = { key: "k", string: "s", number: "n", literal: "n", punct: "", space: "" };
|
|
950
|
+
|
|
951
|
+
// JSON colorido; clicar no { ou [ recolhe para "{ ▸ N campos }".
|
|
952
|
+
function jsonTree(text) {
|
|
953
|
+
var pre = el("pre");
|
|
954
|
+
var stack = [{ node: pre }];
|
|
955
|
+
jsonTokens(text).forEach(function (t) {
|
|
956
|
+
var top = stack[stack.length - 1];
|
|
957
|
+
if (top.wrap && t.kind !== "space" && !(t.kind === "punct" && (t.text === "}" || t.text === "]"))) top.has = true;
|
|
958
|
+
if (t.kind === "punct" && (t.text === "{" || t.text === "[")) {
|
|
959
|
+
var wrap = el("span", "fold");
|
|
960
|
+
var open = el("span", "open", t.text);
|
|
961
|
+
var inner = el("span", "inner");
|
|
962
|
+
var sum = el("span", "sum");
|
|
963
|
+
wrap.appendChild(open);
|
|
964
|
+
wrap.appendChild(inner);
|
|
965
|
+
wrap.appendChild(sum);
|
|
966
|
+
top.node.appendChild(wrap);
|
|
967
|
+
open.addEventListener("click", function () { if (wrap.dataset.n) wrap.classList.toggle("closed"); });
|
|
968
|
+
stack.push({ node: inner, wrap: wrap, open: open, sum: sum, depth: t.depth, obj: t.text === "{", count: 0, has: false });
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
if (t.kind === "punct" && (t.text === "}" || t.text === "]") && stack.length > 1) {
|
|
972
|
+
var c = stack.pop();
|
|
973
|
+
var n = c.has ? c.count + 1 : 0;
|
|
974
|
+
if (n > 0) {
|
|
975
|
+
c.wrap.dataset.n = String(n);
|
|
976
|
+
c.open.title = "recolher ou abrir";
|
|
977
|
+
c.sum.textContent = " ▸ " + n + (c.obj ? (n === 1 ? " campo " : " campos ") : n === 1 ? " item " : " itens ");
|
|
978
|
+
} else c.open.className = "";
|
|
979
|
+
c.wrap.appendChild(document.createTextNode(t.text));
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
if (top.wrap && t.kind === "punct" && t.text === "," && t.depth === top.depth + 1) top.count++;
|
|
983
|
+
var cls = KIND[t.kind];
|
|
984
|
+
top.node.appendChild(cls ? el("span", cls, t.text) : document.createTextNode(t.text));
|
|
985
|
+
});
|
|
986
|
+
return pre;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function renderButtons() {
|
|
990
|
+
var d = state.detail;
|
|
991
|
+
var replay = $("replay");
|
|
992
|
+
replay.disabled = d.replayBlocked !== null || replaying;
|
|
993
|
+
replay.title = d.replayBlocked || "";
|
|
994
|
+
replay.parentNode.title = d.replayBlocked || "";
|
|
995
|
+
|
|
996
|
+
var copyBody = $("copyBody");
|
|
997
|
+
var b = copyable(d);
|
|
998
|
+
copyBody.disabled = b.text === null;
|
|
999
|
+
copyBody.title = b.text === null ? b.note || "" : "";
|
|
1000
|
+
copyBody.parentNode.title = copyBody.title;
|
|
1001
|
+
|
|
1002
|
+
var curl = $("copyCurl");
|
|
1003
|
+
curl.disabled = d.curl === null;
|
|
1004
|
+
curl.title = d.curl === null ? d.curlNote || "" : "";
|
|
1005
|
+
curl.parentNode.title = curl.title;
|
|
1006
|
+
var notes = [];
|
|
1007
|
+
if (d.curl !== null && d.curlNote) notes.push(d.curlNote);
|
|
1008
|
+
var tunnel = state.tunnels.filter(function (t) { return t.key === d.tunnel; })[0];
|
|
1009
|
+
if (d.curl !== null && tunnel && tunnel.auth) notes.push("Troque SENHA pela senha do túnel.");
|
|
1010
|
+
$("curlNote").textContent = notes.join(" ");
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// "Copiar corpo" copia o corpo da aba ativa: a resposta na aba Resposta, senão a requisição.
|
|
1014
|
+
function copyable(d) {
|
|
1015
|
+
return state.tab === "Resposta" ? { text: d.resBody, note: d.resNote } : { text: d.reqBody, note: d.reqNote };
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function showMsg(text, isErr) {
|
|
1019
|
+
var m = $("msg");
|
|
1020
|
+
m.textContent = text;
|
|
1021
|
+
m.className = "msg" + (isErr ? " err" : "");
|
|
1022
|
+
m.hidden = false;
|
|
1023
|
+
if (msgTimer) clearTimeout(msgTimer);
|
|
1024
|
+
msgTimer = null;
|
|
1025
|
+
}
|
|
1026
|
+
function hideMsg() {
|
|
1027
|
+
if (msgTimer) clearTimeout(msgTimer);
|
|
1028
|
+
msgTimer = null;
|
|
1029
|
+
$("msg").hidden = true;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function copy(text) {
|
|
1033
|
+
navigator.clipboard.writeText(text).then(function () {
|
|
1034
|
+
showMsg("Copiado.", false);
|
|
1035
|
+
msgTimer = setTimeout(hideMsg, 2000);
|
|
1036
|
+
}, function () {
|
|
1037
|
+
showMsg("Não deu para copiar.", true);
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function replay() {
|
|
1042
|
+
var d = state.detail;
|
|
1043
|
+
if (!d || state.ended) return;
|
|
1044
|
+
if (d.replayBlocked !== null) return showMsg(d.replayBlocked, true);
|
|
1045
|
+
if (replaying) return;
|
|
1046
|
+
replaying = true;
|
|
1047
|
+
$("replay").disabled = true;
|
|
1048
|
+
api("/api/requisicoes/" + d.id + "/reenviar", "POST").then(function (r) {
|
|
1049
|
+
replaying = false;
|
|
1050
|
+
if (state.detail) renderButtons();
|
|
1051
|
+
if (r.body && r.body.ok) {
|
|
1052
|
+
hideMsg();
|
|
1053
|
+
return loadList().then(function () { return select(r.body.id); });
|
|
1054
|
+
}
|
|
1055
|
+
showMsg(r.body && r.body.reason ? r.body.reason : "Não deu para reenviar.", true);
|
|
1056
|
+
}).catch(function (e) {
|
|
1057
|
+
replaying = false;
|
|
1058
|
+
if (state.detail) renderButtons();
|
|
1059
|
+
fail(e);
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function move(delta) {
|
|
1064
|
+
var items = visible();
|
|
1065
|
+
if (items.length === 0) return;
|
|
1066
|
+
var i = -1;
|
|
1067
|
+
for (var j = 0; j < items.length; j++) if (items[j].id === state.selected) i = j;
|
|
1068
|
+
var next = i < 0 ? 0 : Math.max(0, Math.min(items.length - 1, i + delta));
|
|
1069
|
+
if (items[next].id !== state.selected) select(items[next].id);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function connect() {
|
|
1073
|
+
if (state.ended) return;
|
|
1074
|
+
es = new EventSource("/api/eventos?t=" + encodeURIComponent(TOKEN));
|
|
1075
|
+
es.addEventListener("open", function () {
|
|
1076
|
+
if (state.ended) return;
|
|
1077
|
+
banner("");
|
|
1078
|
+
// Reconectou: o que mudou no meio do caminho não chegou por evento.
|
|
1079
|
+
if (lost) {
|
|
1080
|
+
lost = false;
|
|
1081
|
+
loadList();
|
|
1082
|
+
if (state.selected !== null) loadDetail(state.selected);
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
es.addEventListener("error", function () {
|
|
1086
|
+
if (state.ended) return;
|
|
1087
|
+
lost = true;
|
|
1088
|
+
banner("Desconectado da sessão.");
|
|
1089
|
+
// Resposta não 200 (ex.: 403 de outra sessão) fecha o EventSource de vez.
|
|
1090
|
+
if (es.readyState === 2) probe();
|
|
1091
|
+
});
|
|
1092
|
+
es.addEventListener("mudou", function (ev) {
|
|
1093
|
+
var ids = [];
|
|
1094
|
+
try { ids = JSON.parse(ev.data).ids || []; } catch (e) {}
|
|
1095
|
+
loadList();
|
|
1096
|
+
// Lista vazia de ids: algo mudou fora da assinatura; busca o detalhe de novo também.
|
|
1097
|
+
if (state.selected !== null && (ids.length === 0 || ids.indexOf(state.selected) >= 0)) loadDetail(state.selected);
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// EventSource fechado: a API diz se a sessão acabou (403 → ended) ou se dá para reconectar.
|
|
1102
|
+
function probe() {
|
|
1103
|
+
api("/api/sessao").then(function () {
|
|
1104
|
+
if (!state.ended) setTimeout(connect, 3000);
|
|
1105
|
+
}, function () {
|
|
1106
|
+
if (!state.ended) setTimeout(connect, 3000);
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function init() {
|
|
1111
|
+
var tabs = document.querySelectorAll(".tab");
|
|
1112
|
+
for (var i = 0; i < tabs.length; i++) {
|
|
1113
|
+
tabs[i].addEventListener("click", function (ev) {
|
|
1114
|
+
state.tab = ev.currentTarget.dataset.tab;
|
|
1115
|
+
if (state.detail) renderDetail();
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
$("filter").addEventListener("input", renderList);
|
|
1119
|
+
$("replay").addEventListener("click", replay);
|
|
1120
|
+
$("copyBody").addEventListener("click", function () {
|
|
1121
|
+
var b = state.detail ? copyable(state.detail) : { text: null };
|
|
1122
|
+
if (b.text !== null && b.text !== undefined) copy(b.text);
|
|
1123
|
+
});
|
|
1124
|
+
$("copyCurl").addEventListener("click", function () { if (state.detail && state.detail.curl !== null) copy(state.detail.curl); });
|
|
1125
|
+
|
|
1126
|
+
document.addEventListener("keydown", function (ev) {
|
|
1127
|
+
if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
|
|
1128
|
+
var inFilter = document.activeElement === $("filter");
|
|
1129
|
+
if (ev.key === "ArrowDown" || ev.key === "ArrowUp") {
|
|
1130
|
+
ev.preventDefault();
|
|
1131
|
+
move(ev.key === "ArrowDown" ? 1 : -1);
|
|
1132
|
+
} else if (ev.key === "Escape") {
|
|
1133
|
+
if ($("filter").value) {
|
|
1134
|
+
$("filter").value = "";
|
|
1135
|
+
renderList();
|
|
1136
|
+
}
|
|
1137
|
+
if (inFilter) $("filter").blur();
|
|
1138
|
+
} else if (ev.key === "/" && !inFilter) {
|
|
1139
|
+
ev.preventDefault();
|
|
1140
|
+
$("filter").focus();
|
|
1141
|
+
} else if ((ev.key === "r" || ev.key === "R") && !inFilter && !ev.repeat) {
|
|
1142
|
+
replay();
|
|
1143
|
+
}
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
loadList();
|
|
1147
|
+
connect();
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
init();
|
|
1151
|
+
`;
|
|
1152
|
+
function inspectorPage(nonce) {
|
|
1153
|
+
const tabs = ["Corpo", "Cabe\xE7alhos", "Resposta", "Raw"].map((t, i) => `<button type="button" class="tab${i === 0 ? " on" : ""}" role="tab" data-tab="${t}">${t}</button>`).join("");
|
|
1154
|
+
return `<!doctype html>
|
|
1155
|
+
<html lang="pt-BR">
|
|
1156
|
+
<head>
|
|
1157
|
+
<meta charset="utf-8">
|
|
1158
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1159
|
+
<title>duto \xB7 inspetor</title>
|
|
1160
|
+
<link rel="icon" href="data:,">
|
|
1161
|
+
<style nonce="${nonce}">${STYLE}</style>
|
|
1162
|
+
</head>
|
|
1163
|
+
<body>
|
|
1164
|
+
<div class="app">
|
|
1165
|
+
<div class="top"><span><b>\u25B0 duto</b> \xB7 inspetor \xB7 <span id="count">0 requisi\xE7\xF5es</span></span><span class="tunnels" id="tunnels"></span></div>
|
|
1166
|
+
<div class="banner" id="banner" role="status" hidden></div>
|
|
1167
|
+
<div class="main">
|
|
1168
|
+
<div class="side">
|
|
1169
|
+
<input class="search" id="filter" type="search" placeholder="filtrar\u2026" aria-label="Filtrar requisi\xE7\xF5es" autocomplete="off" spellcheck="false">
|
|
1170
|
+
<div class="list" id="list" role="listbox" aria-label="Requisi\xE7\xF5es"></div>
|
|
1171
|
+
</div>
|
|
1172
|
+
<div class="detail">
|
|
1173
|
+
<div class="empty" id="none">Escolha uma requisi\xE7\xE3o na lista.</div>
|
|
1174
|
+
<div class="detail pane" id="detail" hidden>
|
|
1175
|
+
<div class="head" id="head"></div>
|
|
1176
|
+
<div class="tabs" role="tablist">${tabs}</div>
|
|
1177
|
+
<div class="content" id="content"></div>
|
|
1178
|
+
<div class="btns">
|
|
1179
|
+
<span><button type="button" class="btn pri" id="replay">\u21BB Reenviar</button></span>
|
|
1180
|
+
<span><button type="button" class="btn" id="copyBody">Copiar corpo</button></span>
|
|
1181
|
+
<span><button type="button" class="btn" id="copyCurl">Copiar como curl</button></span>
|
|
1182
|
+
<span class="msg" id="curlNote"></span>
|
|
1183
|
+
<span class="msg" id="msg" role="status" hidden></span>
|
|
1184
|
+
</div>
|
|
1185
|
+
</div>
|
|
1186
|
+
</div>
|
|
1187
|
+
</div>
|
|
1188
|
+
</div>
|
|
1189
|
+
<script nonce="${nonce}">${SCRIPT}</script>
|
|
1190
|
+
</body>
|
|
1191
|
+
</html>
|
|
1192
|
+
`;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// src/inspector/server.ts
|
|
1196
|
+
import { createHash as createHash2, randomBytes, timingSafeEqual } from "crypto";
|
|
1197
|
+
import { createServer } from "http";
|
|
1198
|
+
|
|
1199
|
+
// src/session/replay.ts
|
|
1200
|
+
var tunnelClosedReason = (tunnel2) => `O t\xFAnel ${tunnel2} n\xE3o est\xE1 aberto nesta sess\xE3o.`;
|
|
1201
|
+
function replayBlock(r) {
|
|
1202
|
+
if (!r) return "Essa requisi\xE7\xE3o j\xE1 saiu da lista.";
|
|
1203
|
+
if (r.websocket) return "WebSocket n\xE3o d\xE1 para reenviar.";
|
|
1204
|
+
if (r.noReplay) return r.noReplay;
|
|
1205
|
+
if (r.access?.kind === "denied") return "Barrada pela senha do t\xFAnel; n\xE3o d\xE1 para reenviar.";
|
|
1206
|
+
if (!r.reqComplete) return "A requisi\xE7\xE3o ainda n\xE3o chegou inteira.";
|
|
1207
|
+
if (r.reqTruncated) return "Corpo acima de 64 KB; n\xE3o d\xE1 para reenviar.";
|
|
1208
|
+
if (r.reqBodyBytes > 0 && r.reqRaw === null) return "Corpo bin\xE1rio n\xE3o \xE9 guardado; n\xE3o d\xE1 para reenviar.";
|
|
1209
|
+
return null;
|
|
1210
|
+
}
|
|
1211
|
+
var DROP = /* @__PURE__ */ new Set(["connection", "keep-alive", "transfer-encoding", "expect", "content-length", "upgrade", "te", "trailer", "proxy-connection"]);
|
|
1212
|
+
function replayHeaders(r) {
|
|
1213
|
+
const out = {};
|
|
1214
|
+
for (const [k, v] of Object.entries(r.reqHeaders)) if (!DROP.has(k.toLowerCase())) out[k] = v;
|
|
1215
|
+
const hadLength = Object.keys(r.reqHeaders).some((k) => k.toLowerCase() === "content-length");
|
|
1216
|
+
if (r.reqBodyBytes > 0 || hadLength) out["content-length"] = String(r.reqRaw?.length ?? 0);
|
|
1217
|
+
return out;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// src/inspector/curl.ts
|
|
1221
|
+
var SKIP = /* @__PURE__ */ new Set(["host", "content-length", "transfer-encoding", "connection", "keep-alive", "expect", "upgrade", "te", "trailer", "proxy-connection"]);
|
|
1222
|
+
var TUNNEL_ADDED = /* @__PURE__ */ new Set(["cdn-loop", "x-forwarded-for", "x-forwarded-proto", "x-real-ip", "true-client-ip"]);
|
|
1223
|
+
var skip = (name) => {
|
|
1224
|
+
const k = name.toLowerCase();
|
|
1225
|
+
return SKIP.has(k) || TUNNEL_ADDED.has(k) || k.startsWith("cf-");
|
|
1226
|
+
};
|
|
1227
|
+
function shellQuote(s) {
|
|
1228
|
+
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
1229
|
+
}
|
|
1230
|
+
function toCurl(r, o) {
|
|
1231
|
+
const parts = [`curl -X ${shellQuote(r.method)} ${shellQuote(o.publicUrl.replace(/\/+$/, "") + r.path)}`];
|
|
1232
|
+
for (const [k, v] of Object.entries(r.reqHeaders)) {
|
|
1233
|
+
if (!skip(k)) parts.push(`-H ${shellQuote(`${k}: ${v}`)}`);
|
|
1234
|
+
}
|
|
1235
|
+
if (o.authUser !== null) parts.push(`-H ${shellQuote(`X-Duto-Auth: ${o.authUser}:SENHA`)}`);
|
|
1236
|
+
if (r.reqBody !== null) parts.push(`--data-binary ${shellQuote(r.reqBody)}`);
|
|
1237
|
+
return parts.join(" \\\n ");
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// src/inspector/server.ts
|
|
1241
|
+
var DEFAULT_PORT = 4141;
|
|
1242
|
+
var PORT_TRIES = 20;
|
|
1243
|
+
var NO_TOKEN = "Abra o inspetor pela tecla i da tela do duto ou pelo endere\xE7o impresso no terminal.";
|
|
1244
|
+
var GONE = "Essa requisi\xE7\xE3o j\xE1 saiu da lista.";
|
|
1245
|
+
var NO_URL = "O t\xFAnel ainda n\xE3o tem endere\xE7o p\xFAblico.";
|
|
1246
|
+
var CURL_BYTES = "O corpo do curl pode n\xE3o ser id\xEAntico ao recebido (bytes fora de UTF-8).";
|
|
1247
|
+
var sha = (s) => createHash2("sha256").update(s).digest();
|
|
1248
|
+
var same = (a, b) => timingSafeEqual(sha(a), sha(b));
|
|
1249
|
+
var first = (v) => Array.isArray(v) ? v[0] : v;
|
|
1250
|
+
function listItem(r) {
|
|
1251
|
+
return {
|
|
1252
|
+
id: r.id,
|
|
1253
|
+
tunnel: r.tunnel,
|
|
1254
|
+
method: r.method,
|
|
1255
|
+
path: r.path,
|
|
1256
|
+
status: r.status,
|
|
1257
|
+
durationMs: r.durationMs,
|
|
1258
|
+
bytes: r.bytes,
|
|
1259
|
+
startedAt: r.startedAt,
|
|
1260
|
+
websocket: r.websocket,
|
|
1261
|
+
...r.replayOf !== void 0 && { replayOf: r.replayOf },
|
|
1262
|
+
...r.error !== void 0 && { error: r.error }
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
function detail(r, session) {
|
|
1266
|
+
const tunnel2 = session.getState().tunnels.find((t) => t.key === r.tunnel);
|
|
1267
|
+
const tunnelOpen = !!tunnel2 && (tunnel2.status === "live" || tunnel2.status === "no-listener");
|
|
1268
|
+
let curl = null;
|
|
1269
|
+
let curlNote = null;
|
|
1270
|
+
if (!tunnel2?.url) curlNote = NO_URL;
|
|
1271
|
+
else {
|
|
1272
|
+
curl = toCurl(r, { publicUrl: tunnel2.url, authUser: session.authUser(r.tunnel) });
|
|
1273
|
+
if (r.reqRaw && !Buffer.from(r.reqBody ?? "", "utf8").equals(r.reqRaw)) curlNote = CURL_BYTES;
|
|
1274
|
+
}
|
|
1275
|
+
return {
|
|
1276
|
+
...listItem(r),
|
|
1277
|
+
reqHeaders: r.reqHeaders,
|
|
1278
|
+
resHeaders: r.resHeaders,
|
|
1279
|
+
reqBody: r.reqBody,
|
|
1280
|
+
resBody: r.resBody,
|
|
1281
|
+
reqNote: bodyNote(r, "req"),
|
|
1282
|
+
resNote: bodyNote(r, "res"),
|
|
1283
|
+
reqTruncatedNote: r.reqTruncated ? TRUNCATED_NOTE : null,
|
|
1284
|
+
resTruncatedNote: r.resTruncated ? TRUNCATED_NOTE : null,
|
|
1285
|
+
replayBlocked: replayBlock(r) ?? (tunnelOpen ? null : tunnelClosedReason(r.tunnel)),
|
|
1286
|
+
curl,
|
|
1287
|
+
curlNote
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
var sig = (r) => `${r.status}|${r.durationMs}|${r.bytes}|${r.resBody?.length ?? -1}|${r.reqComplete}|${r.error ?? ""}|${Object.keys(r.resHeaders).length > 0}`;
|
|
1291
|
+
async function startInspector(o) {
|
|
1292
|
+
const { session } = o;
|
|
1293
|
+
const eventMs = o.eventMs ?? 150;
|
|
1294
|
+
const pingMs = o.pingMs ?? 25e3;
|
|
1295
|
+
const token = randomBytes(24).toString("base64url");
|
|
1296
|
+
let port = 0;
|
|
1297
|
+
const clients = /* @__PURE__ */ new Set();
|
|
1298
|
+
let seen = /* @__PURE__ */ new Map();
|
|
1299
|
+
let timer = null;
|
|
1300
|
+
const prime = () => seen = new Map(session.records.list().map((r) => [r.id, sig(r)]));
|
|
1301
|
+
const flush = () => {
|
|
1302
|
+
timer = null;
|
|
1303
|
+
const next = /* @__PURE__ */ new Map();
|
|
1304
|
+
const ids = [];
|
|
1305
|
+
for (const r of session.records.list()) {
|
|
1306
|
+
const s = sig(r);
|
|
1307
|
+
next.set(r.id, s);
|
|
1308
|
+
if (seen.get(r.id) !== s) ids.push(r.id);
|
|
1309
|
+
}
|
|
1310
|
+
seen = next;
|
|
1311
|
+
const msg = `event: mudou
|
|
1312
|
+
data: ${JSON.stringify({ ids })}
|
|
1313
|
+
|
|
1314
|
+
`;
|
|
1315
|
+
for (const c of clients) c.write(msg);
|
|
1316
|
+
};
|
|
1317
|
+
const schedule = () => {
|
|
1318
|
+
if (clients.size > 0 && !timer) timer = setTimeout(flush, eventMs);
|
|
1319
|
+
};
|
|
1320
|
+
const unsubs = [session.records.subscribe(schedule), session.subscribe(schedule)];
|
|
1321
|
+
const pinger = setInterval(() => {
|
|
1322
|
+
for (const c of clients) c.write(": ping\n\n");
|
|
1323
|
+
}, pingMs);
|
|
1324
|
+
pinger.unref();
|
|
1325
|
+
const base = (res) => {
|
|
1326
|
+
res.setHeader("Cache-Control", "no-store");
|
|
1327
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
1328
|
+
res.setHeader("Referrer-Policy", "no-referrer");
|
|
1329
|
+
res.setHeader("X-Frame-Options", "DENY");
|
|
1330
|
+
};
|
|
1331
|
+
const text = (res, status, body) => {
|
|
1332
|
+
res.writeHead(status, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1333
|
+
res.end(body);
|
|
1334
|
+
};
|
|
1335
|
+
const json = (res, status, body) => {
|
|
1336
|
+
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
1337
|
+
res.end(JSON.stringify(body));
|
|
1338
|
+
};
|
|
1339
|
+
const handle = (req, res) => {
|
|
1340
|
+
base(res);
|
|
1341
|
+
const host = first(req.headers.host)?.toLowerCase();
|
|
1342
|
+
if (host !== `127.0.0.1:${port}` && host !== `localhost:${port}`) return text(res, 403, "Host n\xE3o permitido.");
|
|
1343
|
+
let url2;
|
|
1344
|
+
try {
|
|
1345
|
+
url2 = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
|
|
1346
|
+
} catch {
|
|
1347
|
+
return text(res, 404, "N\xE3o encontrado.");
|
|
1348
|
+
}
|
|
1349
|
+
const path = url2.pathname;
|
|
1350
|
+
const method = req.method ?? "GET";
|
|
1351
|
+
const queryToken = url2.searchParams.get("t") ?? "";
|
|
1352
|
+
const headerToken = first(req.headers["x-duto-inspetor"]) ?? "";
|
|
1353
|
+
if (method === "GET" && path === "/") {
|
|
1354
|
+
if (!same(queryToken, token)) return text(res, 403, NO_TOKEN);
|
|
1355
|
+
const nonce = randomBytes(16).toString("base64url");
|
|
1356
|
+
res.writeHead(200, {
|
|
1357
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1358
|
+
"Content-Security-Policy": `default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'`
|
|
1359
|
+
});
|
|
1360
|
+
return void res.end(o.page(nonce));
|
|
1361
|
+
}
|
|
1362
|
+
const isEvents = method === "GET" && path === "/api/eventos";
|
|
1363
|
+
const detailId = /^\/api\/requisicoes\/([1-9]\d{0,15})(\/reenviar)?$/.exec(path);
|
|
1364
|
+
const known = method === "GET" && (path === "/api/sessao" || path === "/api/requisicoes" || detailId && !detailId[2]) || isEvents || method === "POST" && detailId && detailId[2];
|
|
1365
|
+
if (!known) return text(res, 404, "N\xE3o encontrado.");
|
|
1366
|
+
if (!same(headerToken, token) && !(isEvents && same(queryToken, token))) return json(res, 403, { error: NO_TOKEN });
|
|
1367
|
+
if (isEvents) {
|
|
1368
|
+
res.writeHead(200, { "Content-Type": "text/event-stream; charset=utf-8", Connection: "keep-alive" });
|
|
1369
|
+
res.write(": duto\n\n");
|
|
1370
|
+
if (clients.size === 0) prime();
|
|
1371
|
+
clients.add(res);
|
|
1372
|
+
res.on("close", () => clients.delete(res));
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
if (path === "/api/sessao") {
|
|
1376
|
+
return json(res, 200, { tunnels: session.getState().tunnels.map((t) => ({ key: t.key, url: t.url, auth: !!t.auth })) });
|
|
1377
|
+
}
|
|
1378
|
+
if (path === "/api/requisicoes") return json(res, 200, { items: session.records.list().map(listItem) });
|
|
1379
|
+
const id = Number(detailId[1]);
|
|
1380
|
+
if (!Number.isSafeInteger(id)) return json(res, 404, { error: GONE });
|
|
1381
|
+
if (method === "POST") return json(res, 200, session.replay(id));
|
|
1382
|
+
const r = session.records.get(id);
|
|
1383
|
+
if (!r) return json(res, 404, { error: GONE });
|
|
1384
|
+
return json(res, 200, detail(r, session));
|
|
1385
|
+
};
|
|
1386
|
+
const server = createServer((req, res) => {
|
|
1387
|
+
try {
|
|
1388
|
+
handle(req, res);
|
|
1389
|
+
} catch {
|
|
1390
|
+
if (!res.headersSent) text(res, 500, "Erro interno.");
|
|
1391
|
+
else res.end();
|
|
1392
|
+
}
|
|
1393
|
+
});
|
|
1394
|
+
const listen = (p) => new Promise((resolve, reject) => {
|
|
1395
|
+
const onError = (e) => reject(e);
|
|
1396
|
+
server.once("error", onError);
|
|
1397
|
+
server.listen(p, "127.0.0.1", () => {
|
|
1398
|
+
server.off("error", onError);
|
|
1399
|
+
resolve();
|
|
1400
|
+
});
|
|
1401
|
+
});
|
|
1402
|
+
const want = o.port ?? DEFAULT_PORT;
|
|
1403
|
+
try {
|
|
1404
|
+
let bound = false;
|
|
1405
|
+
if (want !== 0) {
|
|
1406
|
+
for (let p = want; p <= want + PORT_TRIES && !bound; p++) {
|
|
1407
|
+
try {
|
|
1408
|
+
await listen(p);
|
|
1409
|
+
bound = true;
|
|
1410
|
+
} catch (e) {
|
|
1411
|
+
if (e.code !== "EADDRINUSE") throw e;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
if (!bound) await listen(0);
|
|
1416
|
+
} catch (e) {
|
|
1417
|
+
for (const u of unsubs) u();
|
|
1418
|
+
clearInterval(pinger);
|
|
1419
|
+
throw e;
|
|
1420
|
+
}
|
|
1421
|
+
port = server.address().port;
|
|
1422
|
+
const url = `http://127.0.0.1:${port}`;
|
|
1423
|
+
return {
|
|
1424
|
+
port,
|
|
1425
|
+
url,
|
|
1426
|
+
openUrl: `${url}/?t=${token}`,
|
|
1427
|
+
close: () => new Promise((resolve) => {
|
|
1428
|
+
for (const u of unsubs) u();
|
|
1429
|
+
clearInterval(pinger);
|
|
1430
|
+
if (timer) clearTimeout(timer);
|
|
1431
|
+
for (const c of clients) c.end();
|
|
1432
|
+
clients.clear();
|
|
1433
|
+
server.close(() => resolve());
|
|
1434
|
+
server.closeAllConnections();
|
|
1435
|
+
})
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
|
|
520
1439
|
// src/session/session.ts
|
|
521
1440
|
import { spawn } from "child_process";
|
|
522
1441
|
|
|
@@ -540,11 +1459,11 @@ async function isPortListening(port, timeoutMs = 500) {
|
|
|
540
1459
|
}
|
|
541
1460
|
|
|
542
1461
|
// src/session/auth.ts
|
|
543
|
-
import { createHash as
|
|
1462
|
+
import { createHash as createHash3, createHmac, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
544
1463
|
var TUNNEL_COOKIE = "__Host-duto_tunel";
|
|
545
1464
|
var QR_TTL_MS = 5 * 6e4;
|
|
546
|
-
var
|
|
547
|
-
var
|
|
1465
|
+
var sha2 = (s) => createHash3("sha256").update(s).digest();
|
|
1466
|
+
var same2 = (a, b) => timingSafeEqual2(sha2(a), sha2(b));
|
|
548
1467
|
function splitPair(raw) {
|
|
549
1468
|
const i = raw.indexOf(":");
|
|
550
1469
|
return i < 0 ? null : { user: raw.slice(0, i), password: raw.slice(i + 1) };
|
|
@@ -553,11 +1472,11 @@ function parseBasic(value) {
|
|
|
553
1472
|
const m = /^basic +([A-Za-z0-9+/]+={0,2})$/i.exec(value?.trim() ?? "");
|
|
554
1473
|
return m ? splitPair(Buffer.from(m[1], "base64").toString("utf8")) : null;
|
|
555
1474
|
}
|
|
556
|
-
var
|
|
1475
|
+
var first2 = (v) => Array.isArray(v) ? v[0] : v;
|
|
557
1476
|
var bareHost = (s) => s.replace(/:\d+$/, "").toLowerCase();
|
|
558
1477
|
function sameHostOrigin(h) {
|
|
559
|
-
if (
|
|
560
|
-
const origin =
|
|
1478
|
+
if (first2(h["sec-fetch-site"])?.toLowerCase() === "same-site") return false;
|
|
1479
|
+
const origin = first2(h.origin);
|
|
561
1480
|
if (origin === void 0) return true;
|
|
562
1481
|
let host;
|
|
563
1482
|
try {
|
|
@@ -580,27 +1499,27 @@ var TunnelAuth = class {
|
|
|
580
1499
|
this.credentials = o.credentials;
|
|
581
1500
|
this.allow = o.allow;
|
|
582
1501
|
this.now = o.now ?? Date.now;
|
|
583
|
-
this.secret = o.secret ??
|
|
1502
|
+
this.secret = o.secret ?? randomBytes2(32);
|
|
584
1503
|
this.qrUser = o.credentials[0]?.user ?? "";
|
|
585
1504
|
}
|
|
586
1505
|
access(h) {
|
|
587
1506
|
if (this.allow) {
|
|
588
|
-
const ip =
|
|
1507
|
+
const ip = first2(h["cf-connecting-ip"]);
|
|
589
1508
|
const reason = this.allow.match(ip);
|
|
590
1509
|
if (reason) return { kind: "allow", reason };
|
|
591
1510
|
}
|
|
592
1511
|
const cookieUser = this.cookieUser(h.cookie);
|
|
593
1512
|
if (cookieUser && sameHostOrigin(h)) return { kind: "user", user: cookieUser };
|
|
594
|
-
const duto =
|
|
595
|
-
const cand = duto !== void 0 ? splitPair(duto) : parseBasic(
|
|
1513
|
+
const duto = first2(h["x-duto-auth"]);
|
|
1514
|
+
const cand = duto !== void 0 ? splitPair(duto) : parseBasic(first2(h.authorization));
|
|
596
1515
|
if (cand && this.checkPassword(cand.user, cand.password)) return { kind: "user", user: cand.user };
|
|
597
1516
|
return { kind: "denied" };
|
|
598
1517
|
}
|
|
599
1518
|
checkPassword(user, password) {
|
|
600
1519
|
let ok2 = false;
|
|
601
1520
|
for (const c of this.credentials) {
|
|
602
|
-
const userOk =
|
|
603
|
-
const passOk =
|
|
1521
|
+
const userOk = same2(c.user, user);
|
|
1522
|
+
const passOk = same2(c.password, password);
|
|
604
1523
|
if (userOk && passOk) ok2 = true;
|
|
605
1524
|
}
|
|
606
1525
|
return ok2;
|
|
@@ -612,19 +1531,19 @@ var TunnelAuth = class {
|
|
|
612
1531
|
return `${TUNNEL_COOKIE}=; Max-Age=0; Secure; HttpOnly; SameSite=Lax; Path=/`;
|
|
613
1532
|
}
|
|
614
1533
|
issueQrToken() {
|
|
615
|
-
this.qr = { token:
|
|
1534
|
+
this.qr = { token: randomBytes2(24).toString("base64url"), expiresAt: this.now() + QR_TTL_MS };
|
|
616
1535
|
return { ...this.qr };
|
|
617
1536
|
}
|
|
618
1537
|
consumeQrToken(token) {
|
|
619
1538
|
const qr = this.qr;
|
|
620
|
-
if (!qr || !
|
|
1539
|
+
if (!qr || !same2(qr.token, token)) return false;
|
|
621
1540
|
this.qr = null;
|
|
622
1541
|
return this.now() <= qr.expiresAt;
|
|
623
1542
|
}
|
|
624
1543
|
strip(h) {
|
|
625
1544
|
const out = { ...h };
|
|
626
1545
|
delete out["x-duto-auth"];
|
|
627
|
-
const basic = parseBasic(
|
|
1546
|
+
const basic = parseBasic(first2(h.authorization));
|
|
628
1547
|
if (basic && this.checkPassword(basic.user, basic.password)) delete out.authorization;
|
|
629
1548
|
if (h.cookie !== void 0) {
|
|
630
1549
|
const parts = (Array.isArray(h.cookie) ? h.cookie.join("; ") : h.cookie).split(";").map((p) => p.trim()).filter(Boolean);
|
|
@@ -648,10 +1567,10 @@ var TunnelAuth = class {
|
|
|
648
1567
|
for (const p of list) {
|
|
649
1568
|
const item = p.trim();
|
|
650
1569
|
if (!item.startsWith(`${TUNNEL_COOKIE}=`)) continue;
|
|
651
|
-
const [b64,
|
|
652
|
-
if (!b64 || !
|
|
1570
|
+
const [b64, sig2, ...resto] = item.slice(TUNNEL_COOKIE.length + 1).split(".");
|
|
1571
|
+
if (!b64 || !sig2 || resto.length) continue;
|
|
653
1572
|
const user = Buffer.from(b64, "base64url").toString("utf8");
|
|
654
|
-
if (
|
|
1573
|
+
if (same2(sig2, this.sign(user)) && this.credentials.some((c) => c.user === user)) return user;
|
|
655
1574
|
}
|
|
656
1575
|
return null;
|
|
657
1576
|
}
|
|
@@ -682,7 +1601,7 @@ function cors401(origin) {
|
|
|
682
1601
|
}
|
|
683
1602
|
|
|
684
1603
|
// src/session/proxy.ts
|
|
685
|
-
import { Agent, createServer, request } from "http";
|
|
1604
|
+
import { Agent, createServer as createServer2, request } from "http";
|
|
686
1605
|
import { connect as connect2 } from "net";
|
|
687
1606
|
|
|
688
1607
|
// src/session/errorPage.ts
|
|
@@ -788,7 +1707,9 @@ ${o.error ? `<p class="error" role="alert">Usu\xE1rio ou senha incorretos.</p>
|
|
|
788
1707
|
}
|
|
789
1708
|
|
|
790
1709
|
// src/session/proxy.ts
|
|
791
|
-
var
|
|
1710
|
+
var DENIED_NO_REPLAY = "Barrada pela senha do t\xFAnel; n\xE3o d\xE1 para reenviar.";
|
|
1711
|
+
var PREFLIGHT_NO_REPLAY = "Preflight de CORS n\xE3o d\xE1 para reenviar.";
|
|
1712
|
+
var TEXT2 = /^(text\/|application\/(json|xml|javascript|x-www-form-urlencoded|graphql)|[^;]*\+(json|xml))/i;
|
|
792
1713
|
var MAX_BODY = 64 * 1024;
|
|
793
1714
|
var LOCAL_AGENT = new Agent({ keepAlive: false });
|
|
794
1715
|
function flat(h) {
|
|
@@ -805,8 +1726,11 @@ var Capture = class {
|
|
|
805
1726
|
max;
|
|
806
1727
|
chunks = [];
|
|
807
1728
|
size = 0;
|
|
1729
|
+
/** Bytes vistos, capturados ou não. */
|
|
1730
|
+
total = 0;
|
|
808
1731
|
truncated = false;
|
|
809
1732
|
push(c) {
|
|
1733
|
+
this.total += c.length;
|
|
810
1734
|
if (!this.enabled) return;
|
|
811
1735
|
const room = this.max - this.size;
|
|
812
1736
|
if (room <= 0) {
|
|
@@ -818,6 +1742,9 @@ var Capture = class {
|
|
|
818
1742
|
this.chunks.push(part);
|
|
819
1743
|
this.size += part.length;
|
|
820
1744
|
}
|
|
1745
|
+
raw() {
|
|
1746
|
+
return this.enabled && this.size > 0 ? Buffer.concat(this.chunks) : null;
|
|
1747
|
+
}
|
|
821
1748
|
text() {
|
|
822
1749
|
return this.enabled && this.size > 0 ? Buffer.concat(this.chunks).toString("utf8") : null;
|
|
823
1750
|
}
|
|
@@ -986,7 +1913,7 @@ async function startProxy(o) {
|
|
|
986
1913
|
await new Promise((r) => setTimeout(r, 50));
|
|
987
1914
|
}
|
|
988
1915
|
}
|
|
989
|
-
function record(req, route, headers, websocket, access) {
|
|
1916
|
+
function record(req, route, headers, websocket, access, noReplay) {
|
|
990
1917
|
const id = nextId++;
|
|
991
1918
|
const started = Date.now();
|
|
992
1919
|
o.store.add({
|
|
@@ -1003,12 +1930,78 @@ async function startProxy(o) {
|
|
|
1003
1930
|
resHeaders: {},
|
|
1004
1931
|
reqBody: null,
|
|
1005
1932
|
resBody: null,
|
|
1933
|
+
reqRaw: null,
|
|
1934
|
+
reqBodyBytes: 0,
|
|
1935
|
+
reqTruncated: false,
|
|
1936
|
+
reqComplete: false,
|
|
1006
1937
|
truncated: false,
|
|
1007
|
-
|
|
1938
|
+
resTruncated: false,
|
|
1939
|
+
...access && { access },
|
|
1940
|
+
...noReplay && { noReplay }
|
|
1008
1941
|
});
|
|
1009
1942
|
return { id, started };
|
|
1010
1943
|
}
|
|
1011
|
-
|
|
1944
|
+
function trackResponse(id, started, up, reqCap) {
|
|
1945
|
+
const resCap = new Capture(TEXT2.test(up.headers["content-type"] ?? ""), max);
|
|
1946
|
+
let bytes = 0;
|
|
1947
|
+
up.on("data", (c) => {
|
|
1948
|
+
bytes += c.length;
|
|
1949
|
+
resCap.push(c);
|
|
1950
|
+
});
|
|
1951
|
+
up.on(
|
|
1952
|
+
"end",
|
|
1953
|
+
() => o.store.update(id, {
|
|
1954
|
+
status: up.statusCode ?? null,
|
|
1955
|
+
durationMs: Date.now() - started,
|
|
1956
|
+
bytes,
|
|
1957
|
+
resHeaders: flat(up.headers),
|
|
1958
|
+
resBody: resCap.text(),
|
|
1959
|
+
truncated: !!reqCap?.truncated || resCap.truncated,
|
|
1960
|
+
resTruncated: resCap.truncated,
|
|
1961
|
+
...reqCap && { reqBody: reqCap.text(), reqRaw: reqCap.truncated ? null : reqCap.raw(), reqBodyBytes: reqCap.total, reqTruncated: reqCap.truncated }
|
|
1962
|
+
})
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
function replay(id) {
|
|
1966
|
+
const r = o.store.get(id);
|
|
1967
|
+
const blocked = replayBlock(r);
|
|
1968
|
+
if (blocked || !r) return { ok: false, reason: blocked ?? "Essa requisi\xE7\xE3o j\xE1 saiu da lista." };
|
|
1969
|
+
const route = [...o.routes.values()].find((x) => x.tunnel === r.tunnel);
|
|
1970
|
+
if (!route) return { ok: false, reason: tunnelClosedReason(r.tunnel) };
|
|
1971
|
+
const newId = nextId++;
|
|
1972
|
+
const started = Date.now();
|
|
1973
|
+
const headers = replayHeaders(r);
|
|
1974
|
+
o.store.add({
|
|
1975
|
+
id: newId,
|
|
1976
|
+
tunnel: r.tunnel,
|
|
1977
|
+
method: r.method,
|
|
1978
|
+
path: r.path,
|
|
1979
|
+
status: null,
|
|
1980
|
+
durationMs: null,
|
|
1981
|
+
bytes: 0,
|
|
1982
|
+
startedAt: started,
|
|
1983
|
+
websocket: false,
|
|
1984
|
+
reqHeaders: headers,
|
|
1985
|
+
resHeaders: {},
|
|
1986
|
+
reqBody: r.reqBody,
|
|
1987
|
+
resBody: null,
|
|
1988
|
+
reqRaw: r.reqRaw,
|
|
1989
|
+
reqBodyBytes: r.reqBodyBytes,
|
|
1990
|
+
reqTruncated: false,
|
|
1991
|
+
reqComplete: true,
|
|
1992
|
+
truncated: false,
|
|
1993
|
+
resTruncated: false,
|
|
1994
|
+
replayOf: r.id
|
|
1995
|
+
});
|
|
1996
|
+
void (async () => {
|
|
1997
|
+
const host = await localHost(route.port);
|
|
1998
|
+
const upstream = request({ host, port: route.port, agent: LOCAL_AGENT, method: r.method, path: r.path, headers }, (up) => trackResponse(newId, started, up));
|
|
1999
|
+
upstream.on("error", (err) => o.store.update(newId, { status: 503, durationMs: Date.now() - started, error: err.message || err.code || err.name }));
|
|
2000
|
+
upstream.end(r.reqRaw ?? void 0);
|
|
2001
|
+
})();
|
|
2002
|
+
return { ok: true, id: newId };
|
|
2003
|
+
}
|
|
2004
|
+
const server = createServer2(async (req, res) => {
|
|
1012
2005
|
req.pause();
|
|
1013
2006
|
const route = await routeFor(req);
|
|
1014
2007
|
if (!route) {
|
|
@@ -1031,13 +2024,13 @@ async function startProxy(o) {
|
|
|
1031
2024
|
const headers = delivered(req.headers, route);
|
|
1032
2025
|
if (access?.kind === "denied") {
|
|
1033
2026
|
const html = wantsHtml(req.headers.accept);
|
|
1034
|
-
const { id: id2, started: started2 } = record(req, route, headers, false, html ? void 0 : access);
|
|
2027
|
+
const { id: id2, started: started2 } = record(req, route, headers, false, html ? void 0 : access, DENIED_NO_REPLAY);
|
|
1035
2028
|
const { status, out, body } = deny(req, res, html);
|
|
1036
2029
|
o.store.update(id2, { status, durationMs: Date.now() - started2, bytes: Buffer.byteLength(body), resHeaders: out });
|
|
1037
2030
|
return;
|
|
1038
2031
|
}
|
|
1039
|
-
const { id, started } = record(req, route, headers, false, access);
|
|
1040
|
-
const reqCap = new Capture(
|
|
2032
|
+
const { id, started } = record(req, route, headers, false, access, preflight ? PREFLIGHT_NO_REPLAY : void 0);
|
|
2033
|
+
const reqCap = new Capture(TEXT2.test(req.headers["content-type"] ?? ""), max);
|
|
1041
2034
|
req.pause();
|
|
1042
2035
|
const host = await localHost(route.port);
|
|
1043
2036
|
const upstream = request(
|
|
@@ -1051,35 +2044,19 @@ async function startProxy(o) {
|
|
|
1051
2044
|
up.resume();
|
|
1052
2045
|
return;
|
|
1053
2046
|
}
|
|
1054
|
-
const resCap = new Capture(TEXT.test(up.headers["content-type"] ?? ""), max);
|
|
1055
|
-
let bytes = 0;
|
|
1056
2047
|
if (route.host && route.rewriteRedirects !== false && typeof up.headers.location === "string") {
|
|
1057
2048
|
up.headers.location = rewriteLocation(up.headers.location, route.host, hostOf(req));
|
|
1058
2049
|
}
|
|
1059
2050
|
res.writeHead(up.statusCode ?? 502, up.statusMessage, up.headers);
|
|
1060
|
-
|
|
1061
|
-
bytes += c.length;
|
|
1062
|
-
resCap.push(c);
|
|
1063
|
-
});
|
|
2051
|
+
trackResponse(id, started, up, reqCap);
|
|
1064
2052
|
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
2053
|
}
|
|
1078
2054
|
);
|
|
1079
2055
|
res.on("close", () => {
|
|
1080
2056
|
if (!res.writableFinished) upstream.destroy();
|
|
1081
2057
|
});
|
|
1082
2058
|
upstream.on("error", (err) => {
|
|
2059
|
+
req.resume();
|
|
1083
2060
|
if (res.destroyed && res.writableFinished === false && !res.headersSent) return;
|
|
1084
2061
|
const html = wantsHtml(req.headers.accept);
|
|
1085
2062
|
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 +2066,7 @@ async function startProxy(o) {
|
|
|
1089
2066
|
} else res.destroy();
|
|
1090
2067
|
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
2068
|
});
|
|
2069
|
+
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
2070
|
if (preflight) {
|
|
1093
2071
|
req.resume();
|
|
1094
2072
|
upstream.end();
|
|
@@ -1103,7 +2081,7 @@ async function startProxy(o) {
|
|
|
1103
2081
|
if (!route) return void client.end("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
|
|
1104
2082
|
const access = route.auth?.access(req.headers);
|
|
1105
2083
|
const headers = delivered(req.headers, route);
|
|
1106
|
-
const { id, started } = record(req, route, headers, true, access);
|
|
2084
|
+
const { id, started } = record(req, route, headers, true, access, access?.kind === "denied" ? DENIED_NO_REPLAY : void 0);
|
|
1107
2085
|
if (access?.kind === "denied") {
|
|
1108
2086
|
client.end(WS_DENIED);
|
|
1109
2087
|
o.store.update(id, { status: 401, durationMs: Date.now() - started });
|
|
@@ -1122,8 +2100,8 @@ async function startProxy(o) {
|
|
|
1122
2100
|
target.write(`${raw}\r
|
|
1123
2101
|
`);
|
|
1124
2102
|
if (head.length) target.write(head);
|
|
1125
|
-
target.once("data", (
|
|
1126
|
-
const status = Number(/^HTTP\/\d\.\d (\d{3})/.exec(
|
|
2103
|
+
target.once("data", (first3) => {
|
|
2104
|
+
const status = Number(/^HTTP\/\d\.\d (\d{3})/.exec(first3.toString("latin1"))?.[1] ?? 0) || null;
|
|
1127
2105
|
o.store.update(id, { status, durationMs: Date.now() - started });
|
|
1128
2106
|
});
|
|
1129
2107
|
target.pipe(client);
|
|
@@ -1146,6 +2124,7 @@ async function startProxy(o) {
|
|
|
1146
2124
|
});
|
|
1147
2125
|
return {
|
|
1148
2126
|
port: server.address().port,
|
|
2127
|
+
replay,
|
|
1149
2128
|
close: () => new Promise((resolve) => {
|
|
1150
2129
|
for (const s of sockets) s.destroy();
|
|
1151
2130
|
server.close(() => resolve());
|
|
@@ -1241,6 +2220,9 @@ var RecordStore = class {
|
|
|
1241
2220
|
Object.assign(r, patch);
|
|
1242
2221
|
this.emit();
|
|
1243
2222
|
}
|
|
2223
|
+
get(id) {
|
|
2224
|
+
return this.items.find((x) => x.id === id);
|
|
2225
|
+
}
|
|
1244
2226
|
/** Mais recentes primeiro. Mesma referência até a próxima mudança (serve de snapshot para a tela). */
|
|
1245
2227
|
list() {
|
|
1246
2228
|
this.snapshot ??= [...this.items].reverse();
|
|
@@ -1337,6 +2319,10 @@ var Session = class {
|
|
|
1337
2319
|
const { token, expiresAt } = auth.issueQrToken();
|
|
1338
2320
|
return { url: `${t.url}/__duto/qr/${token}`, expiresAt };
|
|
1339
2321
|
}
|
|
2322
|
+
/** Primeiro usuário configurado do túnel, ou null quando ele não tem senha. */
|
|
2323
|
+
authUser(key) {
|
|
2324
|
+
return this.auths.get(key)?.qrUser ?? null;
|
|
2325
|
+
}
|
|
1340
2326
|
/** Cria o TunnelAuth na primeira abertura (precisa do allow resolvido) e o mantém depois. */
|
|
1341
2327
|
async authFor(key) {
|
|
1342
2328
|
const existing = this.auths.get(key);
|
|
@@ -1348,6 +2334,10 @@ var Session = class {
|
|
|
1348
2334
|
this.auths.set(key, auth);
|
|
1349
2335
|
return auth;
|
|
1350
2336
|
}
|
|
2337
|
+
/** Reenvia uma requisição da lista ao serviço local. */
|
|
2338
|
+
replay(id) {
|
|
2339
|
+
return this.proxy ? this.proxy.replay(id) : { ok: false, reason: "Sess\xE3o ainda n\xE3o come\xE7ou." };
|
|
2340
|
+
}
|
|
1351
2341
|
async stop() {
|
|
1352
2342
|
if (this.stopped) return;
|
|
1353
2343
|
this.stopped = true;
|
|
@@ -1600,11 +2590,24 @@ async function runSession(specs, source, flags) {
|
|
|
1600
2590
|
fail2(err instanceof Error ? err.message : String(err));
|
|
1601
2591
|
}
|
|
1602
2592
|
const session = new Session({ api, deviceId: config.deviceId, deviceName: hostname(), cloudflaredPath, source, runDir: join6(dutoHome(), "run") });
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
2593
|
+
let inspector = null;
|
|
2594
|
+
let inspectorError;
|
|
2595
|
+
if (flags.inspect !== false) {
|
|
2596
|
+
try {
|
|
2597
|
+
inspector = await startInspector({ session, page: inspectorPage });
|
|
2598
|
+
} catch (err) {
|
|
2599
|
+
inspectorError = err instanceof Error ? err.message : String(err);
|
|
2600
|
+
}
|
|
1606
2601
|
}
|
|
1607
|
-
|
|
2602
|
+
if (wantsUi(flags)) {
|
|
2603
|
+
const { runFullscreen } = await import("./fullscreen-YK365XSO.js");
|
|
2604
|
+
const code = await runFullscreen(session, specs, "0.7.0", inspector, inspectorError);
|
|
2605
|
+
await inspector?.close().catch(() => void 0);
|
|
2606
|
+
process.exit(code);
|
|
2607
|
+
}
|
|
2608
|
+
console.log(`${mark()} ${paint("muted", `${"0.7.0"} \xB7 ${source} \xB7 Ctrl+C para encerrar`)}`);
|
|
2609
|
+
if (inspector) console.log(paint("muted", `inspetor: ${inspector.openUrl}`));
|
|
2610
|
+
if (inspectorError) console.log(warn(`inspetor n\xE3o subiu: ${inspectorError}`));
|
|
1608
2611
|
const stopLog = runPlain(session, (l) => console.log(l));
|
|
1609
2612
|
const stopped = new Promise((resolve) => {
|
|
1610
2613
|
const onSignal = () => {
|
|
@@ -1621,6 +2624,7 @@ async function runSession(specs, source, flags) {
|
|
|
1621
2624
|
await stopped;
|
|
1622
2625
|
stopLog();
|
|
1623
2626
|
await session.stop();
|
|
2627
|
+
await inspector?.close().catch(() => void 0);
|
|
1624
2628
|
process.exit(0);
|
|
1625
2629
|
}
|
|
1626
2630
|
async function tunnel(rawPort, flags) {
|
|
@@ -1658,8 +2662,8 @@ ${paint("signal", "Dica:")} crie um duto.yml com os t\xFAneis do projeto e rode
|
|
|
1658
2662
|
return runSession([{ name, port, ...host ? { host } : {}, ...keepRedirects ? { rewriteRedirects: false } : {}, ...auth.length ? { auth } : {}, ...allow.length ? { allow } : {} }], "linha de comando", flags);
|
|
1659
2663
|
}
|
|
1660
2664
|
var cli = cac("duto");
|
|
1661
|
-
cli.command("[port]", "Sem porta: abre os t\xFAneis do duto.yml. Com porta: abre um t\xFAnel para ela").option("--name <nome>", "Nome do endere\xE7o fixo (ex.: web \u2192 https://web-<usu\xE1rio>.duto.dev)").option("--host <host>", "Host que o servi\xE7o local recebe no lugar do p\xFAblico (ex.: meuapp.test)").option("--no-rewrite-redirects", "Com --host: n\xE3o troca o host local pelo p\xFAblico nos redirecionamentos").option("--auth <usuario:senha>", "Pede usu\xE1rio e senha para abrir o endere\xE7o (repet\xEDvel)").option("--permitir <faixa>", "Com --auth: IP, faixa (CIDR) ou nome (stripe, github) que entra sem senha (repet\xEDvel)").option("--no-ui", "Sem tela cheia: s\xF3 um log das requisi\xE7\xF5es (\xFAtil em scripts)").option("--ui", "For\xE7a a tela cheia (no Windows ela vem desligada)").action((port, opts) => tunnel(port, opts));
|
|
1662
|
-
cli.command("http <port>", "O mesmo que duto <porta> (para quem vem do ngrok)").option("--name <nome>", "Nome do endere\xE7o fixo").option("--host <host>", "Host que o servi\xE7o local recebe no lugar do p\xFAblico").option("--no-rewrite-redirects", "Com --host: mant\xE9m os redirecionamentos como o servi\xE7o mandou").option("--auth <usuario:senha>", "Pede usu\xE1rio e senha para abrir o endere\xE7o (repet\xEDvel)").option("--permitir <faixa>", "Com --auth: IP, faixa (CIDR) ou nome (stripe, github) que entra sem senha (repet\xEDvel)").option("--no-ui", "Sem tela cheia").action((port, opts) => tunnel(port, opts));
|
|
2665
|
+
cli.command("[port]", "Sem porta: abre os t\xFAneis do duto.yml. Com porta: abre um t\xFAnel para ela").option("--name <nome>", "Nome do endere\xE7o fixo (ex.: web \u2192 https://web-<usu\xE1rio>.duto.dev)").option("--host <host>", "Host que o servi\xE7o local recebe no lugar do p\xFAblico (ex.: meuapp.test)").option("--no-rewrite-redirects", "Com --host: n\xE3o troca o host local pelo p\xFAblico nos redirecionamentos").option("--auth <usuario:senha>", "Pede usu\xE1rio e senha para abrir o endere\xE7o (repet\xEDvel)").option("--permitir <faixa>", "Com --auth: IP, faixa (CIDR) ou nome (stripe, github) que entra sem senha (repet\xEDvel)").option("--no-ui", "Sem tela cheia: s\xF3 um log das requisi\xE7\xF5es (\xFAtil em scripts)").option("--no-inspect", "N\xE3o sobe o inspetor no navegador (127.0.0.1:4141)").option("--ui", "For\xE7a a tela cheia (no Windows ela vem desligada)").action((port, opts) => tunnel(port, opts));
|
|
2666
|
+
cli.command("http <port>", "O mesmo que duto <porta> (para quem vem do ngrok)").option("--name <nome>", "Nome do endere\xE7o fixo").option("--host <host>", "Host que o servi\xE7o local recebe no lugar do p\xFAblico").option("--no-rewrite-redirects", "Com --host: mant\xE9m os redirecionamentos como o servi\xE7o mandou").option("--auth <usuario:senha>", "Pede usu\xE1rio e senha para abrir o endere\xE7o (repet\xEDvel)").option("--permitir <faixa>", "Com --auth: IP, faixa (CIDR) ou nome (stripe, github) que entra sem senha (repet\xEDvel)").option("--no-ui", "Sem tela cheia").option("--no-inspect", "N\xE3o sobe o inspetor no navegador (127.0.0.1:4141)").action((port, opts) => tunnel(port, opts));
|
|
1663
2667
|
cli.command("login <token>", "Salva o authtoken (pegue em https://duto.dev)").action(async (token) => {
|
|
1664
2668
|
try {
|
|
1665
2669
|
const me = await createApiClient({ baseUrl: apiUrl(), token: String(token) }).me();
|
|
@@ -1698,7 +2702,7 @@ cli.help(
|
|
|
1698
2702
|
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
2703
|
}))
|
|
1700
2704
|
);
|
|
1701
|
-
cli.version("0.
|
|
2705
|
+
cli.version("0.7.0");
|
|
1702
2706
|
try {
|
|
1703
2707
|
cli.parse(process.argv, { run: false });
|
|
1704
2708
|
await cli.runMatchedCommand();
|