1 | const {join, resolve} = require('path')
|
2 | const {promisify} = require('util')
|
3 | const {readFile, readFileSync} = require('fs')
|
4 | const {app} = require('./app')
|
5 | const ocsp = require('ocsp')
|
6 | const {createSecureServer} = require('http2')
|
7 | const {createSecureContext} = require('tls')
|
8 | const isDomainName = require('is-domain-name')
|
9 | const isIp = require('is-ip')
|
10 |
|
11 | async function read (filepath) {
|
12 | const resolved = resolve(process.cwd(), filepath)
|
13 | return promisify(readFile)(resolved)
|
14 | }
|
15 |
|
16 | function readSync (filepath) {
|
17 | const resolved = resolve(process.cwd(), filepath)
|
18 | return readFileSync(resolved)
|
19 | }
|
20 |
|
21 | module.exports.server = (options, files) => {
|
22 | const requestListener = app(options, files)
|
23 |
|
24 | const ecdhCurve = 'P-384:P-256'
|
25 | const fallbackKey = readSync(options.https.key)
|
26 | const fallbackCert = readSync(options.https.cert)
|
27 | const fallbackCa = options.https.ca.map(readSync)
|
28 |
|
29 | function fallbackSecureContext () {
|
30 | return createSecureContext({
|
31 | ecdhCurve,
|
32 | key: fallbackKey,
|
33 | cert: fallbackCert,
|
34 | ca: fallbackCa
|
35 | })
|
36 | }
|
37 |
|
38 | const acmeCache = new Map()
|
39 | const serverOptions = {
|
40 | allowHTTP1: true,
|
41 | ecdhCurve,
|
42 | key: fallbackKey,
|
43 | cert: fallbackCert,
|
44 | ca: fallbackCa,
|
45 | SNICallback: async (servername, callback) => {
|
46 | if (acmeCache.has(servername)) {
|
47 | const {key, cert} = acmeCache.get(servername)
|
48 | const contextOptions = {ecdhCurve, key, cert}
|
49 | return callback(null, createSecureContext(contextOptions))
|
50 | }
|
51 |
|
52 | if (!(isDomainName(servername) || isIp(servername))) {
|
53 | return callback(null, fallbackSecureContext())
|
54 | }
|
55 |
|
56 | try {
|
57 | const [key, cert] = await Promise.all([
|
58 | read(join(options.acme.store, servername, 'key.pem')),
|
59 | read(join(options.acme.store, servername, 'cert.pem'))
|
60 | ])
|
61 | const contextOptions = {ecdhCurve, key, cert}
|
62 | acmeCache.set(servername, {key, cert})
|
63 | const context = createSecureContext(contextOptions)
|
64 | return callback(null, context)
|
65 | } catch (error) {
|
66 | if (error.code !== 'ENOENT') {
|
67 | console.error(error)
|
68 | }
|
69 | return callback(null, fallbackSecureContext())
|
70 | }
|
71 | }
|
72 | }
|
73 |
|
74 | const server = createSecureServer(serverOptions, requestListener)
|
75 |
|
76 | const ocspCache = new ocsp.Cache()
|
77 | server.on('OCSPRequest', (certificate, issuer, callback) => {
|
78 | if (!issuer) return callback()
|
79 | ocsp.getOCSPURI(certificate, (error, uri) => {
|
80 | if (error) return callback(error)
|
81 | if (uri === null) return callback()
|
82 | const request = ocsp.request.generate(certificate, issuer)
|
83 | ocspCache.probe(request.id, (error, {response}) => {
|
84 | if (error) return callback(error)
|
85 | if (response) return callback(null, response)
|
86 | const options = {
|
87 | url: uri,
|
88 | ocsp: request.data
|
89 | }
|
90 | ocspCache.request(request.id, options, callback)
|
91 | })
|
92 | })
|
93 | })
|
94 |
|
95 | return server
|
96 | }
|