remove temporary port mapping, if any is left from previous execution

Massimo Melina committed Mar 17, 2024 at 18:57 UTC 2b8b4c922a8c410ef58f7d2014ad2ed5a2e30b54
2 files changed +22 -7
src/acme.ts
+16 -4
@@ -1,4 +1,4 @@
1 -import { DAY, Dict, HOUR, HTTP_BAD_REQUEST, HTTP_FAILED_DEPENDENCY, HTTP_OK, repeat } from './cross'
1 +import { DAY, Dict, haveTimeout, HOUR, HTTP_BAD_REQUEST, HTTP_FAILED_DEPENDENCY, HTTP_OK, MINUTE, repeat } from './cross'
2 import { createServer, RequestListener } from 'http'
3 import { Middleware } from 'koa'
4 import { getNatInfo, upnpClient } from './nat'
@@ -27,6 +27,18 @@ export const acmeMiddleware: Middleware = (ctx, next) => { // koa format
27 return next()
28 }
29
30 +const TEMP_MAP = { private: 80, public: { host: '', port: 80 }, description: 'hfs temporary', ttl: 5000 } // from my tests (zyxel VMG8825), lower values won't make a working mapping
31 +
32 +repeat(MINUTE, async stop => {
33 + await upnpClient.getGateway() // without this, the next call will break upnp support
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
37 + if (acmeMiddlewareEnabled) return // it doesn't count, as we are in the middle of something. Retry later
38 + stop()
39 + return upnpClient.removeMapping(TEMP_MAP)
40 +})
41 +
42 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()
@@ -46,7 +58,7 @@ async function generateSSLCert(domain: string, email?: string, altNames?: string
58 let check = await selfCheck(checkUrl) // some check services may not consider the domain, but we already verified that
59 if (check?.success === false && nat.upnp && !nat.mapped80) {
60 console.debug("setting temporary port forward")
49 - tempMap = await upnpClient.createMapping({ private: 80, public: { host: '', port: 80 }, description: 'hfs temporary', ttl: 0 }).catch(() => {})
61 + tempMap = await haveTimeout(10_000, upnpClient.createMapping(TEMP_MAP).catch(() => {})).catch(() => {})
62 check = await selfCheck(checkUrl) // repeat test
63 }
64 //if (!check) throw new ApiError(HTTP_FAILED_DEPENDENCY, "couldn't test port 80")
@@ -73,7 +85,7 @@ async function generateSSLCert(domain: string, email?: string, altNames?: string
85 finally {
86 if (tempMap) {
87 console.debug("removing temporary port forward")
76 - upnpClient.removeMapping({ public: { host: '', port: 80 } }).catch(() => {})
88 + upnpClient.removeMapping(TEMP_MAP).catch(() => {}) // clean after ourselves
89 }
90 acmeMiddlewareEnabled = false
91 if (tempSrv) await new Promise(res => tempSrv.close(res))
@@ -109,6 +121,6 @@ const renewCert = debounceAsync(async () => {
121 if (now > new Date(cert.validFrom) && now < validTo && validTo.getTime() - now.getTime() >= 30 * DAY)
122 return console.log("certificate still good")
123 await makeCert(domain, acmeEmail.get())
112 - .catch(e => console.log("error renewing certificate: ", String(e)))
124 + .catch(e => console.log("error renewing certificate: ", String(e.message || e)))
125 }, 0, { retain: DAY, retainFailure: HOUR })
126
src/cross.ts
+6 -3
@@ -330,13 +330,16 @@ export async function asyncGeneratorToArray<T>(generator: AsyncIterable<T>): Pro
330 return ret
331 }
332
333 -export function repeat(everyMs: number, cb: Callback): Callback {
333 +export function repeat(everyMs: number, cb: Callback<Callback>): Callback {
334 let stop = false
335 setTimeout(async () => {
336 - while (!stop && await Promise.allSettled([cb()]))
336 + while (!stop && await Promise.allSettled([cb(stopIt)]))
337 await wait(everyMs)
338 })
339 - return () => stop = true
339 + return stopIt
340 + function stopIt() {
341 + stop = true
342 + }
343 }
344
345 export function formatTimestamp(x: string | Date) {