@samitouri / QOSami-HFS / commits / b46f6d59

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 }