@iamken/cloudtunnel 0.9.0 → 0.10.1
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 +85 -0
- package/dist/{chunk-LG5ST75Z.js → chunk-IDTF7SUY.js} +2 -2
- package/dist/{chunk-VR5RHOTJ.js → chunk-NB3OEY2P.js} +2 -2
- package/dist/{chunk-3KBLIPKG.js → chunk-ZIUYW2HQ.js} +127 -6
- package/dist/chunk-ZIUYW2HQ.js.map +1 -0
- package/dist/{dns-C7WXQOED.js → dns-2F5SYQNX.js} +3 -3
- package/dist/index.js +282 -70
- package/dist/index.js.map +1 -1
- package/dist/zones-PCUEPXHD.js +11 -0
- package/package.json +1 -1
- package/dist/chunk-3KBLIPKG.js.map +0 -1
- package/dist/zones-KPRAW4JL.js +0 -11
- /package/dist/{chunk-LG5ST75Z.js.map → chunk-IDTF7SUY.js.map} +0 -0
- /package/dist/{chunk-VR5RHOTJ.js.map → chunk-NB3OEY2P.js.map} +0 -0
- /package/dist/{dns-C7WXQOED.js.map → dns-2F5SYQNX.js.map} +0 -0
- /package/dist/{zones-KPRAW4JL.js.map → zones-PCUEPXHD.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -2,31 +2,46 @@
|
|
|
2
2
|
import {
|
|
3
3
|
listZones,
|
|
4
4
|
resolveZone
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-NB3OEY2P.js";
|
|
6
6
|
import {
|
|
7
7
|
createCname,
|
|
8
8
|
deleteDnsRecord,
|
|
9
9
|
findCname,
|
|
10
10
|
isManagedDns
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-IDTF7SUY.js";
|
|
12
12
|
import {
|
|
13
13
|
CliError,
|
|
14
|
+
DEFAULT_API_BASE,
|
|
15
|
+
RELAY_SECRET_HEADER,
|
|
14
16
|
__export,
|
|
15
17
|
binDir,
|
|
16
18
|
cfPaginate,
|
|
17
19
|
cfRequest,
|
|
18
20
|
configFile,
|
|
21
|
+
confirm,
|
|
22
|
+
dim,
|
|
19
23
|
ensureDirs,
|
|
24
|
+
ensureRelaySecret,
|
|
25
|
+
fetchErrorReason,
|
|
26
|
+
formatRoute,
|
|
27
|
+
getApiBase,
|
|
20
28
|
getCredentials,
|
|
29
|
+
getRelaySecret,
|
|
30
|
+
isHttpUrl,
|
|
21
31
|
loadConfig,
|
|
22
32
|
logDir,
|
|
33
|
+
note,
|
|
34
|
+
printTable,
|
|
23
35
|
profilesFile,
|
|
36
|
+
redactToken,
|
|
24
37
|
registryFile,
|
|
25
38
|
reportError,
|
|
26
39
|
resolveCf,
|
|
27
40
|
saveConfig,
|
|
28
|
-
|
|
29
|
-
|
|
41
|
+
say,
|
|
42
|
+
scanCacheFile,
|
|
43
|
+
selectOne
|
|
44
|
+
} from "./chunk-ZIUYW2HQ.js";
|
|
30
45
|
|
|
31
46
|
// src/index.ts
|
|
32
47
|
import { Command } from "commander";
|
|
@@ -36,50 +51,6 @@ import pc2 from "picocolors";
|
|
|
36
51
|
// src/config/legacy-migrate.ts
|
|
37
52
|
import { existsSync as existsSync3, readFileSync, renameSync, writeFileSync as writeFileSync4 } from "fs";
|
|
38
53
|
|
|
39
|
-
// src/ui/output.ts
|
|
40
|
-
import pc from "picocolors";
|
|
41
|
-
import Table from "cli-table3";
|
|
42
|
-
import { cancel, confirm as clackConfirm, intro, isCancel, note, outro, select, spinner } from "@clack/prompts";
|
|
43
|
-
async function confirm(message) {
|
|
44
|
-
const answer = await clackConfirm({ message });
|
|
45
|
-
return !isCancel(answer) && answer === true;
|
|
46
|
-
}
|
|
47
|
-
function redactToken(token) {
|
|
48
|
-
if (!token) return "";
|
|
49
|
-
const last4 = token.length > 4 ? token.slice(-4) : token;
|
|
50
|
-
return `\u2022\u2022\u2022\u2022${last4}`;
|
|
51
|
-
}
|
|
52
|
-
var say = {
|
|
53
|
-
info: (msg) => console.log(msg),
|
|
54
|
-
ok: (msg) => console.log(pc.green(`\u2713 ${msg}`)),
|
|
55
|
-
warn: (msg) => console.warn(pc.yellow(`! ${msg}`)),
|
|
56
|
-
dim: (msg) => console.log(pc.dim(msg)),
|
|
57
|
-
step: (msg) => console.log(pc.cyan(`\u2192 ${msg}`))
|
|
58
|
-
};
|
|
59
|
-
var dim = (s) => pc.dim(s);
|
|
60
|
-
function formatRoute(host, target) {
|
|
61
|
-
return `${pc.green(pc.bold(`https://${host}`))} ${pc.dim("\u2192")} ${pc.cyan(target)}`;
|
|
62
|
-
}
|
|
63
|
-
function printTable(head, rows) {
|
|
64
|
-
const table = new Table({
|
|
65
|
-
head: head.map((h) => pc.bold(h)),
|
|
66
|
-
style: { head: [], border: [] }
|
|
67
|
-
});
|
|
68
|
-
for (const row of rows) table.push(row);
|
|
69
|
-
console.log(table.toString());
|
|
70
|
-
}
|
|
71
|
-
async function selectOne(message, items, label4) {
|
|
72
|
-
const value = await select({
|
|
73
|
-
message,
|
|
74
|
-
options: items.map((item, i) => ({ value: String(i), label: label4(item) }))
|
|
75
|
-
});
|
|
76
|
-
if (isCancel(value)) {
|
|
77
|
-
cancel("Cancelled.");
|
|
78
|
-
throw new CliError("Cancelled.", { exitCode: 130 });
|
|
79
|
-
}
|
|
80
|
-
return items[Number(value)];
|
|
81
|
-
}
|
|
82
|
-
|
|
83
54
|
// src/core/service.ts
|
|
84
55
|
import { join as join5 } from "path";
|
|
85
56
|
|
|
@@ -165,6 +136,17 @@ function formatTunnelSpec(s) {
|
|
|
165
136
|
var fqdnFor = (subdomain, zone) => subdomain === "@" ? zone : `${subdomain}.${zone}`;
|
|
166
137
|
var serviceSlug = (fqdn) => fqdn.replace(/[^a-zA-Z0-9]+/g, "-");
|
|
167
138
|
function buildUpArgs(p) {
|
|
139
|
+
if (p.command === "relay") {
|
|
140
|
+
return [
|
|
141
|
+
"relay",
|
|
142
|
+
p.subdomain,
|
|
143
|
+
"-d",
|
|
144
|
+
p.zone,
|
|
145
|
+
...p.proto === "https" ? ["--proto", "https"] : [],
|
|
146
|
+
"-f",
|
|
147
|
+
"-y"
|
|
148
|
+
];
|
|
149
|
+
}
|
|
168
150
|
const spec = formatTunnelSpec({ subdomain: p.subdomain, port: p.port, host: p.host });
|
|
169
151
|
return [
|
|
170
152
|
"up",
|
|
@@ -572,15 +554,31 @@ function openBrowser(url) {
|
|
|
572
554
|
}
|
|
573
555
|
|
|
574
556
|
// src/config/resolve-identity.ts
|
|
575
|
-
var API_BASE = "https://api.cloudflare.com/client/v4";
|
|
576
557
|
async function cfGet(path, token) {
|
|
558
|
+
const base = getApiBase();
|
|
559
|
+
const viaRelay = base !== DEFAULT_API_BASE;
|
|
560
|
+
const secret = getRelaySecret();
|
|
561
|
+
const headers = {
|
|
562
|
+
Authorization: `Bearer ${token}`,
|
|
563
|
+
"Content-Type": "application/json"
|
|
564
|
+
};
|
|
565
|
+
if (secret && viaRelay) headers[RELAY_SECRET_HEADER] = secret;
|
|
577
566
|
let res;
|
|
578
567
|
try {
|
|
579
|
-
res = await fetch(`${
|
|
580
|
-
|
|
568
|
+
res = await fetch(`${base}${path}`, { headers });
|
|
569
|
+
} catch (err) {
|
|
570
|
+
const reason = fetchErrorReason(err);
|
|
571
|
+
say.debug(`[cf] GET ${base}${path} -> network error: ${reason}${viaRelay ? " (relay)" : ""}`);
|
|
572
|
+
throw new CliError(`Could not reach the Cloudflare API (${reason})${viaRelay ? ` via relay ${base}` : ""}.`, {
|
|
573
|
+
hint: viaRelay ? "is the relay tunnel up and the base URL correct? run with CLOUDTUNNEL_DEBUG=1" : void 0
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
say.debug(`[cf] GET ${base}${path} -> ${res.status}${viaRelay ? " (relay)" : ""}`);
|
|
577
|
+
const body = await res.json().catch(() => ({}));
|
|
578
|
+
if (viaRelay && !res.ok && body.error && !body.errors) {
|
|
579
|
+
throw new CliError(`Relay rejected the request (${res.status}): ${body.error}.`, {
|
|
580
|
+
hint: res.status === 403 ? "does CLOUDTUNNEL_RELAY_SECRET match the relay's secret?" : `relay base: ${base}`
|
|
581
581
|
});
|
|
582
|
-
} catch {
|
|
583
|
-
throw new CliError("Could not reach the Cloudflare API (network error).");
|
|
584
582
|
}
|
|
585
583
|
if (res.status === 401) {
|
|
586
584
|
throw new CliError("Cloudflare rejected the token (invalid or expired).", {
|
|
@@ -592,7 +590,6 @@ async function cfGet(path, token) {
|
|
|
592
590
|
hint: `token needs: ${REQUIRED_SCOPES.join(", ")}`
|
|
593
591
|
});
|
|
594
592
|
}
|
|
595
|
-
const body = await res.json().catch(() => ({}));
|
|
596
593
|
if (!res.ok || !body.success) {
|
|
597
594
|
throw new CliError(`Cloudflare API error (${res.status}) on ${path}.`);
|
|
598
595
|
}
|
|
@@ -638,6 +635,22 @@ async function acquireToken(opts) {
|
|
|
638
635
|
return { token, fromEnv: false };
|
|
639
636
|
}
|
|
640
637
|
async function runLoginFlow(opts = {}) {
|
|
638
|
+
if (opts.apiBase && !isHttpUrl(opts.apiBase)) {
|
|
639
|
+
throw new CliError(`Invalid --api-base "${opts.apiBase}".`, {
|
|
640
|
+
hint: "must be an http(s) URL, e.g. https://cfapi.example.com/client/v4"
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
if (opts.tokenStdin && opts.relaySecretStdin) {
|
|
644
|
+
throw new CliError("Can't read both the token and the relay secret from stdin.", {
|
|
645
|
+
hint: "run login twice, or set one via env (CLOUDFLARE_API_TOKEN / CLOUDTUNNEL_RELAY_SECRET)"
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
if (opts.apiBase) process.env.CLOUDTUNNEL_API_BASE = opts.apiBase;
|
|
649
|
+
let relaySecretInput;
|
|
650
|
+
if (opts.relaySecretStdin) {
|
|
651
|
+
relaySecretInput = await readStdin();
|
|
652
|
+
if (relaySecretInput) process.env.CLOUDTUNNEL_RELAY_SECRET = relaySecretInput;
|
|
653
|
+
}
|
|
641
654
|
if (process.stdout.isTTY) clack.intro("cloudtunnel \xB7 connect to Cloudflare");
|
|
642
655
|
const { token, fromEnv } = await acquireToken(opts);
|
|
643
656
|
const spin = clack.spinner();
|
|
@@ -660,12 +673,29 @@ async function runLoginFlow(opts = {}) {
|
|
|
660
673
|
defaultZone = (await selectOne("Select a default domain", zones, (z) => z.name)).name;
|
|
661
674
|
}
|
|
662
675
|
}
|
|
663
|
-
saveConfig(
|
|
676
|
+
saveConfig(buildMergedConfig(loadConfig(), {
|
|
677
|
+
token,
|
|
678
|
+
fromEnv,
|
|
679
|
+
accountId: account.id,
|
|
680
|
+
defaultZone,
|
|
681
|
+
apiBase: opts.apiBase,
|
|
682
|
+
relaySecret: relaySecretInput
|
|
683
|
+
}));
|
|
664
684
|
const summary = `Logged in as ${account.name}${defaultZone ? ` \xB7 default domain ${defaultZone}` : ""}`;
|
|
665
685
|
if (process.stdout.isTTY) clack.outro(summary);
|
|
666
686
|
else say.ok(summary);
|
|
667
687
|
if (!defaultZone) say.dim("No default domain set \u2014 pass -d <domain> on `up`, or re-run `login --zone <domain>`.");
|
|
668
688
|
}
|
|
689
|
+
function buildMergedConfig(prev, args) {
|
|
690
|
+
return {
|
|
691
|
+
...prev,
|
|
692
|
+
apiToken: args.fromEnv ? void 0 : args.token,
|
|
693
|
+
accountId: args.accountId,
|
|
694
|
+
defaultZone: args.defaultZone ?? prev.defaultZone,
|
|
695
|
+
apiBase: args.apiBase ?? prev.apiBase,
|
|
696
|
+
relaySecret: args.relaySecret ?? prev.relaySecret
|
|
697
|
+
};
|
|
698
|
+
}
|
|
669
699
|
function showStatus() {
|
|
670
700
|
const config = loadConfig();
|
|
671
701
|
const token = process.env.CLOUDFLARE_API_TOKEN ?? config.apiToken;
|
|
@@ -677,10 +707,14 @@ function showStatus() {
|
|
|
677
707
|
say.info(`Token: ${redactToken(token)} (${source})`);
|
|
678
708
|
say.info(`Account: ${config.accountId ?? "(from env / unresolved)"}`);
|
|
679
709
|
say.info(`Domain: ${config.defaultZone ?? "(none)"}`);
|
|
710
|
+
const baseSrc = process.env.CLOUDTUNNEL_API_BASE ? "env" : config.apiBase ? "config" : "default";
|
|
711
|
+
say.info(`Base: ${getApiBase()} (${baseSrc})`);
|
|
712
|
+
const secretSrc = process.env.CLOUDTUNNEL_RELAY_SECRET ? "env" : config.relaySecret ? "config" : void 0;
|
|
713
|
+
say.info(`Relay secret: ${secretSrc ? `set (${secretSrc})` : "(none)"}`);
|
|
680
714
|
say.dim(`Config: ${configFile}`);
|
|
681
715
|
}
|
|
682
716
|
function registerLogin(program) {
|
|
683
|
-
program.command("login").description("Authenticate with Cloudflare (paste a token once; account + domain auto-resolved)").option("--token-stdin", "read the API token from stdin (scriptable, avoids shell history)").option("--token <token>", "[discouraged] token as an argument (leaks into shell history)").option("--account <id>", "Cloudflare account id (auto-resolved when you have one account)").option("--zone <domain>", "default domain for new tunnels (auto-resolved when you have one)").option("--status", "show current identity (redacted) and exit").action(async (opts) => {
|
|
717
|
+
program.command("login").description("Authenticate with Cloudflare (paste a token once; account + domain auto-resolved)").option("--token-stdin", "read the API token from stdin (scriptable, avoids shell history)").option("--token <token>", "[discouraged] token as an argument (leaks into shell history)").option("--account <id>", "Cloudflare account id (auto-resolved when you have one account)").option("--zone <domain>", "default domain for new tunnels (auto-resolved when you have one)").option("--api-base <url>", "route the CF API through a relay (when api.cloudflare.com is blocked)").option("--relay-secret-stdin", "read the relay shared secret from stdin (pairs with --api-base)").option("--status", "show current identity (redacted) and exit").action(async (opts) => {
|
|
684
718
|
if (opts.status) return showStatus();
|
|
685
719
|
await runLoginFlow(opts);
|
|
686
720
|
});
|
|
@@ -1301,8 +1335,8 @@ async function resolveRemoteTarget(cf, target) {
|
|
|
1301
1335
|
}
|
|
1302
1336
|
const tunnel = matches[0];
|
|
1303
1337
|
if (!tunnel) return null;
|
|
1304
|
-
const { listCargoCnames } = await import("./dns-
|
|
1305
|
-
const { listZones: listZones3 } = await import("./zones-
|
|
1338
|
+
const { listCargoCnames } = await import("./dns-2F5SYQNX.js");
|
|
1339
|
+
const { listZones: listZones3 } = await import("./zones-PCUEPXHD.js");
|
|
1306
1340
|
for (const zone of await listZones3(cf.token)) {
|
|
1307
1341
|
const rec = (await listCargoCnames(cf.token, zone.id)).find((r) => tunnelIdFromCname2(r.content) === tunnel.id);
|
|
1308
1342
|
if (rec) return { tunnel, fqdn: rec.name };
|
|
@@ -1339,8 +1373,8 @@ async function listAll(cf, opts = {}) {
|
|
|
1339
1373
|
};
|
|
1340
1374
|
});
|
|
1341
1375
|
if (opts.all) {
|
|
1342
|
-
const { listCargoCnames } = await import("./dns-
|
|
1343
|
-
const { listZones: listZones3 } = await import("./zones-
|
|
1376
|
+
const { listCargoCnames } = await import("./dns-2F5SYQNX.js");
|
|
1377
|
+
const { listZones: listZones3 } = await import("./zones-PCUEPXHD.js");
|
|
1344
1378
|
const tracked = new Set(entries.map(entryFqdn));
|
|
1345
1379
|
const unmanaged = [];
|
|
1346
1380
|
for (const zone of await listZones3(cf.token)) {
|
|
@@ -1433,6 +1467,17 @@ async function startTunnels(cf, bin, items, opts = {}) {
|
|
|
1433
1467
|
say.dim("Ctrl-C stops and releases them.");
|
|
1434
1468
|
}
|
|
1435
1469
|
|
|
1470
|
+
// src/core/resolve-domain.ts
|
|
1471
|
+
async function resolveDomain(cf, opts, creds) {
|
|
1472
|
+
if (opts.domain) return opts.domain;
|
|
1473
|
+
const zones = await listZones(cf.token);
|
|
1474
|
+
if (zones.length === 0) throw new CliError("No domains found in this Cloudflare account.");
|
|
1475
|
+
if (zones.length === 1) return zones[0].name;
|
|
1476
|
+
if (process.stdin.isTTY) return (await selectOne("Choose a domain", zones, (z) => z.name)).name;
|
|
1477
|
+
if (creds.defaultZone) return creds.defaultZone;
|
|
1478
|
+
throw new CliError("Multiple domains in this account \u2014 pick one.", { hint: "pass -d <domain>" });
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1436
1481
|
// src/core/transport-protocol.ts
|
|
1437
1482
|
function parseTransportProtocol(value) {
|
|
1438
1483
|
if (value === "auto" || value === "http2" || value === "quic") return value;
|
|
@@ -1461,15 +1506,6 @@ async function promptPort() {
|
|
|
1461
1506
|
);
|
|
1462
1507
|
return Number(input);
|
|
1463
1508
|
}
|
|
1464
|
-
async function resolveDomain(cf, opts, creds) {
|
|
1465
|
-
if (opts.domain) return opts.domain;
|
|
1466
|
-
const zones = await listZones(cf.token);
|
|
1467
|
-
if (zones.length === 0) throw new CliError("No domains found in this Cloudflare account.");
|
|
1468
|
-
if (zones.length === 1) return zones[0].name;
|
|
1469
|
-
if (process.stdin.isTTY) return (await selectOne("Choose a domain", zones, (z) => z.name)).name;
|
|
1470
|
-
if (creds.defaultZone) return creds.defaultZone;
|
|
1471
|
-
throw new CliError("Multiple domains in this account \u2014 pick one.", { hint: "pass -d <domain>" });
|
|
1472
|
-
}
|
|
1473
1509
|
async function resolveSpecSubdomain(spec, opts) {
|
|
1474
1510
|
if (spec.subdomain !== void 0) return spec.subdomain;
|
|
1475
1511
|
if (opts.yes || !process.stdin.isTTY) return void 0;
|
|
@@ -1673,6 +1709,182 @@ function registerLogs(program) {
|
|
|
1673
1709
|
});
|
|
1674
1710
|
}
|
|
1675
1711
|
|
|
1712
|
+
// src/commands/relay.ts
|
|
1713
|
+
import { openSync as openSync3 } from "fs";
|
|
1714
|
+
import { spawn as spawn3 } from "child_process";
|
|
1715
|
+
import { join as join8 } from "path";
|
|
1716
|
+
import pc from "picocolors";
|
|
1717
|
+
|
|
1718
|
+
// src/core/api-proxy-server.ts
|
|
1719
|
+
import http from "http";
|
|
1720
|
+
var DEFAULT_UPSTREAM = "https://api.cloudflare.com";
|
|
1721
|
+
var SECRET_HEADER_LC = RELAY_SECRET_HEADER.toLowerCase();
|
|
1722
|
+
var HOP_BY_HOP = /* @__PURE__ */ new Set([
|
|
1723
|
+
"connection",
|
|
1724
|
+
"keep-alive",
|
|
1725
|
+
"proxy-authenticate",
|
|
1726
|
+
"proxy-authorization",
|
|
1727
|
+
"te",
|
|
1728
|
+
"trailer",
|
|
1729
|
+
"transfer-encoding",
|
|
1730
|
+
"upgrade",
|
|
1731
|
+
"host",
|
|
1732
|
+
"content-length"
|
|
1733
|
+
]);
|
|
1734
|
+
function send(res, code, body) {
|
|
1735
|
+
res.writeHead(code, { "content-type": "application/json" });
|
|
1736
|
+
res.end(body === void 0 ? "" : JSON.stringify(body));
|
|
1737
|
+
}
|
|
1738
|
+
function startProxy(opts) {
|
|
1739
|
+
const upstream = opts.upstream ?? DEFAULT_UPSTREAM;
|
|
1740
|
+
const upstreamOrigin = new URL(upstream).origin;
|
|
1741
|
+
const log = opts.log ?? (() => {
|
|
1742
|
+
});
|
|
1743
|
+
const server = http.createServer((req, res) => {
|
|
1744
|
+
handle(req, res).catch(() => {
|
|
1745
|
+
if (!res.headersSent) send(res, 500, { error: "relay internal error" });
|
|
1746
|
+
else res.end();
|
|
1747
|
+
});
|
|
1748
|
+
});
|
|
1749
|
+
async function handle(req, res) {
|
|
1750
|
+
const trace = (status, note4) => log(`[relay] ${status} ${req.method} ${req.url ?? "-"}${note4 ? " " + note4 : ""}`);
|
|
1751
|
+
if (req.method === "CONNECT" || !req.url || !req.url.startsWith("/")) {
|
|
1752
|
+
trace(400, "bad-form");
|
|
1753
|
+
return send(res, 400, { error: "origin-form request required" });
|
|
1754
|
+
}
|
|
1755
|
+
let target;
|
|
1756
|
+
try {
|
|
1757
|
+
target = new URL(req.url, upstream);
|
|
1758
|
+
} catch {
|
|
1759
|
+
trace(400, "bad-path");
|
|
1760
|
+
return send(res, 400, { error: "bad request path" });
|
|
1761
|
+
}
|
|
1762
|
+
if (target.origin !== upstreamOrigin) {
|
|
1763
|
+
trace(400, "ssrf");
|
|
1764
|
+
return send(res, 400, { error: "path escapes upstream" });
|
|
1765
|
+
}
|
|
1766
|
+
if (req.headers[SECRET_HEADER_LC] !== opts.secret) {
|
|
1767
|
+
trace(403, "secret");
|
|
1768
|
+
return send(res, 403, { error: "relay secret missing or invalid" });
|
|
1769
|
+
}
|
|
1770
|
+
const hasBody = req.method !== "GET" && req.method !== "HEAD";
|
|
1771
|
+
let body;
|
|
1772
|
+
if (hasBody) {
|
|
1773
|
+
const chunks = [];
|
|
1774
|
+
for await (const c of req) chunks.push(c);
|
|
1775
|
+
body = chunks.length ? Buffer.concat(chunks) : void 0;
|
|
1776
|
+
}
|
|
1777
|
+
const headers = {};
|
|
1778
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
1779
|
+
if (v === void 0) continue;
|
|
1780
|
+
const lk = k.toLowerCase();
|
|
1781
|
+
if (HOP_BY_HOP.has(lk) || lk === SECRET_HEADER_LC) continue;
|
|
1782
|
+
headers[k] = Array.isArray(v) ? v.join(", ") : v;
|
|
1783
|
+
}
|
|
1784
|
+
let up;
|
|
1785
|
+
try {
|
|
1786
|
+
up = await fetch(target.href, { method: req.method, headers, body, redirect: "manual" });
|
|
1787
|
+
} catch {
|
|
1788
|
+
trace(502, "upstream-error");
|
|
1789
|
+
return send(res, 502, { error: "relay upstream unreachable" });
|
|
1790
|
+
}
|
|
1791
|
+
trace(up.status);
|
|
1792
|
+
const outHeaders = {
|
|
1793
|
+
"content-type": up.headers.get("content-type") ?? "application/json"
|
|
1794
|
+
};
|
|
1795
|
+
const retryAfter = up.headers.get("retry-after");
|
|
1796
|
+
if (retryAfter) outHeaders["retry-after"] = retryAfter;
|
|
1797
|
+
res.writeHead(up.status, outHeaders);
|
|
1798
|
+
res.end(Buffer.from(await up.arrayBuffer()));
|
|
1799
|
+
}
|
|
1800
|
+
return new Promise((resolve, reject) => {
|
|
1801
|
+
server.once("error", reject);
|
|
1802
|
+
server.listen(0, "127.0.0.1", () => {
|
|
1803
|
+
const port = server.address().port;
|
|
1804
|
+
resolve({
|
|
1805
|
+
port,
|
|
1806
|
+
url: `http://127.0.0.1:${port}`,
|
|
1807
|
+
close: () => new Promise((res) => server.close(() => res()))
|
|
1808
|
+
});
|
|
1809
|
+
});
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
// src/commands/relay.ts
|
|
1814
|
+
var DEFAULT_SUB = "cfapi";
|
|
1815
|
+
function fqdnFor2(sub, domain) {
|
|
1816
|
+
return sub === "@" ? domain : `${sub}.${domain}`;
|
|
1817
|
+
}
|
|
1818
|
+
function spawnDetachedRelay(sub, domain) {
|
|
1819
|
+
const script = process.argv[1];
|
|
1820
|
+
if (!script) throw new CliError("Cannot resolve the cloudtunnel executable path.");
|
|
1821
|
+
ensureDirs();
|
|
1822
|
+
const logFile = join8(logDir, `relay-${sub === "@" ? "root" : sub}.log`);
|
|
1823
|
+
const fd = openSync3(logFile, "a", 384);
|
|
1824
|
+
const args = [script, "relay", sub, "-d", domain, "-f", "-y"];
|
|
1825
|
+
const child = spawn3(process.execPath, args, { detached: true, stdio: ["ignore", fd, fd] });
|
|
1826
|
+
child.unref();
|
|
1827
|
+
}
|
|
1828
|
+
function relayReadyLines(fqdn, secret, tty) {
|
|
1829
|
+
const url = `https://${fqdn}`;
|
|
1830
|
+
if (!tty) return [`relay ready at ${url}`];
|
|
1831
|
+
const base = `${url}/client/v4`;
|
|
1832
|
+
return [
|
|
1833
|
+
`URL ${pc.green(url)}`,
|
|
1834
|
+
`Secret ${pc.bold(secret)} ${pc.dim("(store it \u2014 the client needs it, shown once)")}`,
|
|
1835
|
+
"",
|
|
1836
|
+
pc.bold("On the blocked client:"),
|
|
1837
|
+
` export CLOUDTUNNEL_API_BASE=${base}`,
|
|
1838
|
+
` export CLOUDTUNNEL_RELAY_SECRET=${secret}`,
|
|
1839
|
+
` printf %s "$CF_TOKEN" | cloudtunnel login --token-stdin`,
|
|
1840
|
+
pc.dim(" (mint the CF token on an unblocked host \u2014 dash.cloudflare.com is blocked too)")
|
|
1841
|
+
];
|
|
1842
|
+
}
|
|
1843
|
+
function printRelayReady(fqdn, secret, kind) {
|
|
1844
|
+
const tty = !!process.stdout.isTTY;
|
|
1845
|
+
const lines = relayReadyLines(fqdn, secret, tty);
|
|
1846
|
+
if (tty) note(lines.join("\n"), "relay ready");
|
|
1847
|
+
else say.dim(lines[0]);
|
|
1848
|
+
if (tty && kind === "detach") say.dim(" \u2192 running in background \xB7 stop with: cloudtunnel delete " + fqdn);
|
|
1849
|
+
if (tty && kind === "service") say.dim(" \u2192 boot service installed \xB7 view: cloudtunnel ls \xB7 remove: cloudtunnel delete " + fqdn);
|
|
1850
|
+
}
|
|
1851
|
+
async function runRelay(sub, opts) {
|
|
1852
|
+
const creds = await ensureAuth();
|
|
1853
|
+
const cf = resolveCf();
|
|
1854
|
+
const domain = await resolveDomain(cf, opts, creds);
|
|
1855
|
+
const fqdn = fqdnFor2(sub, domain);
|
|
1856
|
+
if (opts.service) {
|
|
1857
|
+
assertServiceSupported();
|
|
1858
|
+
const secret2 = ensureRelaySecret();
|
|
1859
|
+
installServiceForSpec({ command: "relay", subdomain: sub, port: 0, zone: domain, proto: "http" });
|
|
1860
|
+
printRelayReady(fqdn, secret2, "service");
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
if (opts.detach) {
|
|
1864
|
+
const secret2 = ensureRelaySecret();
|
|
1865
|
+
spawnDetachedRelay(sub, domain);
|
|
1866
|
+
printRelayReady(fqdn, secret2, "detach");
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
const bin = await ensureCloudflared();
|
|
1870
|
+
const secret = ensureRelaySecret();
|
|
1871
|
+
const proxy = await startProxy({ secret, log: (line) => say.dim(line) });
|
|
1872
|
+
const item = {
|
|
1873
|
+
port: proxy.port,
|
|
1874
|
+
proto: "http",
|
|
1875
|
+
name: sub,
|
|
1876
|
+
zone: domain,
|
|
1877
|
+
defaultZone: creds.defaultZone,
|
|
1878
|
+
force: opts.force,
|
|
1879
|
+
yes: opts.yes
|
|
1880
|
+
};
|
|
1881
|
+
await startTunnels(cf, bin, [item], {});
|
|
1882
|
+
printRelayReady(fqdn, secret, "foreground");
|
|
1883
|
+
}
|
|
1884
|
+
function registerRelay(program) {
|
|
1885
|
+
program.command("relay [subdomain]").description("Expose a locked Cloudflare-API reverse proxy via a tunnel (for clients that can't reach api.cloudflare.com directly)").option("-d, --domain <domain>", "domain for the relay subdomain (prompted from a list if unset)").option("--detach", "run the relay in the background").option("--service", "register the relay as a boot service (systemd \xB7 launchd \xB7 Task Scheduler)").option("-f, --force", "replace a non-tunnel DNS record occupying the hostname").option("-y, --yes", "don't prompt before replacing an existing record").action((subdomain, opts) => runRelay(subdomain ?? DEFAULT_SUB, opts));
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1676
1888
|
// src/index.ts
|
|
1677
1889
|
var require2 = createRequire(import.meta.url);
|
|
1678
1890
|
var pkg = require2("../package.json");
|
|
@@ -1690,7 +1902,7 @@ function buildProgram() {
|
|
|
1690
1902
|
""
|
|
1691
1903
|
].join("\n")
|
|
1692
1904
|
);
|
|
1693
|
-
for (const register of [registerLogin, registerUp, registerLs, registerDelete, registerLogs]) {
|
|
1905
|
+
for (const register of [registerLogin, registerUp, registerLs, registerDelete, registerLogs, registerRelay]) {
|
|
1694
1906
|
register(program);
|
|
1695
1907
|
}
|
|
1696
1908
|
return program;
|