Files
plainleaf/server/shell_endpoints.ts
T

126 lines
3.3 KiB
TypeScript

import type { Context } from "hono";
import type { ShellRequest } from "@silverbulletmd/silverbullet/type/rpc";
import type { ShellBackend } from "./shell_backend.ts";
import { StreamingShell } from "./shell_backend.ts";
/**
* Handles the /.shell endpoint for non-streaming shell commands
*/
export async function handleShellEndpoint(
c: Context,
shellBackend: ShellBackend,
readOnly: boolean,
): Promise<Response> {
const req = c.req;
const body = await req.json();
try {
if (readOnly) {
return c.text("Read only mode, no shell commands allowed", 405);
}
const shellCommand: ShellRequest = body;
// Note: in read-only this is set to NoShellSupport, so don't worry
const shellResponse = await shellBackend.handle(shellCommand);
return c.json(shellResponse);
} catch (e: any) {
console.error("Shell error", e);
return c.text(e.message, 500);
}
}
/**
* Handles the /.shell/stream endpoint for streaming shell commands
*/
export function handleShellStreamEndpoint(
c: Context,
cwd: string,
readOnly: boolean,
): Response {
const req = c.req;
const url = new URL(req.url);
// Check if read-only mode is enabled
if (readOnly) {
return c.text("Read only mode, no shell commands allowed", 405);
}
// Get command and arguments from query parameters
const cmd = url.searchParams.get("cmd");
if (!cmd) {
return c.text("Missing cmd parameter", 400);
}
let args: string[] = [];
const argsParam = url.searchParams.get("args");
if (argsParam) {
try {
args = JSON.parse(argsParam);
if (!Array.isArray(args)) {
return c.text("args must be a JSON array", 400);
}
} catch (_e) {
return c.text("Invalid args parameter: must be a JSON array", 400);
}
}
// Create a streaming shell
const shell = new StreamingShell(cwd);
// Upgrade the connection to a WebSocket
try {
const { socket, response } = Deno.upgradeWebSocket(req.raw);
// Set up WebSocket event handlers
socket.onopen = () => {
// Start the shell process when the WebSocket is opened
shell.start(cmd, args);
// Forward shell output to the WebSocket
shell.onOutput((type, data) => {
socket.send(JSON.stringify({
type,
data,
}));
});
};
socket.onmessage = async (event) => {
try {
const message = JSON.parse(event.data);
if (message.type === "stdin") {
await shell.writeToStdin(message.data);
} else if (message.type === "signal") {
const success = shell.sendSignal(message.signal);
if (!success) {
socket.send(JSON.stringify({
type: "error",
data: `Failed to send signal: ${message.signal}`,
}));
}
}
} catch (e) {
console.error("Error handling WebSocket message:", e);
socket.send(JSON.stringify({
type: "error",
data: `Error: ${e}`,
}));
}
};
socket.onclose = async () => {
await shell.close();
};
socket.onerror = (error) => {
console.error("WebSocket error:", error);
shell.close().catch((e) => {
console.error("Error closing shell:", e);
});
};
return response;
} catch (e) {
console.error("WebSocket upgrade failed:", e);
return c.text("WebSocket upgrade failed", 400);
}
}