@dutodev/cli 0.4.6 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -0
- package/dist/{fullscreen-EPWB53EP.js → fullscreen-7P5GBCQX.js} +179 -55
- package/dist/index.js +694 -97
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -33,7 +33,7 @@ var fail = (msg) => `${paint("err", "\u2716", process.stderr)} ${msg}`;
|
|
|
33
33
|
|
|
34
34
|
// src/index.ts
|
|
35
35
|
import { hostname } from "os";
|
|
36
|
-
import { join as
|
|
36
|
+
import { join as join6, relative } from "path";
|
|
37
37
|
import cac from "cac";
|
|
38
38
|
|
|
39
39
|
// src/api.ts
|
|
@@ -224,10 +224,142 @@ async function ensureCloudflaredWithProgress(write, opts = {}) {
|
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
// src/session/
|
|
228
|
-
import {
|
|
227
|
+
// src/session/allowlist.ts
|
|
228
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
229
|
+
import { BlockList, isIP } from "net";
|
|
229
230
|
import { dirname, join as join3 } from "path";
|
|
230
|
-
|
|
231
|
+
|
|
232
|
+
// src/session/allowlist-snapshot.ts
|
|
233
|
+
var ALLOW_SNAPSHOT = {
|
|
234
|
+
stripe: [
|
|
235
|
+
"3.18.12.63/32",
|
|
236
|
+
"3.69.109.8/32",
|
|
237
|
+
"3.120.168.93/32",
|
|
238
|
+
"3.130.192.231/32",
|
|
239
|
+
"13.235.14.237/32",
|
|
240
|
+
"13.235.122.149/32",
|
|
241
|
+
"18.211.135.69/32",
|
|
242
|
+
"35.154.171.200/32",
|
|
243
|
+
"35.157.207.129/32",
|
|
244
|
+
"52.15.183.38/32",
|
|
245
|
+
"54.88.130.119/32",
|
|
246
|
+
"54.88.130.237/32",
|
|
247
|
+
"54.187.174.169/32",
|
|
248
|
+
"54.187.205.235/32",
|
|
249
|
+
"54.187.216.72/32"
|
|
250
|
+
],
|
|
251
|
+
github: [
|
|
252
|
+
"9.234.106.48/28",
|
|
253
|
+
"9.234.98.160/28",
|
|
254
|
+
"9.234.98.176/28",
|
|
255
|
+
"192.30.252.0/22",
|
|
256
|
+
"185.199.108.0/22",
|
|
257
|
+
"140.82.112.0/20",
|
|
258
|
+
"143.55.64.0/20",
|
|
259
|
+
"2a0a:a440::/29",
|
|
260
|
+
"2606:50c0::/32"
|
|
261
|
+
]
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// src/session/allowlist.ts
|
|
265
|
+
var ALLOW_NAMES = ["stripe", "github"];
|
|
266
|
+
var CACHE_MS = 24 * 36e5;
|
|
267
|
+
var FETCH_TIMEOUT_MS = 3e3;
|
|
268
|
+
var SOURCES = {
|
|
269
|
+
stripe: { url: "https://stripe.com/files/ips/ips_webhooks.json", pick: (j) => j?.WEBHOOKS },
|
|
270
|
+
github: { url: "https://api.github.com/meta", pick: (j) => j?.hooks }
|
|
271
|
+
};
|
|
272
|
+
function splitCidr(raw) {
|
|
273
|
+
const [ip, p, ...resto] = raw.split("/");
|
|
274
|
+
if (!ip || resto.length) return null;
|
|
275
|
+
const v = isIP(ip);
|
|
276
|
+
if (!v) return null;
|
|
277
|
+
const max = v === 4 ? 32 : 128;
|
|
278
|
+
if (p === void 0) return { ip, family: v, prefix: max };
|
|
279
|
+
if (!/^\d{1,3}$/.test(p)) return null;
|
|
280
|
+
const prefix = Number(p);
|
|
281
|
+
return prefix <= max ? { ip, family: v, prefix } : null;
|
|
282
|
+
}
|
|
283
|
+
function parseAllowEntry(raw) {
|
|
284
|
+
if (ALLOW_NAMES.includes(raw)) return { kind: "name", name: raw };
|
|
285
|
+
const c = splitCidr(raw);
|
|
286
|
+
if (!c) return null;
|
|
287
|
+
return { kind: "cidr", cidr: raw.includes("/") ? raw : `${raw}/${c.prefix}` };
|
|
288
|
+
}
|
|
289
|
+
function blockListOf(cidrs) {
|
|
290
|
+
const bl = new BlockList();
|
|
291
|
+
for (const cidr of cidrs) {
|
|
292
|
+
const c = splitCidr(cidr);
|
|
293
|
+
if (c) bl.addSubnet(c.ip, c.prefix, c.family === 4 ? "ipv4" : "ipv6");
|
|
294
|
+
}
|
|
295
|
+
return bl;
|
|
296
|
+
}
|
|
297
|
+
function readCache(file) {
|
|
298
|
+
try {
|
|
299
|
+
const c = JSON.parse(readFileSync2(file, "utf8"));
|
|
300
|
+
if (typeof c?.fetchedAt === "number" && c.lists && typeof c.lists === "object") return c;
|
|
301
|
+
} catch {
|
|
302
|
+
}
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
async function fetchRanges(name, f) {
|
|
306
|
+
const src = SOURCES[name];
|
|
307
|
+
const res = await f(src.url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), headers: { accept: "application/json" } });
|
|
308
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
309
|
+
const list = src.pick(await res.json());
|
|
310
|
+
if (!Array.isArray(list)) throw new Error("formato inesperado");
|
|
311
|
+
const out = list.filter((x) => typeof x === "string" && splitCidr(x) !== null);
|
|
312
|
+
if (!out.length) throw new Error("lista vazia");
|
|
313
|
+
return out.map((x) => x.includes("/") ? x : `${x}/${splitCidr(x).prefix}`);
|
|
314
|
+
}
|
|
315
|
+
async function resolveAllow(entries, o = {}) {
|
|
316
|
+
const cacheFile = o.cacheFile ?? join3(dutoHome(), "allowlist.json");
|
|
317
|
+
const f = o.fetch ?? fetch;
|
|
318
|
+
const now = (o.now ?? Date.now)();
|
|
319
|
+
const parsed = entries.map((raw) => ({ raw, e: parseAllowEntry(raw) }));
|
|
320
|
+
const names = [...new Set(parsed.flatMap((p) => p.e?.kind === "name" ? [p.e.name] : []))];
|
|
321
|
+
const cached = readCache(cacheFile);
|
|
322
|
+
const fresh = cached && now - cached.fetchedAt >= 0 && now - cached.fetchedAt < CACHE_MS ? cached : null;
|
|
323
|
+
const lists = {};
|
|
324
|
+
if (fresh && names.every((n) => Array.isArray(fresh.lists[n]) && fresh.lists[n].length)) {
|
|
325
|
+
for (const n of names) lists[n] = fresh.lists[n];
|
|
326
|
+
} else {
|
|
327
|
+
const net = {};
|
|
328
|
+
for (const name of names) {
|
|
329
|
+
try {
|
|
330
|
+
net[name] = lists[name] = await fetchRanges(name, f);
|
|
331
|
+
} catch {
|
|
332
|
+
lists[name] = ALLOW_SNAPSHOT[name];
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (names.length && Object.keys(net).length === names.length) {
|
|
336
|
+
try {
|
|
337
|
+
mkdirSync3(dirname(cacheFile), { recursive: true });
|
|
338
|
+
writeFileSync3(cacheFile, JSON.stringify({ fetchedAt: now, lists: net }));
|
|
339
|
+
} catch {
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const blocks = parsed.flatMap(({ raw, e }) => {
|
|
344
|
+
if (!e) return [];
|
|
345
|
+
return [{ reason: e.kind === "name" ? e.name : raw, bl: blockListOf(e.kind === "name" ? lists[e.name] : [e.cidr]) }];
|
|
346
|
+
});
|
|
347
|
+
return {
|
|
348
|
+
match(ip) {
|
|
349
|
+
if (!ip) return null;
|
|
350
|
+
const plain = ip.replace(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i, "$1");
|
|
351
|
+
const v = isIP(plain);
|
|
352
|
+
if (!v) return null;
|
|
353
|
+
const fam = v === 4 ? "ipv4" : "ipv6";
|
|
354
|
+
return blocks.find((b) => b.bl.check(plain, fam))?.reason ?? null;
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// src/session/config.ts
|
|
360
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
361
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
362
|
+
import { isMap, isScalar, isSeq, LineCounter, parseDocument } from "yaml";
|
|
231
363
|
var ConfigError = class extends Error {
|
|
232
364
|
constructor(message2, line, file = CONFIG_FILES[0]) {
|
|
233
365
|
super(!file ? message2 : line ? `${file}, linha ${line}: ${message2}` : `${file}: ${message2}`);
|
|
@@ -240,16 +372,26 @@ var CONFIG_FILES = ["duto.yml", "duto.yaml"];
|
|
|
240
372
|
var LABEL = "[a-z0-9](?:[a-z0-9-]*[a-z0-9])?";
|
|
241
373
|
var HOST_RE = new RegExp(`^${LABEL}(?:\\.${LABEL})*(?::\\d{1,5})?$`);
|
|
242
374
|
var HEADER_NAME_RE = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
243
|
-
var TUNNEL_KEYS = ["port", "host", "headers", "rewrite_redirects"];
|
|
375
|
+
var TUNNEL_KEYS = ["port", "host", "headers", "rewrite_redirects", "auth", "permitir"];
|
|
376
|
+
var ENV_REF_RE = /\$\{([A-Z_0-9]+)\}/g;
|
|
244
377
|
function parseHost(raw) {
|
|
245
378
|
const host = raw.trim().toLowerCase();
|
|
246
379
|
return HOST_RE.test(host) ? host : null;
|
|
247
380
|
}
|
|
381
|
+
function parseCredential(raw) {
|
|
382
|
+
const i = raw.indexOf(":");
|
|
383
|
+
if (i < 0) return { ok: false, reason: "format" };
|
|
384
|
+
const user = raw.slice(0, i);
|
|
385
|
+
const password = raw.slice(i + 1);
|
|
386
|
+
if (user.length < 1 || user.length > 128 || /\s/.test(user)) return { ok: false, reason: "user" };
|
|
387
|
+
if (password.length < 8 || password.length > 128) return { ok: false, reason: "password" };
|
|
388
|
+
return { ok: true, cred: { user, password } };
|
|
389
|
+
}
|
|
248
390
|
function portOf(value) {
|
|
249
391
|
const v = isMap(value) ? value.get("port") : isScalar(value) ? value.value : value;
|
|
250
392
|
return typeof v === "number" && Number.isInteger(v) && v >= 1 && v <= 65535 ? v : null;
|
|
251
393
|
}
|
|
252
|
-
function parseConfig(text, file = CONFIG_FILES[0]) {
|
|
394
|
+
function parseConfig(text, file = CONFIG_FILES[0], env = process.env) {
|
|
253
395
|
const fail3 = (message2, line) => new ConfigError(message2, line, file);
|
|
254
396
|
const lc = new LineCounter();
|
|
255
397
|
const doc = parseDocument(text, { lineCounter: lc, prettyErrors: false });
|
|
@@ -281,7 +423,7 @@ function parseConfig(text, file = CONFIG_FILES[0]) {
|
|
|
281
423
|
const body = pair.value;
|
|
282
424
|
for (const item of body.items) {
|
|
283
425
|
const key = isScalar(item.key) ? String(item.key.value) : "";
|
|
284
|
-
if (!TUNNEL_KEYS.includes(key)) throw fail3(`chave desconhecida em "${raw}": ${key} (use port, host, headers ou
|
|
426
|
+
if (!TUNNEL_KEYS.includes(key)) throw fail3(`chave desconhecida em "${raw}": ${key} (use port, host, headers, rewrite_redirects, auth ou permitir)`, line);
|
|
285
427
|
}
|
|
286
428
|
const hostNode = body.get("host", true);
|
|
287
429
|
if (hostNode !== void 0) {
|
|
@@ -315,6 +457,44 @@ function parseConfig(text, file = CONFIG_FILES[0]) {
|
|
|
315
457
|
if (!spec.host) throw fail3(`rewrite_redirects de "${raw}" s\xF3 vale junto com host:`, line);
|
|
316
458
|
if (value === false) spec.rewriteRedirects = false;
|
|
317
459
|
}
|
|
460
|
+
const authNode = body.get("auth", true);
|
|
461
|
+
if (authNode !== void 0) {
|
|
462
|
+
if (!isSeq(authNode)) throw fail3(`auth de "${raw}" deve ser uma lista de usuario:senha`, line);
|
|
463
|
+
const auth = [];
|
|
464
|
+
for (const item of authNode.items) {
|
|
465
|
+
const iLine = lineAt(item.range?.[0]) ?? line;
|
|
466
|
+
const text2 = isScalar(item) && typeof item.value === "string" ? item.value : null;
|
|
467
|
+
if (text2 === null) throw fail3(`credencial inv\xE1lida em "${raw}": use usuario:senha`, iLine);
|
|
468
|
+
const expanded = text2.replace(ENV_REF_RE, (_m, name2) => {
|
|
469
|
+
const val = env[name2];
|
|
470
|
+
if (val === void 0) throw fail3(`vari\xE1vel de ambiente ${name2} n\xE3o definida (usada em auth de "${raw}")`, iLine);
|
|
471
|
+
return val;
|
|
472
|
+
});
|
|
473
|
+
const parsed = parseCredential(expanded);
|
|
474
|
+
if (!parsed.ok) {
|
|
475
|
+
if (parsed.reason === "format") throw fail3(`credencial inv\xE1lida em "${raw}": use usuario:senha`, iLine);
|
|
476
|
+
if (parsed.reason === "user") throw fail3(`usu\xE1rio inv\xE1lido em "${raw}": de 1 a 128 caracteres, sem espa\xE7o`, iLine);
|
|
477
|
+
throw fail3(`senha de "${expanded.slice(0, expanded.indexOf(":"))}" em "${raw}" precisa ter de 8 a 128 caracteres`, iLine);
|
|
478
|
+
}
|
|
479
|
+
if (auth.some((c) => c.user === parsed.cred.user)) throw fail3(`usu\xE1rio "${parsed.cred.user}" aparece duas vezes em "${raw}"`, iLine);
|
|
480
|
+
auth.push(parsed.cred);
|
|
481
|
+
}
|
|
482
|
+
if (!auth.length) throw fail3(`auth de "${raw}" n\xE3o pode ficar vazia`, line);
|
|
483
|
+
spec.auth = auth;
|
|
484
|
+
}
|
|
485
|
+
const allowNode = body.get("permitir", true);
|
|
486
|
+
if (allowNode !== void 0) {
|
|
487
|
+
if (!isSeq(allowNode)) throw fail3(`permitir de "${raw}" deve ser uma lista`, line);
|
|
488
|
+
if (!spec.auth) throw fail3(`permitir de "${raw}" s\xF3 vale junto com auth:`, line);
|
|
489
|
+
const allow = [];
|
|
490
|
+
for (const item of allowNode.items) {
|
|
491
|
+
const iLine = lineAt(item.range?.[0]) ?? line;
|
|
492
|
+
const value = isScalar(item) ? String(item.value) : "";
|
|
493
|
+
if (!parseAllowEntry(value)) throw fail3(`permitir de "${raw}": "${value}" n\xE3o \xE9 um nome conhecido (stripe, github) nem uma faixa de IP`, iLine);
|
|
494
|
+
allow.push(value);
|
|
495
|
+
}
|
|
496
|
+
if (allow.length) spec.allow = allow;
|
|
497
|
+
}
|
|
318
498
|
}
|
|
319
499
|
if (specs.some((s) => s.name === name)) throw fail3(`o nome "${raw}" aparece duas vezes`, line);
|
|
320
500
|
specs.push(spec);
|
|
@@ -324,14 +504,14 @@ function parseConfig(text, file = CONFIG_FILES[0]) {
|
|
|
324
504
|
function findConfig(cwd) {
|
|
325
505
|
let dir = cwd;
|
|
326
506
|
for (; ; ) {
|
|
327
|
-
const present = CONFIG_FILES.filter((f) => existsSync3(
|
|
507
|
+
const present = CONFIG_FILES.filter((f) => existsSync3(join4(dir, f)));
|
|
328
508
|
if (present.length > 1) throw new ConfigError(`${CONFIG_FILES.join(" e ")} na mesma pasta. Apague um dos dois.`, void 0, "");
|
|
329
509
|
if (present.length === 1) {
|
|
330
|
-
const path =
|
|
331
|
-
return { path, tunnels: parseConfig(
|
|
510
|
+
const path = join4(dir, present[0]);
|
|
511
|
+
return { path, tunnels: parseConfig(readFileSync3(path, "utf8"), present[0]) };
|
|
332
512
|
}
|
|
333
|
-
if (existsSync3(
|
|
334
|
-
const up =
|
|
513
|
+
if (existsSync3(join4(dir, ".git"))) return null;
|
|
514
|
+
const up = dirname2(dir);
|
|
335
515
|
if (up === dir) return null;
|
|
336
516
|
dir = up;
|
|
337
517
|
}
|
|
@@ -359,6 +539,148 @@ async function isPortListening(port, timeoutMs = 500) {
|
|
|
359
539
|
return results.some(Boolean);
|
|
360
540
|
}
|
|
361
541
|
|
|
542
|
+
// src/session/auth.ts
|
|
543
|
+
import { createHash as createHash2, createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
544
|
+
var TUNNEL_COOKIE = "__Host-duto_tunel";
|
|
545
|
+
var QR_TTL_MS = 5 * 6e4;
|
|
546
|
+
var sha = (s) => createHash2("sha256").update(s).digest();
|
|
547
|
+
var same = (a, b) => timingSafeEqual(sha(a), sha(b));
|
|
548
|
+
function splitPair(raw) {
|
|
549
|
+
const i = raw.indexOf(":");
|
|
550
|
+
return i < 0 ? null : { user: raw.slice(0, i), password: raw.slice(i + 1) };
|
|
551
|
+
}
|
|
552
|
+
function parseBasic(value) {
|
|
553
|
+
const m = /^basic +([A-Za-z0-9+/]+={0,2})$/i.exec(value?.trim() ?? "");
|
|
554
|
+
return m ? splitPair(Buffer.from(m[1], "base64").toString("utf8")) : null;
|
|
555
|
+
}
|
|
556
|
+
var first = (v) => Array.isArray(v) ? v[0] : v;
|
|
557
|
+
var bareHost = (s) => s.replace(/:\d+$/, "").toLowerCase();
|
|
558
|
+
function sameHostOrigin(h) {
|
|
559
|
+
if (first(h["sec-fetch-site"])?.toLowerCase() === "same-site") return false;
|
|
560
|
+
const origin = first(h.origin);
|
|
561
|
+
if (origin === void 0) return true;
|
|
562
|
+
let host;
|
|
563
|
+
try {
|
|
564
|
+
host = new URL(origin).host;
|
|
565
|
+
} catch {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
return host !== "" && bareHost(host) === bareHost(h.host ?? "");
|
|
569
|
+
}
|
|
570
|
+
var TunnelAuth = class {
|
|
571
|
+
qrUser;
|
|
572
|
+
tunnel;
|
|
573
|
+
credentials;
|
|
574
|
+
allow;
|
|
575
|
+
now;
|
|
576
|
+
secret;
|
|
577
|
+
qr = null;
|
|
578
|
+
constructor(o) {
|
|
579
|
+
this.tunnel = o.tunnel;
|
|
580
|
+
this.credentials = o.credentials;
|
|
581
|
+
this.allow = o.allow;
|
|
582
|
+
this.now = o.now ?? Date.now;
|
|
583
|
+
this.secret = o.secret ?? randomBytes(32);
|
|
584
|
+
this.qrUser = o.credentials[0]?.user ?? "";
|
|
585
|
+
}
|
|
586
|
+
access(h) {
|
|
587
|
+
if (this.allow) {
|
|
588
|
+
const ip = first(h["cf-connecting-ip"]);
|
|
589
|
+
const reason = this.allow.match(ip);
|
|
590
|
+
if (reason) return { kind: "allow", reason };
|
|
591
|
+
}
|
|
592
|
+
const cookieUser = this.cookieUser(h.cookie);
|
|
593
|
+
if (cookieUser && sameHostOrigin(h)) return { kind: "user", user: cookieUser };
|
|
594
|
+
const duto = first(h["x-duto-auth"]);
|
|
595
|
+
const cand = duto !== void 0 ? splitPair(duto) : parseBasic(first(h.authorization));
|
|
596
|
+
if (cand && this.checkPassword(cand.user, cand.password)) return { kind: "user", user: cand.user };
|
|
597
|
+
return { kind: "denied" };
|
|
598
|
+
}
|
|
599
|
+
checkPassword(user, password) {
|
|
600
|
+
let ok2 = false;
|
|
601
|
+
for (const c of this.credentials) {
|
|
602
|
+
const userOk = same(c.user, user);
|
|
603
|
+
const passOk = same(c.password, password);
|
|
604
|
+
if (userOk && passOk) ok2 = true;
|
|
605
|
+
}
|
|
606
|
+
return ok2;
|
|
607
|
+
}
|
|
608
|
+
loginCookie(user) {
|
|
609
|
+
return `${TUNNEL_COOKIE}=${this.cookieValue(user)}; Secure; HttpOnly; SameSite=Lax; Path=/`;
|
|
610
|
+
}
|
|
611
|
+
logoutCookie() {
|
|
612
|
+
return `${TUNNEL_COOKIE}=; Max-Age=0; Secure; HttpOnly; SameSite=Lax; Path=/`;
|
|
613
|
+
}
|
|
614
|
+
issueQrToken() {
|
|
615
|
+
this.qr = { token: randomBytes(24).toString("base64url"), expiresAt: this.now() + QR_TTL_MS };
|
|
616
|
+
return { ...this.qr };
|
|
617
|
+
}
|
|
618
|
+
consumeQrToken(token) {
|
|
619
|
+
const qr = this.qr;
|
|
620
|
+
if (!qr || !same(qr.token, token)) return false;
|
|
621
|
+
this.qr = null;
|
|
622
|
+
return this.now() <= qr.expiresAt;
|
|
623
|
+
}
|
|
624
|
+
strip(h) {
|
|
625
|
+
const out = { ...h };
|
|
626
|
+
delete out["x-duto-auth"];
|
|
627
|
+
const basic = parseBasic(first(h.authorization));
|
|
628
|
+
if (basic && this.checkPassword(basic.user, basic.password)) delete out.authorization;
|
|
629
|
+
if (h.cookie !== void 0) {
|
|
630
|
+
const parts = (Array.isArray(h.cookie) ? h.cookie.join("; ") : h.cookie).split(";").map((p) => p.trim()).filter(Boolean);
|
|
631
|
+
const kept = parts.filter((p) => !p.startsWith(`${TUNNEL_COOKIE}=`));
|
|
632
|
+
if (kept.length === parts.length) return out;
|
|
633
|
+
if (kept.length) out.cookie = kept.join("; ");
|
|
634
|
+
else delete out.cookie;
|
|
635
|
+
}
|
|
636
|
+
return out;
|
|
637
|
+
}
|
|
638
|
+
// Valor: usuário em base64url + "." + HMAC(túnel, usuário).
|
|
639
|
+
cookieValue(user) {
|
|
640
|
+
return `${Buffer.from(user).toString("base64url")}.${this.sign(user)}`;
|
|
641
|
+
}
|
|
642
|
+
sign(user) {
|
|
643
|
+
return createHmac("sha256", this.secret).update(`${this.tunnel}\0${user}`).digest("base64url");
|
|
644
|
+
}
|
|
645
|
+
cookieUser(header) {
|
|
646
|
+
if (header === void 0) return null;
|
|
647
|
+
const list = (Array.isArray(header) ? header.join("; ") : header).split(";");
|
|
648
|
+
for (const p of list) {
|
|
649
|
+
const item = p.trim();
|
|
650
|
+
if (!item.startsWith(`${TUNNEL_COOKIE}=`)) continue;
|
|
651
|
+
const [b64, sig, ...resto] = item.slice(TUNNEL_COOKIE.length + 1).split(".");
|
|
652
|
+
if (!b64 || !sig || resto.length) continue;
|
|
653
|
+
const user = Buffer.from(b64, "base64url").toString("utf8");
|
|
654
|
+
if (same(sig, this.sign(user)) && this.credentials.some((c) => c.user === user)) return user;
|
|
655
|
+
}
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
function isPreflight(method, h) {
|
|
660
|
+
if (method?.toUpperCase() !== "OPTIONS") return false;
|
|
661
|
+
if (h.origin === void 0 || h["access-control-request-method"] === void 0) return false;
|
|
662
|
+
if (h["transfer-encoding"] !== void 0) return false;
|
|
663
|
+
const len = h["content-length"];
|
|
664
|
+
return len === void 0 || Number(len) === 0;
|
|
665
|
+
}
|
|
666
|
+
function preflightHeaders(res) {
|
|
667
|
+
const out = {};
|
|
668
|
+
for (const [k, v] of Object.entries(res)) if (k.startsWith("access-control-") || k === "vary") out[k] = v;
|
|
669
|
+
return allowDutoHeader(out);
|
|
670
|
+
}
|
|
671
|
+
function allowDutoHeader(res) {
|
|
672
|
+
if (res["access-control-allow-origin"] === void 0) return res;
|
|
673
|
+
const raw = res["access-control-allow-headers"];
|
|
674
|
+
const cur = (Array.isArray(raw) ? raw.join(", ") : raw ?? "").trim();
|
|
675
|
+
const tokens = cur.split(",").map((t) => t.trim().toLowerCase());
|
|
676
|
+
if (tokens.includes("*") || tokens.includes("x-duto-auth")) return res;
|
|
677
|
+
return { ...res, "access-control-allow-headers": cur ? `${cur}, x-duto-auth` : "x-duto-auth" };
|
|
678
|
+
}
|
|
679
|
+
function cors401(origin) {
|
|
680
|
+
if (!origin) return {};
|
|
681
|
+
return { "access-control-allow-origin": origin, vary: "Origin", "access-control-expose-headers": "WWW-Authenticate" };
|
|
682
|
+
}
|
|
683
|
+
|
|
362
684
|
// src/session/proxy.ts
|
|
363
685
|
import { Agent, createServer, request } from "http";
|
|
364
686
|
import { connect as connect2 } from "net";
|
|
@@ -386,16 +708,14 @@ var WAIT_SCRIPT = `
|
|
|
386
708
|
es.onerror = function () { if (++fails >= 3) { es.close(); poll(); } };
|
|
387
709
|
})();
|
|
388
710
|
</script>`;
|
|
389
|
-
function
|
|
390
|
-
const tunnel2 = esc(o.tunnel);
|
|
391
|
-
const local = `localhost:${o.port}`;
|
|
711
|
+
function pageShell(title, body, css = "") {
|
|
392
712
|
return `<!doctype html>
|
|
393
713
|
<html lang="pt-BR">
|
|
394
714
|
<head>
|
|
395
715
|
<meta charset="utf-8">
|
|
396
716
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
397
717
|
<meta name="robots" content="noindex">
|
|
398
|
-
<title
|
|
718
|
+
<title>${title}</title>
|
|
399
719
|
<style>
|
|
400
720
|
:root{--bg:#f6f6f3;--fg:#0f1110;--muted:#5f6360;--line:#dcddd8;--card:#fff;--signal:#2dd4bf;--signal-text:#0f766e}
|
|
401
721
|
@media (prefers-color-scheme:dark){:root{--bg:#0b0d0c;--fg:#e9ebe7;--muted:#8a8f8b;--line:#222624;--card:#111413;--signal-text:#2dd4bf}}
|
|
@@ -410,22 +730,86 @@ code{font:13px ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--fg)}
|
|
|
410
730
|
h1 code{font-size:.86em;font-weight:600}
|
|
411
731
|
.retry{margin-top:20px;padding-top:14px;border-top:1px solid var(--line);font-size:13px;color:var(--muted)}
|
|
412
732
|
.retry b{color:var(--signal-text);font-weight:600}
|
|
413
|
-
</style>
|
|
733
|
+
${css}</style>
|
|
414
734
|
</head>
|
|
415
735
|
<body>
|
|
416
|
-
|
|
736
|
+
${body}
|
|
737
|
+
</body>
|
|
738
|
+
</html>
|
|
739
|
+
`;
|
|
740
|
+
}
|
|
741
|
+
function unavailablePage(o) {
|
|
742
|
+
const tunnel2 = esc(o.tunnel);
|
|
743
|
+
const local = `localhost:${o.port}`;
|
|
744
|
+
return pageShell(
|
|
745
|
+
`Nada respondendo em ${local} \xB7 duto`,
|
|
746
|
+
`<main>
|
|
417
747
|
<div class="mark"><span class="dot"></span>duto \xB7 ${tunnel2}</div>
|
|
418
748
|
<h1>Nada respondendo em <code>${local}</code></h1>
|
|
419
749
|
<p>O t\xFAnel <code>${tunnel2}</code> est\xE1 no ar, mas o servi\xE7o local n\xE3o respondeu. Suba o servidor na m\xE1quina que abriu o t\xFAnel.</p>
|
|
420
750
|
<p>Se voc\xEA s\xF3 recebeu este link, avise quem o enviou.</p>
|
|
421
751
|
<div class="retry">${o.retry ? `<b>Aguardando o servi\xE7o.</b> A p\xE1gina volta sozinha quando ele responder.<noscript> Recarregue quando o servi\xE7o estiver no ar.</noscript>` : "Recarregue a p\xE1gina quando o servi\xE7o estiver no ar."}</div>
|
|
422
|
-
</main>${o.retry ? WAIT_SCRIPT : ""}
|
|
423
|
-
|
|
424
|
-
|
|
752
|
+
</main>${o.retry ? WAIT_SCRIPT : ""}`
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// src/session/authPage.ts
|
|
757
|
+
var CSS = `label{display:block;margin:14px 0 6px;font-size:13px;font-weight:600}
|
|
758
|
+
input{width:100%;padding:9px 10px;border:1px solid var(--line);border-radius:.3rem;background:var(--bg);color:var(--fg);font:inherit}
|
|
759
|
+
input:focus{outline:2px solid var(--signal);outline-offset:1px}
|
|
760
|
+
button{margin-top:18px;width:100%;padding:10px;border:0;border-radius:.3rem;background:var(--fg);color:var(--bg);font:inherit;font-weight:600;cursor:pointer}
|
|
761
|
+
.error{margin:14px 0 0;color:#b42318;font-weight:600}
|
|
762
|
+
@media (prefers-color-scheme:dark){.error{color:#f97066}}
|
|
425
763
|
`;
|
|
764
|
+
function safeVolta(raw) {
|
|
765
|
+
if (!raw || !raw.startsWith("/") || raw.startsWith("//") || raw.startsWith("/\\")) return "/";
|
|
766
|
+
return /^[\x21-\x7e]+$/.test(raw) ? raw : "/";
|
|
767
|
+
}
|
|
768
|
+
function loginPage(o) {
|
|
769
|
+
return pageShell(
|
|
770
|
+
"Entrar \xB7 duto",
|
|
771
|
+
`<main>
|
|
772
|
+
<div class="mark"><span class="dot"></span>duto \xB7 ${esc(o.tunnel)}</div>
|
|
773
|
+
<h1>Este t\xFAnel tem senha</h1>
|
|
774
|
+
<p>Pe\xE7a o acesso a quem enviou o link.</p>
|
|
775
|
+
<form method="post" action="/__duto/entrar">
|
|
776
|
+
<input type="hidden" name="volta" value="${esc(o.volta)}">
|
|
777
|
+
<label for="usuario">Usu\xE1rio</label>
|
|
778
|
+
<input id="usuario" name="usuario" autocomplete="username" autocapitalize="none" spellcheck="false" required autofocus>
|
|
779
|
+
<label for="senha">Senha</label>
|
|
780
|
+
<input id="senha" name="senha" type="password" autocomplete="current-password" required>
|
|
781
|
+
${o.error ? `<p class="error" role="alert">Usu\xE1rio ou senha incorretos.</p>
|
|
782
|
+
` : ""}<button type="submit">Entrar</button>
|
|
783
|
+
</form>
|
|
784
|
+
<div class="retry">Voc\xEA continua conectado at\xE9 o t\xFAnel fechar.</div>
|
|
785
|
+
</main>`,
|
|
786
|
+
CSS
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// src/session/replay.ts
|
|
791
|
+
function replayBlock(r) {
|
|
792
|
+
if (!r) return "Essa requisi\xE7\xE3o j\xE1 saiu da lista.";
|
|
793
|
+
if (r.websocket) return "WebSocket n\xE3o d\xE1 para reenviar.";
|
|
794
|
+
if (r.noReplay) return r.noReplay;
|
|
795
|
+
if (r.access?.kind === "denied") return "Barrada pela senha do t\xFAnel; n\xE3o d\xE1 para reenviar.";
|
|
796
|
+
if (!r.reqComplete) return "A requisi\xE7\xE3o ainda n\xE3o chegou inteira.";
|
|
797
|
+
if (r.reqTruncated) return "Corpo acima de 64 KB; n\xE3o d\xE1 para reenviar.";
|
|
798
|
+
if (r.reqBodyBytes > 0 && r.reqRaw === null) return "Corpo bin\xE1rio n\xE3o \xE9 guardado; n\xE3o d\xE1 para reenviar.";
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
var DROP = /* @__PURE__ */ new Set(["connection", "keep-alive", "transfer-encoding", "expect", "content-length", "upgrade", "te", "trailer", "proxy-connection"]);
|
|
802
|
+
function replayHeaders(r) {
|
|
803
|
+
const out = {};
|
|
804
|
+
for (const [k, v] of Object.entries(r.reqHeaders)) if (!DROP.has(k.toLowerCase())) out[k] = v;
|
|
805
|
+
const hadLength = Object.keys(r.reqHeaders).some((k) => k.toLowerCase() === "content-length");
|
|
806
|
+
if (r.reqBodyBytes > 0 || hadLength) out["content-length"] = String(r.reqRaw?.length ?? 0);
|
|
807
|
+
return out;
|
|
426
808
|
}
|
|
427
809
|
|
|
428
810
|
// src/session/proxy.ts
|
|
811
|
+
var DENIED_NO_REPLAY = "Barrada pela senha do t\xFAnel; n\xE3o d\xE1 para reenviar.";
|
|
812
|
+
var PREFLIGHT_NO_REPLAY = "Preflight de CORS n\xE3o d\xE1 para reenviar.";
|
|
429
813
|
var TEXT = /^(text\/|application\/(json|xml|javascript|x-www-form-urlencoded|graphql)|[^;]*\+(json|xml))/i;
|
|
430
814
|
var MAX_BODY = 64 * 1024;
|
|
431
815
|
var LOCAL_AGENT = new Agent({ keepAlive: false });
|
|
@@ -443,8 +827,11 @@ var Capture = class {
|
|
|
443
827
|
max;
|
|
444
828
|
chunks = [];
|
|
445
829
|
size = 0;
|
|
830
|
+
/** Bytes vistos, capturados ou não. */
|
|
831
|
+
total = 0;
|
|
446
832
|
truncated = false;
|
|
447
833
|
push(c) {
|
|
834
|
+
this.total += c.length;
|
|
448
835
|
if (!this.enabled) return;
|
|
449
836
|
const room = this.max - this.size;
|
|
450
837
|
if (room <= 0) {
|
|
@@ -456,6 +843,9 @@ var Capture = class {
|
|
|
456
843
|
this.chunks.push(part);
|
|
457
844
|
this.size += part.length;
|
|
458
845
|
}
|
|
846
|
+
raw() {
|
|
847
|
+
return this.enabled && this.size > 0 ? Buffer.concat(this.chunks) : null;
|
|
848
|
+
}
|
|
459
849
|
text() {
|
|
460
850
|
return this.enabled && this.size > 0 ? Buffer.concat(this.chunks).toString("utf8") : null;
|
|
461
851
|
}
|
|
@@ -506,6 +896,83 @@ function hostOf(req) {
|
|
|
506
896
|
var DUTO_PREFIX = "/__duto/";
|
|
507
897
|
var WAIT_PATH = "/__duto/espera";
|
|
508
898
|
var WAIT_MAX_MS = 10 * 6e4;
|
|
899
|
+
var LOGIN_PATH = "/__duto/entrar";
|
|
900
|
+
var LOGOUT_PATH = "/__duto/sair";
|
|
901
|
+
var QR_PREFIX = "/__duto/qr/";
|
|
902
|
+
var MAX_FORM = 8 * 1024;
|
|
903
|
+
var DENIED_TEXT = "Este t\xFAnel tem senha. Envie Authorization: Basic ou X-Duto-Auth: usuario:senha.";
|
|
904
|
+
var WS_DENIED = 'HTTP/1.1 401 Unauthorized\r\nWWW-Authenticate: Basic realm="duto"\r\nContent-Length: 0\r\nConnection: close\r\n\r\n';
|
|
905
|
+
function readForm(req, max) {
|
|
906
|
+
return new Promise((resolve) => {
|
|
907
|
+
const chunks = [];
|
|
908
|
+
let size = 0;
|
|
909
|
+
const onData = (c) => {
|
|
910
|
+
size += c.length;
|
|
911
|
+
if (size > max) {
|
|
912
|
+
req.off("data", onData);
|
|
913
|
+
req.pause();
|
|
914
|
+
return resolve(null);
|
|
915
|
+
}
|
|
916
|
+
chunks.push(c);
|
|
917
|
+
};
|
|
918
|
+
req.on("data", onData);
|
|
919
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
920
|
+
req.on("error", () => resolve(null));
|
|
921
|
+
req.resume();
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
async function authRoute(req, res, route, auth) {
|
|
925
|
+
const url = new URL(req.url ?? "/", "http://duto");
|
|
926
|
+
const page = (status, volta, error) => {
|
|
927
|
+
res.writeHead(status, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
928
|
+
res.end(loginPage({ tunnel: route.tunnel, volta, error }));
|
|
929
|
+
};
|
|
930
|
+
const go = (location, cookie) => {
|
|
931
|
+
res.writeHead(303, { location, "cache-control": "no-store", ...cookie && { "set-cookie": cookie } });
|
|
932
|
+
res.end();
|
|
933
|
+
};
|
|
934
|
+
const { pathname } = url;
|
|
935
|
+
if (pathname === LOGIN_PATH && req.method === "GET") {
|
|
936
|
+
page(200, safeVolta(url.searchParams.get("volta")), false);
|
|
937
|
+
return true;
|
|
938
|
+
}
|
|
939
|
+
if (pathname === LOGIN_PATH && req.method === "POST") {
|
|
940
|
+
const raw = await readForm(req, MAX_FORM);
|
|
941
|
+
if (raw === null) {
|
|
942
|
+
res.writeHead(413, { "content-type": "text/plain; charset=utf-8", connection: "close" });
|
|
943
|
+
res.end("Formul\xE1rio grande demais");
|
|
944
|
+
return true;
|
|
945
|
+
}
|
|
946
|
+
const form = new URLSearchParams(raw);
|
|
947
|
+
const user = form.get("usuario") ?? "";
|
|
948
|
+
const volta = safeVolta(form.get("volta"));
|
|
949
|
+
if (auth.checkPassword(user, form.get("senha") ?? "")) go(volta, auth.loginCookie(user));
|
|
950
|
+
else page(401, volta, true);
|
|
951
|
+
return true;
|
|
952
|
+
}
|
|
953
|
+
if (pathname === LOGOUT_PATH && req.method === "GET") {
|
|
954
|
+
go(LOGIN_PATH, auth.logoutCookie());
|
|
955
|
+
return true;
|
|
956
|
+
}
|
|
957
|
+
if (pathname.startsWith(QR_PREFIX) && req.method === "GET") {
|
|
958
|
+
if (auth.consumeQrToken(pathname.slice(QR_PREFIX.length))) go("/", auth.loginCookie(auth.qrUser));
|
|
959
|
+
else go(LOGIN_PATH);
|
|
960
|
+
return true;
|
|
961
|
+
}
|
|
962
|
+
return false;
|
|
963
|
+
}
|
|
964
|
+
function deny(req, res, html) {
|
|
965
|
+
const status = html ? 303 : 401;
|
|
966
|
+
const out = html ? { location: `${LOGIN_PATH}?volta=${encodeURIComponent(req.url ?? "/")}`, "cache-control": "no-store" } : { "www-authenticate": 'Basic realm="duto"', "content-type": "text/plain; charset=utf-8", "cache-control": "no-store", ...cors401(req.headers.origin) };
|
|
967
|
+
const body = html || req.method === "HEAD" ? "" : DENIED_TEXT;
|
|
968
|
+
res.writeHead(status, out);
|
|
969
|
+
res.end(body);
|
|
970
|
+
return { status, out, body };
|
|
971
|
+
}
|
|
972
|
+
function delivered(h, route) {
|
|
973
|
+
const out = rewrite(h, route);
|
|
974
|
+
return route.auth ? route.auth.strip(out) : out;
|
|
975
|
+
}
|
|
509
976
|
async function isUp(port) {
|
|
510
977
|
return await probe("127.0.0.1", port) || await probe("::1", port);
|
|
511
978
|
}
|
|
@@ -547,24 +1014,9 @@ async function startProxy(o) {
|
|
|
547
1014
|
await new Promise((r) => setTimeout(r, 50));
|
|
548
1015
|
}
|
|
549
1016
|
}
|
|
550
|
-
|
|
551
|
-
req.pause();
|
|
552
|
-
const route = await routeFor(req);
|
|
553
|
-
if (!route) {
|
|
554
|
-
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
555
|
-
res.end("T\xFAnel n\xE3o encontrado nesta sess\xE3o");
|
|
556
|
-
return;
|
|
557
|
-
}
|
|
558
|
-
if ((req.url ?? "").startsWith(DUTO_PREFIX)) {
|
|
559
|
-
if (req.url === WAIT_PATH && req.method === "GET") return waitForService(res, route.port, o.waitCheckMs ?? 1e3, o.waitPingMs ?? 25e3);
|
|
560
|
-
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
561
|
-
res.end("Rota reservada do Duto");
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
1017
|
+
function record(req, route, headers, websocket, access, noReplay) {
|
|
564
1018
|
const id = nextId++;
|
|
565
1019
|
const started = Date.now();
|
|
566
|
-
const headers = rewrite(req.headers, route);
|
|
567
|
-
const reqCap = new Capture(TEXT.test(req.headers["content-type"] ?? ""), max);
|
|
568
1020
|
o.store.add({
|
|
569
1021
|
id,
|
|
570
1022
|
tunnel: route.tunnel,
|
|
@@ -574,47 +1026,138 @@ async function startProxy(o) {
|
|
|
574
1026
|
durationMs: null,
|
|
575
1027
|
bytes: 0,
|
|
576
1028
|
startedAt: started,
|
|
577
|
-
websocket
|
|
1029
|
+
websocket,
|
|
578
1030
|
reqHeaders: flat(headers),
|
|
579
1031
|
resHeaders: {},
|
|
580
1032
|
reqBody: null,
|
|
581
1033
|
resBody: null,
|
|
582
|
-
|
|
1034
|
+
reqRaw: null,
|
|
1035
|
+
reqBodyBytes: 0,
|
|
1036
|
+
reqTruncated: false,
|
|
1037
|
+
reqComplete: false,
|
|
1038
|
+
truncated: false,
|
|
1039
|
+
resTruncated: false,
|
|
1040
|
+
...access && { access },
|
|
1041
|
+
...noReplay && { noReplay }
|
|
1042
|
+
});
|
|
1043
|
+
return { id, started };
|
|
1044
|
+
}
|
|
1045
|
+
function trackResponse(id, started, up, reqCap) {
|
|
1046
|
+
const resCap = new Capture(TEXT.test(up.headers["content-type"] ?? ""), max);
|
|
1047
|
+
let bytes = 0;
|
|
1048
|
+
up.on("data", (c) => {
|
|
1049
|
+
bytes += c.length;
|
|
1050
|
+
resCap.push(c);
|
|
583
1051
|
});
|
|
1052
|
+
up.on(
|
|
1053
|
+
"end",
|
|
1054
|
+
() => o.store.update(id, {
|
|
1055
|
+
status: up.statusCode ?? null,
|
|
1056
|
+
durationMs: Date.now() - started,
|
|
1057
|
+
bytes,
|
|
1058
|
+
resHeaders: flat(up.headers),
|
|
1059
|
+
resBody: resCap.text(),
|
|
1060
|
+
truncated: !!reqCap?.truncated || resCap.truncated,
|
|
1061
|
+
resTruncated: resCap.truncated,
|
|
1062
|
+
...reqCap && { reqBody: reqCap.text(), reqRaw: reqCap.truncated ? null : reqCap.raw(), reqBodyBytes: reqCap.total, reqTruncated: reqCap.truncated }
|
|
1063
|
+
})
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
function replay(id) {
|
|
1067
|
+
const r = o.store.get(id);
|
|
1068
|
+
const blocked = replayBlock(r);
|
|
1069
|
+
if (blocked || !r) return { ok: false, reason: blocked ?? "Essa requisi\xE7\xE3o j\xE1 saiu da lista." };
|
|
1070
|
+
const route = [...o.routes.values()].find((x) => x.tunnel === r.tunnel);
|
|
1071
|
+
if (!route) return { ok: false, reason: `O t\xFAnel ${r.tunnel} n\xE3o est\xE1 aberto nesta sess\xE3o.` };
|
|
1072
|
+
const newId = nextId++;
|
|
1073
|
+
const started = Date.now();
|
|
1074
|
+
const headers = replayHeaders(r);
|
|
1075
|
+
o.store.add({
|
|
1076
|
+
id: newId,
|
|
1077
|
+
tunnel: r.tunnel,
|
|
1078
|
+
method: r.method,
|
|
1079
|
+
path: r.path,
|
|
1080
|
+
status: null,
|
|
1081
|
+
durationMs: null,
|
|
1082
|
+
bytes: 0,
|
|
1083
|
+
startedAt: started,
|
|
1084
|
+
websocket: false,
|
|
1085
|
+
reqHeaders: headers,
|
|
1086
|
+
resHeaders: {},
|
|
1087
|
+
reqBody: r.reqBody,
|
|
1088
|
+
resBody: null,
|
|
1089
|
+
reqRaw: r.reqRaw,
|
|
1090
|
+
reqBodyBytes: r.reqBodyBytes,
|
|
1091
|
+
reqTruncated: false,
|
|
1092
|
+
reqComplete: true,
|
|
1093
|
+
truncated: false,
|
|
1094
|
+
resTruncated: false,
|
|
1095
|
+
replayOf: r.id
|
|
1096
|
+
});
|
|
1097
|
+
void (async () => {
|
|
1098
|
+
const host = await localHost(route.port);
|
|
1099
|
+
const upstream = request({ host, port: route.port, agent: LOCAL_AGENT, method: r.method, path: r.path, headers }, (up) => trackResponse(newId, started, up));
|
|
1100
|
+
upstream.on("error", (err) => o.store.update(newId, { status: 503, durationMs: Date.now() - started, error: err.message || err.code || err.name }));
|
|
1101
|
+
upstream.end(r.reqRaw ?? void 0);
|
|
1102
|
+
})();
|
|
1103
|
+
return { ok: true, id: newId };
|
|
1104
|
+
}
|
|
1105
|
+
const server = createServer(async (req, res) => {
|
|
1106
|
+
req.pause();
|
|
1107
|
+
const route = await routeFor(req);
|
|
1108
|
+
if (!route) {
|
|
1109
|
+
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
1110
|
+
res.end("T\xFAnel n\xE3o encontrado nesta sess\xE3o");
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
if ((req.url ?? "").startsWith(DUTO_PREFIX)) {
|
|
1114
|
+
if (req.url === WAIT_PATH && req.method === "GET") {
|
|
1115
|
+
if (route.auth?.access(req.headers).kind === "denied") return void deny(req, res, false);
|
|
1116
|
+
return waitForService(res, route.port, o.waitCheckMs ?? 1e3, o.waitPingMs ?? 25e3);
|
|
1117
|
+
}
|
|
1118
|
+
if (route.auth && await authRoute(req, res, route, route.auth)) return;
|
|
1119
|
+
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
1120
|
+
res.end("Rota reservada do Duto");
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
const preflight = !!route.auth && isPreflight(req.method, req.headers);
|
|
1124
|
+
const access = route.auth && !preflight ? route.auth.access(req.headers) : void 0;
|
|
1125
|
+
const headers = delivered(req.headers, route);
|
|
1126
|
+
if (access?.kind === "denied") {
|
|
1127
|
+
const html = wantsHtml(req.headers.accept);
|
|
1128
|
+
const { id: id2, started: started2 } = record(req, route, headers, false, html ? void 0 : access, DENIED_NO_REPLAY);
|
|
1129
|
+
const { status, out, body } = deny(req, res, html);
|
|
1130
|
+
o.store.update(id2, { status, durationMs: Date.now() - started2, bytes: Buffer.byteLength(body), resHeaders: out });
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
const { id, started } = record(req, route, headers, false, access, preflight ? PREFLIGHT_NO_REPLAY : void 0);
|
|
1134
|
+
const reqCap = new Capture(TEXT.test(req.headers["content-type"] ?? ""), max);
|
|
584
1135
|
req.pause();
|
|
585
1136
|
const host = await localHost(route.port);
|
|
586
1137
|
const upstream = request(
|
|
587
1138
|
{ host, port: route.port, agent: LOCAL_AGENT, method: req.method, path: req.url, headers },
|
|
588
1139
|
(up) => {
|
|
589
|
-
|
|
590
|
-
|
|
1140
|
+
if (preflight) {
|
|
1141
|
+
const out = preflightHeaders(up.headers);
|
|
1142
|
+
res.writeHead(up.statusCode ?? 502, out);
|
|
1143
|
+
res.end();
|
|
1144
|
+
up.on("end", () => o.store.update(id, { status: up.statusCode ?? null, durationMs: Date.now() - started, bytes: 0, resHeaders: flat(out) }));
|
|
1145
|
+
up.resume();
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
591
1148
|
if (route.host && route.rewriteRedirects !== false && typeof up.headers.location === "string") {
|
|
592
1149
|
up.headers.location = rewriteLocation(up.headers.location, route.host, hostOf(req));
|
|
593
1150
|
}
|
|
594
1151
|
res.writeHead(up.statusCode ?? 502, up.statusMessage, up.headers);
|
|
595
|
-
|
|
596
|
-
bytes += c.length;
|
|
597
|
-
resCap.push(c);
|
|
598
|
-
});
|
|
1152
|
+
trackResponse(id, started, up, reqCap);
|
|
599
1153
|
up.pipe(res);
|
|
600
|
-
up.on(
|
|
601
|
-
"end",
|
|
602
|
-
() => o.store.update(id, {
|
|
603
|
-
status: up.statusCode ?? null,
|
|
604
|
-
durationMs: Date.now() - started,
|
|
605
|
-
bytes,
|
|
606
|
-
resHeaders: flat(up.headers),
|
|
607
|
-
reqBody: reqCap.text(),
|
|
608
|
-
resBody: resCap.text(),
|
|
609
|
-
truncated: reqCap.truncated || resCap.truncated
|
|
610
|
-
})
|
|
611
|
-
);
|
|
612
1154
|
}
|
|
613
1155
|
);
|
|
614
1156
|
res.on("close", () => {
|
|
615
1157
|
if (!res.writableFinished) upstream.destroy();
|
|
616
1158
|
});
|
|
617
1159
|
upstream.on("error", (err) => {
|
|
1160
|
+
req.resume();
|
|
618
1161
|
if (res.destroyed && res.writableFinished === false && !res.headersSent) return;
|
|
619
1162
|
const html = wantsHtml(req.headers.accept);
|
|
620
1163
|
const msg = html ? unavailablePage({ tunnel: route.tunnel, port: route.port, retry: req.method === "GET" }) : `Nada escutando em localhost:${route.port}. Suba o servidor e tente de novo.`;
|
|
@@ -624,6 +1167,12 @@ async function startProxy(o) {
|
|
|
624
1167
|
} else res.destroy();
|
|
625
1168
|
o.store.update(id, { status: 503, durationMs: Date.now() - started, bytes: res.headersSent ? 0 : Buffer.byteLength(msg), error: err.message || err.code || err.name, reqBody: reqCap.text() });
|
|
626
1169
|
});
|
|
1170
|
+
req.on("end", () => o.store.update(id, { reqComplete: true, reqRaw: reqCap.truncated ? null : reqCap.raw(), reqBodyBytes: reqCap.total, reqTruncated: reqCap.truncated, reqBody: reqCap.text() }));
|
|
1171
|
+
if (preflight) {
|
|
1172
|
+
req.resume();
|
|
1173
|
+
upstream.end();
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
627
1176
|
req.on("data", (c) => reqCap.push(c));
|
|
628
1177
|
req.pipe(upstream);
|
|
629
1178
|
});
|
|
@@ -631,25 +1180,14 @@ async function startProxy(o) {
|
|
|
631
1180
|
client.on("error", () => client.destroy());
|
|
632
1181
|
const route = await routeFor(req);
|
|
633
1182
|
if (!route) return void client.end("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
|
|
634
|
-
const
|
|
635
|
-
const
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
status: null,
|
|
643
|
-
durationMs: null,
|
|
644
|
-
bytes: 0,
|
|
645
|
-
startedAt: started,
|
|
646
|
-
websocket: true,
|
|
647
|
-
reqHeaders: flat(headers),
|
|
648
|
-
resHeaders: {},
|
|
649
|
-
reqBody: null,
|
|
650
|
-
resBody: null,
|
|
651
|
-
truncated: false
|
|
652
|
-
});
|
|
1183
|
+
const access = route.auth?.access(req.headers);
|
|
1184
|
+
const headers = delivered(req.headers, route);
|
|
1185
|
+
const { id, started } = record(req, route, headers, true, access, access?.kind === "denied" ? DENIED_NO_REPLAY : void 0);
|
|
1186
|
+
if (access?.kind === "denied") {
|
|
1187
|
+
client.end(WS_DENIED);
|
|
1188
|
+
o.store.update(id, { status: 401, durationMs: Date.now() - started });
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
653
1191
|
const target = connect2({ host: await localHost(route.port), port: route.port }, () => {
|
|
654
1192
|
let raw = `${req.method} ${req.url} HTTP/${req.httpVersion}\r
|
|
655
1193
|
`;
|
|
@@ -663,8 +1201,8 @@ async function startProxy(o) {
|
|
|
663
1201
|
target.write(`${raw}\r
|
|
664
1202
|
`);
|
|
665
1203
|
if (head.length) target.write(head);
|
|
666
|
-
target.once("data", (
|
|
667
|
-
const status = Number(/^HTTP\/\d\.\d (\d{3})/.exec(
|
|
1204
|
+
target.once("data", (first2) => {
|
|
1205
|
+
const status = Number(/^HTTP\/\d\.\d (\d{3})/.exec(first2.toString("latin1"))?.[1] ?? 0) || null;
|
|
668
1206
|
o.store.update(id, { status, durationMs: Date.now() - started });
|
|
669
1207
|
});
|
|
670
1208
|
target.pipe(client);
|
|
@@ -687,6 +1225,7 @@ async function startProxy(o) {
|
|
|
687
1225
|
});
|
|
688
1226
|
return {
|
|
689
1227
|
port: server.address().port,
|
|
1228
|
+
replay,
|
|
690
1229
|
close: () => new Promise((resolve) => {
|
|
691
1230
|
for (const s of sockets) s.destroy();
|
|
692
1231
|
server.close(() => resolve());
|
|
@@ -696,13 +1235,13 @@ async function startProxy(o) {
|
|
|
696
1235
|
|
|
697
1236
|
// src/session/orphans.ts
|
|
698
1237
|
import { spawnSync } from "child_process";
|
|
699
|
-
import { existsSync as existsSync4, mkdirSync as
|
|
700
|
-
import { join as
|
|
701
|
-
var fileOf = (dir, cfPid) =>
|
|
1238
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
1239
|
+
import { join as join5 } from "path";
|
|
1240
|
+
var fileOf = (dir, cfPid) => join5(dir, `cloudflared-${cfPid}.json`);
|
|
702
1241
|
function recordCloudflared(dir, cfPid, sessionPid = process.pid) {
|
|
703
1242
|
try {
|
|
704
|
-
|
|
705
|
-
|
|
1243
|
+
mkdirSync4(dir, { recursive: true });
|
|
1244
|
+
writeFileSync4(fileOf(dir, cfPid), JSON.stringify({ sessionPid, cfPid, at: Date.now() }));
|
|
706
1245
|
} catch {
|
|
707
1246
|
}
|
|
708
1247
|
}
|
|
@@ -720,7 +1259,7 @@ var realProc = {
|
|
|
720
1259
|
},
|
|
721
1260
|
isCloudflared(pid) {
|
|
722
1261
|
try {
|
|
723
|
-
if (process.platform === "linux") return
|
|
1262
|
+
if (process.platform === "linux") return readFileSync4(`/proc/${pid}/cmdline`, "utf8").includes("cloudflared");
|
|
724
1263
|
if (process.platform === "darwin") return spawnSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8" }).stdout.includes("cloudflared");
|
|
725
1264
|
} catch {
|
|
726
1265
|
return false;
|
|
@@ -739,10 +1278,10 @@ function reapOrphans(dir, proc = realProc) {
|
|
|
739
1278
|
let killed = 0;
|
|
740
1279
|
for (const name of readdirSync(dir)) {
|
|
741
1280
|
if (!/^cloudflared-\d+\.json$/.test(name)) continue;
|
|
742
|
-
const path =
|
|
1281
|
+
const path = join5(dir, name);
|
|
743
1282
|
let rec;
|
|
744
1283
|
try {
|
|
745
|
-
rec = JSON.parse(
|
|
1284
|
+
rec = JSON.parse(readFileSync4(path, "utf8"));
|
|
746
1285
|
} catch {
|
|
747
1286
|
rmSync2(path, { force: true });
|
|
748
1287
|
continue;
|
|
@@ -782,6 +1321,9 @@ var RecordStore = class {
|
|
|
782
1321
|
Object.assign(r, patch);
|
|
783
1322
|
this.emit();
|
|
784
1323
|
}
|
|
1324
|
+
get(id) {
|
|
1325
|
+
return this.items.find((x) => x.id === id);
|
|
1326
|
+
}
|
|
785
1327
|
/** Mais recentes primeiro. Mesma referência até a próxima mudança (serve de snapshot para a tela). */
|
|
786
1328
|
list() {
|
|
787
1329
|
this.snapshot ??= [...this.items].reverse();
|
|
@@ -822,6 +1364,9 @@ var Session = class {
|
|
|
822
1364
|
records = new RecordStore();
|
|
823
1365
|
tunnels = [];
|
|
824
1366
|
routes = /* @__PURE__ */ new Map();
|
|
1367
|
+
specs = /* @__PURE__ */ new Map();
|
|
1368
|
+
/** Um TunnelAuth por túnel, reaproveitado nas reaberturas para não desconectar quem já entrou. */
|
|
1369
|
+
auths = /* @__PURE__ */ new Map();
|
|
825
1370
|
proxy = null;
|
|
826
1371
|
child = null;
|
|
827
1372
|
token = null;
|
|
@@ -866,6 +1411,30 @@ var Session = class {
|
|
|
866
1411
|
const existing = await this.o.api.endpoints().catch(() => []);
|
|
867
1412
|
await this.open(t, existing);
|
|
868
1413
|
}
|
|
1414
|
+
/** Link para o QR: com senha, um token de uso único; sem senha, só a URL. */
|
|
1415
|
+
qrLink(key) {
|
|
1416
|
+
const t = this.tunnels.find((x) => x.key === key);
|
|
1417
|
+
if (!t?.url) return null;
|
|
1418
|
+
const auth = this.auths.get(key);
|
|
1419
|
+
if (!auth) return { url: t.url, expiresAt: null };
|
|
1420
|
+
const { token, expiresAt } = auth.issueQrToken();
|
|
1421
|
+
return { url: `${t.url}/__duto/qr/${token}`, expiresAt };
|
|
1422
|
+
}
|
|
1423
|
+
/** Cria o TunnelAuth na primeira abertura (precisa do allow resolvido) e o mantém depois. */
|
|
1424
|
+
async authFor(key) {
|
|
1425
|
+
const existing = this.auths.get(key);
|
|
1426
|
+
if (existing) return existing;
|
|
1427
|
+
const spec = this.specs.get(key);
|
|
1428
|
+
if (!spec?.auth?.length) return void 0;
|
|
1429
|
+
const allow = spec.allow?.length ? await (this.o.resolveAllow ?? resolveAllow)(spec.allow) : void 0;
|
|
1430
|
+
const auth = new TunnelAuth({ tunnel: key, credentials: spec.auth, allow });
|
|
1431
|
+
this.auths.set(key, auth);
|
|
1432
|
+
return auth;
|
|
1433
|
+
}
|
|
1434
|
+
/** Reenvia uma requisição da lista ao serviço local. */
|
|
1435
|
+
replay(id) {
|
|
1436
|
+
return this.proxy ? this.proxy.replay(id) : { ok: false, reason: "Sess\xE3o ainda n\xE3o come\xE7ou." };
|
|
1437
|
+
}
|
|
869
1438
|
async stop() {
|
|
870
1439
|
if (this.stopped) return;
|
|
871
1440
|
this.stopped = true;
|
|
@@ -878,7 +1447,8 @@ var Session = class {
|
|
|
878
1447
|
this.emit();
|
|
879
1448
|
}
|
|
880
1449
|
blank(spec, adHoc) {
|
|
881
|
-
|
|
1450
|
+
this.specs.set(keyOf(spec), spec);
|
|
1451
|
+
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 } : {}, ...spec.auth?.length ? { auth: true } : {}, url: null, hostname: null, endpointId: null, status: "opening", error: null, adHoc, requests: 0, errors: 0 };
|
|
882
1452
|
}
|
|
883
1453
|
async open(t, existing, retrying = false) {
|
|
884
1454
|
if (this.stopped) return;
|
|
@@ -898,11 +1468,16 @@ var Session = class {
|
|
|
898
1468
|
await withTimeout(this.o.api.closeTunnel(res.endpointId).catch(() => void 0), CLOSE_TIMEOUT_MS);
|
|
899
1469
|
return;
|
|
900
1470
|
}
|
|
1471
|
+
const auth = await this.authFor(t.key);
|
|
1472
|
+
if (this.stopped) {
|
|
1473
|
+
await withTimeout(this.o.api.closeTunnel(res.endpointId).catch(() => void 0), CLOSE_TIMEOUT_MS);
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
901
1476
|
t.url = res.url;
|
|
902
1477
|
t.hostname = res.hostname;
|
|
903
1478
|
t.endpointId = res.endpointId;
|
|
904
1479
|
t.error = null;
|
|
905
|
-
this.routes.set(res.hostname.toLowerCase(), { tunnel: t.key, port: t.port, host: t.host, headers: t.headers, rewriteRedirects: t.rewriteRedirects });
|
|
1480
|
+
this.routes.set(res.hostname.toLowerCase(), { tunnel: t.key, port: t.port, host: t.host, headers: t.headers, rewriteRedirects: t.rewriteRedirects, auth });
|
|
906
1481
|
t.status = await isPortListening(t.port) ? "live" : "no-listener";
|
|
907
1482
|
this.ensureCloudflared(res.tunnelToken);
|
|
908
1483
|
this.emit();
|
|
@@ -1028,7 +1603,7 @@ function runPlain(session, out) {
|
|
|
1028
1603
|
for (const t of session.getState().tunnels) {
|
|
1029
1604
|
if ((t.status === "live" || t.status === "no-listener") && t.url && !announced.has(t.key)) {
|
|
1030
1605
|
announced.add(t.key);
|
|
1031
|
-
out(`${paint("signal", "\u25CF")} ${t.key} ${t.url} \u2192 ${localLabel(t)}`);
|
|
1606
|
+
out(`${paint("signal", "\u25CF")} ${t.key} ${t.url}${t.auth ? " \u{1F512}" : ""} \u2192 ${localLabel(t)}`);
|
|
1032
1607
|
}
|
|
1033
1608
|
if (t.status === "failed" && t.error && !failed.has(t.key)) {
|
|
1034
1609
|
failed.add(t.key);
|
|
@@ -1078,6 +1653,23 @@ function parseHostFlag(raw) {
|
|
|
1078
1653
|
if (raw === void 0 || raw === true || raw === "") return void 0;
|
|
1079
1654
|
return parseHost(String(raw)) ?? fail2("Host inv\xE1lido: use s\xF3 o dom\xEDnio, como meuapp.test");
|
|
1080
1655
|
}
|
|
1656
|
+
function listFlag(raw) {
|
|
1657
|
+
if (raw === void 0) return [];
|
|
1658
|
+
return (Array.isArray(raw) ? raw : [raw]).map((v) => v === true ? "" : String(v));
|
|
1659
|
+
}
|
|
1660
|
+
function parseAuthFlags(raw) {
|
|
1661
|
+
const out = [];
|
|
1662
|
+
for (const entry of listFlag(raw)) {
|
|
1663
|
+
const r = parseCredential(entry);
|
|
1664
|
+
if (!r.ok) fail2(r.reason === "password" ? "Senha precisa ter de 8 a 128 caracteres" : r.reason === "user" ? "Usu\xE1rio inv\xE1lido: de 1 a 128 caracteres, sem espa\xE7o" : "Credencial inv\xE1lida: use usuario:senha");
|
|
1665
|
+
if (out.some((c) => c.user === r.cred.user)) fail2(`Usu\xE1rio "${r.cred.user}" aparece duas vezes`);
|
|
1666
|
+
out.push(r.cred);
|
|
1667
|
+
}
|
|
1668
|
+
return out;
|
|
1669
|
+
}
|
|
1670
|
+
function parseAllowFlags(raw) {
|
|
1671
|
+
return listFlag(raw).map((v) => parseAllowEntry(v) ? v : fail2(`"${v}" n\xE3o \xE9 um nome conhecido (stripe, github) nem uma faixa de IP`));
|
|
1672
|
+
}
|
|
1081
1673
|
function wantsUi(flags) {
|
|
1082
1674
|
if (flags.ui === false) return false;
|
|
1083
1675
|
if (!process.stdout.isTTY || !process.stdin.isTTY) return false;
|
|
@@ -1094,12 +1686,12 @@ async function runSession(specs, source, flags) {
|
|
|
1094
1686
|
} catch (err) {
|
|
1095
1687
|
fail2(err instanceof Error ? err.message : String(err));
|
|
1096
1688
|
}
|
|
1097
|
-
const session = new Session({ api, deviceId: config.deviceId, deviceName: hostname(), cloudflaredPath, source, runDir:
|
|
1689
|
+
const session = new Session({ api, deviceId: config.deviceId, deviceName: hostname(), cloudflaredPath, source, runDir: join6(dutoHome(), "run") });
|
|
1098
1690
|
if (wantsUi(flags)) {
|
|
1099
|
-
const { runFullscreen } = await import("./fullscreen-
|
|
1100
|
-
process.exit(await runFullscreen(session, specs, "0.
|
|
1691
|
+
const { runFullscreen } = await import("./fullscreen-7P5GBCQX.js");
|
|
1692
|
+
process.exit(await runFullscreen(session, specs, "0.6.0"));
|
|
1101
1693
|
}
|
|
1102
|
-
console.log(`${mark()} ${paint("muted", `${"0.
|
|
1694
|
+
console.log(`${mark()} ${paint("muted", `${"0.6.0"} \xB7 ${source} \xB7 Ctrl+C para encerrar`)}`);
|
|
1103
1695
|
const stopLog = runPlain(session, (l) => console.log(l));
|
|
1104
1696
|
const stopped = new Promise((resolve) => {
|
|
1105
1697
|
const onSignal = () => {
|
|
@@ -1121,6 +1713,8 @@ async function runSession(specs, source, flags) {
|
|
|
1121
1713
|
async function tunnel(rawPort, flags) {
|
|
1122
1714
|
if (rawPort === void 0) {
|
|
1123
1715
|
if (flags.host !== void 0) fail2("Use --host junto com a porta (duto 8080 --host meuapp.test) ou host: no duto.yml");
|
|
1716
|
+
if (flags.auth !== void 0) fail2("Use --auth junto com a porta (duto 3000 --auth ana:senha) ou auth: no duto.yml");
|
|
1717
|
+
if (flags.permitir !== void 0) fail2("Use --permitir junto com a porta (duto 3000 --permitir stripe) ou permitir: no duto.yml");
|
|
1124
1718
|
let found;
|
|
1125
1719
|
try {
|
|
1126
1720
|
found = findConfig(process.cwd());
|
|
@@ -1145,11 +1739,14 @@ ${paint("signal", "Dica:")} crie um duto.yml com os t\xFAneis do projeto e rode
|
|
|
1145
1739
|
const host = parseHostFlag(flags.host);
|
|
1146
1740
|
const keepRedirects = flags.rewriteRedirects === false;
|
|
1147
1741
|
if (keepRedirects && !host) fail2("--no-rewrite-redirects s\xF3 vale junto com --host");
|
|
1148
|
-
|
|
1742
|
+
const auth = parseAuthFlags(flags.auth);
|
|
1743
|
+
const allow = parseAllowFlags(flags.permitir);
|
|
1744
|
+
if (allow.length && !auth.length) fail2("--permitir s\xF3 vale junto com --auth");
|
|
1745
|
+
return runSession([{ name, port, ...host ? { host } : {}, ...keepRedirects ? { rewriteRedirects: false } : {}, ...auth.length ? { auth } : {}, ...allow.length ? { allow } : {} }], "linha de comando", flags);
|
|
1149
1746
|
}
|
|
1150
1747
|
var cli = cac("duto");
|
|
1151
|
-
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));
|
|
1152
|
-
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));
|
|
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));
|
|
1153
1750
|
cli.command("login <token>", "Salva o authtoken (pegue em https://duto.dev)").action(async (token) => {
|
|
1154
1751
|
try {
|
|
1155
1752
|
const me = await createApiClient({ baseUrl: apiUrl(), token: String(token) }).me();
|
|
@@ -1188,7 +1785,7 @@ cli.help(
|
|
|
1188
1785
|
body: s.body.replace(/^\s*For more info, run any command with the `--help` flag:/m, " Mais detalhes de um comando: duto <comando> --help").replace("Display this message", "Mostra esta ajuda").replace("Display version number", "Mostra a vers\xE3o")
|
|
1189
1786
|
}))
|
|
1190
1787
|
);
|
|
1191
|
-
cli.version("0.
|
|
1788
|
+
cli.version("0.6.0");
|
|
1192
1789
|
try {
|
|
1193
1790
|
cli.parse(process.argv, { run: false });
|
|
1194
1791
|
await cli.runMatchedCommand();
|