forked from jeremyckahn/chitchatter
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsimple-api-server.js
More file actions
265 lines (229 loc) · 7.95 KB
/
simple-api-server.js
File metadata and controls
265 lines (229 loc) · 7.95 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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
/**
* Simple API Server for Chitchatter WebRTC Configuration
*
* This lightweight HTTP server provides WebRTC configuration data to enable
* peer-to-peer connections in the Chitchatter messaging application.
*
* PURPOSE:
* Chitchatter is a browser-based, peer-to-peer encrypted messaging app that
* connects users directly without storing messages on servers. To establish
* these P2P connections, browsers need WebRTC configuration including STUN/TURN
* servers for NAT traversal and firewall penetration.
*
* FUNCTIONALITY:
* - Serves WebRTC configuration via GET /api/get-config endpoint
* - Loads RTC config from RTC_CONFIG environment variable (base64 encoded JSON)
* - Falls back to hardcoded config if environment variable is missing/invalid
* - Validates RTC configuration format to ensure compatibility
* - Implements CORS security to restrict access to authorized Chitchatter domains
*
* USAGE CONTEXT:
* This server is primarily used for:
* - Development environments where developers need local RTC config
* - Self-hosted Chitchatter deployments that require custom STUN/TURN servers
* - Testing different WebRTC configurations without rebuilding the frontend
*
* The main Chitchatter app runs entirely in the browser, but WebRTC connections
* often require TURN servers (relay servers) to work through restrictive firewalls.
* This API allows dynamic configuration of those servers without hardcoding
* credentials in the client-side code.
*
* SECURITY NOTES:
* - CORS headers restrict access to approved Chitchatter domains
* - Debug mode (CORS_ALLOW_ALL=true) should only be used in development
* - TURN server credentials are sensitive and should be rotated regularly
*/
import { createServer } from 'http'
import { Buffer } from 'buffer'
// Fallback rtcConfig in case environment variable is missing or invalid
/**
* @type RTCIceServer
*/
// Fallback TURN server in case environment variable is missing or invalid
/**
* @type RTCIceServer
*/
const fallbackTurnServer = {
urls: ['turn:relay1.expressturn.com:3478'],
username: 'efQUQ79N77B5BNVVKF',
credential: 'N4EAUgpjMzPLrxSS',
}
// Validate that the decoded data conforms to RTCConfiguration interface
function isValidRTCConfiguration(data) {
if (!data || typeof data !== 'object') {
return false
}
if (!Array.isArray(data.iceServers)) {
return false
}
// Validate each ice server
for (const server of data.iceServers) {
if (!server || typeof server !== 'object') {
return false
}
// urls is required and can be string or string[]
if (!server.urls) {
return false
}
if (typeof server.urls !== 'string' && !Array.isArray(server.urls)) {
return false
}
if (Array.isArray(server.urls)) {
if (!server.urls.every(url => typeof url === 'string')) {
return false
}
}
// username and credential are optional but if present must be strings
if (server.username !== undefined && typeof server.username !== 'string') {
return false
}
if (
server.credential !== undefined &&
typeof server.credential !== 'string'
) {
return false
}
}
return true
}
// Load and validate rtcConfig from environment variable
function getRtcConfig() {
const rtcConfigEnv = process.env.RTC_CONFIG
if (!rtcConfigEnv) {
if (!process.env.IS_E2E_TEST) {
console.error(
'RTC_CONFIG environment variable is not set. Using fallback configuration.'
)
}
return fallbackTurnServer
}
try {
// Base64 decode the environment variable
const decodedConfig = Buffer.from(rtcConfigEnv, 'base64').toString('utf-8')
const parsedConfig = JSON.parse(decodedConfig)
// Validate the parsed configuration
if (!isValidRTCConfiguration(parsedConfig)) {
console.error(
'Invalid RTC configuration format in environment variable. Configuration must conform to RTCConfiguration interface. Using fallback configuration.'
)
return fallbackTurnServer
}
const turnServer = parsedConfig.iceServers.find(server =>
(Array.isArray(server.urls) ? server.urls : [server.urls]).some(url =>
url.startsWith('turn:')
)
)
if (turnServer) {
return turnServer
}
console.error(
'No TURN server found in RTC_CONFIG environment variable. Using fallback configuration.'
)
return fallbackTurnServer
} catch (error) {
if (error instanceof SyntaxError) {
console.error(
'Failed to parse RTC_CONFIG environment variable as JSON:',
error.message,
'Using fallback configuration.'
)
} else {
console.error(
'Failed to decode RTC_CONFIG environment variable:',
error,
'Using fallback configuration.'
)
}
return fallbackTurnServer
}
}
// CORS configuration
const allowedOrigins = [
'https://chitchatter.im',
'https://chitchatter.vercel.app',
'https://chitchatter-git-develop-jeremyckahn.vercel.app',
'http://localhost:3000', // Development frontend
'http://localhost:3001', // API development
'http://localhost:3003', // Simple API server
]
function getCorsOrigin(req) {
// Check for debug override first
if (process.env.CORS_ALLOW_ALL === 'true') {
return '*'
}
// Production mode: Restrict to allowed domains
const origin = req.headers.origin
if (origin && allowedOrigins.includes(origin)) {
return origin
}
// For same-origin requests or allowed deployments, use the primary domain
return 'https://chitchatter.im'
}
// API handler function
function handleGetConfig(req, res) {
console.log(`API handler called with method: ${req.method}`)
// Only allow GET requests
if (req.method !== 'GET') {
console.log(`Method ${req.method} not allowed, returning 405`)
res.writeHead(405, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': getCorsOrigin(req),
'Access-Control-Allow-Methods': 'GET',
'Access-Control-Allow-Headers': 'Content-Type',
})
res.end(JSON.stringify({ error: 'Method not allowed' }))
return
}
// Set CORS headers - restrict to same domain for security (unless debug override is enabled)
const corsOrigin = getCorsOrigin(req)
res.setHeader('Access-Control-Allow-Origin', corsOrigin)
res.setHeader('Access-Control-Allow-Methods', 'GET')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
if (corsOrigin === '*') {
console.log('CORS headers set with wildcard origin (DEBUG MODE - INSECURE)')
} else {
console.log('CORS headers set with restricted origin')
}
// Get rtcConfig from environment variable with validation
const rtcConfig = getRtcConfig()
console.log('Retrieved RTC config:', JSON.stringify(rtcConfig, null, 2))
// Set content type explicitly
res.setHeader('Content-Type', 'application/json')
console.log('Content-Type header set to application/json')
// Return the rtcConfig as JSON
console.log('Returning RTC config as JSON response')
res.writeHead(200)
res.end(JSON.stringify(rtcConfig))
}
// Create HTTP server
const server = createServer((req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`)
console.log(`${req.method} ${url.pathname}`)
// Handle API routes
if (url.pathname === '/api/get-config') {
handleGetConfig(req, res)
return
}
// Handle other routes (404)
res.writeHead(404, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': getCorsOrigin(req),
})
res.end(JSON.stringify({ error: 'Not found' }))
})
const PORT = process.env.PORT || 3003
server.listen(PORT, '127.0.0.1', () => {
console.log(`🚀 Simple API server running at http://127.0.0.1:${PORT}`)
console.log(
`📡 RTC Config API available at http://127.0.0.1:${PORT}/api/get-config`
)
console.log(`🔧 To test: curl http://127.0.0.1:${PORT}/api/get-config`)
})
// Handle graceful shutdown
process.on('SIGINT', () => {
console.log('\n👋 Shutting down API server...')
server.close(() => {
console.log('✅ API server stopped')
process.exit(0)
})
})