-
Notifications
You must be signed in to change notification settings - Fork 233
Expand file tree
/
Copy pathhttp.ts
More file actions
183 lines (143 loc) · 5.8 KB
/
Copy pathhttp.ts
File metadata and controls
183 lines (143 loc) · 5.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
import { IncomingMessage } from 'http'
import { Settings } from '../@types/settings'
import { createLogger } from '../factories/logger-factory'
const logger = createLogger('http-utils')
const normalizeIpAddress = (input: string): string => {
if (input.startsWith('::ffff:')) {
return input.slice(7)
}
return input
}
const isTrustedProxy = (ipAddress: string, settings: Settings): boolean => {
const trustedProxies = settings.network?.trustedProxies
if (!Array.isArray(trustedProxies) || trustedProxies.length === 0) {
return false
}
const normalizedRemote = normalizeIpAddress(ipAddress)
return trustedProxies.some((trustedProxy) => {
return normalizeIpAddress(trustedProxy) === normalizedRemote
})
}
const warnIfDeprecatedRemoteIpHeaderIsConfigured = (settings: Settings): void => {
const networkSettings = settings.network as Record<string, unknown> | undefined
const deprecatedHeader = networkSettings?.remote_ip_header
if (typeof deprecatedHeader === 'string' && deprecatedHeader.trim() !== '') {
logger.warn(
'WARNING: network.remote_ip_header is deprecated and no longer used. Rename it to network.remoteIpHeader to restore forwarded header handling.',
)
}
}
export const getRemoteAddress = (request: IncomingMessage, settings: Settings): string => {
warnIfDeprecatedRemoteIpHeaderIsConfigured(settings)
const header = settings.network?.remoteIpHeader as string
const trustedProxies = settings.network?.trustedProxies
if (header && (!Array.isArray(trustedProxies) || trustedProxies.length === 0)) {
logger.warn(
'WARNING: network.remoteIpHeader is set but network.trustedProxies is empty. Forwarded headers will be ignored. Add your proxy IP to network.trustedProxies.',
)
}
const rawHeaderAddress = header ? request.headers[header] : undefined
const headerAddress = Array.isArray(rawHeaderAddress) ? rawHeaderAddress[0] : rawHeaderAddress
const socketAddress = request.socket.remoteAddress
const trustedProxy = typeof socketAddress === 'string' && isTrustedProxy(socketAddress, settings)
const result = trustedProxy && typeof headerAddress === 'string' ? headerAddress : socketAddress
return (result as string).split(',')[0].trim()
}
const normalizePathPrefix = (pathPrefix: string | undefined): string => {
if (typeof pathPrefix !== 'string') {
return ''
}
const prefix = pathPrefix.split(',')[0].trim()
if (!prefix.startsWith('/') || prefix.startsWith('//')) {
return ''
}
try {
const { pathname } = new URL(prefix, 'http://nostream.local')
const normalized = pathname.replace(/\/+$/, '')
return normalized === '/' ? '' : normalized
} catch {
return ''
}
}
const getRelayUrlPathPrefix = (relayUrl: string | undefined): string => {
if (typeof relayUrl !== 'string') {
return ''
}
try {
return normalizePathPrefix(new URL(relayUrl).pathname)
} catch {
return ''
}
}
const getTrustedForwardedPathPrefix = (request: IncomingMessage, settings: Settings): string => {
const socketAddress = request.socket?.remoteAddress
if (typeof socketAddress !== 'string' || !isTrustedProxy(socketAddress, settings)) {
return ''
}
const rawHeader = request.headers?.['x-forwarded-prefix']
const rawPrefix = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader
return normalizePathPrefix(rawPrefix)
}
export const getPublicPathPrefix = (request: IncomingMessage, settings: Settings): string => {
return getTrustedForwardedPathPrefix(request, settings) || getRelayUrlPathPrefix(settings.info?.relay_url)
}
export const isSecureRequest = (request: IncomingMessage, settings: Settings): boolean => {
if ('secure' in request && (request as { secure?: boolean }).secure === true) {
return true
}
const socketAddress = request.socket?.remoteAddress
if (typeof socketAddress !== 'string' || !isTrustedProxy(socketAddress, settings)) {
return false
}
const rawHeader = request.headers?.['x-forwarded-proto']
const rawProto = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader
const proto = typeof rawProto === 'string' ? rawProto.split(',')[0].trim().toLowerCase() : ''
return proto === 'https'
}
export const joinPathPrefix = (prefix: string, path: string): string => {
const normalizedPrefix = prefix.replace(/\/+$/, '')
const normalizedPath = path.startsWith('/') ? path : `/${path}`
return `${normalizedPrefix}${normalizedPath}`
}
/**
* Absolute URL for NIP-98 `u` matching (scheme + host + path + query).
* Scheme and host come only from `info.relay_url` (never request Host / forwarded proto).
* Returns undefined if relay_url is missing or not a usable http(s)/ws(s) URL.
*/
export const getAbsoluteHttpRequestUrl = (
request: IncomingMessage & { originalUrl?: string },
settings: Settings,
): string | undefined => {
const origin = getPublicHttpOrigin(settings)
if (!origin) {
return undefined
}
const originalUrl = typeof request.originalUrl === 'string' ? request.originalUrl : '/'
const prefix = getPublicPathPrefix(request, settings)
const pathAndQuery =
!prefix || originalUrl === prefix || originalUrl.startsWith(`${prefix}/`) || originalUrl.startsWith(`${prefix}?`)
? originalUrl
: joinPathPrefix(prefix, originalUrl)
return `${origin}${pathAndQuery}`
}
const getPublicHttpOrigin = (settings: Settings): string | undefined => {
try {
const relayUrl = settings.info?.relay_url
if (typeof relayUrl !== 'string' || relayUrl.length === 0) {
return undefined
}
const parsed = new URL(relayUrl)
if (parsed.host.length === 0) {
return undefined
}
if (parsed.protocol === 'wss:' || parsed.protocol === 'https:') {
return `https://${parsed.host}`
}
if (parsed.protocol === 'ws:' || parsed.protocol === 'http:') {
return `http://${parsed.host}`
}
} catch {
// fall through
}
return undefined
}