-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathproxy-server.js
More file actions
378 lines (327 loc) · 12.1 KB
/
proxy-server.js
File metadata and controls
378 lines (327 loc) · 12.1 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
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
/**
* Daytona Preview Proxy Server
* Proxies requests to Daytona preview URLs with dynamic authentication token injection
* Based on: https://github.com/daytonaio/daytona-proxy-samples/blob/main/typescript/index.ts
*
* This implementation:
* - Dynamically fetches preview URLs and tokens using Daytona API
* - Injects authentication headers for each request
* - Bypasses CORS and iframe restrictions for browser preview mode
*/
import { Configuration, SandboxApi } from '@daytonaio/api-client';
import { config } from 'dotenv';
import http from 'http';
import httpProxy from 'http-proxy';
import { parse } from 'url';
// Load environment variables
config({ quiet: true });
// Environment variables
const PORT = process.env.PROXY_PORT || 8080;
const DAYTONA_API_KEY = process.env.DAYTONA_API_KEY;
const DAYTONA_API_URL = process.env.DAYTONA_API_URL || 'https://app.daytona.io/api';
if (!DAYTONA_API_KEY) {
console.error('❌ DAYTONA_API_KEY is not set');
process.exit(1);
}
if (!DAYTONA_API_URL) {
console.error('❌ DAYTONA_API_URL is not set');
process.exit(1);
}
// Initialize Daytona API client
const sandboxApi = new SandboxApi(
new Configuration({
basePath: DAYTONA_API_URL,
baseOptions: {
headers: {
Authorization: `Bearer ${DAYTONA_API_KEY}`
}
}
})
);
// Create proxy server instance
const proxy = httpProxy.createProxyServer({
changeOrigin: true,
secure: false, // Allow self-signed certificates in development
ws: true, // Enable WebSocket proxying
timeout: 30000, // 30 second timeout
proxyTimeout: 30000
});
// Cache for preview URLs and tokens (sandboxId:port -> { url, token, timestamp })
const previewCache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
/**
* Parse sandboxId and port from URL
* Supports formats:
* - Path-based: /:sandboxId/:port/path/to/resource
* - Query params: /path?sandboxId=abc&port=3000
* - Subdomain: abc-3000.proxy.domain.com/path
*/
function getSandboxIdAndPortFromUrl(url, host) {
const parsedUrl = parse(url || '', true);
const pathname = parsedUrl.pathname || '/';
// Try path-based format first: /:sandboxId/:port/*
const pathMatch = pathname.match(/^\/([^\/]+)\/(\d+)(\/.*)?$/);
if (pathMatch) {
return {
sandboxId: pathMatch[1],
port: parseInt(pathMatch[2]),
path: pathMatch[3] || '/'
};
}
// Try query parameters
const sandboxId = parsedUrl.query.sandboxId || parsedUrl.query.sandbox;
const port = parsedUrl.query.port ? parseInt(parsedUrl.query.port) : null;
if (sandboxId && port) {
return { sandboxId, port, path: pathname };
}
// Try subdomain format (e.g., abc-3000.proxy.domain.com)
if (host) {
const match = host.match(/^([^-]+)-(\d+)\./);
if (match) {
return {
sandboxId: match[1],
port: parseInt(match[2]),
path: pathname
};
}
}
throw new Error('Invalid URL format. Use /:sandboxId/:port/path or ?sandboxId=xxx&port=yyy');
}
/**
* Get preview URL and token from Daytona API
* Implements caching to avoid excessive API calls
*/
async function getPortPreviewUrl(sandboxId, port) {
const cacheKey = `${sandboxId}:${port}`;
const cached = previewCache.get(cacheKey);
// Return cached value if still valid
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
console.log(`📦 Using cached preview URL for ${cacheKey}`);
return { url: cached.url, token: cached.token };
}
// Fetch fresh preview URL and token from Daytona API using API client
try {
console.log(`🔍 Fetching preview URL for sandbox ${sandboxId}, port ${port}`);
// Use the Daytona API client to get preview URL
const response = await sandboxApi.getPortPreviewUrl(sandboxId, port);
if (!response.data || !response.data.url) {
throw new Error('No preview URL in Daytona API response');
}
// Cache the result
previewCache.set(cacheKey, {
url: response.data.url,
token: response.data.token || null,
timestamp: Date.now()
});
console.log(`✅ Fetched preview URL for ${cacheKey}: ${response.data.url}`);
return { url: response.data.url, token: response.data.token || null };
} catch (error) {
const errorMessage = error?.response?.data?.message || error?.message || String(error);
console.error(`❌ Failed to fetch preview URL for ${cacheKey}:`, errorMessage);
throw new Error(`Failed to get preview URL: ${errorMessage}`);
}
}
// Error handling
proxy.on('error', (err, req, res) => {
console.error('❌ Proxy error:', err.message);
// @ts-ignore
const reqErr = req._err;
if (reqErr) {
console.error(' Request error:', reqErr.message || String(reqErr));
}
if (res.writeHead) {
res.writeHead(500, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
});
res.end(
JSON.stringify({
error: 'Proxy Error',
message: err.message,
code: err.code,
details: reqErr ? reqErr.message || String(reqErr) : undefined
})
);
}
});
// Successful proxy response
proxy.on('proxyRes', (proxyRes, req, res) => {
// Add CORS headers to allow iframe embedding
proxyRes.headers['Access-Control-Allow-Origin'] = '*';
proxyRes.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, PATCH, OPTIONS';
proxyRes.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization';
// Remove restrictive headers that prevent iframe embedding
delete proxyRes.headers['x-frame-options'];
delete proxyRes.headers['content-security-policy'];
console.log(`✅ Proxied: ${req.method} ${req.url} -> ${proxyRes.statusCode}`);
});
// Create HTTP server
const server = http.createServer((req, res) => {
const parsedUrl = parse(req.url || '', true);
const pathname = parsedUrl.pathname || '/';
// CORS preflight
if (req.method === 'OPTIONS') {
res.writeHead(200, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Daytona-Token, X-Project-Id'
});
res.end();
return;
}
// Health check endpoint
if (pathname === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({
status: 'ok',
uptime: process.uptime(),
cachedPreviews: previewCache.size
})
);
return;
}
// Clear cache endpoint
if (pathname === '/clear-cache' && req.method === 'POST') {
const size = previewCache.size;
previewCache.clear();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, clearedEntries: size }));
console.log(`�️ Cleared ${size} cached preview URLs`);
return;
}
// Get cache status endpoint
if (pathname === '/cache' && req.method === 'GET') {
const cacheEntries = Array.from(previewCache.entries()).map(([key, value]) => ({
key,
url: value.url,
hasToken: !!value.token,
age: Math.round((Date.now() - value.timestamp) / 1000)
}));
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ entries: cacheEntries, size: previewCache.size }));
return;
}
// Proxy requests - dynamically fetch preview URL and token
(async () => {
try {
// Parse sandboxId, port, and path from request
const { sandboxId, port, path } = getSandboxIdAndPortFromUrl(req.url, req.headers.host);
console.log(`🔄 Proxy request for sandbox ${sandboxId}, port ${port}, path ${path}`);
// Get preview URL and token from Daytona API
const { url: targetUrl, token } = await getPortPreviewUrl(sandboxId, port);
// Store the token for use in proxyReq handler
// @ts-ignore - adding custom property to request
req._authToken = token;
// @ts-ignore - store target URL
req._targetUrl = targetUrl;
// Inject Daytona authentication headers if token is available
if (token) {
req.headers['x-daytona-preview-token'] = token;
req.headers['x-daytona-skip-preview-warning'] = 'true';
req.headers['x-daytona-disable-cors'] = 'true';
console.log(`🔑 Injected Daytona auth token for ${sandboxId}:${port}`);
} else {
console.warn(`⚠️ No token available for ${sandboxId}:${port}`);
}
// Rewrite the request URL to include only the path
req.url = path;
// Remove problematic headers
delete req.headers.host;
// Proxy the request to the target URL
console.log(`➡️ Proxying: ${req.method} ${path} -> ${targetUrl}${path}`);
proxy.web(req, res, {
target: targetUrl,
changeOrigin: true
});
} catch (error) {
console.error('❌ Error handling proxy request:', error.message);
// @ts-ignore - store error for use in error handler
req._err = error;
res.writeHead(500, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
});
res.end(
JSON.stringify({
error: 'Proxy Configuration Error',
message: error.message,
hint: 'Ensure sandboxId and port are provided as query parameters'
})
);
}
})();
});
// WebSocket support
server.on('upgrade', async (req, socket, head) => {
try {
// Parse sandboxId, port, and path from request
const { sandboxId, port, path } = getSandboxIdAndPortFromUrl(req.url, req.headers.host);
console.log(`🔌 WebSocket upgrade for sandbox ${sandboxId}, port ${port}, path ${path}`);
// Get preview URL and token from Daytona API
const { url: targetUrl, token } = await getPortPreviewUrl(sandboxId, port);
// Inject Daytona headers for WebSocket connections
if (token) {
req.headers['x-daytona-preview-token'] = token;
req.headers['x-daytona-skip-preview-warning'] = 'true';
req.headers['x-daytona-disable-cors'] = 'true';
console.log(`🔑 Injected Daytona auth token for WebSocket ${sandboxId}:${port}`);
}
// Rewrite the request URL to include only the path
req.url = path;
// Remove problematic headers
delete req.headers.host;
console.log(`➡️ WebSocket proxying to: ${targetUrl}${path}`);
proxy.ws(req, socket, head, {
target: targetUrl,
changeOrigin: true
});
} catch (error) {
console.error('❌ WebSocket upgrade error:', error.message);
socket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n');
socket.destroy();
}
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('🛑 SIGTERM received, shutting down gracefully...');
server.close(() => {
console.log('✅ Server closed');
process.exit(0);
});
});
process.on('SIGINT', () => {
console.log('\n🛑 SIGINT received, shutting down gracefully...');
server.close(() => {
console.log('✅ Server closed');
process.exit(0);
});
});
// Start server
server.listen(PORT, () => {
console.log(`
╔════════════════════════════════════════════════════════════╗
║ ║
║ 🚀 Daytona Preview Proxy Server (Dynamic) ║
║ ║
║ Listening on: http://localhost:${PORT} ║
║ Health check: http://localhost:${PORT}/health ║
║ ║
║ Usage: ║
║ http://localhost:${PORT}?sandboxId=xxx&port=3000 ║
║ ║
║ Endpoints: ║
║ - GET /health Health check ║
║ - GET /cache View cached preview URLs ║
║ - POST /clear-cache Clear preview URL cache ║
║ ║
║ Features: ║
║ ✓ Dynamic token fetching from Daytona API ║
║ ✓ Automatic authentication header injection ║
║ ✓ Preview URL caching (${CACHE_TTL / 1000}s TTL) ║
║ ✓ WebSocket support ║
║ ║
╚════════════════════════════════════════════════════════════╝
`);
});
export default server;