UNPKG

14.8 kBJavaScriptView Raw
1'use strict'
2
3const http = require('node:http')
4const https = require('node:https')
5const dns = require('node:dns')
6
7const { FSTDEP011 } = require('./warnings')
8const { kState, kOptions, kServerBindings } = require('./symbols')
9const { onListenHookRunner } = require('./hooks')
10const {
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
17module.exports.createServer = createServer
18module.exports.compileValidateHTTPVersion = compileValidateHTTPVersion
19
20function defaultResolveServerListeningText (address) {
21 return `Server listening at ${address}`
22}
23
24function createServer (options, httpHandler) {
25 const server = getServerInstance(options, httpHandler)
26
27 // `this` is the Fastify object
28 function listen (listenOptions, ...args) {
29 let cb = args.slice(-1).pop()
30 // When the variadic signature deprecation is complete, the function
31 // declaration should become:
32 // function listen (listenOptions = { port: 0, host: 'localhost' }, cb = undefined)
33 // Upon doing so, the `normalizeListenArgs` function is no longer needed,
34 // and all of this preamble to feed it correctly also no longer needed.
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 // `.listen(obj, a, ..., n, callback )`
44 FSTDEP011()
45 // Deal with `.listen(port, host, backlog, [cb])`
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 // If we have a path specified, don't default host to 'localhost' so we don't end up listening
67 // on both path and host
68 // See https://github.com/fastify/fastify/issues/4007
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 // the server did not start
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 // the server did not start
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 // https://github.com/nodejs/node/issues/9390
106 // If listening to 'localhost', listen to both 127.0.0.1 or ::1 if they are available.
107 // If listening to 127.0.0.1, only listen to 127.0.0.1.
108 // If listening to ::1, only listen to ::1.
109
110 if (cb === undefined) {
111 const listening = listenPromise.call(this, server, listenOptions)
112 /* istanbul ignore else */
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
135function multipleBindings (mainServer, httpHandler, serverOpts, listenOptions, onListen) {
136 // the main server is started, we need to start the secondary servers
137 this[kState].listening = false
138
139 // let's check if we need to bind additional addresses
140 dns.lookup(listenOptions.host, { all: true }, (dnsErr, addresses) => {
141 if (dnsErr) {
142 // not blocking the main server listening
143 // this.log.warn('dns.lookup error:', dnsErr)
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 /* istanbul ignore next: the else won't be taken unless listening fails */
164 if (!_ignoreErr) {
165 this[kServerBindings].push(secondaryServer)
166 }
167
168 if (bound === binding) {
169 // regardless of the error, we are done
170 onListen()
171 }
172 }
173 })
174
175 const secondaryServer = getServerInstance(serverOpts, httpHandler)
176 const closeSecondary = () => {
177 // To avoid fall into situations where the close of the
178 // secondary server is triggered before the preClose hook
179 // is done running, we better wait until the main server
180 // is closed.
181 // No new TCP connections are accepted
182 // We swallow any error from the secondary
183 // server
184 secondaryServer.close(() => {})
185 if (serverOpts.forceCloseConnections === 'idle') {
186 // Not needed in Node 19
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 // no extra bindings are necessary
203 if (binding === 0) {
204 onListen()
205 return
206 }
207
208 // in test files we are using unref so we need to propagate the unref event
209 // to the secondary servers. It is valid only when the user is
210 // listening on localhost
211 const originUnref = mainServer.unref
212 /* c8 ignore next 4 */
213 mainServer.unref = function () {
214 originUnref.call(mainServer)
215 mainServer.emit('unref')
216 }
217 })
218}
219
220function 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
249function 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 // we set it afterwards because listen can throw
271 this[kState].listening = true
272 })
273
274 return Promise.race([
275 errEvent, // e.g invalid port range error is always emitted before the server listening
276 listen
277 ])
278 })
279}
280
281/**
282 * Creates a function that, based upon initial configuration, will
283 * verify that every incoming request conforms to allowed
284 * HTTP versions for the Fastify instance, e.g. a Fastify HTTP/1.1
285 * server will not serve HTTP/2 requests upon the result of the
286 * verification function.
287 *
288 * @param {object} options fastify option
289 * @param {function} [options.serverFactory] If present, the
290 * validator function will skip all checks.
291 * @param {boolean} [options.http2 = false] If true, the validator
292 * function will allow HTTP/2 requests.
293 * @param {object} [options.https = null] https server options
294 * @param {boolean} [options.https.allowHTTP1] If true and use
295 * with options.http2 the validator function will allow HTTP/1
296 * request to http2 server.
297 *
298 * @returns {function} HTTP version validator function.
299 */
300function compileValidateHTTPVersion (options) {
301 let bypass = false
302 // key-value map to store valid http version
303 const map = new Map()
304 if (options.serverFactory) {
305 // When serverFactory is passed, we cannot identify how to check http version reliably
306 // So, we should skip the http version check
307 bypass = true
308 }
309 if (options.http2) {
310 // HTTP2 must serve HTTP/2.0
311 map.set('2.0', true)
312 if (options.https && options.https.allowHTTP1 === true) {
313 // HTTP2 with HTTPS.allowHTTP1 allow fallback to HTTP/1.1 and HTTP/1.0
314 map.set('1.1', true)
315 map.set('1.0', true)
316 }
317 } else {
318 // HTTP must server HTTP/1.1 and HTTP/1.0
319 map.set('1.1', true)
320 map.set('1.0', true)
321 }
322 // The compiled function here placed in one of the hottest path inside fastify
323 // the implementation here must be as performant as possible
324 return function validateHTTPVersion (httpVersion) {
325 // `bypass` skip the check when custom server factory provided
326 // `httpVersion in obj` check for the valid http version we should support
327 return bypass || map.has(httpVersion)
328 }
329}
330
331function getServerInstance (options, httpHandler) {
332 let server = null
333 // node@20 do not accepts options as boolean
334 // we need to provide proper https option
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 // this is http1
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 // we treat zero as null
355 // and null is the default setting from nodejs
356 // so we do not pass the option to server
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
368function 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 /* Deal with listen (pipe[, backlog]) */
381 options.path = firstArg
382 options.backlog = argsLength > 1 ? lastArg : undefined
383 } else {
384 /* Deal with listen ([port[, host[, backlog]]]) */
385 options.port = argsLength >= 1 && Number.isInteger(firstArg) ? firstArg : normalizePort(firstArg)
386 // This will listen to what localhost is.
387 // It can be 127.0.0.1 or ::1, depending on the operating system.
388 // Fixes https://github.com/fastify/fastify/issues/1022.
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
396function normalizePort (firstArg) {
397 const port = Number(firstArg)
398 return port >= 0 && !Number.isNaN(port) && Number.isInteger(port) ? port : 0
399}
400
401function logServerAddress (server, listenTextResolver) {
402 let address = server.address()
403 const isUnixSocket = typeof address === 'string'
404 /* istanbul ignore next */
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 /* istanbul ignore next */
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
420function http2 () {
421 try {
422 return require('node:http2')
423 } catch (err) {
424 throw new FST_ERR_HTTP2_INVALID_VERSION()
425 }
426}
427
428function sessionTimeout (timeout) {
429 return function (session) {
430 session.setTimeout(timeout, close)
431 }
432}
433
434function close () {
435 this.close()
436}