chore: Restructure files.
This commit is contained in:
parent
559588f726
commit
d25da9091e
93 changed files with 38 additions and 26 deletions
242
sshfrontend/src/index.ts
Normal file
242
sshfrontend/src/index.ts
Normal file
|
@ -0,0 +1,242 @@
|
|||
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
||||
import { timingSafeEqual } from "node:crypto";
|
||||
import { format } from "node:util";
|
||||
|
||||
import parseArgsStringToArgv from "string-argv";
|
||||
import baseAxios from "axios";
|
||||
import ssh2 from "ssh2";
|
||||
|
||||
import { readFromKeyboard } from "./libs/readFromKeyboard.js";
|
||||
import { commands } from "./commands.js";
|
||||
import { runCopyID } from "./copyID.js";
|
||||
|
||||
export type ClientKeys = {
|
||||
publicKey: string;
|
||||
username: string;
|
||||
password: string;
|
||||
}[];
|
||||
|
||||
function checkValue(input: Buffer, allowed: Buffer): boolean {
|
||||
const autoReject = input.length !== allowed.length;
|
||||
if (autoReject) allowed = input;
|
||||
|
||||
const isMatch = timingSafeEqual(input, allowed);
|
||||
return !autoReject && isMatch;
|
||||
}
|
||||
|
||||
let serverKeyFile: Buffer | string | undefined;
|
||||
let clientKeys: ClientKeys = [];
|
||||
|
||||
const serverBaseURL: string =
|
||||
process.env.SERVER_BASE_URL ?? "http://127.0.0.1:3000/";
|
||||
|
||||
const axios = baseAxios.create({
|
||||
baseURL: serverBaseURL,
|
||||
validateStatus: () => true,
|
||||
});
|
||||
|
||||
try {
|
||||
clientKeys = JSON.parse(await readFile("../keys/clients.json", "utf8"));
|
||||
} catch (e) {
|
||||
console.log("INFO: We don't have the client key file.");
|
||||
}
|
||||
|
||||
try {
|
||||
serverKeyFile = await readFile("../keys/host.key");
|
||||
} catch (e) {
|
||||
console.log(
|
||||
"ERROR: Failed to read the host key file! Creating new keypair...",
|
||||
);
|
||||
await mkdir("../keys").catch(() => null);
|
||||
|
||||
const keyPair: { private: string; public: string } = await new Promise(
|
||||
resolve =>
|
||||
ssh2.utils.generateKeyPair("ed25519", (err, keyPair) => resolve(keyPair)),
|
||||
);
|
||||
|
||||
await writeFile("../keys/host.key", keyPair.private);
|
||||
await writeFile("../keys/host.pub", keyPair.public);
|
||||
|
||||
serverKeyFile = keyPair.private;
|
||||
}
|
||||
|
||||
if (!serverKeyFile) throw new Error("Somehow failed to fetch the key file!");
|
||||
|
||||
const server: ssh2.Server = new ssh2.Server({
|
||||
hostKeys: [serverKeyFile],
|
||||
banner: "NextNet-LOM (c) NextNet project et al.",
|
||||
});
|
||||
|
||||
server.on("connection", client => {
|
||||
let token: string = "";
|
||||
|
||||
let username: string = "";
|
||||
let password: string = "";
|
||||
|
||||
client.on("authentication", async auth => {
|
||||
if (auth.method == "password") {
|
||||
const response = await axios.post("/api/v1/users/login", {
|
||||
username: auth.username,
|
||||
password: auth.password,
|
||||
});
|
||||
|
||||
if (response.status == 403) {
|
||||
return auth.reject(["password", "publickey"]);
|
||||
}
|
||||
|
||||
token = response.data.token;
|
||||
|
||||
username = auth.username;
|
||||
password = auth.password;
|
||||
|
||||
auth.accept();
|
||||
} else if (auth.method == "publickey") {
|
||||
const userData = {
|
||||
username: "",
|
||||
password: "",
|
||||
};
|
||||
|
||||
for (const rawKey of clientKeys) {
|
||||
const key = ssh2.utils.parseKey(rawKey.publicKey);
|
||||
|
||||
if (key instanceof Error) {
|
||||
console.log(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
(rawKey.username == auth.username &&
|
||||
auth.key.algo == key.type &&
|
||||
checkValue(auth.key.data, key.getPublicSSH())) ||
|
||||
(auth.signature &&
|
||||
key.verify(auth.blob as Buffer, auth.signature, auth.key.algo))
|
||||
) {
|
||||
userData.username = rawKey.username;
|
||||
userData.password = rawKey.password;
|
||||
}
|
||||
}
|
||||
|
||||
if (!userData.username || !userData.password)
|
||||
return auth.reject(["password", "publickey"]);
|
||||
|
||||
const response = await axios.post("/api/v1/users/login", userData);
|
||||
|
||||
if (response.status == 403) {
|
||||
return auth.reject(["password", "publickey"]);
|
||||
}
|
||||
|
||||
token = response.data.token;
|
||||
|
||||
username = userData.username;
|
||||
password = userData.password;
|
||||
|
||||
auth.accept();
|
||||
} else {
|
||||
return auth.reject(["password", "publickey"]);
|
||||
}
|
||||
});
|
||||
|
||||
client.on("ready", () => {
|
||||
client.on("session", accept => {
|
||||
const conn = accept();
|
||||
|
||||
conn.on("exec", async (accept, reject, info) => {
|
||||
const stream = accept();
|
||||
|
||||
if (
|
||||
info.command.includes(".ssh/authorized_keys") &&
|
||||
info.command.startsWith("exec sh -c")
|
||||
) {
|
||||
return await runCopyID(username, password, clientKeys, stream);
|
||||
}
|
||||
|
||||
// Matches on ; and &&
|
||||
const commandsRecv = info.command.split(/;|&&/).map(i => i.trim());
|
||||
|
||||
function println(...data: unknown[]) {
|
||||
stream.write(format(...data).replaceAll("\n", "\r\n"));
|
||||
}
|
||||
|
||||
for (const command of commandsRecv) {
|
||||
const argv = parseArgsStringToArgv(command);
|
||||
|
||||
if (argv[0] == "exit") {
|
||||
stream.close();
|
||||
} else {
|
||||
const command = commands.find(i => i.name == argv[0]);
|
||||
|
||||
if (!command) {
|
||||
stream.write(`Unknown command ${argv[0]}.\r\n`);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
await command.run(argv, println, axios, token, disableEcho =>
|
||||
readFromKeyboard(stream, disableEcho),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return stream.close();
|
||||
});
|
||||
|
||||
// We're dumb. We don't really care.
|
||||
conn.on("pty", accept => accept());
|
||||
conn.on("window-change", accept => {
|
||||
if (typeof accept != "function") return;
|
||||
accept();
|
||||
});
|
||||
|
||||
conn.on("shell", async accept => {
|
||||
const stream = accept();
|
||||
stream.write(
|
||||
"Welcome to NextNet LOM. Run 'help' to see commands.\r\n\r\n~$ ",
|
||||
);
|
||||
|
||||
function println(...data: unknown[]) {
|
||||
stream.write(format(...data).replaceAll("\n", "\r\n"));
|
||||
}
|
||||
|
||||
// FIXME (greysoh): wtf? this isn't setting correctly.
|
||||
// @eslint-disable-next-line
|
||||
while (true) {
|
||||
const line = await readFromKeyboard(stream);
|
||||
stream.write("\r\n");
|
||||
|
||||
if (line == "") {
|
||||
stream.write(`~$ `);
|
||||
continue;
|
||||
}
|
||||
|
||||
const argv = parseArgsStringToArgv(line);
|
||||
|
||||
if (argv[0] == "exit") {
|
||||
stream.close();
|
||||
} else {
|
||||
const command = commands.find(i => i.name == argv[0]);
|
||||
|
||||
if (!command) {
|
||||
stream.write(
|
||||
`Unknown command ${argv[0]}. Run 'help' to see commands.\r\n~$ `,
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
await command.run(argv, println, axios, token, disableEcho =>
|
||||
readFromKeyboard(stream, disableEcho),
|
||||
);
|
||||
stream.write("~$ ");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(
|
||||
2222,
|
||||
process.env.NODE_ENV == "production" ? "0.0.0.0" : "127.0.0.1",
|
||||
);
|
||||
|
||||
console.log("Started server at ::2222");
|
Loading…
Add table
Add a link
Reference in a new issue