main
ts 76 lines 2.97 KB
Raw
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 }