| 1 | import { defineConfig } from './config' |
| 2 | import { CFG, HOUR, repeat, replace, splitAt } from './cross' |
| 3 | import _ from 'lodash' |
| 4 | import { httpWithBody } from './util-http' |
| 5 | import { isIPv4 } from 'node:net' |
| 6 | import { isIPv6 } from 'net' |
| 7 | import { VERSION } from './const' |
| 8 | import events from './events' |
| 9 | import { getPublicIps } from './nat' |
| 10 | import { Readable } from 'node:stream' |
| 11 | |
| 12 | // optionally you can append '>' and a regular expression to determine what body is considered successful |
| 13 | const dynamicDnsUrl = defineConfig(CFG.dynamic_dns_url, '') |
| 14 | |
| 15 | // listening this event will trigger public-ips fetching |
| 16 | const EVENT = 'publicIpsChanged' |
| 17 | let stopFetching: any |
| 18 | let lastIPs: any |
| 19 | let lastMap: any |
| 20 | events.onListeners(EVENT, cbs => { |
| 21 | stopFetching?.() |
| 22 | if (!cbs?.size) return |
| 23 | stopFetching = repeat(HOUR, async () => { |
| 24 | const IPs = await getPublicIps() |
| 25 | if (_.isEqual(lastIPs, IPs)) return |
| 26 | lastIPs = IPs |
| 27 | events.emit(EVENT, lastMap = { |
| 28 | IPs, |
| 29 | IPX: IPs[0] || '', |
| 30 | IP4: _.find(IPs, isIPv4) || '', |
| 31 | IP6: _.find(IPs, isIPv6) || '', |
| 32 | }) |
| 33 | }) |
| 34 | }) |
| 35 | |
| 36 | export interface DynamicDnsResult { ts: string, error: string, url: string } |
| 37 | let stopListening: any |
| 38 | let last: DynamicDnsResult | undefined |
| 39 | dynamicDnsUrl.sub(v => { |
| 40 | stopListening?.() |
| 41 | if (!v) return |
| 42 | stopListening = events.on(EVENT, async () => { |
| 43 | if (!lastMap) return // called at start once, before first getPublicIps. Just skip it |
| 44 | const lines = dynamicDnsUrl.get() |
| 45 | const all: DynamicDnsResult[] = await Promise.all(lines.split('\n').map(async line => { |
| 46 | const [templateUrl, re] = splitAt('>', line) |
| 47 | const url = replace(templateUrl, lastMap, '$') |
| 48 | const error = await httpWithBody(url, { httpThrow: false, headers: { 'User-Agent': "HFS/" + VERSION } }) // UA specified as requested by no-ip guidelines |
| 49 | .then(async res => { |
| 50 | const str = String(res.body).trim() |
| 51 | return (re ? str.match(re) : res.ok) ? '' : (str || res.statusMessage) |
| 52 | }, (err: any) => err.code || err.message || String(err) ) |
| 53 | return { ts: new Date().toJSON(), error, url } |
| 54 | })) |
| 55 | last = _.find(all, 'error') || all[0] // the system is designed for just one result, and we give precedence to errors |
| 56 | events.emit('dynamicDnsError', last) |
| 57 | console.log('Dynamic dns update', last?.error || 'ok') |
| 58 | }, { callNow: true }) |
| 59 | }) |
| 60 | |
| 61 | export async function get_dynamic_dns_error() { |
| 62 | let unsub: any |
| 63 | return new Readable({ |
| 64 | objectMode: true, |
| 65 | async read() { |
| 66 | if (unsub) return |
| 67 | if (last) |
| 68 | this.push(last) // start by sending current state |
| 69 | unsub = events.on('dynamicDnsError', x => this.push(x)) // send updates, if any. This simplified way to manage the data stream is acceptable for this case of extremely low throughput |
| 70 | }, |
| 71 | async destroy() { |
| 72 | unsub() |
| 73 | this.push(null) |
| 74 | } |
| 75 | }) |
| 76 | } |