@dutodev/cli 0.6.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
|
|
@@ -787,30 +1706,10 @@ ${o.error ? `<p class="error" role="alert">Usu\xE1rio ou senha incorretos.</p>
|
|
|
787
1706
|
);
|
|
788
1707
|
}
|
|
789
1708
|
|
|
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
|
-
|
|
810
1709
|
// src/session/proxy.ts
|
|
811
1710
|
var DENIED_NO_REPLAY = "Barrada pela senha do t\xFAnel; n\xE3o d\xE1 para reenviar.";
|
|
812
1711
|
var PREFLIGHT_NO_REPLAY = "Preflight de CORS n\xE3o d\xE1 para reenviar.";
|
|
813
|
-
var
|
|
1712
|
+
var TEXT2 = /^(text\/|application\/(json|xml|javascript|x-www-form-urlencoded|graphql)|[^;]*\+(json|xml))/i;
|
|
814
1713
|
var MAX_BODY = 64 * 1024;
|
|
815
1714
|
var LOCAL_AGENT = new Agent({ keepAlive: false });
|
|
816
1715
|
function flat(h) {
|
|
@@ -1043,7 +1942,7 @@ async function startProxy(o) {
|
|
|
1043
1942
|
return { id, started };
|
|
1044
1943
|
}
|
|
1045
1944
|
function trackResponse(id, started, up, reqCap) {
|
|
1046
|
-
const resCap = new Capture(
|
|
1945
|
+
const resCap = new Capture(TEXT2.test(up.headers["content-type"] ?? ""), max);
|
|
1047
1946
|
let bytes = 0;
|
|
1048
1947
|
up.on("data", (c) => {
|
|
1049
1948
|
bytes += c.length;
|
|
@@ -1068,7 +1967,7 @@ async function startProxy(o) {
|
|
|
1068
1967
|
const blocked = replayBlock(r);
|
|
1069
1968
|
if (blocked || !r) return { ok: false, reason: blocked ?? "Essa requisi\xE7\xE3o j\xE1 saiu da lista." };
|
|
1070
1969
|
const route = [...o.routes.values()].find((x) => x.tunnel === r.tunnel);
|
|
1071
|
-
if (!route) return { ok: false, reason:
|
|
1970
|
+
if (!route) return { ok: false, reason: tunnelClosedReason(r.tunnel) };
|
|
1072
1971
|
const newId = nextId++;
|
|
1073
1972
|
const started = Date.now();
|
|
1074
1973
|
const headers = replayHeaders(r);
|
|
@@ -1102,7 +2001,7 @@ async function startProxy(o) {
|
|
|
1102
2001
|
})();
|
|
1103
2002
|
return { ok: true, id: newId };
|
|
1104
2003
|
}
|
|
1105
|
-
const server =
|
|
2004
|
+
const server = createServer2(async (req, res) => {
|
|
1106
2005
|
req.pause();
|
|
1107
2006
|
const route = await routeFor(req);
|
|
1108
2007
|
if (!route) {
|
|
@@ -1131,7 +2030,7 @@ async function startProxy(o) {
|
|
|
1131
2030
|
return;
|
|
1132
2031
|
}
|
|
1133
2032
|
const { id, started } = record(req, route, headers, false, access, preflight ? PREFLIGHT_NO_REPLAY : void 0);
|
|
1134
|
-
const reqCap = new Capture(
|
|
2033
|
+
const reqCap = new Capture(TEXT2.test(req.headers["content-type"] ?? ""), max);
|
|
1135
2034
|
req.pause();
|
|
1136
2035
|
const host = await localHost(route.port);
|
|
1137
2036
|
const upstream = request(
|
|
@@ -1201,8 +2100,8 @@ async function startProxy(o) {
|
|
|
1201
2100
|
target.write(`${raw}\r
|
|
1202
2101
|
`);
|
|
1203
2102
|
if (head.length) target.write(head);
|
|
1204
|
-
target.once("data", (
|
|
1205
|
-
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;
|
|
1206
2105
|
o.store.update(id, { status, durationMs: Date.now() - started });
|
|
1207
2106
|
});
|
|
1208
2107
|
target.pipe(client);
|
|
@@ -1420,6 +2319,10 @@ var Session = class {
|
|
|
1420
2319
|
const { token, expiresAt } = auth.issueQrToken();
|
|
1421
2320
|
return { url: `${t.url}/__duto/qr/${token}`, expiresAt };
|
|
1422
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
|
+
}
|
|
1423
2326
|
/** Cria o TunnelAuth na primeira abertura (precisa do allow resolvido) e o mantém depois. */
|
|
1424
2327
|
async authFor(key) {
|
|
1425
2328
|
const existing = this.auths.get(key);
|
|
@@ -1687,11 +2590,24 @@ async function runSession(specs, source, flags) {
|
|
|
1687
2590
|
fail2(err instanceof Error ? err.message : String(err));
|
|
1688
2591
|
}
|
|
1689
2592
|
const session = new Session({ api, deviceId: config.deviceId, deviceName: hostname(), cloudflaredPath, source, runDir: join6(dutoHome(), "run") });
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
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
|
+
}
|
|
1693
2601
|
}
|
|
1694
|
-
|
|
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}`));
|
|
1695
2611
|
const stopLog = runPlain(session, (l) => console.log(l));
|
|
1696
2612
|
const stopped = new Promise((resolve) => {
|
|
1697
2613
|
const onSignal = () => {
|
|
@@ -1708,6 +2624,7 @@ async function runSession(specs, source, flags) {
|
|
|
1708
2624
|
await stopped;
|
|
1709
2625
|
stopLog();
|
|
1710
2626
|
await session.stop();
|
|
2627
|
+
await inspector?.close().catch(() => void 0);
|
|
1711
2628
|
process.exit(0);
|
|
1712
2629
|
}
|
|
1713
2630
|
async function tunnel(rawPort, flags) {
|
|
@@ -1745,8 +2662,8 @@ ${paint("signal", "Dica:")} crie um duto.yml com os t\xFAneis do projeto e rode
|
|
|
1745
2662
|
return runSession([{ name, port, ...host ? { host } : {}, ...keepRedirects ? { rewriteRedirects: false } : {}, ...auth.length ? { auth } : {}, ...allow.length ? { allow } : {} }], "linha de comando", flags);
|
|
1746
2663
|
}
|
|
1747
2664
|
var cli = cac("duto");
|
|
1748
|
-
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));
|
|
1749
|
-
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));
|
|
1750
2667
|
cli.command("login <token>", "Salva o authtoken (pegue em https://duto.dev)").action(async (token) => {
|
|
1751
2668
|
try {
|
|
1752
2669
|
const me = await createApiClient({ baseUrl: apiUrl(), token: String(token) }).me();
|
|
@@ -1785,7 +2702,7 @@ cli.help(
|
|
|
1785
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")
|
|
1786
2703
|
}))
|
|
1787
2704
|
);
|
|
1788
|
-
cli.version("0.
|
|
2705
|
+
cli.version("0.7.0");
|
|
1789
2706
|
try {
|
|
1790
2707
|
cli.parse(process.argv, { run: false });
|
|
1791
2708
|
await cli.runMatchedCommand();
|