CoolFace
Apppublic

strong-tie/inbound-calls

sourceHugging Faceupdated 2y agoView on Hugging Face
0likes
server.js371 linesDownload Raw Back to lib
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