ddeck-remote 0.0.0-stage → 0.9.7-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Проверка «это админ-запрос с компьютера».
3
+ *
4
+ * Одного loopback-адреса мало: DNS-rebinding даёт чужой странице доступ к
5
+ * loopback-API, а `/dsh-remote/pairing` отдаёт токен сопряжения — то есть контроль
6
+ * над компьютером. Поэтому проверяются Host (локальный/приватный), `Sec-Fetch-Site`
7
+ * и Origin, как это делает вендорный `assertLocalAdminTrust` в шлюзе.
8
+ *
9
+ * @module dsh-remote/admin-trust
10
+ */
11
+
12
+ /**
13
+ * @param {string} address адрес сокета.
14
+ * @returns {boolean} это петля?
15
+ */
16
+ export function isLoopbackAddress(address) {
17
+ return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1'
18
+ }
19
+
20
+ /**
21
+ * @param {string} hostname имя хоста без скобок.
22
+ * @returns {boolean} локальное/приватное имя, которому можно отдавать админ-API.
23
+ */
24
+ export function isLocalAdminHostname(hostname) {
25
+ if (hostname === 'localhost' || hostname === '::1') return true
26
+ const parts = hostname.split('.').map(Number)
27
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return false
28
+ const [a, b] = parts
29
+ return a === 127 || a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 169 && b === 254)
30
+ }
31
+
32
+ /**
33
+ * @param {import('node:http').IncomingMessage} request запрос.
34
+ * @returns {boolean} можно ли отдавать админ-ответ.
35
+ */
36
+ export function isTrustedAdminRequest(request) {
37
+ if (request?.socket === undefined || !isLoopbackAddress(request.socket.remoteAddress ?? '')) return false
38
+ const hostHeader = request.headers?.host
39
+ if (typeof hostHeader !== 'string' || /[/?#@\\]/u.test(hostHeader)) return false
40
+ let authority
41
+ let hostname
42
+ try {
43
+ const parsed = new URL(`http://${hostHeader}`)
44
+ if (parsed.pathname !== '/' || parsed.username !== '' || parsed.password !== '') return false
45
+ authority = parsed.host.toLowerCase()
46
+ hostname = parsed.hostname.replace(/^\[|\]$/gu, '').toLowerCase()
47
+ } catch {
48
+ return false
49
+ }
50
+ if (!isLocalAdminHostname(hostname)) return false
51
+ const site = request.headers['sec-fetch-site']
52
+ if (site !== undefined && site !== 'same-origin' && site !== 'none') return false
53
+ const origin = request.headers.origin
54
+ if (origin !== undefined) {
55
+ try {
56
+ const parsed = new URL(origin)
57
+ if (parsed.host.toLowerCase() !== authority) return false
58
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false
59
+ } catch {
60
+ return false
61
+ }
62
+ }
63
+ return true
64
+ }