feature: Implements base API endpoints.

Co-authored-by: dess <devessa@users.noreply.github.com>
This commit is contained in:
greysoh 2024-05-05 11:56:36 -04:00
parent 6cf26da4df
commit 3955b01ede
No known key found for this signature in database
GPG key ID: FE0F173B8FC01571
11 changed files with 480 additions and 7 deletions

View file

@ -1,6 +1,9 @@
import type { BackendBaseClass } from "./base.js";
import { PassyFireBackendProvider } from "./passyfire-reimpl/index.js";
import { SSHBackendProvider } from "./ssh.js";
export const backendProviders: Record<string, typeof BackendBaseClass> = {
"ssh": SSHBackendProvider
"ssh": SSHBackendProvider,
"passyfire": PassyFireBackendProvider
};

View file

@ -0,0 +1,173 @@
import Fastify, { type FastifyInstance } from "fastify";
import type { ForwardRule, ConnectedClient, ParameterReturnedValue, BackendBaseClass } from "../base.js";
import { route } from "./routes.js";
import { generateRandomData } from "../../libs/generateRandom.js";
type BackendProviderUser = {
username: string,
password: string
}
type ForwardRuleExt = ForwardRule & {
protocol: "tcp" | "udp",
userConfig: Record<string, string>
};
// Fight me (for better naming)
type BackendParsedProviderString = {
ip: string,
port: number,
publicPort?: number,
isProxied?: boolean,
users: BackendProviderUser[]
}
type LoggedInUser = {
username: string,
token: string
}
function parseBackendProviderString(data: string): BackendParsedProviderString {
try {
JSON.parse(data);
} catch (e) {
throw new Error("Payload body is not JSON");
}
const jsonData = JSON.parse(data);
if (typeof jsonData.ip != "string") throw new Error("IP field is not a string");
if (typeof jsonData.port != "number") throw new Error("Port is not a number");
if (typeof jsonData.publicPort != "undefined" && typeof jsonData.publicPort != "number") throw new Error("(optional field) Proxied port is not a number");
if (typeof jsonData.isProxied != "undefined" && typeof jsonData.isProxied != "boolean") throw new Error("(optional field) 'Is proxied' is not a boolean");
if (!Array.isArray(jsonData.users)) throw new Error("Users is not an array");
for (const userIndex in jsonData.users) {
const user = jsonData.users[userIndex];
if (typeof user.username != "string") throw new Error("Username is not a string, in users array");
if (typeof user.password != "string") throw new Error("Password is not a string, in users array");
}
return {
ip: jsonData.ip,
port: jsonData.port,
publicPort: jsonData.publicPort,
isProxied: jsonData.isProxied,
users: jsonData.users
}
}
export class PassyFireBackendProvider implements BackendBaseClass {
state: "stopped" | "stopping" | "started" | "starting";
clients: ConnectedClient[];
proxies: ForwardRuleExt[];
users: LoggedInUser[];
logs: string[];
options: BackendParsedProviderString;
fastify: FastifyInstance;
constructor(parameters: string) {
this.logs = [];
this.clients = [];
this.proxies = [];
this.state = "stopped";
this.options = parseBackendProviderString(parameters);
this.users = [];
}
async start(): Promise<boolean> {
this.state = "starting";
this.fastify = Fastify({
logger: true,
trustProxy: this.options.isProxied
});
route(this);
await this.fastify.listen({
port: this.options.port,
host: this.options.ip
});
this.state = "started";
return true;
}
async stop(): Promise<boolean> {
await this.fastify.close();
this.users.splice(0, this.users.length);
this.proxies.splice(0, this.proxies.length);
this.clients.splice(0, this.clients.length);
return true;
};
addConnection(sourceIP: string, sourcePort: number, destPort: number, protocol: "tcp" | "udp"): void {
const proxy: ForwardRuleExt = {
sourceIP,
sourcePort,
destPort,
protocol,
userConfig: {}
};
for (const user of this.options.users) {
proxy.userConfig[user.username] = generateRandomData();
}
this.proxies.push(proxy);
};
removeConnection(sourceIP: string, sourcePort: number, destPort: number, protocol: "tcp" | "udp"): void {
const connectionCheck = PassyFireBackendProvider.checkParametersConnection(sourceIP, sourcePort, destPort, protocol);
if (!connectionCheck.success) throw new Error(connectionCheck.message);
const foundProxyEntry = this.proxies.find((i) => i.sourceIP == sourceIP && i.sourcePort == sourcePort && i.destPort == destPort);
if (!foundProxyEntry) return;
this.proxies.splice(this.proxies.indexOf(foundProxyEntry), 1);
return;
};
getAllConnections(): ConnectedClient[] {
if (this.clients == null) return [];
return this.clients;
};
static checkParametersConnection(sourceIP: string, sourcePort: number, destPort: number, protocol: "tcp" | "udp"): ParameterReturnedValue {
return {
success: true
}
};
static checkParametersBackendInstance(data: string): ParameterReturnedValue {
try {
parseBackendProviderString(data);
// @ts-ignore
} catch (e: Error) {
return {
success: false,
message: e.toString()
}
}
return {
success: true
}
};
}

View file

@ -0,0 +1,162 @@
import { generateRandomData } from "../../libs/generateRandom.js";
import type { PassyFireBackendProvider } from "./index.js";
export function route(instance: PassyFireBackendProvider) {
const { fastify } = instance;
const proxiedPort: number = instance.options.publicPort ?? 443;
const unsupportedSpoofedRoutes: string[] = [
"/api/v1/tunnels/add",
"/api/v1/tunnels/edit",
"/api/v1/tunnels/remove",
// TODO (greysoh): Should we implement these? We have these for internal reasons. We could expose these /shrug
"/api/v1/tunnels/start",
"/api/v1/tunnels/stop",
// Same scenario for this API.
"/api/v1/users",
"/api/v1/users/add",
"/api/v1/users/remove",
"/api/v1/users/enable",
"/api/v1/users/disable",
];
fastify.get("/api/v1/static/getScopes", () => {
return {
success: true,
data: {
users: {
add: true,
remove: true,
get: true,
getPasswords: true
},
routes: {
add: true,
remove: true,
start: true,
stop: true,
get: true,
getPasswords: true
}
}
}
});
for (const spoofedRoute of unsupportedSpoofedRoutes) {
fastify.post(spoofedRoute, (req, res) => {
if (typeof req.body != "string") return res.status(400).send({
error: "Invalid token"
});
try {
JSON.parse(req.body);
} catch (e) {
return res.status(400).send({
error: "Invalid token"
})
}
// @ts-ignore
if (!req.body.token) return res.status(400).send({
error: "Invalid token"
});
return res.status(403).send({
error: "Invalid scope(s)"
});
})
}
fastify.post("/api/v1/users/login", {
schema: {
body: {
type: "object",
required: ["username", "password"],
properties: {
username: { type: "string" },
password: { type: "string" }
}
}
}
}, (req, res) => {
// @ts-ignore
const body: {
username: string,
password: string
} = req.body;
if (!instance.options.users.find((i) => i.username == body.username && i.password == body.password)) {
return res.status(403).send({
error: "Invalid username/password."
});
};
const token = generateRandomData();
instance.users.push({
username: body.username,
token
});
return {
success: true,
data: {
token
}
}
});
fastify.post("/api/v1/tunnels", {
schema: {
body: {
type: "object",
required: ["token"],
properties: {
token: { type: "string" },
},
},
},
}, async (req, res) => {
console.log(req.hostname);
// @ts-ignore
const body: {
token: string
} = req.body;
const userData = instance.users.find(user => user.token == body.token);
if (!userData) return res.status(403).send({
error: "Invalid token"
});
const host = req.hostname.substring(0, req.hostname.indexOf(":"));
const unparsedPort = req.hostname.substring(req.hostname.indexOf(":") + 1);
// @ts-ignore
// parseInt(...) can take a number just fine, at least in Node.JS
const port = parseInt(unparsedPort == "" ? proxiedPort : unparsedPort);
res.send({
success: true,
data: instance.proxies.map((proxy) => ({
proxyUrlSettings: {
host,
port,
protocol: proxy.protocol.toUpperCase()
},
dest: `${proxy.sourceIP}:${proxy.sourcePort}`,
name: `${proxy.protocol.toUpperCase()} on ::${proxy.sourcePort}`,
passwords: [
proxy.userConfig[userData.username]
],
}))
});
});
}

View file

@ -48,7 +48,8 @@ const sessionTokens: Record<number, SessionToken[]> = {};
const backends: Record<number, BackendBaseClass> = {};
const fastify = Fastify({
logger: true
logger: true,
trustProxy: Boolean(process.env.IS_BEHIND_PROXY)
});
const routeOptions: RouteOptions = {