Files
plainleaf/server/shell_backend.ts
T

112 lines
2.9 KiB
TypeScript

import type { ShellRequest, ShellResponse } from "../type/rpc.ts";
import type { ServerOptions } from "./http_server.ts";
/**
* Configuration via environment variables:
* - SB_SHELL_BACKEND: "local" or "off"
*/
export function determineShellBackend(
serverOptions: ServerOptions,
): ShellBackend {
const backendConfig = Deno.env.get("SB_SHELL_BACKEND") || "local";
switch (backendConfig) {
case "local":
if (serverOptions.shellCommandWhiteList) {
console.info(
"Running with the following shell commands enabled:",
serverOptions.shellCommandWhiteList,
);
} else {
console.info("Running with ALL shell commands enabled.");
}
return new LocalShell(
serverOptions.pagesPath,
serverOptions.shellCommandWhiteList,
);
default:
console.info(
"Running in shell-less mode, meaning shell commands are disabled",
);
return new NotSupportedShell();
}
}
export interface ShellBackend {
handle(shellRequest: ShellRequest): Promise<ShellResponse>;
}
export class NotSupportedShell implements ShellBackend {
handle(): Promise<ShellResponse> {
return Promise.resolve({
stdout: "",
stderr: "Not supported",
code: 1,
});
}
}
export class LocalShell implements ShellBackend {
constructor(private cwd: string, private cmdWhiteList?: string[]) {
}
async handle(shellRequest: ShellRequest): Promise<ShellResponse> {
if (this.cmdWhiteList && !this.cmdWhiteList.includes(shellRequest.cmd)) {
console.error(
"Not running shell command because not in whitelist",
shellRequest,
);
return {
code: -1,
stdout: "",
stderr: "Not allowed, command not in whitelist",
};
}
console.log(
"Running shell command:",
shellRequest.cmd,
shellRequest.args,
);
const p = new Deno.Command(shellRequest.cmd, {
cwd: this.cwd,
args: shellRequest.args,
stdin: "piped",
stdout: "piped",
stderr: "piped",
}).spawn();
if (shellRequest.stdin) {
// Write a string to stdin
const encoder = new TextEncoder();
const writer = p.stdin.getWriter();
await writer.write(encoder.encode(shellRequest.stdin));
writer.releaseLock();
}
await p.stdin.close();
// Capture stdout and stderr
const [stdout, stderr] = await Promise.all([
readAll(p.stdout.getReader()),
readAll(p.stderr.getReader()),
]);
// Get the exit status
const status = await p.status;
return {
stderr,
stdout,
code: status.code,
};
}
}
async function readAll(r: ReadableStreamDefaultReader) {
const chunks: string[] = [];
const decoder = new TextDecoder();
while (true) {
const { value, done } = await r.read();
if (done) break;
chunks.push(decoder.decode(value));
}
return chunks.join();
}