• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cameri / nostream / 28917613074

08 Jul 2026 04:30AM UTC coverage: 68.333% (+0.6%) from 67.747%
28917613074

Pull #671

github

web-flow
Merge 9ce1dc9c7 into 237b1a427
Pull Request #671: feat(admin): replace password login with NIP-98 HTTP auth

2146 of 3501 branches covered (61.3%)

Branch coverage included in aggregate %.

185 of 200 new or added lines in 10 files covered. (92.5%)

4824 of 6699 relevant lines covered (72.01%)

21.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.15
/src/utils/http.ts
1
import { IncomingMessage } from 'http'
2

3
import { createLogger } from '../factories/logger-factory'
2✔
4
import { Settings } from '../@types/settings'
5

6
const logger = createLogger('http-utils')
2✔
7

8
const normalizeIpAddress = (input: string): string => {
2✔
9
  if (input.startsWith('::ffff:')) {
1,088✔
10
    return input.slice(7)
266✔
11
  }
12

13
  return input
822✔
14
}
15

16
const isTrustedProxy = (ipAddress: string, settings: Settings): boolean => {
2✔
17
  const trustedProxies = settings.network?.trustedProxies
410✔
18

19
  if (!Array.isArray(trustedProxies) || trustedProxies.length === 0) {
410✔
20
    return false
131✔
21
  }
22

23
  const normalizedRemote = normalizeIpAddress(ipAddress)
279✔
24

25
  return trustedProxies.some((trustedProxy) => {
279✔
26
    return normalizeIpAddress(trustedProxy) === normalizedRemote
809✔
27
  })
28
}
29

30
const warnIfDeprecatedRemoteIpHeaderIsConfigured = (settings: Settings): void => {
2✔
31
  const networkSettings = settings.network as Record<string, unknown> | undefined
315✔
32
  const deprecatedHeader = networkSettings?.remote_ip_header
315✔
33

34
  if (typeof deprecatedHeader === 'string' && deprecatedHeader.trim() !== '') {
315!
35
    logger.warn(
×
36
      'WARNING: network.remote_ip_header is deprecated and no longer used. Rename it to network.remoteIpHeader to restore forwarded header handling.',
37
    )
38
  }
39
}
40

41
export const getRemoteAddress = (request: IncomingMessage, settings: Settings): string => {
2✔
42
  warnIfDeprecatedRemoteIpHeaderIsConfigured(settings)
315✔
43

44
  const header = settings.network?.remoteIpHeader as string
315✔
45

46
  const trustedProxies = settings.network?.trustedProxies
315✔
47
  if (header && (!Array.isArray(trustedProxies) || trustedProxies.length === 0)) {
315✔
48
    logger.warn('WARNING: network.remoteIpHeader is set but network.trustedProxies is empty. Forwarded headers will be ignored. Add your proxy IP to network.trustedProxies.')
4✔
49
  }
50

51
  const rawHeaderAddress = header ? request.headers[header] : undefined
315✔
52
  const headerAddress = Array.isArray(rawHeaderAddress) ? rawHeaderAddress[0] : rawHeaderAddress
315✔
53
  const socketAddress = request.socket.remoteAddress
315✔
54

55
  const trustedProxy = typeof socketAddress === 'string'
315✔
56
    && isTrustedProxy(socketAddress, settings)
57

58
  const result = trustedProxy && typeof headerAddress === 'string'
315✔
59
    ? headerAddress
60
    : socketAddress
61

62
  return (result as string).split(',')[0].trim()
315✔
63
}
64

65
const normalizePathPrefix = (pathPrefix: string | undefined): string => {
2✔
66
  if (typeof pathPrefix !== 'string') {
75✔
67
    return ''
12✔
68
  }
69

70
  const prefix = pathPrefix.split(',')[0].trim()
63✔
71

72
  if (!prefix.startsWith('/') || prefix.startsWith('//')) {
63✔
73
    return ''
2✔
74
  }
75

76
  try {
61✔
77
    const { pathname } = new URL(prefix, 'http://nostream.local')
61✔
78
    const normalized = pathname.replace(/\/+$/, '')
61✔
79

80
    return normalized === '/' ? '' : normalized
61!
81
  } catch {
82
    return ''
×
83
  }
84
}
85

