@jsenv/server 12.8.0 → 14.0.0
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 +66 -81
- package/package.json +3 -2
- package/src/content_negotiation/{applyContentNegotiation.js → pick_accepted_content.js} +1 -3
- package/src/content_negotiation/{negotiateContentEncoding.js → pick_content_encoding.js} +3 -3
- package/src/content_negotiation/{negotiateContentLanguage.js → pick_content_language.js} +3 -3
- package/src/content_negotiation/{negotiateContentType.js → pick_content_type.js} +3 -3
- package/src/fetch_filesystem.js +2 -2
- package/src/internal/dns_resolution.js +17 -0
- package/src/internal/hostname_parser.js +48 -0
- package/src/internal/listen.js +15 -13
- package/src/internal/request_factory.js +3 -1
- package/src/internal/server-polyglot.js +1 -0
- package/src/internal/server_ips.js +49 -0
- package/src/main.js +11 -13
- package/src/serve_directory.js +2 -2
- package/src/service_controller.js +174 -0
- package/src/{cors/plugin_cors.js → services/cors/jsenv_service_cors.js} +29 -45
- package/src/services/error_handler/jsenv_service_error_handler.js +98 -0
- package/src/{content_negotiation/checkContentNegotiation.js → services/response_acceptance_check/jsenv_service_response_acceptance_check.js} +16 -7
- package/src/services/ressource_aliases/jsenv_service_ressource_aliases.js +40 -0
- package/src/sse/{createSSERoom.js → sse_room.js} +18 -16
- package/src/start_server.js +1045 -0
- package/src/content_negotiation/plugin_content_negotiation_check.js +0 -15
- package/src/error_to_response_default.js +0 -94
- package/src/internal/getServerOrigins.js +0 -63
- package/src/plugin_request_waiting_check.js +0 -32
- package/src/ressource_aliases/plugin_ressource_aliases.js +0 -44
- package/src/server_timing/plugin_server_timing.js +0 -28
- package/src/service_composition/service_composition.js +0 -55
- package/src/startServer.js +0 -868
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { checkContentNegotiation } from "./checkContentNegotiation.js"
|
|
2
|
-
|
|
3
|
-
export const pluginContentNegotiationCheck = () => {
|
|
4
|
-
return {
|
|
5
|
-
content_negotiation_check: {
|
|
6
|
-
onRequest: (request, { warn }) => {
|
|
7
|
-
return {
|
|
8
|
-
onResponse: (response) => {
|
|
9
|
-
checkContentNegotiation(request, response, { warn })
|
|
10
|
-
},
|
|
11
|
-
}
|
|
12
|
-
},
|
|
13
|
-
},
|
|
14
|
-
}
|
|
15
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { negotiateContentType } from "./content_negotiation/negotiateContentType.js"
|
|
2
|
-
|
|
3
|
-
export const applyDefaultErrorToResponse = (
|
|
4
|
-
serverInternalError,
|
|
5
|
-
{ request, sendErrorDetails = false },
|
|
6
|
-
) => {
|
|
7
|
-
const serverInternalErrorIsAPrimitive =
|
|
8
|
-
serverInternalError === null ||
|
|
9
|
-
(typeof serverInternalError !== "object" &&
|
|
10
|
-
typeof serverInternalError !== "function")
|
|
11
|
-
|
|
12
|
-
if (!serverInternalErrorIsAPrimitive && serverInternalError.asResponse) {
|
|
13
|
-
return serverInternalError.asResponse()
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const dataToSend = serverInternalErrorIsAPrimitive
|
|
17
|
-
? {
|
|
18
|
-
code: "VALUE_THROWED",
|
|
19
|
-
value: serverInternalError,
|
|
20
|
-
}
|
|
21
|
-
: {
|
|
22
|
-
code: serverInternalError.code || "UNKNOWN_ERROR",
|
|
23
|
-
...(sendErrorDetails
|
|
24
|
-
? {
|
|
25
|
-
stack: serverInternalError.stack,
|
|
26
|
-
...serverInternalError,
|
|
27
|
-
}
|
|
28
|
-
: {}),
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const availableContentTypes = {
|
|
32
|
-
"text/html": () => {
|
|
33
|
-
const renderHtmlForErrorWithoutDetails = () => {
|
|
34
|
-
return `<p>Details not available: to enable them server must be started with sendErrorDetails: true.</p>`
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const renderHtmlForErrorWithDetails = () => {
|
|
38
|
-
if (serverInternalErrorIsAPrimitive) {
|
|
39
|
-
return `<pre>${JSON.stringify(serverInternalError, null, " ")}</pre>`
|
|
40
|
-
}
|
|
41
|
-
return `<pre>${serverInternalError.stack}</pre>`
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const body = `<!DOCTYPE html>
|
|
45
|
-
<html>
|
|
46
|
-
<head>
|
|
47
|
-
<title>Internal server error</title>
|
|
48
|
-
<meta charset="utf-8" />
|
|
49
|
-
<link rel="icon" href="data:," />
|
|
50
|
-
</head>
|
|
51
|
-
|
|
52
|
-
<body>
|
|
53
|
-
<h1>Internal server error</h1>
|
|
54
|
-
<p>${
|
|
55
|
-
serverInternalErrorIsAPrimitive
|
|
56
|
-
? `Code inside server has thrown a literal.`
|
|
57
|
-
: `Code inside server has thrown an error.`
|
|
58
|
-
}</p>
|
|
59
|
-
<details>
|
|
60
|
-
<summary>See internal error details</summary>
|
|
61
|
-
${
|
|
62
|
-
sendErrorDetails
|
|
63
|
-
? renderHtmlForErrorWithDetails()
|
|
64
|
-
: renderHtmlForErrorWithoutDetails()
|
|
65
|
-
}
|
|
66
|
-
</details>
|
|
67
|
-
</body>
|
|
68
|
-
</html>`
|
|
69
|
-
|
|
70
|
-
return {
|
|
71
|
-
headers: {
|
|
72
|
-
"content-type": "text/html",
|
|
73
|
-
"content-length": Buffer.byteLength(body),
|
|
74
|
-
},
|
|
75
|
-
body,
|
|
76
|
-
}
|
|
77
|
-
},
|
|
78
|
-
"application/json": () => {
|
|
79
|
-
const body = JSON.stringify(dataToSend)
|
|
80
|
-
return {
|
|
81
|
-
headers: {
|
|
82
|
-
"content-type": "application/json",
|
|
83
|
-
"content-length": Buffer.byteLength(body),
|
|
84
|
-
},
|
|
85
|
-
body,
|
|
86
|
-
}
|
|
87
|
-
},
|
|
88
|
-
}
|
|
89
|
-
const bestContentType = negotiateContentType(
|
|
90
|
-
request,
|
|
91
|
-
Object.keys(availableContentTypes),
|
|
92
|
-
)
|
|
93
|
-
return availableContentTypes[bestContentType || "application/json"]()
|
|
94
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { networkInterfaces } from "node:os"
|
|
2
|
-
import { lookup } from "node:dns"
|
|
3
|
-
|
|
4
|
-
export const getServerOrigins = async ({ protocol, ip, port }) => {
|
|
5
|
-
const isInternalIp = ip === "127.0.0.1"
|
|
6
|
-
const localhostDnsResolution = await applyDnsResolution("localhost")
|
|
7
|
-
const internalOrigin = createServerOrigin({
|
|
8
|
-
protocol,
|
|
9
|
-
hostname:
|
|
10
|
-
localhostDnsResolution.address === "127.0.0.1"
|
|
11
|
-
? "localhost"
|
|
12
|
-
: "127.0.0.1",
|
|
13
|
-
port,
|
|
14
|
-
})
|
|
15
|
-
if (isInternalIp) {
|
|
16
|
-
return { internal: internalOrigin }
|
|
17
|
-
}
|
|
18
|
-
const isAnyIp = !ip || ip === "::" || ip === "0.0.0.0"
|
|
19
|
-
return {
|
|
20
|
-
internal: internalOrigin,
|
|
21
|
-
external: createServerOrigin({
|
|
22
|
-
protocol,
|
|
23
|
-
hostname: isAnyIp ? getExternalIp(ip) : ip,
|
|
24
|
-
port,
|
|
25
|
-
}),
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const applyDnsResolution = async (hostname) => {
|
|
30
|
-
const dnsResolution = await new Promise((resolve, reject) => {
|
|
31
|
-
lookup(hostname, (error, address, family) => {
|
|
32
|
-
if (error) {
|
|
33
|
-
reject(error)
|
|
34
|
-
} else {
|
|
35
|
-
resolve({ address, family })
|
|
36
|
-
}
|
|
37
|
-
})
|
|
38
|
-
})
|
|
39
|
-
return dnsResolution
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const createServerOrigin = ({ protocol, hostname, port }) => {
|
|
43
|
-
const url = new URL("https://127.0.0.1:80")
|
|
44
|
-
url.protocol = protocol
|
|
45
|
-
url.hostname = hostname
|
|
46
|
-
url.port = port
|
|
47
|
-
return url.origin
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const getExternalIp = () => {
|
|
51
|
-
const networkInterfaceMap = networkInterfaces()
|
|
52
|
-
let internalIPV4NetworkAddress
|
|
53
|
-
Object.keys(networkInterfaceMap).find((key) => {
|
|
54
|
-
const networkAddressArray = networkInterfaceMap[key]
|
|
55
|
-
return networkAddressArray.find((networkAddress) => {
|
|
56
|
-
if (networkAddress.internal) return false
|
|
57
|
-
if (networkAddress.family !== "IPv4") return false
|
|
58
|
-
internalIPV4NetworkAddress = networkAddress
|
|
59
|
-
return true
|
|
60
|
-
})
|
|
61
|
-
})
|
|
62
|
-
return internalIPV4NetworkAddress ? internalIPV4NetworkAddress.address : null
|
|
63
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { createDetailedMessage } from "@jsenv/log"
|
|
2
|
-
|
|
3
|
-
export const pluginRequestWaitingCheck = ({
|
|
4
|
-
requestWaitingMs = 20000,
|
|
5
|
-
requestWaitingCallback = ({ request, logger, requestWaitingMs }) => {
|
|
6
|
-
logger.warn(
|
|
7
|
-
createDetailedMessage(
|
|
8
|
-
`still no response found for request after ${requestWaitingMs} ms`,
|
|
9
|
-
{
|
|
10
|
-
"request url": `${request.origin}${request.ressource}`,
|
|
11
|
-
"request headers": JSON.stringify(request.headers, null, " "),
|
|
12
|
-
},
|
|
13
|
-
),
|
|
14
|
-
)
|
|
15
|
-
},
|
|
16
|
-
} = {}) => {
|
|
17
|
-
return {
|
|
18
|
-
plugin_request_waiting_check: {
|
|
19
|
-
onRequest: (request, { logger }) => {
|
|
20
|
-
const timeout = setTimeout(
|
|
21
|
-
() => requestWaitingCallback({ request, logger, requestWaitingMs }),
|
|
22
|
-
requestWaitingMs,
|
|
23
|
-
)
|
|
24
|
-
return {
|
|
25
|
-
onResponse: () => {
|
|
26
|
-
clearTimeout(timeout)
|
|
27
|
-
},
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
}
|
|
32
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { URL_META } from "@jsenv/url-meta"
|
|
2
|
-
|
|
3
|
-
export const pluginRessourceAliases = (ressourceAliases) => {
|
|
4
|
-
const aliases = {}
|
|
5
|
-
Object.keys(ressourceAliases).forEach((key) => {
|
|
6
|
-
aliases[asFileUrl(key)] = asFileUrl(ressourceAliases[key])
|
|
7
|
-
})
|
|
8
|
-
return {
|
|
9
|
-
ressource_aliases: {
|
|
10
|
-
onRequest: (request, { redirectRequest }) => {
|
|
11
|
-
const ressourceBeforeAlias = request.ressource
|
|
12
|
-
const urlAfterAliasing = URL_META.applyAliases({
|
|
13
|
-
url: asFileUrl(request.pathname),
|
|
14
|
-
aliases,
|
|
15
|
-
})
|
|
16
|
-
const ressourceAfterAlias = urlAfterAliasing.slice("file://".length)
|
|
17
|
-
if (ressourceBeforeAlias === ressourceAfterAlias) {
|
|
18
|
-
return
|
|
19
|
-
}
|
|
20
|
-
const ressource = replaceRessource(
|
|
21
|
-
ressourceBeforeAlias,
|
|
22
|
-
ressourceAfterAlias,
|
|
23
|
-
)
|
|
24
|
-
redirectRequest({
|
|
25
|
-
ressourceBeforeAlias,
|
|
26
|
-
ressource,
|
|
27
|
-
})
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const asFileUrl = (specifier) => new URL(specifier, "file:///").href
|
|
34
|
-
|
|
35
|
-
const replaceRessource = (ressourceBeforeAlias, newValue) => {
|
|
36
|
-
const urlObject = new URL(ressourceBeforeAlias, "file://")
|
|
37
|
-
const searchSeparatorIndex = newValue.indexOf("?")
|
|
38
|
-
if (searchSeparatorIndex > -1) {
|
|
39
|
-
return newValue // let new value override search params
|
|
40
|
-
}
|
|
41
|
-
urlObject.pathname = newValue
|
|
42
|
-
const ressource = `${urlObject.pathname}${urlObject.search}`
|
|
43
|
-
return ressource
|
|
44
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
// https://www.w3.org/TR/server-timing/
|
|
2
|
-
|
|
3
|
-
import { performance } from "node:perf_hooks"
|
|
4
|
-
|
|
5
|
-
import { timingToServerTimingResponseHeaders } from "./timing_header.js"
|
|
6
|
-
|
|
7
|
-
export const pluginServerTiming = () => {
|
|
8
|
-
return {
|
|
9
|
-
server_timing: {
|
|
10
|
-
onRequest: () => {
|
|
11
|
-
const requestStartEvent = performance.now()
|
|
12
|
-
return {
|
|
13
|
-
onResponse: (response) => {
|
|
14
|
-
const responseEndEvent = performance.now()
|
|
15
|
-
const timeToCreateResponse = responseEndEvent - requestStartEvent
|
|
16
|
-
const serverTiming = {
|
|
17
|
-
...response.timing,
|
|
18
|
-
"time to start responding": timeToCreateResponse,
|
|
19
|
-
}
|
|
20
|
-
return {
|
|
21
|
-
headers: timingToServerTimingResponseHeaders(serverTiming),
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
|
-
}
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
}
|
|
28
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { timeFunction } from "../server_timing/timing_measure.js"
|
|
2
|
-
import { composeTwoResponses } from "../internal/response_composition.js"
|
|
3
|
-
import { applyRedirectionToRequest } from "../internal/request_factory.js"
|
|
4
|
-
|
|
5
|
-
export const composeServices = (namedServices) => {
|
|
6
|
-
return async (request, { pushResponse }) => {
|
|
7
|
-
const redirectRequest = (requestRedirection) => {
|
|
8
|
-
request = applyRedirectionToRequest(request, requestRedirection)
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const servicesTiming = {}
|
|
12
|
-
const response = await firstOperationMatching({
|
|
13
|
-
array: Object.keys(namedServices).map((serviceName) => {
|
|
14
|
-
return { serviceName, serviceFn: namedServices[serviceName] }
|
|
15
|
-
}),
|
|
16
|
-
start: async ({ serviceName, serviceFn }) => {
|
|
17
|
-
const [serviceTiming, value] = await timeFunction(serviceName, () =>
|
|
18
|
-
serviceFn(request, { pushResponse, redirectRequest }),
|
|
19
|
-
)
|
|
20
|
-
Object.assign(servicesTiming, serviceTiming)
|
|
21
|
-
return value
|
|
22
|
-
},
|
|
23
|
-
predicate: (returnValue) => {
|
|
24
|
-
if (returnValue === null || typeof returnValue !== "object") {
|
|
25
|
-
return false
|
|
26
|
-
}
|
|
27
|
-
return true
|
|
28
|
-
},
|
|
29
|
-
})
|
|
30
|
-
if (response) {
|
|
31
|
-
return composeTwoResponses({ timing: servicesTiming }, response)
|
|
32
|
-
}
|
|
33
|
-
return null
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const firstOperationMatching = ({ array, start, predicate }) => {
|
|
38
|
-
return new Promise((resolve, reject) => {
|
|
39
|
-
const visit = (index) => {
|
|
40
|
-
if (index >= array.length) {
|
|
41
|
-
return resolve()
|
|
42
|
-
}
|
|
43
|
-
const input = array[index]
|
|
44
|
-
const returnValue = start(input)
|
|
45
|
-
return Promise.resolve(returnValue).then((output) => {
|
|
46
|
-
if (predicate(output)) {
|
|
47
|
-
return resolve(output)
|
|
48
|
-
}
|
|
49
|
-
return visit(index + 1)
|
|
50
|
-
}, reject)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
visit(0)
|
|
54
|
-
})
|
|
55
|
-
}
|