@jsenv/server 17.6.1 → 17.6.3
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 +47 -48
- package/dist/jsenv_server.js +1336 -1270
- package/package.json +2 -2
- package/src/content_negotiation/pick_content_encoding.js +11 -1
- package/src/content_negotiation/pick_content_language.js +11 -1
- package/src/content_negotiation/pick_content_type.js +11 -1
- package/src/content_negotiation/pick_content_version.js +1 -1
- package/src/interfacing_with_node/from_node_request.js +154 -178
- package/src/interfacing_with_node/observable_from_file_handle.js +1 -18
- package/src/interfacing_with_node/observable_from_node_stream.js +29 -65
- package/src/interfacing_with_node/write_node_response.js +59 -103
- package/src/internal/allowed_hosts.js +85 -0
- package/src/internal/escape_html.js +11 -0
- package/src/internal/{headersFromObject.js → headers_from_object.js} +2 -2
- package/src/internal/listen.js +32 -5
- package/src/internal/{listenRequest.js → listen_request.js} +1 -1
- package/src/internal/{listenServerConnectionError.js → listen_server_connection_error.js} +1 -1
- package/src/internal/{multiple-header.js → multiple_header.js} +32 -10
- package/src/internal/reason_phrase.js +17 -0
- package/src/internal/response_composition.js +11 -2
- package/src/internal/secure_server.js +22 -0
- package/src/internal/server_origins.js +78 -0
- package/src/internal/{server-polyglot.js → server_polyglot.js} +13 -38
- package/src/internal/track_server_pending_connections.js +50 -0
- package/src/internal/track_server_pending_requests.js +47 -0
- package/src/plugins/cors/server_plugin_cors.js +39 -6
- package/src/plugins/default_body_4xx_5xx/server_plugin_default_body_4xx_5xx.js +37 -29
- package/src/plugins/error_handler/server_plugin_error_handler.js +26 -10
- package/src/plugins/filesystem/fetch_directory.js +16 -3
- package/src/plugins/filesystem/fetch_file.js +80 -21
- package/src/plugins/filesystem/filesystem_error_to_response.js +12 -6
- package/src/plugins/request_aliases/server_plugin_request_aliases.js +8 -0
- package/src/plugins/response_acceptance_check/server_plugin_response_acceptance_check.js +11 -6
- package/src/plugins/route_inspector/server_plugin_route_inspector.js +11 -5
- package/src/plugins/server_plugins_controller.js +27 -13
- package/src/progressive_response.js +24 -0
- package/src/router/permissions.js +52 -0
- package/src/router/router.js +18 -80
- package/src/server_events.js +3 -1
- package/src/server_timing/timing_header.js +1 -1
- package/src/start_server.js +266 -416
- package/src/internal/headersFromString.js +0 -21
- package/src/internal/headersToObject.js +0 -7
- package/src/internal/headersToString.js +0 -16
- package/src/internal/listenClientError.js +0 -27
- package/src/internal/trackServerPendingConnections.js +0 -145
- package/src/internal/trackServerPendingRequests.js +0 -68
- package/src/request_body_handling.js +0 -22
- /package/src/internal/{colorizeResponseStatus.js → colorize_response_status.js} +0 -0
- /package/src/internal/{listenEvent.js → listen_event.js} +0 -0
- /package/src/internal/{normalizeHeaderName.js → normalize_header_name.js} +0 -0
- /package/src/internal/{normalizeHeaderValue.js → normalize_header_value.js} +0 -0
package/src/start_server.js
CHANGED
|
@@ -2,27 +2,28 @@ import { Abort, raceProcessTeardownEvents } from "@jsenv/abort";
|
|
|
2
2
|
import { createDetailedMessage, createLogger } from "@jsenv/humanize";
|
|
3
3
|
import { memoize } from "@jsenv/utils/src/memoize/memoize.js";
|
|
4
4
|
import cluster from "node:cluster";
|
|
5
|
-
import { isIP } from "node:net";
|
|
6
5
|
|
|
7
6
|
import {
|
|
8
7
|
applyRedirectionToRequest,
|
|
9
|
-
createPushRequest,
|
|
10
8
|
fromNodeRequest,
|
|
11
9
|
} from "./interfacing_with_node/from_node_request.js";
|
|
12
10
|
import { writeNodeResponse } from "./interfacing_with_node/write_node_response.js";
|
|
13
|
-
import { websocketSuffixColorized } from "./internal/
|
|
11
|
+
import { websocketSuffixColorized } from "./internal/colorize_response_status.js";
|
|
14
12
|
import {
|
|
15
13
|
composeTwoHeaders,
|
|
16
14
|
composeTwoHeaderValues,
|
|
17
15
|
} from "./internal/headers_composition.js";
|
|
16
|
+
import { createHostChecker } from "./internal/allowed_hosts.js";
|
|
18
17
|
import { listen, stopListening } from "./internal/listen.js";
|
|
19
|
-
import { listenEvent } from "./internal/
|
|
20
|
-
import { listenRequest } from "./internal/
|
|
21
|
-
import { listenServerConnectionError } from "./internal/
|
|
18
|
+
import { listenEvent } from "./internal/listen_event.js";
|
|
19
|
+
import { listenRequest } from "./internal/listen_request.js";
|
|
20
|
+
import { listenServerConnectionError } from "./internal/listen_server_connection_error.js";
|
|
22
21
|
import { composeTwoResponses } from "./internal/response_composition.js";
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
22
|
+
import { createSecureServer } from "./internal/secure_server.js";
|
|
23
|
+
import { resolveServerOrigins } from "./internal/server_origins.js";
|
|
24
|
+
import { createPolyglotServer } from "./internal/server_polyglot.js";
|
|
25
|
+
import { trackServerPendingConnections } from "./internal/track_server_pending_connections.js";
|
|
26
|
+
import { trackServerPendingRequests } from "./internal/track_server_pending_requests.js";
|
|
26
27
|
import { serverPluginAutoreloadOnRestart } from "./plugins/autoreload_on_server_restart/server_plugin_autoreload_on_server_restart.js";
|
|
27
28
|
import { serverPluginResponseCookies } from "./plugins/cookies/server_plugin_response_cookies.js";
|
|
28
29
|
import { serverPluginDefaultBody4xx5xx } from "./plugins/default_body_4xx_5xx/server_plugin_default_body_4xx_5xx.js";
|
|
@@ -30,6 +31,7 @@ import { serverPluginInternalClientFiles } from "./plugins/internal_client_files
|
|
|
30
31
|
import { serverPluginOpenFile } from "./plugins/open_file/server_plugin_open_file.js";
|
|
31
32
|
import { serverPluginRouteInspector } from "./plugins/route_inspector/server_plugin_route_inspector.js";
|
|
32
33
|
import { createServerPluginsController } from "./plugins/server_plugins_controller.js";
|
|
34
|
+
import { createPermissionHelpers } from "./router/permissions.js";
|
|
33
35
|
import { createRouter } from "./router/router.js";
|
|
34
36
|
import { timingToServerTimingResponseHeaders } from "./server_timing/timing_header.js";
|
|
35
37
|
import {
|
|
@@ -43,75 +45,114 @@ import {
|
|
|
43
45
|
} from "./stop_reasons.js";
|
|
44
46
|
import { getWebSocketHandler } from "./web_socket_response.js";
|
|
45
47
|
|
|
46
|
-
import { applyDnsResolution } from "./internal/dns_resolution.js";
|
|
47
|
-
import { parseHostname } from "./internal/hostname_parser.js";
|
|
48
|
-
import { createIpGetters } from "./internal/server_ips.js";
|
|
49
|
-
|
|
50
48
|
const TIMING_NOOP = () => {
|
|
51
49
|
return { end: () => {} };
|
|
52
50
|
};
|
|
53
51
|
|
|
54
|
-
const permissionsSatisfy = (permissionsSet, permissionsRequired) => {
|
|
55
|
-
for (const p of permissionsRequired) {
|
|
56
|
-
if (!permissionsSet.has(p)) {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return true;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
52
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* @param {Object} params
|
|
67
|
-
*
|
|
68
|
-
* @param {Array<Object>} [params.routes=[]] - Route definitions for the server.
|
|
69
|
-
* Each route is an object with the following properties:
|
|
70
|
-
* - `endpoint` {string} — Required. A string like `"GET /users/:id"` combining the HTTP method
|
|
71
|
-
* and the resource path. Use `"*"` as the method to match all methods, and URL patterns
|
|
72
|
-
* such as `:param` for named segments or `*` for wildcards.
|
|
73
|
-
* - `fetch` {Function} — Required. `(request, helpers) => response`. Must return a `Response`,
|
|
74
|
-
* a response-like object `{ status, headers, body }`, or `null`/`undefined` to skip the route.
|
|
75
|
-
* - `description` {string} — Optional human-readable description shown in the route inspector.
|
|
76
|
-
* - `permissionsRequired` {Array<string>} — Optional. Permissions the client must hold to
|
|
77
|
-
* access the route. An empty array means anyone can access. When omitted the route is hidden
|
|
78
|
-
* by default (404 for everyone).
|
|
79
|
-
* - `permissionsToSee` {Array<string>} — Optional. Permissions needed to know the route exists
|
|
80
|
-
* (403 instead of 404 when access is denied). An empty array means the route is visible to
|
|
81
|
-
* everyone even when access is denied.
|
|
82
|
-
* - `availableMediaTypes` {Array<string>} — Content-types this route can produce (drives
|
|
83
|
-
* `Accept` content negotiation).
|
|
84
|
-
* - `availableLanguages` {Array<string>} — Languages this route can respond with.
|
|
85
|
-
* - `availableEncodings` {Array<string>} — Encodings this route supports.
|
|
86
|
-
* - `acceptedMediaTypes` {Array<string>} — Content-types accepted for request bodies
|
|
87
|
-
* (POST/PATCH/PUT).
|
|
88
|
-
* - `clientCodeExample` {string|Function} — Optional code snippet displayed in the route
|
|
89
|
-
* inspector as a usage example.
|
|
90
|
-
* - `declarationSource` {string} — Optional file URL of where the route is declared, shown
|
|
91
|
-
* in the route inspector when `canExposeSensitiveData` is enabled.
|
|
92
|
-
* - `headers` {Object} — Optional header pattern that must match for the route to be selected.
|
|
93
|
-
*
|
|
94
|
-
* @param {Array<Object>} [params.plugins=[]] - Server plugins that extend behaviour (hooks such
|
|
95
|
-
* as `grantPermissions`, `redirectRequest`, `handleError`, etc.). See plugin documentation for
|
|
96
|
-
* the exact shape; plugins are not described here in detail.
|
|
53
|
+
* Start an http server (https and http2 optional) answering each request with
|
|
54
|
+
* the first route producing a response.
|
|
97
55
|
*
|
|
98
|
-
* @param {
|
|
99
|
-
*
|
|
100
|
-
* `
|
|
56
|
+
* @param {Object} [params={}]
|
|
57
|
+
* @param {Array<Object>} [params.routes=[]] - Route descriptors, tried in order. Each has:
|
|
58
|
+
* - `endpoint` {string} — Required. `"GET /users/:id"`: an http method (or `*` for any)
|
|
59
|
+
* and a resource pattern. `:name` captures a segment and `*` a run of segments, both
|
|
60
|
+
* land in `request.params`; `?page=:page` captures a search param. `"*"` alone
|
|
61
|
+
* matches everything. An endpoint ending in `.websocket` marks a websocket route.
|
|
62
|
+
* - `fetch` {Function} — Required. `(request, helpers) => response`, async or not.
|
|
63
|
+
* Returns a `Response`, a plain `{ status, statusText, statusMessage, headers, body,
|
|
64
|
+
* timing }` object (`status` defaults to 404, `statusMessage` feeds the body of 4xx/5xx
|
|
65
|
+
* responses), a `WebSocketResponse`, or `null`/`undefined` to let the next route try.
|
|
66
|
+
* `request` is described in docs/handling_requests.md. `helpers` holds `timing(name)`,
|
|
67
|
+
* `injectResponseHeader(name, value)`, `contentNegotiation` (`{ mediaType, language,
|
|
68
|
+
* version, encoding }` picked from the `available*` lists below), `responseCookies`
|
|
69
|
+
* (`set`/`delete`), `hasPermissions`, `getAllPermissions`, `router`,
|
|
70
|
+
* `canExposeSensitiveData`, plus whatever plugins add with "augmentRouteFetchSecondArg".
|
|
71
|
+
* - `headers` {Object} — Header pattern the request must match
|
|
72
|
+
* (`{ upgrade: "websocket" }` marks a websocket route).
|
|
73
|
+
* - `availableMediaTypes`, `availableLanguages`, `availableVersions`, `availableEncodings`
|
|
74
|
+
* {Array} — What the route can produce, in order of preference. Drives content
|
|
75
|
+
* negotiation, the `vary` header and 406 responses. Media types are inferred from the
|
|
76
|
+
* endpoint extension when omitted.
|
|
77
|
+
* - `acceptedMediaTypes` {Array<string>} — Request body media types accepted by
|
|
78
|
+
* POST/PATCH/PUT (415 otherwise).
|
|
79
|
+
* - `permissionsRequired` {Array<string>} — Permissions (granted by "grantPermissions"
|
|
80
|
+
* plugins) needed to access the route; `[]` opens it to everyone. Once any route
|
|
81
|
+
* declares permissions, a route without them is hidden (404) from everyone.
|
|
82
|
+
* - `permissionsToSee` {Array<string>} — Permissions needed to learn the route exists
|
|
83
|
+
* (403 instead of 404 when access is denied); `[]` makes it visible to everyone.
|
|
84
|
+
* - `description` {string}, `clientCodeExample` {string|Function}, `declarationSource`
|
|
85
|
+
* {string} — Shown by the route inspector at `/.internal/route_inspector`.
|
|
101
86
|
*
|
|
102
|
-
* @param {
|
|
103
|
-
*
|
|
104
|
-
*
|
|
87
|
+
* @param {Array<Object>} [params.plugins=[]] - Server plugins, see docs/plugins.md.
|
|
88
|
+
* @param {number} [params.port=0] - `0` lets the OS pick a free port.
|
|
89
|
+
* @param {number} [params.portHint] - With `port: 0`, try this port first, then the next ones.
|
|
90
|
+
* @param {string} [params.hostname="localhost"] - Hostname or ip to listen to; `server.origin` is built from it.
|
|
91
|
+
* @param {boolean} [params.acceptAnyIp=false] - Listen on every interface (`0.0.0.0` or `::`)
|
|
92
|
+
* so that other machines on the network can reach the server; `server.origins.externalip`
|
|
93
|
+
* then tells the address to use from there.
|
|
94
|
+
* @param {boolean} [params.preferIpv6] - Favor ipv6 addresses in `server.origins`.
|
|
95
|
+
* @param {Array<string>|true} [params.allowedHosts=[]] - Names the server may be requested
|
|
96
|
+
* with (the `host` header), on top of localhost, `hostname`, the machine name and ips,
|
|
97
|
+
* and every ip when `acceptAnyIp`. `".example.com"` covers the subdomains. Any other
|
|
98
|
+
* host is answered 403: a page served elsewhere cannot read the responses by rebinding
|
|
99
|
+
* its DNS name to this machine. `true` disables the check.
|
|
100
|
+
* @param {Object|false} [params.https=false] - `{ certificate, privateKey }` (PEM strings) to serve https.
|
|
101
|
+
* @param {boolean} [params.redirectHttpToHttps] - With https, answer http requests with a 301
|
|
102
|
+
* to the https origin. Defaults to true unless `allowHttpRequestOnHttps` is set.
|
|
103
|
+
* @param {boolean} [params.allowHttpRequestOnHttps=false] - With https, also serve plain http
|
|
104
|
+
* requests on the same port (`request.origin` tells them apart).
|
|
105
|
+
* @param {boolean} [params.http2=false] - Serve http2 (needs `https`, http/1.1 clients are still
|
|
106
|
+
* accepted). Brings nothing on localhost; a page loading many modules from another machine
|
|
107
|
+
* loads several times faster. Http2 has no reason phrase: `statusText` then only reaches the
|
|
108
|
+
* logs and the body of 4xx/5xx responses.
|
|
109
|
+
* @param {boolean} [params.http1Allowed=true] - With http2, still accept http/1.1 clients.
|
|
110
|
+
* @param {string} [params.logLevel] - `"debug"`, `"info"` (default), `"warn"`, `"error"` or `"off"`.
|
|
111
|
+
* @param {string} [params.routerLogLevel] - Same, for the router logs (which route matched).
|
|
112
|
+
* @param {boolean} [params.startLog=true] - Log "server started at …" once listening.
|
|
113
|
+
* @param {string} [params.serverName="server"] - Name used in the logs.
|
|
114
|
+
* @param {AbortSignal} [params.signal] - Cancels the start (an AbortError is thrown). Once
|
|
115
|
+
* listening the server is stopped with `stop`.
|
|
116
|
+
* @param {boolean} [params.stopOnSIGINT] - Stop on SIGINT (ctrl+c). Defaults to true, except
|
|
117
|
+
* inside a cluster worker where the primary process is in charge.
|
|
118
|
+
* @param {boolean} [params.stopOnExit=true] - Stop on SIGHUP, SIGTERM, beforeExit and exit.
|
|
119
|
+
* @param {boolean} [params.stopOnInternalError=false] - Stop when a route throws (after the
|
|
120
|
+
* "handleError" plugins answered).
|
|
121
|
+
* @param {boolean} [params.keepProcessAlive=true] - When false the server alone does not keep
|
|
122
|
+
* the process alive.
|
|
123
|
+
* @param {boolean} [params.canExposeSensitiveData=false] - Lets the server hand out what
|
|
124
|
+
* belongs to the machine it runs on. Development only, never in production. It unlocks:
|
|
125
|
+
* - file paths in status texts (a 404 from `createFileSystemFetch` says which path);
|
|
126
|
+
* - the declaration source of every route in the route inspector, where every route is
|
|
127
|
+
* listed whatever its permissions;
|
|
128
|
+
* - `GET /.internal/open_file/*`, opening a file of the machine in the editor;
|
|
129
|
+
* - `GET /@jsenv/server/*`, serving this package's own client files;
|
|
130
|
+
* - `/.internal/alive.websocket` and `/.internal/alive.eventsource`, which a client
|
|
131
|
+
* subscribes to in order to reload when the server restarts.
|
|
132
|
+
* @param {boolean|Object} [params.serverTiming=false] - `true` or `{ minDuration }` to send
|
|
133
|
+
* `server-timing` response headers: the time to start responding, the routing of each
|
|
134
|
+
* plugin, what routes measure with `helpers.timing` and the `timing` a response hands
|
|
135
|
+
* back. `minDuration` (ms) drops the entries that took less: 0 keeps everything (what a
|
|
136
|
+
* test wants), a human reading devtools usually wants the sub-millisecond noise gone.
|
|
137
|
+
* @param {number} [params.requestWaitingMs=0] - Call `requestWaitingCallback` when a request
|
|
138
|
+
* still has no response after that many ms (0 disables).
|
|
139
|
+
* @param {Function} [params.requestWaitingCallback] - `({ request, requestWaitingMs })`, logs a warning by default.
|
|
140
|
+
* @param {number} [params.responseTimeout=600000] - Ms a route can take to start responding
|
|
141
|
+
* before a 504 is sent instead (10 minutes).
|
|
142
|
+
* @param {number} [params.requestBodyMaxSize=1048576] - Bytes `request.json()`, `text()`,
|
|
143
|
+
* `buffer()` and `queryString()` accept before answering 413 (1 MiB). A route can pass
|
|
144
|
+
* its own `{ maxSize }` to these readers; `request.body` is never limited.
|
|
105
145
|
*
|
|
106
|
-
* @
|
|
107
|
-
*
|
|
108
|
-
* -
|
|
109
|
-
* -
|
|
110
|
-
*
|
|
111
|
-
* -
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
146
|
+
* @returns {Promise<Object>} The server: `{ origin, origins, port, hostname, nodeServer,
|
|
147
|
+
* webSocketOrigin, stop, stoppedPromise, getStatus, addEffect }`.
|
|
148
|
+
* - `origins`: `{ local, localip, externalip }`, `origin` being `origins.local`.
|
|
149
|
+
* - `stop(reason)`: resolves once every connection is closed; `reason` can be anything
|
|
150
|
+
* and defaults to `STOP_REASON_NOT_SPECIFIED`.
|
|
151
|
+
* - `stoppedPromise`: resolves with the reason the server stopped for (one of the
|
|
152
|
+
* `STOP_REASON_*` exports or what was given to `stop`).
|
|
153
|
+
* - `getStatus()`: `"starting"`, `"opened"`, `"stopping"` or `"stopped"`.
|
|
154
|
+
* - `addEffect(callback)`: runs `callback` right away; the function it returns runs
|
|
155
|
+
* when the server stops.
|
|
115
156
|
*/
|
|
116
157
|
export const startServer = async ({
|
|
117
158
|
signal = new AbortController().signal,
|
|
@@ -127,6 +168,7 @@ export const startServer = async ({
|
|
|
127
168
|
allowHttpRequestOnHttps = false,
|
|
128
169
|
acceptAnyIp = false,
|
|
129
170
|
preferIpv6,
|
|
171
|
+
allowedHosts = [],
|
|
130
172
|
hostname = "localhost",
|
|
131
173
|
port = 0, // assign a random available port
|
|
132
174
|
portHint,
|
|
@@ -142,17 +184,7 @@ export const startServer = async ({
|
|
|
142
184
|
keepProcessAlive = true,
|
|
143
185
|
routes = [],
|
|
144
186
|
plugins = [],
|
|
145
|
-
// When enabled, the server gives more power:
|
|
146
|
-
// - each route show the source where it was declared
|
|
147
|
-
// - server can be requested to open a file on the machine
|
|
148
|
-
// - client can subscribe to server to detect when it restarts
|
|
149
|
-
// This param should be enabled ONLY during development on your machine
|
|
150
187
|
canExposeSensitiveData = false,
|
|
151
|
-
nagle = true,
|
|
152
|
-
// false | true | { minDuration }: server-timing response headers.
|
|
153
|
-
// minDuration (ms) drops entries that took less — 0 keeps everything, which
|
|
154
|
-
// is what a test wants; a human reading devtools usually wants the noise
|
|
155
|
-
// gone (the jsenv dev server passes 0.5 for that).
|
|
156
188
|
serverTiming = false,
|
|
157
189
|
requestWaitingMs = 0,
|
|
158
190
|
requestWaitingCallback = ({ request, requestWaitingMs }) => {
|
|
@@ -166,13 +198,8 @@ export const startServer = async ({
|
|
|
166
198
|
),
|
|
167
199
|
);
|
|
168
200
|
},
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
responseTimeout = 60_000 * 10, // 10s
|
|
172
|
-
// time allocated to server code to start reading the request body
|
|
173
|
-
// after this delay the underlying stream is destroyed, attempting to read it would throw
|
|
174
|
-
// if used the stream stays opened, it's only if the stream is not read at all that it gets destroyed
|
|
175
|
-
requestBodyLifetime = 60_000 * 2, // 2s
|
|
201
|
+
responseTimeout = 60_000 * 10, // 10 minutes
|
|
202
|
+
requestBodyMaxSize = 1024 * 1024,
|
|
176
203
|
...rest
|
|
177
204
|
} = {}) => {
|
|
178
205
|
// param validations
|
|
@@ -197,6 +224,11 @@ export const startServer = async ({
|
|
|
197
224
|
if (http2 && !https) {
|
|
198
225
|
throw new Error(`http2 needs https`);
|
|
199
226
|
}
|
|
227
|
+
if (allowedHosts !== true && !Array.isArray(allowedHosts)) {
|
|
228
|
+
throw new TypeError(
|
|
229
|
+
`allowedHosts must be an array of hosts or true, got ${allowedHosts}`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
200
232
|
}
|
|
201
233
|
const logger = createLogger({ logLevel });
|
|
202
234
|
// param warnings and normalization
|
|
@@ -268,9 +300,7 @@ export const startServer = async ({
|
|
|
268
300
|
let nodeServer;
|
|
269
301
|
const startServerOperation = Abort.startOperation();
|
|
270
302
|
const stopCallbackSet = new Set();
|
|
271
|
-
|
|
272
|
-
local: "", // favors hostname when possible
|
|
273
|
-
};
|
|
303
|
+
let serverOrigins;
|
|
274
304
|
|
|
275
305
|
try {
|
|
276
306
|
startServerOperation.addAbortSignal(signal);
|
|
@@ -295,63 +325,15 @@ export const startServer = async ({
|
|
|
295
325
|
nodeServer.unref();
|
|
296
326
|
}
|
|
297
327
|
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
let hostnameToListen;
|
|
308
|
-
if (acceptAnyIp) {
|
|
309
|
-
const firstInternalIp = ipGetters.getFirstInternalIp({ preferIpv6 });
|
|
310
|
-
serverOrigins.local = createOrigin(firstInternalIp);
|
|
311
|
-
serverOrigins.localip = createOrigin(firstInternalIp);
|
|
312
|
-
const firstExternalIp = ipGetters.getFirstExternalIp({ preferIpv6 });
|
|
313
|
-
serverOrigins.externalip = createOrigin(firstExternalIp);
|
|
314
|
-
hostnameToListen = preferIpv6 ? "::" : "0.0.0.0";
|
|
315
|
-
} else {
|
|
316
|
-
hostnameToListen = hostname;
|
|
317
|
-
}
|
|
318
|
-
const hostnameInfo = parseHostname(hostname);
|
|
319
|
-
if (hostnameInfo.type === "ip") {
|
|
320
|
-
if (acceptAnyIp) {
|
|
321
|
-
throw new Error(
|
|
322
|
-
`hostname cannot be an ip when acceptAnyIp is enabled, got ${hostname}`,
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
preferIpv6 = hostnameInfo.version === 6;
|
|
327
|
-
const firstInternalIp = ipGetters.getFirstInternalIp({ preferIpv6 });
|
|
328
|
-
serverOrigins.local = createOrigin(firstInternalIp);
|
|
329
|
-
serverOrigins.localip = createOrigin(firstInternalIp);
|
|
330
|
-
if (hostnameInfo.label === "unspecified") {
|
|
331
|
-
const firstExternalIp = ipGetters.getFirstExternalIp({ preferIpv6 });
|
|
332
|
-
serverOrigins.externalip = createOrigin(firstExternalIp);
|
|
333
|
-
} else if (hostnameInfo.label === "loopback") {
|
|
334
|
-
// nothing
|
|
335
|
-
} else {
|
|
336
|
-
serverOrigins.local = createOrigin(hostname);
|
|
337
|
-
}
|
|
338
|
-
} else {
|
|
339
|
-
const hostnameDnsResolution = await applyDnsResolution(hostname, {
|
|
340
|
-
verbatim: true,
|
|
341
|
-
});
|
|
342
|
-
if (hostnameDnsResolution) {
|
|
343
|
-
const hostnameIp = hostnameDnsResolution.address;
|
|
344
|
-
serverOrigins.localip = createOrigin(hostnameIp);
|
|
345
|
-
serverOrigins.local = createOrigin(hostname);
|
|
346
|
-
} else {
|
|
347
|
-
const firstInternalIp = ipGetters.getFirstInternalIp({ preferIpv6 });
|
|
348
|
-
// fallback to internal ip because there is no ip
|
|
349
|
-
// associated to this hostname on operating system (in hosts file)
|
|
350
|
-
hostname = firstInternalIp;
|
|
351
|
-
hostnameToListen = firstInternalIp;
|
|
352
|
-
serverOrigins.local = createOrigin(firstInternalIp);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
328
|
+
const resolved = await resolveServerOrigins({
|
|
329
|
+
https,
|
|
330
|
+
hostname,
|
|
331
|
+
acceptAnyIp,
|
|
332
|
+
preferIpv6,
|
|
333
|
+
});
|
|
334
|
+
hostname = resolved.hostname;
|
|
335
|
+
serverOrigins = resolved.serverOrigins;
|
|
336
|
+
const hostnameToListen = resolved.hostnameToListen;
|
|
355
337
|
|
|
356
338
|
port = await listen({
|
|
357
339
|
signal: startServerOperation.signal,
|
|
@@ -386,6 +368,12 @@ export const startServer = async ({
|
|
|
386
368
|
// so we prefer https://locahost or https://local_hostname
|
|
387
369
|
// over the ip
|
|
388
370
|
const serverOrigin = serverOrigins.local;
|
|
371
|
+
const isHostAllowed = createHostChecker({
|
|
372
|
+
allowedHosts,
|
|
373
|
+
hostname,
|
|
374
|
+
serverOrigins,
|
|
375
|
+
acceptAnyIp,
|
|
376
|
+
});
|
|
389
377
|
|
|
390
378
|
// now the server is started (listening) it cannot be aborted anymore
|
|
391
379
|
// (otherwise an AbortError is thrown to the code calling "startServer")
|
|
@@ -449,15 +437,11 @@ export const startServer = async ({
|
|
|
449
437
|
);
|
|
450
438
|
stopCallbackSet.add(removeConnectionErrorListener);
|
|
451
439
|
|
|
452
|
-
const connectionsTracker = trackServerPendingConnections(nodeServer
|
|
453
|
-
http2,
|
|
454
|
-
});
|
|
440
|
+
const connectionsTracker = trackServerPendingConnections(nodeServer);
|
|
455
441
|
// opened connection must be shutdown before the close event is emitted
|
|
456
442
|
stopCallbackSet.add(connectionsTracker.stop);
|
|
457
443
|
|
|
458
|
-
const pendingRequestsTracker = trackServerPendingRequests(nodeServer
|
|
459
|
-
http2,
|
|
460
|
-
});
|
|
444
|
+
const pendingRequestsTracker = trackServerPendingRequests(nodeServer);
|
|
461
445
|
// ensure pending requests got a response from the server
|
|
462
446
|
stopCallbackSet.add((reason) => {
|
|
463
447
|
pendingRequestsTracker.stop({
|
|
@@ -510,7 +494,7 @@ export const startServer = async ({
|
|
|
510
494
|
serverTiming && typeof serverTiming === "object"
|
|
511
495
|
? serverTiming.minDuration || 0
|
|
512
496
|
: 0;
|
|
513
|
-
const getResponseProperties = async (request
|
|
497
|
+
const getResponseProperties = async (request) => {
|
|
514
498
|
const timings = {};
|
|
515
499
|
const timing = serverTiming
|
|
516
500
|
? (name) => {
|
|
@@ -531,9 +515,7 @@ export const startServer = async ({
|
|
|
531
515
|
request.logger.info(
|
|
532
516
|
request.headers["upgrade"] === "websocket"
|
|
533
517
|
? `GET ${request.url} ${websocketSuffixColorized}`
|
|
534
|
-
: request.
|
|
535
|
-
? `Push ${request.resource}`
|
|
536
|
-
: `${request.method} ${request.url}`,
|
|
518
|
+
: `${request.method} ${request.url}`,
|
|
537
519
|
);
|
|
538
520
|
let requestWaitingTimeout;
|
|
539
521
|
if (requestWaitingMs) {
|
|
@@ -613,23 +595,22 @@ export const startServer = async ({
|
|
|
613
595
|
);
|
|
614
596
|
},
|
|
615
597
|
);
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
) {
|
|
623
|
-
clearTimeout(request.body.timeout);
|
|
624
|
-
}
|
|
598
|
+
serverPluginsController.callHooks("inspectResponse", request, {
|
|
599
|
+
response: responseProperties,
|
|
600
|
+
warn: (message) => {
|
|
601
|
+
request.logger.warn(message);
|
|
602
|
+
},
|
|
603
|
+
});
|
|
625
604
|
return responseProperties;
|
|
626
605
|
};
|
|
627
606
|
|
|
628
607
|
let timeout;
|
|
608
|
+
let timedOut = false;
|
|
629
609
|
try {
|
|
630
610
|
request = applyRequestInternalRedirection(request);
|
|
631
611
|
const timeoutResponsePropertiesPromise = new Promise((resolve) => {
|
|
632
612
|
timeout = setTimeout(() => {
|
|
613
|
+
timedOut = true;
|
|
633
614
|
resolve({
|
|
634
615
|
// the correct status code should be 500 because it's
|
|
635
616
|
// we don't really know what takes time
|
|
@@ -645,7 +626,7 @@ export const startServer = async ({
|
|
|
645
626
|
const routerResponsePropertiesPromise = (async () => {
|
|
646
627
|
const fetchSecondArg = {
|
|
647
628
|
timing,
|
|
648
|
-
|
|
629
|
+
canExposeSensitiveData,
|
|
649
630
|
injectResponseHeader: (name, value) => {
|
|
650
631
|
if (!headersToInject) {
|
|
651
632
|
headersToInject = {};
|
|
@@ -667,54 +648,28 @@ export const startServer = async ({
|
|
|
667
648
|
}
|
|
668
649
|
},
|
|
669
650
|
);
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
const permissionsSet = new Set();
|
|
675
|
-
const nextPermissionsHook =
|
|
676
|
-
serverPluginsController.createAsyncHookIterator(
|
|
677
|
-
"grantPermissions",
|
|
678
|
-
request,
|
|
679
|
-
);
|
|
680
|
-
const drainPermissionsUntil = async (permissionsRequired) => {
|
|
681
|
-
for (;;) {
|
|
682
|
-
if (
|
|
683
|
-
permissionsRequired !== undefined &&
|
|
684
|
-
permissionsSatisfy(permissionsSet, permissionsRequired)
|
|
685
|
-
) {
|
|
686
|
-
return true;
|
|
687
|
-
}
|
|
688
|
-
const { done, value } = await nextPermissionsHook();
|
|
689
|
-
if (done) {
|
|
690
|
-
break;
|
|
691
|
-
}
|
|
692
|
-
if (Array.isArray(value)) {
|
|
693
|
-
for (const p of value) {
|
|
694
|
-
permissionsSet.add(p);
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
return false;
|
|
699
|
-
};
|
|
700
|
-
const getAllPermissions = async () => {
|
|
701
|
-
await drainPermissionsUntil(undefined);
|
|
702
|
-
return permissionsSet;
|
|
703
|
-
};
|
|
704
|
-
const hasPermissions = async (permissionsRequired) => {
|
|
705
|
-
if (permissionsRequired.length === 0) {
|
|
706
|
-
return true;
|
|
707
|
-
}
|
|
708
|
-
return drainPermissionsUntil(permissionsRequired);
|
|
709
|
-
};
|
|
710
|
-
fetchSecondArg.getAllPermissions = getAllPermissions;
|
|
711
|
-
fetchSecondArg.hasPermissions = hasPermissions;
|
|
651
|
+
Object.assign(
|
|
652
|
+
fetchSecondArg,
|
|
653
|
+
createPermissionHelpers(serverPluginsController, request),
|
|
654
|
+
);
|
|
712
655
|
const routerResponseProperties = await router.match(
|
|
713
656
|
request,
|
|
714
657
|
fetchSecondArg,
|
|
715
658
|
);
|
|
716
659
|
return routerResponseProperties;
|
|
717
660
|
})();
|
|
661
|
+
// once the 504 is sent the route keeps running: what it throws
|
|
662
|
+
// afterwards would have nobody to reject to
|
|
663
|
+
routerResponsePropertiesPromise.catch((e) => {
|
|
664
|
+
if (timedOut) {
|
|
665
|
+
logger.error(
|
|
666
|
+
createDetailedMessage(`error after the 504 timeout response`, {
|
|
667
|
+
"request url": request.url,
|
|
668
|
+
"error stack": e.stack,
|
|
669
|
+
}),
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
});
|
|
718
673
|
const responseProperties = await Promise.race([
|
|
719
674
|
timeoutResponsePropertiesPromise,
|
|
720
675
|
routerResponsePropertiesPromise,
|
|
@@ -727,6 +682,21 @@ export const startServer = async ({
|
|
|
727
682
|
// let it propagate to the caller that should catch this
|
|
728
683
|
throw e;
|
|
729
684
|
}
|
|
685
|
+
// an error carrying its own response (a request body too large for
|
|
686
|
+
// instance) is not an internal error: it is answered as such, whatever
|
|
687
|
+
// the plugins
|
|
688
|
+
if (e && typeof e.asResponse === "function") {
|
|
689
|
+
const responseProperties = composeTwoResponses(
|
|
690
|
+
{
|
|
691
|
+
status: 500,
|
|
692
|
+
headers: {
|
|
693
|
+
"cache-control": "no-store",
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
await e.asResponse(),
|
|
697
|
+
);
|
|
698
|
+
return finalizeResponseProperties(responseProperties);
|
|
699
|
+
}
|
|
730
700
|
// internal error, create 500 response
|
|
731
701
|
if (
|
|
732
702
|
// stopOnInternalError stops server only if requestToResponse generated
|
|
@@ -764,20 +734,28 @@ export const startServer = async ({
|
|
|
764
734
|
return finalizeResponseProperties(responseProperties);
|
|
765
735
|
}
|
|
766
736
|
};
|
|
737
|
+
// When a response is sent before the request body was read (a 413, a 404
|
|
738
|
+
// on an upload...) node keeps draining what the client sends so that the
|
|
739
|
+
// client gets to read the response, as nginx does with its lingering close.
|
|
740
|
+
// A client that has not finished sending after that delay is cut off:
|
|
741
|
+
// draining a body forever is what the 413 was meant to avoid.
|
|
742
|
+
const lingerAfterEarlyResponse = (nodeRequest) => {
|
|
743
|
+
if (nodeRequest.complete) {
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
const lingerTimeout = setTimeout(() => {
|
|
747
|
+
nodeRequest.socket.destroy();
|
|
748
|
+
}, REQUEST_BODY_LINGER_MS).unref();
|
|
749
|
+
nodeRequest.once("close", () => {
|
|
750
|
+
clearTimeout(lingerTimeout);
|
|
751
|
+
});
|
|
752
|
+
};
|
|
767
753
|
const sendResponse = async (
|
|
768
754
|
responseStream,
|
|
769
755
|
responseProperties,
|
|
770
756
|
{ signal, request },
|
|
771
757
|
) => {
|
|
772
|
-
// When "pushResponse" is called and the parent response has no body
|
|
773
|
-
// the parent response is immediatly ended. It means child responses (pushed streams)
|
|
774
|
-
// won't get a chance to be pushed.
|
|
775
|
-
// To let a chance to pushed streams we wait a little before sending the response
|
|
776
758
|
const ignoreBody = request.method === "HEAD";
|
|
777
|
-
const bodyIsEmpty = !responseProperties.body || ignoreBody;
|
|
778
|
-
if (bodyIsEmpty && request.logger.hasPushChild) {
|
|
779
|
-
await new Promise((resolve) => setTimeout(resolve));
|
|
780
|
-
}
|
|
781
759
|
await writeNodeResponse(responseStream, responseProperties, {
|
|
782
760
|
signal,
|
|
783
761
|
ignoreBody,
|
|
@@ -808,6 +786,15 @@ export const startServer = async ({
|
|
|
808
786
|
|
|
809
787
|
request: {
|
|
810
788
|
const requestEventHandler = async (nodeRequest, nodeResponse) => {
|
|
789
|
+
const requestHost = nodeRequest.authority || nodeRequest.headers.host;
|
|
790
|
+
if (!isHostAllowed(requestHost)) {
|
|
791
|
+
logger.warn(
|
|
792
|
+
`${nodeRequest.method} ${nodeRequest.url} refused: host "${requestHost}" is not allowed (see the allowedHosts option)`,
|
|
793
|
+
);
|
|
794
|
+
nodeResponse.writeHead(403, { "content-type": "text/plain" });
|
|
795
|
+
nodeResponse.end("Host not allowed");
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
811
798
|
if (redirectHttpToHttps && !nodeRequest.connection.encrypted) {
|
|
812
799
|
nodeResponse.writeHead(301, {
|
|
813
800
|
location: `${serverOrigin}${nodeRequest.url}`,
|
|
@@ -826,164 +813,29 @@ export const startServer = async ({
|
|
|
826
813
|
|
|
827
814
|
const [receiveRequestOperation, sendResponseOperation] =
|
|
828
815
|
prepareHandleRequestOperations(nodeRequest, nodeResponse);
|
|
829
|
-
|
|
830
|
-
signal: stopAbortSignal,
|
|
831
|
-
serverOrigin,
|
|
832
|
-
requestBodyLifetime,
|
|
833
|
-
logger,
|
|
834
|
-
nagle,
|
|
835
|
-
});
|
|
836
|
-
|
|
816
|
+
let request;
|
|
837
817
|
try {
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
`response push ignored because path is invalid (must be a string starting with "/", found ${path})`,
|
|
844
|
-
);
|
|
845
|
-
return;
|
|
846
|
-
}
|
|
847
|
-
if (!request.http2) {
|
|
848
|
-
pushRequestLogger.warn(
|
|
849
|
-
`response push ignored because request is not http2`,
|
|
850
|
-
);
|
|
851
|
-
return;
|
|
852
|
-
}
|
|
853
|
-
const canPushStream = testCanPushStream(nodeResponse.stream);
|
|
854
|
-
if (!canPushStream.can) {
|
|
855
|
-
pushRequestLogger.debug(
|
|
856
|
-
`response push ignored because ${canPushStream.reason}`,
|
|
857
|
-
);
|
|
858
|
-
return;
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
let preventedByPlugin = null;
|
|
862
|
-
const prevent = () => {
|
|
863
|
-
preventedByPlugin = serverPluginsController.getCurrentPlugin();
|
|
864
|
-
};
|
|
865
|
-
serverPluginsController.callHooksUntil(
|
|
866
|
-
"onResponsePush",
|
|
867
|
-
{ path, method },
|
|
868
|
-
{ request, prevent },
|
|
869
|
-
() => preventedByPlugin,
|
|
870
|
-
);
|
|
871
|
-
if (preventedByPlugin) {
|
|
872
|
-
pushRequestLogger.debug(
|
|
873
|
-
`response push prevented by "${preventedByPlugin.name}" plugin`,
|
|
874
|
-
);
|
|
875
|
-
return;
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
const http2Stream = nodeResponse.stream;
|
|
879
|
-
|
|
880
|
-
// being able to push a stream is nice to have
|
|
881
|
-
// so when it fails it's not critical
|
|
882
|
-
const onPushStreamError = (e) => {
|
|
883
|
-
pushRequestLogger.error(
|
|
884
|
-
createDetailedMessage(
|
|
885
|
-
`An error occured while pushing a stream to the response for ${request.resource}`,
|
|
886
|
-
{
|
|
887
|
-
"error stack": e.stack,
|
|
888
|
-
},
|
|
889
|
-
),
|
|
890
|
-
);
|
|
891
|
-
};
|
|
892
|
-
|
|
893
|
-
// not aborted, let's try to push a stream into that response
|
|
894
|
-
// https://nodejs.org/docs/latest-v16.x/api/http2.html#http2streampushstreamheaders-options-callback
|
|
895
|
-
let pushStream;
|
|
896
|
-
try {
|
|
897
|
-
pushStream = await new Promise((resolve, reject) => {
|
|
898
|
-
http2Stream.pushStream(
|
|
899
|
-
{
|
|
900
|
-
":path": path,
|
|
901
|
-
...(method ? { ":method": method } : {}),
|
|
902
|
-
},
|
|
903
|
-
async (
|
|
904
|
-
error,
|
|
905
|
-
pushStream,
|
|
906
|
-
// headers
|
|
907
|
-
) => {
|
|
908
|
-
if (error) {
|
|
909
|
-
reject(error);
|
|
910
|
-
}
|
|
911
|
-
resolve(pushStream);
|
|
912
|
-
},
|
|
913
|
-
);
|
|
914
|
-
});
|
|
915
|
-
} catch (e) {
|
|
916
|
-
onPushStreamError(e);
|
|
917
|
-
return;
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
const abortController = new AbortController();
|
|
921
|
-
// It's possible to get NGHTTP2_REFUSED_STREAM errors here
|
|
922
|
-
// https://github.com/nodejs/node/issues/20824
|
|
923
|
-
const pushErrorCallback = (error) => {
|
|
924
|
-
onPushStreamError(error);
|
|
925
|
-
abortController.abort();
|
|
926
|
-
};
|
|
927
|
-
pushStream.on("error", pushErrorCallback);
|
|
928
|
-
sendResponseOperation.addEndCallback(() => {
|
|
929
|
-
pushStream.removeListener("error", onPushStreamError);
|
|
930
|
-
});
|
|
931
|
-
|
|
932
|
-
await sendResponseOperation.withSignal(async (signal) => {
|
|
933
|
-
const pushResponseOperation = Abort.startOperation();
|
|
934
|
-
pushResponseOperation.addAbortSignal(signal);
|
|
935
|
-
pushResponseOperation.addAbortSignal(abortController.signal);
|
|
936
|
-
|
|
937
|
-
const pushRequest = createPushRequest(request, {
|
|
938
|
-
signal: pushResponseOperation.signal,
|
|
939
|
-
pathname: path,
|
|
940
|
-
method,
|
|
941
|
-
logger: pushRequestLogger,
|
|
942
|
-
});
|
|
943
|
-
|
|
944
|
-
try {
|
|
945
|
-
const responseProperties = await getResponseProperties(
|
|
946
|
-
pushRequest,
|
|
947
|
-
{
|
|
948
|
-
pushResponse: () => {
|
|
949
|
-
pushRequest.logger.warn(
|
|
950
|
-
`response push ignored because nested push is not supported`,
|
|
951
|
-
);
|
|
952
|
-
},
|
|
953
|
-
},
|
|
954
|
-
);
|
|
955
|
-
if (!abortController.signal.aborted) {
|
|
956
|
-
if (pushStream.destroyed) {
|
|
957
|
-
abortController.abort();
|
|
958
|
-
} else if (!http2Stream.pushAllowed) {
|
|
959
|
-
abortController.abort();
|
|
960
|
-
} else if (responseProperties.requestAborted) {
|
|
961
|
-
} else {
|
|
962
|
-
const responseLength =
|
|
963
|
-
responseProperties.headers["content-length"] || 0;
|
|
964
|
-
const { effectiveRecvDataLength, remoteWindowSize } =
|
|
965
|
-
http2Stream.session.state;
|
|
966
|
-
if (
|
|
967
|
-
effectiveRecvDataLength + responseLength >
|
|
968
|
-
remoteWindowSize
|
|
969
|
-
) {
|
|
970
|
-
pushRequest.logger.debug(
|
|
971
|
-
`Aborting stream to prevent exceeding remoteWindowSize`,
|
|
972
|
-
);
|
|
973
|
-
abortController.abort();
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
await sendResponse(pushStream, responseProperties, {
|
|
978
|
-
signal: pushResponseOperation.signal,
|
|
979
|
-
request: pushRequest,
|
|
980
|
-
});
|
|
981
|
-
} finally {
|
|
982
|
-
await pushResponseOperation.end();
|
|
983
|
-
}
|
|
984
|
-
});
|
|
985
|
-
},
|
|
818
|
+
request = fromNodeRequest(nodeRequest, {
|
|
819
|
+
signal: stopAbortSignal,
|
|
820
|
+
serverOrigin,
|
|
821
|
+
logger,
|
|
822
|
+
requestBodyMaxSize,
|
|
986
823
|
});
|
|
824
|
+
} catch (e) {
|
|
825
|
+
// the request cannot even be read: there is nothing to hand to the routes
|
|
826
|
+
logger.error(
|
|
827
|
+
createDetailedMessage(`error while reading request`, {
|
|
828
|
+
"request url": nodeRequest.url,
|
|
829
|
+
"error stack": e.stack,
|
|
830
|
+
}),
|
|
831
|
+
);
|
|
832
|
+
nodeResponse.writeHead(500);
|
|
833
|
+
nodeResponse.end();
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
try {
|
|
838
|
+
const responseProperties = await getResponseProperties(request);
|
|
987
839
|
const webSocketHandler = getWebSocketHandler(responseProperties);
|
|
988
840
|
if (webSocketHandler) {
|
|
989
841
|
throw new Error(
|
|
@@ -997,6 +849,7 @@ export const startServer = async ({
|
|
|
997
849
|
signal: sendResponseOperation.signal,
|
|
998
850
|
request,
|
|
999
851
|
});
|
|
852
|
+
lingerAfterEarlyResponse(nodeRequest);
|
|
1000
853
|
} finally {
|
|
1001
854
|
await sendResponseOperation.end();
|
|
1002
855
|
}
|
|
@@ -1038,22 +891,39 @@ export const startServer = async ({
|
|
|
1038
891
|
};
|
|
1039
892
|
// https://github.com/websockets/ws/blob/b92745a9d6760e6b4b2394bfac78cbcd258a8c8d/lib/websocket-server.js#L491
|
|
1040
893
|
const upgradeEventHandler = async (nodeRequest, socket, head) => {
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
894
|
+
const requestHost = nodeRequest.headers.host;
|
|
895
|
+
if (!isHostAllowed(requestHost)) {
|
|
896
|
+
logger.warn(
|
|
897
|
+
`${nodeRequest.method} ${nodeRequest.url} refused: host "${requestHost}" is not allowed (see the allowedHosts option)`,
|
|
898
|
+
);
|
|
899
|
+
socket.write(
|
|
900
|
+
`HTTP/1.1 403 Forbidden\r\ncontent-type: text/plain\r\ncontent-length: 16\r\n\r\nHost not allowed`,
|
|
901
|
+
);
|
|
902
|
+
socket.destroy();
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
let request;
|
|
906
|
+
try {
|
|
907
|
+
request = fromNodeRequest(nodeRequest, {
|
|
908
|
+
signal: stopAbortSignal,
|
|
909
|
+
serverOrigin,
|
|
910
|
+
logger,
|
|
911
|
+
requestBodyMaxSize,
|
|
912
|
+
});
|
|
913
|
+
} catch (e) {
|
|
914
|
+
logger.error(
|
|
915
|
+
createDetailedMessage(`error while reading upgrade request`, {
|
|
916
|
+
"request url": nodeRequest.url,
|
|
917
|
+
"error stack": e.stack,
|
|
918
|
+
}),
|
|
919
|
+
);
|
|
920
|
+
socket.write(`HTTP/1.1 500 Internal Server Error\r\n\r\n`);
|
|
921
|
+
socket.destroy();
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
1048
924
|
const [receiveRequestOperation, sendResponseOperation] =
|
|
1049
925
|
prepareHandleRequestOperations(nodeRequest, socket);
|
|
1050
|
-
const responseProperties = await getResponseProperties(request
|
|
1051
|
-
pushResponse: () => {
|
|
1052
|
-
request.logger.warn(
|
|
1053
|
-
`pushResponse ignored because it's not supported in websocket`,
|
|
1054
|
-
);
|
|
1055
|
-
},
|
|
1056
|
-
});
|
|
926
|
+
const responseProperties = await getResponseProperties(request);
|
|
1057
927
|
if (receiveRequestOperation.signal.aborted) {
|
|
1058
928
|
return;
|
|
1059
929
|
}
|
|
@@ -1105,7 +975,7 @@ export const startServer = async ({
|
|
|
1105
975
|
}
|
|
1106
976
|
return;
|
|
1107
977
|
};
|
|
1108
|
-
// see
|
|
978
|
+
// see server_polyglot.js, upgrade must be listened on https server when used
|
|
1109
979
|
const facadeServer = nodeServer._tlsServer || nodeServer;
|
|
1110
980
|
const removeUpgradeCallback = listenEvent(
|
|
1111
981
|
facadeServer,
|
|
@@ -1122,9 +992,9 @@ export const startServer = async ({
|
|
|
1122
992
|
}
|
|
1123
993
|
|
|
1124
994
|
if (startLog) {
|
|
1125
|
-
if (serverOrigins.
|
|
995
|
+
if (serverOrigins.externalip) {
|
|
1126
996
|
logger.info(
|
|
1127
|
-
`${serverName} started at ${serverOrigins.local} (${serverOrigins.
|
|
997
|
+
`${serverName} started at ${serverOrigins.local} (${serverOrigins.externalip})`,
|
|
1128
998
|
);
|
|
1129
999
|
} else {
|
|
1130
1000
|
logger.info(`${serverName} started at ${serverOrigins.local}`);
|
|
@@ -1167,38 +1037,18 @@ const createNodeServer = async ({
|
|
|
1167
1037
|
http1Allowed,
|
|
1168
1038
|
});
|
|
1169
1039
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1040
|
+
return createSecureServer({
|
|
1041
|
+
certificate,
|
|
1042
|
+
privateKey,
|
|
1043
|
+
http2,
|
|
1044
|
+
http1Allowed,
|
|
1174
1045
|
});
|
|
1175
1046
|
}
|
|
1176
1047
|
const { createServer } = await import("node:http");
|
|
1177
1048
|
return createServer();
|
|
1178
1049
|
};
|
|
1179
1050
|
|
|
1180
|
-
const
|
|
1181
|
-
if (!http2Stream.pushAllowed) {
|
|
1182
|
-
return {
|
|
1183
|
-
can: false,
|
|
1184
|
-
reason: `stream.pushAllowed is false`,
|
|
1185
|
-
};
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
// See https://nodejs.org/dist/latest-v16.x/docs/api/http2.html#http2sessionstate
|
|
1189
|
-
// And https://github.com/google/node-h2-auto-push/blob/67a36c04cbbd6da7b066a4e8d361c593d38853a4/src/index.ts#L100-L106
|
|
1190
|
-
const { remoteWindowSize } = http2Stream.session.state;
|
|
1191
|
-
if (remoteWindowSize === 0) {
|
|
1192
|
-
return {
|
|
1193
|
-
can: false,
|
|
1194
|
-
reason: `no more remoteWindowSize`,
|
|
1195
|
-
};
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
return {
|
|
1199
|
-
can: true,
|
|
1200
|
-
};
|
|
1201
|
-
};
|
|
1051
|
+
const REQUEST_BODY_LINGER_MS = 5_000;
|
|
1202
1052
|
|
|
1203
1053
|
const PROCESS_TEARDOWN_EVENTS_MAP = {
|
|
1204
1054
|
SIGHUP: STOP_REASON_PROCESS_SIGHUP,
|