@dutodev/cli 1.8.1 → 1.9.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.
|
@@ -644,6 +644,8 @@ function reasonOf(c) {
|
|
|
644
644
|
if (c.source === "headers") return c.kind === "remove" ? 'headers: ""' : "headers:";
|
|
645
645
|
if (c.source === "host") return "host:";
|
|
646
646
|
if (c.source === "visitor") return "from the visitor; set trust_forwarded: true to keep it";
|
|
647
|
+
if (c.source === "isolation" && c.name === "set-cookie") return "Domain=duto.page reaches every user's tunnel; kept on this address";
|
|
648
|
+
if (c.source === "isolation") return "from another user's tunnel; only SameSite=None cookies go through";
|
|
647
649
|
return "Duto credential";
|
|
648
650
|
}
|
|
649
651
|
function valueOf(c) {
|
package/dist/index.js
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
updateLine,
|
|
29
29
|
validateName,
|
|
30
30
|
warn
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-QMQWGCIE.js";
|
|
32
32
|
|
|
33
33
|
// src/index.ts
|
|
34
34
|
import { hostname as hostname2 } from "os";
|
|
@@ -36,7 +36,7 @@ import { join as join7, relative } from "path";
|
|
|
36
36
|
import cac from "cac";
|
|
37
37
|
|
|
38
38
|
// src/version.ts
|
|
39
|
-
var CLI_VERSION = "1.
|
|
39
|
+
var CLI_VERSION = "1.9.0";
|
|
40
40
|
|
|
41
41
|
// src/api.ts
|
|
42
42
|
var DutoApiError = class extends Error {
|
|
@@ -561,7 +561,7 @@ async function browserLogin(o) {
|
|
|
561
561
|
const start = await api.startLogin({
|
|
562
562
|
deviceId: o.deviceId,
|
|
563
563
|
deviceName: hostname(),
|
|
564
|
-
cliVersion: "1.
|
|
564
|
+
cliVersion: "1.9.0",
|
|
565
565
|
nodeVersion: process.version,
|
|
566
566
|
os: process.platform,
|
|
567
567
|
arch: process.arch
|
|
@@ -2436,6 +2436,89 @@ function deliver(h, route) {
|
|
|
2436
2436
|
return changes.length ? { headers: out, changes } : { headers: h, changes };
|
|
2437
2437
|
}
|
|
2438
2438
|
|
|
2439
|
+
// src/session/siteIsolation.ts
|
|
2440
|
+
function firstLabel(host) {
|
|
2441
|
+
return host.split(".")[0] ?? "";
|
|
2442
|
+
}
|
|
2443
|
+
function parentDomain(host) {
|
|
2444
|
+
return host.slice(host.indexOf(".") + 1).toLowerCase();
|
|
2445
|
+
}
|
|
2446
|
+
function tunnelOwner(host) {
|
|
2447
|
+
const label = firstLabel(host.toLowerCase());
|
|
2448
|
+
return label.slice(label.lastIndexOf("-") + 1);
|
|
2449
|
+
}
|
|
2450
|
+
function originHost(value) {
|
|
2451
|
+
if (!value) return null;
|
|
2452
|
+
try {
|
|
2453
|
+
const url = new URL(value);
|
|
2454
|
+
return { origin: url.origin, host: url.hostname.toLowerCase() };
|
|
2455
|
+
} catch {
|
|
2456
|
+
return null;
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
function isSiblingTunnel(host, publicHost) {
|
|
2460
|
+
const parent = parentDomain(publicHost);
|
|
2461
|
+
if (!host.endsWith(`.${parent}`)) return false;
|
|
2462
|
+
return !host.slice(0, -parent.length - 1).includes(".");
|
|
2463
|
+
}
|
|
2464
|
+
function crossUserOrigin(h, publicHost) {
|
|
2465
|
+
const site = h["sec-fetch-site"];
|
|
2466
|
+
if (site !== void 0 && site !== "same-site") return null;
|
|
2467
|
+
const from = originHost(h.origin) ?? originHost(h.referer);
|
|
2468
|
+
if (!from || !isSiblingTunnel(from.host, publicHost)) return null;
|
|
2469
|
+
return tunnelOwner(from.host) === tunnelOwner(publicHost) ? null : from.origin;
|
|
2470
|
+
}
|
|
2471
|
+
function scopeSetCookies(lines, publicHost) {
|
|
2472
|
+
const parent = parentDomain(publicHost);
|
|
2473
|
+
const changes = [];
|
|
2474
|
+
const scoped = lines.map((line) => {
|
|
2475
|
+
const parts = line.split(";");
|
|
2476
|
+
const kept = parts.filter((part, i) => {
|
|
2477
|
+
if (i === 0) return true;
|
|
2478
|
+
const [key = "", value = ""] = part.split("=");
|
|
2479
|
+
const shared = key.trim().toLowerCase() === "domain" && value.trim().replace(/^\./, "").toLowerCase() === parent;
|
|
2480
|
+
if (shared) changes.push({ kind: "remove", name: "set-cookie", from: `${parseSetCookie(line).name}: ${part.trim()}`, source: "isolation" });
|
|
2481
|
+
return !shared;
|
|
2482
|
+
});
|
|
2483
|
+
return kept.length === parts.length ? line : kept.join(";");
|
|
2484
|
+
});
|
|
2485
|
+
return { lines: scoped, changes };
|
|
2486
|
+
}
|
|
2487
|
+
var SameSiteMemory = class {
|
|
2488
|
+
byName = /* @__PURE__ */ new Map();
|
|
2489
|
+
learn(setCookies) {
|
|
2490
|
+
for (const line of setCookies) {
|
|
2491
|
+
const c = parseSetCookie(line);
|
|
2492
|
+
const value = typeof c.attrs.samesite === "string" ? c.attrs.samesite.toLowerCase() : "lax";
|
|
2493
|
+
this.byName.set(c.name, value === "none" || value === "strict" ? value : "lax");
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
sentCrossSite(name, navigation) {
|
|
2497
|
+
const sameSite = this.byName.get(name) ?? "lax";
|
|
2498
|
+
return sameSite === "none" || navigation && sameSite === "lax";
|
|
2499
|
+
}
|
|
2500
|
+
};
|
|
2501
|
+
function isNavigation(h, method) {
|
|
2502
|
+
if (method !== "GET" && method !== "HEAD") return false;
|
|
2503
|
+
const mode = h["sec-fetch-mode"];
|
|
2504
|
+
return mode === "navigate" || mode === void 0 && h.origin === void 0;
|
|
2505
|
+
}
|
|
2506
|
+
function isolateCookies(h, publicHost, memory, method = "GET") {
|
|
2507
|
+
const cookie = h.cookie;
|
|
2508
|
+
const origin = cookie ? crossUserOrigin(h, publicHost) : null;
|
|
2509
|
+
if (!cookie || !origin) return { headers: h, changes: [] };
|
|
2510
|
+
const navigation = isNavigation(h, method);
|
|
2511
|
+
const pairs = cookie.split(";").map((p) => p.trim()).filter(Boolean);
|
|
2512
|
+
const nameOf = (pair2) => pair2.split("=")[0].trim();
|
|
2513
|
+
const kept = pairs.filter((p) => memory.sentCrossSite(nameOf(p), navigation));
|
|
2514
|
+
const removed = pairs.filter((p) => !kept.includes(p)).map(nameOf);
|
|
2515
|
+
if (removed.length === 0) return { headers: h, changes: [] };
|
|
2516
|
+
const headers = { ...h };
|
|
2517
|
+
if (kept.length) headers.cookie = kept.join("; ");
|
|
2518
|
+
else delete headers.cookie;
|
|
2519
|
+
return { headers, changes: [{ kind: "remove", name: "cookie", from: `${removed.join(", ")} (from ${origin})`, source: "isolation" }] };
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2439
2522
|
// src/session/errorPage.ts
|
|
2440
2523
|
var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
2441
2524
|
function wantsHtml(accept) {
|
|
@@ -2731,6 +2814,26 @@ function waitForService(res, port, checkMs, pingMs) {
|
|
|
2731
2814
|
async function startProxy(o) {
|
|
2732
2815
|
const max = o.maxBody ?? MAX_BODY;
|
|
2733
2816
|
let nextId = 1;
|
|
2817
|
+
const sameSite = /* @__PURE__ */ new Map();
|
|
2818
|
+
function memoryOf(route) {
|
|
2819
|
+
let memory = sameSite.get(route.tunnel);
|
|
2820
|
+
if (!memory) sameSite.set(route.tunnel, memory = new SameSiteMemory());
|
|
2821
|
+
return memory;
|
|
2822
|
+
}
|
|
2823
|
+
function deliverFor(req, route) {
|
|
2824
|
+
const delivered = deliver(req.headers, route);
|
|
2825
|
+
const isolated = isolateCookies(delivered.headers, hostOf(req), memoryOf(route), req.method ?? "GET");
|
|
2826
|
+
return isolated.changes.length ? { headers: isolated.headers, changes: [...delivered.changes, ...isolated.changes] } : delivered;
|
|
2827
|
+
}
|
|
2828
|
+
function scopeResponseCookies(id, req, route, up) {
|
|
2829
|
+
const lines = up.headers["set-cookie"];
|
|
2830
|
+
if (!lines?.length) return;
|
|
2831
|
+
memoryOf(route).learn(lines);
|
|
2832
|
+
const scoped = scopeSetCookies(lines, hostOf(req));
|
|
2833
|
+
if (!scoped.changes.length) return;
|
|
2834
|
+
up.headers["set-cookie"] = scoped.lines;
|
|
2835
|
+
o.store.update(id, { headerChanges: [...o.store.get(id)?.headerChanges ?? [], ...scoped.changes] });
|
|
2836
|
+
}
|
|
2734
2837
|
const routeWaitMs = o.routeWaitMs ?? DEFAULT_ROUTE_WAIT_MS;
|
|
2735
2838
|
async function routeFor(req) {
|
|
2736
2839
|
const host = hostOf(req);
|
|
@@ -2860,7 +2963,7 @@ async function startProxy(o) {
|
|
|
2860
2963
|
}
|
|
2861
2964
|
const preflight = !!route.auth && isPreflight(req.method, req.headers);
|
|
2862
2965
|
const access = route.auth && !preflight ? route.auth.access(req.headers) : void 0;
|
|
2863
|
-
const { headers, changes } =
|
|
2966
|
+
const { headers, changes } = deliverFor(req, route);
|
|
2864
2967
|
if (access?.kind === "denied") {
|
|
2865
2968
|
const html = wantsHtml(req.headers.accept);
|
|
2866
2969
|
const { id: id2, started: started2 } = record(req, route, headers, changes, false, html ? void 0 : access, DENIED_NO_REPLAY);
|
|
@@ -2894,6 +2997,7 @@ async function startProxy(o) {
|
|
|
2894
2997
|
o.store.update(id, { redirect: { from, to } });
|
|
2895
2998
|
}
|
|
2896
2999
|
}
|
|
3000
|
+
scopeResponseCookies(id, req, route, up);
|
|
2897
3001
|
res.writeHead(up.statusCode ?? 502, up.statusMessage, up.headers);
|
|
2898
3002
|
trackResponse(id, started, up, reqCap);
|
|
2899
3003
|
up.pipe(res);
|
|
@@ -2937,7 +3041,7 @@ async function startProxy(o) {
|
|
|
2937
3041
|
const route = await routeFor(req);
|
|
2938
3042
|
if (!route) return void client.end("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
|
|
2939
3043
|
const access = route.auth?.access(req.headers);
|
|
2940
|
-
const { headers, changes } =
|
|
3044
|
+
const { headers, changes } = deliverFor(req, route);
|
|
2941
3045
|
const { id, started } = record(req, route, headers, changes, true, access, access?.kind === "denied" ? DENIED_NO_REPLAY : void 0);
|
|
2942
3046
|
if (access?.kind === "denied") {
|
|
2943
3047
|
client.end(WS_DENIED);
|
|
@@ -3584,7 +3688,7 @@ async function runSession(specs, source, flags, configPath) {
|
|
|
3584
3688
|
const warnings = credentialWarnings(specs);
|
|
3585
3689
|
const watcher = configPath ? watchConfig({ path: configPath, onChange: (next) => void session.apply(next).catch((err) => session.configFailed(err instanceof Error ? err.message : String(err))), onError: (error) => session.configFailed(error) }) : null;
|
|
3586
3690
|
if (wantsUi(flags)) {
|
|
3587
|
-
const { runFullscreen } = await import("./fullscreen-
|
|
3691
|
+
const { runFullscreen } = await import("./fullscreen-GM4ZV6FW.js");
|
|
3588
3692
|
const code = await runFullscreen(session, specs, CLI_VERSION, inspector, inspectorError, update, warnings);
|
|
3589
3693
|
watcher?.close();
|
|
3590
3694
|
await inspector?.close().catch(() => void 0);
|