@cloudflare/sandbox 0.0.0-ee8c772 → 0.0.0-f5fcd52
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/CHANGELOG.md +44 -0
- package/Dockerfile +86 -9
- package/README.md +2 -0
- package/container_src/index.ts +436 -140
- package/package.json +5 -3
- package/src/client.ts +197 -37
- package/src/index.ts +14 -129
- package/src/request-handler.ts +95 -0
- package/src/sandbox.ts +258 -0
package/container_src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
1
|
+
import { type ChildProcess, type SpawnOptions, spawn } from "node:child_process";
|
|
2
2
|
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
4
|
import { serve } from "bun";
|
|
@@ -6,6 +6,8 @@ import { serve } from "bun";
|
|
|
6
6
|
interface ExecuteRequest {
|
|
7
7
|
command: string;
|
|
8
8
|
args?: string[];
|
|
9
|
+
sessionId?: string;
|
|
10
|
+
background?: boolean;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
interface GitCheckoutRequest {
|
|
@@ -51,15 +53,27 @@ interface MoveFileRequest {
|
|
|
51
53
|
sessionId?: string;
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
interface ExposePortRequest {
|
|
57
|
+
port: number;
|
|
58
|
+
name?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface UnexposePortRequest {
|
|
62
|
+
port: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
54
65
|
interface SessionData {
|
|
55
66
|
sessionId: string;
|
|
56
|
-
activeProcess:
|
|
67
|
+
activeProcess: ChildProcess | null;
|
|
57
68
|
createdAt: Date;
|
|
58
69
|
}
|
|
59
70
|
|
|
60
71
|
// In-memory session storage (in production, you'd want to use a proper database)
|
|
61
72
|
const sessions = new Map<string, SessionData>();
|
|
62
73
|
|
|
74
|
+
// In-memory storage for exposed ports
|
|
75
|
+
const exposedPorts = new Map<number, { name?: string; exposedAt: Date }>();
|
|
76
|
+
|
|
63
77
|
// Generate a unique session ID
|
|
64
78
|
function generateSessionId(): string {
|
|
65
79
|
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
@@ -84,6 +98,8 @@ const server = serve({
|
|
|
84
98
|
const url = new URL(req.url);
|
|
85
99
|
const pathname = url.pathname;
|
|
86
100
|
|
|
101
|
+
console.log(`[Container] Incoming ${req.method} request to ${pathname}`);
|
|
102
|
+
|
|
87
103
|
// Handle CORS
|
|
88
104
|
const corsHeaders = {
|
|
89
105
|
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
@@ -93,11 +109,13 @@ const server = serve({
|
|
|
93
109
|
|
|
94
110
|
// Handle preflight requests
|
|
95
111
|
if (req.method === "OPTIONS") {
|
|
112
|
+
console.log(`[Container] Handling CORS preflight for ${pathname}`);
|
|
96
113
|
return new Response(null, { headers: corsHeaders, status: 200 });
|
|
97
114
|
}
|
|
98
115
|
|
|
99
116
|
try {
|
|
100
117
|
// Handle different routes
|
|
118
|
+
console.log(`[Container] Processing ${req.method} ${pathname}`);
|
|
101
119
|
switch (pathname) {
|
|
102
120
|
case "/":
|
|
103
121
|
return new Response("Hello from Bun server! 🚀", {
|
|
@@ -107,51 +125,6 @@ const server = serve({
|
|
|
107
125
|
},
|
|
108
126
|
});
|
|
109
127
|
|
|
110
|
-
case "/api/hello":
|
|
111
|
-
return new Response(
|
|
112
|
-
JSON.stringify({
|
|
113
|
-
message: "Hello from API!",
|
|
114
|
-
timestamp: new Date().toISOString(),
|
|
115
|
-
}),
|
|
116
|
-
{
|
|
117
|
-
headers: {
|
|
118
|
-
"Content-Type": "application/json",
|
|
119
|
-
...corsHeaders,
|
|
120
|
-
},
|
|
121
|
-
}
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
case "/api/users":
|
|
125
|
-
if (req.method === "GET") {
|
|
126
|
-
return new Response(
|
|
127
|
-
JSON.stringify([
|
|
128
|
-
{ id: 1, name: "Alice" },
|
|
129
|
-
{ id: 2, name: "Bob" },
|
|
130
|
-
{ id: 3, name: "Charlie" },
|
|
131
|
-
]),
|
|
132
|
-
{
|
|
133
|
-
headers: {
|
|
134
|
-
"Content-Type": "application/json",
|
|
135
|
-
...corsHeaders,
|
|
136
|
-
},
|
|
137
|
-
}
|
|
138
|
-
);
|
|
139
|
-
} else if (req.method === "POST") {
|
|
140
|
-
return new Response(
|
|
141
|
-
JSON.stringify({
|
|
142
|
-
message: "User created successfully",
|
|
143
|
-
method: "POST",
|
|
144
|
-
}),
|
|
145
|
-
{
|
|
146
|
-
headers: {
|
|
147
|
-
"Content-Type": "application/json",
|
|
148
|
-
...corsHeaders,
|
|
149
|
-
},
|
|
150
|
-
}
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
break;
|
|
154
|
-
|
|
155
128
|
case "/api/session/create":
|
|
156
129
|
if (req.method === "POST") {
|
|
157
130
|
const sessionId = generateSessionId();
|
|
@@ -351,14 +324,38 @@ const server = serve({
|
|
|
351
324
|
}
|
|
352
325
|
break;
|
|
353
326
|
|
|
327
|
+
case "/api/expose-port":
|
|
328
|
+
if (req.method === "POST") {
|
|
329
|
+
return handleExposePortRequest(req, corsHeaders);
|
|
330
|
+
}
|
|
331
|
+
break;
|
|
332
|
+
|
|
333
|
+
case "/api/unexpose-port":
|
|
334
|
+
if (req.method === "DELETE") {
|
|
335
|
+
return handleUnexposePortRequest(req, corsHeaders);
|
|
336
|
+
}
|
|
337
|
+
break;
|
|
338
|
+
|
|
339
|
+
case "/api/exposed-ports":
|
|
340
|
+
if (req.method === "GET") {
|
|
341
|
+
return handleGetExposedPortsRequest(req, corsHeaders);
|
|
342
|
+
}
|
|
343
|
+
break;
|
|
344
|
+
|
|
354
345
|
default:
|
|
346
|
+
// Check if this is a proxy request for an exposed port
|
|
347
|
+
if (pathname.startsWith("/proxy/")) {
|
|
348
|
+
return handleProxyRequest(req, corsHeaders);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
console.log(`[Container] Route not found: ${pathname}`);
|
|
355
352
|
return new Response("Not Found", {
|
|
356
353
|
headers: corsHeaders,
|
|
357
354
|
status: 404,
|
|
358
355
|
});
|
|
359
356
|
}
|
|
360
357
|
} catch (error) {
|
|
361
|
-
console.error(
|
|
358
|
+
console.error(`[Container] Error handling ${req.method} ${pathname}:`, error);
|
|
362
359
|
return new Response(
|
|
363
360
|
JSON.stringify({
|
|
364
361
|
error: "Internal server error",
|
|
@@ -374,16 +371,19 @@ const server = serve({
|
|
|
374
371
|
);
|
|
375
372
|
}
|
|
376
373
|
},
|
|
374
|
+
hostname: "0.0.0.0",
|
|
377
375
|
port: 3000,
|
|
378
|
-
|
|
376
|
+
// We don't need this, but typescript complains
|
|
377
|
+
websocket: { async message() { } },
|
|
378
|
+
});
|
|
379
379
|
|
|
380
380
|
async function handleExecuteRequest(
|
|
381
381
|
req: Request,
|
|
382
382
|
corsHeaders: Record<string, string>
|
|
383
383
|
): Promise<Response> {
|
|
384
384
|
try {
|
|
385
|
-
const body = (await req.json()) as ExecuteRequest
|
|
386
|
-
const { command, args = [], sessionId } = body;
|
|
385
|
+
const body = (await req.json()) as ExecuteRequest;
|
|
386
|
+
const { command, args = [], sessionId, background } = body;
|
|
387
387
|
|
|
388
388
|
if (!command || typeof command !== "string") {
|
|
389
389
|
return new Response(
|
|
@@ -400,37 +400,9 @@ async function handleExecuteRequest(
|
|
|
400
400
|
);
|
|
401
401
|
}
|
|
402
402
|
|
|
403
|
-
// Basic safety check - prevent dangerous commands
|
|
404
|
-
const dangerousCommands = [
|
|
405
|
-
"rm",
|
|
406
|
-
"rmdir",
|
|
407
|
-
"del",
|
|
408
|
-
"format",
|
|
409
|
-
"shutdown",
|
|
410
|
-
"reboot",
|
|
411
|
-
];
|
|
412
|
-
const lowerCommand = command.toLowerCase();
|
|
413
|
-
|
|
414
|
-
if (
|
|
415
|
-
dangerousCommands.some((dangerous) => lowerCommand.includes(dangerous))
|
|
416
|
-
) {
|
|
417
|
-
return new Response(
|
|
418
|
-
JSON.stringify({
|
|
419
|
-
error: "Dangerous command not allowed",
|
|
420
|
-
}),
|
|
421
|
-
{
|
|
422
|
-
headers: {
|
|
423
|
-
"Content-Type": "application/json",
|
|
424
|
-
...corsHeaders,
|
|
425
|
-
},
|
|
426
|
-
status: 400,
|
|
427
|
-
}
|
|
428
|
-
);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
403
|
console.log(`[Server] Executing command: ${command} ${args.join(" ")}`);
|
|
432
404
|
|
|
433
|
-
const result = await executeCommand(command, args, sessionId);
|
|
405
|
+
const result = await executeCommand(command, args, sessionId, background);
|
|
434
406
|
|
|
435
407
|
return new Response(
|
|
436
408
|
JSON.stringify({
|
|
@@ -472,8 +444,8 @@ async function handleStreamingExecuteRequest(
|
|
|
472
444
|
corsHeaders: Record<string, string>
|
|
473
445
|
): Promise<Response> {
|
|
474
446
|
try {
|
|
475
|
-
const body = (await req.json()) as ExecuteRequest
|
|
476
|
-
const { command, args = [], sessionId } = body;
|
|
447
|
+
const body = (await req.json()) as ExecuteRequest;
|
|
448
|
+
const { command, args = [], sessionId, background } = body;
|
|
477
449
|
|
|
478
450
|
if (!command || typeof command !== "string") {
|
|
479
451
|
return new Response(
|
|
@@ -490,44 +462,19 @@ async function handleStreamingExecuteRequest(
|
|
|
490
462
|
);
|
|
491
463
|
}
|
|
492
464
|
|
|
493
|
-
// Basic safety check - prevent dangerous commands
|
|
494
|
-
const dangerousCommands = [
|
|
495
|
-
"rm",
|
|
496
|
-
"rmdir",
|
|
497
|
-
"del",
|
|
498
|
-
"format",
|
|
499
|
-
"shutdown",
|
|
500
|
-
"reboot",
|
|
501
|
-
];
|
|
502
|
-
const lowerCommand = command.toLowerCase();
|
|
503
|
-
|
|
504
|
-
if (
|
|
505
|
-
dangerousCommands.some((dangerous) => lowerCommand.includes(dangerous))
|
|
506
|
-
) {
|
|
507
|
-
return new Response(
|
|
508
|
-
JSON.stringify({
|
|
509
|
-
error: "Dangerous command not allowed",
|
|
510
|
-
}),
|
|
511
|
-
{
|
|
512
|
-
headers: {
|
|
513
|
-
"Content-Type": "application/json",
|
|
514
|
-
...corsHeaders,
|
|
515
|
-
},
|
|
516
|
-
status: 400,
|
|
517
|
-
}
|
|
518
|
-
);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
465
|
console.log(
|
|
522
466
|
`[Server] Executing streaming command: ${command} ${args.join(" ")}`
|
|
523
467
|
);
|
|
524
468
|
|
|
525
469
|
const stream = new ReadableStream({
|
|
526
470
|
start(controller) {
|
|
527
|
-
const
|
|
471
|
+
const spawnOptions: SpawnOptions = {
|
|
528
472
|
shell: true,
|
|
529
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
530
|
-
|
|
473
|
+
stdio: ["pipe", "pipe", "pipe"] as const,
|
|
474
|
+
detached: background || false,
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const child = spawn(command, args, spawnOptions);
|
|
531
478
|
|
|
532
479
|
// Store the process reference for cleanup if sessionId is provided
|
|
533
480
|
if (sessionId && sessions.has(sessionId)) {
|
|
@@ -535,6 +482,11 @@ async function handleStreamingExecuteRequest(
|
|
|
535
482
|
session.activeProcess = child;
|
|
536
483
|
}
|
|
537
484
|
|
|
485
|
+
// For background processes, unref to prevent blocking
|
|
486
|
+
if (background) {
|
|
487
|
+
child.unref();
|
|
488
|
+
}
|
|
489
|
+
|
|
538
490
|
let stdout = "";
|
|
539
491
|
let stderr = "";
|
|
540
492
|
|
|
@@ -546,6 +498,7 @@ async function handleStreamingExecuteRequest(
|
|
|
546
498
|
command,
|
|
547
499
|
timestamp: new Date().toISOString(),
|
|
548
500
|
type: "command_start",
|
|
501
|
+
background: background || false,
|
|
549
502
|
})}\n\n`
|
|
550
503
|
)
|
|
551
504
|
);
|
|
@@ -611,7 +564,11 @@ async function handleStreamingExecuteRequest(
|
|
|
611
564
|
)
|
|
612
565
|
);
|
|
613
566
|
|
|
614
|
-
|
|
567
|
+
// For non-background processes, close the stream
|
|
568
|
+
// For background processes with streaming, the stream stays open
|
|
569
|
+
if (!background) {
|
|
570
|
+
controller.close();
|
|
571
|
+
}
|
|
615
572
|
});
|
|
616
573
|
|
|
617
574
|
child.on("error", (error) => {
|
|
@@ -2501,7 +2458,8 @@ async function handleStreamingMoveFileRequest(
|
|
|
2501
2458
|
function executeCommand(
|
|
2502
2459
|
command: string,
|
|
2503
2460
|
args: string[],
|
|
2504
|
-
sessionId?: string
|
|
2461
|
+
sessionId?: string,
|
|
2462
|
+
background?: boolean
|
|
2505
2463
|
): Promise<{
|
|
2506
2464
|
success: boolean;
|
|
2507
2465
|
stdout: string;
|
|
@@ -2509,10 +2467,13 @@ function executeCommand(
|
|
|
2509
2467
|
exitCode: number;
|
|
2510
2468
|
}> {
|
|
2511
2469
|
return new Promise((resolve, reject) => {
|
|
2512
|
-
const
|
|
2470
|
+
const spawnOptions: SpawnOptions = {
|
|
2513
2471
|
shell: true,
|
|
2514
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
2515
|
-
|
|
2472
|
+
stdio: ["pipe", "pipe", "pipe"] as const,
|
|
2473
|
+
detached: background || false,
|
|
2474
|
+
};
|
|
2475
|
+
|
|
2476
|
+
const child = spawn(command, args, spawnOptions);
|
|
2516
2477
|
|
|
2517
2478
|
// Store the process reference for cleanup if sessionId is provided
|
|
2518
2479
|
if (sessionId && sessions.has(sessionId)) {
|
|
@@ -2531,32 +2492,54 @@ function executeCommand(
|
|
|
2531
2492
|
stderr += data.toString();
|
|
2532
2493
|
});
|
|
2533
2494
|
|
|
2534
|
-
|
|
2535
|
-
//
|
|
2536
|
-
|
|
2537
|
-
const session = sessions.get(sessionId)!;
|
|
2538
|
-
session.activeProcess = null;
|
|
2539
|
-
}
|
|
2495
|
+
if (background) {
|
|
2496
|
+
// For background processes, unref and return quickly
|
|
2497
|
+
child.unref();
|
|
2540
2498
|
|
|
2541
|
-
|
|
2499
|
+
// Collect initial output for 100ms then return
|
|
2500
|
+
setTimeout(() => {
|
|
2501
|
+
resolve({
|
|
2502
|
+
exitCode: 0, // Process is still running
|
|
2503
|
+
stderr,
|
|
2504
|
+
stdout,
|
|
2505
|
+
success: true,
|
|
2506
|
+
});
|
|
2507
|
+
}, 100);
|
|
2542
2508
|
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
success: code === 0,
|
|
2509
|
+
// Still handle errors
|
|
2510
|
+
child.on("error", (error) => {
|
|
2511
|
+
console.error(`[Server] Background process error: ${command}`, error);
|
|
2512
|
+
// Don't reject since we might have already resolved
|
|
2548
2513
|
});
|
|
2549
|
-
}
|
|
2514
|
+
} else {
|
|
2515
|
+
// Normal synchronous execution
|
|
2516
|
+
child.on("close", (code) => {
|
|
2517
|
+
// Clear the active process reference
|
|
2518
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
2519
|
+
const session = sessions.get(sessionId)!;
|
|
2520
|
+
session.activeProcess = null;
|
|
2521
|
+
}
|
|
2550
2522
|
|
|
2551
|
-
|
|
2552
|
-
// Clear the active process reference
|
|
2553
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
2554
|
-
const session = sessions.get(sessionId)!;
|
|
2555
|
-
session.activeProcess = null;
|
|
2556
|
-
}
|
|
2523
|
+
console.log(`[Server] Command completed: ${command}, Exit code: ${code}`);
|
|
2557
2524
|
|
|
2558
|
-
|
|
2559
|
-
|
|
2525
|
+
resolve({
|
|
2526
|
+
exitCode: code || 0,
|
|
2527
|
+
stderr,
|
|
2528
|
+
stdout,
|
|
2529
|
+
success: code === 0,
|
|
2530
|
+
});
|
|
2531
|
+
});
|
|
2532
|
+
|
|
2533
|
+
child.on("error", (error) => {
|
|
2534
|
+
// Clear the active process reference
|
|
2535
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
2536
|
+
const session = sessions.get(sessionId)!;
|
|
2537
|
+
session.activeProcess = null;
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
reject(error);
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2560
2543
|
});
|
|
2561
2544
|
}
|
|
2562
2545
|
|
|
@@ -2874,7 +2857,316 @@ function executeMoveFile(
|
|
|
2874
2857
|
});
|
|
2875
2858
|
}
|
|
2876
2859
|
|
|
2877
|
-
|
|
2860
|
+
async function handleExposePortRequest(
|
|
2861
|
+
req: Request,
|
|
2862
|
+
corsHeaders: Record<string, string>
|
|
2863
|
+
): Promise<Response> {
|
|
2864
|
+
try {
|
|
2865
|
+
const body = (await req.json()) as ExposePortRequest;
|
|
2866
|
+
const { port, name } = body;
|
|
2867
|
+
|
|
2868
|
+
if (!port || typeof port !== "number") {
|
|
2869
|
+
return new Response(
|
|
2870
|
+
JSON.stringify({
|
|
2871
|
+
error: "Port is required and must be a number",
|
|
2872
|
+
}),
|
|
2873
|
+
{
|
|
2874
|
+
headers: {
|
|
2875
|
+
"Content-Type": "application/json",
|
|
2876
|
+
...corsHeaders,
|
|
2877
|
+
},
|
|
2878
|
+
status: 400,
|
|
2879
|
+
}
|
|
2880
|
+
);
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
// Validate port range
|
|
2884
|
+
if (port < 1 || port > 65535) {
|
|
2885
|
+
return new Response(
|
|
2886
|
+
JSON.stringify({
|
|
2887
|
+
error: "Port must be between 1 and 65535",
|
|
2888
|
+
}),
|
|
2889
|
+
{
|
|
2890
|
+
headers: {
|
|
2891
|
+
"Content-Type": "application/json",
|
|
2892
|
+
...corsHeaders,
|
|
2893
|
+
},
|
|
2894
|
+
status: 400,
|
|
2895
|
+
}
|
|
2896
|
+
);
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
// Store the exposed port
|
|
2900
|
+
exposedPorts.set(port, { name, exposedAt: new Date() });
|
|
2901
|
+
|
|
2902
|
+
console.log(`[Server] Exposed port: ${port}${name ? ` (${name})` : ""}`);
|
|
2903
|
+
|
|
2904
|
+
return new Response(
|
|
2905
|
+
JSON.stringify({
|
|
2906
|
+
port,
|
|
2907
|
+
name,
|
|
2908
|
+
exposedAt: new Date().toISOString(),
|
|
2909
|
+
success: true,
|
|
2910
|
+
timestamp: new Date().toISOString(),
|
|
2911
|
+
}),
|
|
2912
|
+
{
|
|
2913
|
+
headers: {
|
|
2914
|
+
"Content-Type": "application/json",
|
|
2915
|
+
...corsHeaders,
|
|
2916
|
+
},
|
|
2917
|
+
}
|
|
2918
|
+
);
|
|
2919
|
+
} catch (error) {
|
|
2920
|
+
console.error("[Server] Error in handleExposePortRequest:", error);
|
|
2921
|
+
return new Response(
|
|
2922
|
+
JSON.stringify({
|
|
2923
|
+
error: "Failed to expose port",
|
|
2924
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
2925
|
+
}),
|
|
2926
|
+
{
|
|
2927
|
+
headers: {
|
|
2928
|
+
"Content-Type": "application/json",
|
|
2929
|
+
...corsHeaders,
|
|
2930
|
+
},
|
|
2931
|
+
status: 500,
|
|
2932
|
+
}
|
|
2933
|
+
);
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
async function handleUnexposePortRequest(
|
|
2938
|
+
req: Request,
|
|
2939
|
+
corsHeaders: Record<string, string>
|
|
2940
|
+
): Promise<Response> {
|
|
2941
|
+
try {
|
|
2942
|
+
const body = (await req.json()) as UnexposePortRequest;
|
|
2943
|
+
const { port } = body;
|
|
2944
|
+
|
|
2945
|
+
if (!port || typeof port !== "number") {
|
|
2946
|
+
return new Response(
|
|
2947
|
+
JSON.stringify({
|
|
2948
|
+
error: "Port is required and must be a number",
|
|
2949
|
+
}),
|
|
2950
|
+
{
|
|
2951
|
+
headers: {
|
|
2952
|
+
"Content-Type": "application/json",
|
|
2953
|
+
...corsHeaders,
|
|
2954
|
+
},
|
|
2955
|
+
status: 400,
|
|
2956
|
+
}
|
|
2957
|
+
);
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
// Check if port is exposed
|
|
2961
|
+
if (!exposedPorts.has(port)) {
|
|
2962
|
+
return new Response(
|
|
2963
|
+
JSON.stringify({
|
|
2964
|
+
error: "Port is not exposed",
|
|
2965
|
+
}),
|
|
2966
|
+
{
|
|
2967
|
+
headers: {
|
|
2968
|
+
"Content-Type": "application/json",
|
|
2969
|
+
...corsHeaders,
|
|
2970
|
+
},
|
|
2971
|
+
status: 404,
|
|
2972
|
+
}
|
|
2973
|
+
);
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
// Remove the exposed port
|
|
2977
|
+
exposedPorts.delete(port);
|
|
2978
|
+
|
|
2979
|
+
console.log(`[Server] Unexposed port: ${port}`);
|
|
2980
|
+
|
|
2981
|
+
return new Response(
|
|
2982
|
+
JSON.stringify({
|
|
2983
|
+
port,
|
|
2984
|
+
success: true,
|
|
2985
|
+
timestamp: new Date().toISOString(),
|
|
2986
|
+
}),
|
|
2987
|
+
{
|
|
2988
|
+
headers: {
|
|
2989
|
+
"Content-Type": "application/json",
|
|
2990
|
+
...corsHeaders,
|
|
2991
|
+
},
|
|
2992
|
+
}
|
|
2993
|
+
);
|
|
2994
|
+
} catch (error) {
|
|
2995
|
+
console.error("[Server] Error in handleUnexposePortRequest:", error);
|
|
2996
|
+
return new Response(
|
|
2997
|
+
JSON.stringify({
|
|
2998
|
+
error: "Failed to unexpose port",
|
|
2999
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
3000
|
+
}),
|
|
3001
|
+
{
|
|
3002
|
+
headers: {
|
|
3003
|
+
"Content-Type": "application/json",
|
|
3004
|
+
...corsHeaders,
|
|
3005
|
+
},
|
|
3006
|
+
status: 500,
|
|
3007
|
+
}
|
|
3008
|
+
);
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
async function handleGetExposedPortsRequest(
|
|
3013
|
+
req: Request,
|
|
3014
|
+
corsHeaders: Record<string, string>
|
|
3015
|
+
): Promise<Response> {
|
|
3016
|
+
try {
|
|
3017
|
+
const ports = Array.from(exposedPorts.entries()).map(([port, info]) => ({
|
|
3018
|
+
port,
|
|
3019
|
+
name: info.name,
|
|
3020
|
+
exposedAt: info.exposedAt.toISOString(),
|
|
3021
|
+
}));
|
|
3022
|
+
|
|
3023
|
+
return new Response(
|
|
3024
|
+
JSON.stringify({
|
|
3025
|
+
ports,
|
|
3026
|
+
count: ports.length,
|
|
3027
|
+
timestamp: new Date().toISOString(),
|
|
3028
|
+
}),
|
|
3029
|
+
{
|
|
3030
|
+
headers: {
|
|
3031
|
+
"Content-Type": "application/json",
|
|
3032
|
+
...corsHeaders,
|
|
3033
|
+
},
|
|
3034
|
+
}
|
|
3035
|
+
);
|
|
3036
|
+
} catch (error) {
|
|
3037
|
+
console.error("[Server] Error in handleGetExposedPortsRequest:", error);
|
|
3038
|
+
return new Response(
|
|
3039
|
+
JSON.stringify({
|
|
3040
|
+
error: "Failed to get exposed ports",
|
|
3041
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
3042
|
+
}),
|
|
3043
|
+
{
|
|
3044
|
+
headers: {
|
|
3045
|
+
"Content-Type": "application/json",
|
|
3046
|
+
...corsHeaders,
|
|
3047
|
+
},
|
|
3048
|
+
status: 500,
|
|
3049
|
+
}
|
|
3050
|
+
);
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
async function handleProxyRequest(
|
|
3055
|
+
req: Request,
|
|
3056
|
+
corsHeaders: Record<string, string>
|
|
3057
|
+
): Promise<Response> {
|
|
3058
|
+
try {
|
|
3059
|
+
const url = new URL(req.url);
|
|
3060
|
+
const pathParts = url.pathname.split("/");
|
|
3061
|
+
|
|
3062
|
+
// Extract port from path like /proxy/3000/...
|
|
3063
|
+
if (pathParts.length < 3) {
|
|
3064
|
+
return new Response(
|
|
3065
|
+
JSON.stringify({
|
|
3066
|
+
error: "Invalid proxy path",
|
|
3067
|
+
}),
|
|
3068
|
+
{
|
|
3069
|
+
headers: {
|
|
3070
|
+
"Content-Type": "application/json",
|
|
3071
|
+
...corsHeaders,
|
|
3072
|
+
},
|
|
3073
|
+
status: 400,
|
|
3074
|
+
}
|
|
3075
|
+
);
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
const port = parseInt(pathParts[2]);
|
|
3079
|
+
if (!port || Number.isNaN(port)) {
|
|
3080
|
+
return new Response(
|
|
3081
|
+
JSON.stringify({
|
|
3082
|
+
error: "Invalid port in proxy path",
|
|
3083
|
+
}),
|
|
3084
|
+
{
|
|
3085
|
+
headers: {
|
|
3086
|
+
"Content-Type": "application/json",
|
|
3087
|
+
...corsHeaders,
|
|
3088
|
+
},
|
|
3089
|
+
status: 400,
|
|
3090
|
+
}
|
|
3091
|
+
);
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
// Check if port is exposed
|
|
3095
|
+
if (!exposedPorts.has(port)) {
|
|
3096
|
+
return new Response(
|
|
3097
|
+
JSON.stringify({
|
|
3098
|
+
error: `Port ${port} is not exposed`,
|
|
3099
|
+
}),
|
|
3100
|
+
{
|
|
3101
|
+
headers: {
|
|
3102
|
+
"Content-Type": "application/json",
|
|
3103
|
+
...corsHeaders,
|
|
3104
|
+
},
|
|
3105
|
+
status: 404,
|
|
3106
|
+
}
|
|
3107
|
+
);
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
// Construct the target URL
|
|
3111
|
+
const targetPath = `/${pathParts.slice(3).join("/")}`;
|
|
3112
|
+
// Use 127.0.0.1 instead of localhost for more reliable container networking
|
|
3113
|
+
const targetUrl = `http://127.0.0.1:${port}${targetPath}${url.search}`;
|
|
3114
|
+
|
|
3115
|
+
console.log(`[Server] Proxying request to: ${targetUrl}`);
|
|
3116
|
+
console.log(`[Server] Method: ${req.method}, Port: ${port}, Path: ${targetPath}`);
|
|
3117
|
+
|
|
3118
|
+
try {
|
|
3119
|
+
// Forward the request to the target port
|
|
3120
|
+
const targetResponse = await fetch(targetUrl, {
|
|
3121
|
+
method: req.method,
|
|
3122
|
+
headers: req.headers,
|
|
3123
|
+
body: req.body,
|
|
3124
|
+
});
|
|
3125
|
+
|
|
3126
|
+
// Return the response from the target
|
|
3127
|
+
return new Response(targetResponse.body, {
|
|
3128
|
+
status: targetResponse.status,
|
|
3129
|
+
statusText: targetResponse.statusText,
|
|
3130
|
+
headers: {
|
|
3131
|
+
...Object.fromEntries(targetResponse.headers.entries()),
|
|
3132
|
+
...corsHeaders,
|
|
3133
|
+
},
|
|
3134
|
+
});
|
|
3135
|
+
} catch (fetchError) {
|
|
3136
|
+
console.error(`[Server] Error proxying to port ${port}:`, fetchError);
|
|
3137
|
+
return new Response(
|
|
3138
|
+
JSON.stringify({
|
|
3139
|
+
error: `Service on port ${port} is not responding`,
|
|
3140
|
+
message: fetchError instanceof Error ? fetchError.message : "Unknown error",
|
|
3141
|
+
}),
|
|
3142
|
+
{
|
|
3143
|
+
headers: {
|
|
3144
|
+
"Content-Type": "application/json",
|
|
3145
|
+
...corsHeaders,
|
|
3146
|
+
},
|
|
3147
|
+
status: 502,
|
|
3148
|
+
}
|
|
3149
|
+
);
|
|
3150
|
+
}
|
|
3151
|
+
} catch (error) {
|
|
3152
|
+
console.error("[Server] Error in handleProxyRequest:", error);
|
|
3153
|
+
return new Response(
|
|
3154
|
+
JSON.stringify({
|
|
3155
|
+
error: "Failed to proxy request",
|
|
3156
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
3157
|
+
}),
|
|
3158
|
+
{
|
|
3159
|
+
headers: {
|
|
3160
|
+
"Content-Type": "application/json",
|
|
3161
|
+
...corsHeaders,
|
|
3162
|
+
},
|
|
3163
|
+
status: 500,
|
|
3164
|
+
}
|
|
3165
|
+
);
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
console.log(`🚀 Bun server running on http://0.0.0.0:${server.port}`);
|
|
2878
3170
|
console.log(`📡 HTTP API endpoints available:`);
|
|
2879
3171
|
console.log(` POST /api/session/create - Create a new session`);
|
|
2880
3172
|
console.log(` GET /api/session/list - List all sessions`);
|
|
@@ -2896,5 +3188,9 @@ console.log(` POST /api/rename - Rename a file`);
|
|
|
2896
3188
|
console.log(` POST /api/rename/stream - Rename a file (streaming)`);
|
|
2897
3189
|
console.log(` POST /api/move - Move a file`);
|
|
2898
3190
|
console.log(` POST /api/move/stream - Move a file (streaming)`);
|
|
3191
|
+
console.log(` POST /api/expose-port - Expose a port for external access`);
|
|
3192
|
+
console.log(` DELETE /api/unexpose-port - Unexpose a port`);
|
|
3193
|
+
console.log(` GET /api/exposed-ports - List exposed ports`);
|
|
3194
|
+
console.log(` GET /proxy/{port}/* - Proxy requests to exposed ports`);
|
|
2899
3195
|
console.log(` GET /api/ping - Health check`);
|
|
2900
3196
|
console.log(` GET /api/commands - List available commands`);
|