fix: temporary port forward not working
Massimo Melina committed
Nov 5, 2023 at 16:02 UTC
dc7c8211d634aa8a2427a69554854b2429f902bd
4 files changed
+18
-27
admin/src/InternetPage.ts
+3
-1
@@ -2,13 +2,14 @@ import { createElement as h, useEffect, useState } from 'react'
2
import { Alert, Box, Button, Card, CardContent, CircularProgress, Divider, LinearProgress, Link } from '@mui/material'
3
import { CardMembership, HomeWorkTwoTone, Lock, PublicTwoTone, RouterTwoTone, Send } from '@mui/icons-material'
4
import { apiCall, useApiEx } from './api'
5
-import { closeDialog, DAY, formatTimestamp, GetNat, wantArray, with_ } from '@hfs/shared'
5
+import { closeDialog, DAY, formatTimestamp, wantArray, with_ } from '@hfs/shared'
6
import { Flex, LinkBtn, manipulateConfig, isIP, Btn } from './misc'
7
import { alertDialog, confirmDialog, promptDialog, toast } from './dialog'
8
import { BoolField, Form, NumberField } from '@hfs/mui-grid-form'
9
import md from './md'
10
import { isCertError } from './OptionsPage'
11
import { changeBaseUrl } from './FileForm'
12
+import { getNatInfo } from '../../src/nat'
13
14
const PORT_FORWARD_URL = 'https://portforward.com/'
15
const HIGHER_PORT = 1080
@@ -23,6 +24,7 @@ export default function InternetPage() {
24
const { data: config } = useApiEx('get_config', { only: ['base_url'] })
25
const localColor = with_([status.data?.http?.error, status.data?.https?.error], ([h, s]) =>
26
h && s ? 'error' : h || s ? 'warning' : 'success')
27
+ type GetNat = Awaited<ReturnType<typeof getNatInfo>>
28
const { data: nat, reload: reloadNat, error, loading, element } = useApiEx<GetNat>('get_nat')
29
const port = nat?.internalPort
30
const wrongMap = nat?.mapped && nat.mapped.private.port !== port && nat.mapped.private.port
src/acme.ts
+12
-13
@@ -29,24 +29,24 @@ export const acmeMiddleware: Middleware = (ctx, next) => { // koa format
29
30
async function generateSSLCert(domain: string, email?: string) {
31
// will answer challenge through our koa app (if on port 80) or must we spawn a dedicated server?
32
- const { upnp, externalPort } = await getNatInfo()
32
+ const nat = await getNatInfo()
33
const { http } = await getServerStatus()
34
- const tempSrv = externalPort === 80 || http.listening && http.port === 80 ? undefined : createServer(acmeListener)
34
+ const tempSrv = nat.externalPort === 80 || http.listening && http.port === 80 ? undefined : createServer(acmeListener)
35
if (tempSrv)
36
- await new Promise<void>((resolve) =>
36
+ await new Promise<void>(resolve =>
37
tempSrv.listen(80, resolve).on('error', (e: any) => {
38
console.debug("cannot listen on 80", e.code || e)
39
resolve() // go on anyway
40
}) )
41
acmeMiddlewareEnabled = true
42
console.debug('acme challenge server ready')
43
+ let tempMap: any
44
try {
45
const checkUrl = `http://${domain}`
46
let check = await selfCheck(checkUrl) // some check services may not consider the domain, but we already verified that
46
- if (check && !check.success && upnp && externalPort !== 80) { // consider a short-lived mapping
47
+ if (check && !check.success && nat.upnp && !nat.mapped80) {
48
console.debug("setting temporary port forward")
48
- // @ts-ignore
49
- await upnpClient.createMapping({ private: 80, public: { host: '', port: 80 }, description: 'hfs temporary', ttl: 30 }).catch(() => {})
49
+ tempMap = await upnpClient.createMapping({ private: 80, public: { host: '', port: 80 }, description: 'hfs temporary', ttl: 0 }).catch(() => {})
50
check = await selfCheck(checkUrl) // repeat test
51
}
52
//if (!check) throw new ApiError(HTTP_FAILED_DEPENDENCY, "couldn't test port 80")
@@ -63,17 +63,16 @@ async function generateSSLCert(domain: string, email?: string) {
63
challengePriority: ['http-01'],
64
skipChallengeVerification: true, // on NAT, trying to connect to your external ip will likely get your modem instead of the challenge server
65
termsOfServiceAgreed: true,
66
- async challengeCreateFn(_, c, ka) {
67
- console.debug("producing challenge")
68
- acmeTokens[c.token] = ka
69
- },
70
- async challengeRemoveFn(_, c) {
71
- delete acmeTokens[c.token]
72
- },
66
+ async challengeCreateFn(_, c, ka) { acmeTokens[c.token] = ka },
67
+ async challengeRemoveFn(_, c) { delete acmeTokens[c.token] },
68
})
69
return { key, cert }
70
}
71
finally {
72
+ if (tempMap) {
73
+ console.debug("removing temporary port forward")
74
+ upnpClient.removeMapping({ public: { host: '', port: 80 } }).catch(() => {})
75
+ }
76
acmeMiddlewareEnabled = false
77
if (tempSrv) await new Promise(res => tempSrv.close(res))
78
console.debug('acme terminated')
src/cross.ts
-11
@@ -26,17 +26,6 @@ interface Mapping {
26
ttl: number
27
local: boolean
28
}
29
-export interface GetNat {
30
- upnp: boolean,
31
- localIp?: string
32
- gatewayIp?: string
33
- publicIps: string[]
34
- externalIp: string,
35
- mapped?: Mapping
36
- internalPort?: number
37
- externalPort?: number
38
- proto?: string
39
-}
29
30
export interface VfsPerms {
31
can_see?: Who
src/nat.ts
+3
-2
@@ -1,7 +1,7 @@
1
import { proxy } from 'valtio'
2
import { Client } from 'nat-upnp-ts'
3
import { debounceAsync } from './debounceAsync'
4
-import { GetNat, haveTimeout, HOUR, MINUTE, promiseBestEffort, repeat, wantArray } from './cross'
4
+import { haveTimeout, HOUR, MINUTE, promiseBestEffort, repeat, wantArray } from './cross'
5
import { getProjectInfo } from './github'
6
import _ from 'lodash'
7
import { httpString } from './util-http'
@@ -81,10 +81,11 @@ export const getNatInfo = debounceAsync(async () => {
81
publicIps: defaultBaseUrl.publicIps = await gettingIps,
82
externalIp: defaultBaseUrl.externalIp,
83
mapped,
84
+ mapped80: _.find(mappings, x => x.private.host === localIp && x.private.port === 80 && x.public.port === 80),
85
internalPort,
86
externalPort,
87
proto: status?.https?.listening ? 'https' : status?.http?.listening ? 'http' : '',
87
- } satisfies GetNat
88
+ }
89
})
90
91
function findGateway(): Promise<string | undefined> {