@jsenv/server 13.1.0 → 14.1.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 +2 -2
- package/package.json +2 -2
- package/src/fetch_filesystem.js +2 -2
- package/src/internal/hostname_parser.js +48 -0
- package/src/internal/listen.js +11 -11
- package/src/internal/nodeStreamToObservable.js +1 -1
- package/src/internal/request_factory.js +13 -13
- package/src/internal/server_ips.js +49 -0
- package/src/main.js +1 -1
- package/src/services/request_aliases/jsenv_service_request_aliases.js +37 -0
- package/src/sse/sse_room.js +1 -3
- package/src/start_server.js +94 -11
- package/src/internal/getServerOrigins.js +0 -74
- package/src/services/ressource_aliases/jsenv_service_ressource_aliases.js +0 -40
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ const server = await startServer({
|
|
|
43
43
|
{
|
|
44
44
|
name: "index",
|
|
45
45
|
handleRequest: (request) => {
|
|
46
|
-
if (request.
|
|
46
|
+
if (request.resource === "/") {
|
|
47
47
|
return { status: 200 }
|
|
48
48
|
}
|
|
49
49
|
return null
|
|
@@ -104,7 +104,7 @@ await startServer({
|
|
|
104
104
|
services: [
|
|
105
105
|
{
|
|
106
106
|
handleRequest: async (request) => {
|
|
107
|
-
const fileUrl = new URL(request.
|
|
107
|
+
const fileUrl = new URL(request.resource.slice(1), import.meta.url)
|
|
108
108
|
const response = await fetchFileSystem(fileUrl, request)
|
|
109
109
|
return response
|
|
110
110
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "14.1.1",
|
|
4
4
|
"description": "Write your Node.js server using pure functions",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@jsenv/abort": "4.2.3",
|
|
38
|
-
"@jsenv/log": "3.
|
|
38
|
+
"@jsenv/log": "3.2.0",
|
|
39
39
|
"@jsenv/url-meta": "7.0.0",
|
|
40
40
|
"@jsenv/utils": "2.0.1",
|
|
41
41
|
"ws": "8.8.1"
|
package/src/fetch_filesystem.js
CHANGED
|
@@ -64,7 +64,7 @@ export const fetchFileSystem = async (
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// here you might be tempted to add || cacheControl === 'no-cache'
|
|
67
|
-
// but no-cache means
|
|
67
|
+
// but no-cache means resource can be cache but must be revalidated (yeah naming is strange)
|
|
68
68
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Cacheability
|
|
69
69
|
if (cacheControl === "no-store") {
|
|
70
70
|
if (etagEnabled) {
|
|
@@ -206,7 +206,7 @@ const getClientCacheResponse = async ({
|
|
|
206
206
|
sourceUrl,
|
|
207
207
|
}) => {
|
|
208
208
|
// here you might be tempted to add || headers["cache-control"] === "no-cache"
|
|
209
|
-
// but no-cache means
|
|
209
|
+
// but no-cache means resource can be cache but must be revalidated (yeah naming is strange)
|
|
210
210
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Cacheability
|
|
211
211
|
|
|
212
212
|
if (
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { isIP } from "node:net"
|
|
2
|
+
|
|
3
|
+
export const parseHostname = (hostname) => {
|
|
4
|
+
if (hostname === "0.0.0.0") {
|
|
5
|
+
return {
|
|
6
|
+
type: "ip",
|
|
7
|
+
label: "unspecified",
|
|
8
|
+
version: 4,
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
if (
|
|
12
|
+
hostname === "::" ||
|
|
13
|
+
hostname === "0000:0000:0000:0000:0000:0000:0000:0000"
|
|
14
|
+
) {
|
|
15
|
+
return {
|
|
16
|
+
type: "ip",
|
|
17
|
+
label: "unspecified",
|
|
18
|
+
version: 6,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (hostname === "127.0.0.1") {
|
|
22
|
+
return {
|
|
23
|
+
type: "ip",
|
|
24
|
+
label: "loopback",
|
|
25
|
+
version: 4,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (
|
|
29
|
+
hostname === "::1" ||
|
|
30
|
+
hostname === "0000:0000:0000:0000:0000:0000:0000:0001"
|
|
31
|
+
) {
|
|
32
|
+
return {
|
|
33
|
+
type: "ip",
|
|
34
|
+
label: "loopback",
|
|
35
|
+
version: 6,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const ipVersion = isIP(hostname)
|
|
39
|
+
if (ipVersion === 0) {
|
|
40
|
+
return {
|
|
41
|
+
type: "hostname",
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
type: "ip",
|
|
46
|
+
version: ipVersion,
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/internal/listen.js
CHANGED
|
@@ -6,7 +6,7 @@ const listen = async ({
|
|
|
6
6
|
server,
|
|
7
7
|
port,
|
|
8
8
|
portHint,
|
|
9
|
-
|
|
9
|
+
hostname,
|
|
10
10
|
}) => {
|
|
11
11
|
const listeningOperation = Abort.startOperation()
|
|
12
12
|
|
|
@@ -17,11 +17,11 @@ const listen = async ({
|
|
|
17
17
|
listeningOperation.throwIfAborted()
|
|
18
18
|
port = await findFreePort(portHint, {
|
|
19
19
|
signal: listeningOperation.signal,
|
|
20
|
-
|
|
20
|
+
hostname,
|
|
21
21
|
})
|
|
22
22
|
}
|
|
23
23
|
listeningOperation.throwIfAborted()
|
|
24
|
-
port = await startListening({ server, port,
|
|
24
|
+
port = await startListening({ server, port, hostname })
|
|
25
25
|
listeningOperation.addAbortCallback(() => stopListening(server))
|
|
26
26
|
listeningOperation.throwIfAborted()
|
|
27
27
|
|
|
@@ -35,7 +35,7 @@ export const findFreePort = async (
|
|
|
35
35
|
initialPort = 1,
|
|
36
36
|
{
|
|
37
37
|
signal = new AbortController().signal,
|
|
38
|
-
|
|
38
|
+
hostname = "127.0.0.1",
|
|
39
39
|
min = 1,
|
|
40
40
|
max = 65534,
|
|
41
41
|
next = (port) => port + 1,
|
|
@@ -56,26 +56,26 @@ export const findFreePort = async (
|
|
|
56
56
|
const nextPort = next(port)
|
|
57
57
|
if (nextPort > max) {
|
|
58
58
|
throw new Error(
|
|
59
|
-
`${
|
|
59
|
+
`${hostname} has no available port between ${min} and ${max}`,
|
|
60
60
|
)
|
|
61
61
|
}
|
|
62
|
-
return testUntil(nextPort,
|
|
62
|
+
return testUntil(nextPort, hostname)
|
|
63
63
|
}
|
|
64
|
-
const freePort = await testUntil(initialPort,
|
|
64
|
+
const freePort = await testUntil(initialPort, hostname)
|
|
65
65
|
return freePort
|
|
66
66
|
} finally {
|
|
67
67
|
await findFreePortOperation.end()
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
const portIsFree = async (port,
|
|
71
|
+
const portIsFree = async (port, hostname) => {
|
|
72
72
|
const server = createServer()
|
|
73
73
|
|
|
74
74
|
try {
|
|
75
75
|
await startListening({
|
|
76
76
|
server,
|
|
77
77
|
port,
|
|
78
|
-
|
|
78
|
+
hostname,
|
|
79
79
|
})
|
|
80
80
|
} catch (error) {
|
|
81
81
|
if (error && error.code === "EADDRINUSE") {
|
|
@@ -91,7 +91,7 @@ const portIsFree = async (port, host) => {
|
|
|
91
91
|
return true
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
const startListening = ({ server, port,
|
|
94
|
+
const startListening = ({ server, port, hostname }) => {
|
|
95
95
|
return new Promise((resolve, reject) => {
|
|
96
96
|
server.on("error", reject)
|
|
97
97
|
server.on("listening", () => {
|
|
@@ -99,7 +99,7 @@ const startListening = ({ server, port, host }) => {
|
|
|
99
99
|
// https://nodejs.org/api/net.html#net_server_listen_port_host_backlog_callback
|
|
100
100
|
resolve(server.address().port)
|
|
101
101
|
})
|
|
102
|
-
server.listen(port,
|
|
102
|
+
server.listen(port, hostname)
|
|
103
103
|
})
|
|
104
104
|
}
|
|
105
105
|
|
|
@@ -38,7 +38,7 @@ export const nodeStreamToObservable = (nodeStream) => {
|
|
|
38
38
|
// used in the next ${readableStreamLifetimeInSeconds} otherwise destroys it
|
|
39
39
|
const timeout = setTimeout(() => {
|
|
40
40
|
process.emitWarning(
|
|
41
|
-
`Readable stream not used after ${readableStreamLifetimeInSeconds} seconds. It will be destroyed to release
|
|
41
|
+
`Readable stream not used after ${readableStreamLifetimeInSeconds} seconds. It will be destroyed to release resources`,
|
|
42
42
|
{
|
|
43
43
|
CODE: "READABLE_STREAM_TIMEOUT",
|
|
44
44
|
// url is for http client request
|
|
@@ -24,8 +24,8 @@ export const fromNodeRequest = (nodeRequest, { serverOrigin, signal }) => {
|
|
|
24
24
|
signal,
|
|
25
25
|
http2: Boolean(nodeRequest.stream),
|
|
26
26
|
origin: requestOrigin,
|
|
27
|
-
...
|
|
28
|
-
|
|
27
|
+
...getPropertiesFromResource({
|
|
28
|
+
resource: nodeRequest.url,
|
|
29
29
|
baseUrl: requestOrigin,
|
|
30
30
|
}),
|
|
31
31
|
method: nodeRequest.method,
|
|
@@ -36,13 +36,13 @@ export const fromNodeRequest = (nodeRequest, { serverOrigin, signal }) => {
|
|
|
36
36
|
|
|
37
37
|
export const applyRedirectionToRequest = (
|
|
38
38
|
request,
|
|
39
|
-
{
|
|
39
|
+
{ resource, pathname, ...rest },
|
|
40
40
|
) => {
|
|
41
41
|
return {
|
|
42
42
|
...request,
|
|
43
|
-
...(
|
|
44
|
-
?
|
|
45
|
-
|
|
43
|
+
...(resource
|
|
44
|
+
? getPropertiesFromResource({
|
|
45
|
+
resource,
|
|
46
46
|
baseUrl: request.url,
|
|
47
47
|
})
|
|
48
48
|
: pathname
|
|
@@ -55,20 +55,20 @@ export const applyRedirectionToRequest = (
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
const
|
|
59
|
-
const urlObject = new URL(
|
|
58
|
+
const getPropertiesFromResource = ({ resource, baseUrl }) => {
|
|
59
|
+
const urlObject = new URL(resource, baseUrl)
|
|
60
60
|
let pathname = urlObject.pathname
|
|
61
61
|
|
|
62
62
|
return {
|
|
63
63
|
url: String(urlObject),
|
|
64
64
|
pathname,
|
|
65
|
-
|
|
65
|
+
resource,
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
const getPropertiesFromPathname = ({ pathname, baseUrl }) => {
|
|
70
|
-
return
|
|
71
|
-
|
|
70
|
+
return getPropertiesFromResource({
|
|
71
|
+
resource: `${pathname}${new URL(baseUrl).search}`,
|
|
72
72
|
baseUrl,
|
|
73
73
|
})
|
|
74
74
|
}
|
|
@@ -95,12 +95,12 @@ export const createPushRequest = (request, { signal, pathname, method }) => {
|
|
|
95
95
|
const getHeadersInheritedByPushRequest = (request) => {
|
|
96
96
|
const headersInherited = { ...request.headers }
|
|
97
97
|
// mtime sent by the client in request headers concerns the main request
|
|
98
|
-
// Time remains valid for request to other
|
|
98
|
+
// Time remains valid for request to other resources so we keep it
|
|
99
99
|
// in child requests
|
|
100
100
|
// delete childHeaders["if-modified-since"]
|
|
101
101
|
|
|
102
102
|
// eTag sent by the client in request headers concerns the main request
|
|
103
|
-
// A request made to an other
|
|
103
|
+
// A request made to an other resource must not inherit the eTag
|
|
104
104
|
delete headersInherited["if-none-match"]
|
|
105
105
|
|
|
106
106
|
return headersInherited
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { networkInterfaces } from "node:os"
|
|
2
|
+
|
|
3
|
+
export const createIpGetters = () => {
|
|
4
|
+
const networkAddresses = []
|
|
5
|
+
const networkInterfaceMap = networkInterfaces()
|
|
6
|
+
for (const key of Object.keys(networkInterfaceMap)) {
|
|
7
|
+
for (const networkAddress of networkInterfaceMap[key]) {
|
|
8
|
+
networkAddresses.push(networkAddress)
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
getFirstInternalIp: ({ preferIpv6 }) => {
|
|
13
|
+
const isPref = preferIpv6 ? isIpV6 : isIpV4
|
|
14
|
+
let firstInternalIp
|
|
15
|
+
for (const networkAddress of networkAddresses) {
|
|
16
|
+
if (networkAddress.internal) {
|
|
17
|
+
firstInternalIp = networkAddress.address
|
|
18
|
+
if (isPref(networkAddress)) {
|
|
19
|
+
break
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return firstInternalIp
|
|
24
|
+
},
|
|
25
|
+
getFirstExternalIp: ({ preferIpv6 }) => {
|
|
26
|
+
const isPref = preferIpv6 ? isIpV6 : isIpV4
|
|
27
|
+
let firstExternalIp
|
|
28
|
+
for (const networkAddress of networkAddresses) {
|
|
29
|
+
if (!networkAddress.internal) {
|
|
30
|
+
firstExternalIp = networkAddress.address
|
|
31
|
+
if (isPref(networkAddress)) {
|
|
32
|
+
break
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return firstExternalIp
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const isIpV4 = (networkAddress) => {
|
|
42
|
+
// node 18.5
|
|
43
|
+
if (typeof networkAddress.family === "number") {
|
|
44
|
+
return networkAddress.family === 4
|
|
45
|
+
}
|
|
46
|
+
return networkAddress.family === "IPv4"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const isIpV6 = (networkAddress) => !isIpV4(networkAddress)
|
package/src/main.js
CHANGED
|
@@ -42,5 +42,5 @@ export { jsenvServiceResponseAcceptanceCheck } from "./services/response_accepta
|
|
|
42
42
|
export { serveDirectory } from "./serve_directory.js"
|
|
43
43
|
export { fromFetchResponse } from "./from_fetch_response.js"
|
|
44
44
|
export { composeTwoResponses } from "./internal/response_composition.js"
|
|
45
|
-
export {
|
|
45
|
+
export { jsenvServiceRequestAliases } from "./services/request_aliases/jsenv_service_request_aliases.js"
|
|
46
46
|
export { findFreePort } from "./internal/listen.js"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { URL_META } from "@jsenv/url-meta"
|
|
2
|
+
|
|
3
|
+
export const jsenvServiceRequestAliases = (resourceAliases) => {
|
|
4
|
+
const aliases = {}
|
|
5
|
+
Object.keys(resourceAliases).forEach((key) => {
|
|
6
|
+
aliases[asFileUrl(key)] = asFileUrl(resourceAliases[key])
|
|
7
|
+
})
|
|
8
|
+
return {
|
|
9
|
+
name: "jsenv:request_aliases",
|
|
10
|
+
redirectRequest: (request) => {
|
|
11
|
+
const resourceBeforeAlias = request.resource
|
|
12
|
+
const urlAfterAliasing = URL_META.applyAliases({
|
|
13
|
+
url: asFileUrl(request.pathname),
|
|
14
|
+
aliases,
|
|
15
|
+
})
|
|
16
|
+
const resourceAfterAlias = urlAfterAliasing.slice("file://".length)
|
|
17
|
+
if (resourceBeforeAlias === resourceAfterAlias) {
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
const resource = replaceResource(resourceBeforeAlias, resourceAfterAlias)
|
|
21
|
+
return { resource }
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const asFileUrl = (specifier) => new URL(specifier, "file:///").href
|
|
27
|
+
|
|
28
|
+
const replaceResource = (resourceBeforeAlias, newValue) => {
|
|
29
|
+
const urlObject = new URL(resourceBeforeAlias, "file://")
|
|
30
|
+
const searchSeparatorIndex = newValue.indexOf("?")
|
|
31
|
+
if (searchSeparatorIndex > -1) {
|
|
32
|
+
return newValue // let new value override search params
|
|
33
|
+
}
|
|
34
|
+
urlObject.pathname = newValue
|
|
35
|
+
const resource = `${urlObject.pathname}${urlObject.search}`
|
|
36
|
+
return resource
|
|
37
|
+
}
|
package/src/sse/sse_room.js
CHANGED
|
@@ -37,9 +37,7 @@ export const createSSERoom = ({
|
|
|
37
37
|
|
|
38
38
|
const lastKnownId =
|
|
39
39
|
request.headers["last-event-id"] ||
|
|
40
|
-
new URL(request.
|
|
41
|
-
"last-event-id",
|
|
42
|
-
)
|
|
40
|
+
new URL(request.url).searchParams.get("last-event-id")
|
|
43
41
|
|
|
44
42
|
if (clients.size >= maxClientAllowed) {
|
|
45
43
|
return {
|
package/src/start_server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isIP } from "node:net"
|
|
1
2
|
import http from "node:http"
|
|
2
3
|
import cluster from "node:cluster"
|
|
3
4
|
import { createDetailedMessage, createLogger } from "@jsenv/log"
|
|
@@ -23,7 +24,6 @@ import {
|
|
|
23
24
|
statusToType,
|
|
24
25
|
colorizeResponseStatus,
|
|
25
26
|
} from "./internal/colorizeResponseStatus.js"
|
|
26
|
-
import { getServerOrigins } from "./internal/getServerOrigins.js"
|
|
27
27
|
import { listen, stopListening } from "./internal/listen.js"
|
|
28
28
|
import { composeTwoResponses } from "./internal/response_composition.js"
|
|
29
29
|
import { listenRequest } from "./internal/listenRequest.js"
|
|
@@ -40,6 +40,10 @@ import {
|
|
|
40
40
|
} from "./stopReasons.js"
|
|
41
41
|
import { composeTwoHeaders } from "./internal/headers_composition.js"
|
|
42
42
|
|
|
43
|
+
import { createIpGetters } from "./internal/server_ips.js"
|
|
44
|
+
import { parseHostname } from "./internal/hostname_parser.js"
|
|
45
|
+
import { applyDnsResolution } from "./internal/dns_resolution.js"
|
|
46
|
+
|
|
43
47
|
export const startServer = async ({
|
|
44
48
|
signal = new AbortController().signal,
|
|
45
49
|
logLevel,
|
|
@@ -52,7 +56,8 @@ export const startServer = async ({
|
|
|
52
56
|
redirectHttpToHttps,
|
|
53
57
|
allowHttpRequestOnHttps = false,
|
|
54
58
|
acceptAnyIp = false,
|
|
55
|
-
|
|
59
|
+
preferIpv6,
|
|
60
|
+
hostname = "localhost",
|
|
56
61
|
port = 0, // assign a random available port
|
|
57
62
|
portHint,
|
|
58
63
|
privateKey,
|
|
@@ -76,13 +81,15 @@ export const startServer = async ({
|
|
|
76
81
|
createDetailedMessage(
|
|
77
82
|
`still no response found for request after ${requestWaitingMs} ms`,
|
|
78
83
|
{
|
|
79
|
-
"request url":
|
|
84
|
+
"request url": request.url,
|
|
80
85
|
"request headers": JSON.stringify(request.headers, null, " "),
|
|
81
86
|
},
|
|
82
87
|
),
|
|
83
88
|
)
|
|
84
89
|
},
|
|
85
90
|
} = {}) => {
|
|
91
|
+
const logger = createLogger({ logLevel })
|
|
92
|
+
|
|
86
93
|
if (protocol !== "http" && protocol !== "https") {
|
|
87
94
|
throw new Error(`protocol must be http or https, got ${protocol}`)
|
|
88
95
|
}
|
|
@@ -98,7 +105,6 @@ export const startServer = async ({
|
|
|
98
105
|
throw new Error(`http2 needs "https" but protocol is "${protocol}"`)
|
|
99
106
|
}
|
|
100
107
|
|
|
101
|
-
const logger = createLogger({ logLevel })
|
|
102
108
|
if (
|
|
103
109
|
redirectHttpToHttps === undefined &&
|
|
104
110
|
protocol === "https" &&
|
|
@@ -137,6 +143,9 @@ export const startServer = async ({
|
|
|
137
143
|
let nodeServer
|
|
138
144
|
const startServerOperation = Abort.startOperation()
|
|
139
145
|
const stopCallbackList = createCallbackListNotifiedOnce()
|
|
146
|
+
const serverOrigins = {
|
|
147
|
+
local: "", // favors hostname when possible
|
|
148
|
+
}
|
|
140
149
|
|
|
141
150
|
try {
|
|
142
151
|
startServerOperation.addAbortSignal(signal)
|
|
@@ -163,12 +172,73 @@ export const startServer = async ({
|
|
|
163
172
|
nodeServer.unref()
|
|
164
173
|
}
|
|
165
174
|
|
|
175
|
+
const createOrigin = (hostname) => {
|
|
176
|
+
if (isIP(hostname) === 6) {
|
|
177
|
+
return `${protocol}://[${hostname}]`
|
|
178
|
+
}
|
|
179
|
+
return `${protocol}://${hostname}`
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const ipGetters = createIpGetters()
|
|
183
|
+
let hostnameToListen
|
|
184
|
+
if (acceptAnyIp) {
|
|
185
|
+
const firstInternalIp = ipGetters.getFirstInternalIp({ preferIpv6 })
|
|
186
|
+
serverOrigins.local = createOrigin(firstInternalIp)
|
|
187
|
+
serverOrigins.localip = createOrigin(firstInternalIp)
|
|
188
|
+
const firstExternalIp = ipGetters.getFirstExternalIp({ preferIpv6 })
|
|
189
|
+
serverOrigins.externalip = createOrigin(firstExternalIp)
|
|
190
|
+
hostnameToListen = preferIpv6 ? "::" : "0.0.0.0"
|
|
191
|
+
} else {
|
|
192
|
+
hostnameToListen = hostname
|
|
193
|
+
}
|
|
194
|
+
const hostnameInfo = parseHostname(hostname)
|
|
195
|
+
if (hostnameInfo.type === "ip") {
|
|
196
|
+
if (acceptAnyIp) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`hostname cannot be an ip when acceptAnyIp is enabled, got ${hostname}`,
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
preferIpv6 = hostnameInfo.version === 6
|
|
203
|
+
const firstInternalIp = ipGetters.getFirstInternalIp({ preferIpv6 })
|
|
204
|
+
serverOrigins.local = createOrigin(firstInternalIp)
|
|
205
|
+
serverOrigins.localip = createOrigin(firstInternalIp)
|
|
206
|
+
if (hostnameInfo.label === "unspecified") {
|
|
207
|
+
const firstExternalIp = ipGetters.getFirstExternalIp({ preferIpv6 })
|
|
208
|
+
serverOrigins.externalip = createOrigin(firstExternalIp)
|
|
209
|
+
} else if (hostnameInfo.label === "loopback") {
|
|
210
|
+
} else {
|
|
211
|
+
serverOrigins.local = createOrigin(hostname)
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
const hostnameDnsResolution = await applyDnsResolution(hostname, {
|
|
215
|
+
verbatim: true,
|
|
216
|
+
})
|
|
217
|
+
if (hostnameDnsResolution) {
|
|
218
|
+
const hostnameIp = hostnameDnsResolution.address
|
|
219
|
+
serverOrigins.localip = createOrigin(hostnameIp)
|
|
220
|
+
serverOrigins.local = createOrigin(hostname)
|
|
221
|
+
} else {
|
|
222
|
+
const firstInternalIp = ipGetters.getFirstInternalIp({ preferIpv6 })
|
|
223
|
+
// fallback to internal ip because there is no ip
|
|
224
|
+
// associated to this hostname on operating system (in hosts file)
|
|
225
|
+
hostname = firstInternalIp
|
|
226
|
+
hostnameToListen = firstInternalIp
|
|
227
|
+
serverOrigins.local = createOrigin(firstInternalIp)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
166
231
|
port = await listen({
|
|
167
232
|
signal: startServerOperation.signal,
|
|
168
233
|
server: nodeServer,
|
|
169
234
|
port,
|
|
170
235
|
portHint,
|
|
171
|
-
|
|
236
|
+
hostname: hostnameToListen,
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
// normalize origins (remove :80 when port is 80 for instance)
|
|
240
|
+
Object.keys(serverOrigins).forEach((key) => {
|
|
241
|
+
serverOrigins[key] = new URL(`${serverOrigins[key]}:${port}`).origin
|
|
172
242
|
})
|
|
173
243
|
|
|
174
244
|
serviceController.callHooks("serverListening", { port })
|
|
@@ -180,6 +250,18 @@ export const startServer = async ({
|
|
|
180
250
|
await startServerOperation.end()
|
|
181
251
|
}
|
|
182
252
|
|
|
253
|
+
// the main server origin
|
|
254
|
+
// - when protocol is http
|
|
255
|
+
// node-fetch do not apply local dns resolution to map localhost back to 127.0.0.1
|
|
256
|
+
// despites localhost being mapped so we prefer to use the internal ip
|
|
257
|
+
// (127.0.0.1)
|
|
258
|
+
// - when protocol is https
|
|
259
|
+
// using the hostname becomes important because the certificate is generated
|
|
260
|
+
// for hostnames, not for ips
|
|
261
|
+
// so we prefer https://locahost or https://local_hostname
|
|
262
|
+
// over the ip
|
|
263
|
+
const serverOrigin = serverOrigins.local
|
|
264
|
+
|
|
183
265
|
// now the server is started (listening) it cannot be aborted anymore
|
|
184
266
|
// (otherwise an AbortError is thrown to the code calling "startServer")
|
|
185
267
|
// we can proceed to create a stop function to stop it gacefully
|
|
@@ -218,8 +300,6 @@ export const startServer = async ({
|
|
|
218
300
|
}
|
|
219
301
|
|
|
220
302
|
status = "opened"
|
|
221
|
-
const serverOrigins = await getServerOrigins({ protocol, host, port })
|
|
222
|
-
const serverOrigin = serverOrigins.local
|
|
223
303
|
|
|
224
304
|
const removeConnectionErrorListener = listenServerConnectionError(
|
|
225
305
|
nodeServer,
|
|
@@ -387,7 +467,7 @@ export const startServer = async ({
|
|
|
387
467
|
addRequestLog(requestNode, {
|
|
388
468
|
type: "error",
|
|
389
469
|
value: createDetailedMessage(
|
|
390
|
-
`An error occured while pushing a stream to the response for ${request.
|
|
470
|
+
`An error occured while pushing a stream to the response for ${request.resource}`,
|
|
391
471
|
{
|
|
392
472
|
"error stack": e.stack,
|
|
393
473
|
},
|
|
@@ -492,8 +572,8 @@ export const startServer = async ({
|
|
|
492
572
|
addRequestLog(requestNode, {
|
|
493
573
|
type: "info",
|
|
494
574
|
value: request.parent
|
|
495
|
-
? `Push ${request.
|
|
496
|
-
: `${request.method} ${request.
|
|
575
|
+
? `Push ${request.resource}`
|
|
576
|
+
: `${request.method} ${request.url}`,
|
|
497
577
|
})
|
|
498
578
|
const warn = (value) => {
|
|
499
579
|
addRequestLog(requestNode, {
|
|
@@ -829,7 +909,9 @@ export const startServer = async ({
|
|
|
829
909
|
const { WebSocketServer } = await import("ws")
|
|
830
910
|
let websocketServer = new WebSocketServer({ noServer: true })
|
|
831
911
|
const websocketOrigin =
|
|
832
|
-
protocol === "https"
|
|
912
|
+
protocol === "https"
|
|
913
|
+
? `wss://${hostname}:${port}`
|
|
914
|
+
: `ws://${hostname}:${port}`
|
|
833
915
|
server.websocketOrigin = websocketOrigin
|
|
834
916
|
const upgradeCallback = (nodeRequest, socket, head) => {
|
|
835
917
|
websocketServer.handleUpgrade(
|
|
@@ -884,6 +966,7 @@ export const startServer = async ({
|
|
|
884
966
|
Object.assign(server, {
|
|
885
967
|
getStatus: () => status,
|
|
886
968
|
port,
|
|
969
|
+
hostname,
|
|
887
970
|
origin: serverOrigin,
|
|
888
971
|
origins: serverOrigins,
|
|
889
972
|
nodeServer,
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { networkInterfaces } from "node:os"
|
|
2
|
-
|
|
3
|
-
import { applyDnsResolution } from "./dns_resolution.js"
|
|
4
|
-
|
|
5
|
-
export const getServerOrigins = async ({ protocol, host, port }) => {
|
|
6
|
-
const isLocal = LOOPBACK_HOSTNAMES.includes(host)
|
|
7
|
-
const localhostDnsResolution = await applyDnsResolution("localhost")
|
|
8
|
-
const localOrigin = createServerOrigin({
|
|
9
|
-
protocol,
|
|
10
|
-
hostname:
|
|
11
|
-
localhostDnsResolution.address === "127.0.0.1"
|
|
12
|
-
? "localhost"
|
|
13
|
-
: "127.0.0.1",
|
|
14
|
-
port,
|
|
15
|
-
})
|
|
16
|
-
if (isLocal) {
|
|
17
|
-
return { local: localOrigin }
|
|
18
|
-
}
|
|
19
|
-
const isAnyIp = WILDCARD_HOSTNAMES.includes(host)
|
|
20
|
-
const networkOrigin = createServerOrigin({
|
|
21
|
-
protocol,
|
|
22
|
-
hostname: isAnyIp ? getExternalIp() : host,
|
|
23
|
-
port,
|
|
24
|
-
})
|
|
25
|
-
return {
|
|
26
|
-
local: localOrigin,
|
|
27
|
-
network: networkOrigin,
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const LOOPBACK_HOSTNAMES = [
|
|
32
|
-
"localhost",
|
|
33
|
-
"127.0.0.1",
|
|
34
|
-
"::1",
|
|
35
|
-
"0000:0000:0000:0000:0000:0000:0000:0001",
|
|
36
|
-
]
|
|
37
|
-
|
|
38
|
-
const WILDCARD_HOSTNAMES = [
|
|
39
|
-
undefined,
|
|
40
|
-
"0.0.0.0",
|
|
41
|
-
"::",
|
|
42
|
-
"0000:0000:0000:0000:0000:0000:0000:0000",
|
|
43
|
-
]
|
|
44
|
-
|
|
45
|
-
const createServerOrigin = ({ protocol, hostname, port }) => {
|
|
46
|
-
const url = new URL("https://127.0.0.1:80")
|
|
47
|
-
url.protocol = protocol
|
|
48
|
-
url.hostname = hostname
|
|
49
|
-
url.port = port
|
|
50
|
-
return url.origin
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const getExternalIp = () => {
|
|
54
|
-
const networkInterfaceMap = networkInterfaces()
|
|
55
|
-
let internalIPV4NetworkAddress
|
|
56
|
-
Object.keys(networkInterfaceMap).find((key) => {
|
|
57
|
-
const networkAddressArray = networkInterfaceMap[key]
|
|
58
|
-
return networkAddressArray.find((networkAddress) => {
|
|
59
|
-
if (networkAddress.internal) return false
|
|
60
|
-
if (!isIpV4(networkAddress)) return false
|
|
61
|
-
internalIPV4NetworkAddress = networkAddress
|
|
62
|
-
return true
|
|
63
|
-
})
|
|
64
|
-
})
|
|
65
|
-
return internalIPV4NetworkAddress ? internalIPV4NetworkAddress.address : null
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const isIpV4 = (networkAddress) => {
|
|
69
|
-
// node 18+
|
|
70
|
-
if (typeof networkAddress.family === "number") {
|
|
71
|
-
return networkAddress.family === 4
|
|
72
|
-
}
|
|
73
|
-
return networkAddress.family === "IPv4"
|
|
74
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { URL_META } from "@jsenv/url-meta"
|
|
2
|
-
|
|
3
|
-
export const jsenvServiceRessourceAliases = (ressourceAliases) => {
|
|
4
|
-
const aliases = {}
|
|
5
|
-
Object.keys(ressourceAliases).forEach((key) => {
|
|
6
|
-
aliases[asFileUrl(key)] = asFileUrl(ressourceAliases[key])
|
|
7
|
-
})
|
|
8
|
-
return {
|
|
9
|
-
name: "jsenv:ressource_aliases",
|
|
10
|
-
redirectRequest: (request) => {
|
|
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 null
|
|
19
|
-
}
|
|
20
|
-
const ressource = replaceRessource(
|
|
21
|
-
ressourceBeforeAlias,
|
|
22
|
-
ressourceAfterAlias,
|
|
23
|
-
)
|
|
24
|
-
return { ressource }
|
|
25
|
-
},
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const asFileUrl = (specifier) => new URL(specifier, "file:///").href
|
|
30
|
-
|
|
31
|
-
const replaceRessource = (ressourceBeforeAlias, newValue) => {
|
|
32
|
-
const urlObject = new URL(ressourceBeforeAlias, "file://")
|
|
33
|
-
const searchSeparatorIndex = newValue.indexOf("?")
|
|
34
|
-
if (searchSeparatorIndex > -1) {
|
|
35
|
-
return newValue // let new value override search params
|
|
36
|
-
}
|
|
37
|
-
urlObject.pathname = newValue
|
|
38
|
-
const ressource = `${urlObject.pathname}${urlObject.search}`
|
|
39
|
-
return ressource
|
|
40
|
-
}
|