@iamken/cloudtunnel 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -11
- 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 +363 -114
- 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,7 +25,7 @@ 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";
|
|
@@ -32,7 +33,7 @@ import { createRequire } from "module";
|
|
|
32
33
|
import pc2 from "picocolors";
|
|
33
34
|
|
|
34
35
|
// src/config/legacy-migrate.ts
|
|
35
|
-
import { existsSync as
|
|
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,11 +79,13 @@ async function selectOne(message, items, label) {
|
|
|
78
79
|
return items[Number(value)];
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
// src/core/
|
|
82
|
-
import {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
import {
|
|
82
|
+
// src/core/service.ts
|
|
83
|
+
import { join as join5 } from "path";
|
|
84
|
+
|
|
85
|
+
// src/core/service-exec.ts
|
|
86
|
+
import { realpathSync } from "fs";
|
|
87
|
+
import os from "os";
|
|
88
|
+
import { join } from "path";
|
|
86
89
|
|
|
87
90
|
// src/core/ingress.ts
|
|
88
91
|
var HOSTNAME_RE = /^[a-zA-Z0-9.-]+$/;
|
|
@@ -157,29 +160,74 @@ function formatTunnelSpec(s) {
|
|
|
157
160
|
return `${s.subdomain}:${s.port}${s.host ? `@${s.host}` : ""}`;
|
|
158
161
|
}
|
|
159
162
|
|
|
160
|
-
// src/core/
|
|
161
|
-
|
|
162
|
-
|
|
163
|
+
// src/core/service-exec.ts
|
|
164
|
+
var fqdnFor = (subdomain, zone) => subdomain === "@" ? zone : `${subdomain}.${zone}`;
|
|
165
|
+
var serviceSlug = (fqdn) => fqdn.replace(/[^a-zA-Z0-9]+/g, "-");
|
|
166
|
+
function buildUpArgs(p) {
|
|
167
|
+
const spec = formatTunnelSpec({ subdomain: p.subdomain, port: p.port, host: p.host });
|
|
168
|
+
return [
|
|
169
|
+
"up",
|
|
170
|
+
spec,
|
|
171
|
+
"-d",
|
|
172
|
+
p.zone,
|
|
173
|
+
...p.proto === "https" ? ["--proto", "https"] : [],
|
|
174
|
+
...p.protocol ? ["--protocol", p.protocol] : [],
|
|
175
|
+
"-f",
|
|
176
|
+
"-y"
|
|
177
|
+
];
|
|
178
|
+
}
|
|
179
|
+
function entryScript() {
|
|
180
|
+
const p = process.argv[1];
|
|
181
|
+
if (!p) throw new CliError("Cannot resolve the cloudtunnel executable path.");
|
|
182
|
+
return realpathSync(p);
|
|
163
183
|
}
|
|
164
|
-
function
|
|
165
|
-
|
|
184
|
+
function describeService(p) {
|
|
185
|
+
const fqdn = fqdnFor(p.subdomain, p.zone);
|
|
186
|
+
const slug = serviceSlug(fqdn);
|
|
187
|
+
return {
|
|
188
|
+
fqdn,
|
|
189
|
+
slug,
|
|
190
|
+
argv: buildUpArgs(p),
|
|
191
|
+
nodePath: process.execPath,
|
|
192
|
+
scriptPath: entryScript(),
|
|
193
|
+
user: os.userInfo().username,
|
|
194
|
+
home: os.homedir(),
|
|
195
|
+
logFile: join(logDir, `${slug}.service.log`)
|
|
196
|
+
};
|
|
166
197
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
198
|
+
|
|
199
|
+
// src/core/service-systemd.ts
|
|
200
|
+
var service_systemd_exports = {};
|
|
201
|
+
__export(service_systemd_exports, {
|
|
202
|
+
assertSupported: () => assertSupported,
|
|
203
|
+
buildUnit: () => buildUnit,
|
|
204
|
+
install: () => install,
|
|
205
|
+
label: () => label,
|
|
206
|
+
legacyUnitExists: () => legacyUnitExists,
|
|
207
|
+
removeLegacyUnit: () => removeLegacyUnit,
|
|
208
|
+
state: () => state,
|
|
209
|
+
uninstall: () => uninstall
|
|
210
|
+
});
|
|
211
|
+
import { execFileSync } from "child_process";
|
|
212
|
+
import { existsSync, writeFileSync } from "fs";
|
|
213
|
+
import { tmpdir } from "os";
|
|
214
|
+
import { dirname, join as join2 } from "path";
|
|
215
|
+
var label = (fqdn) => `cloudtunnel-${serviceSlug(fqdn)}.service`;
|
|
216
|
+
var unitPath = (fqdn) => `/etc/systemd/system/${label(fqdn)}`;
|
|
217
|
+
function buildUnit(d) {
|
|
218
|
+
const nodeBin = dirname(d.nodePath);
|
|
171
219
|
return [
|
|
172
220
|
"[Unit]",
|
|
173
|
-
`Description=cloudtunnel ${
|
|
221
|
+
`Description=cloudtunnel ${d.fqdn} (Cloudflare Tunnel)`,
|
|
174
222
|
"After=network-online.target",
|
|
175
223
|
"Wants=network-online.target",
|
|
176
224
|
"",
|
|
177
225
|
"[Service]",
|
|
178
226
|
"Type=simple",
|
|
179
|
-
`User=${
|
|
180
|
-
`Environment=HOME=${
|
|
227
|
+
`User=${d.user}`,
|
|
228
|
+
`Environment=HOME=${d.home}`,
|
|
181
229
|
`Environment=PATH=${nodeBin}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`,
|
|
182
|
-
`ExecStart=${
|
|
230
|
+
`ExecStart=${d.nodePath} ${d.scriptPath} ${d.argv.join(" ")}`,
|
|
183
231
|
"Restart=on-failure",
|
|
184
232
|
"RestartSec=5",
|
|
185
233
|
"",
|
|
@@ -188,18 +236,6 @@ function buildUnit(p) {
|
|
|
188
236
|
""
|
|
189
237
|
].join("\n");
|
|
190
238
|
}
|
|
191
|
-
function assertSystemd() {
|
|
192
|
-
if (process.platform !== "linux") {
|
|
193
|
-
throw new CliError("Service registration is Linux/systemd only.", {
|
|
194
|
-
hint: "on macOS/Windows run `cloudtunnel up <spec> --detach` at login instead"
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
try {
|
|
198
|
-
execFileSync("systemctl", ["--version"], { stdio: "ignore" });
|
|
199
|
-
} catch {
|
|
200
|
-
throw new CliError("systemd (systemctl) was not found on this host.");
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
239
|
function privileged(args) {
|
|
204
240
|
const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
|
|
205
241
|
const argv = isRoot ? args : ["sudo", ...args];
|
|
@@ -213,40 +249,37 @@ function query(args) {
|
|
|
213
249
|
return out ? out.toString().trim() : "";
|
|
214
250
|
}
|
|
215
251
|
}
|
|
216
|
-
function
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
252
|
+
function assertSupported() {
|
|
253
|
+
try {
|
|
254
|
+
execFileSync("systemctl", ["--version"], { stdio: "ignore" });
|
|
255
|
+
} catch {
|
|
256
|
+
throw new CliError("systemd (systemctl) was not found on this host.");
|
|
257
|
+
}
|
|
220
258
|
}
|
|
221
|
-
function
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
spec: formatTunnelSpec({ subdomain: params.subdomain, port: params.port, host: params.host }),
|
|
227
|
-
zone: params.zone,
|
|
228
|
-
proto: params.proto,
|
|
229
|
-
user: os.userInfo().username,
|
|
230
|
-
home: os.homedir(),
|
|
231
|
-
nodePath: process.execPath,
|
|
232
|
-
scriptPath: entryScript(),
|
|
233
|
-
protocol: params.protocol
|
|
234
|
-
});
|
|
235
|
-
const tmp = join(tmpdir(), serviceName(fqdn));
|
|
236
|
-
writeFileSync(tmp, unit, { mode: 420 });
|
|
237
|
-
privileged(["install", "-m", "0644", tmp, unitPath(fqdn)]);
|
|
259
|
+
function install(d) {
|
|
260
|
+
assertSupported();
|
|
261
|
+
const tmp = join2(tmpdir(), label(d.fqdn));
|
|
262
|
+
writeFileSync(tmp, buildUnit(d), { mode: 420 });
|
|
263
|
+
privileged(["install", "-m", "0644", tmp, unitPath(d.fqdn)]);
|
|
238
264
|
privileged(["systemctl", "daemon-reload"]);
|
|
239
|
-
privileged(["systemctl", "enable", "--now",
|
|
265
|
+
privileged(["systemctl", "enable", "--now", label(d.fqdn)]);
|
|
240
266
|
}
|
|
241
|
-
function
|
|
242
|
-
assertSystemd();
|
|
267
|
+
function uninstall(fqdn) {
|
|
243
268
|
try {
|
|
244
|
-
privileged(["systemctl", "disable", "--now",
|
|
269
|
+
privileged(["systemctl", "disable", "--now", label(fqdn)]);
|
|
245
270
|
} catch {
|
|
246
271
|
}
|
|
247
272
|
privileged(["rm", "-f", unitPath(fqdn)]);
|
|
248
273
|
privileged(["systemctl", "daemon-reload"]);
|
|
249
274
|
}
|
|
275
|
+
function state(fqdn) {
|
|
276
|
+
const name = label(fqdn);
|
|
277
|
+
if (query(["is-active", name]) === "active") return "active";
|
|
278
|
+
const enabled = query(["is-enabled", name]);
|
|
279
|
+
if (enabled === "enabled" || enabled === "enabled-runtime") return "enabled";
|
|
280
|
+
if (enabled === "disabled" || enabled === "static") return "disabled";
|
|
281
|
+
return "none";
|
|
282
|
+
}
|
|
250
283
|
function legacyUnitExists(profile) {
|
|
251
284
|
return existsSync(`/etc/systemd/system/cloudtunnel-${profile}.service`);
|
|
252
285
|
}
|
|
@@ -259,27 +292,219 @@ function removeLegacyUnit(profile) {
|
|
|
259
292
|
privileged(["rm", "-f", `/etc/systemd/system/${name}`]);
|
|
260
293
|
privileged(["systemctl", "daemon-reload"]);
|
|
261
294
|
}
|
|
295
|
+
|
|
296
|
+
// src/core/service-launchd.ts
|
|
297
|
+
var service_launchd_exports = {};
|
|
298
|
+
__export(service_launchd_exports, {
|
|
299
|
+
assertSupported: () => assertSupported2,
|
|
300
|
+
buildPlist: () => buildPlist,
|
|
301
|
+
install: () => install2,
|
|
302
|
+
label: () => label2,
|
|
303
|
+
state: () => state2,
|
|
304
|
+
uninstall: () => uninstall2
|
|
305
|
+
});
|
|
306
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
307
|
+
import { existsSync as existsSync2, mkdirSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
308
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
309
|
+
import os2 from "os";
|
|
310
|
+
var label2 = (fqdn) => `com.cloudtunnel.${serviceSlug(fqdn)}`;
|
|
311
|
+
var agentsDir = () => join3(os2.homedir(), "Library", "LaunchAgents");
|
|
312
|
+
var plistPath = (fqdn) => join3(agentsDir(), `${label2(fqdn)}.plist`);
|
|
313
|
+
var xml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
314
|
+
function buildPlist(d) {
|
|
315
|
+
const args = [d.nodePath, d.scriptPath, ...d.argv].map((a) => ` <string>${xml(a)}</string>`).join("\n");
|
|
316
|
+
const nodeBin = dirname2(d.nodePath);
|
|
317
|
+
const path = `${nodeBin}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`;
|
|
318
|
+
return [
|
|
319
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
320
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
321
|
+
'<plist version="1.0">',
|
|
322
|
+
"<dict>",
|
|
323
|
+
` <key>Label</key><string>${xml(label2(d.fqdn))}</string>`,
|
|
324
|
+
" <key>ProgramArguments</key>",
|
|
325
|
+
" <array>",
|
|
326
|
+
args,
|
|
327
|
+
" </array>",
|
|
328
|
+
" <key>RunAtLoad</key><true/>",
|
|
329
|
+
" <key>KeepAlive</key><true/>",
|
|
330
|
+
" <key>EnvironmentVariables</key>",
|
|
331
|
+
" <dict>",
|
|
332
|
+
` <key>PATH</key><string>${xml(path)}</string>`,
|
|
333
|
+
` <key>HOME</key><string>${xml(d.home)}</string>`,
|
|
334
|
+
" </dict>",
|
|
335
|
+
` <key>StandardOutPath</key><string>${xml(d.logFile)}</string>`,
|
|
336
|
+
` <key>StandardErrorPath</key><string>${xml(d.logFile)}</string>`,
|
|
337
|
+
"</dict>",
|
|
338
|
+
"</plist>",
|
|
339
|
+
""
|
|
340
|
+
].join("\n");
|
|
341
|
+
}
|
|
342
|
+
function launchctl(args) {
|
|
343
|
+
try {
|
|
344
|
+
return execFileSync2("launchctl", args, { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" });
|
|
345
|
+
} catch (err) {
|
|
346
|
+
const out = err.stdout;
|
|
347
|
+
return out ? out.toString() : "";
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function assertSupported2() {
|
|
351
|
+
}
|
|
352
|
+
function install2(d) {
|
|
353
|
+
ensureDirs();
|
|
354
|
+
mkdirSync(agentsDir(), { recursive: true });
|
|
355
|
+
const plist = plistPath(d.fqdn);
|
|
356
|
+
writeFileSync2(plist, buildPlist(d), { mode: 420 });
|
|
357
|
+
launchctl(["unload", "-w", plist]);
|
|
358
|
+
execFileSync2("launchctl", ["load", "-w", plist], { stdio: "inherit" });
|
|
359
|
+
}
|
|
360
|
+
function uninstall2(fqdn) {
|
|
361
|
+
const plist = plistPath(fqdn);
|
|
362
|
+
launchctl(["unload", "-w", plist]);
|
|
363
|
+
rmSync(plist, { force: true });
|
|
364
|
+
}
|
|
365
|
+
function state2(fqdn) {
|
|
366
|
+
const info = launchctl(["list", label2(fqdn)]);
|
|
367
|
+
if (/"PID"\s*=/.test(info)) return "active";
|
|
368
|
+
return existsSync2(plistPath(fqdn)) ? "enabled" : "none";
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// src/core/service-windows.ts
|
|
372
|
+
var service_windows_exports = {};
|
|
373
|
+
__export(service_windows_exports, {
|
|
374
|
+
assertSupported: () => assertSupported3,
|
|
375
|
+
buildTaskXml: () => buildTaskXml,
|
|
376
|
+
install: () => install3,
|
|
377
|
+
label: () => label3,
|
|
378
|
+
state: () => state3,
|
|
379
|
+
uninstall: () => uninstall3
|
|
380
|
+
});
|
|
381
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
382
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
383
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
384
|
+
import { join as join4 } from "path";
|
|
385
|
+
var label3 = (fqdn) => `cloudtunnel\\${serviceSlug(fqdn)}`;
|
|
386
|
+
var xml2 = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
387
|
+
function buildTaskXml(d) {
|
|
388
|
+
const args = `"${d.scriptPath}" ${d.argv.join(" ")}`;
|
|
389
|
+
return [
|
|
390
|
+
'<?xml version="1.0" encoding="UTF-16"?>',
|
|
391
|
+
'<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">',
|
|
392
|
+
` <RegistrationInfo><Description>cloudtunnel ${xml2(d.fqdn)} (Cloudflare Tunnel)</Description></RegistrationInfo>`,
|
|
393
|
+
` <Triggers><LogonTrigger><Enabled>true</Enabled><UserId>${xml2(d.user)}</UserId></LogonTrigger></Triggers>`,
|
|
394
|
+
` <Principals><Principal id="Author"><UserId>${xml2(d.user)}</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>`,
|
|
395
|
+
" <Settings>",
|
|
396
|
+
" <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>",
|
|
397
|
+
" <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>",
|
|
398
|
+
" <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>",
|
|
399
|
+
" <StartWhenAvailable>true</StartWhenAvailable>",
|
|
400
|
+
" <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>",
|
|
401
|
+
" <RestartOnFailure><Interval>PT1M</Interval><Count>3</Count></RestartOnFailure>",
|
|
402
|
+
" <Enabled>true</Enabled>",
|
|
403
|
+
" </Settings>",
|
|
404
|
+
' <Actions Context="Author">',
|
|
405
|
+
` <Exec><Command>${xml2(d.nodePath)}</Command><Arguments>${xml2(args)}</Arguments></Exec>`,
|
|
406
|
+
" </Actions>",
|
|
407
|
+
"</Task>",
|
|
408
|
+
""
|
|
409
|
+
].join("\r\n");
|
|
410
|
+
}
|
|
411
|
+
function schtasks(args) {
|
|
412
|
+
try {
|
|
413
|
+
return execFileSync3("schtasks", args, { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" });
|
|
414
|
+
} catch (err) {
|
|
415
|
+
const out = err.stdout;
|
|
416
|
+
return out ? out.toString() : "";
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function assertSupported3() {
|
|
420
|
+
}
|
|
421
|
+
function install3(d) {
|
|
422
|
+
const file = join4(tmpdir2(), `${d.slug}.task.xml`);
|
|
423
|
+
writeFileSync3(file, "\uFEFF" + buildTaskXml(d), { encoding: "utf16le" });
|
|
424
|
+
execFileSync3("schtasks", ["/Create", "/TN", label3(d.fqdn), "/XML", file, "/F"], { stdio: "inherit" });
|
|
425
|
+
schtasks(["/Run", "/TN", label3(d.fqdn)]);
|
|
426
|
+
}
|
|
427
|
+
function uninstall3(fqdn) {
|
|
428
|
+
schtasks(["/Delete", "/TN", label3(fqdn), "/F"]);
|
|
429
|
+
}
|
|
430
|
+
function state3(fqdn) {
|
|
431
|
+
const out = schtasks(["/Query", "/TN", label3(fqdn), "/FO", "LIST"]);
|
|
432
|
+
if (!out) return "none";
|
|
433
|
+
if (/\bRunning\b/.test(out)) return "active";
|
|
434
|
+
if (/\bDisabled\b/.test(out)) return "disabled";
|
|
435
|
+
if (/\bReady\b/.test(out)) return "enabled";
|
|
436
|
+
return "enabled";
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// src/core/service.ts
|
|
440
|
+
function pick() {
|
|
441
|
+
switch (process.platform) {
|
|
442
|
+
case "linux":
|
|
443
|
+
return service_systemd_exports;
|
|
444
|
+
case "darwin":
|
|
445
|
+
return service_launchd_exports;
|
|
446
|
+
case "win32":
|
|
447
|
+
return service_windows_exports;
|
|
448
|
+
default:
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function required() {
|
|
453
|
+
const b = pick();
|
|
454
|
+
if (!b) {
|
|
455
|
+
throw new CliError(`Boot services aren't supported on ${process.platform}.`, {
|
|
456
|
+
hint: "run `cloudtunnel up <spec> --detach` and use your OS's own autostart"
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
return b;
|
|
460
|
+
}
|
|
461
|
+
function assertServiceSupported() {
|
|
462
|
+
required().assertSupported();
|
|
463
|
+
}
|
|
464
|
+
function serviceName(fqdn) {
|
|
465
|
+
return pick()?.label(fqdn) ?? `cloudtunnel-${fqdn}`;
|
|
466
|
+
}
|
|
467
|
+
function installServiceForSpec(params) {
|
|
468
|
+
const b = required();
|
|
469
|
+
b.assertSupported();
|
|
470
|
+
b.install(describeService(params));
|
|
471
|
+
}
|
|
472
|
+
function uninstallService(fqdn) {
|
|
473
|
+
pick()?.uninstall(fqdn);
|
|
474
|
+
}
|
|
262
475
|
function serviceState(fqdn) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
476
|
+
return pick()?.state(fqdn) ?? "none";
|
|
477
|
+
}
|
|
478
|
+
function serviceLogsHint(fqdn) {
|
|
479
|
+
switch (process.platform) {
|
|
480
|
+
case "linux":
|
|
481
|
+
return `journalctl -u ${serviceName(fqdn)} -n 50 --no-pager`;
|
|
482
|
+
case "darwin":
|
|
483
|
+
return `tail ${join5(logDir, `${serviceSlug(fqdn)}.service.log`)}`;
|
|
484
|
+
case "win32":
|
|
485
|
+
return `schtasks /Query /TN "${serviceName(fqdn)}" /V /FO LIST`;
|
|
486
|
+
default:
|
|
487
|
+
return "check your OS service logs";
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function legacyUnitExists2(profile) {
|
|
491
|
+
return process.platform === "linux" ? legacyUnitExists(profile) : false;
|
|
492
|
+
}
|
|
493
|
+
function removeLegacyUnit2(profile) {
|
|
494
|
+
if (process.platform === "linux") removeLegacyUnit(profile);
|
|
270
495
|
}
|
|
271
496
|
|
|
272
497
|
// src/config/legacy-migrate.ts
|
|
273
498
|
var skipMarker = `${profilesFile}.migrate-skip`;
|
|
274
499
|
async function migrateLegacyProfiles() {
|
|
275
|
-
if (!
|
|
500
|
+
if (!existsSync3(profilesFile) || existsSync3(skipMarker)) return;
|
|
276
501
|
let profiles;
|
|
277
502
|
try {
|
|
278
503
|
profiles = JSON.parse(readFileSync(profilesFile, "utf8"));
|
|
279
504
|
} catch {
|
|
280
505
|
return;
|
|
281
506
|
}
|
|
282
|
-
const legacy = Object.entries(profiles).filter(([name]) =>
|
|
507
|
+
const legacy = Object.entries(profiles).filter(([name]) => legacyUnitExists2(name));
|
|
283
508
|
if (legacy.length === 0) {
|
|
284
509
|
try {
|
|
285
510
|
renameSync(profilesFile, `${profilesFile}.migrated`);
|
|
@@ -289,7 +514,7 @@ async function migrateLegacyProfiles() {
|
|
|
289
514
|
}
|
|
290
515
|
const ok = await confirm(`Found ${legacy.length} boot service(s) from an older cloudtunnel. Migrate them now? (needs sudo)`);
|
|
291
516
|
if (!ok) {
|
|
292
|
-
|
|
517
|
+
writeFileSync4(skipMarker, "");
|
|
293
518
|
say.dim(` Skipped. Delete ${skipMarker} to be asked again.`);
|
|
294
519
|
return;
|
|
295
520
|
}
|
|
@@ -309,12 +534,12 @@ async function migrateLegacyProfiles() {
|
|
|
309
534
|
});
|
|
310
535
|
migrated++;
|
|
311
536
|
}
|
|
312
|
-
|
|
537
|
+
removeLegacyUnit2(name);
|
|
313
538
|
}
|
|
314
539
|
renameSync(profilesFile, `${profilesFile}.migrated`);
|
|
315
540
|
say.ok(`Migrated ${migrated} boot service(s). See them with: cloudtunnel ls`);
|
|
316
541
|
} catch (err) {
|
|
317
|
-
|
|
542
|
+
writeFileSync4(skipMarker, "");
|
|
318
543
|
say.warn(`Migration incomplete: ${err.message}. Won't retry automatically (delete ${skipMarker} to retry).`);
|
|
319
544
|
}
|
|
320
545
|
}
|
|
@@ -478,10 +703,10 @@ async function ensureAuth() {
|
|
|
478
703
|
}
|
|
479
704
|
|
|
480
705
|
// src/connector/binary.ts
|
|
481
|
-
import { execFileSync as
|
|
706
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
482
707
|
import { createHash } from "crypto";
|
|
483
|
-
import { chmodSync, existsSync as
|
|
484
|
-
import { join as
|
|
708
|
+
import { chmodSync, existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
709
|
+
import { join as join6 } from "path";
|
|
485
710
|
var PINNED_VERSION = "2025.1.0";
|
|
486
711
|
var RELEASE_BASE = `https://github.com/cloudflare/cloudflared/releases/download/${PINNED_VERSION}`;
|
|
487
712
|
var ASSETS = {
|
|
@@ -493,14 +718,14 @@ var ASSETS = {
|
|
|
493
718
|
};
|
|
494
719
|
function binaryWorks(bin) {
|
|
495
720
|
try {
|
|
496
|
-
|
|
721
|
+
execFileSync4(bin, ["--version"], { stdio: "ignore" });
|
|
497
722
|
return true;
|
|
498
723
|
} catch {
|
|
499
724
|
return false;
|
|
500
725
|
}
|
|
501
726
|
}
|
|
502
727
|
function cachedPath() {
|
|
503
|
-
return
|
|
728
|
+
return join6(binDir, process.platform === "win32" ? "cloudflared.exe" : "cloudflared");
|
|
504
729
|
}
|
|
505
730
|
function isMusl() {
|
|
506
731
|
try {
|
|
@@ -512,7 +737,7 @@ function isMusl() {
|
|
|
512
737
|
async function ensureCloudflared() {
|
|
513
738
|
if (binaryWorks("cloudflared")) return "cloudflared";
|
|
514
739
|
const cached = cachedPath();
|
|
515
|
-
if (
|
|
740
|
+
if (existsSync4(cached) && binaryWorks(cached)) return cached;
|
|
516
741
|
return downloadCloudflared(cached);
|
|
517
742
|
}
|
|
518
743
|
async function downloadCloudflared(dest) {
|
|
@@ -540,7 +765,7 @@ async function downloadCloudflared(dest) {
|
|
|
540
765
|
}
|
|
541
766
|
ensureDirs();
|
|
542
767
|
const binary = asset.archive ? extractTgz(bytes) : bytes;
|
|
543
|
-
|
|
768
|
+
writeFileSync5(dest, binary, { mode: 493 });
|
|
544
769
|
chmodSync(dest, 493);
|
|
545
770
|
if (!binaryWorks(dest)) throw new CliError("Downloaded cloudflared is not runnable.");
|
|
546
771
|
return dest;
|
|
@@ -552,17 +777,17 @@ function extractTgz(_bytes) {
|
|
|
552
777
|
}
|
|
553
778
|
|
|
554
779
|
// src/core/up-runner.ts
|
|
555
|
-
import { join as
|
|
780
|
+
import { join as join7 } from "path";
|
|
556
781
|
import * as clack2 from "@clack/prompts";
|
|
557
782
|
|
|
558
783
|
// src/connector/process.ts
|
|
559
|
-
import { execFileSync as
|
|
784
|
+
import { execFileSync as execFileSync5, spawn as spawn2 } from "child_process";
|
|
560
785
|
import { openSync } from "fs";
|
|
561
786
|
|
|
562
787
|
// src/connector/registry.ts
|
|
563
|
-
import { existsSync as
|
|
788
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
564
789
|
import { readFile } from "fs/promises";
|
|
565
|
-
import
|
|
790
|
+
import os3 from "os";
|
|
566
791
|
import lockfile from "proper-lockfile";
|
|
567
792
|
function entryFqdn(e) {
|
|
568
793
|
return e.subdomain === "@" ? e.zone : `${e.subdomain}.${e.zone}`;
|
|
@@ -571,8 +796,8 @@ function currentBootId() {
|
|
|
571
796
|
try {
|
|
572
797
|
return readFileSync3("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
573
798
|
} catch {
|
|
574
|
-
const bootMinute = Math.floor((Date.now() -
|
|
575
|
-
return `boot-${bootMinute}-${
|
|
799
|
+
const bootMinute = Math.floor((Date.now() - os3.uptime() * 1e3) / 6e4);
|
|
800
|
+
return `boot-${bootMinute}-${os3.hostname()}`;
|
|
576
801
|
}
|
|
577
802
|
}
|
|
578
803
|
function readRegistry() {
|
|
@@ -585,12 +810,12 @@ function readRegistry() {
|
|
|
585
810
|
function writeRegistry(reg) {
|
|
586
811
|
ensureDirs();
|
|
587
812
|
const tmp = `${registryFile}.tmp`;
|
|
588
|
-
|
|
813
|
+
writeFileSync6(tmp, JSON.stringify(reg, null, 2), { mode: 384 });
|
|
589
814
|
renameSync2(tmp, registryFile);
|
|
590
815
|
}
|
|
591
816
|
async function mutateRegistry(fn) {
|
|
592
817
|
ensureDirs();
|
|
593
|
-
if (!
|
|
818
|
+
if (!existsSync5(registryFile)) writeFileSync6(registryFile, "{}", { mode: 384 });
|
|
594
819
|
const release = await lockfile.lock(registryFile, { retries: { retries: 10, minTimeout: 50 } });
|
|
595
820
|
try {
|
|
596
821
|
const reg = readRegistry();
|
|
@@ -698,7 +923,7 @@ async function stopConnector(entry) {
|
|
|
698
923
|
const pid = entry.pid;
|
|
699
924
|
if (process.platform === "win32") {
|
|
700
925
|
try {
|
|
701
|
-
|
|
926
|
+
execFileSync5("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
|
|
702
927
|
} catch {
|
|
703
928
|
return false;
|
|
704
929
|
}
|
|
@@ -829,10 +1054,10 @@ var NOUNS = [
|
|
|
829
1054
|
"tiger",
|
|
830
1055
|
"walnut"
|
|
831
1056
|
];
|
|
832
|
-
var
|
|
1057
|
+
var pick2 = (arr) => arr[randomInt(arr.length)];
|
|
833
1058
|
function randomSlug() {
|
|
834
1059
|
const suffix = randomInt(65536).toString(16).padStart(4, "0");
|
|
835
|
-
return `${
|
|
1060
|
+
return `${pick2(ADJECTIVES)}-${pick2(NOUNS)}-${suffix}`;
|
|
836
1061
|
}
|
|
837
1062
|
function resolveHostSpec(opts, defaultZone) {
|
|
838
1063
|
if (opts.hostname) {
|
|
@@ -889,8 +1114,8 @@ async function createTunnelSubdomain(cf, opts) {
|
|
|
889
1114
|
let dnsRecordId;
|
|
890
1115
|
try {
|
|
891
1116
|
const suffix = randomInt2(65536).toString(16).padStart(4, "0");
|
|
892
|
-
const
|
|
893
|
-
const tunnel = await createTunnel(cf, `${MANAGED_TUNNEL_PREFIX}${
|
|
1117
|
+
const label4 = host.subdomain === "@" ? "root" : host.subdomain;
|
|
1118
|
+
const tunnel = await createTunnel(cf, `${MANAGED_TUNNEL_PREFIX}${label4}-${suffix}`);
|
|
894
1119
|
tunnelId = tunnel.id;
|
|
895
1120
|
const token = await getTunnelToken(cf, tunnelId);
|
|
896
1121
|
await putIngress(cf, tunnelId, buildIngress({ hostname: host.hostname, port: opts.port, proto: opts.proto, host: opts.host }));
|
|
@@ -1037,8 +1262,8 @@ async function listAll(cf, opts = {}) {
|
|
|
1037
1262
|
};
|
|
1038
1263
|
});
|
|
1039
1264
|
if (opts.all) {
|
|
1040
|
-
const { listCargoCnames } = await import("./dns-
|
|
1041
|
-
const { listZones: listZones3 } = await import("./zones-
|
|
1265
|
+
const { listCargoCnames } = await import("./dns-5OXFAQ4D.js");
|
|
1266
|
+
const { listZones: listZones3 } = await import("./zones-QYV3DSEY.js");
|
|
1042
1267
|
const tracked = new Set(entries.map(entryFqdn));
|
|
1043
1268
|
for (const zone of await listZones3(cf.token)) {
|
|
1044
1269
|
for (const rec of await listCargoCnames(cf.token, zone.id)) {
|
|
@@ -1053,7 +1278,7 @@ async function listAll(cf, opts = {}) {
|
|
|
1053
1278
|
|
|
1054
1279
|
// src/core/up-runner.ts
|
|
1055
1280
|
function logFileFor(subdomain) {
|
|
1056
|
-
return
|
|
1281
|
+
return join7(logDir, `${subdomain === "@" ? "root" : subdomain}.log`);
|
|
1057
1282
|
}
|
|
1058
1283
|
async function startTunnels(cf, bin, items, opts = {}) {
|
|
1059
1284
|
const started = [];
|
|
@@ -1196,30 +1421,56 @@ async function runUp(specArgs, opts) {
|
|
|
1196
1421
|
});
|
|
1197
1422
|
}
|
|
1198
1423
|
if (opts.service) {
|
|
1199
|
-
registerServices(items, domain, opts.proto, protocol);
|
|
1424
|
+
await registerServices(cf, items, domain, opts.proto, protocol);
|
|
1200
1425
|
return;
|
|
1201
1426
|
}
|
|
1202
1427
|
await startTunnels(cf, bin, items, { detach: opts.detach, protocol });
|
|
1203
1428
|
}
|
|
1204
|
-
|
|
1205
|
-
|
|
1429
|
+
var SERVICE_UP_TIMEOUT_MS = 2e4;
|
|
1430
|
+
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1431
|
+
async function registerServices(cf, items, domain, proto, protocol) {
|
|
1432
|
+
assertServiceSupported();
|
|
1206
1433
|
if (!protocol) {
|
|
1207
1434
|
say.warn("No edge protocol set \u2014 cloudflared will pick QUIC, which some networks drop.");
|
|
1208
1435
|
say.dim(" \u2192 add --protocol http2 for UDP-hostile networks");
|
|
1209
1436
|
}
|
|
1210
|
-
const
|
|
1437
|
+
const fqdns = [];
|
|
1211
1438
|
for (const item of items) {
|
|
1212
1439
|
const subdomain = item.name;
|
|
1213
1440
|
const fqdn = subdomain === "@" ? domain : `${subdomain}.${domain}`;
|
|
1214
1441
|
installServiceForSpec({ subdomain, port: item.port, host: item.host, zone: domain, proto, protocol });
|
|
1215
|
-
|
|
1442
|
+
fqdns.push(fqdn);
|
|
1443
|
+
}
|
|
1444
|
+
say.ok(`Registered ${fqdns.length} boot service(s) \u2014 waiting for them to come up\u2026`);
|
|
1445
|
+
const notUp = await waitServicesUp(fqdns, SERVICE_UP_TIMEOUT_MS);
|
|
1446
|
+
const rows = await listAll(cf);
|
|
1447
|
+
if (rows.length) {
|
|
1448
|
+
printTable(
|
|
1449
|
+
["#", "URL", "TARGET", "STATE", "SERVICE", "PID"],
|
|
1450
|
+
rows.map((r) => [r.num, r.url, r.target, r.state, r.service, r.pid])
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
if (notUp.length) {
|
|
1454
|
+
say.warn(`${notUp.length} service(s) didn't report up within ${SERVICE_UP_TIMEOUT_MS / 1e3}s:`);
|
|
1455
|
+
for (const fqdn of notUp) say.dim(` ${fqdn} \u2192 ${serviceLogsHint(fqdn)}`);
|
|
1216
1456
|
}
|
|
1217
|
-
say.
|
|
1218
|
-
|
|
1219
|
-
|
|
1457
|
+
say.dim(" \u2192 manage: cloudtunnel ls \xB7 remove: cloudtunnel delete <#>");
|
|
1458
|
+
}
|
|
1459
|
+
async function waitServicesUp(fqdns, timeoutMs) {
|
|
1460
|
+
const deadline = Date.now() + timeoutMs;
|
|
1461
|
+
const pending = new Set(fqdns);
|
|
1462
|
+
while (pending.size > 0) {
|
|
1463
|
+
for (const fqdn of [...pending]) {
|
|
1464
|
+
const entry = getEntry(fqdn);
|
|
1465
|
+
if (entry?.state === "running" && entry.pid) pending.delete(fqdn);
|
|
1466
|
+
}
|
|
1467
|
+
if (pending.size === 0 || Date.now() >= deadline) break;
|
|
1468
|
+
await delay(500);
|
|
1469
|
+
}
|
|
1470
|
+
return [...pending];
|
|
1220
1471
|
}
|
|
1221
1472
|
function registerUp(program) {
|
|
1222
|
-
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
|
|
1473
|
+
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));
|
|
1223
1474
|
}
|
|
1224
1475
|
|
|
1225
1476
|
// src/commands/ls.ts
|
|
@@ -1241,14 +1492,12 @@ function registerLs(program) {
|
|
|
1241
1492
|
|
|
1242
1493
|
// src/commands/delete.ts
|
|
1243
1494
|
async function deleteOne(cf, fqdn, opts) {
|
|
1495
|
+
const hasService = serviceState(fqdn) !== "none";
|
|
1496
|
+
if (hasService && !opts.dryRun) uninstallService(fqdn);
|
|
1244
1497
|
await removeTunnelSubdomain(cf, fqdn, { force: opts.force, dryRun: opts.dryRun });
|
|
1245
|
-
if (
|
|
1246
|
-
if (opts.dryRun) {
|
|
1247
|
-
|
|
1248
|
-
return;
|
|
1249
|
-
}
|
|
1250
|
-
uninstallService(fqdn);
|
|
1251
|
-
say.ok(`Removed boot service ${serviceName(fqdn)}`);
|
|
1498
|
+
if (!hasService) return;
|
|
1499
|
+
if (opts.dryRun) say.info(`Would also remove boot service ${serviceName(fqdn)}`);
|
|
1500
|
+
else say.ok(`Removed boot service ${serviceName(fqdn)}`);
|
|
1252
1501
|
}
|
|
1253
1502
|
function registerDelete(program) {
|
|
1254
1503
|
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) => {
|
|
@@ -1279,7 +1528,7 @@ function registerDelete(program) {
|
|
|
1279
1528
|
}
|
|
1280
1529
|
|
|
1281
1530
|
// src/commands/logs.ts
|
|
1282
|
-
import { closeSync, existsSync as
|
|
1531
|
+
import { closeSync, existsSync as existsSync6, openSync as openSync2, readFileSync as readFileSync4, readSync, statSync, watch } from "fs";
|
|
1283
1532
|
function printTail(file, n) {
|
|
1284
1533
|
const lines = readFileSync4(file, "utf8").split("\n");
|
|
1285
1534
|
const tail = lines.slice(-n).join("\n");
|
|
@@ -1313,7 +1562,7 @@ function follow(file, fromPos) {
|
|
|
1313
1562
|
function registerLogs(program) {
|
|
1314
1563
|
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) => {
|
|
1315
1564
|
const { fqdn, entry } = resolveTarget(name);
|
|
1316
|
-
if (!entry?.logFile || !
|
|
1565
|
+
if (!entry?.logFile || !existsSync6(entry.logFile)) {
|
|
1317
1566
|
throw new CliError(`No logs for ${fqdn} yet.`, { hint: "start it with `cloudtunnel up` or `cloudtunnel run`" });
|
|
1318
1567
|
}
|
|
1319
1568
|
const n = Math.max(1, Number(opts.lines) || 50);
|