All files / packages/core/src same-port-ssl.ts

0% Statements 0/60
100% Branches 1/1
100% Functions 1/1
0% Lines 0/60

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113                                                                                                                                                                                                                                 
/* eslint-disable no-sequences */
import type { Prettify } from '@https-enable/types'
import type { AppType, SamePortOptions } from './type'
import http from 'node:http'
import net from 'node:net'
import process from 'node:process'
import tls from 'node:tls'
import { verifyCertificate } from '@https-enable/mkcert'
 
/**
 *
 * @param app
 * @param options
 * @link https://gist.github.com/Coolchickenguy/a424ab0f4d32f024b39cd8cdd2b912ae
 */
export function samePortSSL(app: AppType, options: SamePortOptions) {
  const isRedirect = options.redirect ?? false
  const host = options.host
  const port = Number(options.port)
 
  // The tcp server that receves all the requests
  const tcpserver = net.createServer()
  // The normal server ( MUST be http or else it will try sorting the encription out itself and will fail in this configuration)
  // This is just as secure as the normal nodejs https server
  // eslint-disable-next-line ts/ban-ts-comment
  // @ts-expect-error
  let server = http.createServer(options, app)
  // A server that redirect all the requests to https, you could have this be the normal server too.
  const redirectServer
    = http.createServer(isRedirect
      ? (req, res) => (res.writeHead(302, { location: `https://${req.headers.host || req.url}` }), res.end())
      : app)
 
  // Make the proxy server listen
  tcpserver.listen(port, host)
 
  // Handle request
  tcpserver.on('connection', (socket) => {
    // Detect http or https/tls handskake
    socket.once('data', (data) => {
      // Buffer incomeing requests
      socket.pause()
      // Detect if the provided handshake data is TLS by checking if it starts with 22, which TLS always dose
      if (data[0] === 22) {
        // Https
        // You may use this socket as a TLS socket, meaning you can attach this to the same http server
        const sock = new tls.TLSSocket(socket, { isServer: true, ...options })
        // Add the TLS socket as a connection to the main http server
        server.emit('connection', sock)
        // Append data to start of data buffer
        socket.unshift(data)
      }
      else {
        // Http
        // Emit the socket to the redirect server
        redirectServer.emit('connection', socket)
        // Http views the events, meaning I can just refire the eventEmiter
        socket.emit('data', data)
      }
      // Resume socket
      process.nextTick(() => socket.resume())
    })
  })
 
  class ServerInstance {
    isAlive: boolean = true
 
    /**
     * Kill the server
     * @returns A promise that resolves when the server ends
     */
    kill() {
      return new Promise<void>((resolve, fail) => {
        tcpserver.close((err) => {
          if (typeof err === 'undefined') {
            server.closeAllConnections()
            redirectServer.closeAllConnections()
            this.isAlive = false
            resolve()
          }
          else {
            fail(err)
          }
        })
      })
    }
 
    /**
     * Change the server options
     * @param newOptions The new server options
     * @param newOptions.cert cert content
     * @param newOptions.key key content
     */
    async refresh(newOptions: { cert: string, key: string }) {
      if (newOptions && newOptions.cert && newOptions.key && typeof newOptions.cert === 'string' && typeof newOptions.key === 'string') {
        const verify = await verifyCertificate(newOptions.key, newOptions.cert)
        if (!verify.match)
          return
 
        // 证书校验有效再更新 server
        options = { ...options, ...newOptions }
        // eslint-disable-next-line ts/ban-ts-comment
        // @ts-expect-error
        return server = http.createServer(options, app)
      }
    }
  }
 
  return new Promise<ServerInstance>(resolve => tcpserver.on('listening', () => resolve(new ServerInstance())))
}
 
export type ServerInstance = Prettify<Awaited<ReturnType<typeof samePortSSL>>>