1 | 'use strict'
|
2 |
|
3 | const http = require('node:http')
|
4 | const https = require('node:https')
|
5 | const dns = require('node:dns')
|
6 |
|
7 | const { FSTDEP011 } = require('./warnings')
|
8 | const { kState, kOptions, kServerBindings } = require('./symbols')
|
9 | const { onListenHookRunner } = require('./hooks')
|
10 | const {
|
11 | FST_ERR_HTTP2_INVALID_VERSION,
|
12 | FST_ERR_REOPENED_CLOSE_SERVER,
|
13 | FST_ERR_REOPENED_SERVER,
|
14 | FST_ERR_LISTEN_OPTIONS_INVALID
|
15 | } = require('./errors')
|
16 |
|
17 | module.exports.createServer = createServer
|
18 | module.exports.compileValidateHTTPVersion = compileValidateHTTPVersion
|
19 |
|
20 | function defaultResolveServerListeningText (address) {
|
21 | return `Server listening at ${address}`
|
22 | }
|
23 |
|
24 | function createServer (options, httpHandler) {
|
25 | const server = getServerInstance(options, httpHandler)
|
26 |
|
27 |
|
28 | function listen (listenOptions, ...args) {
|
29 | let cb = args.slice(-1).pop()
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 | const firstArgType = Object.prototype.toString.call(arguments[0])
|
36 | if (arguments.length === 0) {
|
37 | listenOptions = normalizeListenArgs([])
|
38 | } else if (arguments.length > 0 && (firstArgType !== '[object Object]' && firstArgType !== '[object Function]')) {
|
39 | FSTDEP011()
|
40 | listenOptions = normalizeListenArgs(Array.from(arguments))
|
41 | cb = listenOptions.cb
|
42 | } else if (args.length > 1) {
|
43 |
|
44 | FSTDEP011()
|
45 |
|
46 | const hostPath = listenOptions.path ? [listenOptions.path] : [listenOptions.port ?? 0, listenOptions.host ?? 'localhost']
|
47 | Object.assign(listenOptions, normalizeListenArgs([...hostPath, ...args]))
|
48 | } else {
|
49 | listenOptions.cb = cb
|
50 | }
|
51 | if (listenOptions.signal) {
|
52 | if (typeof listenOptions.signal.on !== 'function' && typeof listenOptions.signal.addEventListener !== 'function') {
|
53 | throw new FST_ERR_LISTEN_OPTIONS_INVALID('Invalid options.signal')
|
54 | }
|
55 |
|
56 | if (listenOptions.signal.aborted) {
|
57 | this.close()
|
58 | } else {
|
59 | const onAborted = () => {
|
60 | this.close()
|
61 | }
|
62 | listenOptions.signal.addEventListener('abort', onAborted, { once: true })
|
63 | }
|
64 | }
|
65 |
|
66 |
|
67 |
|
68 |
|
69 | let host
|
70 | if (listenOptions.path == null) {
|
71 | host = listenOptions.host ?? 'localhost'
|
72 | } else {
|
73 | host = listenOptions.host
|
74 | }
|
75 | if (Object.prototype.hasOwnProperty.call(listenOptions, 'host') === false) {
|
76 | listenOptions.host = host
|
77 | }
|
78 | if (host === 'localhost') {
|
79 | listenOptions.cb = (err, address) => {
|
80 | if (err) {
|
81 |
|
82 | cb(err, address)
|
83 | return
|
84 | }
|
85 |
|
86 | multipleBindings.call(this, server, httpHandler, options, listenOptions, () => {
|
87 | this[kState].listening = true
|
88 | cb(null, address)
|
89 | onListenHookRunner(this)
|
90 | })
|
91 | }
|
92 | } else {
|
93 | listenOptions.cb = (err, address) => {
|
94 |
|
95 | if (err) {
|
96 | cb(err, address)
|
97 | return
|
98 | }
|
99 | this[kState].listening = true
|
100 | cb(null, address)
|
101 | onListenHookRunner(this)
|
102 | }
|
103 | }
|
104 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 | if (cb === undefined) {
|
111 | const listening = listenPromise.call(this, server, listenOptions)
|
112 |
|
113 | return listening.then(address => {
|
114 | return new Promise((resolve, reject) => {
|
115 | if (host === 'localhost') {
|
116 | multipleBindings.call(this, server, httpHandler, options, listenOptions, () => {
|
117 | this[kState].listening = true
|
118 | resolve(address)
|
119 | onListenHookRunner(this)
|
120 | })
|
121 | } else {
|
122 | resolve(address)
|
123 | onListenHookRunner(this)
|
124 | }
|
125 | })
|
126 | })
|
127 | }
|
128 |
|
129 | this.ready(listenCallback.call(this, server, listenOptions))
|
130 | }
|
131 |
|
132 | return { server, listen }
|
133 | }
|
134 |
|
135 | function multipleBindings (mainServer, httpHandler, serverOpts, listenOptions, onListen) {
|
136 |
|
137 | this[kState].listening = false
|
138 |
|
139 |
|
140 | dns.lookup(listenOptions.host, { all: true }, (dnsErr, addresses) => {
|
141 | if (dnsErr) {
|
142 |
|
143 |
|
144 | onListen()
|
145 | return
|
146 | }
|
147 |
|
148 | const isMainServerListening = mainServer.listening && serverOpts.serverFactory
|
149 |
|
150 | let binding = 0
|
151 | let bound = 0
|
152 | if (!isMainServerListening) {
|
153 | const primaryAddress = mainServer.address()
|
154 | for (const adr of addresses) {
|
155 | if (adr.address !== primaryAddress.address) {
|
156 | binding++
|
157 | const secondaryOpts = Object.assign({}, listenOptions, {
|
158 | host: adr.address,
|
159 | port: primaryAddress.port,
|
160 | cb: (_ignoreErr) => {
|
161 | bound++
|
162 |
|
163 |
|
164 | if (!_ignoreErr) {
|
165 | this[kServerBindings].push(secondaryServer)
|
166 | }
|
167 |
|
168 | if (bound === binding) {
|
169 |
|
170 | onListen()
|
171 | }
|
172 | }
|
173 | })
|
174 |
|
175 | const secondaryServer = getServerInstance(serverOpts, httpHandler)
|
176 | const closeSecondary = () => {
|
177 |
|
178 |
|
179 |
|
180 |
|
181 |
|
182 |
|
183 |
|
184 | secondaryServer.close(() => {})
|
185 | if (serverOpts.forceCloseConnections === 'idle') {
|
186 |
|
187 | secondaryServer.closeIdleConnections()
|
188 | } else if (typeof secondaryServer.closeAllConnections === 'function' && serverOpts.forceCloseConnections) {
|
189 | secondaryServer.closeAllConnections()
|
190 | }
|
191 | }
|
192 |
|
193 | secondaryServer.on('upgrade', mainServer.emit.bind(mainServer, 'upgrade'))
|
194 | mainServer.on('unref', closeSecondary)
|
195 | mainServer.on('close', closeSecondary)
|
196 | mainServer.on('error', closeSecondary)
|
197 | this[kState].listening = false
|
198 | listenCallback.call(this, secondaryServer, secondaryOpts)()
|
199 | }
|
200 | }
|
201 | }
|
202 |
|
203 | if (binding === 0) {
|
204 | onListen()
|
205 | return
|
206 | }
|
207 |
|
208 |
|
209 |
|
210 |
|
211 | const originUnref = mainServer.unref
|
212 |
|
213 | mainServer.unref = function () {
|
214 | originUnref.call(mainServer)
|
215 | mainServer.emit('unref')
|
216 | }
|
217 | })
|
218 | }
|
219 |
|
220 | function listenCallback (server, listenOptions) {
|
221 | const wrap = (err) => {
|
222 | server.removeListener('error', wrap)
|
223 | if (!err) {
|
224 | const address = logServerAddress.call(this, server, listenOptions.listenTextResolver || defaultResolveServerListeningText)
|
225 | listenOptions.cb(null, address)
|
226 | } else {
|
227 | this[kState].listening = false
|
228 | listenOptions.cb(err, null)
|
229 | }
|
230 | }
|
231 |
|
232 | return (err) => {
|
233 | if (err != null) return listenOptions.cb(err)
|
234 |
|
235 | if (this[kState].listening && this[kState].closing) {
|
236 | return listenOptions.cb(new FST_ERR_REOPENED_CLOSE_SERVER(), null)
|
237 | } else if (this[kState].listening) {
|
238 | return listenOptions.cb(new FST_ERR_REOPENED_SERVER(), null)
|
239 | }
|
240 |
|
241 | server.once('error', wrap)
|
242 | if (!this[kState].closing) {
|
243 | server.listen(listenOptions, wrap)
|
244 | this[kState].listening = true
|
245 | }
|
246 | }
|
247 | }
|
248 |
|
249 | function listenPromise (server, listenOptions) {
|
250 | if (this[kState].listening && this[kState].closing) {
|
251 | return Promise.reject(new FST_ERR_REOPENED_CLOSE_SERVER())
|
252 | } else if (this[kState].listening) {
|
253 | return Promise.reject(new FST_ERR_REOPENED_SERVER())
|
254 | }
|
255 |
|
256 | return this.ready().then(() => {
|
257 | let errEventHandler
|
258 | const errEvent = new Promise((resolve, reject) => {
|
259 | errEventHandler = (err) => {
|
260 | this[kState].listening = false
|
261 | reject(err)
|
262 | }
|
263 | server.once('error', errEventHandler)
|
264 | })
|
265 | const listen = new Promise((resolve, reject) => {
|
266 | server.listen(listenOptions, () => {
|
267 | server.removeListener('error', errEventHandler)
|
268 | resolve(logServerAddress.call(this, server, listenOptions.listenTextResolver || defaultResolveServerListeningText))
|
269 | })
|
270 |
|
271 | this[kState].listening = true
|
272 | })
|
273 |
|
274 | return Promise.race([
|
275 | errEvent,
|
276 | listen
|
277 | ])
|
278 | })
|
279 | }
|
280 |
|
281 |
|
282 |
|
283 |
|
284 |
|
285 |
|
286 |
|
287 |
|
288 |
|
289 |
|
290 |
|
291 |
|
292 |
|
293 |
|
294 |
|
295 |
|
296 |
|
297 |
|
298 |
|
299 |
|
300 | function compileValidateHTTPVersion (options) {
|
301 | let bypass = false
|
302 |
|
303 | const map = new Map()
|
304 | if (options.serverFactory) {
|
305 |
|
306 |
|
307 | bypass = true
|
308 | }
|
309 | if (options.http2) {
|
310 |
|
311 | map.set('2.0', true)
|
312 | if (options.https && options.https.allowHTTP1 === true) {
|
313 |
|
314 | map.set('1.1', true)
|
315 | map.set('1.0', true)
|
316 | }
|
317 | } else {
|
318 |
|
319 | map.set('1.1', true)
|
320 | map.set('1.0', true)
|
321 | }
|
322 |
|
323 |
|
324 | return function validateHTTPVersion (httpVersion) {
|
325 |
|
326 |
|
327 | return bypass || map.has(httpVersion)
|
328 | }
|
329 | }
|
330 |
|
331 | function getServerInstance (options, httpHandler) {
|
332 | let server = null
|
333 |
|
334 |
|
335 | const httpsOptions = options.https === true ? {} : options.https
|
336 | if (options.serverFactory) {
|
337 | server = options.serverFactory(httpHandler, options)
|
338 | } else if (options.http2) {
|
339 | if (typeof httpsOptions === 'object') {
|
340 | server = http2().createSecureServer(httpsOptions, httpHandler)
|
341 | } else {
|
342 | server = http2().createServer(httpHandler)
|
343 | }
|
344 | server.on('session', sessionTimeout(options.http2SessionTimeout))
|
345 | } else {
|
346 |
|
347 | if (httpsOptions) {
|
348 | server = https.createServer(httpsOptions, httpHandler)
|
349 | } else {
|
350 | server = http.createServer(options.http, httpHandler)
|
351 | }
|
352 | server.keepAliveTimeout = options.keepAliveTimeout
|
353 | server.requestTimeout = options.requestTimeout
|
354 |
|
355 |
|
356 |
|
357 | if (options.maxRequestsPerSocket > 0) {
|
358 | server.maxRequestsPerSocket = options.maxRequestsPerSocket
|
359 | }
|
360 | }
|
361 |
|
362 | if (!options.serverFactory) {
|
363 | server.setTimeout(options.connectionTimeout)
|
364 | }
|
365 | return server
|
366 | }
|
367 |
|
368 | function normalizeListenArgs (args) {
|
369 | if (args.length === 0) {
|
370 | return { port: 0, host: 'localhost' }
|
371 | }
|
372 |
|
373 | const cb = typeof args[args.length - 1] === 'function' ? args.pop() : undefined
|
374 | const options = { cb }
|
375 |
|
376 | const firstArg = args[0]
|
377 | const argsLength = args.length
|
378 | const lastArg = args[argsLength - 1]
|
379 | if (typeof firstArg === 'string' && isNaN(firstArg)) {
|
380 |
|
381 | options.path = firstArg
|
382 | options.backlog = argsLength > 1 ? lastArg : undefined
|
383 | } else {
|
384 |
|
385 | options.port = argsLength >= 1 && Number.isInteger(firstArg) ? firstArg : normalizePort(firstArg)
|
386 |
|
387 |
|
388 |
|
389 | options.host = argsLength >= 2 && args[1] ? args[1] : 'localhost'
|
390 | options.backlog = argsLength >= 3 ? args[2] : undefined
|
391 | }
|
392 |
|
393 | return options
|
394 | }
|
395 |
|
396 | function normalizePort (firstArg) {
|
397 | const port = Number(firstArg)
|
398 | return port >= 0 && !Number.isNaN(port) && Number.isInteger(port) ? port : 0
|
399 | }
|
400 |
|
401 | function logServerAddress (server, listenTextResolver) {
|
402 | let address = server.address()
|
403 | const isUnixSocket = typeof address === 'string'
|
404 |
|
405 | if (!isUnixSocket) {
|
406 | if (address.address.indexOf(':') === -1) {
|
407 | address = address.address + ':' + address.port
|
408 | } else {
|
409 | address = '[' + address.address + ']:' + address.port
|
410 | }
|
411 | }
|
412 |
|
413 | address = (isUnixSocket ? '' : ('http' + (this[kOptions].https ? 's' : '') + '://')) + address
|
414 |
|
415 | const serverListeningText = listenTextResolver(address)
|
416 | this.log.info(serverListeningText)
|
417 | return address
|
418 | }
|
419 |
|
420 | function http2 () {
|
421 | try {
|
422 | return require('node:http2')
|
423 | } catch (err) {
|
424 | throw new FST_ERR_HTTP2_INVALID_VERSION()
|
425 | }
|
426 | }
|
427 |
|
428 | function sessionTimeout (timeout) {
|
429 | return function (session) {
|
430 | session.setTimeout(timeout, close)
|
431 | }
|
432 | }
|
433 |
|
434 | function close () {
|
435 | this.close()
|
436 | }
|