strong-tie/inbound-calls
0
1'use strict'2 3const http = require('node:http')4const https = require('node:https')5const dns = require('node:dns')6const os = require('node:os')7 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_INVALID15} = require('./errors')16 17module.exports.createServer = createServer18 19function defaultResolveServerListeningText (address) {20 return `Server listening at ${address}`21}22 23function createServer (options, httpHandler) {24 const server = getServerInstance(options, httpHandler)25 26 // `this` is the Fastify object27 function listen (28 listenOptions = { port: 0, host: 'localhost' },29 cb = undefined30 ) {31 if (typeof cb === 'function') {32 listenOptions.cb = cb33 }34 if (listenOptions.signal) {35 if (typeof listenOptions.signal.on !== 'function' && typeof listenOptions.signal.addEventListener !== 'function') {36 throw new FST_ERR_LISTEN_OPTIONS_INVALID('Invalid options.signal')37 }38 39 if (listenOptions.signal.aborted) {40 this.close()41 } else {42 const onAborted = () => {43 this.close()44 }45 listenOptions.signal.addEventListener('abort', onAborted, { once: true })46 }47 }48 49 // If we have a path specified, don't default host to 'localhost' so we don't end up listening50 // on both path and host51 // See https://github.com/fastify/fastify/issues/400752 let host53 if (listenOptions.path == null) {54 host = listenOptions.host ?? 'localhost'55 } else {56 host = listenOptions.host57 }58 if (!Object.hasOwn(listenOptions, 'host') ||59 listenOptions.host == null) {60 listenOptions.host = host61 }62 if (host === 'localhost') {63 listenOptions.cb = (err, address) => {64 if (err) {65 // the server did not start66 cb(err, address)67 return68 }69 70 multipleBindings.call(this, server, httpHandler, options, listenOptions, () => {71 this[kState].listening = true72 cb(null, address)73 onListenHookRunner(this)74 })75 }76 } else {77 listenOptions.cb = (err, address) => {78 // the server did not start79 if (err) {80 cb(err, address)81 return82 }83 this[kState].listening = true84 cb(null, address)85 onListenHookRunner(this)86 }87 }88 89 // https://github.com/nodejs/node/issues/939090 // If listening to 'localhost', listen to both 127.0.0.1 or ::1 if they are available.91 // If listening to 127.0.0.1, only listen to 127.0.0.1.92 // If listening to ::1, only listen to ::1.93 94 if (cb === undefined) {95 const listening = listenPromise.call(this, server, listenOptions)96 /* istanbul ignore else */97 return listening.then(address => {98 return new Promise((resolve, reject) => {99 if (host === 'localhost') {100 multipleBindings.call(this, server, httpHandler, options, listenOptions, () => {101 this[kState].listening = true102 resolve(address)103 onListenHookRunner(this)104 })105 } else {106 resolve(address)107 onListenHookRunner(this)108 }109 })110 })111 }112 113 this.ready(listenCallback.call(this, server, listenOptions))114 }115 116 return { server, listen }117}118 119function multipleBindings (mainServer, httpHandler, serverOpts, listenOptions, onListen) {120 // the main server is started, we need to start the secondary servers121 this[kState].listening = false122 123 // let's check if we need to bind additional addresses124 dns.lookup(listenOptions.host, { all: true }, (dnsErr, addresses) => {125 if (dnsErr) {126 // not blocking the main server listening127 // this.log.warn('dns.lookup error:', dnsErr)128 onListen()129 return130 }131 132 const isMainServerListening = mainServer.listening && serverOpts.serverFactory133 134 let binding = 0135 let bound = 0136 if (!isMainServerListening) {137 const primaryAddress = mainServer.address()138 for (const adr of addresses) {139 if (adr.address !== primaryAddress.address) {140 binding++141 const secondaryOpts = Object.assign({}, listenOptions, {142 host: adr.address,143 port: primaryAddress.port,144 cb: (_ignoreErr) => {145 bound++146 147 if (!_ignoreErr) {148 this[kServerBindings].push(secondaryServer)149 }150 151 if (bound === binding) {152 // regardless of the error, we are done153 onListen()154 }155 }156 })157 158 const secondaryServer = getServerInstance(serverOpts, httpHandler)159 const closeSecondary = () => {160 // To avoid falling into situations where the close of the161 // secondary server is triggered before the preClose hook162 // is done running, we better wait until the main server is closed.163 // No new TCP connections are accepted164 // We swallow any error from the secondary server165 secondaryServer.close(() => {})166 if (typeof secondaryServer.closeAllConnections === 'function' && serverOpts.forceCloseConnections === true) {167 secondaryServer.closeAllConnections()168 }169 }170 171 secondaryServer.on('upgrade', mainServer.emit.bind(mainServer, 'upgrade'))172 mainServer.on('unref', closeSecondary)173 mainServer.on('close', closeSecondary)174 mainServer.on('error', closeSecondary)175 this[kState].listening = false176 listenCallback.call(this, secondaryServer, secondaryOpts)()177 }178 }179 }180 // no extra bindings are necessary181 if (binding === 0) {182 onListen()183 return184 }185 186 // in test files we are using unref so we need to propagate the unref event187 // to the secondary servers. It is valid only when the user is188 // listening on localhost189 const originUnref = mainServer.unref190 /* c8 ignore next 4 */191 mainServer.unref = function () {192 originUnref.call(mainServer)193 mainServer.emit('unref')194 }195 })196}197 198function listenCallback (server, listenOptions) {199 const wrap = (err) => {200 server.removeListener('error', wrap)201 server.removeListener('listening', wrap)202 if (!err) {203 const address = logServerAddress.call(this, server, listenOptions.listenTextResolver || defaultResolveServerListeningText)204 listenOptions.cb(null, address)205 } else {206 this[kState].listening = false207 listenOptions.cb(err, null)208 }209 }210 211 return (err) => {212 if (err != null) return listenOptions.cb(err)213 214 if (this[kState].listening && this[kState].closing) {215 return listenOptions.cb(new FST_ERR_REOPENED_CLOSE_SERVER(), null)216 } else if (this[kState].listening) {217 return listenOptions.cb(new FST_ERR_REOPENED_SERVER(), null)218 }219 220 server.once('error', wrap)221 if (!this[kState].closing) {222 server.once('listening', wrap)223 server.listen(listenOptions)224 this[kState].listening = true225 }226 }227}228 229function listenPromise (server, listenOptions) {230 if (this[kState].listening && this[kState].closing) {231 return Promise.reject(new FST_ERR_REOPENED_CLOSE_SERVER())232 } else if (this[kState].listening) {233 return Promise.reject(new FST_ERR_REOPENED_SERVER())234 }235 236 return this.ready().then(() => {237 let errEventHandler238 let listeningEventHandler239 function cleanup () {240 server.removeListener('error', errEventHandler)241 server.removeListener('listening', listeningEventHandler)242 }243 const errEvent = new Promise((resolve, reject) => {244 errEventHandler = (err) => {245 cleanup()246 this[kState].listening = false247 reject(err)248 }249 server.once('error', errEventHandler)250 })251 const listeningEvent = new Promise((resolve, reject) => {252 listeningEventHandler = () => {253 cleanup()254 this[kState].listening = true255 resolve(logServerAddress.call(this, server, listenOptions.listenTextResolver || defaultResolveServerListeningText))256 }257 server.once('listening', listeningEventHandler)258 })259 260 server.listen(listenOptions)261 262 return Promise.race([263 errEvent, // e.g invalid port range error is always emitted before the server listening264 listeningEvent265 ])266 })267}268 269function getServerInstance (options, httpHandler) {270 let server = null271 // node@20 do not accepts options as boolean272 // we need to provide proper https option273 const httpsOptions = options.https === true ? {} : options.https274 if (options.serverFactory) {275 server = options.serverFactory(httpHandler, options)276 } else if (options.http2) {277 if (typeof httpsOptions === 'object') {278 server = http2().createSecureServer(httpsOptions, httpHandler)279 } else {280 server = http2().createServer(httpHandler)281 }282 server.on('session', sessionTimeout(options.http2SessionTimeout))283 } else {284 // this is http1285 if (httpsOptions) {286 server = https.createServer(httpsOptions, httpHandler)287 } else {288 server = http.createServer(options.http, httpHandler)289 }290 server.keepAliveTimeout = options.keepAliveTimeout291 server.requestTimeout = options.requestTimeout292 // we treat zero as null293 // and null is the default setting from nodejs294 // so we do not pass the option to server295 if (options.maxRequestsPerSocket > 0) {296 server.maxRequestsPerSocket = options.maxRequestsPerSocket297 }298 }299 300 if (!options.serverFactory) {301 server.setTimeout(options.connectionTimeout)302 }303 return server304}305/**306 * Inspects the provided `server.address` object and returns a307 * normalized list of IP address strings. Normalization in this308 * case refers to mapping wildcard `0.0.0.0` to the list of IP309 * addresses the wildcard refers to.310 *311 * @see https://nodejs.org/docs/latest/api/net.html#serveraddress312 *313 * @param {object} A server address object as described in the314 * linked docs.315 *316 * @returns {string[]}317 */318function getAddresses (address) {319 if (address.address === '0.0.0.0') {320 return Object.values(os.networkInterfaces()).flatMap((iface) => {321 return iface.filter((iface) => iface.family === 'IPv4')322 }).sort((iface) => {323 /* c8 ignore next 2 */324 // Order the interfaces so that internal ones come first325 return iface.internal ? -1 : 1326 }).map((iface) => { return iface.address })327 }328 return [address.address]329}330 331function logServerAddress (server, listenTextResolver) {332 let addresses333 const isUnixSocket = typeof server.address() === 'string'334 if (!isUnixSocket) {335 if (server.address().address.indexOf(':') === -1) {336 // IPv4337 addresses = getAddresses(server.address()).map((address) => address + ':' + server.address().port)338 } else {339 // IPv6340 addresses = ['[' + server.address().address + ']:' + server.address().port]341 }342 343 addresses = addresses.map((address) => ('http' + (this[kOptions].https ? 's' : '') + '://') + address)344 } else {345 addresses = [server.address()]346 }347 348 for (const address of addresses) {349 this.log.info(listenTextResolver(address))350 }351 return addresses[0]352}353 354function http2 () {355 try {356 return require('node:http2')357 } catch (err) {358 throw new FST_ERR_HTTP2_INVALID_VERSION()359 }360}361 362function sessionTimeout (timeout) {363 return function (session) {364 session.setTimeout(timeout, close)365 }366}367 368function close () {369 this.close()370}371 