poll upnp externalIp and use it for links
Massimo Melina committed
Sep 6, 2023 at 23:31 UTC
3773c18dc8d9fc79ede05ddf2db28f785b91659d
4 files changed
+36
-24
src/adminApis.ts
+1
-1
@@ -95,7 +95,7 @@ export const adminApis: ApiHandlers = {
95
apiVersion: API_VERSION,
96
compatibleApiVersion: COMPATIBLE_API_VERSION,
97
...await getServerStatus(),
98
- urls: getUrls(),
98
+ urls: await getUrls(),
99
baseUrl: baseUrl.get(), // can be retrieved with get_config, but it's very handy with urls and low overhead. Case is different because the context is
100
updatePossible: !updateSupported() ? false : await localUpdateAvailable() ? 'local' : true,
101
proxyDetected: getProxyDetected(),
src/api.net.ts
+14
-13
@@ -10,30 +10,31 @@ 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'
13
+import { debounceAsync, MINUTE, repeat } from './misc'
14
15
const client = new Client({ timeout: 5_000 })
16
const original = client.getGateway
17
-client.getGateway = () => {
17
+// other client methods call getGateway too, so this will ensure they reuse this same result
18
+client.getGateway = function getGatewayCaching() {
19
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
- }, ()=>{})
20
+ client.getGateway = () => promise // multiple callings = same job
21
+ promise.then(() => console.debug('caching gateway'), // store in cache only if successful.
22
+ ()=> client.getGateway = getGatewayCaching) // failed, try again
23
return promise
24
}
25
client.getGateway()
26
27
+export let externalIp = Promise.resolve('') // poll external ip
28
+repeat(10 * MINUTE, () => externalIp = client.getPublicIp().catch(() => externalIp))
29
+
30
const getNatInfo = debounceAsync(async () => {
31
const gettingIp = getPublicIp() // don't wait, do it in parallel
32
const res = await client.getGateway().catch(() => null)
33
const status = await getServerStatus()
34
const mappings = res && await client.getMappings().catch(() => null)
35
console.debug('mappings found', mappings)
34
- const externalIp = res && await client.getPublicIp().catch(() => null)
35
- const gatewayIp = res ? new URL(res.gateway.description).hostname : await getGateway().catch(() => null)
36
- const localIp = res?.address || getIps()[0]
36
+ const gatewayIp = res ? new URL(res.gateway.description).hostname : await findGateway().catch(() => null)
37
+ const localIp = res?.address || (await getIps())[0]
38
const internalPort = status?.https?.listening && status.https.port || status?.http?.listening && status.http.port
39
const mapped = _.find(mappings, x => x.private.host === localIp && x.private.port === internalPort || x.description === 'hfs')
40
console.debug('responding')
@@ -41,8 +42,8 @@ const getNatInfo = debounceAsync(async () => {
42
upnp: Boolean(res),
43
localIp,
44
gatewayIp,
44
- publicIp: await gettingIp || externalIp,
45
- externalIp,
45
+ publicIp: await gettingIp || await externalIp,
46
+ externalIp: await externalIp,
47
mapped,
48
internalPort,
49
externalPort: mapped?.public.port,
@@ -63,7 +64,7 @@ async function getPublicIp() {
64
catch (e: any) { console.debug(String(e)) }
65
}
66
66
-function getGateway(): Promise<string | undefined> {
67
+function findGateway(): Promise<string | undefined> {
68
return new Promise((resolve, reject) =>
69
exec(IS_WINDOWS || IS_MAC ? 'netstat -rn' : 'route -n', (err, out) => {
70
if (err) return reject(err)
src/cross.ts
+5
@@ -4,6 +4,7 @@ import _ from 'lodash'
4
5
export const REPO_URL = 'https://github.com/rejetto/hfs/'
6
export const WIKI_URL = REPO_URL + 'wiki/'
7
+export const MINUTE = 60_000
8
9
export type Dict<T=any> = Record<string, T>
10
export type Falsy = false | null | undefined | '' | 0
@@ -216,3 +217,7 @@ export async function asyncGeneratorToArray<T>(generator: AsyncIterable<T>): Pro
217
return ret
218
}
219
220
+export function repeat(every: number, cb: () => unknown) {
221
+ Promise.allSettled([cb()]).then(() =>
222
+ setTimeout(() => repeat(every, cb), every) )
223
+}
\ No newline at end of file
src/listen.ts
+16
-10
@@ -14,6 +14,7 @@ import findProcess from 'find-process'
14
import { anyAccountCanLoginAdmin } from './adminApis'
15
import _ from 'lodash'
16
import { X509Certificate } from 'crypto'
17
+import { externalIp } from './api.net'
18
19
interface ServerExtra { name: string, error?: string, busy?: Promise<string> }
20
let httpSrv: undefined | http.Server & ServerExtra
@@ -44,10 +45,10 @@ export function openAdmin() {
45
const a = srv?.address()
46
if (!a || typeof a === 'string') continue
47
const baseUrl = srv!.name + '://localhost:' + a.port
47
- open(baseUrl + ADMIN_URI, { wait: true}).catch(e => {
48
+ open(baseUrl + ADMIN_URI, { wait: true}).catch(async e => {
49
console.debug(String(e))
50
console.warn("cannot launch browser on this machine >PLEASE< open your browser and reach one of these (you may need a different address)",
50
- ...Object.values(getUrls()).flat().map(x => '\n - ' + x + ADMIN_URI))
51
+ ...Object.values(await getUrls()).flat().map(x => '\n - ' + x + ADMIN_URI))
52
if (! anyAccountCanLoginAdmin())
53
console.log(`HINT: you can enter command: create-admin YOUR_PASSWORD`)
54
})
@@ -212,11 +213,14 @@ export async function getServerStatus() {
213
214
const ignore = /^(lo|.*loopback.*|virtualbox.*|.*\(wsl\).*|llw\d|awdl\d|utun\d|anpi\d)$/i // avoid giving too much information
215
215
-export function getIps() {
216
- return v4first(onlyTruthy(Object.entries(networkInterfaces()).map(([name, nets]) =>
216
+export async function getIps() {
217
+ const ips = onlyTruthy(Object.entries(networkInterfaces()).map(([name, nets]) =>
218
nets && !ignore.test(name)
218
- && v4first(onlyTruthy(nets.map(net => !net.internal && net.address)))[0] // for each interface we consider only 1 address
219
- )).flat())
219
+ && v4first(onlyTruthy(nets.map(net => !net.internal && net.address)))[0] // for each interface we consider only 1 address
220
+ )).flat()
221
+ const e = await externalIp
222
+ if (e) ips.unshift(e)
223
+ return v4first(ips)
224
.filter((x,i,a) => a.length > 1 || !x.startsWith('169.254')) // 169.254 = dhcp failure on the interface, but keep it if it's our only one
225
226
function v4first(a: string[]) {
@@ -224,8 +228,8 @@ export function getIps() {
228
}
229
}
230
227
-export function getUrls() {
228
- const ips = getIps().map(ip => ip.includes(':') ? '[' + ip + ']' : ip)
231
+export async function getUrls() {
232
+ const ips = (await getIps()).map(ip => ip.includes(':') ? '[' + ip + ']' : ip)
233
return Object.fromEntries(onlyTruthy([httpSrv, httpsSrv].map(srv => {
234
if (!srv?.listening)
235
return false
@@ -237,6 +241,8 @@ export function getUrls() {
241
}
242
243
function printUrls(srvName: string) {
240
- for (const url of getUrls()[srvName]!)
241
- console.log('serving on', url)
244
+ getUrls().then(urls => {
245
+ for (const url of urls[srvName]!)
246
+ console.log('serving on', url)
247
+ })
248
}