@velarscript/node 0.11.1 → 0.12.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 +102 -14
- package/dist/compiler.d.ts +3 -0
- package/dist/compiler.d.ts.map +1 -1
- package/dist/compiler.js +300 -76
- package/dist/compiler.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/node-host-runtime.d.ts.map +1 -1
- package/dist/node-host-runtime.js +30 -14
- package/dist/node-host-runtime.js.map +1 -1
- package/dist/node-host-worker-runtime.d.ts.map +1 -1
- package/dist/node-host-worker-runtime.js +495 -93
- package/dist/node-host-worker-runtime.js.map +1 -1
- package/dist/project-config.d.ts +17 -0
- package/dist/project-config.d.ts.map +1 -0
- package/dist/project-config.js +66 -0
- package/dist/project-config.js.map +1 -0
- package/dist/serve-runtime.d.ts.map +1 -1
- package/dist/serve-runtime.js +2942 -158
- package/dist/serve-runtime.js.map +1 -1
- package/dist/server-analyzer.d.ts +35 -0
- package/dist/server-analyzer.d.ts.map +1 -0
- package/dist/server-analyzer.js +692 -0
- package/dist/server-analyzer.js.map +1 -0
- package/dist/server-ast.d.ts +54 -0
- package/dist/server-ast.d.ts.map +1 -0
- package/dist/server-ast.js +16 -0
- package/dist/server-ast.js.map +1 -0
- package/dist/server-emitter.d.ts +17 -0
- package/dist/server-emitter.d.ts.map +1 -0
- package/dist/server-emitter.js +177 -0
- package/dist/server-emitter.js.map +1 -0
- package/dist/server-inspection.d.ts +3 -0
- package/dist/server-inspection.d.ts.map +1 -0
- package/dist/server-inspection.js +12 -0
- package/dist/server-inspection.js.map +1 -0
- package/dist/server-lexer.d.ts +13 -0
- package/dist/server-lexer.d.ts.map +1 -0
- package/dist/server-lexer.js +65 -0
- package/dist/server-lexer.js.map +1 -0
- package/dist/server-parser.d.ts +18 -0
- package/dist/server-parser.d.ts.map +1 -0
- package/dist/server-parser.js +232 -0
- package/dist/server-parser.js.map +1 -0
- package/dist/server-semantic.d.ts +3 -0
- package/dist/server-semantic.d.ts.map +1 -0
- package/dist/server-semantic.js +31 -0
- package/dist/server-semantic.js.map +1 -0
- package/dist/server-types.d.ts +25 -0
- package/dist/server-types.d.ts.map +1 -0
- package/dist/server-types.js +60 -0
- package/dist/server-types.js.map +1 -0
- package/dist/websocket-runtime.d.ts.map +1 -1
- package/dist/websocket-runtime.js +169 -15
- package/dist/websocket-runtime.js.map +1 -1
- package/package.json +10 -2
- package/dist/sqlite-runtime.d.ts +0 -3
- package/dist/sqlite-runtime.d.ts.map +0 -1
- package/dist/sqlite-runtime.js +0 -92
- package/dist/sqlite-runtime.js.map +0 -1
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
// application code and npm dependencies remain in the application Realm.
|
|
4
4
|
export const VELAR_NODE_HOST_WORKER_SOURCE = String.raw `
|
|
5
5
|
import { Buffer } from "node:buffer";
|
|
6
|
-
import { watch as watchNode } from "node:fs";
|
|
6
|
+
import { createReadStream, watch as watchNode } from "node:fs";
|
|
7
7
|
import { appendFile, copyFile, lstat, mkdir, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
8
8
|
import { createServer, request as createHttpRequest } from "node:http";
|
|
9
9
|
import { request as createHttpsRequest } from "node:https";
|
|
10
10
|
import { basename, dirname, extname, isAbsolute, relative, resolve } from "node:path";
|
|
11
11
|
import { URL as NodeURL } from "node:url";
|
|
12
|
+
import { brotliCompress as brotliCompressNode, gzip as gzipNode } from "node:zlib";
|
|
13
|
+
import { promisify } from "node:util";
|
|
12
14
|
import { workerData } from "node:worker_threads";
|
|
13
15
|
|
|
14
16
|
const port = workerData;
|
|
@@ -21,6 +23,16 @@ const maxServeFileBytes = 64 * 1024 * 1024;
|
|
|
21
23
|
const maxServeStreamBytes = 64 * 1024 * 1024;
|
|
22
24
|
const maxServeStreamChunkBytes = 1024 * 1024;
|
|
23
25
|
const maxServeAggregateBytes = 128 * 1024 * 1024;
|
|
26
|
+
const maxServeRequestTargetBytes = 64 * 1024;
|
|
27
|
+
const maxServeRequestMetadataBytes = 512 * 1024;
|
|
28
|
+
const maxServeQueryFields = 1000;
|
|
29
|
+
const maxServeHeaderTextBytes = 64 * 1024;
|
|
30
|
+
const maxServeSockets = 4096;
|
|
31
|
+
const maxServeSocketsPerServer = 2048;
|
|
32
|
+
const serveHeadersTimeoutMilliseconds = 10_000;
|
|
33
|
+
const serveRequestTimeoutMilliseconds = 60_000;
|
|
34
|
+
const serveKeepAliveTimeoutMilliseconds = 5_000;
|
|
35
|
+
const serveShutdownTimeoutMilliseconds = 30_000;
|
|
24
36
|
const maxHttpBodyBytes = 16 * 1024 * 1024;
|
|
25
37
|
const maxHttpResponseBytes = 64 * 1024 * 1024;
|
|
26
38
|
const maxHttpResponseChunks = 1000000;
|
|
@@ -35,7 +47,7 @@ const operations = new Set([
|
|
|
35
47
|
"fs.readFile", "fs.createFile", "fs.replaceFileIfMatches", "fs.writeFile", "fs.appendFile", "fs.exists", "fs.list", "fs.info",
|
|
36
48
|
"fs.canonical", "fs.makeDirectory", "fs.copyFile", "fs.move", "fs.removeFile", "fs.watchStart", "fs.watchNext", "fs.watchClose",
|
|
37
49
|
"http.request", "http.read", "http.readBytes", "http.cancel", "http.close",
|
|
38
|
-
"serve.start", "serve.stop", "serve.body", "serve.respond", "serve.respondFile",
|
|
50
|
+
"serve.start", "serve.stop", "serve.body", "serve.bodyBytes", "serve.readFile", "serve.respond", "serve.respondFile",
|
|
39
51
|
"serve.streamStart", "serve.streamWrite", "serve.streamEnd", "serve.fail",
|
|
40
52
|
]);
|
|
41
53
|
const servers = new Map();
|
|
@@ -48,6 +60,7 @@ let nextRequestHandle = 1;
|
|
|
48
60
|
let nextTextReplacementIdentity = 1;
|
|
49
61
|
let nextFileWatcherHandle = 1;
|
|
50
62
|
let reservedServeBytes = 0;
|
|
63
|
+
let activeServeSockets = 0;
|
|
51
64
|
const contentTypes = Object.freeze({
|
|
52
65
|
".css": "text/css; charset=utf-8", ".gif": "image/gif", ".html": "text/html; charset=utf-8",
|
|
53
66
|
".ico": "image/x-icon", ".jpeg": "image/jpeg", ".jpg": "image/jpeg", ".js": "text/javascript; charset=utf-8",
|
|
@@ -55,8 +68,11 @@ const contentTypes = Object.freeze({
|
|
|
55
68
|
".png": "image/png", ".svg": "image/svg+xml", ".txt": "text/plain; charset=utf-8", ".wasm": "application/wasm",
|
|
56
69
|
".webp": "image/webp", ".woff": "font/woff", ".woff2": "font/woff2",
|
|
57
70
|
});
|
|
71
|
+
const gzip = promisify(gzipNode);
|
|
72
|
+
const brotliCompress = promisify(brotliCompressNode);
|
|
58
73
|
|
|
59
74
|
class StaticNotFound extends Error {}
|
|
75
|
+
class RequestHeadersTooLarge extends RangeError {}
|
|
60
76
|
|
|
61
77
|
function allocateHandle(values, next, maximum, name) {
|
|
62
78
|
let candidate = next;
|
|
@@ -106,7 +122,15 @@ function cleanupRequest(task) {
|
|
|
106
122
|
releaseServeBytes(task);
|
|
107
123
|
}
|
|
108
124
|
|
|
109
|
-
function
|
|
125
|
+
function cancelRequest(task, reason = "client_disconnect") {
|
|
126
|
+
if (task.cancelled || task.completed) return;
|
|
127
|
+
task.cancelled = true;
|
|
128
|
+
try { port.postMessage({kind: "event", event: "serve.cancel", value: {token: task.token, request: task.handle, reason}}); }
|
|
129
|
+
catch {}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function closeRequest(task, cancelled = false) {
|
|
133
|
+
if (cancelled) cancelRequest(task);
|
|
110
134
|
task.transportDone = true;
|
|
111
135
|
cleanupRequest(task);
|
|
112
136
|
}
|
|
@@ -120,6 +144,24 @@ async function withRequest(task, action) {
|
|
|
120
144
|
}
|
|
121
145
|
}
|
|
122
146
|
|
|
147
|
+
async function withTerminalResponse(task, action) {
|
|
148
|
+
if (task.responseMode !== "idle") throw new Error("Node serve request already owns a response operation");
|
|
149
|
+
task.responseMode = "terminal";
|
|
150
|
+
try { return await withRequest(task, action); }
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (!task.completed && !task.transportDone && !task.response.headersSent) task.responseMode = "idle";
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function withStreamWrite(task, action) {
|
|
158
|
+
if (task.responseMode !== "streaming") throw new Error("ServeResponse stream has not started");
|
|
159
|
+
if (task.writeActive) throw new Error("ServeResponse allows only one active stream write");
|
|
160
|
+
task.writeActive = true;
|
|
161
|
+
try { return await withRequest(task, action); }
|
|
162
|
+
finally { task.writeActive = false; }
|
|
163
|
+
}
|
|
164
|
+
|
|
123
165
|
function completeRequest(task) {
|
|
124
166
|
if (task.completed) throw new Error("Node serve request is already completed");
|
|
125
167
|
task.completed = true;
|
|
@@ -650,14 +692,33 @@ function headerPairs(value) {
|
|
|
650
692
|
throw new TypeError("ServeResponse cannot set transport-owned header '" + item[0] + "'");
|
|
651
693
|
}
|
|
652
694
|
units += item[0].length + item[1].length;
|
|
653
|
-
if (units >
|
|
695
|
+
if (units > 64 * 1024) throw new RangeError("ServeResponse headers cannot exceed 64 KiB of text");
|
|
654
696
|
output.push(item);
|
|
655
697
|
}
|
|
656
698
|
return output;
|
|
657
699
|
}
|
|
658
700
|
|
|
659
|
-
function
|
|
660
|
-
|
|
701
|
+
function cookieValues(value) {
|
|
702
|
+
if (!Array.isArray(value) || value.length > 64) throw new TypeError("ServeResponse cookies must be a bounded list");
|
|
703
|
+
const output = [];
|
|
704
|
+
let bytes = 0;
|
|
705
|
+
for (const cookie of value) {
|
|
706
|
+
if (typeof cookie !== "string" || cookie.length === 0 || cookie.length > 8192 || /[\0\r\n]/u.test(cookie)) throw new TypeError("ServeResponse cookie is invalid");
|
|
707
|
+
bytes += Buffer.byteLength(cookie, "utf8");
|
|
708
|
+
if (bytes > 64 * 1024) throw new RangeError("ServeResponse cookies cannot exceed 64 KiB");
|
|
709
|
+
output.push(cookie);
|
|
710
|
+
}
|
|
711
|
+
return output;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function setHeaders(response, values, cookies = []) {
|
|
715
|
+
const setCookies = [];
|
|
716
|
+
for (const [name, value] of headerPairs(values)) {
|
|
717
|
+
if (name.toLowerCase() === "set-cookie") setCookies.push(value);
|
|
718
|
+
else response.setHeader(name, value);
|
|
719
|
+
}
|
|
720
|
+
for (const cookie of cookieValues(cookies)) setCookies.push(cookie);
|
|
721
|
+
if (setCookies.length > 0) response.setHeader("Set-Cookie", setCookies);
|
|
661
722
|
}
|
|
662
723
|
|
|
663
724
|
function requestPath(value) {
|
|
@@ -675,7 +736,7 @@ function inside(root, target) {
|
|
|
675
736
|
return path === "" || !path.startsWith("..") && !isAbsolute(path);
|
|
676
737
|
}
|
|
677
738
|
|
|
678
|
-
async function staticFile(
|
|
739
|
+
async function staticFile(rootValue, pathValue, fallbackValue) {
|
|
679
740
|
const root = await realpath(resolve(boundedPath(rootValue, "fileResponse")));
|
|
680
741
|
const relativePath = requestPath(pathValue);
|
|
681
742
|
const fallback = fallbackValue === null ? null : requestPath(fallbackValue);
|
|
@@ -684,19 +745,103 @@ async function staticFile(task, rootValue, pathValue, fallbackValue) {
|
|
|
684
745
|
if (!inside(root, target)) throw new StaticNotFound();
|
|
685
746
|
const metadata = await stat(target);
|
|
686
747
|
if (!metadata.isFile() || metadata.size > maxServeFileBytes) throw new StaticNotFound();
|
|
687
|
-
|
|
688
|
-
|
|
748
|
+
return {target, metadata, contentType: contentTypes[extname(target).toLowerCase()] ?? "application/octet-stream"};
|
|
749
|
+
};
|
|
750
|
+
try { return await load(relativePath); }
|
|
751
|
+
catch (error) {
|
|
752
|
+
if ((error instanceof StaticNotFound || missing(error) || error?.code === "EISDIR") && fallback !== null) return load(fallback);
|
|
753
|
+
throw error;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function staticEtag(metadata) {
|
|
758
|
+
const modified = Number.isFinite(metadata.mtimeMs) ? Math.floor(metadata.mtimeMs) : 0;
|
|
759
|
+
return 'W/"' + metadata.size.toString(16) + "-" + modified.toString(16) + '"';
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function staticNotModified(request, metadata, etag) {
|
|
763
|
+
const noneMatch = request.headers["if-none-match"];
|
|
764
|
+
if (typeof noneMatch === "string") {
|
|
765
|
+
for (const candidate of noneMatch.split(",")) if (candidate.trim() === "*" || candidate.trim() === etag) return true;
|
|
766
|
+
return false;
|
|
767
|
+
}
|
|
768
|
+
const modifiedSince = request.headers["if-modified-since"];
|
|
769
|
+
if (typeof modifiedSince !== "string") return false;
|
|
770
|
+
const time = Date.parse(modifiedSince);
|
|
771
|
+
return Number.isFinite(time) && Math.floor(metadata.mtimeMs / 1000) * 1000 <= time;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function staticRange(request, metadata, etag) {
|
|
775
|
+
const value = request.headers.range;
|
|
776
|
+
if (typeof value !== "string") return null;
|
|
777
|
+
const ifRange = request.headers["if-range"];
|
|
778
|
+
if (typeof ifRange === "string" && ifRange !== etag) {
|
|
779
|
+
const time = Date.parse(ifRange);
|
|
780
|
+
if (!Number.isFinite(time) || Math.floor(metadata.mtimeMs / 1000) * 1000 > time) return null;
|
|
781
|
+
}
|
|
782
|
+
const match = /^bytes=(\d*)-(\d*)$/u.exec(value.trim());
|
|
783
|
+
if (!match || match[1] === "" && match[2] === "" || metadata.size === 0) return false;
|
|
784
|
+
let start;
|
|
785
|
+
let end;
|
|
786
|
+
if (match[1] === "") {
|
|
787
|
+
const suffix = Number(match[2]);
|
|
788
|
+
if (!Number.isSafeInteger(suffix) || suffix < 1) return false;
|
|
789
|
+
start = Math.max(0, metadata.size - suffix);
|
|
790
|
+
end = metadata.size - 1;
|
|
791
|
+
} else {
|
|
792
|
+
start = Number(match[1]);
|
|
793
|
+
end = match[2] === "" ? metadata.size - 1 : Number(match[2]);
|
|
794
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || start >= metadata.size || end < start) return false;
|
|
795
|
+
if (end >= metadata.size) end = metadata.size - 1;
|
|
796
|
+
}
|
|
797
|
+
return {start, end};
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
async function writeStaticRange(task, file, start, end) {
|
|
801
|
+
if (end < start) return;
|
|
802
|
+
const source = createReadStream(file.target, {start, end, highWaterMark: 64 * 1024});
|
|
803
|
+
try {
|
|
804
|
+
for await (const chunk of source) {
|
|
805
|
+
const bytes = chunk.byteLength;
|
|
806
|
+
reserveTransientServeBytes(bytes);
|
|
807
|
+
try {
|
|
808
|
+
await new Promise((resolveWrite, rejectWrite) => {
|
|
809
|
+
let settled = false;
|
|
810
|
+
const cleanup = () => { task.response.off("error", failed); task.response.off("close", closed); };
|
|
811
|
+
const finish = action => { if (settled) return; settled = true; cleanup(); action(); };
|
|
812
|
+
const failed = error => finish(() => rejectWrite(error));
|
|
813
|
+
const closed = () => finish(() => rejectWrite(new Error("ServeResponse client connection is closed")));
|
|
814
|
+
task.response.once("error", failed);
|
|
815
|
+
task.response.once("close", closed);
|
|
816
|
+
task.response.write(chunk, error => error ? failed(error) : finish(resolveWrite));
|
|
817
|
+
});
|
|
818
|
+
} finally { releaseTransientServeBytes(bytes); }
|
|
819
|
+
}
|
|
820
|
+
} finally { source.destroy(); }
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
async function testStaticFile(rootValue, pathValue, fallbackValue) {
|
|
824
|
+
const root = await realpath(resolve(boundedPath(rootValue, "fileResponse")));
|
|
825
|
+
const relativePath = requestPath(pathValue);
|
|
826
|
+
const fallback = fallbackValue === null ? null : requestPath(fallbackValue);
|
|
827
|
+
const load = async path => {
|
|
828
|
+
const target = await realpath(resolve(root, path));
|
|
829
|
+
if (!inside(root, target)) throw new StaticNotFound();
|
|
830
|
+
const metadata = await stat(target);
|
|
831
|
+
if (!metadata.isFile() || metadata.size > maxServeBodyBytes) throw new StaticNotFound();
|
|
832
|
+
let reserved = metadata.size * 2;
|
|
833
|
+
reserveTransientServeBytes(reserved);
|
|
689
834
|
try {
|
|
690
|
-
const
|
|
691
|
-
if (
|
|
692
|
-
if (
|
|
693
|
-
else if (
|
|
694
|
-
|
|
835
|
+
const source = await readFile(target);
|
|
836
|
+
if (source.byteLength > maxServeBodyBytes) throw new StaticNotFound();
|
|
837
|
+
if (source.byteLength > metadata.size) { const extra = (source.byteLength - metadata.size) * 2; reserveTransientServeBytes(extra); reserved += extra; }
|
|
838
|
+
else if (source.byteLength < metadata.size) { const surplus = (metadata.size - source.byteLength) * 2; releaseTransientServeBytes(surplus); reserved -= surplus; }
|
|
839
|
+
const data = new Uint8Array(source.byteLength);
|
|
840
|
+
data.set(source);
|
|
841
|
+
releaseTransientServeBytes(source.byteLength);
|
|
842
|
+
reserved -= source.byteLength;
|
|
695
843
|
return {data, contentType: contentTypes[extname(target).toLowerCase()] ?? "application/octet-stream"};
|
|
696
|
-
} catch (error) {
|
|
697
|
-
releaseServeBytes(task, reserved);
|
|
698
|
-
throw error;
|
|
699
|
-
}
|
|
844
|
+
} catch (error) { releaseTransientServeBytes(reserved); throw error; }
|
|
700
845
|
};
|
|
701
846
|
try { return await load(relativePath); }
|
|
702
847
|
catch (error) {
|
|
@@ -710,37 +855,76 @@ function opaqueFailure(task) {
|
|
|
710
855
|
else {
|
|
711
856
|
task.response.statusCode = 500;
|
|
712
857
|
task.response.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
713
|
-
task.response.end("Internal server error");
|
|
858
|
+
task.response.end(task.request.method === "HEAD" ? undefined : "Internal server error");
|
|
714
859
|
}
|
|
715
860
|
completeRequest(task);
|
|
716
861
|
}
|
|
717
862
|
|
|
718
|
-
function
|
|
863
|
+
function responseHasNoBody(task, status) {
|
|
864
|
+
return task.request.method === "HEAD" || status >= 100 && status < 200 || status === 204 || status === 304;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
async function endServeResponse(task, value) {
|
|
868
|
+
await new Promise((resolveEnd, rejectEnd) => {
|
|
869
|
+
let settled = false;
|
|
870
|
+
const cleanup = () => {
|
|
871
|
+
task.response.off("error", failed);
|
|
872
|
+
task.response.off("close", closed);
|
|
873
|
+
};
|
|
874
|
+
const finish = action => { if (settled) return; settled = true; cleanup(); action(); };
|
|
875
|
+
const failed = error => finish(() => rejectEnd(error));
|
|
876
|
+
const closed = () => finish(() => task.response.writableFinished ? resolveEnd(null) : rejectEnd(new Error("ServeResponse client connection is closed")));
|
|
877
|
+
task.response.once("error", failed);
|
|
878
|
+
task.response.once("close", closed);
|
|
879
|
+
task.response.end(value, () => finish(() => resolveEnd(null)));
|
|
880
|
+
if (task.response.destroyed && !task.response.writableFinished) closed();
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function rawBodyOf(task, maximum) {
|
|
719
885
|
if (task.body !== null) return task.body;
|
|
720
886
|
task.body = (async () => {
|
|
721
887
|
const declaredText = task.request.headers["content-length"];
|
|
888
|
+
let declared = null;
|
|
722
889
|
if (typeof declaredText === "string" && /^[0-9]+$/u.test(declaredText)) {
|
|
723
|
-
|
|
724
|
-
if (!Number.isSafeInteger(declared) || declared >
|
|
890
|
+
declared = Number(declaredText);
|
|
891
|
+
if (!Number.isSafeInteger(declared) || declared > maximum) {
|
|
725
892
|
task.request.resume();
|
|
726
|
-
|
|
893
|
+
return {data: null, bytes: maximum, tooLarge: true};
|
|
727
894
|
}
|
|
728
895
|
}
|
|
896
|
+
if (declared !== null) {
|
|
897
|
+
const output = Buffer.allocUnsafe(declared);
|
|
898
|
+
reserveServeBytes(task, declared);
|
|
899
|
+
let offset = 0;
|
|
900
|
+
try {
|
|
901
|
+
for await (const chunk of task.request) {
|
|
902
|
+
const data = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
903
|
+
if (offset + data.byteLength > declared) { task.request.resume(); releaseServeBytes(task, declared); return {data: null, bytes: maximum, tooLarge: true}; }
|
|
904
|
+
data.copy(output, offset);
|
|
905
|
+
offset += data.byteLength;
|
|
906
|
+
}
|
|
907
|
+
if (offset !== declared) throw new TypeError("Request body length does not match Content-Length");
|
|
908
|
+
return {data: output, bytes: declared, tooLarge: false};
|
|
909
|
+
} catch (error) { if (task.reservedBytes >= declared) releaseServeBytes(task, declared); throw error; }
|
|
910
|
+
}
|
|
729
911
|
const chunks = [];
|
|
730
912
|
let total = 0;
|
|
731
913
|
try {
|
|
732
914
|
for await (const chunk of task.request) {
|
|
733
915
|
const data = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
734
|
-
if (total + data.byteLength >
|
|
916
|
+
if (total + data.byteLength > maximum) {
|
|
735
917
|
task.request.resume();
|
|
736
|
-
|
|
918
|
+
releaseServeBytes(task, total);
|
|
919
|
+
return {data: null, bytes: maximum, tooLarge: true};
|
|
737
920
|
}
|
|
738
921
|
reserveServeBytes(task, data.byteLength);
|
|
739
922
|
total += data.byteLength;
|
|
740
923
|
chunks.push(data);
|
|
741
924
|
}
|
|
742
|
-
|
|
743
|
-
|
|
925
|
+
reserveTransientServeBytes(total);
|
|
926
|
+
try { return {data: Buffer.concat(chunks, total), bytes: total, tooLarge: false}; }
|
|
927
|
+
finally { releaseTransientServeBytes(total); }
|
|
744
928
|
} catch (error) {
|
|
745
929
|
releaseServeBytes(task, total);
|
|
746
930
|
throw error;
|
|
@@ -749,44 +933,134 @@ function bodyOf(task) {
|
|
|
749
933
|
return task.body;
|
|
750
934
|
}
|
|
751
935
|
|
|
936
|
+
async function bodyOf(task, maximum) {
|
|
937
|
+
const value = await rawBodyOf(task, maximum);
|
|
938
|
+
if (value.tooLarge) return {text: null, bytes: value.bytes, tooLarge: true};
|
|
939
|
+
try { return {text: new TextDecoder("utf-8", {fatal: true}).decode(value.data), bytes: value.bytes, tooLarge: false}; }
|
|
940
|
+
catch { throw new TypeError("Request body must be valid UTF-8 text"); }
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
async function bodyBytesOf(task, maximum) {
|
|
944
|
+
const value = await rawBodyOf(task, maximum);
|
|
945
|
+
return value.tooLarge
|
|
946
|
+
? {data: null, bytes: value.bytes, tooLarge: true}
|
|
947
|
+
: {data: new Uint8Array(value.data.buffer, value.data.byteOffset, value.data.byteLength), bytes: value.bytes, tooLarge: false};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function canonicalServePath(rawPath) {
|
|
951
|
+
const source = rawPath.split("/");
|
|
952
|
+
const decoded = new Array(source.length);
|
|
953
|
+
let units = 0;
|
|
954
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
955
|
+
let segment;
|
|
956
|
+
try { segment = decodeURIComponent(source[index]); }
|
|
957
|
+
catch { throw new TypeError("Request path is not valid percent-encoded UTF-8"); }
|
|
958
|
+
if (segment.includes("/") || segment.includes("\\") || segment.includes("\0") || segment === "." || segment === "..") {
|
|
959
|
+
throw new TypeError("Request path contains an unsafe encoded segment");
|
|
960
|
+
}
|
|
961
|
+
units += segment.length + (index === 0 ? 0 : 1);
|
|
962
|
+
if (units > maxPathCodeUnits) throw new RangeError("Request target path is too long");
|
|
963
|
+
decoded[index] = segment;
|
|
964
|
+
}
|
|
965
|
+
const path = decoded.join("/");
|
|
966
|
+
if (!path.startsWith("/")) throw new TypeError("Request target must use an absolute path");
|
|
967
|
+
return path;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function serveHeaderPairs(request) {
|
|
971
|
+
const headers = [];
|
|
972
|
+
let bytes = 0;
|
|
973
|
+
for (const [name, value] of Object.entries(request.headers)) {
|
|
974
|
+
if (typeof value !== "string" && !Array.isArray(value)) continue;
|
|
975
|
+
const text = typeof value === "string" ? value : value.join(", ");
|
|
976
|
+
bytes += Buffer.byteLength(name, "utf8") + Buffer.byteLength(text, "utf8");
|
|
977
|
+
if (bytes > maxServeHeaderTextBytes || headers.length >= maxServeQueryFields) throw new RequestHeadersTooLarge("Request headers are too large");
|
|
978
|
+
headers.push([name, text]);
|
|
979
|
+
}
|
|
980
|
+
return {headers, bytes};
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function serveQueryPairs(source) {
|
|
984
|
+
const query = [];
|
|
985
|
+
let bytes = 0;
|
|
986
|
+
if (source === "") return {query, bytes};
|
|
987
|
+
const fields = source.split("&");
|
|
988
|
+
if (fields.length > maxServeQueryFields) throw new RangeError("Request query is too large");
|
|
989
|
+
for (const field of fields) {
|
|
990
|
+
const separator = field.indexOf("=");
|
|
991
|
+
const rawName = separator < 0 ? field : field.slice(0, separator);
|
|
992
|
+
const rawValue = separator < 0 ? "" : field.slice(separator + 1);
|
|
993
|
+
let name;
|
|
994
|
+
let value;
|
|
995
|
+
try {
|
|
996
|
+
name = decodeURIComponent(rawName.replaceAll("+", " "));
|
|
997
|
+
value = decodeURIComponent(rawValue.replaceAll("+", " "));
|
|
998
|
+
} catch { throw new TypeError("Request query is not valid percent-encoded UTF-8"); }
|
|
999
|
+
bytes += Buffer.byteLength(name, "utf8") + Buffer.byteLength(value, "utf8");
|
|
1000
|
+
if (bytes > maxServeRequestTargetBytes) throw new RangeError("Request query is too large");
|
|
1001
|
+
query.push([name, value]);
|
|
1002
|
+
}
|
|
1003
|
+
return {query, bytes};
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function rejectIncomingRequest(request, response, status, message) {
|
|
1007
|
+
response.statusCode = status;
|
|
1008
|
+
response.setHeader("Connection", "close");
|
|
1009
|
+
response.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
1010
|
+
response.end(message);
|
|
1011
|
+
request.resume();
|
|
1012
|
+
}
|
|
1013
|
+
|
|
752
1014
|
function incomingRequest(server, request, response) {
|
|
753
|
-
if (
|
|
1015
|
+
if (server.stopping) { rejectIncomingRequest(request, response, 503, "Service unavailable"); return; }
|
|
1016
|
+
if (requests.size >= maxRequests) { rejectIncomingRequest(request, response, 503, "Service unavailable"); return; }
|
|
754
1017
|
const target = request.url ?? "/";
|
|
755
|
-
if (typeof target !== "string" || target.length === 0 || target.
|
|
756
|
-
response
|
|
1018
|
+
if (typeof target !== "string" || target.length === 0 || !target.startsWith("/") || Buffer.byteLength(target, "utf8") > maxServeRequestTargetBytes || target.includes("\0")) {
|
|
1019
|
+
rejectIncomingRequest(request, response, 414, "Request target too long"); return;
|
|
757
1020
|
}
|
|
758
|
-
let url;
|
|
759
1021
|
let path;
|
|
1022
|
+
let headerResult;
|
|
1023
|
+
let queryResult;
|
|
760
1024
|
try {
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
1025
|
+
if (target.includes("#")) throw new TypeError("Request target must not contain a URL fragment");
|
|
1026
|
+
const separator = target.indexOf("?");
|
|
1027
|
+
path = canonicalServePath(separator < 0 ? target : target.slice(0, separator));
|
|
1028
|
+
headerResult = serveHeaderPairs(request);
|
|
1029
|
+
queryResult = serveQueryPairs(separator < 0 ? "" : target.slice(separator + 1));
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
const status = error instanceof RequestHeadersTooLarge ? 431 : error instanceof RangeError ? 414 : 400;
|
|
1032
|
+
rejectIncomingRequest(request, response, status, status === 431 ? "Request headers too large" : status === 414 ? "Request target too long" : "Bad request");
|
|
1033
|
+
return;
|
|
766
1034
|
}
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1035
|
+
const declaredText = request.headers["content-length"];
|
|
1036
|
+
if (typeof declaredText === "string" && /^[0-9]+$/u.test(declaredText)) {
|
|
1037
|
+
const declared = Number(declaredText);
|
|
1038
|
+
if (!Number.isSafeInteger(declared) || declared > maxServeBodyBytes) {
|
|
1039
|
+
rejectIncomingRequest(request, response, 413, "Request body too large");
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
771
1042
|
}
|
|
772
|
-
const query = [];
|
|
773
|
-
for (const [name, value] of url.searchParams) if (!query.some(item => item[0] === name)) query.push([name, value]);
|
|
774
1043
|
const handle = allocateHandle(requests, nextRequestHandle, maxRequests, "Node serve request");
|
|
775
1044
|
nextRequestHandle = advanceHandle(handle);
|
|
776
1045
|
const task = {
|
|
777
|
-
handle, server: server.handle, request, response, body: null, streamBytes: 0,
|
|
778
|
-
reservedBytes: 0, completed: false, abandoned: false, transportDone: false, activeOperations: 0,
|
|
1046
|
+
handle, token: server.token, server: server.handle, request, response, body: null, streamBytes: 0, responseMode: "idle", writeActive: false, suppressBody: false,
|
|
1047
|
+
reservedBytes: 0, completed: false, abandoned: false, cancelled: false, transportDone: false, activeOperations: 0,
|
|
779
1048
|
};
|
|
1049
|
+
const metadataBytes = (Buffer.byteLength(target, "utf8") + headerResult.bytes + queryResult.bytes + Buffer.byteLength(path, "utf8") + 256) * 2;
|
|
1050
|
+
if (metadataBytes > maxServeRequestMetadataBytes) { rejectIncomingRequest(request, response, 431, "Request metadata too large"); return; }
|
|
1051
|
+
try { reserveServeBytes(task, metadataBytes); }
|
|
1052
|
+
catch { rejectIncomingRequest(request, response, 503, "Service unavailable"); return; }
|
|
780
1053
|
requests.set(handle, task);
|
|
781
|
-
|
|
782
|
-
response.once("
|
|
1054
|
+
request.once("aborted", () => cancelRequest(task));
|
|
1055
|
+
response.once("finish", () => closeRequest(task, false));
|
|
1056
|
+
response.once("close", () => closeRequest(task, !response.writableFinished));
|
|
783
1057
|
port.postMessage({kind: "event", event: "serve.request", value: {
|
|
784
1058
|
token: server.token,
|
|
785
1059
|
request: handle,
|
|
786
1060
|
method: request.method ?? "GET",
|
|
787
1061
|
path,
|
|
788
|
-
query,
|
|
789
|
-
headers,
|
|
1062
|
+
query: queryResult.query,
|
|
1063
|
+
headers: headerResult.headers,
|
|
790
1064
|
}});
|
|
791
1065
|
}
|
|
792
1066
|
|
|
@@ -797,33 +1071,98 @@ async function startServer(args) {
|
|
|
797
1071
|
const host = boundedHost(args[2]);
|
|
798
1072
|
const handle = allocateHandle(servers, nextServerHandle, maxServers, "Node serve server");
|
|
799
1073
|
nextServerHandle = advanceHandle(handle);
|
|
800
|
-
const task = {handle, token, server: null};
|
|
801
|
-
const server = createServer(
|
|
1074
|
+
const task = {handle, token, server: null, sockets: new Set(), stopping: false, stopPromise: null};
|
|
1075
|
+
const server = createServer({
|
|
1076
|
+
maxHeaderSize: maxServeHeaderTextBytes,
|
|
1077
|
+
headersTimeout: serveHeadersTimeoutMilliseconds,
|
|
1078
|
+
requestTimeout: serveRequestTimeoutMilliseconds,
|
|
1079
|
+
keepAliveTimeout: serveKeepAliveTimeoutMilliseconds,
|
|
1080
|
+
connectionsCheckingInterval: 1000,
|
|
1081
|
+
}, (request, response) => incomingRequest(task, request, response));
|
|
802
1082
|
task.server = server;
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
1083
|
+
server.maxConnections = maxServeSocketsPerServer;
|
|
1084
|
+
server.maxRequestsPerSocket = 1000;
|
|
1085
|
+
server.on("connection", socket => {
|
|
1086
|
+
if (task.stopping || task.sockets.size >= maxServeSocketsPerServer || activeServeSockets >= maxServeSockets) {
|
|
1087
|
+
socket.destroy();
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
task.sockets.add(socket);
|
|
1091
|
+
activeServeSockets += 1;
|
|
1092
|
+
socket.setNoDelay(true);
|
|
1093
|
+
socket.setKeepAlive(true, serveKeepAliveTimeoutMilliseconds);
|
|
1094
|
+
socket.once("close", () => {
|
|
1095
|
+
if (!task.sockets.delete(socket)) return;
|
|
1096
|
+
activeServeSockets -= 1;
|
|
1097
|
+
if (activeServeSockets < 0) activeServeSockets = 0;
|
|
1098
|
+
});
|
|
809
1099
|
});
|
|
810
|
-
const address = server.address();
|
|
811
|
-
if (!address || typeof address === "string") throw new Error("velar/serve could not determine the bound port");
|
|
812
1100
|
servers.set(handle, task);
|
|
813
|
-
|
|
1101
|
+
try {
|
|
1102
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
1103
|
+
const failed = error => { server.off("listening", ready); rejectListen(error); };
|
|
1104
|
+
const ready = () => { server.off("error", failed); resolveListen(); };
|
|
1105
|
+
server.once("error", failed);
|
|
1106
|
+
server.once("listening", ready);
|
|
1107
|
+
server.listen({port: portValue, host});
|
|
1108
|
+
});
|
|
1109
|
+
const address = server.address();
|
|
1110
|
+
if (!address || typeof address === "string") throw new Error("velar/serve could not determine the bound port");
|
|
1111
|
+
server.on("error", error => {
|
|
1112
|
+
if (task.stopping) return;
|
|
1113
|
+
const failure = errorRecord(error);
|
|
1114
|
+
try { port.postMessage({kind: "event", event: "serve.error", value: {token, message: failure.message}}); }
|
|
1115
|
+
catch {}
|
|
1116
|
+
});
|
|
1117
|
+
return {handle, port: address.port};
|
|
1118
|
+
} catch (error) {
|
|
1119
|
+
if (servers.get(handle) === task) servers.delete(handle);
|
|
1120
|
+
try { server.close(); } catch {}
|
|
1121
|
+
throw error;
|
|
1122
|
+
}
|
|
814
1123
|
}
|
|
815
1124
|
|
|
816
1125
|
async function stopServer(args) {
|
|
817
|
-
if (args.length
|
|
1126
|
+
if (args.length < 1 || args.length > 2) throw new TypeError("serve.stop arguments are invalid");
|
|
818
1127
|
const handle = integer(args[0], 1, Number.MAX_SAFE_INTEGER, "Node serve server handle");
|
|
1128
|
+
const grace = args.length === 2 ? integer(args[1], 1, 120_000, "serve stop grace") : serveShutdownTimeoutMilliseconds;
|
|
819
1129
|
const task = servers.get(handle);
|
|
820
1130
|
if (!task) return null;
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
1131
|
+
if (task.stopPromise !== null) return await task.stopPromise;
|
|
1132
|
+
task.stopping = true;
|
|
1133
|
+
for (const request of requests.values()) if (request.server === handle && !request.completed) {
|
|
1134
|
+
request.response.shouldKeepAlive = false;
|
|
1135
|
+
if (!request.response.headersSent) request.response.setHeader("Connection", "close");
|
|
1136
|
+
cancelRequest(request, "server_stopping");
|
|
1137
|
+
}
|
|
1138
|
+
const pending = (async () => {
|
|
1139
|
+
await new Promise((resolveStop, rejectStop) => {
|
|
1140
|
+
let settled = false;
|
|
1141
|
+
let forceTimer = null;
|
|
1142
|
+
let finalTimer = null;
|
|
1143
|
+
const finish = action => {
|
|
1144
|
+
if (settled) return;
|
|
1145
|
+
settled = true;
|
|
1146
|
+
if (forceTimer !== null) clearTimeout(forceTimer);
|
|
1147
|
+
if (finalTimer !== null) clearTimeout(finalTimer);
|
|
1148
|
+
action();
|
|
1149
|
+
};
|
|
1150
|
+
task.server.close(error => error ? finish(() => rejectStop(error)) : finish(() => resolveStop(null)));
|
|
1151
|
+
task.server.closeIdleConnections?.();
|
|
1152
|
+
forceTimer = setTimeout(() => {
|
|
1153
|
+
try { task.server.closeAllConnections?.(); } catch {}
|
|
1154
|
+
for (const socket of task.sockets) try { socket.destroy(); } catch {}
|
|
1155
|
+
finalTimer = setTimeout(() => finish(() => rejectStop(new Error("Node serve transport did not stop within its graceful shutdown deadline"))), 1000);
|
|
1156
|
+
finalTimer.unref?.();
|
|
1157
|
+
}, grace);
|
|
1158
|
+
forceTimer.unref?.();
|
|
1159
|
+
});
|
|
1160
|
+
if (servers.get(handle) === task) servers.delete(handle);
|
|
1161
|
+
return null;
|
|
1162
|
+
})();
|
|
1163
|
+
task.stopPromise = pending;
|
|
1164
|
+
try { return await pending; }
|
|
1165
|
+
catch (error) { if (task.stopPromise === pending) { task.stopPromise = null; task.stopping = false; } throw error; }
|
|
827
1166
|
}
|
|
828
1167
|
|
|
829
1168
|
async function dispatch(operation, args) {
|
|
@@ -980,77 +1319,129 @@ async function dispatch(operation, args) {
|
|
|
980
1319
|
const task = requestHandle(args[0]);
|
|
981
1320
|
return withRequest(task, async () => {
|
|
982
1321
|
const maximum = integer(args[1], 1, maxServeBodyBytes, "Request body maxBytes");
|
|
983
|
-
|
|
984
|
-
return body.bytes > maximum ? {text: null, bytes: body.bytes, tooLarge: true} : {text: body.text, bytes: body.bytes, tooLarge: false};
|
|
1322
|
+
return bodyOf(task, maximum);
|
|
985
1323
|
});
|
|
986
1324
|
}
|
|
987
|
-
if (operation === "serve.
|
|
988
|
-
if (args.length !==
|
|
1325
|
+
if (operation === "serve.bodyBytes") {
|
|
1326
|
+
if (args.length !== 2) throw new TypeError("serve.bodyBytes arguments are invalid");
|
|
989
1327
|
const task = requestHandle(args[0]);
|
|
990
1328
|
return withRequest(task, async () => {
|
|
991
|
-
const
|
|
1329
|
+
const maximum = integer(args[1], 1, maxServeBodyBytes, "Request body maxBytes");
|
|
1330
|
+
return bodyBytesOf(task, maximum);
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
if (operation === "serve.readFile") {
|
|
1334
|
+
if (args.length !== 3) throw new TypeError("serve.readFile arguments are invalid");
|
|
1335
|
+
return testStaticFile(args[0], args[1], args[2]);
|
|
1336
|
+
}
|
|
1337
|
+
if (operation === "serve.respond") {
|
|
1338
|
+
if (args.length !== 8) throw new TypeError("serve.respond arguments are invalid");
|
|
1339
|
+
const task = requestHandle(args[0]);
|
|
1340
|
+
return withTerminalResponse(task, async () => {
|
|
1341
|
+
const status = integer(args[1], 200, 599, "ServeResponse.status");
|
|
992
1342
|
const headers = headerPairs(args[2]);
|
|
993
1343
|
const kind = args[3];
|
|
994
1344
|
const body = args[4];
|
|
995
1345
|
const contentType = args[5];
|
|
1346
|
+
const compression = args[6];
|
|
1347
|
+
const cookies = cookieValues(args[7]);
|
|
996
1348
|
if (kind !== "json" && kind !== "text" || typeof body !== "string" || Buffer.byteLength(body, "utf8") > maxServeBodyBytes) {
|
|
997
1349
|
throw new TypeError("ServeResponse body is invalid");
|
|
998
1350
|
}
|
|
999
1351
|
if (contentType !== null && (typeof contentType !== "string" || contentType.length === 0 || contentType.length > 1024 || /[\0\r\n]/u.test(contentType))) {
|
|
1000
1352
|
throw new TypeError("ServeResponse.contentType must be bounded single-line text");
|
|
1001
1353
|
}
|
|
1002
|
-
|
|
1354
|
+
if (compression !== null && compression !== "gzip" && compression !== "br") throw new TypeError("ServeResponse compression is invalid");
|
|
1355
|
+
const suppressBody = responseHasNoBody(task, status);
|
|
1356
|
+
let output = body;
|
|
1357
|
+
if (!suppressBody && compression !== null) {
|
|
1358
|
+
const inputBytes = Buffer.byteLength(body, "utf8");
|
|
1359
|
+
reserveTransientServeBytes(inputBytes * 2);
|
|
1360
|
+
try { output = await (compression === "br" ? brotliCompress(body) : gzip(body)); }
|
|
1361
|
+
finally { releaseTransientServeBytes(inputBytes * 2); }
|
|
1362
|
+
if (output.byteLength > maxServeBodyBytes) throw new RangeError("Compressed ServeResponse exceeds 16 MiB");
|
|
1363
|
+
}
|
|
1364
|
+
if (!suppressBody) reserveServeBytes(task, typeof output === "string" ? Buffer.byteLength(output, "utf8") : output.byteLength);
|
|
1003
1365
|
task.response.statusCode = status;
|
|
1004
|
-
setHeaders(task.response, headers);
|
|
1005
|
-
if (!task.response.hasHeader("Content-Type")) {
|
|
1366
|
+
setHeaders(task.response, headers, cookies);
|
|
1367
|
+
if (!task.response.hasHeader("Content-Type") && (task.request.method === "HEAD" || !suppressBody)) {
|
|
1006
1368
|
task.response.setHeader("Content-Type", kind === "json" ? "application/json; charset=utf-8" : contentType ?? "text/plain; charset=utf-8");
|
|
1007
1369
|
}
|
|
1008
|
-
|
|
1370
|
+
if (compression !== null) {
|
|
1371
|
+
task.response.setHeader("Content-Encoding", compression);
|
|
1372
|
+
if (!task.response.hasHeader("Vary")) task.response.setHeader("Vary", "Accept-Encoding");
|
|
1373
|
+
}
|
|
1374
|
+
await endServeResponse(task, suppressBody ? undefined : output);
|
|
1009
1375
|
completeRequest(task);
|
|
1010
1376
|
return null;
|
|
1011
1377
|
});
|
|
1012
1378
|
}
|
|
1013
1379
|
if (operation === "serve.respondFile") {
|
|
1014
|
-
if (args.length !==
|
|
1380
|
+
if (args.length !== 6) throw new TypeError("serve.respondFile arguments are invalid");
|
|
1015
1381
|
const task = requestHandle(args[0]);
|
|
1016
|
-
return
|
|
1382
|
+
return withTerminalResponse(task, async () => {
|
|
1017
1383
|
try {
|
|
1018
|
-
const file = await staticFile(
|
|
1019
|
-
task.response
|
|
1384
|
+
const file = await staticFile(args[1], args[2], args[3]);
|
|
1385
|
+
setHeaders(task.response, headerPairs(args[4]), args[5]);
|
|
1020
1386
|
task.response.setHeader("Content-Type", file.contentType);
|
|
1021
|
-
task.response.setHeader("
|
|
1022
|
-
|
|
1387
|
+
task.response.setHeader("Accept-Ranges", "bytes");
|
|
1388
|
+
const etag = staticEtag(file.metadata);
|
|
1389
|
+
task.response.setHeader("ETag", etag);
|
|
1390
|
+
task.response.setHeader("Last-Modified", file.metadata.mtime.toUTCString());
|
|
1391
|
+
if (staticNotModified(task.request, file.metadata, etag)) {
|
|
1392
|
+
task.response.statusCode = 304;
|
|
1393
|
+
await endServeResponse(task);
|
|
1394
|
+
} else {
|
|
1395
|
+
const range = staticRange(task.request, file.metadata, etag);
|
|
1396
|
+
if (range === false) {
|
|
1397
|
+
task.response.statusCode = 416;
|
|
1398
|
+
task.response.setHeader("Content-Range", "bytes */" + file.metadata.size);
|
|
1399
|
+
await endServeResponse(task, task.request.method === "HEAD" ? undefined : "Range not satisfiable");
|
|
1400
|
+
} else {
|
|
1401
|
+
const start = range === null ? 0 : range.start;
|
|
1402
|
+
const end = range === null ? file.metadata.size - 1 : range.end;
|
|
1403
|
+
task.response.statusCode = range === null ? 200 : 206;
|
|
1404
|
+
task.response.setHeader("Content-Length", Math.max(0, end - start + 1));
|
|
1405
|
+
if (range !== null) task.response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + file.metadata.size);
|
|
1406
|
+
if (task.request.method !== "HEAD") await writeStaticRange(task, file, start, end);
|
|
1407
|
+
await endServeResponse(task);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1023
1410
|
} catch (error) {
|
|
1024
1411
|
if (!(error instanceof StaticNotFound) && !missing(error) && error?.code !== "EISDIR") throw error;
|
|
1025
1412
|
task.response.statusCode = 404;
|
|
1026
1413
|
task.response.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
1027
|
-
task
|
|
1414
|
+
await endServeResponse(task, "Not found");
|
|
1028
1415
|
}
|
|
1029
1416
|
completeRequest(task);
|
|
1030
1417
|
return null;
|
|
1031
1418
|
});
|
|
1032
1419
|
}
|
|
1033
1420
|
if (operation === "serve.streamStart") {
|
|
1034
|
-
if (args.length !==
|
|
1421
|
+
if (args.length !== 4) throw new TypeError("serve.streamStart arguments are invalid");
|
|
1035
1422
|
const task = requestHandle(args[0]);
|
|
1423
|
+
const status = integer(args[1], 200, 599, "ServeResponse.status");
|
|
1424
|
+
const headers = headerPairs(args[2]);
|
|
1036
1425
|
return withRequest(task, async () => {
|
|
1037
|
-
if (task.
|
|
1038
|
-
task.
|
|
1039
|
-
|
|
1040
|
-
task.
|
|
1426
|
+
if (task.responseMode !== "idle") throw new Error("Node serve request already owns a response operation");
|
|
1427
|
+
task.responseMode = "streaming";
|
|
1428
|
+
task.response.statusCode = status;
|
|
1429
|
+
setHeaders(task.response, headers, args[3]);
|
|
1430
|
+
task.suppressBody = responseHasNoBody(task, task.response.statusCode);
|
|
1041
1431
|
return null;
|
|
1042
1432
|
});
|
|
1043
1433
|
}
|
|
1044
1434
|
if (operation === "serve.streamWrite") {
|
|
1045
1435
|
if (args.length !== 2) throw new TypeError("serve.streamWrite arguments are invalid");
|
|
1046
1436
|
const task = requestHandle(args[0]);
|
|
1047
|
-
return
|
|
1437
|
+
return withStreamWrite(task, async () => {
|
|
1048
1438
|
const chunk = args[1];
|
|
1049
|
-
if (
|
|
1439
|
+
if (typeof chunk !== "string") throw new TypeError("ServeResponse.stream chunks must be strings");
|
|
1050
1440
|
const bytes = Buffer.byteLength(chunk, "utf8");
|
|
1051
1441
|
if (bytes > maxServeStreamChunkBytes || task.streamBytes + bytes > maxServeStreamBytes) {
|
|
1052
1442
|
throw new RangeError("ServeResponse.stream exceeded its bounded output");
|
|
1053
1443
|
}
|
|
1444
|
+
if (task.suppressBody) return null;
|
|
1054
1445
|
if (task.response.destroyed || task.response.writableEnded) throw new Error("ServeResponse.stream client connection is closed");
|
|
1055
1446
|
reserveTransientServeBytes(bytes);
|
|
1056
1447
|
task.streamBytes += bytes;
|
|
@@ -1081,8 +1472,10 @@ async function dispatch(operation, args) {
|
|
|
1081
1472
|
if (args.length !== 1) throw new TypeError("serve.streamEnd arguments are invalid");
|
|
1082
1473
|
const task = requestHandle(args[0]);
|
|
1083
1474
|
return withRequest(task, async () => {
|
|
1084
|
-
if (
|
|
1085
|
-
task.
|
|
1475
|
+
if (task.responseMode !== "streaming") throw new Error("ServeResponse stream has not started");
|
|
1476
|
+
if (task.writeActive) throw new Error("ServeResponse stream still has an active write");
|
|
1477
|
+
task.responseMode = "terminal";
|
|
1478
|
+
await endServeResponse(task);
|
|
1086
1479
|
completeRequest(task);
|
|
1087
1480
|
return null;
|
|
1088
1481
|
});
|
|
@@ -1090,7 +1483,7 @@ async function dispatch(operation, args) {
|
|
|
1090
1483
|
if (operation === "serve.fail") {
|
|
1091
1484
|
if (args.length !== 1) throw new TypeError("serve.fail arguments are invalid");
|
|
1092
1485
|
const task = requestHandle(args[0]);
|
|
1093
|
-
return withRequest(task, async () => { opaqueFailure(task); return null; });
|
|
1486
|
+
return withRequest(task, async () => { task.responseMode = "terminal"; opaqueFailure(task); return null; });
|
|
1094
1487
|
}
|
|
1095
1488
|
throw new TypeError("Unknown Node host operation");
|
|
1096
1489
|
}
|
|
@@ -1131,7 +1524,16 @@ port.on("message", value => {
|
|
|
1131
1524
|
}
|
|
1132
1525
|
return dispatch(value.operation, value.args);
|
|
1133
1526
|
}).then(
|
|
1134
|
-
result =>
|
|
1527
|
+
result => {
|
|
1528
|
+
const message = {kind: "response", id, ok: true, value: result, error: null};
|
|
1529
|
+
if ((value?.operation === "serve.bodyBytes" || value?.operation === "serve.readFile") && result?.data instanceof Uint8Array && result.data.byteLength > 0) {
|
|
1530
|
+
let data = result.data;
|
|
1531
|
+
if (data.byteOffset !== 0 || data.buffer.byteLength !== data.byteLength) data = new Uint8Array(data);
|
|
1532
|
+
result.data = data;
|
|
1533
|
+
try { port.postMessage(message, [data.buffer]); }
|
|
1534
|
+
finally { if (value.operation === "serve.readFile") releaseTransientServeBytes(data.byteLength); }
|
|
1535
|
+
} else port.postMessage(message);
|
|
1536
|
+
},
|
|
1135
1537
|
error => port.postMessage({kind: "response", id, ok: false, value: null, error: errorRecord(error)}),
|
|
1136
1538
|
);
|
|
1137
1539
|
});
|