86
const getRelayUrlPathPrefix = (relayUrl: string | undefined): string => {
2✔
87
  if (typeof relayUrl !== 'string') {
95✔
88
    return ''
37✔
89
  }
90

91
  try {
58✔
92
    return normalizePathPrefix(new URL(relayUrl).pathname)
58✔
93
  } catch {
94
    return ''
×
95
  }
96
}
97

98
const getTrustedForwardedPathPrefix = (request: IncomingMessage, settings: Settings): string => {
2✔
99
  const socketAddress = request.socket?.remoteAddress
98✔
100
  if (typeof socketAddress !== 'string' || !isTrustedProxy(socketAddress, settings)) {
98✔
101
    return ''
81✔
102
  }
103

104
  const rawHeader = request.headers?.['x-forwarded-prefix']
17✔
105
  const rawPrefix = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader
17!
106

107
  return normalizePathPrefix(rawPrefix)
17✔
108
}
109

110
export const getPublicPathPrefix = (request: IncomingMessage, settings: Settings): string => {
2✔
111
  return getTrustedForwardedPathPrefix(request, settings) || getRelayUrlPathPrefix(settings.info?.relay_url)
98✔
112
}
113

114
export const isSecureRequest = (request: IncomingMessage, settings: Settings): boolean => {
2✔
115
  if ('secure' in request && (request as { secure?: boolean }).secure === true) {
30✔
116
    return true
1✔
117
  }
118

119
  const socketAddress = request.socket?.remoteAddress
29✔
120
  if (typeof socketAddress !== 'string' || !isTrustedProxy(socketAddress, settings)) {
29✔
121
    return false
26✔
122
  }
123

124
  const rawHeader = request.headers?.['x-forwarded-proto']
3✔
125
  const rawProto = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader
3!
126
  const proto = typeof rawProto === 'string' ? rawProto.split(',')[0].trim().toLowerCase() : ''
3!
127

128
  return proto === 'https'
3✔
129
}
130

131
export const joinPathPrefix = (prefix: string, path: string): string => {
2✔
132
  const normalizedPrefix = prefix.replace(/\/+$/, '')
46✔
133
  const normalizedPath = path.startsWith('/') ? path : `/${path}`
46!
134

135
  return `${normalizedPrefix}${normalizedPath}`
46✔
136
}
137

138
const getTrustedForwardedHost = (request: IncomingMessage, settings: Settings): string | undefined => {
2✔
139
  const socketAddress = request.socket?.remoteAddress
16✔
140
  if (typeof socketAddress !== 'string' || !isTrustedProxy(socketAddress, settings)) {
16!
141
    return undefined
16✔
142
  }
143

NEW
144
  const rawHeader = request.headers?.['x-forwarded-host']
×
NEW
145
  const rawHost = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader
×
NEW
146
  const host = typeof rawHost === 'string' ? rawHost.split(',')[0].trim() : ''
×
147

NEW
148
  return host.length > 0 ? host : undefined
×
149
}
150

151
/**
152
 * Reconstructs the absolute URL the client used to reach this request,
153
 * honoring trusted-proxy forwarded headers and any public path prefix.
154
 * NIP-98 clients must sign this exact URL (including the query string).
155
 */
156
export const getPublicRequestUrl = (
2✔
157
  request: IncomingMessage & { originalUrl?: string },
158
  settings: Settings,
159
): string | undefined => {
160
  const host = getTrustedForwardedHost(request, settings) ?? request.headers?.host
16✔
161
  if (typeof host !== 'string' || host.length === 0) {
16!
NEW
162
    return undefined
×
163
  }
164

165
  const proto = isSecureRequest(request, settings) ? 'https' : 'http'
16!
166
  const prefix = getPublicPathPrefix(request, settings)
16✔
167
  const path = request.originalUrl ?? request.url ?? '/'
16!
168

169
  return `${proto}://${host}${joinPathPrefix(prefix, path)}`
16✔
170
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc