@cloudflare/sandbox 0.0.0-60af265 → 0.0.0-6704961
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 +12 -0
- package/Dockerfile +86 -9
- package/container_src/index.ts +428 -138
- package/package.json +2 -2
- package/src/client.ts +163 -34
- package/src/index.ts +14 -136
- 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)}`;
|
|
@@ -111,51 +125,6 @@ const server = serve({
|
|
|
111
125
|
},
|
|
112
126
|
});
|
|
113
127
|
|
|
114
|
-
case "/api/hello":
|
|
115
|
-
return new Response(
|
|
116
|
-
JSON.stringify({
|
|
117
|
-
message: "Hello from API!",
|
|
118
|
-
timestamp: new Date().toISOString(),
|
|
119
|
-
}),
|
|
120
|
-
{
|
|
121
|
-
headers: {
|
|
122
|
-
"Content-Type": "application/json",
|
|
123
|
-
...corsHeaders,
|
|
124
|
-
},
|
|
125
|
-
}
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
case "/api/users":
|
|
129
|
-
if (req.method === "GET") {
|
|
130
|
-
return new Response(
|
|
131
|
-
JSON.stringify([
|
|
132
|
-
{ id: 1, name: "Alice" },
|
|
133
|
-
{ id: 2, name: "Bob" },
|
|
134
|
-
{ id: 3, name: "Charlie" },
|
|
135
|
-
]),
|
|
136
|
-
{
|
|
137
|
-
headers: {
|
|
138
|
-
"Content-Type": "application/json",
|
|
139
|
-
...corsHeaders,
|
|
140
|
-
},
|
|
141
|
-
}
|
|
142
|
-
);
|
|
143
|
-
} else if (req.method === "POST") {
|
|
144
|
-
return new Response(
|
|
145
|
-
JSON.stringify({
|
|
146
|
-
message: "User created successfully",
|
|
147
|
-
method: "POST",
|
|
148
|
-
}),
|
|
149
|
-
{
|
|
150
|
-
headers: {
|
|
151
|
-
"Content-Type": "application/json",
|
|
152
|
-
...corsHeaders,
|
|
153
|
-
},
|
|
154
|
-
}
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
break;
|
|
158
|
-
|
|
159
128
|
case "/api/session/create":
|
|
160
129
|
if (req.method === "POST") {
|
|
161
130
|
const sessionId = generateSessionId();
|
|
@@ -355,7 +324,30 @@ const server = serve({
|
|
|
355
324
|
}
|
|
356
325
|
break;
|
|
357
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
|
+
|
|
358
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
|
+
|
|
359
351
|
console.log(`[Container] Route not found: ${pathname}`);
|
|
360
352
|
return new Response("Not Found", {
|
|
361
353
|
headers: corsHeaders,
|
|
@@ -381,15 +373,17 @@ const server = serve({
|
|
|
381
373
|
},
|
|
382
374
|
hostname: "0.0.0.0",
|
|
383
375
|
port: 3000,
|
|
384
|
-
|
|
376
|
+
// We don't need this, but typescript complains
|
|
377
|
+
websocket: { async message() { } },
|
|
378
|
+
});
|
|
385
379
|
|
|
386
380
|
async function handleExecuteRequest(
|
|
387
381
|
req: Request,
|
|
388
382
|
corsHeaders: Record<string, string>
|
|
389
383
|
): Promise<Response> {
|
|
390
384
|
try {
|
|
391
|
-
const body = (await req.json()) as ExecuteRequest
|
|
392
|
-
const { command, args = [], sessionId } = body;
|
|
385
|
+
const body = (await req.json()) as ExecuteRequest;
|
|
386
|
+
const { command, args = [], sessionId, background } = body;
|
|
393
387
|
|
|
394
388
|
if (!command || typeof command !== "string") {
|
|
395
389
|
return new Response(
|
|
@@ -406,37 +400,9 @@ async function handleExecuteRequest(
|
|
|
406
400
|
);
|
|
407
401
|
}
|
|
408
402
|
|
|
409
|
-
// Basic safety check - prevent dangerous commands
|
|
410
|
-
const dangerousCommands = [
|
|
411
|
-
"rm",
|
|
412
|
-
"rmdir",
|
|
413
|
-
"del",
|
|
414
|
-
"format",
|
|
415
|
-
"shutdown",
|
|
416
|
-
"reboot",
|
|
417
|
-
];
|
|
418
|
-
const lowerCommand = command.toLowerCase();
|
|
419
|
-
|
|
420
|
-
if (
|
|
421
|
-
dangerousCommands.some((dangerous) => lowerCommand.includes(dangerous))
|
|
422
|
-
) {
|
|
423
|
-
return new Response(
|
|
424
|
-
JSON.stringify({
|
|
425
|
-
error: "Dangerous command not allowed",
|
|
426
|
-
}),
|
|
427
|
-
{
|
|
428
|
-
headers: {
|
|
429
|
-
"Content-Type": "application/json",
|
|
430
|
-
...corsHeaders,
|
|
431
|
-
},
|
|
432
|
-
status: 400,
|
|
433
|
-
}
|
|
434
|
-
);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
403
|
console.log(`[Server] Executing command: ${command} ${args.join(" ")}`);
|
|
438
404
|
|
|
439
|
-
const result = await executeCommand(command, args, sessionId);
|
|
405
|
+
const result = await executeCommand(command, args, sessionId, background);
|
|
440
406
|
|
|
441
407
|
return new Response(
|
|
442
408
|
JSON.stringify({
|
|
@@ -478,8 +444,8 @@ async function handleStreamingExecuteRequest(
|
|
|
478
444
|
corsHeaders: Record<string, string>
|
|
479
445
|
): Promise<Response> {
|
|
480
446
|
try {
|
|
481
|
-
const body = (await req.json()) as ExecuteRequest
|
|
482
|
-
const { command, args = [], sessionId } = body;
|
|
447
|
+
const body = (await req.json()) as ExecuteRequest;
|
|
448
|
+
const { command, args = [], sessionId, background } = body;
|
|
483
449
|
|
|
484
450
|
if (!command || typeof command !== "string") {
|
|
485
451
|
return new Response(
|
|
@@ -496,44 +462,19 @@ async function handleStreamingExecuteRequest(
|
|
|
496
462
|
);
|
|
497
463
|
}
|
|
498
464
|
|
|
499
|
-
// Basic safety check - prevent dangerous commands
|
|
500
|
-
const dangerousCommands = [
|
|
501
|
-
"rm",
|
|
502
|
-
"rmdir",
|
|
503
|
-
"del",
|
|
504
|
-
"format",
|
|
505
|
-
"shutdown",
|
|
506
|
-
"reboot",
|
|
507
|
-
];
|
|
508
|
-
const lowerCommand = command.toLowerCase();
|
|
509
|
-
|
|
510
|
-
if (
|
|
511
|
-
dangerousCommands.some((dangerous) => lowerCommand.includes(dangerous))
|
|
512
|
-
) {
|
|
513
|
-
return new Response(
|
|
514
|
-
JSON.stringify({
|
|
515
|
-
error: "Dangerous command not allowed",
|
|
516
|
-
}),
|
|
517
|
-
{
|
|
518
|
-
headers: {
|
|
519
|
-
"Content-Type": "application/json",
|
|
520
|
-
...corsHeaders,
|
|
521
|
-
},
|
|
522
|
-
status: 400,
|
|
523
|
-
}
|
|
524
|
-
);
|
|
525
|
-
}
|
|
526
|
-
|
|
527
465
|
console.log(
|
|
528
466
|
`[Server] Executing streaming command: ${command} ${args.join(" ")}`
|
|
529
467
|
);
|
|
530
468
|
|
|
531
469
|
const stream = new ReadableStream({
|
|
532
470
|
start(controller) {
|
|
533
|
-
const
|
|
471
|
+
const spawnOptions: SpawnOptions = {
|
|
534
472
|
shell: true,
|
|
535
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
536
|
-
|
|
473
|
+
stdio: ["pipe", "pipe", "pipe"] as const,
|
|
474
|
+
detached: background || false,
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const child = spawn(command, args, spawnOptions);
|
|
537
478
|
|
|
538
479
|
// Store the process reference for cleanup if sessionId is provided
|
|
539
480
|
if (sessionId && sessions.has(sessionId)) {
|
|
@@ -541,6 +482,11 @@ async function handleStreamingExecuteRequest(
|
|
|
541
482
|
session.activeProcess = child;
|
|
542
483
|
}
|
|
543
484
|
|
|
485
|
+
// For background processes, unref to prevent blocking
|
|
486
|
+
if (background) {
|
|
487
|
+
child.unref();
|
|
488
|
+
}
|
|
489
|
+
|
|
544
490
|
let stdout = "";
|
|
545
491
|
let stderr = "";
|
|
546
492
|
|
|
@@ -552,6 +498,7 @@ async function handleStreamingExecuteRequest(
|
|
|
552
498
|
command,
|
|
553
499
|
timestamp: new Date().toISOString(),
|
|
554
500
|
type: "command_start",
|
|
501
|
+
background: background || false,
|
|
555
502
|
})}\n\n`
|
|
556
503
|
)
|
|
557
504
|
);
|
|
@@ -617,7 +564,11 @@ async function handleStreamingExecuteRequest(
|
|
|
617
564
|
)
|
|
618
565
|
);
|
|
619
566
|
|
|
620
|
-
|
|
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
|
+
}
|
|
621
572
|
});
|
|
622
573
|
|
|
623
574
|
child.on("error", (error) => {
|
|
@@ -2507,7 +2458,8 @@ async function handleStreamingMoveFileRequest(
|
|
|
2507
2458
|
function executeCommand(
|
|
2508
2459
|
command: string,
|
|
2509
2460
|
args: string[],
|
|
2510
|
-
sessionId?: string
|
|
2461
|
+
sessionId?: string,
|
|
2462
|
+
background?: boolean
|
|
2511
2463
|
): Promise<{
|
|
2512
2464
|
success: boolean;
|
|
2513
2465
|
stdout: string;
|
|
@@ -2515,10 +2467,13 @@ function executeCommand(
|
|
|
2515
2467
|
exitCode: number;
|
|
2516
2468
|
}> {
|
|
2517
2469
|
return new Promise((resolve, reject) => {
|
|
2518
|
-
const
|
|
2470
|
+
const spawnOptions: SpawnOptions = {
|
|
2519
2471
|
shell: true,
|
|
2520
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
2521
|
-
|
|
2472
|
+
stdio: ["pipe", "pipe", "pipe"] as const,
|
|
2473
|
+
detached: background || false,
|
|
2474
|
+
};
|
|
2475
|
+
|
|
2476
|
+
const child = spawn(command, args, spawnOptions);
|
|
2522
2477
|
|
|
2523
2478
|
// Store the process reference for cleanup if sessionId is provided
|
|
2524
2479
|
if (sessionId && sessions.has(sessionId)) {
|
|
@@ -2537,32 +2492,54 @@ function executeCommand(
|
|
|
2537
2492
|
stderr += data.toString();
|
|
2538
2493
|
});
|
|
2539
2494
|
|
|
2540
|
-
|
|
2541
|
-
//
|
|
2542
|
-
|
|
2543
|
-
const session = sessions.get(sessionId)!;
|
|
2544
|
-
session.activeProcess = null;
|
|
2545
|
-
}
|
|
2495
|
+
if (background) {
|
|
2496
|
+
// For background processes, unref and return quickly
|
|
2497
|
+
child.unref();
|
|
2546
2498
|
|
|
2547
|
-
|
|
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);
|
|
2548
2508
|
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
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
|
|
2554
2513
|
});
|
|
2555
|
-
}
|
|
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
|
+
}
|
|
2556
2522
|
|
|
2557
|
-
|
|
2558
|
-
// Clear the active process reference
|
|
2559
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
2560
|
-
const session = sessions.get(sessionId)!;
|
|
2561
|
-
session.activeProcess = null;
|
|
2562
|
-
}
|
|
2523
|
+
console.log(`[Server] Command completed: ${command}, Exit code: ${code}`);
|
|
2563
2524
|
|
|
2564
|
-
|
|
2565
|
-
|
|
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
|
+
}
|
|
2566
2543
|
});
|
|
2567
2544
|
}
|
|
2568
2545
|
|
|
@@ -2880,6 +2857,315 @@ function executeMoveFile(
|
|
|
2880
2857
|
});
|
|
2881
2858
|
}
|
|
2882
2859
|
|
|
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
|
+
|
|
2883
3169
|
console.log(`🚀 Bun server running on http://0.0.0.0:${server.port}`);
|
|
2884
3170
|
console.log(`📡 HTTP API endpoints available:`);
|
|
2885
3171
|
console.log(` POST /api/session/create - Create a new session`);
|
|
@@ -2902,5 +3188,9 @@ console.log(` POST /api/rename - Rename a file`);
|
|
|
2902
3188
|
console.log(` POST /api/rename/stream - Rename a file (streaming)`);
|
|
2903
3189
|
console.log(` POST /api/move - Move a file`);
|
|
2904
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`);
|
|
2905
3195
|
console.log(` GET /api/ping - Health check`);
|
|
2906
3196
|
console.log(` GET /api/commands - List available commands`);
|