@samitouri / QOSami-HFS / commits / dc7c8211

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> {