@goodandready/dsh-messenger-gateway 0.4.4 → 0.4.6

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.
package/lib/http.js CHANGED
@@ -1,8 +1,25 @@
1
1
  import { timingSafeEqual } from 'node:crypto'
2
2
 
3
- export async function readBody(req) {
3
+ export async function readBody(req, maxBytes = 1_048_576) {
4
+ const cl = Number(req?.headers?.['content-length'])
5
+ if (Number.isFinite(cl) && cl > maxBytes) {
6
+ const err = new Error(`Payload Too Large: content-length ${cl} exceeds ${maxBytes}`)
7
+ err.status = 413
8
+ err.statusCode = 413
9
+ throw err
10
+ }
4
11
  const chunks = []
5
- for await (const chunk of req) chunks.push(chunk)
12
+ let total = 0
13
+ for await (const chunk of req) {
14
+ total += chunk.length
15
+ if (total > maxBytes) {
16
+ const err = new Error(`Payload Too Large: exceeded ${maxBytes} bytes`)
17
+ err.status = 413
18
+ err.statusCode = 413
19
+ throw err
20
+ }
21
+ chunks.push(chunk)
22
+ }
6
23
  return Buffer.concat(chunks)
7
24
  }
8
25
 
@@ -12,11 +29,70 @@ export function writeJson(res, status, body) {
12
29
  res.end(JSON.stringify(body))
13
30
  }
14
31
 
32
+ export function isLoopback(value) {
33
+ const address = String(value || '').toLowerCase().replace(/^\[|\]$/g, '')
34
+ return address === 'localhost' || address === 'localhost.' || address === '::1'
35
+ || address.startsWith('127.')
36
+ || address.startsWith('::ffff:127.')
37
+ }
38
+
15
39
  export function isTrustedSettingsRequest(req) {
16
- const origin = String(req.headers?.origin || '')
17
- const host = String(req.headers?.host || '')
18
- if (!origin || !host) return false
19
- try { return new URL(origin).host === host } catch { return false }
40
+ const remote = req?.socket?.remoteAddress
41
+ const host = String(req?.headers?.host || '')
42
+ if (!host) return false
43
+
44
+ const site = String(req?.headers?.['sec-fetch-site'] || '').toLowerCase()
45
+ if (site === 'cross-site') return false
46
+
47
+ const origin = String(req?.headers?.origin || '')
48
+ if (origin) {
49
+ try {
50
+ const url = new URL(origin)
51
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') return false
52
+ return url.host.toLowerCase() === host.toLowerCase()
53
+ } catch {
54
+ return false
55
+ }
56
+ }
57
+
58
+ const referer = String(req?.headers?.referer || '')
59
+ if (referer) {
60
+ try {
61
+ const url = new URL(referer)
62
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') return false
63
+ return url.host.toLowerCase() === host.toLowerCase()
64
+ } catch {
65
+ return false
66
+ }
67
+ }
68
+
69
+ if (site === 'same-origin') {
70
+ if (remote !== undefined && !isLoopback(remote)) return false
71
+ return true
72
+ }
73
+
74
+ return isLoopback(remote)
75
+ }
76
+
77
+ export function isAuthorizedMessengerRequest(req, webhooksConfig) {
78
+ const site = String(req?.headers?.['sec-fetch-site'] || '').toLowerCase()
79
+ if (site === 'cross-site') return false
80
+
81
+ if (isTrustedSettingsRequest(req)) return true
82
+ if (isLoopback(req?.socket?.remoteAddress)) return true
83
+
84
+ const secret = String(webhooksConfig?.secret || '').trim()
85
+ if (secret) {
86
+ const authHeader = req?.headers?.authorization || ''
87
+ const bearer = authHeader.startsWith('Bearer ') ? authHeader.slice(7).trim() : ''
88
+ const tokenHeader = req?.headers?.['x-webhook-secret'] || ''
89
+ const provided = bearer || tokenHeader
90
+ if (provided && timingSafeCompare(provided, secret)) {
91
+ return true
92
+ }
93
+ }
94
+
95
+ return false
20
96
  }
21
97
 
22
98
  export function timingSafeCompare(a, b) {