Skip to content

Commit 0aea323

Browse files
add sf vm ssh
1 parent 0779197 commit 0aea323

5 files changed

Lines changed: 188 additions & 1 deletion

File tree

deno.lock

Lines changed: 48 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"test": "deno test --allow-all"
99
},
1010
"dependencies": {
11+
"@alphahydrae/exec": "^1.1.0",
1112
"@commander-js/extra-typings": "^12.1.0",
1213
"@inkjs/ui": "^2.0.0",
1314
"@inquirer/prompts": "^5.1.2",
@@ -36,6 +37,7 @@
3637
"prettier": "^3.5.3",
3738
"react": "^18.3.1",
3839
"semver": "^7.6.3",
40+
"shescape": "^2.1.1",
3941
"tiny-invariant": "^1.3.3",
4042
"tweetnacl": "^1.0.3",
4143
"tweetnacl-util": "^0.15.1",

src/helpers/urls.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,10 @@ const apiPaths: Record<string, Path<IdParams | never>> = {
4545

4646
vms_instances_list: "/v0/vms/instances",
4747
vms_logs_list: "/v0/vms/logs",
48+
vms_replace: "/v0/vms/replace",
4849
vms_script_post: "/v0/vms/script",
4950
vms_script_get: "/v0/vms/script",
50-
vms_replace: "/v0/vms/replace",
51+
vms_ssh_get: "/v0/vms/ssh",
5152
};
5253

5354
export async function getWebAppUrl<T extends TokenParams | never>(

src/lib/ssh.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import type { Command } from "@commander-js/extra-typings";
2+
import console from "node:console";
3+
import process from "node:process";
4+
import { Shescape } from "shescape";
5+
import { execvp } from "@alphahydrae/exec";
6+
import {
7+
logAndQuit,
8+
logSessionTokenExpiredAndQuit,
9+
} from "../helpers/errors.ts";
10+
import { getApiUrl } from "../helpers/urls.ts";
11+
import { getAuthToken } from "../helpers/config.ts";
12+
13+
type SshHostKey = {
14+
keyType: string;
15+
base64EncodedKey: string;
16+
};
17+
18+
export function registerSsh(program: Command) {
19+
program
20+
.command("ssh")
21+
.option("-q, --quiet", "Quiet mode", false)
22+
.option(
23+
"--use-host-keys [value]",
24+
"Use API provided SSH server host keys if known",
25+
true,
26+
)
27+
.argument(
28+
"<destination>",
29+
"USERNAME@VM_ID The (optional) username, and VM id to SSH into.",
30+
)
31+
.allowExcessArguments(false)
32+
.action(async (destination, options) => {
33+
const splitDestination = destination.split("@");
34+
let vmId: string;
35+
let sshUsername: string | undefined;
36+
if (splitDestination.length == 1) {
37+
sshUsername = undefined;
38+
vmId = splitDestination[0];
39+
} else if (splitDestination.length == 2) {
40+
sshUsername = splitDestination[0];
41+
vmId = splitDestination[1];
42+
} else {
43+
logAndQuit(`Invalid SSH destination string: ${destination}`);
44+
}
45+
46+
const response = await fetch(await getApiUrl("vms_ssh_get"), {
47+
method: "POST",
48+
headers: {
49+
"Content-Type": "application/json",
50+
Authorization: `Bearer ${await getAuthToken()}`,
51+
},
52+
body: JSON.stringify({ vm_id: vmId }),
53+
});
54+
55+
if (!response.ok) {
56+
if (response.status === 401) {
57+
logSessionTokenExpiredAndQuit();
58+
}
59+
60+
logAndQuit(
61+
`Failed to retrieve ssh information: ${response.statusText}`,
62+
);
63+
}
64+
65+
const data = (await response.json()) as {
66+
ssh_hostname: string;
67+
ssh_port: number;
68+
ssh_host_keys: SshHostKey[] | undefined;
69+
};
70+
const sshHostname = data.ssh_hostname;
71+
const sshPort = data.ssh_port;
72+
const sshHostKeys = data.ssh_host_keys || [];
73+
74+
let sshDestination = sshHostname;
75+
if (sshUsername !== undefined) {
76+
sshDestination = `${sshUsername}@${sshDestination}`;
77+
}
78+
if (sshPort !== undefined) {
79+
sshDestination = `${sshDestination}:${sshPort}`;
80+
}
81+
sshDestination = `ssh://${sshDestination}`;
82+
83+
let cmd = ["ssh"];
84+
85+
if (sshHostKeys.length > 0 && options.useHostKeys) {
86+
let knownHostsCommand = ["/usr/bin/env", "printf", "%s %s %s\\n"];
87+
for (const sshHostKey of sshHostKeys) {
88+
knownHostsCommand = knownHostsCommand.concat([
89+
sshHostname,
90+
sshHostKey.keyType,
91+
sshHostKey.base64EncodedKey,
92+
]);
93+
}
94+
// Escape all characters for proper pass through
95+
for (const i in knownHostsCommand) {
96+
knownHostsCommand[i] = knownHostsCommand[i].replaceAll("%", "%%");
97+
knownHostsCommand[i] = knownHostsCommand[i].replaceAll('"', '\\"');
98+
knownHostsCommand[i] = '"' + knownHostsCommand[i] + '"';
99+
}
100+
const knownHostsCommand_str = knownHostsCommand.join(" ");
101+
cmd = cmd.concat(["-o", `KnownHostsCommand=${knownHostsCommand_str}`]);
102+
}
103+
104+
cmd = cmd.concat([sshDestination]);
105+
106+
let shescape: undefined | Shescape = undefined;
107+
let shell: undefined | string = undefined;
108+
if (process.env.SHELL !== undefined) {
109+
try {
110+
shescape = new Shescape({
111+
flagProtection: false,
112+
shell: process.env.SHELL,
113+
});
114+
shell = process.env.SHELL;
115+
} catch {
116+
// shescape will stay undefined
117+
}
118+
}
119+
if (shescape === undefined) {
120+
shescape = new Shescape({
121+
flagProtection: false,
122+
shell: "/bin/sh",
123+
});
124+
shell = "/bin/sh";
125+
}
126+
const shell_cmd = shescape.quoteAll(cmd).join(" ");
127+
if (!options.quiet) {
128+
console.log(`Executing (${shell} style output): ${shell_cmd}`);
129+
}
130+
131+
execvp(cmd[0], cmd.slice(1));
132+
});
133+
}

src/lib/vm.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from "../helpers/errors.ts";
1515
import { getApiUrl } from "../helpers/urls.ts";
1616
import { isFeatureEnabled } from "./posthog.ts";
17+
import { registerSsh } from "./ssh.ts";
1718

1819
type VMInstance = {
1920
id: string;
@@ -34,6 +35,8 @@ export async function registerVM(program: Command) {
3435
.aliases(["v", "vms"])
3536
.description("Manage virtual machines");
3637

38+
registerSsh(vm);
39+
3740
vm.command("list")
3841
.description("List all virtual machines")
3942
.action(async () => {

0 commit comments

Comments
 (0)