faster nat operations

Massimo Melina committed Sep 2, 2023 at 23:38 UTC 1a1f00dba9dc961d76466e43938a284c27bbe3af
2 files changed +25 -12
frontend/src/BrowseFiles.ts
+1 -1
@@ -18,7 +18,7 @@ import { Checkbox, CustomCode, Spinner } from './components'
18 import { Head } from './Head'
19 import { DirEntry, state, useSnapState } from './state'
20 import { alertDialog } from './dialog'
21 -import useFetchList, { usePath } from './useFetchList'
21 +import useFetchList from './useFetchList'
22 import { useAuthorized } from './login'
23 import { acceptDropFiles, enqueue } from './upload'
24 import _ from 'lodash'
src/api.net.ts
+24 -11
@@ -2,7 +2,7 @@
2
3 import { ApiError, ApiHandlers } from './apiMiddleware'
4 import { Client } from 'nat-upnp'
5 -import { HTTP_FAILED_DEPENDENCY, HTTP_SERVICE_UNAVAILABLE, IS_MAC, IS_WINDOWS } from './const'
5 +import { HTTP_FAILED_DEPENDENCY, HTTP_SERVER_ERROR, HTTP_SERVICE_UNAVAILABLE, IS_MAC, IS_WINDOWS } from './const'
6 import axios from 'axios'
7 import {parse} from 'node-html-parser'
8 import _ from 'lodash'
@@ -10,14 +10,28 @@ import { getIps, getServerStatus } from './listen'
10 import { getProjectInfo } from './github'
11 import { httpString } from './util-http'
12 import { exec } from 'child_process'
13 +import { debounceAsync } from './misc'
14
14 -async function getNatInfo() {
15 - const client = new Client({ timeout: 3000 })
15 +const client = new Client({ timeout: 5_000 })
16 +const original = client.getGateway
17 +client.getGateway = () => {
18 + const promise = original.apply(client)
19 + promise.then(() => { // store in cache only if successful
20 + console.debug('caching gateway')
21 + // other client methods call getGateway too, so this will ensure they reuse this same result
22 + client.getGateway = () => promise
23 + }, ()=>{})
24 + return promise
25 +}
26 +client.getGateway()
27 +
28 +const getNatInfo = debounceAsync(async () => {
29 + const gettingIp = getPublicIp() // don't wait, do it in parallel
30 const res = await client.getGateway().catch(() => null)
31 const status = await getServerStatus()
32 const mappings = res && await client.getMappings().catch(() => null)
33 + console.debug('mappings found', mappings)
34 const externalIp = res && await client.getPublicIp().catch(() => null)
20 - const publicIp = await getPublicIp() || externalIp
35 const gatewayIp = res ? new URL(res.gateway.description).hostname : await getGateway().catch(() => null)
36 const localIp = res?.address || getIps()[0]
37 const internalPort = status?.https?.listening && status.https.port || status?.http?.listening && status.http.port
@@ -27,14 +41,13 @@ async function getNatInfo() {
41 upnp: Boolean(res),
42 localIp,
43 gatewayIp,
30 - publicIp,
44 + publicIp: await gettingIp || externalIp,
45 externalIp,
46 mapped,
33 - mappings,
47 internalPort,
48 externalPort: mapped?.public.port,
49 }
37 -}
50 +})
51
52 async function getPublicIp() {
53 const prjInfo = await getProjectInfo()
@@ -54,7 +67,7 @@ function getGateway(): Promise<string | undefined> {
67 return new Promise((resolve, reject) =>
68 exec(IS_WINDOWS || IS_MAC ? 'netstat -rn' : 'route -n', (err, out) => {
69 if (err) return reject(err)
57 - const re = IS_WINDOWS ? /(?:0\.0\.0\.0 +){2}([\d\.]+)/ : IS_MAC ? /default +([\d\.]+)/ : /^0\.0\.0\.0 +([\d\.]+)/
70 + const re = IS_WINDOWS ? /(?:0\.0\.0\.0 +){2}([\d.]+)/ : IS_MAC ? /default +([\d.]+)/ : /^0\.0\.0\.0 +([\d.]+)/
71 resolve(re.exec(out)?.[1])
72 }) )
73 }
@@ -68,9 +81,9 @@ const apis: ApiHandlers = {
81 return new ApiError(HTTP_SERVICE_UNAVAILABLE, 'upnp failed')
82 if (!internalPort)
83 return new ApiError(HTTP_FAILED_DEPENDENCY, 'no internal port')
71 - const client = new Client()
84 if (mapped)
73 - await client.removeMapping({ private: mapped.private.port, public: mapped.public.port, protocol: 'tcp' })
85 + try { await client.removeMapping({ private: mapped.private.port, public: mapped.public.port, protocol: 'tcp' }) }
86 + catch (e: any) { return new ApiError(HTTP_SERVER_ERROR, 'removeMapping failed: ' + String(e) ) }
87 if (external)
88 await client.createMapping({ private: internalPort, public: external, description: 'hfs', ttl: 0 })
89 return {}
@@ -102,12 +115,12 @@ const apis: ApiHandlers = {
115 const api = (axios as any)[svc.method]
116 const body = svc.body?.replace('$IP', publicIp).replace('$PORT', String(port)) || ''
117 const res = await api(svc.url, body, {headers: svc.headers})
105 - console.debug(service, 'responded')
118 const parsed = parse(res.data).querySelector(svc.selector)?.innerText
119 if (!parsed) throw console.debug('empty:' + service)
120 const success = new RegExp(svc.regexpSuccess).test(parsed)
121 const failure = new RegExp(svc.regexpFailure).test(parsed)
122 if (success === failure) throw console.debug('inconsistent:' + service) // this result cannot be trusted
123 + console.debug(service, 'responded', success)
124 return { success, service }
125 }))
126 }