Skip to content

Instantly share code, notes, and snippets.

@Garciat
Last active February 22, 2025 07:33
Show Gist options
  • Save Garciat/66dbdb1b7534c63e82560a7d0440bdb8 to your computer and use it in GitHub Desktop.
Save Garciat/66dbdb1b7534c63e82560a7d0440bdb8 to your computer and use it in GitHub Desktop.
Manage your VS Code Remote Tunnel with Deno + CGI.
#!/usr/bin/env -S /home/garciat/homebrew/bin/deno --allow-env --allow-run --allow-read --allow-write --allow-net
import "jsr:@std/dotenv/load";
import { readAllSync } from "jsr:@std/io/read-all";
import { writeAllSync } from "jsr:@std/io/write-all";
import { deleteCookie, getCookies, setCookie } from "jsr:@std/http/cookie";
import { Application, Context, Router } from "jsr:@oak/oak";
import { render } from "npm:preact-render-to-string";
import { LoginTicket, OAuth2Client } from "npm:google-auth-library";
const css = String.raw;
function assertNotNull<T>(t: T): NonNullable<T> {
if (t === null || t === undefined) {
throw new Error("expected non-null");
}
return t;
}
function decodeText(input: BufferSource): string {
return new TextDecoder().decode(input);
}
function newExceptionResponse(e: Error | HTTPError): Response {
const status = e instanceof HTTPError ? e.status : 500;
return newPlainTextResponse(`Uncaught Error:\n\n${e?.stack}`, status);
}
function newPlainTextResponse(text: string, status = 200): Response {
return new Response(text, {
status,
headers: {
"content-type": "text/plain",
},
});
}
function newHTMLResponse(contents: string): Response {
return new Response(`<!DOCTYPE html>\n${contents}`, {
headers: {
"content-type": "text/html",
},
});
}
function newFileResponse(mime: string, contents: string): Response {
return new Response(contents, {
headers: {
"content-type": mime,
},
});
}
function newRedirectResponse(uri: string): Response {
return new Response("", {
status: 302,
headers: {
"content-type": "text/html",
"location": uri,
},
});
}
function newLogoutResponse(redirect: string, cookies: string[]): Response {
const headers = new Headers({
"content-type": "text/html",
"location": redirect,
});
for (const cookie of cookies) {
deleteCookie(headers, cookie, {
"secure": true,
});
}
return new Response("", {
status: 302,
headers,
});
}
abstract class HTTPError extends Error {
abstract readonly status: number;
}
class HTTPBadRequestError extends HTTPError {
readonly status = 400;
}
class HTTPUnauthorized extends HTTPError {
readonly status = 401;
}
class CGI {
static parseRequest(env: Deno.Env): Request {
const method = assertNotNull(env.get("REQUEST_METHOD"));
const scheme = assertNotNull(env.get("REQUEST_SCHEME"));
const host = assertNotNull(env.get("HTTP_HOST"));
const uri = assertNotNull(env.get("REQUEST_URI"));
const url = `${scheme}://${host}${uri}`;
const headers = new Headers();
for (const [name, value] of Object.entries(env.toObject())) {
if (name.startsWith("HTTP_")) {
const rest = name.slice("HTTP_".length);
const parts = rest.split("_");
const headerName = parts.join("-").toLowerCase();
headers.set(headerName, value);
}
}
const body = readAllSync(Deno.stdin);
return new Request(url.toString(), {
method,
headers,
body: method === "GET" || method === "HEAD" ? null : body,
});
}
static async writeReponse(res: Response): Promise<void> {
const enc = new TextEncoder();
Deno.stdout.writeSync(
enc.encode(`Status: ${res.status} ${res.statusText}\r\n`),
);
for (const [name, value] of res.headers.entries()) {
Deno.stdout.writeSync(enc.encode(`${name}: ${value}\r\n`));
}
Deno.stdout.writeSync(enc.encode("\r\n"));
const bytes = await res.bytes();
writeAllSync(Deno.stdout, bytes);
Deno.stdout.writeSync(enc.encode("\r\n"));
Deno.stdout.close();
}
}
type BasicHandler = (req: Request) => Promise<Response>;
type RouteParams = Record<string, string | undefined>;
type RouteHandler = (req: Request, params: RouteParams) => Promise<Response>;
type RouteMethod = "GET" | "POST";
type RoutePath = string;
type Routes = ReadonlyArray<Readonly<[RouteMethod, RoutePath, RouteHandler]>>;
function handleStatic(mime: string, contents: string): BasicHandler {
return () => Promise.resolve(newFileResponse(mime, contents));
}
function googleAuthMiddleware(
options: {
baseURL: string;
tokenCookieName: string;
clientId: string;
emailAllowlist: string[];
},
) {
const handleBadAuth = () => {
return newLogoutResponse(options.baseURL, [options.tokenCookieName]);
};
const showAuthPage = () => {
function main(callbackName: string) {
Object.defineProperty(globalThis, callbackName, {
value: async (token: { credential: string }) => {
const response = await fetch("", {
method: "POST",
body: token.credential,
});
console.log(await response.text());
if (response.ok) {
globalThis.location.reload();
} else {
alert("Could not auth your user.");
}
},
});
}
const callbackName = "handleToken";
const source = `${main}; main("${callbackName}");`;
const doc = (
<html style="height:100%;display:flex;color-scheme:dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body style="flex-grow:1;display:flex;align-items:center;justify-content:center">
<script src="https://accounts.google.com/gsi/client" async />
<script dangerouslySetInnerHTML={{ __html: source }}></script>
<style
dangerouslySetInnerHTML={{
__html: "iframe{color-scheme: normal;}",
}}
>
</style>
<div
id="g_id_onload"
data-client_id={options.clientId}
data-use_fedcm_for_prompt="true"
data-callback={callbackName}
>
</div>
<div
class="g_id_signin"
data-type="standard"
data-size="large"
data-theme="filled_blue"
data-text="signin"
data-shape="pill"
data-logo_alignment="left"
>
</div>
</body>
</html>
);
return newHTMLResponse(render(doc));
};
const handleAuthSave = async (req: Request) => {
const token = await req.text();
if (!await verifyAuth(token)) {
return new Response("", { status: 401 });
}
const headers = new Headers();
setCookie(headers, {
name: options.tokenCookieName,
value: token,
secure: true,
});
return new Response("", {
status: 201,
headers,
});
};
const verifyAuth = async (token: string) => {
const client = new OAuth2Client();
let ticket: LoginTicket;
try {
ticket = await client.verifyIdToken({
idToken: token,
audience: options.clientId,
});
} catch {
return false;
}
const payload = ticket.getPayload();
const email = payload?.email;
if (!email) {
return false;
}
if (!options.emailAllowlist.includes(email)) {
return false;
}
return true;
};
const handler = async (req: Request) => {
const cookies = getCookies(req.headers);
const token = cookies[options.tokenCookieName];
if (!token) {
switch (req.method) {
case "GET":
return showAuthPage();
case "POST":
return handleAuthSave(req);
default:
throw new HTTPUnauthorized("no auth");
}
}
if (await verifyAuth(token)) {
return null; // signal to use next middleware
} else {
return handleBadAuth();
}
};
// adapt to oak
return async (ctx: Context, next: () => Promise<unknown>) => {
const res = await handler(ctx.request.source!);
if (res) return ctx.response.with(res);
await next();
};
}
function oak(handler: BasicHandler): (ctx: Context) => Promise<void> {
return async (ctx) => ctx.response.with(await handler(ctx.request.source!));
}
class App {
readonly handle: BasicHandler;
constructor(
private readonly options: {
scriptName: string;
tokenCookieName: string;
vscodeCommand: string;
vscodeOutputFile: string;
vscodeCwd: string;
googleClientId: string;
googleEmailAllowlist: string[];
},
) {
const router = new Router({ prefix: this.options.scriptName });
router.get("/", oak(this.index));
router.post("/logout", oak(this.logout));
router.post("/start", oak(this.start));
router.post("/kill", oak(this.kill("TERM")));
router.post("/kill9", oak(this.kill("9")));
router.get("/main.css", oak(handleStatic("text/css", this.mainCSS)));
const app = new Application();
app.use(googleAuthMiddleware({
baseURL: this.route("/"),
tokenCookieName: this.options.tokenCookieName,
clientId: this.options.googleClientId,
emailAllowlist: this.options.googleEmailAllowlist,
}));
app.use(router.routes());
app.use(router.allowedMethods());
this.handle = async (req) => (await app.handle(req))!;
}
route(path: string): string {
return `${this.options.scriptName}${path}`;
}
readonly index = async () => {
const cmdPid = await PosixTools.pgrepCommand(
this.options.vscodeCommand,
);
const allPids = cmdPid ? await PosixTools.pgrepWholeTree(cmdPid) : [];
const processes = (await Promise.all(allPids.map(PosixTools.psInfo)))
.flatMap((process) => process === null ? [] : [process]);
const output = decodeText(
await Deno.readFile(this.options.vscodeOutputFile),
).replaceAll(/(https:\/\/[^\(\)\s]+)/g, `<a href="$1">$1</a>`);
const doc = (
<html>
<head>
<meta charset="UTF-8" />
<title>VS Code Tunnel Manager</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href={this.route("/main.css")} />
</head>
<body>
<main>
<header class="hstack">
<h1>VS Code Tunnel Manager</h1>
<div class="spacer" />
<aside>
Status {cmdPid ? "🟢" : "🔴"}
</aside>
</header>
<section class="hstack gap-small">
{!cmdPid && (
<>
<form action={this.route("/start")} method="post">
<button type="submit">Start</button>
</form>
</>
)}
{cmdPid && (
<>
<form action={this.route("/kill")} method="post">
<button type="submit" hidden>Kill -TERM</button>
</form>
<form action={this.route("/kill9")} method="post">
<button type="submit">Kill -9</button>
</form>
</>
)}
<form action={this.route("/logout")} method="post">
<button type="submit">Log out</button>
</form>
</section>
<section>
<h2>Processes</h2>
<pre>
{processes.map((process) => (
`${process.pid}\t${process.command}\n`
))}
</pre>
</section>
<section>
<h2>Output</h2>
<pre dangerouslySetInnerHTML={{ __html: output }}></pre>
</section>
</main>
</body>
</html>
);
return newHTMLResponse(render(doc));
};
readonly logout = () =>
Promise.resolve(newLogoutResponse(
this.route("/"),
[this.options.tokenCookieName],
));
readonly start = async () => {
const cmdPid = await PosixTools.pgrepCommand(this.options.vscodeCommand);
if (cmdPid !== null) {
throw new HTTPBadRequestError("Program is already running");
}
await Deno.truncate(this.options.vscodeOutputFile);
const command = new Deno.Command("/usr/bin/env", {
args: [
"bash",
"-c",
`cd ${this.options.vscodeCwd}; ${this.options.vscodeCommand} 2>&1 1>${this.options.vscodeOutputFile} &`,
],
clearEnv: true,
stdin: "null",
stdout: "null",
stderr: "null",
});
const child = command.spawn();
child.unref();
return newRedirectResponse(this.route("/"));
};
readonly kill = (mode: "TERM" | "9") => async () => {
const cmdPid = await PosixTools.pgrepCommand(this.options.vscodeCommand);
if (cmdPid === null) {
throw new HTTPBadRequestError("Program is not running");
}
const allPids = await PosixTools.pgrepWholeTree(cmdPid);
for (const pid of allPids.toReversed()) {
try {
await PosixTools.kill(pid, mode);
} catch {
continue;
}
}
await Deno.truncate(this.options.vscodeOutputFile);
return newRedirectResponse(this.route("/"));
};
readonly mainCSS = css`
html {
color-scheme: dark;
height: 100%;
display: flex;
flex-direction: column;
}
body {
flex-grow: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;
}
main {
width: min(48rem, 100vw - 2rem);
padding: 1rem;
display: flex;
flex-direction: column;
}
h1, h2, h3 {
font-weight: normal;
}
pre {
overflow: auto;
}
form {
display: contents;
}
.hstack {
display: flex;
flex-direction: row;
align-items: center;
}
.gap-small {
gap: 0.5rem;
}
.spacer {
flex-grow: 1;
}
`;
}
class PosixTools {
static async pgrepCommand(cmd: string): Promise<number | null> {
const command = new Deno.Command("pgrep", {
args: ["-f", cmd],
});
const { code, stdout } = await command.output();
if (code !== 0) {
return null;
}
const out = new TextDecoder().decode(stdout);
return parseInt(out.trim());
}
static async pgrepChildren(pid: number): Promise<number[]> {
const command = new Deno.Command("pgrep", {
args: ["-P", String(pid)],
});
const { code, stdout } = await command.output();
if (code !== 0) {
return [];
}
const out = new TextDecoder().decode(stdout);
return out.trim().split("\n").map((line) => parseInt(line));
}
static async pgrepWholeTree(pid: number): Promise<number[]> {
const pids = [pid];
for (const child of await this.pgrepChildren(pid)) {
pids.push(...await this.pgrepWholeTree(child));
}
return pids;
}
static async psInfo(pid: number) {
const command = new Deno.Command("ps", {
args: ["-o", "pid=,command=", "-p", String(pid)],
});
const { code, stdout } = await command.output();
if (code !== 0) {
return null;
}
{
const out = new TextDecoder().decode(stdout);
const match = out.trim().match(/^([0-9]+) (.*)$/);
if (match === null) {
throw new Error(`bad format: "${out}"`);
}
return {
pid: parseInt(match[1]),
command: match[2],
};
}
}
static async kill(pid: number, mode: "TERM" | "9"): Promise<void> {
const command = new Deno.Command("kill", {
args: [`-${mode}`, String(pid)],
});
const { code } = await command.output();
if (code !== 0) {
throw new Error("fail: kill");
}
}
}
async function main() {
const app = new App({
scriptName: assertNotNull(Deno.env.get("SCRIPT_NAME")),
tokenCookieName: "auth_token",
vscodeCommand: assertNotNull(Deno.env.get("VSCODE_COMMAND")),
vscodeOutputFile: assertNotNull(Deno.env.get("VSCODE_OUTPUT_FILE")),
vscodeCwd: assertNotNull(Deno.env.get("VSCODE_CWD")),
googleClientId: assertNotNull(Deno.env.get("GOOGLE_CLIENT_ID")),
googleEmailAllowlist: assertNotNull(Deno.env.get("AUTH_USER_EMAIL")).split(
",",
),
});
const req = CGI.parseRequest(Deno.env);
const res = await app.handle(req);
await CGI.writeReponse(res);
}
try {
await main();
} catch (e) {
await CGI.writeReponse(newExceptionResponse(e as Error));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment