@jsenv/server 13.1.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/package.json
CHANGED
|
@@ -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
|
|
|
@@ -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/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,
|
|
@@ -83,6 +88,8 @@ export const startServer = async ({
|
|
|
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,
|
|
@@ -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
|
-
}
|