@cloudflare/sandbox 0.0.0-6d1b288 → 0.0.0-6f16c81
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 +57 -0
- package/Dockerfile +86 -9
- package/README.md +3 -0
- package/container_src/index.ts +846 -214
- package/package.json +16 -3
- package/src/client.ts +396 -31
- package/src/index.ts +14 -79
- package/src/request-handler.ts +95 -0
- package/src/sandbox.ts +252 -0
- package/tests/test2.ts +219 -0
package/container_src/index.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { mkdir, rename, unlink, writeFile } from "node:fs/promises";
|
|
1
|
+
import { type ChildProcess, type SpawnOptions, spawn } from "node:child_process";
|
|
2
|
+
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
4
|
import { serve } from "bun";
|
|
5
5
|
|
|
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 {
|
|
@@ -28,6 +30,12 @@ interface WriteFileRequest {
|
|
|
28
30
|
sessionId?: string;
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
interface ReadFileRequest {
|
|
34
|
+
path: string;
|
|
35
|
+
encoding?: string;
|
|
36
|
+
sessionId?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
31
39
|
interface DeleteFileRequest {
|
|
32
40
|
path: string;
|
|
33
41
|
sessionId?: string;
|
|
@@ -45,15 +53,27 @@ interface MoveFileRequest {
|
|
|
45
53
|
sessionId?: string;
|
|
46
54
|
}
|
|
47
55
|
|
|
56
|
+
interface ExposePortRequest {
|
|
57
|
+
port: number;
|
|
58
|
+
name?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface UnexposePortRequest {
|
|
62
|
+
port: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
48
65
|
interface SessionData {
|
|
49
66
|
sessionId: string;
|
|
50
|
-
activeProcess:
|
|
67
|
+
activeProcess: ChildProcess | null;
|
|
51
68
|
createdAt: Date;
|
|
52
69
|
}
|
|
53
70
|
|
|
54
71
|
// In-memory session storage (in production, you'd want to use a proper database)
|
|
55
72
|
const sessions = new Map<string, SessionData>();
|
|
56
73
|
|
|
74
|
+
// In-memory storage for exposed ports
|
|
75
|
+
const exposedPorts = new Map<number, { name?: string; exposedAt: Date }>();
|
|
76
|
+
|
|
57
77
|
// Generate a unique session ID
|
|
58
78
|
function generateSessionId(): string {
|
|
59
79
|
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
@@ -78,6 +98,8 @@ const server = serve({
|
|
|
78
98
|
const url = new URL(req.url);
|
|
79
99
|
const pathname = url.pathname;
|
|
80
100
|
|
|
101
|
+
console.log(`[Container] Incoming ${req.method} request to ${pathname}`);
|
|
102
|
+
|
|
81
103
|
// Handle CORS
|
|
82
104
|
const corsHeaders = {
|
|
83
105
|
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
@@ -87,11 +109,13 @@ const server = serve({
|
|
|
87
109
|
|
|
88
110
|
// Handle preflight requests
|
|
89
111
|
if (req.method === "OPTIONS") {
|
|
112
|
+
console.log(`[Container] Handling CORS preflight for ${pathname}`);
|
|
90
113
|
return new Response(null, { headers: corsHeaders, status: 200 });
|
|
91
114
|
}
|
|
92
115
|
|
|
93
116
|
try {
|
|
94
117
|
// Handle different routes
|
|
118
|
+
console.log(`[Container] Processing ${req.method} ${pathname}`);
|
|
95
119
|
switch (pathname) {
|
|
96
120
|
case "/":
|
|
97
121
|
return new Response("Hello from Bun server! 🚀", {
|
|
@@ -101,51 +125,6 @@ const server = serve({
|
|
|
101
125
|
},
|
|
102
126
|
});
|
|
103
127
|
|
|
104
|
-
case "/api/hello":
|
|
105
|
-
return new Response(
|
|
106
|
-
JSON.stringify({
|
|
107
|
-
message: "Hello from API!",
|
|
108
|
-
timestamp: new Date().toISOString(),
|
|
109
|
-
}),
|
|
110
|
-
{
|
|
111
|
-
headers: {
|
|
112
|
-
"Content-Type": "application/json",
|
|
113
|
-
...corsHeaders,
|
|
114
|
-
},
|
|
115
|
-
}
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
case "/api/users":
|
|
119
|
-
if (req.method === "GET") {
|
|
120
|
-
return new Response(
|
|
121
|
-
JSON.stringify([
|
|
122
|
-
{ id: 1, name: "Alice" },
|
|
123
|
-
{ id: 2, name: "Bob" },
|
|
124
|
-
{ id: 3, name: "Charlie" },
|
|
125
|
-
]),
|
|
126
|
-
{
|
|
127
|
-
headers: {
|
|
128
|
-
"Content-Type": "application/json",
|
|
129
|
-
...corsHeaders,
|
|
130
|
-
},
|
|
131
|
-
}
|
|
132
|
-
);
|
|
133
|
-
} else if (req.method === "POST") {
|
|
134
|
-
return new Response(
|
|
135
|
-
JSON.stringify({
|
|
136
|
-
message: "User created successfully",
|
|
137
|
-
method: "POST",
|
|
138
|
-
}),
|
|
139
|
-
{
|
|
140
|
-
headers: {
|
|
141
|
-
"Content-Type": "application/json",
|
|
142
|
-
...corsHeaders,
|
|
143
|
-
},
|
|
144
|
-
}
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
break;
|
|
148
|
-
|
|
149
128
|
case "/api/session/create":
|
|
150
129
|
if (req.method === "POST") {
|
|
151
130
|
const sessionId = generateSessionId();
|
|
@@ -297,6 +276,18 @@ const server = serve({
|
|
|
297
276
|
}
|
|
298
277
|
break;
|
|
299
278
|
|
|
279
|
+
case "/api/read":
|
|
280
|
+
if (req.method === "POST") {
|
|
281
|
+
return handleReadFileRequest(req, corsHeaders);
|
|
282
|
+
}
|
|
283
|
+
break;
|
|
284
|
+
|
|
285
|
+
case "/api/read/stream":
|
|
286
|
+
if (req.method === "POST") {
|
|
287
|
+
return handleStreamingReadFileRequest(req, corsHeaders);
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
|
|
300
291
|
case "/api/delete":
|
|
301
292
|
if (req.method === "POST") {
|
|
302
293
|
return handleDeleteFileRequest(req, corsHeaders);
|
|
@@ -333,14 +324,38 @@ const server = serve({
|
|
|
333
324
|
}
|
|
334
325
|
break;
|
|
335
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
|
+
|
|
336
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}`);
|
|
337
352
|
return new Response("Not Found", {
|
|
338
353
|
headers: corsHeaders,
|
|
339
354
|
status: 404,
|
|
340
355
|
});
|
|
341
356
|
}
|
|
342
357
|
} catch (error) {
|
|
343
|
-
console.error(
|
|
358
|
+
console.error(`[Container] Error handling ${req.method} ${pathname}:`, error);
|
|
344
359
|
return new Response(
|
|
345
360
|
JSON.stringify({
|
|
346
361
|
error: "Internal server error",
|
|
@@ -356,16 +371,19 @@ const server = serve({
|
|
|
356
371
|
);
|
|
357
372
|
}
|
|
358
373
|
},
|
|
374
|
+
hostname: "0.0.0.0",
|
|
359
375
|
port: 3000,
|
|
360
|
-
|
|
376
|
+
// We don't need this, but typescript complains
|
|
377
|
+
websocket: { async message() { } },
|
|
378
|
+
});
|
|
361
379
|
|
|
362
380
|
async function handleExecuteRequest(
|
|
363
381
|
req: Request,
|
|
364
382
|
corsHeaders: Record<string, string>
|
|
365
383
|
): Promise<Response> {
|
|
366
384
|
try {
|
|
367
|
-
const body = (await req.json()) as ExecuteRequest
|
|
368
|
-
const { command, args = [], sessionId } = body;
|
|
385
|
+
const body = (await req.json()) as ExecuteRequest;
|
|
386
|
+
const { command, args = [], sessionId, background } = body;
|
|
369
387
|
|
|
370
388
|
if (!command || typeof command !== "string") {
|
|
371
389
|
return new Response(
|
|
@@ -412,7 +430,7 @@ async function handleExecuteRequest(
|
|
|
412
430
|
|
|
413
431
|
console.log(`[Server] Executing command: ${command} ${args.join(" ")}`);
|
|
414
432
|
|
|
415
|
-
const result = await executeCommand(command, args, sessionId);
|
|
433
|
+
const result = await executeCommand(command, args, sessionId, background);
|
|
416
434
|
|
|
417
435
|
return new Response(
|
|
418
436
|
JSON.stringify({
|
|
@@ -454,8 +472,8 @@ async function handleStreamingExecuteRequest(
|
|
|
454
472
|
corsHeaders: Record<string, string>
|
|
455
473
|
): Promise<Response> {
|
|
456
474
|
try {
|
|
457
|
-
const body = (await req.json()) as ExecuteRequest
|
|
458
|
-
const { command, args = [], sessionId } = body;
|
|
475
|
+
const body = (await req.json()) as ExecuteRequest;
|
|
476
|
+
const { command, args = [], sessionId, background } = body;
|
|
459
477
|
|
|
460
478
|
if (!command || typeof command !== "string") {
|
|
461
479
|
return new Response(
|
|
@@ -506,10 +524,13 @@ async function handleStreamingExecuteRequest(
|
|
|
506
524
|
|
|
507
525
|
const stream = new ReadableStream({
|
|
508
526
|
start(controller) {
|
|
509
|
-
const
|
|
527
|
+
const spawnOptions: SpawnOptions = {
|
|
510
528
|
shell: true,
|
|
511
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
512
|
-
|
|
529
|
+
stdio: ["pipe", "pipe", "pipe"] as const,
|
|
530
|
+
detached: background || false,
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
const child = spawn(command, args, spawnOptions);
|
|
513
534
|
|
|
514
535
|
// Store the process reference for cleanup if sessionId is provided
|
|
515
536
|
if (sessionId && sessions.has(sessionId)) {
|
|
@@ -517,6 +538,11 @@ async function handleStreamingExecuteRequest(
|
|
|
517
538
|
session.activeProcess = child;
|
|
518
539
|
}
|
|
519
540
|
|
|
541
|
+
// For background processes, unref to prevent blocking
|
|
542
|
+
if (background) {
|
|
543
|
+
child.unref();
|
|
544
|
+
}
|
|
545
|
+
|
|
520
546
|
let stdout = "";
|
|
521
547
|
let stderr = "";
|
|
522
548
|
|
|
@@ -528,6 +554,7 @@ async function handleStreamingExecuteRequest(
|
|
|
528
554
|
command,
|
|
529
555
|
timestamp: new Date().toISOString(),
|
|
530
556
|
type: "command_start",
|
|
557
|
+
background: background || false,
|
|
531
558
|
})}\n\n`
|
|
532
559
|
)
|
|
533
560
|
);
|
|
@@ -593,7 +620,11 @@ async function handleStreamingExecuteRequest(
|
|
|
593
620
|
)
|
|
594
621
|
);
|
|
595
622
|
|
|
596
|
-
|
|
623
|
+
// For non-background processes, close the stream
|
|
624
|
+
// For background processes with streaming, the stream stays open
|
|
625
|
+
if (!background) {
|
|
626
|
+
controller.close();
|
|
627
|
+
}
|
|
597
628
|
});
|
|
598
629
|
|
|
599
630
|
child.on("error", (error) => {
|
|
@@ -1476,13 +1507,13 @@ async function handleStreamingWriteFileRequest(
|
|
|
1476
1507
|
}
|
|
1477
1508
|
}
|
|
1478
1509
|
|
|
1479
|
-
async function
|
|
1510
|
+
async function handleReadFileRequest(
|
|
1480
1511
|
req: Request,
|
|
1481
1512
|
corsHeaders: Record<string, string>
|
|
1482
1513
|
): Promise<Response> {
|
|
1483
1514
|
try {
|
|
1484
|
-
const body = (await req.json()) as
|
|
1485
|
-
const { path, sessionId } = body;
|
|
1515
|
+
const body = (await req.json()) as ReadFileRequest;
|
|
1516
|
+
const { path, encoding = "utf-8", sessionId } = body;
|
|
1486
1517
|
|
|
1487
1518
|
if (!path || typeof path !== "string") {
|
|
1488
1519
|
return new Response(
|
|
@@ -1530,12 +1561,13 @@ async function handleDeleteFileRequest(
|
|
|
1530
1561
|
);
|
|
1531
1562
|
}
|
|
1532
1563
|
|
|
1533
|
-
console.log(`[Server]
|
|
1564
|
+
console.log(`[Server] Reading file: ${path}`);
|
|
1534
1565
|
|
|
1535
|
-
const result = await
|
|
1566
|
+
const result = await executeReadFile(path, encoding, sessionId);
|
|
1536
1567
|
|
|
1537
1568
|
return new Response(
|
|
1538
1569
|
JSON.stringify({
|
|
1570
|
+
content: result.content,
|
|
1539
1571
|
exitCode: result.exitCode,
|
|
1540
1572
|
path,
|
|
1541
1573
|
success: result.success,
|
|
@@ -1549,10 +1581,10 @@ async function handleDeleteFileRequest(
|
|
|
1549
1581
|
}
|
|
1550
1582
|
);
|
|
1551
1583
|
} catch (error) {
|
|
1552
|
-
console.error("[Server] Error in
|
|
1584
|
+
console.error("[Server] Error in handleReadFileRequest:", error);
|
|
1553
1585
|
return new Response(
|
|
1554
1586
|
JSON.stringify({
|
|
1555
|
-
error: "Failed to
|
|
1587
|
+
error: "Failed to read file",
|
|
1556
1588
|
message: error instanceof Error ? error.message : "Unknown error",
|
|
1557
1589
|
}),
|
|
1558
1590
|
{
|
|
@@ -1566,13 +1598,13 @@ async function handleDeleteFileRequest(
|
|
|
1566
1598
|
}
|
|
1567
1599
|
}
|
|
1568
1600
|
|
|
1569
|
-
async function
|
|
1601
|
+
async function handleStreamingReadFileRequest(
|
|
1570
1602
|
req: Request,
|
|
1571
1603
|
corsHeaders: Record<string, string>
|
|
1572
1604
|
): Promise<Response> {
|
|
1573
1605
|
try {
|
|
1574
|
-
const body = (await req.json()) as
|
|
1575
|
-
const { path, sessionId } = body;
|
|
1606
|
+
const body = (await req.json()) as ReadFileRequest;
|
|
1607
|
+
const { path, encoding = "utf-8", sessionId } = body;
|
|
1576
1608
|
|
|
1577
1609
|
if (!path || typeof path !== "string") {
|
|
1578
1610
|
return new Response(
|
|
@@ -1620,7 +1652,7 @@ async function handleStreamingDeleteFileRequest(
|
|
|
1620
1652
|
);
|
|
1621
1653
|
}
|
|
1622
1654
|
|
|
1623
|
-
console.log(`[Server]
|
|
1655
|
+
console.log(`[Server] Reading file (streaming): ${path}`);
|
|
1624
1656
|
|
|
1625
1657
|
const stream = new ReadableStream({
|
|
1626
1658
|
start(controller) {
|
|
@@ -1637,15 +1669,18 @@ async function handleStreamingDeleteFileRequest(
|
|
|
1637
1669
|
)
|
|
1638
1670
|
);
|
|
1639
1671
|
|
|
1640
|
-
//
|
|
1641
|
-
await
|
|
1672
|
+
// Read the file
|
|
1673
|
+
const content = await readFile(path, {
|
|
1674
|
+
encoding: encoding as BufferEncoding,
|
|
1675
|
+
});
|
|
1642
1676
|
|
|
1643
|
-
console.log(`[Server] File
|
|
1677
|
+
console.log(`[Server] File read successfully: ${path}`);
|
|
1644
1678
|
|
|
1645
1679
|
// Send command completion event
|
|
1646
1680
|
controller.enqueue(
|
|
1647
1681
|
new TextEncoder().encode(
|
|
1648
1682
|
`data: ${JSON.stringify({
|
|
1683
|
+
content,
|
|
1649
1684
|
path,
|
|
1650
1685
|
success: true,
|
|
1651
1686
|
timestamp: new Date().toISOString(),
|
|
@@ -1656,7 +1691,7 @@ async function handleStreamingDeleteFileRequest(
|
|
|
1656
1691
|
|
|
1657
1692
|
controller.close();
|
|
1658
1693
|
} catch (error) {
|
|
1659
|
-
console.error(`[Server] Error
|
|
1694
|
+
console.error(`[Server] Error reading file: ${path}`, error);
|
|
1660
1695
|
|
|
1661
1696
|
controller.enqueue(
|
|
1662
1697
|
new TextEncoder().encode(
|
|
@@ -1684,10 +1719,10 @@ async function handleStreamingDeleteFileRequest(
|
|
|
1684
1719
|
},
|
|
1685
1720
|
});
|
|
1686
1721
|
} catch (error) {
|
|
1687
|
-
console.error("[Server] Error in
|
|
1722
|
+
console.error("[Server] Error in handleStreamingReadFileRequest:", error);
|
|
1688
1723
|
return new Response(
|
|
1689
1724
|
JSON.stringify({
|
|
1690
|
-
error: "Failed to
|
|
1725
|
+
error: "Failed to read file",
|
|
1691
1726
|
message: error instanceof Error ? error.message : "Unknown error",
|
|
1692
1727
|
}),
|
|
1693
1728
|
{
|
|
@@ -1701,33 +1736,18 @@ async function handleStreamingDeleteFileRequest(
|
|
|
1701
1736
|
}
|
|
1702
1737
|
}
|
|
1703
1738
|
|
|
1704
|
-
async function
|
|
1739
|
+
async function handleDeleteFileRequest(
|
|
1705
1740
|
req: Request,
|
|
1706
1741
|
corsHeaders: Record<string, string>
|
|
1707
1742
|
): Promise<Response> {
|
|
1708
1743
|
try {
|
|
1709
|
-
const body = (await req.json()) as
|
|
1710
|
-
const {
|
|
1711
|
-
|
|
1712
|
-
if (!oldPath || typeof oldPath !== "string") {
|
|
1713
|
-
return new Response(
|
|
1714
|
-
JSON.stringify({
|
|
1715
|
-
error: "Old path is required and must be a string",
|
|
1716
|
-
}),
|
|
1717
|
-
{
|
|
1718
|
-
headers: {
|
|
1719
|
-
"Content-Type": "application/json",
|
|
1720
|
-
...corsHeaders,
|
|
1721
|
-
},
|
|
1722
|
-
status: 400,
|
|
1723
|
-
}
|
|
1724
|
-
);
|
|
1725
|
-
}
|
|
1744
|
+
const body = (await req.json()) as DeleteFileRequest;
|
|
1745
|
+
const { path, sessionId } = body;
|
|
1726
1746
|
|
|
1727
|
-
if (!
|
|
1747
|
+
if (!path || typeof path !== "string") {
|
|
1728
1748
|
return new Response(
|
|
1729
1749
|
JSON.stringify({
|
|
1730
|
-
error: "
|
|
1750
|
+
error: "Path is required and must be a string",
|
|
1731
1751
|
}),
|
|
1732
1752
|
{
|
|
1733
1753
|
headers: {
|
|
@@ -1755,11 +1775,7 @@ async function handleRenameFileRequest(
|
|
|
1755
1775
|
/\.\./, // Path traversal attempts
|
|
1756
1776
|
];
|
|
1757
1777
|
|
|
1758
|
-
if (
|
|
1759
|
-
dangerousPatterns.some(
|
|
1760
|
-
(pattern) => pattern.test(oldPath) || pattern.test(newPath)
|
|
1761
|
-
)
|
|
1762
|
-
) {
|
|
1778
|
+
if (dangerousPatterns.some((pattern) => pattern.test(path))) {
|
|
1763
1779
|
return new Response(
|
|
1764
1780
|
JSON.stringify({
|
|
1765
1781
|
error: "Dangerous path not allowed",
|
|
@@ -1774,15 +1790,14 @@ async function handleRenameFileRequest(
|
|
|
1774
1790
|
);
|
|
1775
1791
|
}
|
|
1776
1792
|
|
|
1777
|
-
console.log(`[Server]
|
|
1793
|
+
console.log(`[Server] Deleting file: ${path}`);
|
|
1778
1794
|
|
|
1779
|
-
const result = await
|
|
1795
|
+
const result = await executeDeleteFile(path, sessionId);
|
|
1780
1796
|
|
|
1781
1797
|
return new Response(
|
|
1782
1798
|
JSON.stringify({
|
|
1783
1799
|
exitCode: result.exitCode,
|
|
1784
|
-
|
|
1785
|
-
oldPath,
|
|
1800
|
+
path,
|
|
1786
1801
|
success: result.success,
|
|
1787
1802
|
timestamp: new Date().toISOString(),
|
|
1788
1803
|
}),
|
|
@@ -1794,10 +1809,10 @@ async function handleRenameFileRequest(
|
|
|
1794
1809
|
}
|
|
1795
1810
|
);
|
|
1796
1811
|
} catch (error) {
|
|
1797
|
-
console.error("[Server] Error in
|
|
1812
|
+
console.error("[Server] Error in handleDeleteFileRequest:", error);
|
|
1798
1813
|
return new Response(
|
|
1799
1814
|
JSON.stringify({
|
|
1800
|
-
error: "Failed to
|
|
1815
|
+
error: "Failed to delete file",
|
|
1801
1816
|
message: error instanceof Error ? error.message : "Unknown error",
|
|
1802
1817
|
}),
|
|
1803
1818
|
{
|
|
@@ -1811,33 +1826,18 @@ async function handleRenameFileRequest(
|
|
|
1811
1826
|
}
|
|
1812
1827
|
}
|
|
1813
1828
|
|
|
1814
|
-
async function
|
|
1829
|
+
async function handleStreamingDeleteFileRequest(
|
|
1815
1830
|
req: Request,
|
|
1816
1831
|
corsHeaders: Record<string, string>
|
|
1817
1832
|
): Promise<Response> {
|
|
1818
1833
|
try {
|
|
1819
|
-
const body = (await req.json()) as
|
|
1820
|
-
const {
|
|
1821
|
-
|
|
1822
|
-
if (!oldPath || typeof oldPath !== "string") {
|
|
1823
|
-
return new Response(
|
|
1824
|
-
JSON.stringify({
|
|
1825
|
-
error: "Old path is required and must be a string",
|
|
1826
|
-
}),
|
|
1827
|
-
{
|
|
1828
|
-
headers: {
|
|
1829
|
-
"Content-Type": "application/json",
|
|
1830
|
-
...corsHeaders,
|
|
1831
|
-
},
|
|
1832
|
-
status: 400,
|
|
1833
|
-
}
|
|
1834
|
-
);
|
|
1835
|
-
}
|
|
1834
|
+
const body = (await req.json()) as DeleteFileRequest;
|
|
1835
|
+
const { path, sessionId } = body;
|
|
1836
1836
|
|
|
1837
|
-
if (!
|
|
1837
|
+
if (!path || typeof path !== "string") {
|
|
1838
1838
|
return new Response(
|
|
1839
1839
|
JSON.stringify({
|
|
1840
|
-
error: "
|
|
1840
|
+
error: "Path is required and must be a string",
|
|
1841
1841
|
}),
|
|
1842
1842
|
{
|
|
1843
1843
|
headers: {
|
|
@@ -1865,11 +1865,7 @@ async function handleStreamingRenameFileRequest(
|
|
|
1865
1865
|
/\.\./, // Path traversal attempts
|
|
1866
1866
|
];
|
|
1867
1867
|
|
|
1868
|
-
if (
|
|
1869
|
-
dangerousPatterns.some(
|
|
1870
|
-
(pattern) => pattern.test(oldPath) || pattern.test(newPath)
|
|
1871
|
-
)
|
|
1872
|
-
) {
|
|
1868
|
+
if (dangerousPatterns.some((pattern) => pattern.test(path))) {
|
|
1873
1869
|
return new Response(
|
|
1874
1870
|
JSON.stringify({
|
|
1875
1871
|
error: "Dangerous path not allowed",
|
|
@@ -1884,7 +1880,7 @@ async function handleStreamingRenameFileRequest(
|
|
|
1884
1880
|
);
|
|
1885
1881
|
}
|
|
1886
1882
|
|
|
1887
|
-
console.log(`[Server]
|
|
1883
|
+
console.log(`[Server] Deleting file (streaming): ${path}`);
|
|
1888
1884
|
|
|
1889
1885
|
const stream = new ReadableStream({
|
|
1890
1886
|
start(controller) {
|
|
@@ -1894,27 +1890,23 @@ async function handleStreamingRenameFileRequest(
|
|
|
1894
1890
|
controller.enqueue(
|
|
1895
1891
|
new TextEncoder().encode(
|
|
1896
1892
|
`data: ${JSON.stringify({
|
|
1897
|
-
|
|
1898
|
-
oldPath,
|
|
1893
|
+
path,
|
|
1899
1894
|
timestamp: new Date().toISOString(),
|
|
1900
1895
|
type: "command_start",
|
|
1901
1896
|
})}\n\n`
|
|
1902
1897
|
)
|
|
1903
1898
|
);
|
|
1904
1899
|
|
|
1905
|
-
//
|
|
1906
|
-
await
|
|
1900
|
+
// Delete the file
|
|
1901
|
+
await executeDeleteFile(path, sessionId);
|
|
1907
1902
|
|
|
1908
|
-
console.log(
|
|
1909
|
-
`[Server] File renamed successfully: ${oldPath} -> ${newPath}`
|
|
1910
|
-
);
|
|
1903
|
+
console.log(`[Server] File deleted successfully: ${path}`);
|
|
1911
1904
|
|
|
1912
1905
|
// Send command completion event
|
|
1913
1906
|
controller.enqueue(
|
|
1914
1907
|
new TextEncoder().encode(
|
|
1915
1908
|
`data: ${JSON.stringify({
|
|
1916
|
-
|
|
1917
|
-
oldPath,
|
|
1909
|
+
path,
|
|
1918
1910
|
success: true,
|
|
1919
1911
|
timestamp: new Date().toISOString(),
|
|
1920
1912
|
type: "command_complete",
|
|
@@ -1924,18 +1916,14 @@ async function handleStreamingRenameFileRequest(
|
|
|
1924
1916
|
|
|
1925
1917
|
controller.close();
|
|
1926
1918
|
} catch (error) {
|
|
1927
|
-
console.error(
|
|
1928
|
-
`[Server] Error renaming file: ${oldPath} -> ${newPath}`,
|
|
1929
|
-
error
|
|
1930
|
-
);
|
|
1919
|
+
console.error(`[Server] Error deleting file: ${path}`, error);
|
|
1931
1920
|
|
|
1932
1921
|
controller.enqueue(
|
|
1933
1922
|
new TextEncoder().encode(
|
|
1934
1923
|
`data: ${JSON.stringify({
|
|
1935
1924
|
error:
|
|
1936
1925
|
error instanceof Error ? error.message : "Unknown error",
|
|
1937
|
-
|
|
1938
|
-
oldPath,
|
|
1926
|
+
path,
|
|
1939
1927
|
type: "error",
|
|
1940
1928
|
})}\n\n`
|
|
1941
1929
|
)
|
|
@@ -1956,10 +1944,10 @@ async function handleStreamingRenameFileRequest(
|
|
|
1956
1944
|
},
|
|
1957
1945
|
});
|
|
1958
1946
|
} catch (error) {
|
|
1959
|
-
console.error("[Server] Error in
|
|
1947
|
+
console.error("[Server] Error in handleStreamingDeleteFileRequest:", error);
|
|
1960
1948
|
return new Response(
|
|
1961
1949
|
JSON.stringify({
|
|
1962
|
-
error: "Failed to
|
|
1950
|
+
error: "Failed to delete file",
|
|
1963
1951
|
message: error instanceof Error ? error.message : "Unknown error",
|
|
1964
1952
|
}),
|
|
1965
1953
|
{
|
|
@@ -1973,18 +1961,18 @@ async function handleStreamingRenameFileRequest(
|
|
|
1973
1961
|
}
|
|
1974
1962
|
}
|
|
1975
1963
|
|
|
1976
|
-
async function
|
|
1964
|
+
async function handleRenameFileRequest(
|
|
1977
1965
|
req: Request,
|
|
1978
1966
|
corsHeaders: Record<string, string>
|
|
1979
1967
|
): Promise<Response> {
|
|
1980
1968
|
try {
|
|
1981
|
-
const body = (await req.json()) as
|
|
1982
|
-
const {
|
|
1969
|
+
const body = (await req.json()) as RenameFileRequest;
|
|
1970
|
+
const { oldPath, newPath, sessionId } = body;
|
|
1983
1971
|
|
|
1984
|
-
if (!
|
|
1972
|
+
if (!oldPath || typeof oldPath !== "string") {
|
|
1985
1973
|
return new Response(
|
|
1986
1974
|
JSON.stringify({
|
|
1987
|
-
error: "
|
|
1975
|
+
error: "Old path is required and must be a string",
|
|
1988
1976
|
}),
|
|
1989
1977
|
{
|
|
1990
1978
|
headers: {
|
|
@@ -1996,10 +1984,10 @@ async function handleMoveFileRequest(
|
|
|
1996
1984
|
);
|
|
1997
1985
|
}
|
|
1998
1986
|
|
|
1999
|
-
if (!
|
|
1987
|
+
if (!newPath || typeof newPath !== "string") {
|
|
2000
1988
|
return new Response(
|
|
2001
1989
|
JSON.stringify({
|
|
2002
|
-
error: "
|
|
1990
|
+
error: "New path is required and must be a string",
|
|
2003
1991
|
}),
|
|
2004
1992
|
{
|
|
2005
1993
|
headers: {
|
|
@@ -2029,7 +2017,7 @@ async function handleMoveFileRequest(
|
|
|
2029
2017
|
|
|
2030
2018
|
if (
|
|
2031
2019
|
dangerousPatterns.some(
|
|
2032
|
-
(pattern) => pattern.test(
|
|
2020
|
+
(pattern) => pattern.test(oldPath) || pattern.test(newPath)
|
|
2033
2021
|
)
|
|
2034
2022
|
) {
|
|
2035
2023
|
return new Response(
|
|
@@ -2046,19 +2034,15 @@ async function handleMoveFileRequest(
|
|
|
2046
2034
|
);
|
|
2047
2035
|
}
|
|
2048
2036
|
|
|
2049
|
-
console.log(`[Server]
|
|
2037
|
+
console.log(`[Server] Renaming file: ${oldPath} -> ${newPath}`);
|
|
2050
2038
|
|
|
2051
|
-
const result = await
|
|
2052
|
-
sourcePath,
|
|
2053
|
-
destinationPath,
|
|
2054
|
-
sessionId
|
|
2055
|
-
);
|
|
2039
|
+
const result = await executeRenameFile(oldPath, newPath, sessionId);
|
|
2056
2040
|
|
|
2057
2041
|
return new Response(
|
|
2058
2042
|
JSON.stringify({
|
|
2059
|
-
destinationPath,
|
|
2060
2043
|
exitCode: result.exitCode,
|
|
2061
|
-
|
|
2044
|
+
newPath,
|
|
2045
|
+
oldPath,
|
|
2062
2046
|
success: result.success,
|
|
2063
2047
|
timestamp: new Date().toISOString(),
|
|
2064
2048
|
}),
|
|
@@ -2070,10 +2054,10 @@ async function handleMoveFileRequest(
|
|
|
2070
2054
|
}
|
|
2071
2055
|
);
|
|
2072
2056
|
} catch (error) {
|
|
2073
|
-
console.error("[Server] Error in
|
|
2057
|
+
console.error("[Server] Error in handleRenameFileRequest:", error);
|
|
2074
2058
|
return new Response(
|
|
2075
2059
|
JSON.stringify({
|
|
2076
|
-
error: "Failed to
|
|
2060
|
+
error: "Failed to rename file",
|
|
2077
2061
|
message: error instanceof Error ? error.message : "Unknown error",
|
|
2078
2062
|
}),
|
|
2079
2063
|
{
|
|
@@ -2087,18 +2071,18 @@ async function handleMoveFileRequest(
|
|
|
2087
2071
|
}
|
|
2088
2072
|
}
|
|
2089
2073
|
|
|
2090
|
-
async function
|
|
2074
|
+
async function handleStreamingRenameFileRequest(
|
|
2091
2075
|
req: Request,
|
|
2092
2076
|
corsHeaders: Record<string, string>
|
|
2093
2077
|
): Promise<Response> {
|
|
2094
2078
|
try {
|
|
2095
|
-
const body = (await req.json()) as
|
|
2096
|
-
const {
|
|
2079
|
+
const body = (await req.json()) as RenameFileRequest;
|
|
2080
|
+
const { oldPath, newPath, sessionId } = body;
|
|
2097
2081
|
|
|
2098
|
-
if (!
|
|
2082
|
+
if (!oldPath || typeof oldPath !== "string") {
|
|
2099
2083
|
return new Response(
|
|
2100
2084
|
JSON.stringify({
|
|
2101
|
-
error: "
|
|
2085
|
+
error: "Old path is required and must be a string",
|
|
2102
2086
|
}),
|
|
2103
2087
|
{
|
|
2104
2088
|
headers: {
|
|
@@ -2110,10 +2094,10 @@ async function handleStreamingMoveFileRequest(
|
|
|
2110
2094
|
);
|
|
2111
2095
|
}
|
|
2112
2096
|
|
|
2113
|
-
if (!
|
|
2097
|
+
if (!newPath || typeof newPath !== "string") {
|
|
2114
2098
|
return new Response(
|
|
2115
2099
|
JSON.stringify({
|
|
2116
|
-
error: "
|
|
2100
|
+
error: "New path is required and must be a string",
|
|
2117
2101
|
}),
|
|
2118
2102
|
{
|
|
2119
2103
|
headers: {
|
|
@@ -2143,7 +2127,7 @@ async function handleStreamingMoveFileRequest(
|
|
|
2143
2127
|
|
|
2144
2128
|
if (
|
|
2145
2129
|
dangerousPatterns.some(
|
|
2146
|
-
(pattern) => pattern.test(
|
|
2130
|
+
(pattern) => pattern.test(oldPath) || pattern.test(newPath)
|
|
2147
2131
|
)
|
|
2148
2132
|
) {
|
|
2149
2133
|
return new Response(
|
|
@@ -2160,9 +2144,7 @@ async function handleStreamingMoveFileRequest(
|
|
|
2160
2144
|
);
|
|
2161
2145
|
}
|
|
2162
2146
|
|
|
2163
|
-
console.log(
|
|
2164
|
-
`[Server] Moving file (streaming): ${sourcePath} -> ${destinationPath}`
|
|
2165
|
-
);
|
|
2147
|
+
console.log(`[Server] Renaming file (streaming): ${oldPath} -> ${newPath}`);
|
|
2166
2148
|
|
|
2167
2149
|
const stream = new ReadableStream({
|
|
2168
2150
|
start(controller) {
|
|
@@ -2172,27 +2154,27 @@ async function handleStreamingMoveFileRequest(
|
|
|
2172
2154
|
controller.enqueue(
|
|
2173
2155
|
new TextEncoder().encode(
|
|
2174
2156
|
`data: ${JSON.stringify({
|
|
2175
|
-
|
|
2176
|
-
|
|
2157
|
+
newPath,
|
|
2158
|
+
oldPath,
|
|
2177
2159
|
timestamp: new Date().toISOString(),
|
|
2178
2160
|
type: "command_start",
|
|
2179
2161
|
})}\n\n`
|
|
2180
2162
|
)
|
|
2181
2163
|
);
|
|
2182
2164
|
|
|
2183
|
-
//
|
|
2184
|
-
await
|
|
2165
|
+
// Rename the file
|
|
2166
|
+
await executeRenameFile(oldPath, newPath, sessionId);
|
|
2185
2167
|
|
|
2186
2168
|
console.log(
|
|
2187
|
-
`[Server] File
|
|
2169
|
+
`[Server] File renamed successfully: ${oldPath} -> ${newPath}`
|
|
2188
2170
|
);
|
|
2189
2171
|
|
|
2190
2172
|
// Send command completion event
|
|
2191
2173
|
controller.enqueue(
|
|
2192
2174
|
new TextEncoder().encode(
|
|
2193
2175
|
`data: ${JSON.stringify({
|
|
2194
|
-
|
|
2195
|
-
|
|
2176
|
+
newPath,
|
|
2177
|
+
oldPath,
|
|
2196
2178
|
success: true,
|
|
2197
2179
|
timestamp: new Date().toISOString(),
|
|
2198
2180
|
type: "command_complete",
|
|
@@ -2203,14 +2185,292 @@ async function handleStreamingMoveFileRequest(
|
|
|
2203
2185
|
controller.close();
|
|
2204
2186
|
} catch (error) {
|
|
2205
2187
|
console.error(
|
|
2206
|
-
`[Server] Error
|
|
2188
|
+
`[Server] Error renaming file: ${oldPath} -> ${newPath}`,
|
|
2207
2189
|
error
|
|
2208
2190
|
);
|
|
2209
2191
|
|
|
2210
2192
|
controller.enqueue(
|
|
2211
2193
|
new TextEncoder().encode(
|
|
2212
2194
|
`data: ${JSON.stringify({
|
|
2213
|
-
|
|
2195
|
+
error:
|
|
2196
|
+
error instanceof Error ? error.message : "Unknown error",
|
|
2197
|
+
newPath,
|
|
2198
|
+
oldPath,
|
|
2199
|
+
type: "error",
|
|
2200
|
+
})}\n\n`
|
|
2201
|
+
)
|
|
2202
|
+
);
|
|
2203
|
+
|
|
2204
|
+
controller.close();
|
|
2205
|
+
}
|
|
2206
|
+
})();
|
|
2207
|
+
},
|
|
2208
|
+
});
|
|
2209
|
+
|
|
2210
|
+
return new Response(stream, {
|
|
2211
|
+
headers: {
|
|
2212
|
+
"Cache-Control": "no-cache",
|
|
2213
|
+
Connection: "keep-alive",
|
|
2214
|
+
"Content-Type": "text/event-stream",
|
|
2215
|
+
...corsHeaders,
|
|
2216
|
+
},
|
|
2217
|
+
});
|
|
2218
|
+
} catch (error) {
|
|
2219
|
+
console.error("[Server] Error in handleStreamingRenameFileRequest:", error);
|
|
2220
|
+
return new Response(
|
|
2221
|
+
JSON.stringify({
|
|
2222
|
+
error: "Failed to rename file",
|
|
2223
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
2224
|
+
}),
|
|
2225
|
+
{
|
|
2226
|
+
headers: {
|
|
2227
|
+
"Content-Type": "application/json",
|
|
2228
|
+
...corsHeaders,
|
|
2229
|
+
},
|
|
2230
|
+
status: 500,
|
|
2231
|
+
}
|
|
2232
|
+
);
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
async function handleMoveFileRequest(
|
|
2237
|
+
req: Request,
|
|
2238
|
+
corsHeaders: Record<string, string>
|
|
2239
|
+
): Promise<Response> {
|
|
2240
|
+
try {
|
|
2241
|
+
const body = (await req.json()) as MoveFileRequest;
|
|
2242
|
+
const { sourcePath, destinationPath, sessionId } = body;
|
|
2243
|
+
|
|
2244
|
+
if (!sourcePath || typeof sourcePath !== "string") {
|
|
2245
|
+
return new Response(
|
|
2246
|
+
JSON.stringify({
|
|
2247
|
+
error: "Source path is required and must be a string",
|
|
2248
|
+
}),
|
|
2249
|
+
{
|
|
2250
|
+
headers: {
|
|
2251
|
+
"Content-Type": "application/json",
|
|
2252
|
+
...corsHeaders,
|
|
2253
|
+
},
|
|
2254
|
+
status: 400,
|
|
2255
|
+
}
|
|
2256
|
+
);
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
if (!destinationPath || typeof destinationPath !== "string") {
|
|
2260
|
+
return new Response(
|
|
2261
|
+
JSON.stringify({
|
|
2262
|
+
error: "Destination path is required and must be a string",
|
|
2263
|
+
}),
|
|
2264
|
+
{
|
|
2265
|
+
headers: {
|
|
2266
|
+
"Content-Type": "application/json",
|
|
2267
|
+
...corsHeaders,
|
|
2268
|
+
},
|
|
2269
|
+
status: 400,
|
|
2270
|
+
}
|
|
2271
|
+
);
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
// Basic safety check - prevent dangerous paths
|
|
2275
|
+
const dangerousPatterns = [
|
|
2276
|
+
/^\/$/, // Root directory
|
|
2277
|
+
/^\/etc/, // System directories
|
|
2278
|
+
/^\/var/, // System directories
|
|
2279
|
+
/^\/usr/, // System directories
|
|
2280
|
+
/^\/bin/, // System directories
|
|
2281
|
+
/^\/sbin/, // System directories
|
|
2282
|
+
/^\/boot/, // System directories
|
|
2283
|
+
/^\/dev/, // System directories
|
|
2284
|
+
/^\/proc/, // System directories
|
|
2285
|
+
/^\/sys/, // System directories
|
|
2286
|
+
/^\/tmp\/\.\./, // Path traversal attempts
|
|
2287
|
+
/\.\./, // Path traversal attempts
|
|
2288
|
+
];
|
|
2289
|
+
|
|
2290
|
+
if (
|
|
2291
|
+
dangerousPatterns.some(
|
|
2292
|
+
(pattern) => pattern.test(sourcePath) || pattern.test(destinationPath)
|
|
2293
|
+
)
|
|
2294
|
+
) {
|
|
2295
|
+
return new Response(
|
|
2296
|
+
JSON.stringify({
|
|
2297
|
+
error: "Dangerous path not allowed",
|
|
2298
|
+
}),
|
|
2299
|
+
{
|
|
2300
|
+
headers: {
|
|
2301
|
+
"Content-Type": "application/json",
|
|
2302
|
+
...corsHeaders,
|
|
2303
|
+
},
|
|
2304
|
+
status: 400,
|
|
2305
|
+
}
|
|
2306
|
+
);
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
console.log(`[Server] Moving file: ${sourcePath} -> ${destinationPath}`);
|
|
2310
|
+
|
|
2311
|
+
const result = await executeMoveFile(
|
|
2312
|
+
sourcePath,
|
|
2313
|
+
destinationPath,
|
|
2314
|
+
sessionId
|
|
2315
|
+
);
|
|
2316
|
+
|
|
2317
|
+
return new Response(
|
|
2318
|
+
JSON.stringify({
|
|
2319
|
+
destinationPath,
|
|
2320
|
+
exitCode: result.exitCode,
|
|
2321
|
+
sourcePath,
|
|
2322
|
+
success: result.success,
|
|
2323
|
+
timestamp: new Date().toISOString(),
|
|
2324
|
+
}),
|
|
2325
|
+
{
|
|
2326
|
+
headers: {
|
|
2327
|
+
"Content-Type": "application/json",
|
|
2328
|
+
...corsHeaders,
|
|
2329
|
+
},
|
|
2330
|
+
}
|
|
2331
|
+
);
|
|
2332
|
+
} catch (error) {
|
|
2333
|
+
console.error("[Server] Error in handleMoveFileRequest:", error);
|
|
2334
|
+
return new Response(
|
|
2335
|
+
JSON.stringify({
|
|
2336
|
+
error: "Failed to move file",
|
|
2337
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
2338
|
+
}),
|
|
2339
|
+
{
|
|
2340
|
+
headers: {
|
|
2341
|
+
"Content-Type": "application/json",
|
|
2342
|
+
...corsHeaders,
|
|
2343
|
+
},
|
|
2344
|
+
status: 500,
|
|
2345
|
+
}
|
|
2346
|
+
);
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
async function handleStreamingMoveFileRequest(
|
|
2351
|
+
req: Request,
|
|
2352
|
+
corsHeaders: Record<string, string>
|
|
2353
|
+
): Promise<Response> {
|
|
2354
|
+
try {
|
|
2355
|
+
const body = (await req.json()) as MoveFileRequest;
|
|
2356
|
+
const { sourcePath, destinationPath, sessionId } = body;
|
|
2357
|
+
|
|
2358
|
+
if (!sourcePath || typeof sourcePath !== "string") {
|
|
2359
|
+
return new Response(
|
|
2360
|
+
JSON.stringify({
|
|
2361
|
+
error: "Source path is required and must be a string",
|
|
2362
|
+
}),
|
|
2363
|
+
{
|
|
2364
|
+
headers: {
|
|
2365
|
+
"Content-Type": "application/json",
|
|
2366
|
+
...corsHeaders,
|
|
2367
|
+
},
|
|
2368
|
+
status: 400,
|
|
2369
|
+
}
|
|
2370
|
+
);
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
if (!destinationPath || typeof destinationPath !== "string") {
|
|
2374
|
+
return new Response(
|
|
2375
|
+
JSON.stringify({
|
|
2376
|
+
error: "Destination path is required and must be a string",
|
|
2377
|
+
}),
|
|
2378
|
+
{
|
|
2379
|
+
headers: {
|
|
2380
|
+
"Content-Type": "application/json",
|
|
2381
|
+
...corsHeaders,
|
|
2382
|
+
},
|
|
2383
|
+
status: 400,
|
|
2384
|
+
}
|
|
2385
|
+
);
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
// Basic safety check - prevent dangerous paths
|
|
2389
|
+
const dangerousPatterns = [
|
|
2390
|
+
/^\/$/, // Root directory
|
|
2391
|
+
/^\/etc/, // System directories
|
|
2392
|
+
/^\/var/, // System directories
|
|
2393
|
+
/^\/usr/, // System directories
|
|
2394
|
+
/^\/bin/, // System directories
|
|
2395
|
+
/^\/sbin/, // System directories
|
|
2396
|
+
/^\/boot/, // System directories
|
|
2397
|
+
/^\/dev/, // System directories
|
|
2398
|
+
/^\/proc/, // System directories
|
|
2399
|
+
/^\/sys/, // System directories
|
|
2400
|
+
/^\/tmp\/\.\./, // Path traversal attempts
|
|
2401
|
+
/\.\./, // Path traversal attempts
|
|
2402
|
+
];
|
|
2403
|
+
|
|
2404
|
+
if (
|
|
2405
|
+
dangerousPatterns.some(
|
|
2406
|
+
(pattern) => pattern.test(sourcePath) || pattern.test(destinationPath)
|
|
2407
|
+
)
|
|
2408
|
+
) {
|
|
2409
|
+
return new Response(
|
|
2410
|
+
JSON.stringify({
|
|
2411
|
+
error: "Dangerous path not allowed",
|
|
2412
|
+
}),
|
|
2413
|
+
{
|
|
2414
|
+
headers: {
|
|
2415
|
+
"Content-Type": "application/json",
|
|
2416
|
+
...corsHeaders,
|
|
2417
|
+
},
|
|
2418
|
+
status: 400,
|
|
2419
|
+
}
|
|
2420
|
+
);
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
console.log(
|
|
2424
|
+
`[Server] Moving file (streaming): ${sourcePath} -> ${destinationPath}`
|
|
2425
|
+
);
|
|
2426
|
+
|
|
2427
|
+
const stream = new ReadableStream({
|
|
2428
|
+
start(controller) {
|
|
2429
|
+
(async () => {
|
|
2430
|
+
try {
|
|
2431
|
+
// Send command start event
|
|
2432
|
+
controller.enqueue(
|
|
2433
|
+
new TextEncoder().encode(
|
|
2434
|
+
`data: ${JSON.stringify({
|
|
2435
|
+
destinationPath,
|
|
2436
|
+
sourcePath,
|
|
2437
|
+
timestamp: new Date().toISOString(),
|
|
2438
|
+
type: "command_start",
|
|
2439
|
+
})}\n\n`
|
|
2440
|
+
)
|
|
2441
|
+
);
|
|
2442
|
+
|
|
2443
|
+
// Move the file
|
|
2444
|
+
await executeMoveFile(sourcePath, destinationPath, sessionId);
|
|
2445
|
+
|
|
2446
|
+
console.log(
|
|
2447
|
+
`[Server] File moved successfully: ${sourcePath} -> ${destinationPath}`
|
|
2448
|
+
);
|
|
2449
|
+
|
|
2450
|
+
// Send command completion event
|
|
2451
|
+
controller.enqueue(
|
|
2452
|
+
new TextEncoder().encode(
|
|
2453
|
+
`data: ${JSON.stringify({
|
|
2454
|
+
destinationPath,
|
|
2455
|
+
sourcePath,
|
|
2456
|
+
success: true,
|
|
2457
|
+
timestamp: new Date().toISOString(),
|
|
2458
|
+
type: "command_complete",
|
|
2459
|
+
})}\n\n`
|
|
2460
|
+
)
|
|
2461
|
+
);
|
|
2462
|
+
|
|
2463
|
+
controller.close();
|
|
2464
|
+
} catch (error) {
|
|
2465
|
+
console.error(
|
|
2466
|
+
`[Server] Error moving file: ${sourcePath} -> ${destinationPath}`,
|
|
2467
|
+
error
|
|
2468
|
+
);
|
|
2469
|
+
|
|
2470
|
+
controller.enqueue(
|
|
2471
|
+
new TextEncoder().encode(
|
|
2472
|
+
`data: ${JSON.stringify({
|
|
2473
|
+
destinationPath,
|
|
2214
2474
|
error:
|
|
2215
2475
|
error instanceof Error ? error.message : "Unknown error",
|
|
2216
2476
|
sourcePath,
|
|
@@ -2254,7 +2514,8 @@ async function handleStreamingMoveFileRequest(
|
|
|
2254
2514
|
function executeCommand(
|
|
2255
2515
|
command: string,
|
|
2256
2516
|
args: string[],
|
|
2257
|
-
sessionId?: string
|
|
2517
|
+
sessionId?: string,
|
|
2518
|
+
background?: boolean
|
|
2258
2519
|
): Promise<{
|
|
2259
2520
|
success: boolean;
|
|
2260
2521
|
stdout: string;
|
|
@@ -2262,10 +2523,13 @@ function executeCommand(
|
|
|
2262
2523
|
exitCode: number;
|
|
2263
2524
|
}> {
|
|
2264
2525
|
return new Promise((resolve, reject) => {
|
|
2265
|
-
const
|
|
2526
|
+
const spawnOptions: SpawnOptions = {
|
|
2266
2527
|
shell: true,
|
|
2267
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
2268
|
-
|
|
2528
|
+
stdio: ["pipe", "pipe", "pipe"] as const,
|
|
2529
|
+
detached: background || false,
|
|
2530
|
+
};
|
|
2531
|
+
|
|
2532
|
+
const child = spawn(command, args, spawnOptions);
|
|
2269
2533
|
|
|
2270
2534
|
// Store the process reference for cleanup if sessionId is provided
|
|
2271
2535
|
if (sessionId && sessions.has(sessionId)) {
|
|
@@ -2284,32 +2548,54 @@ function executeCommand(
|
|
|
2284
2548
|
stderr += data.toString();
|
|
2285
2549
|
});
|
|
2286
2550
|
|
|
2287
|
-
|
|
2288
|
-
//
|
|
2289
|
-
|
|
2290
|
-
const session = sessions.get(sessionId)!;
|
|
2291
|
-
session.activeProcess = null;
|
|
2292
|
-
}
|
|
2551
|
+
if (background) {
|
|
2552
|
+
// For background processes, unref and return quickly
|
|
2553
|
+
child.unref();
|
|
2293
2554
|
|
|
2294
|
-
|
|
2555
|
+
// Collect initial output for 100ms then return
|
|
2556
|
+
setTimeout(() => {
|
|
2557
|
+
resolve({
|
|
2558
|
+
exitCode: 0, // Process is still running
|
|
2559
|
+
stderr,
|
|
2560
|
+
stdout,
|
|
2561
|
+
success: true,
|
|
2562
|
+
});
|
|
2563
|
+
}, 100);
|
|
2295
2564
|
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
success: code === 0,
|
|
2565
|
+
// Still handle errors
|
|
2566
|
+
child.on("error", (error) => {
|
|
2567
|
+
console.error(`[Server] Background process error: ${command}`, error);
|
|
2568
|
+
// Don't reject since we might have already resolved
|
|
2301
2569
|
});
|
|
2302
|
-
}
|
|
2570
|
+
} else {
|
|
2571
|
+
// Normal synchronous execution
|
|
2572
|
+
child.on("close", (code) => {
|
|
2573
|
+
// Clear the active process reference
|
|
2574
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
2575
|
+
const session = sessions.get(sessionId)!;
|
|
2576
|
+
session.activeProcess = null;
|
|
2577
|
+
}
|
|
2303
2578
|
|
|
2304
|
-
|
|
2305
|
-
// Clear the active process reference
|
|
2306
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
2307
|
-
const session = sessions.get(sessionId)!;
|
|
2308
|
-
session.activeProcess = null;
|
|
2309
|
-
}
|
|
2579
|
+
console.log(`[Server] Command completed: ${command}, Exit code: ${code}`);
|
|
2310
2580
|
|
|
2311
|
-
|
|
2312
|
-
|
|
2581
|
+
resolve({
|
|
2582
|
+
exitCode: code || 0,
|
|
2583
|
+
stderr,
|
|
2584
|
+
stdout,
|
|
2585
|
+
success: code === 0,
|
|
2586
|
+
});
|
|
2587
|
+
});
|
|
2588
|
+
|
|
2589
|
+
child.on("error", (error) => {
|
|
2590
|
+
// Clear the active process reference
|
|
2591
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
2592
|
+
const session = sessions.get(sessionId)!;
|
|
2593
|
+
session.activeProcess = null;
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
reject(error);
|
|
2597
|
+
});
|
|
2598
|
+
}
|
|
2313
2599
|
});
|
|
2314
2600
|
}
|
|
2315
2601
|
|
|
@@ -2506,6 +2792,37 @@ function executeWriteFile(
|
|
|
2506
2792
|
});
|
|
2507
2793
|
}
|
|
2508
2794
|
|
|
2795
|
+
function executeReadFile(
|
|
2796
|
+
path: string,
|
|
2797
|
+
encoding: string,
|
|
2798
|
+
sessionId?: string
|
|
2799
|
+
): Promise<{
|
|
2800
|
+
success: boolean;
|
|
2801
|
+
exitCode: number;
|
|
2802
|
+
content: string;
|
|
2803
|
+
}> {
|
|
2804
|
+
return new Promise((resolve, reject) => {
|
|
2805
|
+
(async () => {
|
|
2806
|
+
try {
|
|
2807
|
+
// Read the file
|
|
2808
|
+
const content = await readFile(path, {
|
|
2809
|
+
encoding: encoding as BufferEncoding,
|
|
2810
|
+
});
|
|
2811
|
+
|
|
2812
|
+
console.log(`[Server] File read successfully: ${path}`);
|
|
2813
|
+
resolve({
|
|
2814
|
+
content,
|
|
2815
|
+
exitCode: 0,
|
|
2816
|
+
success: true,
|
|
2817
|
+
});
|
|
2818
|
+
} catch (error) {
|
|
2819
|
+
console.error(`[Server] Error reading file: ${path}`, error);
|
|
2820
|
+
reject(error);
|
|
2821
|
+
}
|
|
2822
|
+
})();
|
|
2823
|
+
});
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2509
2826
|
function executeDeleteFile(
|
|
2510
2827
|
path: string,
|
|
2511
2828
|
sessionId?: string
|
|
@@ -2596,7 +2913,316 @@ function executeMoveFile(
|
|
|
2596
2913
|
});
|
|
2597
2914
|
}
|
|
2598
2915
|
|
|
2599
|
-
|
|
2916
|
+
async function handleExposePortRequest(
|
|
2917
|
+
req: Request,
|
|
2918
|
+
corsHeaders: Record<string, string>
|
|
2919
|
+
): Promise<Response> {
|
|
2920
|
+
try {
|
|
2921
|
+
const body = (await req.json()) as ExposePortRequest;
|
|
2922
|
+
const { port, name } = body;
|
|
2923
|
+
|
|
2924
|
+
if (!port || typeof port !== "number") {
|
|
2925
|
+
return new Response(
|
|
2926
|
+
JSON.stringify({
|
|
2927
|
+
error: "Port is required and must be a number",
|
|
2928
|
+
}),
|
|
2929
|
+
{
|
|
2930
|
+
headers: {
|
|
2931
|
+
"Content-Type": "application/json",
|
|
2932
|
+
...corsHeaders,
|
|
2933
|
+
},
|
|
2934
|
+
status: 400,
|
|
2935
|
+
}
|
|
2936
|
+
);
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
// Validate port range
|
|
2940
|
+
if (port < 1 || port > 65535) {
|
|
2941
|
+
return new Response(
|
|
2942
|
+
JSON.stringify({
|
|
2943
|
+
error: "Port must be between 1 and 65535",
|
|
2944
|
+
}),
|
|
2945
|
+
{
|
|
2946
|
+
headers: {
|
|
2947
|
+
"Content-Type": "application/json",
|
|
2948
|
+
...corsHeaders,
|
|
2949
|
+
},
|
|
2950
|
+
status: 400,
|
|
2951
|
+
}
|
|
2952
|
+
);
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
// Store the exposed port
|
|
2956
|
+
exposedPorts.set(port, { name, exposedAt: new Date() });
|
|
2957
|
+
|
|
2958
|
+
console.log(`[Server] Exposed port: ${port}${name ? ` (${name})` : ""}`);
|
|
2959
|
+
|
|
2960
|
+
return new Response(
|
|
2961
|
+
JSON.stringify({
|
|
2962
|
+
port,
|
|
2963
|
+
name,
|
|
2964
|
+
exposedAt: new Date().toISOString(),
|
|
2965
|
+
success: true,
|
|
2966
|
+
timestamp: new Date().toISOString(),
|
|
2967
|
+
}),
|
|
2968
|
+
{
|
|
2969
|
+
headers: {
|
|
2970
|
+
"Content-Type": "application/json",
|
|
2971
|
+
...corsHeaders,
|
|
2972
|
+
},
|
|
2973
|
+
}
|
|
2974
|
+
);
|
|
2975
|
+
} catch (error) {
|
|
2976
|
+
console.error("[Server] Error in handleExposePortRequest:", error);
|
|
2977
|
+
return new Response(
|
|
2978
|
+
JSON.stringify({
|
|
2979
|
+
error: "Failed to expose port",
|
|
2980
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
2981
|
+
}),
|
|
2982
|
+
{
|
|
2983
|
+
headers: {
|
|
2984
|
+
"Content-Type": "application/json",
|
|
2985
|
+
...corsHeaders,
|
|
2986
|
+
},
|
|
2987
|
+
status: 500,
|
|
2988
|
+
}
|
|
2989
|
+
);
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
async function handleUnexposePortRequest(
|
|
2994
|
+
req: Request,
|
|
2995
|
+
corsHeaders: Record<string, string>
|
|
2996
|
+
): Promise<Response> {
|
|
2997
|
+
try {
|
|
2998
|
+
const body = (await req.json()) as UnexposePortRequest;
|
|
2999
|
+
const { port } = body;
|
|
3000
|
+
|
|
3001
|
+
if (!port || typeof port !== "number") {
|
|
3002
|
+
return new Response(
|
|
3003
|
+
JSON.stringify({
|
|
3004
|
+
error: "Port is required and must be a number",
|
|
3005
|
+
}),
|
|
3006
|
+
{
|
|
3007
|
+
headers: {
|
|
3008
|
+
"Content-Type": "application/json",
|
|
3009
|
+
...corsHeaders,
|
|
3010
|
+
},
|
|
3011
|
+
status: 400,
|
|
3012
|
+
}
|
|
3013
|
+
);
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
// Check if port is exposed
|
|
3017
|
+
if (!exposedPorts.has(port)) {
|
|
3018
|
+
return new Response(
|
|
3019
|
+
JSON.stringify({
|
|
3020
|
+
error: "Port is not exposed",
|
|
3021
|
+
}),
|
|
3022
|
+
{
|
|
3023
|
+
headers: {
|
|
3024
|
+
"Content-Type": "application/json",
|
|
3025
|
+
...corsHeaders,
|
|
3026
|
+
},
|
|
3027
|
+
status: 404,
|
|
3028
|
+
}
|
|
3029
|
+
);
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
// Remove the exposed port
|
|
3033
|
+
exposedPorts.delete(port);
|
|
3034
|
+
|
|
3035
|
+
console.log(`[Server] Unexposed port: ${port}`);
|
|
3036
|
+
|
|
3037
|
+
return new Response(
|
|
3038
|
+
JSON.stringify({
|
|
3039
|
+
port,
|
|
3040
|
+
success: true,
|
|
3041
|
+
timestamp: new Date().toISOString(),
|
|
3042
|
+
}),
|
|
3043
|
+
{
|
|
3044
|
+
headers: {
|
|
3045
|
+
"Content-Type": "application/json",
|
|
3046
|
+
...corsHeaders,
|
|
3047
|
+
},
|
|
3048
|
+
}
|
|
3049
|
+
);
|
|
3050
|
+
} catch (error) {
|
|
3051
|
+
console.error("[Server] Error in handleUnexposePortRequest:", error);
|
|
3052
|
+
return new Response(
|
|
3053
|
+
JSON.stringify({
|
|
3054
|
+
error: "Failed to unexpose port",
|
|
3055
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
3056
|
+
}),
|
|
3057
|
+
{
|
|
3058
|
+
headers: {
|
|
3059
|
+
"Content-Type": "application/json",
|
|
3060
|
+
...corsHeaders,
|
|
3061
|
+
},
|
|
3062
|
+
status: 500,
|
|
3063
|
+
}
|
|
3064
|
+
);
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
async function handleGetExposedPortsRequest(
|
|
3069
|
+
req: Request,
|
|
3070
|
+
corsHeaders: Record<string, string>
|
|
3071
|
+
): Promise<Response> {
|
|
3072
|
+
try {
|
|
3073
|
+
const ports = Array.from(exposedPorts.entries()).map(([port, info]) => ({
|
|
3074
|
+
port,
|
|
3075
|
+
name: info.name,
|
|
3076
|
+
exposedAt: info.exposedAt.toISOString(),
|
|
3077
|
+
}));
|
|
3078
|
+
|
|
3079
|
+
return new Response(
|
|
3080
|
+
JSON.stringify({
|
|
3081
|
+
ports,
|
|
3082
|
+
count: ports.length,
|
|
3083
|
+
timestamp: new Date().toISOString(),
|
|
3084
|
+
}),
|
|
3085
|
+
{
|
|
3086
|
+
headers: {
|
|
3087
|
+
"Content-Type": "application/json",
|
|
3088
|
+
...corsHeaders,
|
|
3089
|
+
},
|
|
3090
|
+
}
|
|
3091
|
+
);
|
|
3092
|
+
} catch (error) {
|
|
3093
|
+
console.error("[Server] Error in handleGetExposedPortsRequest:", error);
|
|
3094
|
+
return new Response(
|
|
3095
|
+
JSON.stringify({
|
|
3096
|
+
error: "Failed to get exposed ports",
|
|
3097
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
3098
|
+
}),
|
|
3099
|
+
{
|
|
3100
|
+
headers: {
|
|
3101
|
+
"Content-Type": "application/json",
|
|
3102
|
+
...corsHeaders,
|
|
3103
|
+
},
|
|
3104
|
+
status: 500,
|
|
3105
|
+
}
|
|
3106
|
+
);
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
async function handleProxyRequest(
|
|
3111
|
+
req: Request,
|
|
3112
|
+
corsHeaders: Record<string, string>
|
|
3113
|
+
): Promise<Response> {
|
|
3114
|
+
try {
|
|
3115
|
+
const url = new URL(req.url);
|
|
3116
|
+
const pathParts = url.pathname.split("/");
|
|
3117
|
+
|
|
3118
|
+
// Extract port from path like /proxy/3000/...
|
|
3119
|
+
if (pathParts.length < 3) {
|
|
3120
|
+
return new Response(
|
|
3121
|
+
JSON.stringify({
|
|
3122
|
+
error: "Invalid proxy path",
|
|
3123
|
+
}),
|
|
3124
|
+
{
|
|
3125
|
+
headers: {
|
|
3126
|
+
"Content-Type": "application/json",
|
|
3127
|
+
...corsHeaders,
|
|
3128
|
+
},
|
|
3129
|
+
status: 400,
|
|
3130
|
+
}
|
|
3131
|
+
);
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
const port = parseInt(pathParts[2]);
|
|
3135
|
+
if (!port || Number.isNaN(port)) {
|
|
3136
|
+
return new Response(
|
|
3137
|
+
JSON.stringify({
|
|
3138
|
+
error: "Invalid port in proxy path",
|
|
3139
|
+
}),
|
|
3140
|
+
{
|
|
3141
|
+
headers: {
|
|
3142
|
+
"Content-Type": "application/json",
|
|
3143
|
+
...corsHeaders,
|
|
3144
|
+
},
|
|
3145
|
+
status: 400,
|
|
3146
|
+
}
|
|
3147
|
+
);
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
// Check if port is exposed
|
|
3151
|
+
if (!exposedPorts.has(port)) {
|
|
3152
|
+
return new Response(
|
|
3153
|
+
JSON.stringify({
|
|
3154
|
+
error: `Port ${port} is not exposed`,
|
|
3155
|
+
}),
|
|
3156
|
+
{
|
|
3157
|
+
headers: {
|
|
3158
|
+
"Content-Type": "application/json",
|
|
3159
|
+
...corsHeaders,
|
|
3160
|
+
},
|
|
3161
|
+
status: 404,
|
|
3162
|
+
}
|
|
3163
|
+
);
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
// Construct the target URL
|
|
3167
|
+
const targetPath = `/${pathParts.slice(3).join("/")}`;
|
|
3168
|
+
// Use 127.0.0.1 instead of localhost for more reliable container networking
|
|
3169
|
+
const targetUrl = `http://127.0.0.1:${port}${targetPath}${url.search}`;
|
|
3170
|
+
|
|
3171
|
+
console.log(`[Server] Proxying request to: ${targetUrl}`);
|
|
3172
|
+
console.log(`[Server] Method: ${req.method}, Port: ${port}, Path: ${targetPath}`);
|
|
3173
|
+
|
|
3174
|
+
try {
|
|
3175
|
+
// Forward the request to the target port
|
|
3176
|
+
const targetResponse = await fetch(targetUrl, {
|
|
3177
|
+
method: req.method,
|
|
3178
|
+
headers: req.headers,
|
|
3179
|
+
body: req.body,
|
|
3180
|
+
});
|
|
3181
|
+
|
|
3182
|
+
// Return the response from the target
|
|
3183
|
+
return new Response(targetResponse.body, {
|
|
3184
|
+
status: targetResponse.status,
|
|
3185
|
+
statusText: targetResponse.statusText,
|
|
3186
|
+
headers: {
|
|
3187
|
+
...Object.fromEntries(targetResponse.headers.entries()),
|
|
3188
|
+
...corsHeaders,
|
|
3189
|
+
},
|
|
3190
|
+
});
|
|
3191
|
+
} catch (fetchError) {
|
|
3192
|
+
console.error(`[Server] Error proxying to port ${port}:`, fetchError);
|
|
3193
|
+
return new Response(
|
|
3194
|
+
JSON.stringify({
|
|
3195
|
+
error: `Service on port ${port} is not responding`,
|
|
3196
|
+
message: fetchError instanceof Error ? fetchError.message : "Unknown error",
|
|
3197
|
+
}),
|
|
3198
|
+
{
|
|
3199
|
+
headers: {
|
|
3200
|
+
"Content-Type": "application/json",
|
|
3201
|
+
...corsHeaders,
|
|
3202
|
+
},
|
|
3203
|
+
status: 502,
|
|
3204
|
+
}
|
|
3205
|
+
);
|
|
3206
|
+
}
|
|
3207
|
+
} catch (error) {
|
|
3208
|
+
console.error("[Server] Error in handleProxyRequest:", error);
|
|
3209
|
+
return new Response(
|
|
3210
|
+
JSON.stringify({
|
|
3211
|
+
error: "Failed to proxy request",
|
|
3212
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
3213
|
+
}),
|
|
3214
|
+
{
|
|
3215
|
+
headers: {
|
|
3216
|
+
"Content-Type": "application/json",
|
|
3217
|
+
...corsHeaders,
|
|
3218
|
+
},
|
|
3219
|
+
status: 500,
|
|
3220
|
+
}
|
|
3221
|
+
);
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
console.log(`🚀 Bun server running on http://0.0.0.0:${server.port}`);
|
|
2600
3226
|
console.log(`📡 HTTP API endpoints available:`);
|
|
2601
3227
|
console.log(` POST /api/session/create - Create a new session`);
|
|
2602
3228
|
console.log(` GET /api/session/list - List all sessions`);
|
|
@@ -2610,11 +3236,17 @@ console.log(` POST /api/mkdir - Create a directory`);
|
|
|
2610
3236
|
console.log(` POST /api/mkdir/stream - Create a directory (streaming)`);
|
|
2611
3237
|
console.log(` POST /api/write - Write a file`);
|
|
2612
3238
|
console.log(` POST /api/write/stream - Write a file (streaming)`);
|
|
3239
|
+
console.log(` POST /api/read - Read a file`);
|
|
3240
|
+
console.log(` POST /api/read/stream - Read a file (streaming)`);
|
|
2613
3241
|
console.log(` POST /api/delete - Delete a file`);
|
|
2614
3242
|
console.log(` POST /api/delete/stream - Delete a file (streaming)`);
|
|
2615
3243
|
console.log(` POST /api/rename - Rename a file`);
|
|
2616
3244
|
console.log(` POST /api/rename/stream - Rename a file (streaming)`);
|
|
2617
3245
|
console.log(` POST /api/move - Move a file`);
|
|
2618
3246
|
console.log(` POST /api/move/stream - Move a file (streaming)`);
|
|
3247
|
+
console.log(` POST /api/expose-port - Expose a port for external access`);
|
|
3248
|
+
console.log(` DELETE /api/unexpose-port - Unexpose a port`);
|
|
3249
|
+
console.log(` GET /api/exposed-ports - List exposed ports`);
|
|
3250
|
+
console.log(` GET /proxy/{port}/* - Proxy requests to exposed ports`);
|
|
2619
3251
|
console.log(` GET /api/ping - Health check`);
|
|
2620
3252
|
console.log(` GET /api/commands - List available commands`);
|