@iamken/cloudtunnel 0.11.4 → 0.11.5
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/dist/index.js +129 -107
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -247,11 +247,13 @@ function assertSupported() {
|
|
|
247
247
|
}
|
|
248
248
|
function install(d) {
|
|
249
249
|
assertSupported();
|
|
250
|
-
const
|
|
250
|
+
const name = label(d.fqdn);
|
|
251
|
+
const tmp = join2(tmpdir(), name);
|
|
251
252
|
writeFileSync(tmp, buildUnit(d), { mode: 420 });
|
|
252
253
|
privileged(["install", "-m", "0644", tmp, unitPath(d.fqdn)]);
|
|
253
254
|
privileged(["systemctl", "daemon-reload"]);
|
|
254
|
-
privileged(["systemctl", "enable",
|
|
255
|
+
privileged(["systemctl", "enable", name]);
|
|
256
|
+
privileged(["systemctl", "restart", name]);
|
|
255
257
|
}
|
|
256
258
|
function uninstall(fqdn) {
|
|
257
259
|
try {
|
|
@@ -389,7 +391,7 @@ function buildTaskXml(d) {
|
|
|
389
391
|
" <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>",
|
|
390
392
|
" <RestartOnFailure><Interval>PT1M</Interval><Count>999</Count></RestartOnFailure>",
|
|
391
393
|
" <Enabled>true</Enabled>",
|
|
392
|
-
"
|
|
394
|
+
"</Settings>",
|
|
393
395
|
' <Actions Context="Author">',
|
|
394
396
|
` <Exec><Command>${xml2(d.nodePath)}</Command><Arguments>${xml2(args)}</Arguments></Exec>`,
|
|
395
397
|
" </Actions>",
|
|
@@ -409,9 +411,11 @@ function assertSupported3() {
|
|
|
409
411
|
}
|
|
410
412
|
function install3(d) {
|
|
411
413
|
const file = join4(tmpdir2(), `${d.slug}.task.xml`);
|
|
414
|
+
const name = label3(d.fqdn);
|
|
412
415
|
writeFileSync3(file, "\uFEFF" + buildTaskXml(d), { encoding: "utf16le" });
|
|
413
|
-
|
|
414
|
-
schtasks
|
|
416
|
+
schtasks(["/End", "/TN", name]);
|
|
417
|
+
execFileSync3("schtasks", ["/Create", "/TN", name, "/XML", file, "/F"], { stdio: "inherit" });
|
|
418
|
+
schtasks(["/Run", "/TN", name]);
|
|
415
419
|
}
|
|
416
420
|
function uninstall3(fqdn) {
|
|
417
421
|
schtasks(["/Delete", "/TN", label3(fqdn), "/F"]);
|
|
@@ -941,6 +945,103 @@ function extractTgz(bytes) {
|
|
|
941
945
|
});
|
|
942
946
|
}
|
|
943
947
|
|
|
948
|
+
// src/cloudflare/tunnels.ts
|
|
949
|
+
var MANAGED_TUNNEL_PREFIX = "ct-";
|
|
950
|
+
function isManagedTunnel(tunnel) {
|
|
951
|
+
return tunnel.name.startsWith(MANAGED_TUNNEL_PREFIX);
|
|
952
|
+
}
|
|
953
|
+
async function createTunnel(cf, name) {
|
|
954
|
+
const env = await cfRequest(cf.token, "POST", `/accounts/${cf.accountId}/cfd_tunnel`, {
|
|
955
|
+
name,
|
|
956
|
+
config_src: "cloudflare"
|
|
957
|
+
});
|
|
958
|
+
return env.result;
|
|
959
|
+
}
|
|
960
|
+
function listTunnels(cf) {
|
|
961
|
+
return cfPaginate(cf.token, `/accounts/${cf.accountId}/cfd_tunnel?is_deleted=false`);
|
|
962
|
+
}
|
|
963
|
+
async function getTunnel(cf, id) {
|
|
964
|
+
return (await cfRequest(cf.token, "GET", `/accounts/${cf.accountId}/cfd_tunnel/${id}`)).result;
|
|
965
|
+
}
|
|
966
|
+
async function deleteTunnel(cf, id) {
|
|
967
|
+
await cfRequest(cf.token, "DELETE", `/accounts/${cf.accountId}/cfd_tunnel/${id}`);
|
|
968
|
+
}
|
|
969
|
+
async function cleanupConnections(cf, id) {
|
|
970
|
+
await cfRequest(cf.token, "DELETE", `/accounts/${cf.accountId}/cfd_tunnel/${id}/connections`);
|
|
971
|
+
}
|
|
972
|
+
async function deleteTunnelWithConnections(cf, id) {
|
|
973
|
+
try {
|
|
974
|
+
await deleteTunnel(cf, id);
|
|
975
|
+
} catch (err) {
|
|
976
|
+
if (err instanceof CliError && /active connections/i.test(err.message)) {
|
|
977
|
+
await cleanupConnections(cf, id);
|
|
978
|
+
await deleteTunnel(cf, id);
|
|
979
|
+
} else {
|
|
980
|
+
throw err;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
async function getTunnelToken(cf, id) {
|
|
985
|
+
return (await cfRequest(cf.token, "GET", `/accounts/${cf.accountId}/cfd_tunnel/${id}/token`)).result;
|
|
986
|
+
}
|
|
987
|
+
async function putIngress(cf, id, ingress) {
|
|
988
|
+
await cfRequest(cf.token, "PUT", `/accounts/${cf.accountId}/cfd_tunnel/${id}/configurations`, {
|
|
989
|
+
config: { ingress }
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
async function getConnections(cf, id) {
|
|
993
|
+
const env = await cfRequest(
|
|
994
|
+
cf.token,
|
|
995
|
+
"GET",
|
|
996
|
+
`/accounts/${cf.accountId}/cfd_tunnel/${id}/connections`
|
|
997
|
+
);
|
|
998
|
+
return env.result ?? [];
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// src/connector/health.ts
|
|
1002
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1003
|
+
async function waitHealthy(cf, tunnelId, opts = {}) {
|
|
1004
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 3e4);
|
|
1005
|
+
while (Date.now() < deadline) {
|
|
1006
|
+
if (opts.signal?.aborted) return "dead";
|
|
1007
|
+
try {
|
|
1008
|
+
const connections = await getConnections(cf, tunnelId);
|
|
1009
|
+
if (connections.length > 0) return "healthy";
|
|
1010
|
+
} catch {
|
|
1011
|
+
}
|
|
1012
|
+
await sleep(2e3);
|
|
1013
|
+
}
|
|
1014
|
+
return opts.signal?.aborted ? "dead" : "provisioning";
|
|
1015
|
+
}
|
|
1016
|
+
function extractMetricsPort(log) {
|
|
1017
|
+
const matches = [...log.matchAll(/Starting metrics server on 127\.0\.0\.1:(\d+)\/metrics/g)];
|
|
1018
|
+
if (!matches.length) return void 0;
|
|
1019
|
+
const last = matches[matches.length - 1];
|
|
1020
|
+
const port = Number(last?.[1]);
|
|
1021
|
+
return Number.isInteger(port) && port > 0 && port <= 65535 ? port : void 0;
|
|
1022
|
+
}
|
|
1023
|
+
async function checkLocalReady(port, opts = {}) {
|
|
1024
|
+
const controller = new AbortController();
|
|
1025
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 2e3);
|
|
1026
|
+
try {
|
|
1027
|
+
const res = await fetch(`http://127.0.0.1:${port}/ready`, { signal: controller.signal });
|
|
1028
|
+
if (!res.ok) return { healthy: false, readyConnections: 0, status: res.status };
|
|
1029
|
+
const data = await res.json().catch(() => ({}));
|
|
1030
|
+
const readyConnections = Number(data.readyConnections) || 0;
|
|
1031
|
+
return { healthy: readyConnections > 0, readyConnections, status: res.status };
|
|
1032
|
+
} catch {
|
|
1033
|
+
return { healthy: false, readyConnections: 0 };
|
|
1034
|
+
} finally {
|
|
1035
|
+
clearTimeout(timer);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
async function probeEntryState(entry, gone) {
|
|
1039
|
+
if (gone || entry.state !== "running" || !entry.pid) return "down";
|
|
1040
|
+
if (!entry.metricsPort) return "up";
|
|
1041
|
+
const ready = await checkLocalReady(entry.metricsPort, { timeoutMs: 300 });
|
|
1042
|
+
return ready.healthy ? "up" : "reconnecting";
|
|
1043
|
+
}
|
|
1044
|
+
|
|
944
1045
|
// src/core/up-runner.ts
|
|
945
1046
|
import { join as join7 } from "path";
|
|
946
1047
|
import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
|
|
@@ -1084,7 +1185,7 @@ async function reconcile() {
|
|
|
1084
1185
|
}
|
|
1085
1186
|
|
|
1086
1187
|
// src/connector/process.ts
|
|
1087
|
-
var
|
|
1188
|
+
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1088
1189
|
function startConnector(opts) {
|
|
1089
1190
|
const args = ["tunnel", "--metrics", "127.0.0.1:0", "run"];
|
|
1090
1191
|
if (opts.protocol) args.push("--protocol", opts.protocol);
|
|
@@ -1116,7 +1217,7 @@ async function stopConnector(entry) {
|
|
|
1116
1217
|
} catch {
|
|
1117
1218
|
return false;
|
|
1118
1219
|
}
|
|
1119
|
-
await
|
|
1220
|
+
await sleep2(3e3);
|
|
1120
1221
|
if (await isOurConnector(entry)) {
|
|
1121
1222
|
try {
|
|
1122
1223
|
process.kill(pid, "SIGKILL");
|
|
@@ -1126,103 +1227,6 @@ async function stopConnector(entry) {
|
|
|
1126
1227
|
return true;
|
|
1127
1228
|
}
|
|
1128
1229
|
|
|
1129
|
-
// src/cloudflare/tunnels.ts
|
|
1130
|
-
var MANAGED_TUNNEL_PREFIX = "ct-";
|
|
1131
|
-
function isManagedTunnel(tunnel) {
|
|
1132
|
-
return tunnel.name.startsWith(MANAGED_TUNNEL_PREFIX);
|
|
1133
|
-
}
|
|
1134
|
-
async function createTunnel(cf, name) {
|
|
1135
|
-
const env = await cfRequest(cf.token, "POST", `/accounts/${cf.accountId}/cfd_tunnel`, {
|
|
1136
|
-
name,
|
|
1137
|
-
config_src: "cloudflare"
|
|
1138
|
-
});
|
|
1139
|
-
return env.result;
|
|
1140
|
-
}
|
|
1141
|
-
function listTunnels(cf) {
|
|
1142
|
-
return cfPaginate(cf.token, `/accounts/${cf.accountId}/cfd_tunnel?is_deleted=false`);
|
|
1143
|
-
}
|
|
1144
|
-
async function getTunnel(cf, id) {
|
|
1145
|
-
return (await cfRequest(cf.token, "GET", `/accounts/${cf.accountId}/cfd_tunnel/${id}`)).result;
|
|
1146
|
-
}
|
|
1147
|
-
async function deleteTunnel(cf, id) {
|
|
1148
|
-
await cfRequest(cf.token, "DELETE", `/accounts/${cf.accountId}/cfd_tunnel/${id}`);
|
|
1149
|
-
}
|
|
1150
|
-
async function cleanupConnections(cf, id) {
|
|
1151
|
-
await cfRequest(cf.token, "DELETE", `/accounts/${cf.accountId}/cfd_tunnel/${id}/connections`);
|
|
1152
|
-
}
|
|
1153
|
-
async function deleteTunnelWithConnections(cf, id) {
|
|
1154
|
-
try {
|
|
1155
|
-
await deleteTunnel(cf, id);
|
|
1156
|
-
} catch (err) {
|
|
1157
|
-
if (err instanceof CliError && /active connections/i.test(err.message)) {
|
|
1158
|
-
await cleanupConnections(cf, id);
|
|
1159
|
-
await deleteTunnel(cf, id);
|
|
1160
|
-
} else {
|
|
1161
|
-
throw err;
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
async function getTunnelToken(cf, id) {
|
|
1166
|
-
return (await cfRequest(cf.token, "GET", `/accounts/${cf.accountId}/cfd_tunnel/${id}/token`)).result;
|
|
1167
|
-
}
|
|
1168
|
-
async function putIngress(cf, id, ingress) {
|
|
1169
|
-
await cfRequest(cf.token, "PUT", `/accounts/${cf.accountId}/cfd_tunnel/${id}/configurations`, {
|
|
1170
|
-
config: { ingress }
|
|
1171
|
-
});
|
|
1172
|
-
}
|
|
1173
|
-
async function getConnections(cf, id) {
|
|
1174
|
-
const env = await cfRequest(
|
|
1175
|
-
cf.token,
|
|
1176
|
-
"GET",
|
|
1177
|
-
`/accounts/${cf.accountId}/cfd_tunnel/${id}/connections`
|
|
1178
|
-
);
|
|
1179
|
-
return env.result ?? [];
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
// src/connector/health.ts
|
|
1183
|
-
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1184
|
-
async function waitHealthy(cf, tunnelId, opts = {}) {
|
|
1185
|
-
const deadline = Date.now() + (opts.timeoutMs ?? 3e4);
|
|
1186
|
-
while (Date.now() < deadline) {
|
|
1187
|
-
if (opts.signal?.aborted) return "dead";
|
|
1188
|
-
try {
|
|
1189
|
-
const connections = await getConnections(cf, tunnelId);
|
|
1190
|
-
if (connections.length > 0) return "healthy";
|
|
1191
|
-
} catch {
|
|
1192
|
-
}
|
|
1193
|
-
await sleep2(2e3);
|
|
1194
|
-
}
|
|
1195
|
-
return opts.signal?.aborted ? "dead" : "provisioning";
|
|
1196
|
-
}
|
|
1197
|
-
function extractMetricsPort(log) {
|
|
1198
|
-
const matches = [...log.matchAll(/Starting metrics server on 127\.0\.0\.1:(\d+)\/metrics/g)];
|
|
1199
|
-
if (!matches.length) return void 0;
|
|
1200
|
-
const last = matches[matches.length - 1];
|
|
1201
|
-
const port = Number(last?.[1]);
|
|
1202
|
-
return Number.isInteger(port) && port > 0 && port <= 65535 ? port : void 0;
|
|
1203
|
-
}
|
|
1204
|
-
async function checkLocalReady(port, opts = {}) {
|
|
1205
|
-
const controller = new AbortController();
|
|
1206
|
-
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 2e3);
|
|
1207
|
-
try {
|
|
1208
|
-
const res = await fetch(`http://127.0.0.1:${port}/ready`, { signal: controller.signal });
|
|
1209
|
-
if (!res.ok) return { healthy: false, readyConnections: 0, status: res.status };
|
|
1210
|
-
const data = await res.json().catch(() => ({}));
|
|
1211
|
-
const readyConnections = Number(data.readyConnections) || 0;
|
|
1212
|
-
return { healthy: readyConnections > 0, readyConnections, status: res.status };
|
|
1213
|
-
} catch {
|
|
1214
|
-
return { healthy: false, readyConnections: 0 };
|
|
1215
|
-
} finally {
|
|
1216
|
-
clearTimeout(timer);
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
async function probeEntryState(entry, gone) {
|
|
1220
|
-
if (gone || entry.state !== "running" || !entry.pid) return "down";
|
|
1221
|
-
if (!entry.metricsPort) return "up";
|
|
1222
|
-
const ready = await checkLocalReady(entry.metricsPort, { timeoutMs: 300 });
|
|
1223
|
-
return ready.healthy ? "up" : "reconnecting";
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
1230
|
// src/core/orchestrator-create.ts
|
|
1227
1231
|
import { randomInt as randomInt2 } from "crypto";
|
|
1228
1232
|
|
|
@@ -1309,9 +1313,14 @@ async function createTunnelSubdomain(cf, opts) {
|
|
|
1309
1313
|
throw new CliError("Cancelled.", { exitCode: 130 });
|
|
1310
1314
|
}
|
|
1311
1315
|
}
|
|
1316
|
+
const prev = getEntry(host.hostname);
|
|
1317
|
+
if (prev) await stopConnector(prev);
|
|
1312
1318
|
for (const rec of existingList) {
|
|
1313
1319
|
await releaseHostname(cf, zone.id, rec);
|
|
1314
1320
|
}
|
|
1321
|
+
} else {
|
|
1322
|
+
const prev = getEntry(host.hostname);
|
|
1323
|
+
if (prev) await stopConnector(prev);
|
|
1315
1324
|
}
|
|
1316
1325
|
await upsertEntry(host.hostname, {
|
|
1317
1326
|
subdomain: host.subdomain,
|
|
@@ -1320,7 +1329,9 @@ async function createTunnelSubdomain(cf, opts) {
|
|
|
1320
1329
|
port: opts.port,
|
|
1321
1330
|
proto: opts.proto,
|
|
1322
1331
|
host: opts.host,
|
|
1323
|
-
state: "provisioning"
|
|
1332
|
+
state: "provisioning",
|
|
1333
|
+
pid: void 0,
|
|
1334
|
+
metricsPort: void 0
|
|
1324
1335
|
});
|
|
1325
1336
|
let tunnelId;
|
|
1326
1337
|
let dnsRecordId;
|
|
@@ -1857,7 +1868,7 @@ async function runUp(specArgs, opts) {
|
|
|
1857
1868
|
}
|
|
1858
1869
|
await startTunnels(cf, bin, items, { detach: opts.detach, protocol, keepAlive: opts.keepAlive });
|
|
1859
1870
|
}
|
|
1860
|
-
var SERVICE_UP_TIMEOUT_MS =
|
|
1871
|
+
var SERVICE_UP_TIMEOUT_MS = 3e4;
|
|
1861
1872
|
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1862
1873
|
async function registerServices(cf, items, domain, proto, protocol) {
|
|
1863
1874
|
assertServiceSupported();
|
|
@@ -1869,6 +1880,7 @@ async function registerServices(cf, items, domain, proto, protocol) {
|
|
|
1869
1880
|
for (const item of items) {
|
|
1870
1881
|
const subdomain = item.name;
|
|
1871
1882
|
const fqdn = subdomain === "@" ? domain : `${subdomain}.${domain}`;
|
|
1883
|
+
await patchEntry(fqdn, { state: "provisioning", pid: void 0, metricsPort: void 0 });
|
|
1872
1884
|
installServiceForSpec({ subdomain, port: item.port, host: item.host, zone: domain, proto, protocol });
|
|
1873
1885
|
fqdns.push(fqdn);
|
|
1874
1886
|
}
|
|
@@ -1893,11 +1905,21 @@ async function waitServicesUp(fqdns, timeoutMs) {
|
|
|
1893
1905
|
while (pending.size > 0) {
|
|
1894
1906
|
for (const fqdn of [...pending]) {
|
|
1895
1907
|
const entry = getEntry(fqdn);
|
|
1896
|
-
if (entry?.state === "running" && entry.pid
|
|
1908
|
+
if (entry?.state === "running" && entry.pid && await isOurConnector(entry)) {
|
|
1909
|
+
if (!entry.metricsPort || (await checkLocalReady(entry.metricsPort, { timeoutMs: 500 })).healthy) {
|
|
1910
|
+
pending.delete(fqdn);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1897
1913
|
}
|
|
1898
1914
|
if (pending.size === 0 || Date.now() >= deadline) break;
|
|
1899
1915
|
await delay(500);
|
|
1900
1916
|
}
|
|
1917
|
+
for (const fqdn of [...pending]) {
|
|
1918
|
+
const entry = getEntry(fqdn);
|
|
1919
|
+
if (entry?.state === "running" && entry.pid && await isOurConnector(entry)) {
|
|
1920
|
+
pending.delete(fqdn);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1901
1923
|
return [...pending];
|
|
1902
1924
|
}
|
|
1903
1925
|
function registerUp(program) {
|