simpler public ip search
Massimo Melina committed
Oct 1, 2023 at 11:12 UTC
b46f6d5929bca8dc264b4e382679b8780631e30f
3 files changed
+49
-62
central.json
+22
-9
@@ -28,14 +28,27 @@
28
"http://ipinfo.io/ip"
29
],
30
"publicIpServices_049": [
31
- { "url": "http://ipv6.icanhazip.com", "v": 6 },
32
- { "url": "http://checkip.amazonaws.com", "prefers": 4 },
33
- { "url": "http://ifconfig.io/ip", "v": 6 },
34
- { "url": "http://ipv4.icanhazip.com", "v": 4 },
35
- { "url": "http://ip4only.me/api/", "v": 4, "after": "IPv4," },
36
- { "url": "http://ip6only.me/api/", "v": 6, "after": "IPv6," },
37
- { "url": "https://www.showmyip.com", "v": 46, "after": "IPv$V</td><td><b>" },
38
- { "url": "https://dnschecker.org/whats-my-ip-address.php", "v": 46, "after": "ipv$V_html.+>" },
39
- { "url": "http://ipinfo.io/ip", "prefers": 4 }
31
+ {
32
+ "v": 4, "type": "dns",
33
+ "ips": ["208.67.222.222", "208.67.220.220"],
34
+ "name": "myip.opendns.com", "dnsRecord": "A"
35
+ },
36
+ {
37
+ "v": 4, "type": "dns",
38
+ "ips": ["216.239.32.10", "216.239.34.10", "216.239.36.10", "216.239.38.10"],
39
+ "name": "o-o.myaddr.l.google.com", "dnsRecord": "TXT"
40
+ },
41
+ { "v": 4, "type": "http", "url": "http://ipv4.icanhazip.com" },
42
+ { "v": 6, "type": "http", "url": "http://ipv6.icanhazip.com" },
43
+ {
44
+ "v": 6, "type": "dns",
45
+ "ips": ["2620:0:ccc::2", "2620:0:ccd::2"],
46
+ "name": "myip.opendns.com", "dnsRecord": "AAAA"
47
+ },
48
+ {
49
+ "v": 6, "type": "dns",
50
+ "ips": ["2001:4860:4802:32::a", "2001:4860:4802:34::a", "2001:4860:4802:36::a", "2001:4860:4802:38::a"],
51
+ "name": "o-o.myaddr.l.google.com", "dnsRecord": "TXT"
52
+ }
53
]
54
}
src/api.net.ts
+22
-35
@@ -11,8 +11,10 @@ import { cert, getCertObject, getIps, getServerStatus, privateKey } from './list
11
import { getProjectInfo } from './github'
12
import { httpString } from './util-http'
13
import { exec } from 'child_process'
14
-import { apiAssertTypes, DAY, debounceAsync, haveTimeout, HOUR, MINUTE, objSameKeys, onlyTruthy, repeat, Dict, GetNat,
15
- parallelJobs } from './misc'
14
+import {
15
+ apiAssertTypes, DAY, debounceAsync, haveTimeout, HOUR, MINUTE, objSameKeys, onlyTruthy, repeat, Dict,
16
+ GetNat, promiseBestEffort, wantArray
17
+} from './misc'
18
import acme from 'acme-client'
19
import fs from 'fs/promises'
20
import { createServer, RequestListener } from 'http'
@@ -36,7 +38,7 @@ const getNatInfo = debounceAsync(async () => {
38
const res = await upnpClient.getGateway().catch(() => null)
39
const status = await getServerStatus()
40
const mappings = res && await haveTimeout(5_000, upnpClient.getMappings()).catch(() => null)
39
- console.debug('mappings found', mappings)
41
+ console.debug('mappings found', mappings?.map(x => x.description))
42
const gatewayIp = res ? new URL(res.gateway.description).hostname : await findGateway().catch(() => undefined)
43
const localIp = res?.address || (await getIps())[0]
44
const internalPort = status?.https?.listening && status.https.port || status?.http?.listening && status.http.port || undefined
@@ -45,7 +47,7 @@ const getNatInfo = debounceAsync(async () => {
47
upnp: Boolean(res),
48
localIp,
49
gatewayIp,
48
- publicIps: Object.values(await gettingIps),
50
+ publicIps: await gettingIps,
51
externalIp,
52
mapped,
53
internalPort,
@@ -54,37 +56,22 @@ const getNatInfo = debounceAsync(async () => {
56
})
57
58
async function getPublicIps() {
57
- const ret: any = {}
58
- const attemptsMade: any = { 4: 0, 6: 0 }
59
- const prjInfo = await getProjectInfo()
60
- // small parallelization. The logic is a bit complex because considers many things
61
- await parallelJobs(2, prjInfo.publicIpServices_049.map((s: any) => async () => {
62
- if (ret[4] && ret[6]) return // no more to do
63
- if (s.v && s.v !== 46 && ret[s.v] || s.prefers && ret[s.prefers]) return // no good
64
- const expected = String(s.v||'').split('')
65
- const checkAttempt = expected.length === 1 ? expected[0]! : ret[4] ? 6 : ret[6] ? 4 : 0
66
- if (attemptsMade[checkAttempt] > 1) return // give up on this (you may not have this version, after all)
67
- console.debug("trying service", s.url)
68
- for (const k of expected) attemptsMade[k]++
69
- const res = await httpString(s.url).catch(() => null)
70
- if (!res) return
71
- for (const afterV of s.v && s.after && expected.length ? expected : ['']) {
72
- if (ret[afterV]) continue
73
- let workOn = res
74
- if (s.after) {
75
- const found = workOn.match(new RegExp(s.after.replace('$V', afterV), 'i'))
76
- workOn = !found ? '' : workOn.slice(found.index! + found[0].length)
77
- }
78
- const ip = workOn.match(/[.:0-9a-fA-F]+/)?.[0]?.trim()
79
- if (ip && isIP(ip)) {
80
- ret[isIPv6(ip) ? 6 : 4] = ip
81
- continue
82
- }
83
- if (s.v === afterV)
84
- console.debug("bad reply", s.url)
85
- }
86
- }))
87
- return ret
59
+ const res = await getProjectInfo()
60
+ const groupedByVersion = Object.values(_.groupBy(res.publicIpServices_049, x => x.v))
61
+ const ips = await promiseBestEffort(groupedByVersion.map(singleVersion =>
62
+ Promise.any(singleVersion.map(async (svc: any) => {
63
+ if (svc.type === 'http')
64
+ return httpString(svc.url)
65
+ if (svc.type !== 'dns') throw "unsupported"
66
+ const resolver = new Resolver({ timeout: 2_000 })
67
+ resolver.setServers(svc.ips)
68
+ return resolver.resolve(svc.name, svc.dnsRecord)
69
+ }).map(async ret => {
70
+ const validIps = wantArray(await ret).map(x => x.trim()).filter(isIP)
71
+ if (!validIps.length) throw "no good"
72
+ return validIps
73
+ }) )))
74
+ return _.uniq(ips.flat())
75
}
76
77
function findGateway(): Promise<string | undefined> {
src/cross.ts
+5
-18
@@ -1,7 +1,6 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
// all content here is shared between client and server
3
import _ from 'lodash'
4
-import { isIPv6 } from 'net'
4
5
export const REPO_URL = 'https://github.com/rejetto/hfs/'
6
export const WIKI_URL = REPO_URL + 'wiki/'
@@ -287,7 +286,7 @@ export function ipLocalHost(ip: string) {
286
}
287
288
export function ipForUrl(ip: string) {
290
- return isIPv6(ip) ? '[' + ip + ']' : ip
289
+ return ip.includes(':') ? '[' + ip + ']' : ip
290
}
291
292
export function escapeHTML(text: string) {
@@ -295,20 +294,8 @@ export function escapeHTML(text: string) {
294
c => '&#' + ('000' + c.charCodeAt(0)).slice(-4) + ';')
295
}
296
298
-export async function parallelJobs<T>(parallelization: number, promiseReturners: (() => Promise<T>)[]) {
299
- const now = promiseReturners.slice(0, parallelization).map(x => x())
300
- let i = now.length
301
- while (now.length) {
302
- now.splice(await raceIndex(now), 1)
303
- const next = promiseReturners[i++]?.()
304
- if (next)
305
- now.push(next)
306
- }
307
-
308
- async function raceIndex(promises: Promise<unknown>[]) {
309
- let ret: number
310
- promises.forEach((p,i) => p.finally(() => ret ??= i))
311
- await Promise.race(promises)
312
- return ret!
313
- }
297
+// wait for all, but returns only those that resolved
298
+export async function promiseBestEffort<T>(promises: Promise<T>[]) {
299
+ const res = await Promise.allSettled(promises)
300
+ return res.filter(x => x.status === 'fulfilled').map((x: any) => x.value as T)
301
}