@iamken/cloudtunnel 0.3.0 → 0.5.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 +82 -77
- package/dist/{chunk-2TCFCMJS.js → chunk-CWJA4L7J.js} +2 -2
- package/dist/{chunk-YLTQB4F7.js → chunk-NZKGIDIW.js} +2 -2
- package/dist/{chunk-UPBVRXLF.js → chunk-RWR6VXNB.js} +7 -1
- package/dist/{chunk-UPBVRXLF.js.map → chunk-RWR6VXNB.js.map} +1 -1
- package/dist/{dns-PAPFSYFP.js → dns-5OXFAQ4D.js} +3 -3
- package/dist/index.js +681 -551
- package/dist/index.js.map +1 -1
- package/dist/zones-QYV3DSEY.js +11 -0
- package/package.json +1 -1
- package/dist/zones-YNGQYXAF.js +0 -11
- /package/dist/{chunk-2TCFCMJS.js.map → chunk-CWJA4L7J.js.map} +0 -0
- /package/dist/{chunk-YLTQB4F7.js.map → chunk-NZKGIDIW.js.map} +0 -0
- /package/dist/{dns-PAPFSYFP.js.map → dns-5OXFAQ4D.js.map} +0 -0
- /package/dist/{zones-YNGQYXAF.js.map → zones-QYV3DSEY.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
import {
|
|
3
3
|
listZones,
|
|
4
4
|
resolveZone
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-CWJA4L7J.js";
|
|
6
6
|
import {
|
|
7
7
|
createCname,
|
|
8
8
|
deleteDnsRecord,
|
|
9
9
|
findCname,
|
|
10
10
|
isManagedDns
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-NZKGIDIW.js";
|
|
12
12
|
import {
|
|
13
13
|
CliError,
|
|
14
|
+
__export,
|
|
14
15
|
binDir,
|
|
15
16
|
cfPaginate,
|
|
16
17
|
cfRequest,
|
|
@@ -24,15 +25,15 @@ import {
|
|
|
24
25
|
reportError,
|
|
25
26
|
resolveCf,
|
|
26
27
|
saveConfig
|
|
27
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-RWR6VXNB.js";
|
|
28
29
|
|
|
29
30
|
// src/index.ts
|
|
30
31
|
import { Command } from "commander";
|
|
31
32
|
import { createRequire } from "module";
|
|
32
33
|
import pc2 from "picocolors";
|
|
33
34
|
|
|
34
|
-
// src/
|
|
35
|
-
import
|
|
35
|
+
// src/config/legacy-migrate.ts
|
|
36
|
+
import { existsSync as existsSync3, readFileSync, renameSync, writeFileSync as writeFileSync4 } from "fs";
|
|
36
37
|
|
|
37
38
|
// src/ui/output.ts
|
|
38
39
|
import pc from "picocolors";
|
|
@@ -66,10 +67,10 @@ function printTable(head, rows) {
|
|
|
66
67
|
for (const row of rows) table.push(row);
|
|
67
68
|
console.log(table.toString());
|
|
68
69
|
}
|
|
69
|
-
async function selectOne(message, items,
|
|
70
|
+
async function selectOne(message, items, label4) {
|
|
70
71
|
const value = await select({
|
|
71
72
|
message,
|
|
72
|
-
options: items.map((item, i) => ({ value: String(i), label:
|
|
73
|
+
options: items.map((item, i) => ({ value: String(i), label: label4(item) }))
|
|
73
74
|
});
|
|
74
75
|
if (isCancel(value)) {
|
|
75
76
|
cancel("Cancelled.");
|
|
@@ -78,6 +79,459 @@ async function selectOne(message, items, label) {
|
|
|
78
79
|
return items[Number(value)];
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
// src/core/service-exec.ts
|
|
83
|
+
import { realpathSync } from "fs";
|
|
84
|
+
import os from "os";
|
|
85
|
+
import { join } from "path";
|
|
86
|
+
|
|
87
|
+
// src/core/ingress.ts
|
|
88
|
+
var HOSTNAME_RE = /^[a-zA-Z0-9.-]+$/;
|
|
89
|
+
var IPV6_RE = /^[0-9a-fA-F:.]+$/;
|
|
90
|
+
function validateHost(host) {
|
|
91
|
+
let h = host.trim();
|
|
92
|
+
const bracketed = h.startsWith("[") && h.endsWith("]");
|
|
93
|
+
if (bracketed) h = h.slice(1, -1);
|
|
94
|
+
const isV6 = bracketed || h.includes("::") || (h.match(/:/g)?.length ?? 0) >= 2;
|
|
95
|
+
const ok = h.length > 0 && (isV6 ? IPV6_RE.test(h) : HOSTNAME_RE.test(h));
|
|
96
|
+
if (!ok) {
|
|
97
|
+
throw new CliError(`Invalid host "${host}".`, {
|
|
98
|
+
hint: "use a hostname, IPv4, or IPv6 literal (e.g. 192.168.1.5 or ::1) \u2014 no port, scheme, or path"
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return h;
|
|
102
|
+
}
|
|
103
|
+
function serviceUrl(proto, host, port) {
|
|
104
|
+
const authority = host.includes(":") ? `[${host}]` : host;
|
|
105
|
+
return `${proto}://${authority}:${port}`;
|
|
106
|
+
}
|
|
107
|
+
function buildIngress(opts) {
|
|
108
|
+
return [
|
|
109
|
+
{ hostname: opts.hostname, service: serviceUrl(opts.proto, opts.host ?? "localhost", opts.port) },
|
|
110
|
+
{ service: "http_status:404" }
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/core/tunnel-spec.ts
|
|
115
|
+
function parseTunnelSpec(spec) {
|
|
116
|
+
const raw = spec.trim();
|
|
117
|
+
const bad = (hint) => new CliError(`Invalid spec "${spec}".`, { hint });
|
|
118
|
+
if (!raw) throw bad("use [subdomain:]port[@host], e.g. api:8080 or api:8080@192.168.1.20");
|
|
119
|
+
let rest = raw;
|
|
120
|
+
let subdomain;
|
|
121
|
+
if (rest.startsWith("@")) {
|
|
122
|
+
subdomain = "@";
|
|
123
|
+
rest = rest.slice(1);
|
|
124
|
+
if (rest.startsWith(":")) rest = rest.slice(1);
|
|
125
|
+
}
|
|
126
|
+
let host;
|
|
127
|
+
const at = rest.indexOf("@");
|
|
128
|
+
if (at >= 0) {
|
|
129
|
+
host = validateHost(rest.slice(at + 1));
|
|
130
|
+
rest = rest.slice(0, at);
|
|
131
|
+
}
|
|
132
|
+
const parts = rest.split(":");
|
|
133
|
+
let portStr;
|
|
134
|
+
if (parts.length === 1) {
|
|
135
|
+
portStr = parts[0];
|
|
136
|
+
} else if (parts.length === 2) {
|
|
137
|
+
if (subdomain === void 0) {
|
|
138
|
+
if (!parts[0]) throw bad("subdomain label is empty");
|
|
139
|
+
subdomain = parts[0];
|
|
140
|
+
} else if (parts[0]) {
|
|
141
|
+
throw bad("unexpected label after '@' root marker");
|
|
142
|
+
}
|
|
143
|
+
portStr = parts[1];
|
|
144
|
+
} else {
|
|
145
|
+
throw bad("too many ':' \u2014 spec is [subdomain:]port[@host] (protocol via --proto)");
|
|
146
|
+
}
|
|
147
|
+
const port = Number(portStr);
|
|
148
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
149
|
+
throw bad("port must be a number 1\u201365535");
|
|
150
|
+
}
|
|
151
|
+
if (subdomain !== void 0 && subdomain !== "@" && !/^[a-zA-Z0-9-]+$/.test(subdomain)) {
|
|
152
|
+
throw bad("subdomain may contain only letters, digits, and hyphens");
|
|
153
|
+
}
|
|
154
|
+
return { subdomain, port, ...host ? { host } : {} };
|
|
155
|
+
}
|
|
156
|
+
function formatTunnelSpec(s) {
|
|
157
|
+
return `${s.subdomain}:${s.port}${s.host ? `@${s.host}` : ""}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/core/service-exec.ts
|
|
161
|
+
var fqdnFor = (subdomain, zone) => subdomain === "@" ? zone : `${subdomain}.${zone}`;
|
|
162
|
+
var serviceSlug = (fqdn) => fqdn.replace(/[^a-zA-Z0-9]+/g, "-");
|
|
163
|
+
function buildUpArgs(p) {
|
|
164
|
+
const spec = formatTunnelSpec({ subdomain: p.subdomain, port: p.port, host: p.host });
|
|
165
|
+
return [
|
|
166
|
+
"up",
|
|
167
|
+
spec,
|
|
168
|
+
"-d",
|
|
169
|
+
p.zone,
|
|
170
|
+
...p.proto === "https" ? ["--proto", "https"] : [],
|
|
171
|
+
...p.protocol ? ["--protocol", p.protocol] : [],
|
|
172
|
+
"-f",
|
|
173
|
+
"-y"
|
|
174
|
+
];
|
|
175
|
+
}
|
|
176
|
+
function entryScript() {
|
|
177
|
+
const p = process.argv[1];
|
|
178
|
+
if (!p) throw new CliError("Cannot resolve the cloudtunnel executable path.");
|
|
179
|
+
return realpathSync(p);
|
|
180
|
+
}
|
|
181
|
+
function describeService(p) {
|
|
182
|
+
const fqdn = fqdnFor(p.subdomain, p.zone);
|
|
183
|
+
const slug = serviceSlug(fqdn);
|
|
184
|
+
return {
|
|
185
|
+
fqdn,
|
|
186
|
+
slug,
|
|
187
|
+
argv: buildUpArgs(p),
|
|
188
|
+
nodePath: process.execPath,
|
|
189
|
+
scriptPath: entryScript(),
|
|
190
|
+
user: os.userInfo().username,
|
|
191
|
+
home: os.homedir(),
|
|
192
|
+
logFile: join(logDir, `${slug}.service.log`)
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/core/service-systemd.ts
|
|
197
|
+
var service_systemd_exports = {};
|
|
198
|
+
__export(service_systemd_exports, {
|
|
199
|
+
assertSupported: () => assertSupported,
|
|
200
|
+
buildUnit: () => buildUnit,
|
|
201
|
+
install: () => install,
|
|
202
|
+
label: () => label,
|
|
203
|
+
legacyUnitExists: () => legacyUnitExists,
|
|
204
|
+
removeLegacyUnit: () => removeLegacyUnit,
|
|
205
|
+
state: () => state,
|
|
206
|
+
uninstall: () => uninstall
|
|
207
|
+
});
|
|
208
|
+
import { execFileSync } from "child_process";
|
|
209
|
+
import { existsSync, writeFileSync } from "fs";
|
|
210
|
+
import { tmpdir } from "os";
|
|
211
|
+
import { dirname, join as join2 } from "path";
|
|
212
|
+
var label = (fqdn) => `cloudtunnel-${serviceSlug(fqdn)}.service`;
|
|
213
|
+
var unitPath = (fqdn) => `/etc/systemd/system/${label(fqdn)}`;
|
|
214
|
+
function buildUnit(d) {
|
|
215
|
+
const nodeBin = dirname(d.nodePath);
|
|
216
|
+
return [
|
|
217
|
+
"[Unit]",
|
|
218
|
+
`Description=cloudtunnel ${d.fqdn} (Cloudflare Tunnel)`,
|
|
219
|
+
"After=network-online.target",
|
|
220
|
+
"Wants=network-online.target",
|
|
221
|
+
"",
|
|
222
|
+
"[Service]",
|
|
223
|
+
"Type=simple",
|
|
224
|
+
`User=${d.user}`,
|
|
225
|
+
`Environment=HOME=${d.home}`,
|
|
226
|
+
`Environment=PATH=${nodeBin}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`,
|
|
227
|
+
`ExecStart=${d.nodePath} ${d.scriptPath} ${d.argv.join(" ")}`,
|
|
228
|
+
"Restart=on-failure",
|
|
229
|
+
"RestartSec=5",
|
|
230
|
+
"",
|
|
231
|
+
"[Install]",
|
|
232
|
+
"WantedBy=multi-user.target",
|
|
233
|
+
""
|
|
234
|
+
].join("\n");
|
|
235
|
+
}
|
|
236
|
+
function privileged(args) {
|
|
237
|
+
const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
|
|
238
|
+
const argv = isRoot ? args : ["sudo", ...args];
|
|
239
|
+
execFileSync(argv[0], argv.slice(1), { stdio: "inherit" });
|
|
240
|
+
}
|
|
241
|
+
function query(args) {
|
|
242
|
+
try {
|
|
243
|
+
return execFileSync("systemctl", args, { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" }).trim();
|
|
244
|
+
} catch (err) {
|
|
245
|
+
const out = err.stdout;
|
|
246
|
+
return out ? out.toString().trim() : "";
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function assertSupported() {
|
|
250
|
+
try {
|
|
251
|
+
execFileSync("systemctl", ["--version"], { stdio: "ignore" });
|
|
252
|
+
} catch {
|
|
253
|
+
throw new CliError("systemd (systemctl) was not found on this host.");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function install(d) {
|
|
257
|
+
assertSupported();
|
|
258
|
+
const tmp = join2(tmpdir(), label(d.fqdn));
|
|
259
|
+
writeFileSync(tmp, buildUnit(d), { mode: 420 });
|
|
260
|
+
privileged(["install", "-m", "0644", tmp, unitPath(d.fqdn)]);
|
|
261
|
+
privileged(["systemctl", "daemon-reload"]);
|
|
262
|
+
privileged(["systemctl", "enable", "--now", label(d.fqdn)]);
|
|
263
|
+
}
|
|
264
|
+
function uninstall(fqdn) {
|
|
265
|
+
try {
|
|
266
|
+
privileged(["systemctl", "disable", "--now", label(fqdn)]);
|
|
267
|
+
} catch {
|
|
268
|
+
}
|
|
269
|
+
privileged(["rm", "-f", unitPath(fqdn)]);
|
|
270
|
+
privileged(["systemctl", "daemon-reload"]);
|
|
271
|
+
}
|
|
272
|
+
function state(fqdn) {
|
|
273
|
+
const name = label(fqdn);
|
|
274
|
+
if (query(["is-active", name]) === "active") return "active";
|
|
275
|
+
const enabled = query(["is-enabled", name]);
|
|
276
|
+
if (enabled === "enabled" || enabled === "enabled-runtime") return "enabled";
|
|
277
|
+
if (enabled === "disabled" || enabled === "static") return "disabled";
|
|
278
|
+
return "none";
|
|
279
|
+
}
|
|
280
|
+
function legacyUnitExists(profile) {
|
|
281
|
+
return existsSync(`/etc/systemd/system/cloudtunnel-${profile}.service`);
|
|
282
|
+
}
|
|
283
|
+
function removeLegacyUnit(profile) {
|
|
284
|
+
const name = `cloudtunnel-${profile}.service`;
|
|
285
|
+
try {
|
|
286
|
+
privileged(["systemctl", "disable", "--now", name]);
|
|
287
|
+
} catch {
|
|
288
|
+
}
|
|
289
|
+
privileged(["rm", "-f", `/etc/systemd/system/${name}`]);
|
|
290
|
+
privileged(["systemctl", "daemon-reload"]);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/core/service-launchd.ts
|
|
294
|
+
var service_launchd_exports = {};
|
|
295
|
+
__export(service_launchd_exports, {
|
|
296
|
+
assertSupported: () => assertSupported2,
|
|
297
|
+
buildPlist: () => buildPlist,
|
|
298
|
+
install: () => install2,
|
|
299
|
+
label: () => label2,
|
|
300
|
+
state: () => state2,
|
|
301
|
+
uninstall: () => uninstall2
|
|
302
|
+
});
|
|
303
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
304
|
+
import { existsSync as existsSync2, mkdirSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
305
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
306
|
+
import os2 from "os";
|
|
307
|
+
var label2 = (fqdn) => `com.cloudtunnel.${serviceSlug(fqdn)}`;
|
|
308
|
+
var agentsDir = () => join3(os2.homedir(), "Library", "LaunchAgents");
|
|
309
|
+
var plistPath = (fqdn) => join3(agentsDir(), `${label2(fqdn)}.plist`);
|
|
310
|
+
var xml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
311
|
+
function buildPlist(d) {
|
|
312
|
+
const args = [d.nodePath, d.scriptPath, ...d.argv].map((a) => ` <string>${xml(a)}</string>`).join("\n");
|
|
313
|
+
const nodeBin = dirname2(d.nodePath);
|
|
314
|
+
const path = `${nodeBin}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`;
|
|
315
|
+
return [
|
|
316
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
317
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
318
|
+
'<plist version="1.0">',
|
|
319
|
+
"<dict>",
|
|
320
|
+
` <key>Label</key><string>${xml(label2(d.fqdn))}</string>`,
|
|
321
|
+
" <key>ProgramArguments</key>",
|
|
322
|
+
" <array>",
|
|
323
|
+
args,
|
|
324
|
+
" </array>",
|
|
325
|
+
" <key>RunAtLoad</key><true/>",
|
|
326
|
+
" <key>KeepAlive</key><true/>",
|
|
327
|
+
" <key>EnvironmentVariables</key>",
|
|
328
|
+
" <dict>",
|
|
329
|
+
` <key>PATH</key><string>${xml(path)}</string>`,
|
|
330
|
+
` <key>HOME</key><string>${xml(d.home)}</string>`,
|
|
331
|
+
" </dict>",
|
|
332
|
+
` <key>StandardOutPath</key><string>${xml(d.logFile)}</string>`,
|
|
333
|
+
` <key>StandardErrorPath</key><string>${xml(d.logFile)}</string>`,
|
|
334
|
+
"</dict>",
|
|
335
|
+
"</plist>",
|
|
336
|
+
""
|
|
337
|
+
].join("\n");
|
|
338
|
+
}
|
|
339
|
+
function launchctl(args) {
|
|
340
|
+
try {
|
|
341
|
+
return execFileSync2("launchctl", args, { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" });
|
|
342
|
+
} catch (err) {
|
|
343
|
+
const out = err.stdout;
|
|
344
|
+
return out ? out.toString() : "";
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function assertSupported2() {
|
|
348
|
+
}
|
|
349
|
+
function install2(d) {
|
|
350
|
+
ensureDirs();
|
|
351
|
+
mkdirSync(agentsDir(), { recursive: true });
|
|
352
|
+
const plist = plistPath(d.fqdn);
|
|
353
|
+
writeFileSync2(plist, buildPlist(d), { mode: 420 });
|
|
354
|
+
launchctl(["unload", "-w", plist]);
|
|
355
|
+
execFileSync2("launchctl", ["load", "-w", plist], { stdio: "inherit" });
|
|
356
|
+
}
|
|
357
|
+
function uninstall2(fqdn) {
|
|
358
|
+
const plist = plistPath(fqdn);
|
|
359
|
+
launchctl(["unload", "-w", plist]);
|
|
360
|
+
rmSync(plist, { force: true });
|
|
361
|
+
}
|
|
362
|
+
function state2(fqdn) {
|
|
363
|
+
const info = launchctl(["list", label2(fqdn)]);
|
|
364
|
+
if (/"PID"\s*=/.test(info)) return "active";
|
|
365
|
+
return existsSync2(plistPath(fqdn)) ? "enabled" : "none";
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// src/core/service-windows.ts
|
|
369
|
+
var service_windows_exports = {};
|
|
370
|
+
__export(service_windows_exports, {
|
|
371
|
+
assertSupported: () => assertSupported3,
|
|
372
|
+
buildTaskXml: () => buildTaskXml,
|
|
373
|
+
install: () => install3,
|
|
374
|
+
label: () => label3,
|
|
375
|
+
state: () => state3,
|
|
376
|
+
uninstall: () => uninstall3
|
|
377
|
+
});
|
|
378
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
379
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
380
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
381
|
+
import { join as join4 } from "path";
|
|
382
|
+
var label3 = (fqdn) => `cloudtunnel\\${serviceSlug(fqdn)}`;
|
|
383
|
+
var xml2 = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
384
|
+
function buildTaskXml(d) {
|
|
385
|
+
const args = `"${d.scriptPath}" ${d.argv.join(" ")}`;
|
|
386
|
+
return [
|
|
387
|
+
'<?xml version="1.0" encoding="UTF-16"?>',
|
|
388
|
+
'<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">',
|
|
389
|
+
` <RegistrationInfo><Description>cloudtunnel ${xml2(d.fqdn)} (Cloudflare Tunnel)</Description></RegistrationInfo>`,
|
|
390
|
+
` <Triggers><LogonTrigger><Enabled>true</Enabled><UserId>${xml2(d.user)}</UserId></LogonTrigger></Triggers>`,
|
|
391
|
+
` <Principals><Principal id="Author"><UserId>${xml2(d.user)}</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>`,
|
|
392
|
+
" <Settings>",
|
|
393
|
+
" <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>",
|
|
394
|
+
" <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>",
|
|
395
|
+
" <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>",
|
|
396
|
+
" <StartWhenAvailable>true</StartWhenAvailable>",
|
|
397
|
+
" <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>",
|
|
398
|
+
" <RestartOnFailure><Interval>PT1M</Interval><Count>3</Count></RestartOnFailure>",
|
|
399
|
+
" <Enabled>true</Enabled>",
|
|
400
|
+
" </Settings>",
|
|
401
|
+
' <Actions Context="Author">',
|
|
402
|
+
` <Exec><Command>${xml2(d.nodePath)}</Command><Arguments>${xml2(args)}</Arguments></Exec>`,
|
|
403
|
+
" </Actions>",
|
|
404
|
+
"</Task>",
|
|
405
|
+
""
|
|
406
|
+
].join("\r\n");
|
|
407
|
+
}
|
|
408
|
+
function schtasks(args) {
|
|
409
|
+
try {
|
|
410
|
+
return execFileSync3("schtasks", args, { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" });
|
|
411
|
+
} catch (err) {
|
|
412
|
+
const out = err.stdout;
|
|
413
|
+
return out ? out.toString() : "";
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
function assertSupported3() {
|
|
417
|
+
}
|
|
418
|
+
function install3(d) {
|
|
419
|
+
const file = join4(tmpdir2(), `${d.slug}.task.xml`);
|
|
420
|
+
writeFileSync3(file, "\uFEFF" + buildTaskXml(d), { encoding: "utf16le" });
|
|
421
|
+
execFileSync3("schtasks", ["/Create", "/TN", label3(d.fqdn), "/XML", file, "/F"], { stdio: "inherit" });
|
|
422
|
+
schtasks(["/Run", "/TN", label3(d.fqdn)]);
|
|
423
|
+
}
|
|
424
|
+
function uninstall3(fqdn) {
|
|
425
|
+
schtasks(["/Delete", "/TN", label3(fqdn), "/F"]);
|
|
426
|
+
}
|
|
427
|
+
function state3(fqdn) {
|
|
428
|
+
const out = schtasks(["/Query", "/TN", label3(fqdn), "/FO", "LIST"]);
|
|
429
|
+
if (!out) return "none";
|
|
430
|
+
if (/\bRunning\b/.test(out)) return "active";
|
|
431
|
+
if (/\bDisabled\b/.test(out)) return "disabled";
|
|
432
|
+
if (/\bReady\b/.test(out)) return "enabled";
|
|
433
|
+
return "enabled";
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// src/core/service.ts
|
|
437
|
+
function pick() {
|
|
438
|
+
switch (process.platform) {
|
|
439
|
+
case "linux":
|
|
440
|
+
return service_systemd_exports;
|
|
441
|
+
case "darwin":
|
|
442
|
+
return service_launchd_exports;
|
|
443
|
+
case "win32":
|
|
444
|
+
return service_windows_exports;
|
|
445
|
+
default:
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function required() {
|
|
450
|
+
const b = pick();
|
|
451
|
+
if (!b) {
|
|
452
|
+
throw new CliError(`Boot services aren't supported on ${process.platform}.`, {
|
|
453
|
+
hint: "run `cloudtunnel up <spec> --detach` and use your OS's own autostart"
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
return b;
|
|
457
|
+
}
|
|
458
|
+
function assertServiceSupported() {
|
|
459
|
+
required().assertSupported();
|
|
460
|
+
}
|
|
461
|
+
function serviceName(fqdn) {
|
|
462
|
+
return pick()?.label(fqdn) ?? `cloudtunnel-${fqdn}`;
|
|
463
|
+
}
|
|
464
|
+
function installServiceForSpec(params) {
|
|
465
|
+
const b = required();
|
|
466
|
+
b.assertSupported();
|
|
467
|
+
b.install(describeService(params));
|
|
468
|
+
}
|
|
469
|
+
function uninstallService(fqdn) {
|
|
470
|
+
pick()?.uninstall(fqdn);
|
|
471
|
+
}
|
|
472
|
+
function serviceState(fqdn) {
|
|
473
|
+
return pick()?.state(fqdn) ?? "none";
|
|
474
|
+
}
|
|
475
|
+
function legacyUnitExists2(profile) {
|
|
476
|
+
return process.platform === "linux" ? legacyUnitExists(profile) : false;
|
|
477
|
+
}
|
|
478
|
+
function removeLegacyUnit2(profile) {
|
|
479
|
+
if (process.platform === "linux") removeLegacyUnit(profile);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// src/config/legacy-migrate.ts
|
|
483
|
+
var skipMarker = `${profilesFile}.migrate-skip`;
|
|
484
|
+
async function migrateLegacyProfiles() {
|
|
485
|
+
if (!existsSync3(profilesFile) || existsSync3(skipMarker)) return;
|
|
486
|
+
let profiles;
|
|
487
|
+
try {
|
|
488
|
+
profiles = JSON.parse(readFileSync(profilesFile, "utf8"));
|
|
489
|
+
} catch {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const legacy = Object.entries(profiles).filter(([name]) => legacyUnitExists2(name));
|
|
493
|
+
if (legacy.length === 0) {
|
|
494
|
+
try {
|
|
495
|
+
renameSync(profilesFile, `${profilesFile}.migrated`);
|
|
496
|
+
} catch {
|
|
497
|
+
}
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const ok = await confirm(`Found ${legacy.length} boot service(s) from an older cloudtunnel. Migrate them now? (needs sudo)`);
|
|
501
|
+
if (!ok) {
|
|
502
|
+
writeFileSync4(skipMarker, "");
|
|
503
|
+
say.dim(` Skipped. Delete ${skipMarker} to be asked again.`);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
let migrated = 0;
|
|
507
|
+
try {
|
|
508
|
+
for (const [name, profile] of legacy) {
|
|
509
|
+
for (const svc of profile.services ?? []) {
|
|
510
|
+
const zone = svc.domain ?? profile.domain;
|
|
511
|
+
if (!zone) continue;
|
|
512
|
+
installServiceForSpec({
|
|
513
|
+
subdomain: svc.name,
|
|
514
|
+
port: svc.port,
|
|
515
|
+
host: svc.host,
|
|
516
|
+
zone,
|
|
517
|
+
proto: svc.proto,
|
|
518
|
+
protocol: profile.protocol
|
|
519
|
+
});
|
|
520
|
+
migrated++;
|
|
521
|
+
}
|
|
522
|
+
removeLegacyUnit2(name);
|
|
523
|
+
}
|
|
524
|
+
renameSync(profilesFile, `${profilesFile}.migrated`);
|
|
525
|
+
say.ok(`Migrated ${migrated} boot service(s). See them with: cloudtunnel ls`);
|
|
526
|
+
} catch (err) {
|
|
527
|
+
writeFileSync4(skipMarker, "");
|
|
528
|
+
say.warn(`Migration incomplete: ${err.message}. Won't retry automatically (delete ${skipMarker} to retry).`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/commands/login.ts
|
|
533
|
+
import * as clack from "@clack/prompts";
|
|
534
|
+
|
|
81
535
|
// src/config/token-url.ts
|
|
82
536
|
import { spawn } from "child_process";
|
|
83
537
|
var REQUIRED_SCOPES = [
|
|
@@ -217,9 +671,7 @@ function registerLogin(program) {
|
|
|
217
671
|
}
|
|
218
672
|
|
|
219
673
|
// src/commands/up.ts
|
|
220
|
-
import
|
|
221
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
222
|
-
import * as clack2 from "@clack/prompts";
|
|
674
|
+
import * as clack3 from "@clack/prompts";
|
|
223
675
|
|
|
224
676
|
// src/config/ensure-auth.ts
|
|
225
677
|
async function ensureAuth() {
|
|
@@ -236,10 +688,10 @@ async function ensureAuth() {
|
|
|
236
688
|
}
|
|
237
689
|
|
|
238
690
|
// src/connector/binary.ts
|
|
239
|
-
import { execFileSync } from "child_process";
|
|
691
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
240
692
|
import { createHash } from "crypto";
|
|
241
|
-
import { chmodSync, existsSync, readFileSync, writeFileSync } from "fs";
|
|
242
|
-
import { join } from "path";
|
|
693
|
+
import { chmodSync, existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
694
|
+
import { join as join5 } from "path";
|
|
243
695
|
var PINNED_VERSION = "2025.1.0";
|
|
244
696
|
var RELEASE_BASE = `https://github.com/cloudflare/cloudflared/releases/download/${PINNED_VERSION}`;
|
|
245
697
|
var ASSETS = {
|
|
@@ -251,18 +703,18 @@ var ASSETS = {
|
|
|
251
703
|
};
|
|
252
704
|
function binaryWorks(bin) {
|
|
253
705
|
try {
|
|
254
|
-
|
|
706
|
+
execFileSync4(bin, ["--version"], { stdio: "ignore" });
|
|
255
707
|
return true;
|
|
256
708
|
} catch {
|
|
257
709
|
return false;
|
|
258
710
|
}
|
|
259
711
|
}
|
|
260
712
|
function cachedPath() {
|
|
261
|
-
return
|
|
713
|
+
return join5(binDir, process.platform === "win32" ? "cloudflared.exe" : "cloudflared");
|
|
262
714
|
}
|
|
263
715
|
function isMusl() {
|
|
264
716
|
try {
|
|
265
|
-
return process.platform === "linux" &&
|
|
717
|
+
return process.platform === "linux" && readFileSync2("/usr/bin/ldd", "utf8").includes("musl");
|
|
266
718
|
} catch {
|
|
267
719
|
return false;
|
|
268
720
|
}
|
|
@@ -270,7 +722,7 @@ function isMusl() {
|
|
|
270
722
|
async function ensureCloudflared() {
|
|
271
723
|
if (binaryWorks("cloudflared")) return "cloudflared";
|
|
272
724
|
const cached = cachedPath();
|
|
273
|
-
if (
|
|
725
|
+
if (existsSync4(cached) && binaryWorks(cached)) return cached;
|
|
274
726
|
return downloadCloudflared(cached);
|
|
275
727
|
}
|
|
276
728
|
async function downloadCloudflared(dest) {
|
|
@@ -298,7 +750,7 @@ async function downloadCloudflared(dest) {
|
|
|
298
750
|
}
|
|
299
751
|
ensureDirs();
|
|
300
752
|
const binary = asset.archive ? extractTgz(bytes) : bytes;
|
|
301
|
-
|
|
753
|
+
writeFileSync5(dest, binary, { mode: 493 });
|
|
302
754
|
chmodSync(dest, 493);
|
|
303
755
|
if (!binaryWorks(dest)) throw new CliError("Downloaded cloudflared is not runnable.");
|
|
304
756
|
return dest;
|
|
@@ -309,26 +761,33 @@ function extractTgz(_bytes) {
|
|
|
309
761
|
});
|
|
310
762
|
}
|
|
311
763
|
|
|
764
|
+
// src/core/up-runner.ts
|
|
765
|
+
import { join as join6 } from "path";
|
|
766
|
+
import * as clack2 from "@clack/prompts";
|
|
767
|
+
|
|
312
768
|
// src/connector/process.ts
|
|
313
|
-
import { execFileSync as
|
|
769
|
+
import { execFileSync as execFileSync5, spawn as spawn2 } from "child_process";
|
|
314
770
|
import { openSync } from "fs";
|
|
315
771
|
|
|
316
772
|
// src/connector/registry.ts
|
|
317
|
-
import { existsSync as
|
|
773
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
318
774
|
import { readFile } from "fs/promises";
|
|
319
|
-
import
|
|
775
|
+
import os3 from "os";
|
|
320
776
|
import lockfile from "proper-lockfile";
|
|
777
|
+
function entryFqdn(e) {
|
|
778
|
+
return e.subdomain === "@" ? e.zone : `${e.subdomain}.${e.zone}`;
|
|
779
|
+
}
|
|
321
780
|
function currentBootId() {
|
|
322
781
|
try {
|
|
323
|
-
return
|
|
782
|
+
return readFileSync3("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
324
783
|
} catch {
|
|
325
|
-
const bootMinute = Math.floor((Date.now() -
|
|
326
|
-
return `boot-${bootMinute}-${
|
|
784
|
+
const bootMinute = Math.floor((Date.now() - os3.uptime() * 1e3) / 6e4);
|
|
785
|
+
return `boot-${bootMinute}-${os3.hostname()}`;
|
|
327
786
|
}
|
|
328
787
|
}
|
|
329
788
|
function readRegistry() {
|
|
330
789
|
try {
|
|
331
|
-
return JSON.parse(
|
|
790
|
+
return JSON.parse(readFileSync3(registryFile, "utf8"));
|
|
332
791
|
} catch {
|
|
333
792
|
return {};
|
|
334
793
|
}
|
|
@@ -336,12 +795,12 @@ function readRegistry() {
|
|
|
336
795
|
function writeRegistry(reg) {
|
|
337
796
|
ensureDirs();
|
|
338
797
|
const tmp = `${registryFile}.tmp`;
|
|
339
|
-
|
|
340
|
-
|
|
798
|
+
writeFileSync6(tmp, JSON.stringify(reg, null, 2), { mode: 384 });
|
|
799
|
+
renameSync2(tmp, registryFile);
|
|
341
800
|
}
|
|
342
801
|
async function mutateRegistry(fn) {
|
|
343
802
|
ensureDirs();
|
|
344
|
-
if (!
|
|
803
|
+
if (!existsSync5(registryFile)) writeFileSync6(registryFile, "{}", { mode: 384 });
|
|
345
804
|
const release = await lockfile.lock(registryFile, { retries: { retries: 10, minTimeout: 50 } });
|
|
346
805
|
try {
|
|
347
806
|
const reg = readRegistry();
|
|
@@ -414,7 +873,7 @@ async function reconcile() {
|
|
|
414
873
|
const entries = listEntries();
|
|
415
874
|
for (const entry of entries) {
|
|
416
875
|
if (entry.state === "running" && !await isOurConnector(entry)) {
|
|
417
|
-
const fqdn =
|
|
876
|
+
const fqdn = entryFqdn(entry);
|
|
418
877
|
await mutateRegistry((reg) => {
|
|
419
878
|
const e = reg[fqdn];
|
|
420
879
|
if (e) {
|
|
@@ -449,7 +908,7 @@ async function stopConnector(entry) {
|
|
|
449
908
|
const pid = entry.pid;
|
|
450
909
|
if (process.platform === "win32") {
|
|
451
910
|
try {
|
|
452
|
-
|
|
911
|
+
execFileSync5("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
|
|
453
912
|
} catch {
|
|
454
913
|
return false;
|
|
455
914
|
}
|
|
@@ -542,33 +1001,6 @@ async function waitHealthy(cf, tunnelId, opts = {}) {
|
|
|
542
1001
|
// src/core/orchestrator-create.ts
|
|
543
1002
|
import { randomInt as randomInt2 } from "crypto";
|
|
544
1003
|
|
|
545
|
-
// src/core/ingress.ts
|
|
546
|
-
var HOSTNAME_RE = /^[a-zA-Z0-9.-]+$/;
|
|
547
|
-
var IPV6_RE = /^[0-9a-fA-F:.]+$/;
|
|
548
|
-
function validateHost(host) {
|
|
549
|
-
let h = host.trim();
|
|
550
|
-
const bracketed = h.startsWith("[") && h.endsWith("]");
|
|
551
|
-
if (bracketed) h = h.slice(1, -1);
|
|
552
|
-
const isV6 = bracketed || h.includes("::") || (h.match(/:/g)?.length ?? 0) >= 2;
|
|
553
|
-
const ok = h.length > 0 && (isV6 ? IPV6_RE.test(h) : HOSTNAME_RE.test(h));
|
|
554
|
-
if (!ok) {
|
|
555
|
-
throw new CliError(`Invalid host "${host}".`, {
|
|
556
|
-
hint: "use a hostname, IPv4, or IPv6 literal (e.g. 192.168.1.5 or ::1) \u2014 no port, scheme, or path"
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
return h;
|
|
560
|
-
}
|
|
561
|
-
function serviceUrl(proto, host, port) {
|
|
562
|
-
const authority = host.includes(":") ? `[${host}]` : host;
|
|
563
|
-
return `${proto}://${authority}:${port}`;
|
|
564
|
-
}
|
|
565
|
-
function buildIngress(opts) {
|
|
566
|
-
return [
|
|
567
|
-
{ hostname: opts.hostname, service: serviceUrl(opts.proto, opts.host ?? "localhost", opts.port) },
|
|
568
|
-
{ service: "http_status:404" }
|
|
569
|
-
];
|
|
570
|
-
}
|
|
571
|
-
|
|
572
1004
|
// src/core/slug.ts
|
|
573
1005
|
import { randomInt } from "crypto";
|
|
574
1006
|
var ADJECTIVES = [
|
|
@@ -607,10 +1039,10 @@ var NOUNS = [
|
|
|
607
1039
|
"tiger",
|
|
608
1040
|
"walnut"
|
|
609
1041
|
];
|
|
610
|
-
var
|
|
1042
|
+
var pick2 = (arr) => arr[randomInt(arr.length)];
|
|
611
1043
|
function randomSlug() {
|
|
612
1044
|
const suffix = randomInt(65536).toString(16).padStart(4, "0");
|
|
613
|
-
return `${
|
|
1045
|
+
return `${pick2(ADJECTIVES)}-${pick2(NOUNS)}-${suffix}`;
|
|
614
1046
|
}
|
|
615
1047
|
function resolveHostSpec(opts, defaultZone) {
|
|
616
1048
|
if (opts.hostname) {
|
|
@@ -667,8 +1099,8 @@ async function createTunnelSubdomain(cf, opts) {
|
|
|
667
1099
|
let dnsRecordId;
|
|
668
1100
|
try {
|
|
669
1101
|
const suffix = randomInt2(65536).toString(16).padStart(4, "0");
|
|
670
|
-
const
|
|
671
|
-
const tunnel = await createTunnel(cf, `${MANAGED_TUNNEL_PREFIX}${
|
|
1102
|
+
const label4 = host.subdomain === "@" ? "root" : host.subdomain;
|
|
1103
|
+
const tunnel = await createTunnel(cf, `${MANAGED_TUNNEL_PREFIX}${label4}-${suffix}`);
|
|
672
1104
|
tunnelId = tunnel.id;
|
|
673
1105
|
const token = await getTunnelToken(cf, tunnelId);
|
|
674
1106
|
await putIngress(cf, tunnelId, buildIngress({ hostname: host.hostname, port: opts.port, proto: opts.proto, host: opts.host }));
|
|
@@ -738,20 +1170,20 @@ function resolveTarget(target) {
|
|
|
738
1170
|
const entries = listEntries();
|
|
739
1171
|
if (/^\d+$/.test(target)) {
|
|
740
1172
|
const byIndex = entries.find((e) => e.index === Number(target));
|
|
741
|
-
if (byIndex) return { fqdn:
|
|
1173
|
+
if (byIndex) return { fqdn: entryFqdn(byIndex), entry: byIndex };
|
|
742
1174
|
}
|
|
743
1175
|
const byId = entries.filter((e) => e.tunnelId?.startsWith(target));
|
|
744
1176
|
const matches = byId.length > 0 ? byId : entries.filter((e) => e.subdomain === target);
|
|
745
1177
|
if (matches.length > 1) {
|
|
746
1178
|
throw new CliError(`"${target}" matches multiple subdomains.`, {
|
|
747
|
-
hint: `use a full hostname or a longer id: ${matches.map(
|
|
1179
|
+
hint: `use a full hostname or a longer id: ${matches.map(entryFqdn).join(", ")}`
|
|
748
1180
|
});
|
|
749
1181
|
}
|
|
750
1182
|
const entry = matches[0];
|
|
751
1183
|
if (!entry) {
|
|
752
1184
|
throw new CliError(`No tracked subdomain matching "${target}".`, { hint: "see `cloudtunnel ls` for the #, name, or id" });
|
|
753
1185
|
}
|
|
754
|
-
return { fqdn:
|
|
1186
|
+
return { fqdn: entryFqdn(entry), entry };
|
|
755
1187
|
}
|
|
756
1188
|
async function removeTunnelSubdomain(cf, target, opts = {}) {
|
|
757
1189
|
const { fqdn, entry } = resolveTarget(target);
|
|
@@ -801,24 +1233,27 @@ async function listAll(cf, opts = {}) {
|
|
|
801
1233
|
const entries = await reconcile();
|
|
802
1234
|
const tunnels = new Map((await listTunnels(cf)).map((t) => [t.id, t]));
|
|
803
1235
|
const rows = entries.map((e) => {
|
|
1236
|
+
const fqdn = entryFqdn(e);
|
|
804
1237
|
const gone = e.tunnelId ? !tunnels.has(e.tunnelId) : false;
|
|
1238
|
+
const svc = serviceState(fqdn);
|
|
805
1239
|
return {
|
|
806
1240
|
num: e.index ? String(e.index) : "-",
|
|
807
|
-
|
|
808
|
-
|
|
1241
|
+
url: `https://${fqdn}`,
|
|
1242
|
+
target: serviceUrl(e.proto, e.host ?? "localhost", e.port),
|
|
809
1243
|
state: !gone && e.state === "running" ? "up" : "down",
|
|
1244
|
+
service: svc === "none" ? "-" : svc,
|
|
810
1245
|
pid: e.state === "running" && e.pid ? String(e.pid) : "-",
|
|
811
1246
|
managed: true
|
|
812
1247
|
};
|
|
813
1248
|
});
|
|
814
1249
|
if (opts.all) {
|
|
815
|
-
const { listCargoCnames } = await import("./dns-
|
|
816
|
-
const { listZones: listZones3 } = await import("./zones-
|
|
817
|
-
const tracked = new Set(entries.map(
|
|
1250
|
+
const { listCargoCnames } = await import("./dns-5OXFAQ4D.js");
|
|
1251
|
+
const { listZones: listZones3 } = await import("./zones-QYV3DSEY.js");
|
|
1252
|
+
const tracked = new Set(entries.map(entryFqdn));
|
|
818
1253
|
for (const zone of await listZones3(cf.token)) {
|
|
819
1254
|
for (const rec of await listCargoCnames(cf.token, zone.id)) {
|
|
820
1255
|
if (!tracked.has(rec.name)) {
|
|
821
|
-
rows.push({ num: "-",
|
|
1256
|
+
rows.push({ num: "-", url: `https://${rec.name}`, target: "-", state: "unmanaged", service: "-", pid: "-", managed: false });
|
|
822
1257
|
}
|
|
823
1258
|
}
|
|
824
1259
|
}
|
|
@@ -826,70 +1261,108 @@ async function listAll(cf, opts = {}) {
|
|
|
826
1261
|
return rows;
|
|
827
1262
|
}
|
|
828
1263
|
|
|
829
|
-
// src/core/
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
if (value === "auto" || value === "http2" || value === "quic") return value;
|
|
833
|
-
throw new CliError(`Invalid protocol "${value}".`, { hint: "use auto, http2, or quic" });
|
|
1264
|
+
// src/core/up-runner.ts
|
|
1265
|
+
function logFileFor(subdomain) {
|
|
1266
|
+
return join6(logDir, `${subdomain === "@" ? "root" : subdomain}.log`);
|
|
834
1267
|
}
|
|
835
|
-
function
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
return
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
1268
|
+
async function startTunnels(cf, bin, items, opts = {}) {
|
|
1269
|
+
const started = [];
|
|
1270
|
+
let tornDown = false;
|
|
1271
|
+
const teardownAll = async (code) => {
|
|
1272
|
+
if (tornDown) return;
|
|
1273
|
+
tornDown = true;
|
|
1274
|
+
try {
|
|
1275
|
+
for (const s of started) {
|
|
1276
|
+
try {
|
|
1277
|
+
await removeTunnelSubdomain(cf, s.fqdn, { force: true, quiet: true });
|
|
1278
|
+
} catch {
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
if (process.stdout.isTTY) clack2.outro(`Stopped \xB7 released ${started.length} subdomain(s)`);
|
|
1282
|
+
} catch (err) {
|
|
1283
|
+
reportError(err);
|
|
1284
|
+
} finally {
|
|
1285
|
+
process.exit(code);
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
const spin = clack2.spinner();
|
|
1289
|
+
spin.start(items.length > 1 ? "Creating tunnels\u2026" : "Creating tunnel\u2026");
|
|
1290
|
+
for (const item of items) {
|
|
1291
|
+
spin.message(`Creating ${item.name ?? "tunnel"} (:${item.port})\u2026`);
|
|
1292
|
+
const result = await createTunnelSubdomain(cf, item);
|
|
1293
|
+
const fqdn = result.host.hostname;
|
|
1294
|
+
const logFile = logFileFor(result.host.subdomain);
|
|
1295
|
+
const conn = startConnector({
|
|
1296
|
+
bin,
|
|
1297
|
+
token: result.token,
|
|
1298
|
+
detach: !!opts.detach,
|
|
1299
|
+
logFile,
|
|
1300
|
+
protocol: opts.protocol,
|
|
1301
|
+
onExit: opts.detach ? void 0 : (code) => {
|
|
1302
|
+
if (!tornDown) {
|
|
1303
|
+
say.warn(`Connector for ${fqdn} exited.`);
|
|
1304
|
+
void teardownAll(code ?? 1);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1308
|
+
await patchEntry(fqdn, { pid: conn.pid, bootId: currentBootId(), logFile });
|
|
1309
|
+
started.push({
|
|
1310
|
+
fqdn,
|
|
1311
|
+
subdomain: result.host.subdomain,
|
|
1312
|
+
tunnelId: result.tunnelId,
|
|
1313
|
+
target: serviceUrl(item.proto, item.host ?? "localhost", item.port),
|
|
1314
|
+
pid: conn.pid
|
|
1315
|
+
});
|
|
853
1316
|
}
|
|
854
|
-
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
}
|
|
861
|
-
function removeProfile(name) {
|
|
862
|
-
const profiles = readProfiles();
|
|
863
|
-
if (!profiles[name]) throw new CliError(`No profile named "${name}".`);
|
|
864
|
-
delete profiles[name];
|
|
865
|
-
writeProfiles(profiles);
|
|
866
|
-
}
|
|
867
|
-
function parseServiceSpec(spec) {
|
|
868
|
-
const at = spec.indexOf("@");
|
|
869
|
-
const host = at >= 0 ? validateHost(spec.slice(at + 1)) : void 0;
|
|
870
|
-
const [name, portStr, proto] = (at >= 0 ? spec.slice(0, at) : spec).split(":");
|
|
871
|
-
const port = Number(portStr);
|
|
872
|
-
if (!name || !Number.isInteger(port) || port < 1 || port > 65535) {
|
|
873
|
-
throw new CliError(`Invalid service "${spec}".`, { hint: "use name:port, e.g. api:3000 or web:5173:https" });
|
|
1317
|
+
if (opts.detach) {
|
|
1318
|
+
spin.stop(`${started.length} tunnel(s) started in the background`);
|
|
1319
|
+
const lines2 = started.map((s) => `${formatRoute(s.fqdn, s.target)} ${dim(`pid ${s.pid}`)}`);
|
|
1320
|
+
clack2.note(lines2.join("\n"), "running in background");
|
|
1321
|
+
if (process.stdout.isTTY) clack2.outro("Stop with: cloudtunnel delete <#|--all>");
|
|
1322
|
+
return;
|
|
874
1323
|
}
|
|
875
|
-
|
|
876
|
-
|
|
1324
|
+
for (const sig of ["SIGINT", "SIGHUP", "SIGTERM"]) {
|
|
1325
|
+
process.on(sig, () => void teardownAll(0));
|
|
877
1326
|
}
|
|
878
|
-
|
|
1327
|
+
spin.message("Connecting to the Cloudflare edge\u2026");
|
|
1328
|
+
const healths = await Promise.all(started.map((s) => waitHealthy(cf, s.tunnelId, { timeoutMs: 3e4 })));
|
|
1329
|
+
const live = healths.filter((h) => h === "healthy").length;
|
|
1330
|
+
spin.stop(`${started.length} tunnel(s) started`);
|
|
1331
|
+
const lines = started.map((s, i) => `${formatRoute(s.fqdn, s.target)}${healths[i] === "healthy" ? "" : dim(` (${healths[i]})`)}`);
|
|
1332
|
+
clack2.note(lines.join("\n"), `${live}/${started.length} live`);
|
|
1333
|
+
say.dim("Ctrl-C stops and releases them.");
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// src/core/transport-protocol.ts
|
|
1337
|
+
function parseTransportProtocol(value) {
|
|
1338
|
+
if (value === "auto" || value === "http2" || value === "quic") return value;
|
|
1339
|
+
throw new CliError(`Invalid protocol "${value}".`, { hint: "use auto, http2, or quic" });
|
|
879
1340
|
}
|
|
880
1341
|
|
|
881
1342
|
// src/commands/up.ts
|
|
882
|
-
function
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1343
|
+
function promptOrExit(value) {
|
|
1344
|
+
if (clack3.isCancel(value)) {
|
|
1345
|
+
clack3.cancel("Cancelled.");
|
|
1346
|
+
process.exit(130);
|
|
886
1347
|
}
|
|
887
|
-
return
|
|
1348
|
+
return value;
|
|
1349
|
+
}
|
|
1350
|
+
async function promptPort() {
|
|
1351
|
+
const input = promptOrExit(
|
|
1352
|
+
await clack3.text({
|
|
1353
|
+
message: "Port to expose",
|
|
1354
|
+
placeholder: "e.g. 3000",
|
|
1355
|
+
validate: (v) => {
|
|
1356
|
+
const n = Number(v);
|
|
1357
|
+
if (!Number.isInteger(n) || n < 1 || n > 65535) return "Enter a port 1\u201365535";
|
|
1358
|
+
return void 0;
|
|
1359
|
+
}
|
|
1360
|
+
})
|
|
1361
|
+
);
|
|
1362
|
+
return Number(input);
|
|
888
1363
|
}
|
|
889
1364
|
async function resolveDomain(cf, opts, creds) {
|
|
890
|
-
if (opts.
|
|
891
|
-
const explicit = opts.domain ?? opts.zone;
|
|
892
|
-
if (explicit) return explicit;
|
|
1365
|
+
if (opts.domain) return opts.domain;
|
|
893
1366
|
const zones = await listZones(cf.token);
|
|
894
1367
|
if (zones.length === 0) throw new CliError("No domains found in this Cloudflare account.");
|
|
895
1368
|
if (zones.length === 1) return zones[0].name;
|
|
@@ -897,111 +1370,66 @@ async function resolveDomain(cf, opts, creds) {
|
|
|
897
1370
|
if (creds.defaultZone) return creds.defaultZone;
|
|
898
1371
|
throw new CliError("Multiple domains in this account \u2014 pick one.", { hint: "pass -d <domain>" });
|
|
899
1372
|
}
|
|
900
|
-
async function
|
|
901
|
-
|
|
902
|
-
if (
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
clack2.cancel("Cancelled.");
|
|
907
|
-
process.exit(130);
|
|
908
|
-
}
|
|
1373
|
+
async function resolveSpecSubdomain(spec, opts) {
|
|
1374
|
+
if (spec.subdomain !== void 0) return spec.subdomain;
|
|
1375
|
+
if (opts.yes || !process.stdin.isTTY) return void 0;
|
|
1376
|
+
const input = promptOrExit(
|
|
1377
|
+
await clack3.text({ message: `Subdomain for :${spec.port}`, placeholder: "blank = random \xB7 @ = root domain" })
|
|
1378
|
+
);
|
|
909
1379
|
return input.trim() || void 0;
|
|
910
1380
|
}
|
|
911
|
-
function
|
|
912
|
-
try {
|
|
913
|
-
const tail = readFileSync4(logFile, "utf8").trim().split("\n").slice(-8).join("\n");
|
|
914
|
-
if (tail) say.dim(tail);
|
|
915
|
-
} catch {
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
async function runUp(portArg, opts) {
|
|
919
|
-
const port = parsePort(portArg);
|
|
1381
|
+
async function runUp(specArgs, opts) {
|
|
920
1382
|
const protocol = opts.protocol ? parseTransportProtocol(opts.protocol) : void 0;
|
|
921
|
-
const
|
|
1383
|
+
const parsed = specArgs.length ? specArgs.map(parseTunnelSpec) : null;
|
|
1384
|
+
if (parsed === null && !process.stdin.isTTY) {
|
|
1385
|
+
throw new CliError("No tunnel spec given.", { hint: "e.g. cloudtunnel api:8080" });
|
|
1386
|
+
}
|
|
922
1387
|
const creds = await ensureAuth();
|
|
923
1388
|
const cf = resolveCf();
|
|
924
1389
|
const bin = await ensureCloudflared();
|
|
925
|
-
if (process.stdout.isTTY)
|
|
1390
|
+
if (process.stdout.isTTY) clack3.intro("cloudtunnel");
|
|
1391
|
+
const specs = parsed ?? [{ port: await promptPort() }];
|
|
926
1392
|
const domain = await resolveDomain(cf, opts, creds);
|
|
927
|
-
const
|
|
928
|
-
const
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
const started2 = startConnector({ bin, token: result.token, detach: true, logFile, protocol });
|
|
945
|
-
await patchEntry(fqdn, { pid: started2.pid, bootId: currentBootId(), logFile });
|
|
946
|
-
clack2.note(formatRoute(fqdn, target), `pid ${started2.pid}`);
|
|
947
|
-
if (process.stdout.isTTY) clack2.outro(`Stop it with: cloudtunnel down ${result.host.subdomain}`);
|
|
1393
|
+
const items = [];
|
|
1394
|
+
for (const spec of specs) {
|
|
1395
|
+
let name = await resolveSpecSubdomain(spec, opts);
|
|
1396
|
+
if (opts.service && name === void 0) name = randomSlug();
|
|
1397
|
+
items.push({
|
|
1398
|
+
port: spec.port,
|
|
1399
|
+
proto: opts.proto,
|
|
1400
|
+
name,
|
|
1401
|
+
zone: domain,
|
|
1402
|
+
host: spec.host,
|
|
1403
|
+
defaultZone: creds.defaultZone,
|
|
1404
|
+
force: opts.force,
|
|
1405
|
+
yes: opts.yes
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
if (opts.service) {
|
|
1409
|
+
registerServices(items, domain, opts.proto, protocol);
|
|
948
1410
|
return;
|
|
949
1411
|
}
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
};
|
|
958
|
-
spin.start("Connecting to the Cloudflare edge\u2026");
|
|
959
|
-
const controller = new AbortController();
|
|
960
|
-
let tornDown = false;
|
|
961
|
-
const teardown = async (exitCode) => {
|
|
962
|
-
if (tornDown) return;
|
|
963
|
-
tornDown = true;
|
|
964
|
-
controller.abort();
|
|
965
|
-
stopSpin("Stopping\u2026");
|
|
966
|
-
try {
|
|
967
|
-
await removeTunnelSubdomain(cf, fqdn, { force: true, quiet: true });
|
|
968
|
-
clack2.outro(`Stopped \xB7 ${fqdn} released`);
|
|
969
|
-
} catch (err) {
|
|
970
|
-
reportError(err);
|
|
971
|
-
} finally {
|
|
972
|
-
process.exit(exitCode);
|
|
973
|
-
}
|
|
974
|
-
};
|
|
975
|
-
const started = startConnector({
|
|
976
|
-
bin,
|
|
977
|
-
token: result.token,
|
|
978
|
-
detach: false,
|
|
979
|
-
logFile,
|
|
980
|
-
protocol,
|
|
981
|
-
onExit: (code) => {
|
|
982
|
-
if (!tornDown) {
|
|
983
|
-
stopSpin("cloudflared exited");
|
|
984
|
-
showLogTail(logFile);
|
|
985
|
-
void teardown(code ?? 1);
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
});
|
|
989
|
-
await patchEntry(fqdn, { pid: started.pid, bootId: currentBootId(), logFile });
|
|
990
|
-
for (const sig of ["SIGINT", "SIGHUP", "SIGTERM"]) {
|
|
991
|
-
process.on(sig, () => void teardown(0));
|
|
1412
|
+
await startTunnels(cf, bin, items, { detach: opts.detach, protocol });
|
|
1413
|
+
}
|
|
1414
|
+
function registerServices(items, domain, proto, protocol) {
|
|
1415
|
+
assertServiceSupported();
|
|
1416
|
+
if (!protocol) {
|
|
1417
|
+
say.warn("No edge protocol set \u2014 cloudflared will pick QUIC, which some networks drop.");
|
|
1418
|
+
say.dim(" \u2192 add --protocol http2 for UDP-hostile networks");
|
|
992
1419
|
}
|
|
993
|
-
const
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
stopSpin("Provisioning");
|
|
1000
|
-
say.warn(`${fqdn} is not healthy yet \u2014 it should be live shortly.`);
|
|
1420
|
+
const done = [];
|
|
1421
|
+
for (const item of items) {
|
|
1422
|
+
const subdomain = item.name;
|
|
1423
|
+
const fqdn = subdomain === "@" ? domain : `${subdomain}.${domain}`;
|
|
1424
|
+
installServiceForSpec({ subdomain, port: item.port, host: item.host, zone: domain, proto, protocol });
|
|
1425
|
+
done.push(`${serviceName(fqdn)} \u2192 https://${fqdn}`);
|
|
1001
1426
|
}
|
|
1427
|
+
say.ok(`Registered ${done.length} boot service(s):`);
|
|
1428
|
+
for (const line of done) say.dim(` ${line}`);
|
|
1429
|
+
say.dim(" \u2192 check them: cloudtunnel ls \xB7 remove: cloudtunnel delete <#>");
|
|
1002
1430
|
}
|
|
1003
1431
|
function registerUp(program) {
|
|
1004
|
-
program.command("up").argument("
|
|
1432
|
+
program.command("up", { isDefault: true }).argument("[specs...]", "tunnels to start: [subdomain:]port[@host] (e.g. api:8080 web:8081@localhost)").description("Start one or more tunnels (also: `cloudtunnel 8080`)").option("-d, --domain <domain>", "domain for the subdomains (prompted from a list if unset)").option("--proto <proto>", "local service protocol: http | https", "http").option("--protocol <proto>", "cloudflared edge transport: auto | http2 | quic (http2 for UDP-hostile networks)").option("--detach", "run the connectors in the background").option("--service", "register each subdomain as a boot service (Linux systemd \xB7 macOS launchd \xB7 Windows Task Scheduler)").option("-f, --force", "replace a non-tunnel DNS record occupying the hostname").option("-y, --yes", "don't prompt; don't ask before replacing an existing record").action((specs, opts) => runUp(specs, opts));
|
|
1005
1433
|
}
|
|
1006
1434
|
|
|
1007
1435
|
// src/commands/ls.ts
|
|
@@ -1015,15 +1443,23 @@ function registerLs(program) {
|
|
|
1015
1443
|
return;
|
|
1016
1444
|
}
|
|
1017
1445
|
printTable(
|
|
1018
|
-
["#", "
|
|
1019
|
-
rows.map((r) => [r.num, r.
|
|
1446
|
+
["#", "URL", "TARGET", "STATE", "SERVICE", "PID"],
|
|
1447
|
+
rows.map((r) => [r.num, r.url, r.target, r.state, r.service, r.pid])
|
|
1020
1448
|
);
|
|
1021
1449
|
});
|
|
1022
1450
|
}
|
|
1023
1451
|
|
|
1024
|
-
// src/commands/
|
|
1025
|
-
function
|
|
1026
|
-
|
|
1452
|
+
// src/commands/delete.ts
|
|
1453
|
+
async function deleteOne(cf, fqdn, opts) {
|
|
1454
|
+
const hasService = serviceState(fqdn) !== "none";
|
|
1455
|
+
if (hasService && !opts.dryRun) uninstallService(fqdn);
|
|
1456
|
+
await removeTunnelSubdomain(cf, fqdn, { force: opts.force, dryRun: opts.dryRun });
|
|
1457
|
+
if (!hasService) return;
|
|
1458
|
+
if (opts.dryRun) say.info(`Would also remove boot service ${serviceName(fqdn)}`);
|
|
1459
|
+
else say.ok(`Removed boot service ${serviceName(fqdn)}`);
|
|
1460
|
+
}
|
|
1461
|
+
function registerDelete(program) {
|
|
1462
|
+
program.command("delete").argument("[targets...]", "subdomains to remove by # / name / URL (omit with --all)").description("Release tunnel(s) \u2014 deletes the tunnel + DNS, and any systemd boot service").option("--all", "release every tracked subdomain").option("-f, --force", "release even a resource not created by cloudtunnel").option("--dry-run", "show what would be released without doing it").action(async (targets, opts) => {
|
|
1027
1463
|
await ensureAuth();
|
|
1028
1464
|
const cf = resolveCf();
|
|
1029
1465
|
if (opts.all) {
|
|
@@ -1033,259 +1469,27 @@ function registerDown(program) {
|
|
|
1033
1469
|
return;
|
|
1034
1470
|
}
|
|
1035
1471
|
for (const e of entries) {
|
|
1472
|
+
const fqdn = entryFqdn(e);
|
|
1036
1473
|
try {
|
|
1037
|
-
await
|
|
1474
|
+
await deleteOne(cf, fqdn, opts);
|
|
1038
1475
|
} catch (err) {
|
|
1039
|
-
say.warn(`Could not release ${
|
|
1476
|
+
say.warn(`Could not release ${fqdn}: ${err.message}`);
|
|
1040
1477
|
}
|
|
1041
1478
|
}
|
|
1042
1479
|
return;
|
|
1043
1480
|
}
|
|
1044
|
-
if (
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
// src/commands/zones.ts
|
|
1050
|
-
function registerZones(program) {
|
|
1051
|
-
program.command("zones").description("List the zones (domains) available in your Cloudflare account").action(async () => {
|
|
1052
|
-
await ensureAuth();
|
|
1053
|
-
const cf = resolveCf();
|
|
1054
|
-
const zones = await listZones(cf.token);
|
|
1055
|
-
if (zones.length === 0) {
|
|
1056
|
-
say.info("No zones in this account.");
|
|
1057
|
-
return;
|
|
1058
|
-
}
|
|
1059
|
-
printTable(
|
|
1060
|
-
["ZONE", "STATUS", "ID"],
|
|
1061
|
-
zones.map((z) => [z.name, z.status ?? "-", z.id])
|
|
1062
|
-
);
|
|
1063
|
-
});
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
// src/commands/save.ts
|
|
1067
|
-
function registerSave(program) {
|
|
1068
|
-
program.command("save").argument("<profile>", "profile name, e.g. mb").argument("[services...]", "services as name:port[:proto], e.g. api:3000 web:5173").description("Save a group of services as a profile you can `run` together").option("--from-running", "snapshot the currently tracked tunnels instead of listing services").option("-d, --domain <domain>", "default domain for this profile").option("--protocol <proto>", "edge transport for this profile: auto | http2 | quic").action((profile, specs, opts) => {
|
|
1069
|
-
let services;
|
|
1070
|
-
if (opts.fromRunning) {
|
|
1071
|
-
const entries = listEntries().filter((e) => e.tunnelId);
|
|
1072
|
-
if (entries.length === 0) {
|
|
1073
|
-
throw new CliError("No tunnels to snapshot.", { hint: "start some with `cloudtunnel up`, or pass services like api:3000" });
|
|
1074
|
-
}
|
|
1075
|
-
services = entries.map((e) => ({ name: e.subdomain, port: e.port, proto: e.proto, domain: e.zone, ...e.host ? { host: e.host } : {} }));
|
|
1076
|
-
} else {
|
|
1077
|
-
if (specs.length === 0) {
|
|
1078
|
-
throw new CliError("No services given.", { hint: "e.g. `cloudtunnel save mb api:3000 web:5173`" });
|
|
1079
|
-
}
|
|
1080
|
-
services = specs.map(parseServiceSpec);
|
|
1481
|
+
if (targets.length === 0) throw new CliError("Pass a subdomain (# / name / URL) or --all.");
|
|
1482
|
+
for (const target of targets) {
|
|
1483
|
+
const { fqdn } = resolveTarget(target);
|
|
1484
|
+
await deleteOne(cf, fqdn, opts);
|
|
1081
1485
|
}
|
|
1082
|
-
const protocol = opts.protocol ? parseTransportProtocol(opts.protocol) : void 0;
|
|
1083
|
-
saveProfile(profile, { services, domain: opts.domain, protocol });
|
|
1084
|
-
say.ok(`Saved profile "${profile}" (${services.length} service${services.length === 1 ? "" : "s"}). Run it: cloudtunnel run ${profile}`);
|
|
1085
|
-
});
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
// src/commands/run.ts
|
|
1089
|
-
import { join as join3 } from "path";
|
|
1090
|
-
import * as clack3 from "@clack/prompts";
|
|
1091
|
-
async function runProfile(name, opts) {
|
|
1092
|
-
const creds = await ensureAuth();
|
|
1093
|
-
const cf = resolveCf();
|
|
1094
|
-
const bin = await ensureCloudflared();
|
|
1095
|
-
const profile = getProfile(name);
|
|
1096
|
-
const protocol = opts.protocol ? parseTransportProtocol(opts.protocol) : profile.protocol;
|
|
1097
|
-
if (process.stdout.isTTY) clack3.intro(`cloudtunnel \xB7 profile "${name}"`);
|
|
1098
|
-
const spin = clack3.spinner();
|
|
1099
|
-
spin.start("Creating tunnels\u2026");
|
|
1100
|
-
const started = [];
|
|
1101
|
-
for (const svc of profile.services) {
|
|
1102
|
-
spin.message(`Creating ${svc.name} (:${svc.port})\u2026`);
|
|
1103
|
-
const result = await createTunnelSubdomain(cf, {
|
|
1104
|
-
port: svc.port,
|
|
1105
|
-
proto: svc.proto,
|
|
1106
|
-
name: svc.name,
|
|
1107
|
-
host: svc.host,
|
|
1108
|
-
zone: svc.domain ?? opts.domain ?? profile.domain,
|
|
1109
|
-
defaultZone: creds.defaultZone,
|
|
1110
|
-
force: opts.force,
|
|
1111
|
-
yes: true
|
|
1112
|
-
// batch: never prompt per service
|
|
1113
|
-
});
|
|
1114
|
-
const fqdn = result.host.hostname;
|
|
1115
|
-
const logFile = join3(logDir, `${result.host.subdomain}.log`);
|
|
1116
|
-
const conn = startConnector({
|
|
1117
|
-
bin,
|
|
1118
|
-
token: result.token,
|
|
1119
|
-
detach: !!opts.detach,
|
|
1120
|
-
logFile,
|
|
1121
|
-
protocol,
|
|
1122
|
-
onExit: opts.detach ? void 0 : () => say.warn(`Connector for ${fqdn} exited.`)
|
|
1123
|
-
});
|
|
1124
|
-
await patchEntry(fqdn, { pid: conn.pid, bootId: currentBootId(), logFile });
|
|
1125
|
-
started.push({ fqdn, subdomain: result.host.subdomain, tunnelId: result.tunnelId, target: serviceUrl(svc.proto, svc.host ?? "localhost", svc.port), pid: conn.pid });
|
|
1126
|
-
}
|
|
1127
|
-
if (opts.detach) {
|
|
1128
|
-
spin.stop(`${started.length} service(s) started in the background`);
|
|
1129
|
-
const lines2 = started.map((s) => `${formatRoute(s.fqdn, s.target)} ${dim(`pid ${s.pid}`)}`);
|
|
1130
|
-
clack3.note(lines2.join("\n"), `profile "${name}" \u2014 running in background`);
|
|
1131
|
-
if (process.stdout.isTTY) clack3.outro("Stop them with: cloudtunnel down --all");
|
|
1132
|
-
return;
|
|
1133
|
-
}
|
|
1134
|
-
spin.message("Connecting to the Cloudflare edge\u2026");
|
|
1135
|
-
const healths = await Promise.all(started.map((s) => waitHealthy(cf, s.tunnelId, { timeoutMs: 3e4 })));
|
|
1136
|
-
const live = healths.filter((h) => h === "healthy").length;
|
|
1137
|
-
spin.stop(`${started.length} service(s) started`);
|
|
1138
|
-
const lines = started.map((s, i) => `${formatRoute(s.fqdn, s.target)}${healths[i] === "healthy" ? "" : dim(` (${healths[i]})`)}`);
|
|
1139
|
-
clack3.note(lines.join("\n"), `profile "${name}" \u2014 ${live}/${started.length} live`);
|
|
1140
|
-
say.dim("Ctrl-C stops and releases all of them.");
|
|
1141
|
-
let tornDown = false;
|
|
1142
|
-
const teardownAll = async (code) => {
|
|
1143
|
-
if (tornDown) return;
|
|
1144
|
-
tornDown = true;
|
|
1145
|
-
try {
|
|
1146
|
-
for (const s of started) {
|
|
1147
|
-
try {
|
|
1148
|
-
await removeTunnelSubdomain(cf, s.fqdn, { force: true, quiet: true });
|
|
1149
|
-
} catch {
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
if (process.stdout.isTTY) clack3.outro(`Stopped \xB7 released ${started.length} subdomain(s)`);
|
|
1153
|
-
} catch (err) {
|
|
1154
|
-
reportError(err);
|
|
1155
|
-
} finally {
|
|
1156
|
-
process.exit(code);
|
|
1157
|
-
}
|
|
1158
|
-
};
|
|
1159
|
-
for (const sig of ["SIGINT", "SIGHUP", "SIGTERM"]) {
|
|
1160
|
-
process.on(sig, () => void teardownAll(0));
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
function registerRun(program) {
|
|
1164
|
-
program.command("run").argument("<profile>", "name of a saved profile (see `cloudtunnel profiles`)").description("Start every service in a saved profile at once").option("-f, --force", "take over subdomains already occupied by another record").option("-d, --domain <domain>", "override the profile's domain for this run").option("--detach", "run all connectors in the background (stop with `cloudtunnel down --all`)").option("--protocol <proto>", "edge transport: auto | http2 | quic (overrides the profile's saved protocol)").action((name, opts) => runProfile(name, opts));
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
// src/core/systemd.ts
|
|
1168
|
-
import { execFileSync as execFileSync3 } from "child_process";
|
|
1169
|
-
import { writeFileSync as writeFileSync4 } from "fs";
|
|
1170
|
-
import { tmpdir } from "os";
|
|
1171
|
-
import { dirname, join as join4 } from "path";
|
|
1172
|
-
function serviceName(profile) {
|
|
1173
|
-
return `cloudtunnel-${profile}.service`;
|
|
1174
|
-
}
|
|
1175
|
-
function unitPath(profile) {
|
|
1176
|
-
return `/etc/systemd/system/${serviceName(profile)}`;
|
|
1177
|
-
}
|
|
1178
|
-
function buildUnit(p) {
|
|
1179
|
-
const nodeBin = dirname(p.nodePath);
|
|
1180
|
-
const proto = p.protocol ? ` --protocol ${p.protocol}` : "";
|
|
1181
|
-
return [
|
|
1182
|
-
"[Unit]",
|
|
1183
|
-
`Description=cloudtunnel profile "${p.profile}" (Cloudflare Tunnel)`,
|
|
1184
|
-
"After=network-online.target",
|
|
1185
|
-
"Wants=network-online.target",
|
|
1186
|
-
"",
|
|
1187
|
-
"[Service]",
|
|
1188
|
-
"Type=simple",
|
|
1189
|
-
`User=${p.user}`,
|
|
1190
|
-
`Environment=HOME=${p.home}`,
|
|
1191
|
-
`Environment=PATH=${nodeBin}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`,
|
|
1192
|
-
`ExecStart=${p.nodePath} ${p.scriptPath} run ${p.profile} -f${proto}`,
|
|
1193
|
-
"Restart=on-failure",
|
|
1194
|
-
"RestartSec=5",
|
|
1195
|
-
"",
|
|
1196
|
-
"[Install]",
|
|
1197
|
-
"WantedBy=multi-user.target",
|
|
1198
|
-
""
|
|
1199
|
-
].join("\n");
|
|
1200
|
-
}
|
|
1201
|
-
function assertSystemd() {
|
|
1202
|
-
if (process.platform !== "linux") {
|
|
1203
|
-
throw new CliError("Service registration is Linux/systemd only.", {
|
|
1204
|
-
hint: "on macOS/Windows run `cloudtunnel run <profile> --detach` at login instead"
|
|
1205
|
-
});
|
|
1206
|
-
}
|
|
1207
|
-
try {
|
|
1208
|
-
execFileSync3("systemctl", ["--version"], { stdio: "ignore" });
|
|
1209
|
-
} catch {
|
|
1210
|
-
throw new CliError("systemd (systemctl) was not found on this host.");
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
function privileged(args) {
|
|
1214
|
-
const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
|
|
1215
|
-
const argv = isRoot ? args : ["sudo", ...args];
|
|
1216
|
-
execFileSync3(argv[0], argv.slice(1), { stdio: "inherit" });
|
|
1217
|
-
}
|
|
1218
|
-
function query(args) {
|
|
1219
|
-
try {
|
|
1220
|
-
return execFileSync3("systemctl", args, {
|
|
1221
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
1222
|
-
encoding: "utf8"
|
|
1223
|
-
}).trim();
|
|
1224
|
-
} catch (err) {
|
|
1225
|
-
const out = err.stdout;
|
|
1226
|
-
return out ? out.toString().trim() : "";
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
function installService(p) {
|
|
1230
|
-
assertSystemd();
|
|
1231
|
-
const tmp = join4(tmpdir(), serviceName(p.profile));
|
|
1232
|
-
writeFileSync4(tmp, buildUnit(p), { mode: 420 });
|
|
1233
|
-
privileged(["install", "-m", "0644", tmp, unitPath(p.profile)]);
|
|
1234
|
-
privileged(["systemctl", "daemon-reload"]);
|
|
1235
|
-
privileged(["systemctl", "enable", "--now", serviceName(p.profile)]);
|
|
1236
|
-
}
|
|
1237
|
-
function uninstallService(profile) {
|
|
1238
|
-
assertSystemd();
|
|
1239
|
-
try {
|
|
1240
|
-
privileged(["systemctl", "disable", "--now", serviceName(profile)]);
|
|
1241
|
-
} catch {
|
|
1242
|
-
}
|
|
1243
|
-
privileged(["rm", "-f", unitPath(profile)]);
|
|
1244
|
-
privileged(["systemctl", "daemon-reload"]);
|
|
1245
|
-
}
|
|
1246
|
-
function serviceState(profile) {
|
|
1247
|
-
if (process.platform !== "linux") return "none";
|
|
1248
|
-
const name = serviceName(profile);
|
|
1249
|
-
if (query(["is-active", name]) === "active") return "active";
|
|
1250
|
-
const enabled = query(["is-enabled", name]);
|
|
1251
|
-
if (enabled === "enabled" || enabled === "enabled-runtime") return "enabled";
|
|
1252
|
-
if (enabled === "disabled" || enabled === "static") return "disabled";
|
|
1253
|
-
return "none";
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
// src/commands/profiles.ts
|
|
1257
|
-
function formatService(state) {
|
|
1258
|
-
return state === "none" ? dim("\u2013") : state;
|
|
1259
|
-
}
|
|
1260
|
-
function registerProfiles(program) {
|
|
1261
|
-
program.command("profiles").description("List saved profiles (or delete one with --rm)").option("--rm <name>", "delete a profile").action((opts) => {
|
|
1262
|
-
if (opts.rm) {
|
|
1263
|
-
removeProfile(opts.rm);
|
|
1264
|
-
say.ok(`Deleted profile "${opts.rm}".`);
|
|
1265
|
-
return;
|
|
1266
|
-
}
|
|
1267
|
-
const profiles = listProfiles();
|
|
1268
|
-
if (profiles.length === 0) {
|
|
1269
|
-
say.info("No profiles yet. Create one: `cloudtunnel save mb api:3000 web:5173`");
|
|
1270
|
-
return;
|
|
1271
|
-
}
|
|
1272
|
-
printTable(
|
|
1273
|
-
["PROFILE", "SERVICES", "DOMAIN", "PROTOCOL", "SERVICE"],
|
|
1274
|
-
profiles.map(({ name, profile }) => [
|
|
1275
|
-
name,
|
|
1276
|
-
profile.services.map((s) => `${s.name}:${s.port}${s.host ? `@${s.host}` : ""}`).join(", "),
|
|
1277
|
-
profile.domain ?? "(default)",
|
|
1278
|
-
profile.protocol ?? "auto",
|
|
1279
|
-
formatService(serviceState(name))
|
|
1280
|
-
])
|
|
1281
|
-
);
|
|
1282
1486
|
});
|
|
1283
1487
|
}
|
|
1284
1488
|
|
|
1285
1489
|
// src/commands/logs.ts
|
|
1286
|
-
import { closeSync, existsSync as
|
|
1490
|
+
import { closeSync, existsSync as existsSync6, openSync as openSync2, readFileSync as readFileSync4, readSync, statSync, watch } from "fs";
|
|
1287
1491
|
function printTail(file, n) {
|
|
1288
|
-
const lines =
|
|
1492
|
+
const lines = readFileSync4(file, "utf8").split("\n");
|
|
1289
1493
|
const tail = lines.slice(-n).join("\n");
|
|
1290
1494
|
process.stdout.write(tail.endsWith("\n") ? tail : `${tail}
|
|
1291
1495
|
`);
|
|
@@ -1317,7 +1521,7 @@ function follow(file, fromPos) {
|
|
|
1317
1521
|
function registerLogs(program) {
|
|
1318
1522
|
program.command("logs").argument("<target>", "subdomain name / hostname / id / #").description("Show the connector log for a subdomain (use -f to follow)").option("-f, --follow", "keep printing new log lines (like tail -f)").option("-n, --lines <n>", "number of lines to show", "50").action((name, opts) => {
|
|
1319
1523
|
const { fqdn, entry } = resolveTarget(name);
|
|
1320
|
-
if (!entry?.logFile || !
|
|
1524
|
+
if (!entry?.logFile || !existsSync6(entry.logFile)) {
|
|
1321
1525
|
throw new CliError(`No logs for ${fqdn} yet.`, { hint: "start it with `cloudtunnel up` or `cloudtunnel run`" });
|
|
1322
1526
|
}
|
|
1323
1527
|
const n = Math.max(1, Number(opts.lines) || 50);
|
|
@@ -1326,113 +1530,39 @@ function registerLogs(program) {
|
|
|
1326
1530
|
});
|
|
1327
1531
|
}
|
|
1328
1532
|
|
|
1329
|
-
// src/commands/service.ts
|
|
1330
|
-
import os2 from "os";
|
|
1331
|
-
import { realpathSync } from "fs";
|
|
1332
|
-
function entryScript() {
|
|
1333
|
-
const p = process.argv[1];
|
|
1334
|
-
if (!p) throw new CliError("Cannot resolve the cloudtunnel executable path.");
|
|
1335
|
-
return realpathSync(p);
|
|
1336
|
-
}
|
|
1337
|
-
function enable(name, opts) {
|
|
1338
|
-
const profile = getProfile(name);
|
|
1339
|
-
let protocol = profile.protocol;
|
|
1340
|
-
if (opts.protocol) {
|
|
1341
|
-
protocol = parseTransportProtocol(opts.protocol);
|
|
1342
|
-
saveProfile(name, { ...profile, protocol });
|
|
1343
|
-
}
|
|
1344
|
-
if (!protocol) {
|
|
1345
|
-
say.warn("No edge protocol set \u2014 cloudflared will pick QUIC, which some networks drop.");
|
|
1346
|
-
say.dim(" \u2192 set one with: cloudtunnel service enable " + name + " --protocol http2");
|
|
1347
|
-
}
|
|
1348
|
-
installService({
|
|
1349
|
-
profile: name,
|
|
1350
|
-
user: os2.userInfo().username,
|
|
1351
|
-
home: os2.homedir(),
|
|
1352
|
-
nodePath: process.execPath,
|
|
1353
|
-
scriptPath: entryScript(),
|
|
1354
|
-
protocol
|
|
1355
|
-
});
|
|
1356
|
-
say.ok(`Service ${serviceName(name)} enabled \u2014 starts on boot.`);
|
|
1357
|
-
say.dim(` \u2192 check it: cloudtunnel service status ${name}`);
|
|
1358
|
-
}
|
|
1359
|
-
function disable(name) {
|
|
1360
|
-
getProfile(name);
|
|
1361
|
-
uninstallService(name);
|
|
1362
|
-
say.ok(`Service ${serviceName(name)} disabled and removed.`);
|
|
1363
|
-
}
|
|
1364
|
-
function status(name) {
|
|
1365
|
-
getProfile(name);
|
|
1366
|
-
say.info(`${serviceName(name)}: ${serviceState(name)}`);
|
|
1367
|
-
}
|
|
1368
|
-
function registerService(program) {
|
|
1369
|
-
const svc = program.command("service").description("Register a profile as a systemd service that starts on boot");
|
|
1370
|
-
svc.command("enable").argument("<profile>", "profile to register").option("--protocol <proto>", "edge transport for the service: auto | http2 | quic").description("Install + enable a boot service for the profile (needs sudo)").action((name, opts) => enable(name, opts));
|
|
1371
|
-
svc.command("disable").argument("<profile>", "profile to unregister").description("Stop, disable, and remove the profile's boot service (needs sudo)").action((name) => disable(name));
|
|
1372
|
-
svc.command("status").argument("<profile>", "profile to check").description("Show the systemd state of the profile's service").action((name) => status(name));
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
1533
|
// src/index.ts
|
|
1376
1534
|
var require2 = createRequire(import.meta.url);
|
|
1377
1535
|
var pkg = require2("../package.json");
|
|
1378
|
-
var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
1379
|
-
"login",
|
|
1380
|
-
"up",
|
|
1381
|
-
"ls",
|
|
1382
|
-
"ps",
|
|
1383
|
-
"down",
|
|
1384
|
-
"rm",
|
|
1385
|
-
"remove",
|
|
1386
|
-
"delete",
|
|
1387
|
-
"stop",
|
|
1388
|
-
"logs",
|
|
1389
|
-
"zones",
|
|
1390
|
-
"save",
|
|
1391
|
-
"run",
|
|
1392
|
-
"profiles",
|
|
1393
|
-
"service",
|
|
1394
|
-
"help"
|
|
1395
|
-
]);
|
|
1396
|
-
function applyBarePortAlias(argv) {
|
|
1397
|
-
const args = argv.slice(2);
|
|
1398
|
-
const first = args[0];
|
|
1399
|
-
if (first && /^\d{1,5}$/.test(first) && !KNOWN_COMMANDS.has(first)) {
|
|
1400
|
-
args.unshift("up");
|
|
1401
|
-
}
|
|
1402
|
-
return [argv[0], argv[1], ...args];
|
|
1403
|
-
}
|
|
1404
1536
|
function buildProgram() {
|
|
1405
1537
|
const program = new Command();
|
|
1406
|
-
program.name("cloudtunnel").description("
|
|
1538
|
+
program.name("cloudtunnel").description("Expose local ports at HTTPS subdomains on your own Cloudflare domains.").version(pkg.version, "-v, --version").showHelpAfterError();
|
|
1407
1539
|
program.addHelpText(
|
|
1408
1540
|
"before",
|
|
1409
1541
|
[
|
|
1410
1542
|
pc2.bold("Quickstart:"),
|
|
1411
|
-
` ${pc2.cyan("cloudtunnel login")}
|
|
1412
|
-
` ${pc2.cyan("cloudtunnel
|
|
1543
|
+
` ${pc2.cyan("cloudtunnel login")} once \u2014 paste a token (or set CLOUDFLARE_API_TOKEN)`,
|
|
1544
|
+
` ${pc2.cyan("cloudtunnel 8080")} your local :8080 goes live at an HTTPS URL`,
|
|
1545
|
+
` ${pc2.cyan("cloudtunnel api:8080")} api.<domain> \u2192 localhost:8080`,
|
|
1546
|
+
` ${pc2.cyan("cloudtunnel ls")} list tunnels ${pc2.dim("\xB7")} ${pc2.cyan("cloudtunnel delete <#>")} remove one`,
|
|
1413
1547
|
""
|
|
1414
1548
|
].join("\n")
|
|
1415
1549
|
);
|
|
1416
|
-
for (const register of [
|
|
1417
|
-
registerLogin,
|
|
1418
|
-
registerUp,
|
|
1419
|
-
registerLs,
|
|
1420
|
-
registerDown,
|
|
1421
|
-
registerZones,
|
|
1422
|
-
registerSave,
|
|
1423
|
-
registerRun,
|
|
1424
|
-
registerProfiles,
|
|
1425
|
-
registerService,
|
|
1426
|
-
registerLogs
|
|
1427
|
-
]) {
|
|
1550
|
+
for (const register of [registerLogin, registerUp, registerLs, registerDelete, registerLogs]) {
|
|
1428
1551
|
register(program);
|
|
1429
1552
|
}
|
|
1430
1553
|
return program;
|
|
1431
1554
|
}
|
|
1555
|
+
function shouldMigrate(argv) {
|
|
1556
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
1557
|
+
const rest = argv.slice(2);
|
|
1558
|
+
const infoFlag = /* @__PURE__ */ new Set(["-h", "--help", "-v", "--version", "help"]);
|
|
1559
|
+
return !rest.some((a) => infoFlag.has(a));
|
|
1560
|
+
}
|
|
1432
1561
|
async function main() {
|
|
1562
|
+
if (shouldMigrate(process.argv)) await migrateLegacyProfiles();
|
|
1433
1563
|
const program = buildProgram();
|
|
1434
1564
|
try {
|
|
1435
|
-
await program.parseAsync(
|
|
1565
|
+
await program.parseAsync(process.argv);
|
|
1436
1566
|
} catch (err) {
|
|
1437
1567
|
process.exitCode = reportError(err);
|
|
1438
1568
|
}
|