@kvbox/server 0.5.15 → 0.5.16
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 +67 -21
- package/dist/kvbox-server.js.map +1 -1
- package/package.json +1 -1
package/dist/kvbox-server.js
CHANGED
|
@@ -2685,7 +2685,7 @@ function versionOf(srcFileUrl, injected) {
|
|
|
2685
2685
|
}
|
|
2686
2686
|
|
|
2687
2687
|
// src/version.ts
|
|
2688
|
-
var VERSION = versionOf(import.meta.url, true ? "0.5.
|
|
2688
|
+
var VERSION = versionOf(import.meta.url, true ? "0.5.16" : void 0);
|
|
2689
2689
|
|
|
2690
2690
|
// src/caller.ts
|
|
2691
2691
|
import { createHash as createHash2, timingSafeEqual } from "node:crypto";
|
|
@@ -2695,6 +2695,9 @@ import { createHmac } from "node:crypto";
|
|
|
2695
2695
|
var PROXY_ERROR_HEADER = "x-kvbox-proxy-error";
|
|
2696
2696
|
var PROXIED_HEADER = "x-kvbox-proxied";
|
|
2697
2697
|
var VISITOR_PATH_HEADER = "x-kvbox-path";
|
|
2698
|
+
var WAKE_HEADER = "x-kvbox-wake";
|
|
2699
|
+
var WAKE_WAIT_MS = 3e4;
|
|
2700
|
+
var WOKE_HEADER = "x-kvbox-woke";
|
|
2698
2701
|
var VISITOR_AUTH_HEADER = "x-kvbox-authorization";
|
|
2699
2702
|
var NOT_FORWARDED = /* @__PURE__ */ new Set(["expect"]);
|
|
2700
2703
|
var HOP_BY_HOP = /* @__PURE__ */ new Set(["connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "proxy-connection", "te", "trailer", "transfer-encoding", "upgrade"]);
|
|
@@ -2799,7 +2802,7 @@ function rewriteHead(head, drop, add = {}) {
|
|
|
2799
2802
|
var HEADERS_TIMEOUT_MS = 55e3;
|
|
2800
2803
|
var MAX_HANDSHAKE_BYTES = 16 * 1024;
|
|
2801
2804
|
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]);
|
|
2805
|
+
var DROPPED = /* @__PURE__ */ new Set([...HOP_BY_HOP, ...NOT_FORWARDED, "host", "authorization", OWNER_HEADER, VISITOR_AUTH_HEADER, VISITOR_PATH_HEADER, WAKE_HEADER]);
|
|
2803
2806
|
var ProxyFailure = class extends Error {
|
|
2804
2807
|
constructor(status, code, message) {
|
|
2805
2808
|
super(message);
|
|
@@ -2809,24 +2812,64 @@ var ProxyFailure = class extends Error {
|
|
|
2809
2812
|
status;
|
|
2810
2813
|
code;
|
|
2811
2814
|
};
|
|
2815
|
+
function sharedWaker(bring = defaultBring, capMs = WAKE_WAIT_MS) {
|
|
2816
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
2817
|
+
return async (ref, status) => {
|
|
2818
|
+
const key = `${ref.owner}/${ref.name}`;
|
|
2819
|
+
let wake = inFlight.get(key);
|
|
2820
|
+
if (!wake) {
|
|
2821
|
+
wake = bring(ref, status).finally(() => inFlight.delete(key));
|
|
2822
|
+
inFlight.set(key, wake);
|
|
2823
|
+
wake.catch(() => {
|
|
2824
|
+
});
|
|
2825
|
+
}
|
|
2826
|
+
let timer;
|
|
2827
|
+
const cap = new Promise((_, reject) => {
|
|
2828
|
+
timer = setTimeout(() => reject(new ProxyFailure(503, "waking", `box ${ref.name} is waking up`)), capMs);
|
|
2829
|
+
});
|
|
2830
|
+
try {
|
|
2831
|
+
await Promise.race([wake, cap]);
|
|
2832
|
+
} finally {
|
|
2833
|
+
clearTimeout(timer);
|
|
2834
|
+
}
|
|
2835
|
+
};
|
|
2836
|
+
}
|
|
2837
|
+
var defaultBring = async (ref, status) => {
|
|
2838
|
+
await (status === "hibernated" ? wakeMachine(ref) : resumeMachine(ref));
|
|
2839
|
+
};
|
|
2840
|
+
var wakeBox = sharedWaker();
|
|
2841
|
+
var notRunning = (status) => status === "hibernated" || status === "standby" ? status : "stopped";
|
|
2812
2842
|
function visitorPath(value) {
|
|
2813
2843
|
const path = Array.isArray(value) ? value[0] : value;
|
|
2814
2844
|
return path !== void 0 && path !== null && path.startsWith("/") && !/[\s]/.test(path) ? path : "/";
|
|
2815
2845
|
}
|
|
2816
|
-
async function targetOf(url, owner, path) {
|
|
2846
|
+
async function targetOf(url, owner, path, wake) {
|
|
2817
2847
|
const m = BOX_HTTP_PATH_RE.exec(url.pathname);
|
|
2818
2848
|
const port = Number(m?.[2]);
|
|
2819
2849
|
if (!m || port < 1 || port > 65535) throw new ProxyFailure(404, "not-found", `no route: ${url.pathname}`);
|
|
2820
2850
|
const name = decodeURIComponent(m[1]);
|
|
2851
|
+
const ref = parseRef(name, owner);
|
|
2821
2852
|
let box;
|
|
2822
2853
|
try {
|
|
2823
|
-
box = await getMachine(
|
|
2854
|
+
box = await getMachine(ref);
|
|
2824
2855
|
} catch (err) {
|
|
2825
2856
|
if (err instanceof NotFoundError) throw new ProxyFailure(404, "not-found", err.message);
|
|
2826
2857
|
throw err;
|
|
2827
2858
|
}
|
|
2828
|
-
|
|
2829
|
-
|
|
2859
|
+
let woke = false;
|
|
2860
|
+
if (box.status !== "running" && wake && (box.status === "hibernated" || box.status === "standby")) {
|
|
2861
|
+
try {
|
|
2862
|
+
await wake(ref, box.status);
|
|
2863
|
+
woke = true;
|
|
2864
|
+
} catch (err) {
|
|
2865
|
+
if (err instanceof ProxyFailure) throw err;
|
|
2866
|
+
box = await getMachine(ref);
|
|
2867
|
+
if (box.status !== "running") throw new ProxyFailure(502, "wake-failed", `box ${name} did not wake: ${err.message}`);
|
|
2868
|
+
}
|
|
2869
|
+
if (woke) box = await getMachine(ref);
|
|
2870
|
+
}
|
|
2871
|
+
if (box.status !== "running") throw new ProxyFailure(409, notRunning(box.status), `box ${name} is ${box.status}`);
|
|
2872
|
+
return { ip: box.ip, port, path, woke };
|
|
2830
2873
|
}
|
|
2831
2874
|
function boxHeaders(incoming, target, upgrade = false) {
|
|
2832
2875
|
const out = [];
|
|
@@ -2841,16 +2884,17 @@ function boxHeaders(incoming, target, upgrade = false) {
|
|
|
2841
2884
|
out.push(["host", host]);
|
|
2842
2885
|
return out;
|
|
2843
2886
|
}
|
|
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
|
-
|
|
2887
|
+
var failure = (err, woke = false) => new Response(JSON.stringify({ error: err.message }), { status: err.status, headers: { "content-type": "application/json", [PROXY_ERROR_HEADER]: err.code, ...woke ? { [WOKE_HEADER]: "1" } : {} } });
|
|
2888
|
+
var wants = (value) => value === "1";
|
|
2889
|
+
function boxHttpRoutes(wake = wakeBox) {
|
|
2846
2890
|
const r = new Hono();
|
|
2847
|
-
r.all("/:name/http/:port", forward);
|
|
2891
|
+
r.all("/:name/http/:port", (c) => forward(c, wake));
|
|
2848
2892
|
return r;
|
|
2849
2893
|
}
|
|
2850
|
-
async function forward(c) {
|
|
2894
|
+
async function forward(c, wake) {
|
|
2851
2895
|
let target;
|
|
2852
2896
|
try {
|
|
2853
|
-
target = await targetOf(new URL(c.req.url), c.get("owner"), visitorPath(c.req.header(VISITOR_PATH_HEADER)));
|
|
2897
|
+
target = await targetOf(new URL(c.req.url), c.get("owner"), visitorPath(c.req.header(VISITOR_PATH_HEADER)), wants(c.req.header(WAKE_HEADER)) ? wake : void 0);
|
|
2854
2898
|
} catch (err) {
|
|
2855
2899
|
if (err instanceof ProxyFailure) return failure(err);
|
|
2856
2900
|
throw err;
|
|
@@ -2870,8 +2914,8 @@ async function forward(c) {
|
|
|
2870
2914
|
});
|
|
2871
2915
|
} catch (err) {
|
|
2872
2916
|
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})`));
|
|
2917
|
+
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}`), target.woke);
|
|
2918
|
+
return failure(new ProxyFailure(502, "unreachable", `box answered nothing on port ${target.port} (${e.code ?? e.message})`), target.woke);
|
|
2875
2919
|
}
|
|
2876
2920
|
const headers = new Headers();
|
|
2877
2921
|
for (const [name, value] of Object.entries(res.headers)) {
|
|
@@ -2879,17 +2923,19 @@ async function forward(c) {
|
|
|
2879
2923
|
for (const v of Array.isArray(value) ? value : [value]) headers.append(name, v);
|
|
2880
2924
|
}
|
|
2881
2925
|
headers.set(PROXIED_HEADER, "1");
|
|
2926
|
+
if (target.woke) headers.set(WOKE_HEADER, "1");
|
|
2882
2927
|
const empty = res.statusCode === 204 || res.statusCode === 304 || raw.method === "HEAD";
|
|
2883
2928
|
if (empty) res.body.resume();
|
|
2884
2929
|
return new Response(empty ? null : Readable.toWeb(res.body), { status: res.statusCode, headers });
|
|
2885
2930
|
}
|
|
2886
|
-
function refuse(socket, status, text, code = "refused") {
|
|
2931
|
+
function refuse(socket, status, text, code = "refused", woke = false) {
|
|
2887
2932
|
const body = JSON.stringify({ error: text });
|
|
2888
2933
|
socket.end(`HTTP/1.1 ${status} ${status === 401 ? "Unauthorized" : "Error"}\r
|
|
2889
2934
|
content-type: application/json\r
|
|
2890
2935
|
content-length: ${Buffer.byteLength(body)}\r
|
|
2891
2936
|
${PROXY_ERROR_HEADER}: ${code}\r
|
|
2892
|
-
|
|
2937
|
+
${woke ? `${WOKE_HEADER}: 1\r
|
|
2938
|
+
` : ""}connection: close\r
|
|
2893
2939
|
\r
|
|
2894
2940
|
${body}`);
|
|
2895
2941
|
}
|
|
@@ -2898,7 +2944,7 @@ function headerPairs(raw) {
|
|
|
2898
2944
|
for (let i = 0; i + 1 < raw.length; i += 2) out.push([raw[i], raw[i + 1]]);
|
|
2899
2945
|
return out;
|
|
2900
2946
|
}
|
|
2901
|
-
function boxUpgradeHandler(cp, headersTimeoutMs = HEADERS_TIMEOUT_MS) {
|
|
2947
|
+
function boxUpgradeHandler(cp, headersTimeoutMs = HEADERS_TIMEOUT_MS, wake = wakeBox) {
|
|
2902
2948
|
const full = secretCheck(cp);
|
|
2903
2949
|
const edge = edgeCheck(cp);
|
|
2904
2950
|
const authorized = (auth) => full(auth) || edge(auth);
|
|
@@ -2911,7 +2957,7 @@ function boxUpgradeHandler(cp, headersTimeoutMs = HEADERS_TIMEOUT_MS) {
|
|
|
2911
2957
|
if (typeof user !== "string" || !LOCAL_OWNER_RE.test(user)) return refuse(socket, 400, "X-Kvbox-Owner header required");
|
|
2912
2958
|
let target;
|
|
2913
2959
|
try {
|
|
2914
|
-
target = await targetOf(new URL(req.url ?? "/", "https://host"), `${user}@${cp.name}`, visitorPath(headers[VISITOR_PATH_HEADER]));
|
|
2960
|
+
target = await targetOf(new URL(req.url ?? "/", "https://host"), `${user}@${cp.name}`, visitorPath(headers[VISITOR_PATH_HEADER]), wants(headers[WAKE_HEADER]) ? wake : void 0);
|
|
2915
2961
|
} catch (err) {
|
|
2916
2962
|
if (err instanceof ProxyFailure) return refuse(socket, err.status, err.message, err.code);
|
|
2917
2963
|
return refuse(socket, 500, err.message);
|
|
@@ -2922,7 +2968,7 @@ function boxUpgradeHandler(cp, headersTimeoutMs = HEADERS_TIMEOUT_MS) {
|
|
|
2922
2968
|
let piped = false;
|
|
2923
2969
|
box.once("timeout", () => box.destroy(new Error("timed out")));
|
|
2924
2970
|
box.once("error", (err) => {
|
|
2925
|
-
if (!connected && !socket.destroyed) refuse(socket, 502, `box answered nothing on port ${target.port} (${err.code ?? err.message})`, "unreachable");
|
|
2971
|
+
if (!connected && !socket.destroyed) refuse(socket, 502, `box answered nothing on port ${target.port} (${err.code ?? err.message})`, "unreachable", target.woke);
|
|
2926
2972
|
else if (piped) socket.destroy();
|
|
2927
2973
|
});
|
|
2928
2974
|
socket.once("close", () => box.destroy());
|
|
@@ -2941,7 +2987,7 @@ ${lines.join("")}\r
|
|
|
2941
2987
|
readHead(box, { maxBytes: MAX_HANDSHAKE_BYTES, timeoutMs: headersTimeoutMs }).then(
|
|
2942
2988
|
(answer) => {
|
|
2943
2989
|
if (socket.destroyed) return void box.destroy();
|
|
2944
|
-
socket.write(rewriteHead(answer.head, (name) => name.startsWith("x-kvbox-"), { [PROXIED_HEADER]: "1" }));
|
|
2990
|
+
socket.write(rewriteHead(answer.head, (name) => name.startsWith("x-kvbox-"), { [PROXIED_HEADER]: "1", ...target.woke ? { [WOKE_HEADER]: "1" } : {} }));
|
|
2945
2991
|
if (answer.rest.length > 0) socket.write(answer.rest);
|
|
2946
2992
|
piped = true;
|
|
2947
2993
|
box.pipe(socket);
|
|
@@ -2950,8 +2996,8 @@ ${lines.join("")}\r
|
|
|
2950
2996
|
(err) => {
|
|
2951
2997
|
box.destroy();
|
|
2952
2998
|
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");
|
|
2999
|
+
if (err instanceof HeadTimeoutError) refuse(socket, 504, `box took over ${headersTimeoutMs / 1e3} s to answer on port ${target.port}`, "timeout", target.woke);
|
|
3000
|
+
else refuse(socket, 502, `box gave no answer on port ${target.port} (${err.message})`, "unreachable", target.woke);
|
|
2955
3001
|
}
|
|
2956
3002
|
);
|
|
2957
3003
|
});
|