main
ts 98 lines 4.98 KB
Raw
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
3 import { ApiError, ApiHandlers } from './apiMiddleware'
4 import {
5 HTTP_BAD_REQUEST, HTTP_FAILED_DEPENDENCY, HTTP_SERVER_ERROR, HTTP_SERVICE_UNAVAILABLE, HTTP_PRECONDITION_FAILED
6 } from './const'
7 import _ from 'lodash'
8 import { getCertObject } from './listen'
9 import { getProjectInfo } from './github'
10 import { apiAssertTypes, haveTimeout, onlyTruthy, promiseBestEffort } from './misc'
11 import { lookup, Resolver } from 'dns/promises'
12 import { isIPv6 } from 'net'
13 import { getNatInfo, getPublicIps, getUpnpClient, mappedPort, upnpMappingParam } from './nat'
14 import { makeCert } from './acme'
15 import { selfCheck } from './selfCheck'
16
17 export default {
18 get_nat: getNatInfo,
19 get_public_ips: getPublicIps,
20
21 async check_domain({ domain }) {
22 apiAssertTypes({ string: { domain } })
23 const resolver = new Resolver({ timeout: 3_000, tries: 2 })
24 const prjInfo = await getProjectInfo()
25 resolver.setServers(prjInfo.dnsServers)
26 const timeout = 7_000 // the external timeout should be larger
27 const settled = await Promise.allSettled([
28 haveTimeout(timeout, resolver.resolve(domain, 'A')),
29 haveTimeout(timeout, resolver.resolve(domain, 'AAAA')),
30 haveTimeout(timeout, lookup(domain).then(x => [x.address])),
31 ])
32 if (settled[0].status === 'rejected' && settled[0].reason.code === 'ECONNREFUSED')
33 return new ApiError(HTTP_SERVICE_UNAVAILABLE, "cannot resolve domain")
34 // merge all results
35 const domainIps = _.uniq(onlyTruthy(settled.map(x => x.status === 'fulfilled' && x.value)).flat())
36 if (!domainIps.length)
37 return new ApiError(HTTP_FAILED_DEPENDENCY, "domain not working")
38 const publicIps = await getPublicIps() // do this before stopping the server
39 for (const v6 of [false, true]) {
40 const domainIpsThisVersion = domainIps.filter(x => isIPv6(x) === v6)
41 const ipsThisVersion = publicIps.filter(x => isIPv6(x) === v6)
42 if (domainIpsThisVersion.length && ipsThisVersion.length && !_.intersection(domainIpsThisVersion, ipsThisVersion).length)
43 return new ApiError(HTTP_PRECONDITION_FAILED, `configure your domain to point to ${ipsThisVersion} (currently on ${domainIpsThisVersion[0]}) – a change can take hours to be effective`)
44 }
45 return {}
46 },
47
48 async map_port({ external, internal }) {
49 apiAssertTypes({ number_undefined: { external, internal } })
50 const { upnp, externalPort, internalPort } = await getNatInfo()
51 if (!upnp)
52 return new ApiError(HTTP_SERVICE_UNAVAILABLE, "upnp failed")
53 if (!internalPort)
54 return new ApiError(HTTP_FAILED_DEPENDENCY, "no internal port")
55 if (externalPort)
56 try { await getUpnpClient().removeMapping({ public: { host: '', port: externalPort } }) }
57 catch (e: any) { return new ApiError(HTTP_SERVER_ERROR, "removeMapping failed: " + String(e) ) }
58 if (external)
59 await getUpnpClient().createMapping(upnpMappingParam(internal || internalPort, external))
60 .catch(res => {
61 throw new ApiError(res.errorCode || HTTP_SERVER_ERROR, res.errorCode === 718 ? "Port not available" : res.errorDescription || res.message || "unknown error")
62 })
63 mappedPort.set(external || 0) // remember only successful HFS mappings
64 return {}
65 },
66
67 async self_check({ url }) {
68 apiAssertTypes({ string_undefined: { url } })
69 if (url)
70 return await selfCheck(url)
71 || new ApiError(HTTP_SERVICE_UNAVAILABLE)
72 const [publicIps, nat] = await Promise.all([getPublicIps(), getNatInfo()])
73 if (!publicIps.length)
74 return new ApiError(HTTP_FAILED_DEPENDENCY, 'cannot detect public ip')
75 if (!nat.internalPort)
76 return new ApiError(HTTP_FAILED_DEPENDENCY, 'no internal port')
77 const finalPort = nat.externalPort || nat.internalPort
78 const proto = nat.proto || (getCertObject() ? 'https' : 'http')
79 const defPort = proto === 'https' ? 443 : 80
80 const results = onlyTruthy(await promiseBestEffort(publicIps.map(ip =>
81 selfCheck(`${proto}://${ip}${finalPort === defPort ? '' : ':' + finalPort}`) )))
82 return results.length ? results : new ApiError(HTTP_SERVICE_UNAVAILABLE)
83 },
84
85 async make_cert({domain, email, altNames}) {
86 apiAssertTypes({ string: { domain }, string_undefined: { email }, array_undefined: { altNames } })
87 if (altNames?.some((name: unknown) => typeof name !== 'string'))
88 return new ApiError(HTTP_BAD_REQUEST, 'bad altNames')
89 await makeCert(domain, email, altNames).catch(e => {
90 throw new ApiError(HTTP_SERVER_ERROR, e.message || String(e))
91 })
92 return {}
93 },
94
95 get_cert() {
96 return getCertObject() || { none: true }
97 }
98 } satisfies ApiHandlers