@kvbox/server 0.5.12 → 0.5.14
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/kvbox-server.js +414 -55
- package/dist/kvbox-server.js.map +1 -1
- package/package.json +1 -1
- package/scripts/net-setup.sh +3 -0
package/dist/kvbox-server.js
CHANGED
|
@@ -339,6 +339,30 @@ import { request } from "undici";
|
|
|
339
339
|
import { z as z3 } from "zod";
|
|
340
340
|
var SizeSchema = z3.enum(SIZE_NAMES);
|
|
341
341
|
var BoxStatusSchema = z3.enum(["running", "standby", "hibernated", "stopped", "unknown"]);
|
|
342
|
+
var PortProtoSchema = z3.enum(["tcp", "udp"]);
|
|
343
|
+
var MAX_BOX_PORTS = 16;
|
|
344
|
+
var PortForwardSchema = z3.object({
|
|
345
|
+
guestPort: z3.number().int().min(1).max(65535),
|
|
346
|
+
hostPort: z3.number().int(),
|
|
347
|
+
proto: PortProtoSchema,
|
|
348
|
+
/** The host's public address; only the control plane fills it in, as for `ssh`. */
|
|
349
|
+
address: z3.string().optional()
|
|
350
|
+
});
|
|
351
|
+
var PortsRequest = z3.object({
|
|
352
|
+
ports: z3.array(z3.object({ guestPort: z3.number().int().min(1).max(65535), proto: PortProtoSchema.optional() })).min(1).max(MAX_BOX_PORTS)
|
|
353
|
+
});
|
|
354
|
+
var BoxPortSchema = z3.number().int().min(1).max(65535);
|
|
355
|
+
var ClaimSubdomainRequest = z3.object({ name: z3.string(), port: BoxPortSchema });
|
|
356
|
+
var SubdomainPortRequest = z3.object({ port: BoxPortSchema });
|
|
357
|
+
var SubdomainRouteRequest = z3.object({ name: z3.string(), port: BoxPortSchema });
|
|
358
|
+
var SubdomainSchema = z3.object({
|
|
359
|
+
owner: z3.string(),
|
|
360
|
+
box: z3.string(),
|
|
361
|
+
subdomain: z3.string(),
|
|
362
|
+
port: BoxPortSchema,
|
|
363
|
+
url: z3.string(),
|
|
364
|
+
routes: z3.array(z3.object({ name: z3.string(), port: BoxPortSchema, url: z3.string() }))
|
|
365
|
+
});
|
|
342
366
|
var MachineSchema = z3.object({
|
|
343
367
|
/** Host-unique short id (the tap is `kv-<id>`). Missing only on a box made before ids that has not restarted since. */
|
|
344
368
|
id: z3.string(),
|
|
@@ -371,6 +395,8 @@ var MachineSchema = z3.object({
|
|
|
371
395
|
* whose host reported no port or did not answer.
|
|
372
396
|
*/
|
|
373
397
|
ssh: z3.object({ host: z3.string(), port: z3.number().int(), user: z3.string() }).optional(),
|
|
398
|
+
/** Box ports published on the host (#30). Absent: none. */
|
|
399
|
+
ports: z3.array(PortForwardSchema).optional(),
|
|
374
400
|
/** Whether the owner slug is a user or an org. Only the control plane knows; it fills it in on `GET /machines`. */
|
|
375
401
|
ownerKind: z3.enum(["user", "org"]).optional(),
|
|
376
402
|
/** Seconds without network traffic before the host hibernates the box (#47). Absent: never. */
|
|
@@ -1023,6 +1049,12 @@ var MachineMetaSchema = z4.object({
|
|
|
1023
1049
|
sshKeysStale: z4.boolean().optional(),
|
|
1024
1050
|
/** Public port on the host DNATed to the box's SSH while it runs (#49); fixed for the box's life. Absent: none. */
|
|
1025
1051
|
sshPort: z4.number().int().optional(),
|
|
1052
|
+
/**
|
|
1053
|
+
* Box ports published on the host (#30, `kvbox vm proxy`): unique per guest port and protocol,
|
|
1054
|
+
* each host port the box's for its life. DNATed while the box runs, rebuilt from here with its
|
|
1055
|
+
* current IP every time it comes up. Absent: none.
|
|
1056
|
+
*/
|
|
1057
|
+
ports: z4.array(z4.object({ guestPort: z4.number().int(), hostPort: z4.number().int(), proto: z4.enum(["tcp", "udp"]) })).optional(),
|
|
1026
1058
|
/** Seconds without traffic on the box's tap before the server hibernates it (#47, core/idle.ts). Absent: never. */
|
|
1027
1059
|
autoHibernateSec: z4.number().int().positive().optional()
|
|
1028
1060
|
});
|
|
@@ -1292,8 +1324,13 @@ function usageEventsInScope(scope, window, p = paths()) {
|
|
|
1292
1324
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1293
1325
|
var tails = /* @__PURE__ */ new Map();
|
|
1294
1326
|
var held = new AsyncLocalStorage();
|
|
1295
|
-
|
|
1296
|
-
|
|
1327
|
+
function withBoxLock(ref, fn) {
|
|
1328
|
+
return withLock(refKey(ref), fn);
|
|
1329
|
+
}
|
|
1330
|
+
function withHostLock(name, fn) {
|
|
1331
|
+
return withLock(`host:${name}`, fn);
|
|
1332
|
+
}
|
|
1333
|
+
async function withLock(key, fn) {
|
|
1297
1334
|
const mine = held.getStore();
|
|
1298
1335
|
if (mine?.has(key)) return fn();
|
|
1299
1336
|
const previous = tails.get(key);
|
|
@@ -1502,11 +1539,14 @@ function parseListeningPorts(ssOutput) {
|
|
|
1502
1539
|
return ports;
|
|
1503
1540
|
}
|
|
1504
1541
|
async function listeningPorts() {
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1542
|
+
const ports = /* @__PURE__ */ new Set();
|
|
1543
|
+
for (const flags of ["-ltnH", "-lunH"]) {
|
|
1544
|
+
try {
|
|
1545
|
+
for (const port of parseListeningPorts((await run(["ss", flags])).stdout)) ports.add(port);
|
|
1546
|
+
} catch {
|
|
1547
|
+
}
|
|
1509
1548
|
}
|
|
1549
|
+
return ports;
|
|
1510
1550
|
}
|
|
1511
1551
|
var proxyArgs = (p) => [p.box, p.boxIp, String(p.hostPort), String(p.guestPort), p.proto ?? "tcp"];
|
|
1512
1552
|
async function openProxy(proxy, cfg) {
|
|
@@ -1541,23 +1581,34 @@ var AUTO_PREFIX = "box";
|
|
|
1541
1581
|
var GUEST_CID = 3;
|
|
1542
1582
|
var OVERLAY_REL = "overlay.ext4";
|
|
1543
1583
|
var idsInUse = (metas) => new Set(metas.map((m) => m.id));
|
|
1544
|
-
var
|
|
1584
|
+
var publicPortsInUse = (metas) => metas.flatMap((m) => [...m.sshPort === void 0 ? [] : [m.sshPort], ...(m.ports ?? []).map((f) => f.hostPort)]);
|
|
1545
1585
|
var isCloudBox = (owner) => realmOf(owner) !== null;
|
|
1546
|
-
function
|
|
1547
|
-
|
|
1548
|
-
return { box: meta.id, boxIp: meta.ip, hostPort:
|
|
1549
|
-
}
|
|
1550
|
-
async function
|
|
1551
|
-
const proxy
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
async function
|
|
1560
|
-
return
|
|
1586
|
+
function boxProxies(meta) {
|
|
1587
|
+
const ssh = meta.sshPort === void 0 ? [] : [{ box: meta.id, boxIp: meta.ip, hostPort: meta.sshPort, guestPort: 22 }];
|
|
1588
|
+
return [...ssh, ...(meta.ports ?? []).map((f) => ({ box: meta.id, boxIp: meta.ip, hostPort: f.hostPort, guestPort: f.guestPort, proto: f.proto }))];
|
|
1589
|
+
}
|
|
1590
|
+
async function openPorts(meta, cfg) {
|
|
1591
|
+
for (const proxy of boxProxies(meta)) await openProxy(proxy, cfg);
|
|
1592
|
+
}
|
|
1593
|
+
async function closePorts(meta, cfg) {
|
|
1594
|
+
for (const proxy of boxProxies(meta)) {
|
|
1595
|
+
await closeProxy(proxy, cfg).catch((err) => console.error(`warning: could not close port ${proxy.hostPort}: ${err.message}`));
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
var reservedPorts = /* @__PURE__ */ new Set();
|
|
1599
|
+
async function drawPorts(count, p) {
|
|
1600
|
+
return withHostLock("ports", async () => {
|
|
1601
|
+
const reserved = [...reservedPorts];
|
|
1602
|
+
const taken = /* @__PURE__ */ new Set([...reserved, ...publicPortsInUse(await readAllMeta(p)), ...await listeningPorts()]);
|
|
1603
|
+
const ports = [];
|
|
1604
|
+
for (let i = 0; i < count; i++) {
|
|
1605
|
+
const port = pickSshPort(taken);
|
|
1606
|
+
taken.add(port);
|
|
1607
|
+
ports.push(port);
|
|
1608
|
+
}
|
|
1609
|
+
for (const port of ports) reservedPorts.add(port);
|
|
1610
|
+
return { ports, release: () => ports.forEach((port) => reservedPorts.delete(port)) };
|
|
1611
|
+
});
|
|
1561
1612
|
}
|
|
1562
1613
|
function diskStats(disk) {
|
|
1563
1614
|
try {
|
|
@@ -1615,6 +1666,7 @@ function toMachine(meta, p = paths()) {
|
|
|
1615
1666
|
source: meta.source,
|
|
1616
1667
|
status: statusOf(meta, p),
|
|
1617
1668
|
...meta.sshPort === void 0 ? {} : { sshPort: meta.sshPort },
|
|
1669
|
+
...meta.ports?.length ? { ports: meta.ports.map((f) => ({ ...f })) } : {},
|
|
1618
1670
|
...meta.autoHibernateSec ? { autoHibernateSec: meta.autoHibernateSec } : {}
|
|
1619
1671
|
};
|
|
1620
1672
|
}
|
|
@@ -1828,7 +1880,6 @@ async function createMachine(req, owner, p = paths()) {
|
|
|
1828
1880
|
const id = newBoxId(idsInUse(existing));
|
|
1829
1881
|
const ip = nextFreeIp(cfg.subnet, [...existing.map((m2) => m2.ip), cfg.gateway]);
|
|
1830
1882
|
const mac = macFor(ip);
|
|
1831
|
-
const sshPort = cloud ? await newSshPort(existing) : void 0;
|
|
1832
1883
|
const m = p.machine(owner, name);
|
|
1833
1884
|
const base = await currentBase(cfg, p);
|
|
1834
1885
|
await assertOverlayCapable(base.file, base.version);
|
|
@@ -1836,23 +1887,29 @@ async function createMachine(req, owner, p = paths()) {
|
|
|
1836
1887
|
await mkdir3(m.dir, { recursive: true, mode: 448 });
|
|
1837
1888
|
let tapCreated = false;
|
|
1838
1889
|
let pid;
|
|
1890
|
+
let sshPort;
|
|
1891
|
+
let drawn;
|
|
1839
1892
|
try {
|
|
1893
|
+
drawn = cloud ? await drawPorts(1, p) : void 0;
|
|
1894
|
+
sshPort = drawn?.ports[0];
|
|
1840
1895
|
await createOverlay(m.overlay, size.diskGib);
|
|
1841
1896
|
const tap = await createTap(id, cfg);
|
|
1842
1897
|
tapCreated = true;
|
|
1843
1898
|
const spec = { id, name, owner, ip, mac, vcpu: size.vcpu, memMib: size.memMib, base: base.version, sshKeys, sshPort };
|
|
1844
1899
|
pid = await launch(spec, tap, cfg, keysB64, p);
|
|
1845
|
-
await
|
|
1900
|
+
await openPorts(spec, cfg);
|
|
1846
1901
|
const meta = { ...spec, pid, bootId: currentBootId(), createdAt: (/* @__PURE__ */ new Date()).toISOString(), source: "base", autoHibernateSec: autoHibernateFor(req, cfg) };
|
|
1847
1902
|
await writeMachineMeta(meta, p);
|
|
1848
1903
|
recordState(meta, "running", "new", p, meta.createdAt);
|
|
1849
1904
|
return toMachine(meta, p);
|
|
1850
1905
|
} catch (err) {
|
|
1851
1906
|
if (pid !== void 0) killQuietly(pid);
|
|
1852
|
-
await
|
|
1907
|
+
await closePorts({ id, ip, sshPort }, cfg);
|
|
1853
1908
|
if (tapCreated) await deleteTap(id, cfg).catch(() => void 0);
|
|
1854
1909
|
await rm(m.dir, { recursive: true, force: true });
|
|
1855
1910
|
throw err;
|
|
1911
|
+
} finally {
|
|
1912
|
+
drawn?.release();
|
|
1856
1913
|
}
|
|
1857
1914
|
}
|
|
1858
1915
|
async function readSnap(ref, p) {
|
|
@@ -1885,14 +1942,17 @@ async function restoreMachine(fromText, owner, req, cfg, p) {
|
|
|
1885
1942
|
const id = newBoxId(idsInUse(existing));
|
|
1886
1943
|
const ip = nextFreeIp(cfg.subnet, [...existing.map((m2) => m2.ip), cfg.gateway]);
|
|
1887
1944
|
const mac = macFor(ip);
|
|
1888
|
-
const sshPort = cloud ? await newSshPort(existing) : void 0;
|
|
1889
1945
|
const m = p.machine(owner, name);
|
|
1890
1946
|
const s = p.snapshot(from.owner, from.name);
|
|
1891
1947
|
await ensureHostNetwork(cfg);
|
|
1892
1948
|
await mkdir3(m.dir, { recursive: true, mode: 448 });
|
|
1893
1949
|
let tapCreated = false;
|
|
1894
1950
|
let pid;
|
|
1951
|
+
let sshPort;
|
|
1952
|
+
let drawn;
|
|
1895
1953
|
try {
|
|
1954
|
+
drawn = cloud ? await drawPorts(1, p) : void 0;
|
|
1955
|
+
sshPort = drawn?.ports[0];
|
|
1896
1956
|
await run(["cp", "--sparse=always", "--reflink=auto", s.overlay, m.overlay]);
|
|
1897
1957
|
await link(s.mem, m.memBase);
|
|
1898
1958
|
const tap = await createTap(id, cfg, { detached: true });
|
|
@@ -1906,21 +1966,23 @@ async function restoreMachine(fromText, owner, req, cfg, p) {
|
|
|
1906
1966
|
});
|
|
1907
1967
|
const stale = await pushAuthorizedKeys({ owner, name, sshKeys }, m.vsock, p);
|
|
1908
1968
|
await attachTap(id, cfg);
|
|
1909
|
-
await
|
|
1969
|
+
await openPorts(spec, cfg);
|
|
1910
1970
|
const meta = { ...spec, pid, bootId: currentBootId(), createdAt: (/* @__PURE__ */ new Date()).toISOString(), source: `snapshot:${refKey(from)}`, memSnapshots: 1, sshKeysStale: stale ? true : void 0, autoHibernateSec: autoHibernateFor(req, cfg) };
|
|
1911
1971
|
await writeMachineMeta(meta, p);
|
|
1912
1972
|
recordState(meta, "running", "new", p, meta.createdAt);
|
|
1913
1973
|
return toMachine(meta, p);
|
|
1914
1974
|
} catch (err) {
|
|
1915
1975
|
if (pid !== void 0) killQuietly(pid);
|
|
1916
|
-
await
|
|
1976
|
+
await closePorts({ id, ip, sshPort }, cfg);
|
|
1917
1977
|
if (tapCreated) await deleteTap(id, cfg).catch(() => void 0);
|
|
1918
1978
|
await rm(m.dir, { recursive: true, force: true });
|
|
1919
1979
|
throw err;
|
|
1980
|
+
} finally {
|
|
1981
|
+
drawn?.release();
|
|
1920
1982
|
}
|
|
1921
1983
|
}
|
|
1922
1984
|
var AGENT_REPLY_MS = 2e4;
|
|
1923
|
-
async function introduce(spec, vsock, cfg,
|
|
1985
|
+
async function introduce(spec, vsock, cfg, failure2) {
|
|
1924
1986
|
const id = { name: spec.name, ip: spec.ip, prefixLen: prefixLenOf(cfg.subnet), gateway: cfg.gateway, mac: spec.mac, time: Math.floor(Date.now() / 1e3) };
|
|
1925
1987
|
const deadline = Date.now() + 3e3;
|
|
1926
1988
|
for (; ; ) {
|
|
@@ -1935,9 +1997,9 @@ async function introduce(spec, vsock, cfg, failure) {
|
|
|
1935
1997
|
continue;
|
|
1936
1998
|
}
|
|
1937
1999
|
if (transient) throw new KvboxError(`${msg}
|
|
1938
|
-
${
|
|
2000
|
+
${failure2.silent}`);
|
|
1939
2001
|
throw new KvboxError(`${msg}
|
|
1940
|
-
${
|
|
2002
|
+
${failure2.noAnswer}`);
|
|
1941
2003
|
}
|
|
1942
2004
|
}
|
|
1943
2005
|
}
|
|
@@ -1982,7 +2044,7 @@ async function waitDead(pid, graceMs) {
|
|
|
1982
2044
|
}
|
|
1983
2045
|
}
|
|
1984
2046
|
async function cleanupProcess(meta, cfg, p) {
|
|
1985
|
-
await
|
|
2047
|
+
await closePorts(meta, cfg);
|
|
1986
2048
|
await deleteTap(meta.id, cfg);
|
|
1987
2049
|
const m = p.machine(meta.owner, meta.name);
|
|
1988
2050
|
await rm(m.sock, { force: true });
|
|
@@ -2015,14 +2077,14 @@ var startMachine = locked(async function startMachine2(ref, p = paths()) {
|
|
|
2015
2077
|
let pid;
|
|
2016
2078
|
try {
|
|
2017
2079
|
pid = await launch(meta, tap, cfg, keysB64, p);
|
|
2018
|
-
await
|
|
2080
|
+
await openPorts(meta, cfg);
|
|
2019
2081
|
const started = { ...meta, pid, bootId: currentBootId(), sshKeysStale: void 0 };
|
|
2020
2082
|
await writeMachineMeta(started, p);
|
|
2021
2083
|
recordState(started, "running", "start", p);
|
|
2022
2084
|
return toMachine(started, p);
|
|
2023
2085
|
} catch (err) {
|
|
2024
2086
|
if (pid !== void 0) killQuietly(pid);
|
|
2025
|
-
await
|
|
2087
|
+
await closePorts(meta, cfg);
|
|
2026
2088
|
await deleteTap(meta.id, cfg).catch(() => void 0);
|
|
2027
2089
|
throw err;
|
|
2028
2090
|
}
|
|
@@ -2163,10 +2225,10 @@ var wakeMachine = locked(async function wakeMachine2(ref, p = paths()) {
|
|
|
2163
2225
|
});
|
|
2164
2226
|
stale = await pushAuthorizedKeys(meta, m.vsock, p);
|
|
2165
2227
|
await attachTap(meta.id, cfg);
|
|
2166
|
-
await
|
|
2228
|
+
await openPorts(meta, cfg);
|
|
2167
2229
|
} catch (err) {
|
|
2168
2230
|
if (pid !== void 0) killQuietly(pid);
|
|
2169
|
-
await
|
|
2231
|
+
await closePorts(meta, cfg);
|
|
2170
2232
|
await deleteTap(meta.id, cfg).catch(() => void 0);
|
|
2171
2233
|
throw err;
|
|
2172
2234
|
}
|
|
@@ -2234,6 +2296,58 @@ var setMachine = locked(async function setMachine2(ref, req, p = paths()) {
|
|
|
2234
2296
|
await writeMachineMeta(next, p);
|
|
2235
2297
|
return toMachine(next, p);
|
|
2236
2298
|
});
|
|
2299
|
+
var portKeys = (req) => {
|
|
2300
|
+
const keys = req.ports.map((x) => ({ guestPort: x.guestPort, proto: x.proto ?? "tcp" }));
|
|
2301
|
+
return keys.filter((k, i) => keys.findIndex((o) => o.guestPort === k.guestPort && o.proto === k.proto) === i);
|
|
2302
|
+
};
|
|
2303
|
+
var samePort = (a) => (b) => a.guestPort === b.guestPort && a.proto === b.proto;
|
|
2304
|
+
var addPorts = locked(async function addPorts2(ref, req, p = paths()) {
|
|
2305
|
+
const meta = await readMeta(ref, p);
|
|
2306
|
+
const cfg = await loadConfig(p);
|
|
2307
|
+
const current = meta.ports ?? [];
|
|
2308
|
+
const wanted = portKeys(req);
|
|
2309
|
+
const already = wanted.find((w) => current.some(samePort(w)));
|
|
2310
|
+
if (already) throw new KvboxError(`box ${shown(ref)} already publishes port ${already.guestPort} (${already.proto})`);
|
|
2311
|
+
if (meta.sshPort !== void 0 && wanted.some(samePort({ guestPort: 22, proto: "tcp" }))) {
|
|
2312
|
+
throw new KvboxError(`box ${shown(ref)} already reaches port 22 from outside on port ${meta.sshPort} (kvbox connect)`);
|
|
2313
|
+
}
|
|
2314
|
+
if (current.length + wanted.length > MAX_BOX_PORTS) {
|
|
2315
|
+
throw new KvboxError(`a box publishes at most ${MAX_BOX_PORTS} ports; box ${shown(ref)} has ${current.length}`);
|
|
2316
|
+
}
|
|
2317
|
+
const drawn = await drawPorts(wanted.length, p);
|
|
2318
|
+
const opened = [];
|
|
2319
|
+
try {
|
|
2320
|
+
const added = wanted.map((w, i) => ({ ...w, hostPort: drawn.ports[i] }));
|
|
2321
|
+
if (vmAlive(meta)) {
|
|
2322
|
+
for (const proxy of boxProxies({ id: meta.id, ip: meta.ip, ports: added })) {
|
|
2323
|
+
await openProxy(proxy, cfg);
|
|
2324
|
+
opened.push(proxy);
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
const next = { ...meta, ports: [...current, ...added] };
|
|
2328
|
+
await writeMachineMeta(next, p);
|
|
2329
|
+
return toMachine(next, p);
|
|
2330
|
+
} catch (err) {
|
|
2331
|
+
for (const proxy of opened) await closeProxy(proxy, cfg).catch(() => void 0);
|
|
2332
|
+
throw err;
|
|
2333
|
+
} finally {
|
|
2334
|
+
drawn.release();
|
|
2335
|
+
}
|
|
2336
|
+
});
|
|
2337
|
+
var removePorts = locked(async function removePorts2(ref, req, p = paths()) {
|
|
2338
|
+
const meta = await readMeta(ref, p);
|
|
2339
|
+
const cfg = await loadConfig(p);
|
|
2340
|
+
const current = meta.ports ?? [];
|
|
2341
|
+
const gone = portKeys(req);
|
|
2342
|
+
const missing = gone.find((g) => !current.some(samePort(g)));
|
|
2343
|
+
if (missing) throw new KvboxError(`box ${shown(ref)} does not publish port ${missing.guestPort} (${missing.proto})`);
|
|
2344
|
+
const removed = current.filter((f) => gone.some(samePort(f)));
|
|
2345
|
+
for (const proxy of boxProxies({ id: meta.id, ip: meta.ip, ports: removed })) await closeProxy(proxy, cfg);
|
|
2346
|
+
const kept = current.filter((f) => !gone.some(samePort(f)));
|
|
2347
|
+
const next = { ...meta, ports: kept.length > 0 ? kept : void 0 };
|
|
2348
|
+
await writeMachineMeta(next, p);
|
|
2349
|
+
return toMachine(next, p);
|
|
2350
|
+
});
|
|
2237
2351
|
async function growDisk(file, gib) {
|
|
2238
2352
|
await truncate(file, gib * 1024 ** 3);
|
|
2239
2353
|
await run(["e2fsck", "-f", "-p", file], { okExitCodes: [1] });
|
|
@@ -2556,7 +2670,7 @@ import { createServer as createHttpsServer } from "node:https";
|
|
|
2556
2670
|
import { createAdaptorServer } from "@hono/node-server";
|
|
2557
2671
|
|
|
2558
2672
|
// src/app.ts
|
|
2559
|
-
import { Hono as
|
|
2673
|
+
import { Hono as Hono7 } from "hono";
|
|
2560
2674
|
|
|
2561
2675
|
// ../shared/src/version.ts
|
|
2562
2676
|
import { createRequire } from "node:module";
|
|
@@ -2571,10 +2685,25 @@ function versionOf(srcFileUrl, injected) {
|
|
|
2571
2685
|
}
|
|
2572
2686
|
|
|
2573
2687
|
// src/version.ts
|
|
2574
|
-
var VERSION = versionOf(import.meta.url, true ? "0.5.
|
|
2688
|
+
var VERSION = versionOf(import.meta.url, true ? "0.5.14" : void 0);
|
|
2575
2689
|
|
|
2576
2690
|
// src/caller.ts
|
|
2577
2691
|
import { createHash as createHash2, timingSafeEqual } from "node:crypto";
|
|
2692
|
+
|
|
2693
|
+
// ../shared/src/names.ts
|
|
2694
|
+
import { createHmac } from "node:crypto";
|
|
2695
|
+
var PROXY_ERROR_HEADER = "x-kvbox-proxy-error";
|
|
2696
|
+
var PROXIED_HEADER = "x-kvbox-proxied";
|
|
2697
|
+
var VISITOR_PATH_HEADER = "x-kvbox-path";
|
|
2698
|
+
var VISITOR_AUTH_HEADER = "x-kvbox-authorization";
|
|
2699
|
+
var NOT_FORWARDED = /* @__PURE__ */ new Set(["expect"]);
|
|
2700
|
+
var HOP_BY_HOP = /* @__PURE__ */ new Set(["connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "proxy-connection", "te", "trailer", "transfer-encoding", "upgrade"]);
|
|
2701
|
+
var BOX_HTTP_PATH_RE = /^\/machines\/([^/]+)\/http\/(\d{1,5})\/?$/;
|
|
2702
|
+
function edgeCredential(hostSecret) {
|
|
2703
|
+
return createHmac("sha256", hostSecret).update("kvbox-edge-v1").digest("base64url");
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
// src/caller.ts
|
|
2578
2707
|
var OWNER_HEADER = "x-kvbox-owner";
|
|
2579
2708
|
function resolveCaller(opts = {}) {
|
|
2580
2709
|
const cp = opts.controlPlane;
|
|
@@ -2586,10 +2715,11 @@ function resolveCaller(opts = {}) {
|
|
|
2586
2715
|
await next();
|
|
2587
2716
|
};
|
|
2588
2717
|
}
|
|
2589
|
-
const
|
|
2718
|
+
const authorized = secretCheck(cp);
|
|
2719
|
+
const edge = edgeCheck(cp);
|
|
2590
2720
|
return async (c, next) => {
|
|
2591
|
-
const
|
|
2592
|
-
if (!
|
|
2721
|
+
const auth = c.req.header("authorization");
|
|
2722
|
+
if (!authorized(auth) && !(edge(auth) && BOX_HTTP_PATH_RE.test(c.req.path))) return c.json({ error: "not authorized" }, 401);
|
|
2593
2723
|
const user = c.req.header(OWNER_HEADER);
|
|
2594
2724
|
if (user === void 0) {
|
|
2595
2725
|
const path = c.req.path.replace(/\/$/, "");
|
|
@@ -2608,20 +2738,237 @@ function resolveCaller(opts = {}) {
|
|
|
2608
2738
|
await next();
|
|
2609
2739
|
};
|
|
2610
2740
|
}
|
|
2741
|
+
function secretCheck(cp) {
|
|
2742
|
+
return bearerCheck(cp.secret);
|
|
2743
|
+
}
|
|
2744
|
+
function edgeCheck(cp) {
|
|
2745
|
+
return bearerCheck(edgeCredential(cp.secret));
|
|
2746
|
+
}
|
|
2747
|
+
function bearerCheck(token) {
|
|
2748
|
+
const expected = digest(token);
|
|
2749
|
+
return (authorization) => {
|
|
2750
|
+
const m = /^Bearer\s+(\S+)$/i.exec(authorization ?? "");
|
|
2751
|
+
return m !== null && timingSafeEqual(digest(m[1]), expected);
|
|
2752
|
+
};
|
|
2753
|
+
}
|
|
2611
2754
|
function digest(secret) {
|
|
2612
2755
|
return createHash2("sha256").update(secret).digest();
|
|
2613
2756
|
}
|
|
2614
2757
|
|
|
2615
|
-
// src/routes/
|
|
2758
|
+
// src/routes/boxHttp.ts
|
|
2759
|
+
import { connect as connect2 } from "node:net";
|
|
2760
|
+
import { Readable } from "node:stream";
|
|
2616
2761
|
import { Hono } from "hono";
|
|
2617
|
-
|
|
2762
|
+
import { request as request3 } from "undici";
|
|
2763
|
+
|
|
2764
|
+
// ../shared/src/httpHead.ts
|
|
2765
|
+
var END = "\r\n\r\n";
|
|
2766
|
+
var HeadTimeoutError = class extends Error {
|
|
2767
|
+
};
|
|
2768
|
+
function readHead(socket, opts) {
|
|
2769
|
+
return new Promise((resolve, reject) => {
|
|
2770
|
+
let bytes = Buffer.alloc(0);
|
|
2771
|
+
const done = (err) => {
|
|
2772
|
+
clearTimeout(timer);
|
|
2773
|
+
socket.off("data", onData);
|
|
2774
|
+
socket.off("close", onClose);
|
|
2775
|
+
socket.pause();
|
|
2776
|
+
if (err) return reject(err);
|
|
2777
|
+
const end = bytes.indexOf(END);
|
|
2778
|
+
resolve({ head: bytes.subarray(0, end).toString("latin1"), rest: bytes.subarray(end + END.length) });
|
|
2779
|
+
};
|
|
2780
|
+
const onData = (chunk) => {
|
|
2781
|
+
bytes = Buffer.concat([bytes, chunk]);
|
|
2782
|
+
if (bytes.includes(END)) done();
|
|
2783
|
+
else if (bytes.length > opts.maxBytes) done(new Error("answer head too large"));
|
|
2784
|
+
};
|
|
2785
|
+
const onClose = () => done(new Error("closed before answering"));
|
|
2786
|
+
const timer = setTimeout(() => done(new HeadTimeoutError(`no answer within ${opts.timeoutMs / 1e3} s`)), opts.timeoutMs);
|
|
2787
|
+
socket.on("data", onData);
|
|
2788
|
+
socket.once("close", onClose);
|
|
2789
|
+
});
|
|
2790
|
+
}
|
|
2791
|
+
function rewriteHead(head, drop, add = {}) {
|
|
2792
|
+
const [status, ...lines] = head.split("\r\n");
|
|
2793
|
+
const kept = lines.filter((l) => !drop(l.slice(0, l.indexOf(":")).trim().toLowerCase()));
|
|
2794
|
+
const extra = Object.entries(add).map(([k, v]) => `${k}: ${v}`);
|
|
2795
|
+
return Buffer.from(`${[status, ...kept, ...extra].join("\r\n")}${END}`, "latin1");
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
// src/routes/boxHttp.ts
|
|
2799
|
+
var HEADERS_TIMEOUT_MS = 55e3;
|
|
2800
|
+
var MAX_HANDSHAKE_BYTES = 16 * 1024;
|
|
2801
|
+
var CONNECT_TIMEOUT_MS = 5e3;
|
|
2802
|
+
var DROPPED = /* @__PURE__ */ new Set([...HOP_BY_HOP, ...NOT_FORWARDED, "host", "authorization", OWNER_HEADER, VISITOR_AUTH_HEADER, VISITOR_PATH_HEADER]);
|
|
2803
|
+
var ProxyFailure = class extends Error {
|
|
2804
|
+
constructor(status, code, message) {
|
|
2805
|
+
super(message);
|
|
2806
|
+
this.status = status;
|
|
2807
|
+
this.code = code;
|
|
2808
|
+
}
|
|
2809
|
+
status;
|
|
2810
|
+
code;
|
|
2811
|
+
};
|
|
2812
|
+
function visitorPath(value) {
|
|
2813
|
+
const path = Array.isArray(value) ? value[0] : value;
|
|
2814
|
+
return path !== void 0 && path !== null && path.startsWith("/") && !/[\s]/.test(path) ? path : "/";
|
|
2815
|
+
}
|
|
2816
|
+
async function targetOf(url, owner, path) {
|
|
2817
|
+
const m = BOX_HTTP_PATH_RE.exec(url.pathname);
|
|
2818
|
+
const port = Number(m?.[2]);
|
|
2819
|
+
if (!m || port < 1 || port > 65535) throw new ProxyFailure(404, "not-found", `no route: ${url.pathname}`);
|
|
2820
|
+
const name = decodeURIComponent(m[1]);
|
|
2821
|
+
let box;
|
|
2822
|
+
try {
|
|
2823
|
+
box = await getMachine(parseRef(name, owner));
|
|
2824
|
+
} catch (err) {
|
|
2825
|
+
if (err instanceof NotFoundError) throw new ProxyFailure(404, "not-found", err.message);
|
|
2826
|
+
throw err;
|
|
2827
|
+
}
|
|
2828
|
+
if (box.status !== "running") throw new ProxyFailure(409, "not-running", `box ${name} is ${box.status}`);
|
|
2829
|
+
return { ip: box.ip, port, path };
|
|
2830
|
+
}
|
|
2831
|
+
function boxHeaders(incoming, target, upgrade = false) {
|
|
2832
|
+
const out = [];
|
|
2833
|
+
let host = `${target.ip}:${target.port}`;
|
|
2834
|
+
for (const [name, value] of incoming) {
|
|
2835
|
+
const key = name.toLowerCase();
|
|
2836
|
+
if (key === "x-forwarded-host") host = value;
|
|
2837
|
+
if (key === VISITOR_AUTH_HEADER) out.push(["authorization", value]);
|
|
2838
|
+
if (upgrade && (key === "upgrade" || key === "connection")) out.push([name, value]);
|
|
2839
|
+
else if (!DROPPED.has(key)) out.push([name, value]);
|
|
2840
|
+
}
|
|
2841
|
+
out.push(["host", host]);
|
|
2842
|
+
return out;
|
|
2843
|
+
}
|
|
2844
|
+
var failure = (err) => new Response(JSON.stringify({ error: err.message }), { status: err.status, headers: { "content-type": "application/json", [PROXY_ERROR_HEADER]: err.code } });
|
|
2845
|
+
function boxHttpRoutes() {
|
|
2618
2846
|
const r = new Hono();
|
|
2847
|
+
r.all("/:name/http/:port", forward);
|
|
2848
|
+
return r;
|
|
2849
|
+
}
|
|
2850
|
+
async function forward(c) {
|
|
2851
|
+
let target;
|
|
2852
|
+
try {
|
|
2853
|
+
target = await targetOf(new URL(c.req.url), c.get("owner"), visitorPath(c.req.header(VISITOR_PATH_HEADER)));
|
|
2854
|
+
} catch (err) {
|
|
2855
|
+
if (err instanceof ProxyFailure) return failure(err);
|
|
2856
|
+
throw err;
|
|
2857
|
+
}
|
|
2858
|
+
const raw = c.req.raw;
|
|
2859
|
+
const pairs = [];
|
|
2860
|
+
raw.headers.forEach((value, name) => pairs.push([name, value]));
|
|
2861
|
+
let res;
|
|
2862
|
+
try {
|
|
2863
|
+
res = await request3(`http://${target.ip}:${target.port}${target.path}`, {
|
|
2864
|
+
method: raw.method,
|
|
2865
|
+
headers: boxHeaders(pairs, target).flat(),
|
|
2866
|
+
body: raw.body === null ? null : Readable.fromWeb(raw.body),
|
|
2867
|
+
headersTimeout: HEADERS_TIMEOUT_MS,
|
|
2868
|
+
bodyTimeout: 0,
|
|
2869
|
+
signal: raw.signal
|
|
2870
|
+
});
|
|
2871
|
+
} catch (err) {
|
|
2872
|
+
const e = err;
|
|
2873
|
+
if (e.code === "UND_ERR_HEADERS_TIMEOUT") return failure(new ProxyFailure(504, "timeout", `box took over ${HEADERS_TIMEOUT_MS / 1e3} s to answer on port ${target.port}`));
|
|
2874
|
+
return failure(new ProxyFailure(502, "unreachable", `box answered nothing on port ${target.port} (${e.code ?? e.message})`));
|
|
2875
|
+
}
|
|
2876
|
+
const headers = new Headers();
|
|
2877
|
+
for (const [name, value] of Object.entries(res.headers)) {
|
|
2878
|
+
if (value === void 0 || HOP_BY_HOP.has(name) || name.startsWith("x-kvbox-")) continue;
|
|
2879
|
+
for (const v of Array.isArray(value) ? value : [value]) headers.append(name, v);
|
|
2880
|
+
}
|
|
2881
|
+
headers.set(PROXIED_HEADER, "1");
|
|
2882
|
+
const empty = res.statusCode === 204 || res.statusCode === 304 || raw.method === "HEAD";
|
|
2883
|
+
if (empty) res.body.resume();
|
|
2884
|
+
return new Response(empty ? null : Readable.toWeb(res.body), { status: res.statusCode, headers });
|
|
2885
|
+
}
|
|
2886
|
+
function refuse(socket, status, text, code = "refused") {
|
|
2887
|
+
const body = JSON.stringify({ error: text });
|
|
2888
|
+
socket.end(`HTTP/1.1 ${status} ${status === 401 ? "Unauthorized" : "Error"}\r
|
|
2889
|
+
content-type: application/json\r
|
|
2890
|
+
content-length: ${Buffer.byteLength(body)}\r
|
|
2891
|
+
${PROXY_ERROR_HEADER}: ${code}\r
|
|
2892
|
+
connection: close\r
|
|
2893
|
+
\r
|
|
2894
|
+
${body}`);
|
|
2895
|
+
}
|
|
2896
|
+
function headerPairs(raw) {
|
|
2897
|
+
const out = [];
|
|
2898
|
+
for (let i = 0; i + 1 < raw.length; i += 2) out.push([raw[i], raw[i + 1]]);
|
|
2899
|
+
return out;
|
|
2900
|
+
}
|
|
2901
|
+
function boxUpgradeHandler(cp, headersTimeoutMs = HEADERS_TIMEOUT_MS) {
|
|
2902
|
+
const full = secretCheck(cp);
|
|
2903
|
+
const edge = edgeCheck(cp);
|
|
2904
|
+
const authorized = (auth) => full(auth) || edge(auth);
|
|
2905
|
+
return (req, socket, head) => {
|
|
2906
|
+
socket.on("error", () => socket.destroy());
|
|
2907
|
+
void (async () => {
|
|
2908
|
+
const headers = req.headers;
|
|
2909
|
+
if (!authorized(headers.authorization)) return refuse(socket, 401, "not authorized");
|
|
2910
|
+
const user = headers[OWNER_HEADER];
|
|
2911
|
+
if (typeof user !== "string" || !LOCAL_OWNER_RE.test(user)) return refuse(socket, 400, "X-Kvbox-Owner header required");
|
|
2912
|
+
let target;
|
|
2913
|
+
try {
|
|
2914
|
+
target = await targetOf(new URL(req.url ?? "/", "https://host"), `${user}@${cp.name}`, visitorPath(headers[VISITOR_PATH_HEADER]));
|
|
2915
|
+
} catch (err) {
|
|
2916
|
+
if (err instanceof ProxyFailure) return refuse(socket, err.status, err.message, err.code);
|
|
2917
|
+
return refuse(socket, 500, err.message);
|
|
2918
|
+
}
|
|
2919
|
+
if (socket.destroyed) return;
|
|
2920
|
+
const box = connect2({ host: target.ip, port: target.port, timeout: CONNECT_TIMEOUT_MS });
|
|
2921
|
+
let connected = false;
|
|
2922
|
+
let piped = false;
|
|
2923
|
+
box.once("timeout", () => box.destroy(new Error("timed out")));
|
|
2924
|
+
box.once("error", (err) => {
|
|
2925
|
+
if (!connected && !socket.destroyed) refuse(socket, 502, `box answered nothing on port ${target.port} (${err.code ?? err.message})`, "unreachable");
|
|
2926
|
+
else if (piped) socket.destroy();
|
|
2927
|
+
});
|
|
2928
|
+
socket.once("close", () => box.destroy());
|
|
2929
|
+
box.once("close", () => {
|
|
2930
|
+
if (piped) socket.destroy();
|
|
2931
|
+
});
|
|
2932
|
+
box.once("connect", () => {
|
|
2933
|
+
connected = true;
|
|
2934
|
+
box.setTimeout(0);
|
|
2935
|
+
const lines = boxHeaders(headerPairs(req.rawHeaders), target, true).map(([k, v]) => `${k}: ${v}\r
|
|
2936
|
+
`);
|
|
2937
|
+
box.write(`${req.method ?? "GET"} ${target.path} HTTP/1.1\r
|
|
2938
|
+
${lines.join("")}\r
|
|
2939
|
+
`);
|
|
2940
|
+
if (head.length > 0) box.write(head);
|
|
2941
|
+
readHead(box, { maxBytes: MAX_HANDSHAKE_BYTES, timeoutMs: headersTimeoutMs }).then(
|
|
2942
|
+
(answer) => {
|
|
2943
|
+
if (socket.destroyed) return void box.destroy();
|
|
2944
|
+
socket.write(rewriteHead(answer.head, (name) => name.startsWith("x-kvbox-"), { [PROXIED_HEADER]: "1" }));
|
|
2945
|
+
if (answer.rest.length > 0) socket.write(answer.rest);
|
|
2946
|
+
piped = true;
|
|
2947
|
+
box.pipe(socket);
|
|
2948
|
+
socket.pipe(box);
|
|
2949
|
+
},
|
|
2950
|
+
(err) => {
|
|
2951
|
+
box.destroy();
|
|
2952
|
+
if (socket.destroyed) return;
|
|
2953
|
+
if (err instanceof HeadTimeoutError) refuse(socket, 504, `box took over ${headersTimeoutMs / 1e3} s to answer on port ${target.port}`, "timeout");
|
|
2954
|
+
else refuse(socket, 502, `box gave no answer on port ${target.port} (${err.message})`, "unreachable");
|
|
2955
|
+
}
|
|
2956
|
+
);
|
|
2957
|
+
});
|
|
2958
|
+
})();
|
|
2959
|
+
};
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
// src/routes/capacity.ts
|
|
2963
|
+
import { Hono as Hono2 } from "hono";
|
|
2964
|
+
function capacityRoutes() {
|
|
2965
|
+
const r = new Hono2();
|
|
2619
2966
|
r.get("/", async (c) => c.json(await capacity()));
|
|
2620
2967
|
return r;
|
|
2621
2968
|
}
|
|
2622
2969
|
|
|
2623
2970
|
// src/routes/machines.ts
|
|
2624
|
-
import { Hono as
|
|
2971
|
+
import { Hono as Hono3 } from "hono";
|
|
2625
2972
|
|
|
2626
2973
|
// src/core/stats.ts
|
|
2627
2974
|
var STATS_REPLY_MS = 3e3;
|
|
@@ -2654,7 +3001,7 @@ async function usageReport(scope, window, p = paths()) {
|
|
|
2654
3001
|
|
|
2655
3002
|
// src/routes/machines.ts
|
|
2656
3003
|
function machineRoutes() {
|
|
2657
|
-
const r = new
|
|
3004
|
+
const r = new Hono3();
|
|
2658
3005
|
r.get("/", async (c) => c.json(await listMachines(c.get("listScope"))));
|
|
2659
3006
|
r.post("/", async (c) => {
|
|
2660
3007
|
const body = CreateMachineRequest.parse(await c.req.json().catch(() => ({})));
|
|
@@ -2681,6 +3028,16 @@ function machineRoutes() {
|
|
|
2681
3028
|
const { machine, applied } = await setSshKeys(parseRef(c.req.param("name"), c.get("owner")), body.data.sshKeys);
|
|
2682
3029
|
return c.json({ ...machine, sshKeysApplied: applied });
|
|
2683
3030
|
});
|
|
3031
|
+
r.post("/:name/ports", async (c) => {
|
|
3032
|
+
const body = PortsRequest.safeParse(await c.req.json().catch(() => ({})));
|
|
3033
|
+
if (!body.success) throw new KvboxError("ports: expected a list of box ports");
|
|
3034
|
+
return c.json(await addPorts(parseRef(c.req.param("name"), c.get("owner")), body.data), 201);
|
|
3035
|
+
});
|
|
3036
|
+
r.delete("/:name/ports", async (c) => {
|
|
3037
|
+
const body = PortsRequest.safeParse(await c.req.json().catch(() => ({})));
|
|
3038
|
+
if (!body.success) throw new KvboxError("ports: expected a list of box ports");
|
|
3039
|
+
return c.json(await removePorts(parseRef(c.req.param("name"), c.get("owner")), body.data));
|
|
3040
|
+
});
|
|
2684
3041
|
r.get("/:name/usage", async (c) => {
|
|
2685
3042
|
const since = c.req.query("since");
|
|
2686
3043
|
let from;
|
|
@@ -2700,7 +3057,7 @@ function machineRoutes() {
|
|
|
2700
3057
|
}
|
|
2701
3058
|
|
|
2702
3059
|
// src/routes/snapshots.ts
|
|
2703
|
-
import { Hono as
|
|
3060
|
+
import { Hono as Hono4 } from "hono";
|
|
2704
3061
|
|
|
2705
3062
|
// src/core/snapshots.ts
|
|
2706
3063
|
import { existsSync as existsSync3 } from "node:fs";
|
|
@@ -2770,21 +3127,21 @@ async function snapshotBox(ref, req, owner, p) {
|
|
|
2770
3127
|
try {
|
|
2771
3128
|
const since = box.memSnapshots ?? 0;
|
|
2772
3129
|
const mode = since === 0 || existsSync3(m.memBase) ? "Diff" : "Full";
|
|
2773
|
-
let
|
|
3130
|
+
let failure2;
|
|
2774
3131
|
await fc.pause();
|
|
2775
3132
|
try {
|
|
2776
3133
|
await fc.snapshotCreate(s.vmstate, s.mem, mode);
|
|
2777
3134
|
lineageTouched = true;
|
|
2778
3135
|
await run(["cp", "--sparse=always", "--reflink=auto", m.overlay, s.overlay]);
|
|
2779
3136
|
} catch (err) {
|
|
2780
|
-
|
|
3137
|
+
failure2 = err;
|
|
2781
3138
|
} finally {
|
|
2782
3139
|
if (!box.paused) await fc.resume();
|
|
2783
3140
|
}
|
|
2784
3141
|
if (lineageTouched) {
|
|
2785
3142
|
await writeMachineMeta({ ...box, memSnapshots: since + 1 }, p);
|
|
2786
3143
|
}
|
|
2787
|
-
if (
|
|
3144
|
+
if (failure2 !== void 0) throw failure2;
|
|
2788
3145
|
if (mode === "Full") {
|
|
2789
3146
|
await run(["fallocate", "--dig-holes", s.mem]);
|
|
2790
3147
|
} else if (since > 0) {
|
|
@@ -2827,7 +3184,7 @@ async function deleteSnapshot(ref, p = paths()) {
|
|
|
2827
3184
|
|
|
2828
3185
|
// src/routes/snapshots.ts
|
|
2829
3186
|
function snapshotRoutes() {
|
|
2830
|
-
const r = new
|
|
3187
|
+
const r = new Hono4();
|
|
2831
3188
|
r.get("/", async (c) => c.json(await listSnapshots(c.get("listScope"))));
|
|
2832
3189
|
r.post("/", async (c) => {
|
|
2833
3190
|
const body = CreateSnapshotRequest.parse(await c.req.json());
|
|
@@ -2841,9 +3198,9 @@ function snapshotRoutes() {
|
|
|
2841
3198
|
}
|
|
2842
3199
|
|
|
2843
3200
|
// src/routes/ssh.ts
|
|
2844
|
-
import { Hono as
|
|
3201
|
+
import { Hono as Hono5 } from "hono";
|
|
2845
3202
|
function sshRoutes(opts = {}) {
|
|
2846
|
-
const r = new
|
|
3203
|
+
const r = new Hono5();
|
|
2847
3204
|
if (opts.controlPlane) {
|
|
2848
3205
|
r.all("/*", (c) => c.json({ error: "trusting a CA is done on the host itself (kvbox server trust-ca there); the control plane cannot ask a host to" }, 403));
|
|
2849
3206
|
return r;
|
|
@@ -2858,10 +3215,10 @@ function sshRoutes(opts = {}) {
|
|
|
2858
3215
|
}
|
|
2859
3216
|
|
|
2860
3217
|
// src/routes/usage.ts
|
|
2861
|
-
import { Hono as
|
|
3218
|
+
import { Hono as Hono6 } from "hono";
|
|
2862
3219
|
var PULL_LIMIT = 1e3;
|
|
2863
3220
|
function usageRoutes(opts = {}) {
|
|
2864
|
-
const r = new
|
|
3221
|
+
const r = new Hono6();
|
|
2865
3222
|
r.get("/", async (c) => {
|
|
2866
3223
|
let window;
|
|
2867
3224
|
try {
|
|
@@ -2888,7 +3245,7 @@ function whole(value, name) {
|
|
|
2888
3245
|
|
|
2889
3246
|
// src/app.ts
|
|
2890
3247
|
function createApp(opts = {}) {
|
|
2891
|
-
const app = new
|
|
3248
|
+
const app = new Hono7();
|
|
2892
3249
|
app.use(resolveCaller(opts));
|
|
2893
3250
|
app.get("/health", async (c) => {
|
|
2894
3251
|
const cfg = await loadConfig();
|
|
@@ -2904,6 +3261,7 @@ function createApp(opts = {}) {
|
|
|
2904
3261
|
recovery: lastRecovery()
|
|
2905
3262
|
});
|
|
2906
3263
|
});
|
|
3264
|
+
if (opts.controlPlane) app.route("/machines", boxHttpRoutes());
|
|
2907
3265
|
app.route("/machines", machineRoutes());
|
|
2908
3266
|
app.route("/snapshots", snapshotRoutes());
|
|
2909
3267
|
app.route("/capacity", capacityRoutes());
|
|
@@ -2949,6 +3307,7 @@ async function listen(opts = {}) {
|
|
|
2949
3307
|
createServer: createHttpsServer,
|
|
2950
3308
|
serverOptions: { key, cert }
|
|
2951
3309
|
});
|
|
3310
|
+
httpsServer.on("upgrade", boxUpgradeHandler({ name: cp.name, secret: cp.secret }));
|
|
2952
3311
|
const port = opts.httpsPort ?? cp.port;
|
|
2953
3312
|
await new Promise((resolve, reject) => {
|
|
2954
3313
|
httpsServer.once("error", reject);
|