fix: (regression beta) acme certificate not working when not using port 80

Massimo Melina committed Mar 28, 2024 at 14:51 UTC b808302e773175c0ed5592a41d8adab2277fe790
1 file changed +11 -21
src/acme.ts
+11 -21
@@ -1,16 +1,5 @@
1 -import {
2 - DAY,
3 - Dict,
4 - haveTimeout,
5 - HOUR,
6 - HTTP_BAD_REQUEST,
7 - HTTP_FAILED_DEPENDENCY,
8 - HTTP_OK,
9 - HTTP_SERVER_ERROR,
10 - MINUTE,
11 - repeat
12 -} from './cross'
13 -import { createServer, RequestListener } from 'http'
1 +import { DAY, Dict, haveTimeout, HOUR, HTTP_BAD_REQUEST, HTTP_FAILED_DEPENDENCY, HTTP_OK, MINUTE, repeat } from './misc'
2 +import { createServer, IncomingMessage, ServerResponse } from 'http'
3 import { Middleware } from 'koa'
4 import { getNatInfo, upnpClient } from './nat'
5 import { cert, getCertObject, getServerStatus, privateKey } from './listen'
@@ -22,19 +11,19 @@ import { defineConfig } from './config'
11 import events from './events'
12 import { selfCheck } from './selfCheck'
13
25 -let acmeMiddlewareEnabled = false
14 +let acmeOngoing = false
15 const acmeTokens: Dict<string> = {}
27 -const acmeListener: RequestListener = (req, res) => { // node format
16 +const acmeListener = (req: IncomingMessage, res: ServerResponse) => { // node listener
17 const BASE = '/.well-known/acme-challenge/'
18 if (!req.url?.startsWith(BASE)) return
19 const token = req.url.slice(BASE.length)
20 console.debug("got http challenge", token)
21 res.statusCode = HTTP_OK
22 res.end(acmeTokens[token])
34 - return true
23 + return true // true = responded
24 }
25 export const acmeMiddleware: Middleware = (ctx, next) => { // koa format
37 - if (!acmeMiddlewareEnabled || !Boolean(acmeListener(ctx.req, ctx.res)))
26 + if (!acmeOngoing || !acmeListener(ctx.req, ctx.res))
27 return next()
28 }
29
@@ -45,7 +34,7 @@ repeat(MINUTE, async stop => {
34 const res = await upnpClient.getMappings()
35 const leftover = res.find(x => x.description === TEMP_MAP.description) // in case the process is interrupted
36 if (!leftover) return void stop() // we are good
48 - if (acmeMiddlewareEnabled) return // it doesn't count, as we are in the middle of something. Retry later
37 + if (acmeOngoing) return // it doesn't count, as we are in the middle of something. Retry later
38 stop()
39 return upnpClient.removeMapping(TEMP_MAP)
40 })
@@ -54,14 +43,15 @@ async function generateSSLCert(domain: string, email?: string, altNames?: string
43 // will answer challenge through our koa app (if on port 80) or must we spawn a dedicated server?
44 const nat = await getNatInfo()
45 const { http } = await getServerStatus()
57 - const tempSrv = nat.externalPort === 80 || http.listening && http.port === 80 ? undefined : createServer(acmeListener)
46 + const tempSrv = nat.externalPort === 80 || http.listening && http.port === 80 ? undefined
47 + : createServer((req, res) => acmeListener(req, res) || res.end('HFS')) // also satisfy self-check
48 if (tempSrv)
49 await new Promise<void>(resolve =>
50 tempSrv.listen(80, resolve).on('error', (e: any) => {
51 console.debug("cannot listen on 80", e.code || e)
52 resolve() // go on anyway
53 }) )
64 - acmeMiddlewareEnabled = true
54 + acmeOngoing = true
55 console.debug("acme challenge server ready")
56 let tempMap: any
57 try {
@@ -98,7 +88,7 @@ async function generateSSLCert(domain: string, email?: string, altNames?: string
88 console.debug("removing temporary port forward")
89 upnpClient.removeMapping(TEMP_MAP).catch(() => {}) // clean after ourselves
90 }
101 - acmeMiddlewareEnabled = false
91 + acmeOngoing = false
92 if (tempSrv) await new Promise(res => tempSrv.close(res))
93 console.debug('acme terminated')
94 }