@dutodev/cli 1.2.0 → 1.4.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
CHANGED
|
@@ -203,3 +203,4 @@ The page has the list on the left and the details on the right, with the tabs **
|
|
|
203
203
|
|
|
204
204
|
- Config lives in `~/.duto/config.json` (mode 600). `DUTO_HOME` changes that folder.
|
|
205
205
|
- `DUTO_API_URL` points the CLI at another server (default: `https://duto.dev`).
|
|
206
|
+
- Once a day, duto asks npm for the latest version and shows a line when there is a newer one. `DUTO_NO_UPDATE_CHECK=1` turns that off.
|
|
@@ -388,6 +388,107 @@ function placeOf(colo) {
|
|
|
388
388
|
return entry ? { city: entry[0], country: entry[1] } : null;
|
|
389
389
|
}
|
|
390
390
|
|
|
391
|
+
// ../shared/src/cliVersion.ts
|
|
392
|
+
var CLI_VERSION_HEADER = "x-duto-cli-version";
|
|
393
|
+
var CLI_USER_AGENT = "duto-cli";
|
|
394
|
+
var CLI_UPDATE_COMMAND = "npm i -g @dutodev/cli";
|
|
395
|
+
var VERSION = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/;
|
|
396
|
+
function parts(version) {
|
|
397
|
+
const m = VERSION.exec(version.trim());
|
|
398
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
399
|
+
}
|
|
400
|
+
function compareVersions(a, b) {
|
|
401
|
+
const x = parts(a);
|
|
402
|
+
const y = parts(b);
|
|
403
|
+
if (!x || !y) return null;
|
|
404
|
+
for (let i = 0; i < 3; i++) {
|
|
405
|
+
const d = (x[i] ?? 0) - (y[i] ?? 0);
|
|
406
|
+
if (d !== 0) return d;
|
|
407
|
+
}
|
|
408
|
+
return 0;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/config.ts
|
|
412
|
+
import { randomUUID } from "crypto";
|
|
413
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
414
|
+
import { homedir } from "os";
|
|
415
|
+
import { join } from "path";
|
|
416
|
+
function dutoHome() {
|
|
417
|
+
return process.env.DUTO_HOME || join(homedir(), ".duto");
|
|
418
|
+
}
|
|
419
|
+
function configPath() {
|
|
420
|
+
return join(dutoHome(), "config.json");
|
|
421
|
+
}
|
|
422
|
+
function apiUrl() {
|
|
423
|
+
return (process.env.DUTO_API_URL || "https://duto.dev").replace(/\/+$/, "");
|
|
424
|
+
}
|
|
425
|
+
function saveConfig(c) {
|
|
426
|
+
mkdirSync(dutoHome(), { recursive: true, mode: 448 });
|
|
427
|
+
const path = configPath();
|
|
428
|
+
writeFileSync(path, JSON.stringify(c, null, 2) + "\n", { mode: 384 });
|
|
429
|
+
chmodSync(path, 384);
|
|
430
|
+
}
|
|
431
|
+
function loadConfig() {
|
|
432
|
+
const path = configPath();
|
|
433
|
+
if (existsSync(path)) {
|
|
434
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
435
|
+
if (parsed.deviceId) return parsed;
|
|
436
|
+
const fixed = { ...parsed, deviceId: randomUUID() };
|
|
437
|
+
saveConfig(fixed);
|
|
438
|
+
return fixed;
|
|
439
|
+
}
|
|
440
|
+
const fresh = { deviceId: randomUUID() };
|
|
441
|
+
saveConfig(fresh);
|
|
442
|
+
return fresh;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// src/updates.ts
|
|
446
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
447
|
+
import { join as join2 } from "path";
|
|
448
|
+
var LATEST_URL = "https://registry.npmjs.org/@dutodev/cli/latest";
|
|
449
|
+
var CHECK_EVERY_MS = 24 * 60 * 60 * 1e3;
|
|
450
|
+
var TIMEOUT_MS = 3e3;
|
|
451
|
+
async function fetchLatest(fetchImpl) {
|
|
452
|
+
const res = await fetchImpl(LATEST_URL, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(TIMEOUT_MS) });
|
|
453
|
+
if (!res.ok) return null;
|
|
454
|
+
const { version } = await res.json();
|
|
455
|
+
return typeof version === "string" && compareVersions(version, version) === 0 ? version : null;
|
|
456
|
+
}
|
|
457
|
+
async function availableUpdate(o) {
|
|
458
|
+
const cached = o.read();
|
|
459
|
+
let latest = cached?.latest ?? null;
|
|
460
|
+
if (!cached || o.now - cached.checkedAt >= CHECK_EVERY_MS) {
|
|
461
|
+
latest = await fetchLatest(o.fetchImpl ?? fetch).catch(() => null) ?? latest;
|
|
462
|
+
o.write({ checkedAt: o.now, latest });
|
|
463
|
+
}
|
|
464
|
+
const diff = latest ? compareVersions(latest, o.current) : null;
|
|
465
|
+
return diff !== null && diff > 0 ? latest : null;
|
|
466
|
+
}
|
|
467
|
+
var cachePath = () => join2(dutoHome(), "update.json");
|
|
468
|
+
function readUpdateCache() {
|
|
469
|
+
try {
|
|
470
|
+
const c = JSON.parse(readFileSync2(cachePath(), "utf8"));
|
|
471
|
+
return typeof c.checkedAt === "number" ? { checkedAt: c.checkedAt, latest: typeof c.latest === "string" ? c.latest : null } : null;
|
|
472
|
+
} catch {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function writeUpdateCache(cache) {
|
|
477
|
+
try {
|
|
478
|
+
mkdirSync2(dutoHome(), { recursive: true, mode: 448 });
|
|
479
|
+
writeFileSync2(cachePath(), JSON.stringify(cache) + "\n", { mode: 384 });
|
|
480
|
+
} catch {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function checkForUpdate(current) {
|
|
485
|
+
if (process.env.DUTO_NO_UPDATE_CHECK) return Promise.resolve(null);
|
|
486
|
+
return availableUpdate({ current, now: Date.now(), read: readUpdateCache, write: writeUpdateCache }).catch(() => null);
|
|
487
|
+
}
|
|
488
|
+
function updateLine(latest) {
|
|
489
|
+
return `duto ${latest} is available. Update: ${CLI_UPDATE_COMMAND}`;
|
|
490
|
+
}
|
|
491
|
+
|
|
391
492
|
// src/ui/theme.ts
|
|
392
493
|
var C = {
|
|
393
494
|
signal: "#2dd4bf",
|
|
@@ -449,6 +550,12 @@ export {
|
|
|
449
550
|
normalizeName,
|
|
450
551
|
validateName,
|
|
451
552
|
placeOf,
|
|
553
|
+
CLI_VERSION_HEADER,
|
|
554
|
+
CLI_USER_AGENT,
|
|
555
|
+
dutoHome,
|
|
556
|
+
apiUrl,
|
|
557
|
+
saveConfig,
|
|
558
|
+
loadConfig,
|
|
452
559
|
C,
|
|
453
560
|
tunnelColor,
|
|
454
561
|
fit,
|
|
@@ -458,5 +565,7 @@ export {
|
|
|
458
565
|
formatDuration,
|
|
459
566
|
formatBytes,
|
|
460
567
|
formatTime,
|
|
461
|
-
localLabel
|
|
568
|
+
localLabel,
|
|
569
|
+
checkForUpdate,
|
|
570
|
+
updateLine
|
|
462
571
|
};
|
|
@@ -12,8 +12,9 @@ import {
|
|
|
12
12
|
normalizeName,
|
|
13
13
|
statusColor,
|
|
14
14
|
tunnelColor,
|
|
15
|
+
updateLine,
|
|
15
16
|
validateName
|
|
16
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-4SIBC52A.js";
|
|
17
18
|
|
|
18
19
|
// src/ui/fullscreen.tsx
|
|
19
20
|
import { render } from "ink";
|
|
@@ -75,6 +76,17 @@ function Detail({ record }) {
|
|
|
75
76
|
record.replayOf
|
|
76
77
|
] }),
|
|
77
78
|
record.error && /* @__PURE__ */ jsx(Text, { color: C.err, children: record.error }),
|
|
79
|
+
record.redirect && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
80
|
+
/* @__PURE__ */ jsx(Text, { color: C.signal, children: "\u21AA Location rewritten" }),
|
|
81
|
+
/* @__PURE__ */ jsxs(Text, { color: C.muted, children: [
|
|
82
|
+
" ",
|
|
83
|
+
record.redirect.from
|
|
84
|
+
] }),
|
|
85
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
86
|
+
" \u2192 ",
|
|
87
|
+
record.redirect.to
|
|
88
|
+
] })
|
|
89
|
+
] }),
|
|
78
90
|
/* @__PURE__ */ jsx(Section, { title: "Request", headers: record.reqHeaders, body: record.reqBody, truncated: record.reqTruncated, note: bodyNote(record, "req") }),
|
|
79
91
|
/* @__PURE__ */ jsx(Section, { title: "Response", headers: record.resHeaders, body: record.resBody, truncated: record.resTruncated, note: bodyNote(record, "res") }),
|
|
80
92
|
/* @__PURE__ */ jsxs(Text, { color: C.muted, children: [
|
|
@@ -168,6 +180,7 @@ function QrView({ tunnel, link, now }) {
|
|
|
168
180
|
// src/ui/RequestList.tsx
|
|
169
181
|
import { Box as Box3, Text as Text4 } from "ink";
|
|
170
182
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
183
|
+
var REWRITTEN = " \u21AA rewritten";
|
|
171
184
|
function accessLabel(a) {
|
|
172
185
|
if (!a) return null;
|
|
173
186
|
if (a.kind === "user") return { text: a.user, color: C.muted };
|
|
@@ -179,7 +192,8 @@ function RequestList(props) {
|
|
|
179
192
|
const start = Math.max(0, Math.min(selected - Math.floor(height / 2), records.length - height));
|
|
180
193
|
const visible = records.slice(start, start + height);
|
|
181
194
|
const accessW = Math.max(0, ...visible.map((r) => (accessLabel(r.access)?.text.length ?? -1) + 1));
|
|
182
|
-
const
|
|
195
|
+
const redirectW = visible.some((r) => r.redirect) ? REWRITTEN.length : 0;
|
|
196
|
+
const pathW = Math.max(10, width - 54 - accessW - redirectW);
|
|
183
197
|
const title = props.filter ? `REQUESTS \xB7 filter: ${props.filter} ` : "REQUESTS ";
|
|
184
198
|
return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", children: [
|
|
185
199
|
/* @__PURE__ */ jsx4(Text4, { color: C.muted, children: title + "\u2500".repeat(Math.max(0, width - title.length)) }),
|
|
@@ -203,7 +217,8 @@ function RequestList(props) {
|
|
|
203
217
|
/* @__PURE__ */ jsx4(Text4, { color: C.muted, children: tail.padStart(7) }),
|
|
204
218
|
" ",
|
|
205
219
|
/* @__PURE__ */ jsx4(Text4, { color: C.muted, children: r.websocket ? "" : formatBytes(r.bytes).padStart(8) }),
|
|
206
|
-
acc ? /* @__PURE__ */ jsx4(Text4, { color: acc.color, children: ` ${acc.text}` }) : null
|
|
220
|
+
acc ? /* @__PURE__ */ jsx4(Text4, { color: acc.color, children: ` ${acc.text}` }) : null,
|
|
221
|
+
r.redirect ? /* @__PURE__ */ jsx4(Text4, { color: C.signal, children: REWRITTEN }) : null
|
|
207
222
|
] }, r.id);
|
|
208
223
|
})
|
|
209
224
|
] });
|
|
@@ -284,7 +299,7 @@ var KEYS = [
|
|
|
284
299
|
["tab", "focus"],
|
|
285
300
|
["q", "quit"]
|
|
286
301
|
];
|
|
287
|
-
function App({ session, version, system, onExit, inspector = null, inspectorError }) {
|
|
302
|
+
function App({ session, version, system, onExit, inspector = null, inspectorError, update = null }) {
|
|
288
303
|
const subState = useCallback((fn) => session.subscribe(fn), [session]);
|
|
289
304
|
const subRecords = useCallback((fn) => session.records.subscribe(fn), [session]);
|
|
290
305
|
const state = useSyncExternalStore(subState, () => session.getState());
|
|
@@ -435,7 +450,7 @@ function App({ session, version, system, onExit, inspector = null, inspectorErro
|
|
|
435
450
|
});
|
|
436
451
|
const live = state.tunnels.filter((t) => t.status === "live").length;
|
|
437
452
|
const conn = state.connection === "online" ? { text: "connected \u25CF", color: C.signal } : state.connection === "connecting" ? { text: "connecting\u2026", color: C.muted } : { text: "reconnecting\u2026", color: C.warn };
|
|
438
|
-
const listHeight = Math.max(3, rows - state.tunnels.length - (inspector ? 10 : 9));
|
|
453
|
+
const listHeight = Math.max(3, rows - state.tunnels.length - (inspector ? 10 : 9) - (update ? 1 : 0));
|
|
439
454
|
const detailRecord = mode.kind === "detail" ? all.find((r) => r.id === mode.id) : void 0;
|
|
440
455
|
const qrTunnel = mode.kind === "qr" ? state.tunnels.find((t) => t.key === mode.key) : void 0;
|
|
441
456
|
return /* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", width, children: [
|
|
@@ -460,6 +475,7 @@ function App({ session, version, system, onExit, inspector = null, inspectorErro
|
|
|
460
475
|
] }),
|
|
461
476
|
/* @__PURE__ */ jsx6(Text6, { color: conn.color, children: conn.text })
|
|
462
477
|
] }),
|
|
478
|
+
update && /* @__PURE__ */ jsx6(Text6, { color: C.warn, children: updateLine(update) }),
|
|
463
479
|
/* @__PURE__ */ jsx6(Text6, { children: " " }),
|
|
464
480
|
/* @__PURE__ */ jsx6(TunnelTable, { tunnels: state.tunnels, selected: selTun, focused: focus === "tunnels" }),
|
|
465
481
|
inspector && /* @__PURE__ */ jsx6(Text6, { color: C.muted, children: `inspector ${inspector.url}` }),
|
|
@@ -548,7 +564,7 @@ var realSystem = {
|
|
|
548
564
|
|
|
549
565
|
// src/ui/fullscreen.tsx
|
|
550
566
|
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
551
|
-
function runFullscreen(session, specs, version, inspector, inspectorError) {
|
|
567
|
+
function runFullscreen(session, specs, version, inspector, inspectorError, update = Promise.resolve(null)) {
|
|
552
568
|
return withFullscreen(
|
|
553
569
|
process.stdout,
|
|
554
570
|
(signal) => new Promise((resolve) => {
|
|
@@ -559,7 +575,11 @@ function runFullscreen(session, specs, version, inspector, inspectorError) {
|
|
|
559
575
|
app.unmount();
|
|
560
576
|
resolve(code);
|
|
561
577
|
};
|
|
562
|
-
const
|
|
578
|
+
const view = (latest) => /* @__PURE__ */ jsx7(App, { session, version, system: realSystem, inspector, inspectorError, update: latest, onExit: () => finish(0) });
|
|
579
|
+
const app = render(view(null), { exitOnCtrlC: false });
|
|
580
|
+
void update.then((latest) => {
|
|
581
|
+
if (latest && !done) app.rerender(view(latest));
|
|
582
|
+
});
|
|
563
583
|
signal.addEventListener("abort", () => void session.stop().then(() => finish(0)), { once: true });
|
|
564
584
|
session.start(specs).catch(() => void 0);
|
|
565
585
|
})
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
C,
|
|
4
|
+
CLI_USER_AGENT,
|
|
5
|
+
CLI_VERSION_HEADER,
|
|
4
6
|
MAX_NAME,
|
|
5
7
|
TRUNCATED_NOTE,
|
|
8
|
+
apiUrl,
|
|
6
9
|
bodyNote,
|
|
10
|
+
checkForUpdate,
|
|
11
|
+
dutoHome,
|
|
7
12
|
formatBytes,
|
|
8
13
|
formatDuration,
|
|
9
14
|
formatTime,
|
|
15
|
+
loadConfig,
|
|
10
16
|
localLabel,
|
|
11
17
|
normalizeName,
|
|
12
18
|
placeOf,
|
|
19
|
+
saveConfig,
|
|
13
20
|
statusColor,
|
|
21
|
+
updateLine,
|
|
14
22
|
validateName
|
|
15
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-4SIBC52A.js";
|
|
16
24
|
|
|
17
25
|
// src/ui/brand.ts
|
|
18
26
|
function colorOn(stream = process.stdout) {
|
|
@@ -38,9 +46,12 @@ var warn = (msg) => `${paint("warn", "\u26A0")} ${msg}`;
|
|
|
38
46
|
|
|
39
47
|
// src/index.ts
|
|
40
48
|
import { hostname as hostname2 } from "os";
|
|
41
|
-
import { join as
|
|
49
|
+
import { join as join5, relative } from "path";
|
|
42
50
|
import cac from "cac";
|
|
43
51
|
|
|
52
|
+
// src/version.ts
|
|
53
|
+
var CLI_VERSION = "1.4.0";
|
|
54
|
+
|
|
44
55
|
// src/api.ts
|
|
45
56
|
var DutoApiError = class extends Error {
|
|
46
57
|
constructor(message2, status, code) {
|
|
@@ -71,7 +82,8 @@ function createApiClient(o) {
|
|
|
71
82
|
...o.token === void 0 ? {} : { authorization: `Bearer ${o.token}` },
|
|
72
83
|
...body === void 0 ? {} : { "content-type": "application/json" },
|
|
73
84
|
...bookmark === null ? {} : { [BOOKMARK_HEADER]: bookmark },
|
|
74
|
-
"user-agent":
|
|
85
|
+
"user-agent": CLI_USER_AGENT,
|
|
86
|
+
[CLI_VERSION_HEADER]: CLI_VERSION
|
|
75
87
|
},
|
|
76
88
|
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
77
89
|
});
|
|
@@ -118,8 +130,8 @@ function createApiClient(o) {
|
|
|
118
130
|
|
|
119
131
|
// src/cloudflared.ts
|
|
120
132
|
import { createHash } from "crypto";
|
|
121
|
-
import { chmodSync
|
|
122
|
-
import { join
|
|
133
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
134
|
+
import { join } from "path";
|
|
123
135
|
import { extract } from "tar";
|
|
124
136
|
|
|
125
137
|
// src/checksums.ts
|
|
@@ -132,40 +144,6 @@ var CHECKSUMS = {
|
|
|
132
144
|
"cloudflared-windows-amd64.exe": "2493dbc168923bb3e59e5e7716f71deced92992eea9acd6e9d74f84ac7751343"
|
|
133
145
|
};
|
|
134
146
|
|
|
135
|
-
// src/config.ts
|
|
136
|
-
import { randomUUID } from "crypto";
|
|
137
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
138
|
-
import { homedir } from "os";
|
|
139
|
-
import { join } from "path";
|
|
140
|
-
function dutoHome() {
|
|
141
|
-
return process.env.DUTO_HOME || join(homedir(), ".duto");
|
|
142
|
-
}
|
|
143
|
-
function configPath() {
|
|
144
|
-
return join(dutoHome(), "config.json");
|
|
145
|
-
}
|
|
146
|
-
function apiUrl() {
|
|
147
|
-
return (process.env.DUTO_API_URL || "https://duto.dev").replace(/\/+$/, "");
|
|
148
|
-
}
|
|
149
|
-
function saveConfig(c) {
|
|
150
|
-
mkdirSync(dutoHome(), { recursive: true, mode: 448 });
|
|
151
|
-
const path = configPath();
|
|
152
|
-
writeFileSync(path, JSON.stringify(c, null, 2) + "\n", { mode: 384 });
|
|
153
|
-
chmodSync(path, 384);
|
|
154
|
-
}
|
|
155
|
-
function loadConfig() {
|
|
156
|
-
const path = configPath();
|
|
157
|
-
if (existsSync(path)) {
|
|
158
|
-
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
159
|
-
if (parsed.deviceId) return parsed;
|
|
160
|
-
const fixed = { ...parsed, deviceId: randomUUID() };
|
|
161
|
-
saveConfig(fixed);
|
|
162
|
-
return fixed;
|
|
163
|
-
}
|
|
164
|
-
const fresh = { deviceId: randomUUID() };
|
|
165
|
-
saveConfig(fresh);
|
|
166
|
-
return fresh;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
147
|
// src/cloudflared.ts
|
|
170
148
|
function assetFor(platform, arch) {
|
|
171
149
|
const key = `${platform}/${arch}`;
|
|
@@ -190,11 +168,11 @@ function downloadUrl(asset, version = CLOUDFLARED_VERSION) {
|
|
|
190
168
|
async function ensureCloudflared(opts = {}) {
|
|
191
169
|
const platform = opts.platform ?? process.platform;
|
|
192
170
|
const { asset, kind } = assetFor(platform, opts.arch ?? process.arch);
|
|
193
|
-
const binDir =
|
|
194
|
-
const target =
|
|
195
|
-
if (
|
|
171
|
+
const binDir = join(dutoHome(), "bin");
|
|
172
|
+
const target = join(binDir, `cloudflared-${CLOUDFLARED_VERSION}${platform === "win32" ? ".exe" : ""}`);
|
|
173
|
+
if (existsSync(target)) return target;
|
|
196
174
|
opts.onDownload?.();
|
|
197
|
-
|
|
175
|
+
mkdirSync(binDir, { recursive: true });
|
|
198
176
|
const res = await (opts.fetchImpl ?? fetch)(downloadUrl(asset));
|
|
199
177
|
if (!res.ok) throw new Error(`Could not download cloudflared (HTTP ${res.status}).`);
|
|
200
178
|
const bytes = Buffer.from(await res.arrayBuffer());
|
|
@@ -204,22 +182,22 @@ async function ensureCloudflared(opts = {}) {
|
|
|
204
182
|
rmSync(target, { force: true });
|
|
205
183
|
throw new Error("cloudflared checksum mismatch. Try again; if it keeps failing, update duto.");
|
|
206
184
|
}
|
|
207
|
-
const tmp = mkdtempSync(
|
|
185
|
+
const tmp = mkdtempSync(join(binDir, ".download-"));
|
|
208
186
|
try {
|
|
209
187
|
if (kind === "tgz") {
|
|
210
|
-
const tgz =
|
|
211
|
-
|
|
188
|
+
const tgz = join(tmp, asset);
|
|
189
|
+
writeFileSync(tgz, bytes);
|
|
212
190
|
await extract({ file: tgz, cwd: tmp });
|
|
213
|
-
renameSync(
|
|
191
|
+
renameSync(join(tmp, "cloudflared"), target);
|
|
214
192
|
} else {
|
|
215
|
-
const file =
|
|
216
|
-
|
|
193
|
+
const file = join(tmp, "cloudflared");
|
|
194
|
+
writeFileSync(file, bytes);
|
|
217
195
|
renameSync(file, target);
|
|
218
196
|
}
|
|
219
197
|
} finally {
|
|
220
198
|
rmSync(tmp, { recursive: true, force: true });
|
|
221
199
|
}
|
|
222
|
-
if (platform !== "win32")
|
|
200
|
+
if (platform !== "win32") chmodSync(target, 493);
|
|
223
201
|
return target;
|
|
224
202
|
}
|
|
225
203
|
async function ensureCloudflaredWithProgress(write, opts = {}) {
|
|
@@ -241,9 +219,9 @@ async function ensureCloudflaredWithProgress(write, opts = {}) {
|
|
|
241
219
|
}
|
|
242
220
|
|
|
243
221
|
// src/session/allowlist.ts
|
|
244
|
-
import { mkdirSync as
|
|
222
|
+
import { mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
|
|
245
223
|
import { BlockList, isIP } from "net";
|
|
246
|
-
import { dirname, join as
|
|
224
|
+
import { dirname, join as join2 } from "path";
|
|
247
225
|
|
|
248
226
|
// src/session/allowlist-snapshot.ts
|
|
249
227
|
var ALLOW_SNAPSHOT = {
|
|
@@ -312,7 +290,7 @@ function blockListOf(cidrs) {
|
|
|
312
290
|
}
|
|
313
291
|
function readCache(file) {
|
|
314
292
|
try {
|
|
315
|
-
const c = JSON.parse(
|
|
293
|
+
const c = JSON.parse(readFileSync(file, "utf8"));
|
|
316
294
|
if (typeof c?.fetchedAt === "number" && c.lists && typeof c.lists === "object") return c;
|
|
317
295
|
} catch {
|
|
318
296
|
}
|
|
@@ -329,7 +307,7 @@ async function fetchRanges(name, f) {
|
|
|
329
307
|
return out.map((x) => x.includes("/") ? x : `${x}/${splitCidr(x).prefix}`);
|
|
330
308
|
}
|
|
331
309
|
async function resolveAllow(entries, o = {}) {
|
|
332
|
-
const cacheFile = o.cacheFile ??
|
|
310
|
+
const cacheFile = o.cacheFile ?? join2(dutoHome(), "allowlist.json");
|
|
333
311
|
const f = o.fetch ?? fetch;
|
|
334
312
|
const now = (o.now ?? Date.now)();
|
|
335
313
|
const parsed = entries.map((raw) => ({ raw, e: parseAllowEntry(raw) }));
|
|
@@ -352,8 +330,8 @@ async function resolveAllow(entries, o = {}) {
|
|
|
352
330
|
const everyListFromNetwork = names.length > 0 && Object.keys(net).length === names.length;
|
|
353
331
|
if (everyListFromNetwork) {
|
|
354
332
|
try {
|
|
355
|
-
|
|
356
|
-
|
|
333
|
+
mkdirSync2(dirname(cacheFile), { recursive: true });
|
|
334
|
+
writeFileSync2(cacheFile, JSON.stringify({ fetchedAt: now, lists: net }));
|
|
357
335
|
} catch {
|
|
358
336
|
}
|
|
359
337
|
}
|
|
@@ -375,8 +353,8 @@ async function resolveAllow(entries, o = {}) {
|
|
|
375
353
|
}
|
|
376
354
|
|
|
377
355
|
// src/session/config.ts
|
|
378
|
-
import { existsSync as
|
|
379
|
-
import { dirname as dirname2, join as
|
|
356
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
357
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
380
358
|
import { isMap, isScalar, isSeq, LineCounter, parseDocument } from "yaml";
|
|
381
359
|
var ConfigError = class extends Error {
|
|
382
360
|
constructor(message2, line, file = CONFIG_FILES[0]) {
|
|
@@ -522,13 +500,13 @@ function parseConfig(text, file = CONFIG_FILES[0], env = process.env) {
|
|
|
522
500
|
function findConfig(cwd) {
|
|
523
501
|
let dir = cwd;
|
|
524
502
|
for (; ; ) {
|
|
525
|
-
const present = CONFIG_FILES.filter((f) =>
|
|
503
|
+
const present = CONFIG_FILES.filter((f) => existsSync2(join3(dir, f)));
|
|
526
504
|
if (present.length > 1) throw new ConfigError(`${CONFIG_FILES.join(" and ")} in the same folder. Delete one of them.`, void 0, "");
|
|
527
505
|
if (present.length === 1) {
|
|
528
|
-
const path =
|
|
529
|
-
return { path, tunnels: parseConfig(
|
|
506
|
+
const path = join3(dir, present[0]);
|
|
507
|
+
return { path, tunnels: parseConfig(readFileSync2(path, "utf8"), present[0]) };
|
|
530
508
|
}
|
|
531
|
-
if (
|
|
509
|
+
if (existsSync2(join3(dir, ".git"))) return null;
|
|
532
510
|
const up = dirname2(dir);
|
|
533
511
|
if (up === dir) return null;
|
|
534
512
|
dir = up;
|
|
@@ -545,7 +523,7 @@ async function browserLogin(o) {
|
|
|
545
523
|
const start = await api.startLogin({
|
|
546
524
|
deviceId: o.deviceId,
|
|
547
525
|
deviceName: hostname(),
|
|
548
|
-
cliVersion: "1.
|
|
526
|
+
cliVersion: "1.4.0",
|
|
549
527
|
nodeVersion: process.version,
|
|
550
528
|
os: process.platform,
|
|
551
529
|
arch: process.arch
|
|
@@ -777,6 +755,8 @@ button { font: inherit; color: inherit; background: none; border: 0; padding: 0;
|
|
|
777
755
|
.row .dur { text-align: right; }
|
|
778
756
|
.m { font-weight: 700; }
|
|
779
757
|
.mut { color: ${C.muted}; }
|
|
758
|
+
.sig { color: ${C.signal}; }
|
|
759
|
+
.redirect { margin-top: 6px; display: grid; gap: 2px; }
|
|
780
760
|
.empty { padding: 12px; color: ${C.muted}; }
|
|
781
761
|
.detail { display: flex; flex-direction: column; min-height: 0; min-width: 0; }
|
|
782
762
|
.pane { flex: 1; }
|
|
@@ -913,7 +893,12 @@ function renderList() {
|
|
|
913
893
|
row.setAttribute("role", "option");
|
|
914
894
|
row.setAttribute("aria-selected", it.id === state.selected ? "true" : "false");
|
|
915
895
|
row.dataset.id = String(it.id);
|
|
916
|
-
row.appendChild(el("span", "mut",
|
|
896
|
+
if (it.replayOf !== undefined) row.appendChild(el("span", "mut", "↻"));
|
|
897
|
+
else if (it.rewritten) {
|
|
898
|
+
var mark = el("span", "sig", "↪");
|
|
899
|
+
mark.title = "Location rewritten";
|
|
900
|
+
row.appendChild(mark);
|
|
901
|
+
} else row.appendChild(el("span", "mut", ""));
|
|
917
902
|
if (multi) row.appendChild(el("span", "mut", it.tunnel));
|
|
918
903
|
row.appendChild(el("span", "m", methodOf(it)));
|
|
919
904
|
var p = el("span", "", it.path);
|
|
@@ -995,6 +980,13 @@ function renderDetail() {
|
|
|
995
980
|
if (d.replayOf !== undefined) parts.push("↻ replay of #" + d.replayOf);
|
|
996
981
|
head.appendChild(el("span", "mut", " · " + parts.join(" · ")));
|
|
997
982
|
if (d.error) head.appendChild(el("div", "msg err", d.error));
|
|
983
|
+
if (d.redirect) {
|
|
984
|
+
var moved = el("div", "redirect");
|
|
985
|
+
moved.appendChild(el("div", "sig", "↪ Location rewritten"));
|
|
986
|
+
moved.appendChild(el("div", "mut", d.redirect.from));
|
|
987
|
+
moved.appendChild(el("div", "", "→ " + d.redirect.to));
|
|
988
|
+
head.appendChild(moved);
|
|
989
|
+
}
|
|
998
990
|
|
|
999
991
|
var tabs = document.querySelectorAll(".tab");
|
|
1000
992
|
for (var i = 0; i < tabs.length; i++) {
|
|
@@ -1370,7 +1362,8 @@ function listItem(r) {
|
|
|
1370
1362
|
startedAt: r.startedAt,
|
|
1371
1363
|
websocket: r.websocket,
|
|
1372
1364
|
...r.replayOf !== void 0 && { replayOf: r.replayOf },
|
|
1373
|
-
...r.error !== void 0 && { error: r.error }
|
|
1365
|
+
...r.error !== void 0 && { error: r.error },
|
|
1366
|
+
...r.redirect !== void 0 && { rewritten: true }
|
|
1374
1367
|
};
|
|
1375
1368
|
}
|
|
1376
1369
|
function detail(r, session) {
|
|
@@ -1386,6 +1379,7 @@ function detail(r, session) {
|
|
|
1386
1379
|
return {
|
|
1387
1380
|
...listItem(r),
|
|
1388
1381
|
edge: edgeOf(r.reqHeaders),
|
|
1382
|
+
redirect: r.redirect ?? null,
|
|
1389
1383
|
reqHeaders: r.reqHeaders,
|
|
1390
1384
|
resHeaders: r.resHeaders,
|
|
1391
1385
|
reqBody: r.reqBody,
|
|
@@ -2160,7 +2154,12 @@ async function startProxy(o) {
|
|
|
2160
2154
|
return;
|
|
2161
2155
|
}
|
|
2162
2156
|
if (route.host && route.rewriteRedirects !== false && typeof up.headers.location === "string") {
|
|
2163
|
-
|
|
2157
|
+
const from = up.headers.location;
|
|
2158
|
+
const to = rewriteLocation(from, route.host, hostOf(req));
|
|
2159
|
+
if (to !== from) {
|
|
2160
|
+
up.headers.location = to;
|
|
2161
|
+
o.store.update(id, { redirect: { from, to } });
|
|
2162
|
+
}
|
|
2164
2163
|
}
|
|
2165
2164
|
res.writeHead(up.statusCode ?? 502, up.statusMessage, up.headers);
|
|
2166
2165
|
trackResponse(id, started, up, reqCap);
|
|
@@ -2249,13 +2248,13 @@ async function startProxy(o) {
|
|
|
2249
2248
|
|
|
2250
2249
|
// src/session/orphans.ts
|
|
2251
2250
|
import { spawnSync } from "child_process";
|
|
2252
|
-
import { existsSync as
|
|
2253
|
-
import { join as
|
|
2254
|
-
var fileOf = (dir, cfPid) =>
|
|
2251
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
2252
|
+
import { join as join4 } from "path";
|
|
2253
|
+
var fileOf = (dir, cfPid) => join4(dir, `cloudflared-${cfPid}.json`);
|
|
2255
2254
|
function recordCloudflared(dir, cfPid, sessionPid = process.pid) {
|
|
2256
2255
|
try {
|
|
2257
|
-
|
|
2258
|
-
|
|
2256
|
+
mkdirSync3(dir, { recursive: true });
|
|
2257
|
+
writeFileSync3(fileOf(dir, cfPid), JSON.stringify({ sessionPid, cfPid, at: Date.now() }));
|
|
2259
2258
|
} catch {
|
|
2260
2259
|
}
|
|
2261
2260
|
}
|
|
@@ -2273,7 +2272,7 @@ var realProc = {
|
|
|
2273
2272
|
},
|
|
2274
2273
|
isCloudflared(pid) {
|
|
2275
2274
|
try {
|
|
2276
|
-
if (process.platform === "linux") return
|
|
2275
|
+
if (process.platform === "linux") return readFileSync3(`/proc/${pid}/cmdline`, "utf8").includes("cloudflared");
|
|
2277
2276
|
if (process.platform === "darwin") return spawnSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8" }).stdout.includes("cloudflared");
|
|
2278
2277
|
} catch {
|
|
2279
2278
|
return false;
|
|
@@ -2288,14 +2287,14 @@ var realProc = {
|
|
|
2288
2287
|
}
|
|
2289
2288
|
};
|
|
2290
2289
|
function reapOrphans(dir, proc = realProc) {
|
|
2291
|
-
if (!
|
|
2290
|
+
if (!existsSync3(dir)) return 0;
|
|
2292
2291
|
let killed = 0;
|
|
2293
2292
|
for (const name of readdirSync(dir)) {
|
|
2294
2293
|
if (!/^cloudflared-\d+\.json$/.test(name)) continue;
|
|
2295
|
-
const path =
|
|
2294
|
+
const path = join4(dir, name);
|
|
2296
2295
|
let rec;
|
|
2297
2296
|
try {
|
|
2298
|
-
rec = JSON.parse(
|
|
2297
|
+
rec = JSON.parse(readFileSync3(path, "utf8"));
|
|
2299
2298
|
} catch {
|
|
2300
2299
|
rmSync2(path, { force: true });
|
|
2301
2300
|
continue;
|
|
@@ -2623,7 +2622,7 @@ function runPlain(session, out) {
|
|
|
2623
2622
|
printed.add(r.id);
|
|
2624
2623
|
const method = r.websocket ? "WS" : r.method;
|
|
2625
2624
|
out(
|
|
2626
|
-
`${formatTime(r.startedAt)} ${r.tunnel.padEnd(8)} ${method} ${r.path} ${paint(statusColor(r.status), String(r.status))} ${formatDuration(r.durationMs)} ${formatBytes(r.bytes)}`
|
|
2625
|
+
`${formatTime(r.startedAt)} ${r.tunnel.padEnd(8)} ${method} ${r.path} ${paint(statusColor(r.status), String(r.status))} ${formatDuration(r.durationMs)} ${formatBytes(r.bytes)}${r.redirect ? ` ${paint("signal", `\u21AA Location rewritten to ${r.redirect.to}`)}` : ""}`
|
|
2627
2626
|
);
|
|
2628
2627
|
}
|
|
2629
2628
|
};
|
|
@@ -2686,13 +2685,14 @@ async function runSession(specs, source, flags) {
|
|
|
2686
2685
|
const config = loadConfig();
|
|
2687
2686
|
if (!config.authtoken) fail2(LOGIN_HINT);
|
|
2688
2687
|
const api = createApiClient({ baseUrl: apiUrl(), token: config.authtoken });
|
|
2688
|
+
const update = checkForUpdate(CLI_VERSION);
|
|
2689
2689
|
let cloudflaredPath;
|
|
2690
2690
|
try {
|
|
2691
2691
|
cloudflaredPath = process.env.DUTO_CLOUDFLARED || await ensureCloudflaredWithProgress((s) => process.stdout.write(s));
|
|
2692
2692
|
} catch (err) {
|
|
2693
2693
|
fail2(err instanceof Error ? err.message : String(err));
|
|
2694
2694
|
}
|
|
2695
|
-
const session = new Session({ api, deviceId: config.deviceId, deviceName: hostname2(), cloudflaredPath, source, runDir:
|
|
2695
|
+
const session = new Session({ api, deviceId: config.deviceId, deviceName: hostname2(), cloudflaredPath, source, runDir: join5(dutoHome(), "run") });
|
|
2696
2696
|
let inspector = null;
|
|
2697
2697
|
let inspectorError;
|
|
2698
2698
|
if (flags.inspect !== false) {
|
|
@@ -2703,14 +2703,15 @@ async function runSession(specs, source, flags) {
|
|
|
2703
2703
|
}
|
|
2704
2704
|
}
|
|
2705
2705
|
if (wantsUi(flags)) {
|
|
2706
|
-
const { runFullscreen } = await import("./fullscreen-
|
|
2707
|
-
const code = await runFullscreen(session, specs,
|
|
2706
|
+
const { runFullscreen } = await import("./fullscreen-MZMQ6ADN.js");
|
|
2707
|
+
const code = await runFullscreen(session, specs, CLI_VERSION, inspector, inspectorError, update);
|
|
2708
2708
|
await inspector?.close().catch(() => void 0);
|
|
2709
2709
|
process.exit(code);
|
|
2710
2710
|
}
|
|
2711
|
-
console.log(`${mark()} ${paint("muted", `${
|
|
2711
|
+
console.log(`${mark()} ${paint("muted", `${CLI_VERSION} \xB7 ${source} \xB7 Ctrl+C to stop`)}`);
|
|
2712
2712
|
if (inspector) console.log(paint("muted", `inspector: ${inspector.openUrl}`));
|
|
2713
2713
|
if (inspectorError) console.log(warn(`inspector failed to start: ${inspectorError}`));
|
|
2714
|
+
void update.then((latest) => latest && console.log(warn(updateLine(latest))));
|
|
2714
2715
|
const stopLog = runPlain(session, (l) => console.log(l));
|
|
2715
2716
|
const stopped = new Promise((resolve) => {
|
|
2716
2717
|
const onSignal = () => {
|
|
@@ -2836,7 +2837,7 @@ cli.command("status", "Show your account and live tunnels").action(async () => {
|
|
|
2836
2837
|
}
|
|
2837
2838
|
});
|
|
2838
2839
|
cli.help();
|
|
2839
|
-
cli.version(
|
|
2840
|
+
cli.version(CLI_VERSION);
|
|
2840
2841
|
try {
|
|
2841
2842
|
cli.parse(process.argv, { run: false });
|
|
2842
2843
|
await cli.runMatchedCommand();
|