@dutodev/cli 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -10
- package/dist/index.js +28 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,24 +51,47 @@ Todos os túneis sobem juntos, numa única tela. A CLI procura o arquivo na past
|
|
|
51
51
|
|
|
52
52
|
### Host e cabeçalhos
|
|
53
53
|
|
|
54
|
-
Por padrão, o serviço local recebe
|
|
54
|
+
Por padrão, o serviço local recebe a requisição como ela chegou da internet: `Host: web-<usuário>.duto.dev`, `X-Forwarded-Proto: https` e os cabeçalhos da Cloudflare (`CF-Connecting-IP` e outros). Na maioria dos apps isso basta. Quando o app decide algo pelo domínio, há duas ferramentas por túnel:
|
|
55
55
|
|
|
56
56
|
```yaml
|
|
57
57
|
tunnels:
|
|
58
58
|
loja:
|
|
59
59
|
port: 8080
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
port: 8080 # mesma porta: vale quando cada túnel tem um host
|
|
63
|
-
host: painel.meuapp.test
|
|
64
|
-
headers:
|
|
65
|
-
X-Ambiente: tunel # acrescenta ou sobrescreve
|
|
60
|
+
headers: # acrescenta ou sobrescreve cabeçalhos
|
|
61
|
+
X-Forwarded-Host: loja.meuapp.test
|
|
66
62
|
X-Debug: "" # valor vazio remove o cabeçalho
|
|
63
|
+
painel:
|
|
64
|
+
port: 8081
|
|
65
|
+
host: painel.meuapp.test # troca o Host que o serviço recebe
|
|
67
66
|
```
|
|
68
67
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
**Qual usar:**
|
|
69
|
+
|
|
70
|
+
| Situação | Use | Por quê |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| Um proxy reverso local (Traefik, Nginx, Caddy) escolhe o serviço pelo Host | `host:` | O proxy só conhece os domínios locais |
|
|
73
|
+
| Vários túneis na mesma porta, um por domínio | `host:` ou `headers:` diferentes em cada um | A porta pode se repetir; cada túnel tem o próprio endereço |
|
|
74
|
+
| O app identifica o domínio ou o cliente pelo `X-Forwarded-Host`, mas monta URLs a partir do `Host` | `headers:` com `X-Forwarded-Host` | O app vê o domínio local e continua gerando links públicos |
|
|
75
|
+
| Login OAuth em que o app calcula o `redirect_uri` sozinho | evite `host:`; tente `headers:` | Veja abaixo |
|
|
76
|
+
|
|
77
|
+
**O que o `host:` faz:**
|
|
78
|
+
|
|
79
|
+
- O serviço recebe o Host informado, e o `X-Forwarded-Host` não é enviado. Para enviá-lo assim mesmo, coloque-o em `headers:`.
|
|
80
|
+
- Os redirecionamentos voltam com o endereço público. Um `Location: http://painel.meuapp.test:8081/login` chega ao navegador como `https://painel-<usuário>.duto.dev/login`. Isso vale também dentro de parâmetros codificados no `Location`. Para desligar, use `rewrite_redirects: false` no túnel.
|
|
81
|
+
- Links no corpo das respostas (HTML, JSON) não são reescritos. URLs que o app montar a partir do Host só abrem na sua máquina.
|
|
82
|
+
- Na linha de comando: `duto 8081 --host painel.meuapp.test`, e `--no-rewrite-redirects` para desligar a troca.
|
|
83
|
+
|
|
84
|
+
**OAuth e `host:`.** No fluxo de autorização por código, o app manda um `redirect_uri` duas vezes: na ida do navegador ao provedor de login e depois, servidor a servidor, ao trocar o código por token. O provedor exige que as duas sejam idênticas. Com `host:`, o app gera as duas com o domínio local. O Duto corrige a primeira, que passa pelo navegador, mas não alcança a segunda, que não passa pelo túnel. O provedor recusa a troca, em geral com `invalid_request` ou `invalid_grant`, e o app costuma voltar ao login em loop (`ERR_TOO_MANY_REDIRECTS`).
|
|
85
|
+
|
|
86
|
+
Saídas, da mais simples para a mais trabalhosa:
|
|
87
|
+
|
|
88
|
+
1. Não troque o Host. O app gera o `redirect_uri` com o endereço público nas duas vezes.
|
|
89
|
+
2. Se o app precisa ver o domínio local para outras decisões e lê o `X-Forwarded-Host`, passe o domínio por `headers:` e mantenha o Host público.
|
|
90
|
+
3. Configure no app a URL pública de retorno, se ele permitir.
|
|
91
|
+
|
|
92
|
+
Em todos os casos, cadastre `https://<nome>-<usuário>.duto.dev/<caminho de retorno>` como URL de retorno no provedor. A comparação costuma ser exata, sem curingas: cada pessoa do time tem o próprio endereço.
|
|
93
|
+
|
|
94
|
+
**Sobre o `X-Forwarded-*`.** `X-Forwarded-Host`, `X-Forwarded-Proto` e `X-Forwarded-For` são uma convenção antiga entre proxies reversos para contar ao app o que o cliente pediu de verdade. A versão padronizada é o cabeçalho `Forwarded` (RFC 7239), menos usado na prática. Frameworks costumam ler esses cabeçalhos só quando o proxy é marcado como confiável (`trusted proxies`, `trust proxy`, `USE_X_FORWARDED_HOST`), então o efeito de `headers:` depende de como o app está configurado. Por outro lado, se um app lê o `X-Forwarded-Host` sem essa checagem, qualquer cliente pode forjá-lo em produção. Vale garantir que o proxy da frente sobrescreva ou remova esse cabeçalho.
|
|
72
95
|
|
|
73
96
|
## A tela
|
|
74
97
|
|
|
@@ -100,6 +123,7 @@ Fora de um terminal interativo (pipe, CI) ou com `--no-ui`, a CLI imprime um log
|
|
|
100
123
|
|---|---|
|
|
101
124
|
| `--name <nome>` | Endereço fixo `<nome>-<usuário>.duto.dev` |
|
|
102
125
|
| `--host <host>` | Host que o serviço local recebe no lugar do público |
|
|
126
|
+
| `--no-rewrite-redirects` | Com `--host`, mantém os redirecionamentos como o serviço mandou |
|
|
103
127
|
| `--no-ui` / `--ui` | Desliga / força a tela cheia |
|
|
104
128
|
|
|
105
129
|
## Configuração
|
package/dist/index.js
CHANGED
|
@@ -240,7 +240,7 @@ var CONFIG_FILES = ["duto.yml", "duto.yaml"];
|
|
|
240
240
|
var LABEL = "[a-z0-9](?:[a-z0-9-]*[a-z0-9])?";
|
|
241
241
|
var HOST_RE = new RegExp(`^${LABEL}(?:\\.${LABEL})*(?::\\d{1,5})?$`);
|
|
242
242
|
var HEADER_NAME_RE = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
243
|
-
var TUNNEL_KEYS = ["port", "host", "headers"];
|
|
243
|
+
var TUNNEL_KEYS = ["port", "host", "headers", "rewrite_redirects"];
|
|
244
244
|
function parseHost(raw) {
|
|
245
245
|
const host = raw.trim().toLowerCase();
|
|
246
246
|
return HOST_RE.test(host) ? host : null;
|
|
@@ -263,7 +263,6 @@ function parseConfig(text, file = CONFIG_FILES[0]) {
|
|
|
263
263
|
throw fail3('declare os t\xFAneis em "tunnels:", um por linha (ex.: web: 3000).');
|
|
264
264
|
}
|
|
265
265
|
const specs = [];
|
|
266
|
-
const seen = /* @__PURE__ */ new Map();
|
|
267
266
|
for (const pair of tunnels.items) {
|
|
268
267
|
const raw = isScalar(pair.key) ? String(pair.key.value) : "";
|
|
269
268
|
const line = lineAt(pair.key?.range?.[0]);
|
|
@@ -282,7 +281,7 @@ function parseConfig(text, file = CONFIG_FILES[0]) {
|
|
|
282
281
|
const body = pair.value;
|
|
283
282
|
for (const item of body.items) {
|
|
284
283
|
const key = isScalar(item.key) ? String(item.key.value) : "";
|
|
285
|
-
if (!TUNNEL_KEYS.includes(key)) throw fail3(`chave desconhecida em "${raw}": ${key} (use port, host ou
|
|
284
|
+
if (!TUNNEL_KEYS.includes(key)) throw fail3(`chave desconhecida em "${raw}": ${key} (use port, host, headers ou rewrite_redirects)`, line);
|
|
286
285
|
}
|
|
287
286
|
const hostNode = body.get("host", true);
|
|
288
287
|
if (hostNode !== void 0) {
|
|
@@ -309,17 +308,15 @@ function parseConfig(text, file = CONFIG_FILES[0]) {
|
|
|
309
308
|
}
|
|
310
309
|
spec.headers = headers;
|
|
311
310
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
);
|
|
311
|
+
const rewriteNode = body.get("rewrite_redirects", true);
|
|
312
|
+
if (rewriteNode !== void 0) {
|
|
313
|
+
const value = isScalar(rewriteNode) ? rewriteNode.value : void 0;
|
|
314
|
+
if (typeof value !== "boolean") throw fail3(`rewrite_redirects de "${raw}" deve ser true ou false`, line);
|
|
315
|
+
if (!spec.host) throw fail3(`rewrite_redirects de "${raw}" s\xF3 vale junto com host:`, line);
|
|
316
|
+
if (value === false) spec.rewriteRedirects = false;
|
|
317
|
+
}
|
|
320
318
|
}
|
|
321
319
|
if (specs.some((s) => s.name === name)) throw fail3(`o nome "${raw}" aparece duas vezes`, line);
|
|
322
|
-
seen.set(port, [...users, { raw, host: spec.host }]);
|
|
323
320
|
specs.push(spec);
|
|
324
321
|
}
|
|
325
322
|
return specs;
|
|
@@ -433,6 +430,12 @@ function rewrite(h, route) {
|
|
|
433
430
|
}
|
|
434
431
|
return out;
|
|
435
432
|
}
|
|
433
|
+
var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
434
|
+
function rewriteLocation(location, localHost2, publicHost) {
|
|
435
|
+
const host = escapeRe(localHost2.replace(/:\d+$/, ""));
|
|
436
|
+
const re = new RegExp(`https?(://|%3A%2F%2F)${host}(?::\\d+|%3A\\d+)?(?=$|[/?#&]|%2F|%3F|%23|%26)`, "gi");
|
|
437
|
+
return location.replace(re, (_m, sep) => `https${sep}${publicHost}`);
|
|
438
|
+
}
|
|
436
439
|
function hostOf(req) {
|
|
437
440
|
return (req.headers.host ?? "").replace(/:\d+$/, "").toLowerCase();
|
|
438
441
|
}
|
|
@@ -473,6 +476,9 @@ async function startProxy(o) {
|
|
|
473
476
|
(up) => {
|
|
474
477
|
const resCap = new Capture(TEXT.test(up.headers["content-type"] ?? ""), max);
|
|
475
478
|
let bytes = 0;
|
|
479
|
+
if (route.host && route.rewriteRedirects !== false && typeof up.headers.location === "string") {
|
|
480
|
+
up.headers.location = rewriteLocation(up.headers.location, route.host, hostOf(req));
|
|
481
|
+
}
|
|
476
482
|
res.writeHead(up.statusCode ?? 502, up.statusMessage, up.headers);
|
|
477
483
|
up.on("data", (c) => {
|
|
478
484
|
bytes += c.length;
|
|
@@ -690,7 +696,7 @@ var Session = class {
|
|
|
690
696
|
this.emit();
|
|
691
697
|
}
|
|
692
698
|
blank(spec, adHoc) {
|
|
693
|
-
return { key: keyOf(spec), name: spec.name, port: spec.port, ...spec.host ? { host: spec.host } : {}, ...spec.headers ? { headers: spec.headers } : {}, url: null, hostname: null, endpointId: null, status: "opening", error: null, adHoc, requests: 0, errors: 0 };
|
|
699
|
+
return { key: keyOf(spec), name: spec.name, port: spec.port, ...spec.host ? { host: spec.host } : {}, ...spec.headers ? { headers: spec.headers } : {}, ...spec.rewriteRedirects === false ? { rewriteRedirects: false } : {}, url: null, hostname: null, endpointId: null, status: "opening", error: null, adHoc, requests: 0, errors: 0 };
|
|
694
700
|
}
|
|
695
701
|
async open(t, existing, retrying = false) {
|
|
696
702
|
if (this.stopped) return;
|
|
@@ -714,7 +720,7 @@ var Session = class {
|
|
|
714
720
|
t.hostname = res.hostname;
|
|
715
721
|
t.endpointId = res.endpointId;
|
|
716
722
|
t.error = null;
|
|
717
|
-
this.routes.set(res.hostname.toLowerCase(), { tunnel: t.key, port: t.port, host: t.host, headers: t.headers });
|
|
723
|
+
this.routes.set(res.hostname.toLowerCase(), { tunnel: t.key, port: t.port, host: t.host, headers: t.headers, rewriteRedirects: t.rewriteRedirects });
|
|
718
724
|
t.status = await isPortListening(t.port) ? "live" : "no-listener";
|
|
719
725
|
this.ensureCloudflared(res.tunnelToken);
|
|
720
726
|
this.emit();
|
|
@@ -904,9 +910,9 @@ async function runSession(specs, source, flags) {
|
|
|
904
910
|
const session = new Session({ api, deviceId: config.deviceId, deviceName: hostname(), cloudflaredPath, source });
|
|
905
911
|
if (wantsUi(flags)) {
|
|
906
912
|
const { runFullscreen } = await import("./fullscreen-5437X7MP.js");
|
|
907
|
-
process.exit(await runFullscreen(session, specs, "0.4.
|
|
913
|
+
process.exit(await runFullscreen(session, specs, "0.4.2"));
|
|
908
914
|
}
|
|
909
|
-
console.log(`${mark()} ${paint("muted", `${"0.4.
|
|
915
|
+
console.log(`${mark()} ${paint("muted", `${"0.4.2"} \xB7 ${source} \xB7 Ctrl+C para encerrar`)}`);
|
|
910
916
|
const stopLog = runPlain(session, (l) => console.log(l));
|
|
911
917
|
const stopped = new Promise((resolve) => {
|
|
912
918
|
const onSignal = () => {
|
|
@@ -948,11 +954,13 @@ ${paint("signal", "Dica:")} crie um duto.yml com os t\xFAneis do projeto e rode
|
|
|
948
954
|
const port = parsePort(rawPort);
|
|
949
955
|
const name = parseName(flags.name) ?? null;
|
|
950
956
|
const host = parseHostFlag(flags.host);
|
|
951
|
-
|
|
957
|
+
const keepRedirects = flags.rewriteRedirects === false;
|
|
958
|
+
if (keepRedirects && !host) fail2("--no-rewrite-redirects s\xF3 vale junto com --host");
|
|
959
|
+
return runSession([{ name, port, ...host ? { host } : {}, ...keepRedirects ? { rewriteRedirects: false } : {} }], "linha de comando", flags);
|
|
952
960
|
}
|
|
953
961
|
var cli = cac("duto");
|
|
954
|
-
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-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));
|
|
955
|
-
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-ui", "Sem tela cheia").action((port, opts) => tunnel(port, opts));
|
|
962
|
+
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("--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));
|
|
963
|
+
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("--no-ui", "Sem tela cheia").action((port, opts) => tunnel(port, opts));
|
|
956
964
|
cli.command("login <token>", "Salva o authtoken (pegue em https://duto.dev)").action(async (token) => {
|
|
957
965
|
try {
|
|
958
966
|
const me = await createApiClient({ baseUrl: apiUrl(), token: String(token) }).me();
|
|
@@ -991,7 +999,7 @@ cli.help(
|
|
|
991
999
|
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")
|
|
992
1000
|
}))
|
|
993
1001
|
);
|
|
994
|
-
cli.version("0.4.
|
|
1002
|
+
cli.version("0.4.2");
|
|
995
1003
|
try {
|
|
996
1004
|
cli.parse(process.argv, { run: false });
|
|
997
1005
|
await cli.runMatchedCommand();
|