mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-25 22:36:38 +00:00
Move things around; rename to olm
This commit is contained in:
106
server/routers/olm/createOlm.ts
Normal file
106
server/routers/olm/createOlm.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import db from "@server/db";
|
||||
import { hash } from "@node-rs/argon2";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { z } from "zod";
|
||||
import { newts } from "@server/db/schema";
|
||||
import createHttpError from "http-errors";
|
||||
import response from "@server/lib/response";
|
||||
import { SqliteError } from "better-sqlite3";
|
||||
import moment from "moment";
|
||||
import { generateSessionToken } from "@server/auth/sessions/app";
|
||||
import { createNewtSession } from "@server/auth/sessions/newt";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { hashPassword } from "@server/auth/password";
|
||||
|
||||
export const createNewtBodySchema = z.object({});
|
||||
|
||||
export type CreateNewtBody = z.infer<typeof createNewtBodySchema>;
|
||||
|
||||
export type CreateNewtResponse = {
|
||||
token: string;
|
||||
newtId: string;
|
||||
secret: string;
|
||||
};
|
||||
|
||||
const createNewtSchema = z
|
||||
.object({
|
||||
newtId: z.string(),
|
||||
secret: z.string()
|
||||
})
|
||||
.strict();
|
||||
|
||||
export async function createNewt(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
|
||||
const parsedBody = createNewtSchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { newtId, secret } = parsedBody.data;
|
||||
|
||||
if (!req.userOrgRoleId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
|
||||
);
|
||||
}
|
||||
|
||||
const secretHash = await hashPassword(secret);
|
||||
|
||||
await db.insert(newts).values({
|
||||
newtId: newtId,
|
||||
secretHash,
|
||||
dateCreated: moment().toISOString(),
|
||||
});
|
||||
|
||||
// give the newt their default permissions:
|
||||
// await db.insert(newtActions).values({
|
||||
// newtId: newtId,
|
||||
// actionId: ActionsEnum.createOrg,
|
||||
// orgId: null,
|
||||
// });
|
||||
|
||||
const token = generateSessionToken();
|
||||
await createNewtSession(token, newtId);
|
||||
|
||||
return response<CreateNewtResponse>(res, {
|
||||
data: {
|
||||
newtId,
|
||||
secret,
|
||||
token,
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Newt created successfully",
|
||||
status: HttpCode.OK,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_UNIQUE") {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"A newt with that email address already exists"
|
||||
)
|
||||
);
|
||||
} else {
|
||||
console.error(e);
|
||||
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Failed to create newt"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
115
server/routers/olm/getToken.ts
Normal file
115
server/routers/olm/getToken.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { generateSessionToken } from "@server/auth/sessions/app";
|
||||
import db from "@server/db";
|
||||
import { newts } from "@server/db/schema";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import response from "@server/lib/response";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import {
|
||||
createNewtSession,
|
||||
validateNewtSessionToken
|
||||
} from "@server/auth/sessions/newt";
|
||||
import { verifyPassword } from "@server/auth/password";
|
||||
import logger from "@server/logger";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
export const newtGetTokenBodySchema = z.object({
|
||||
newtId: z.string(),
|
||||
secret: z.string(),
|
||||
token: z.string().optional()
|
||||
});
|
||||
|
||||
export type NewtGetTokenBody = z.infer<typeof newtGetTokenBodySchema>;
|
||||
|
||||
export async function getToken(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
const parsedBody = newtGetTokenBodySchema.safeParse(req.body);
|
||||
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { newtId, secret, token } = parsedBody.data;
|
||||
|
||||
try {
|
||||
if (token) {
|
||||
const { session, newt } = await validateNewtSessionToken(token);
|
||||
if (session) {
|
||||
if (config.getRawConfig().app.log_failed_attempts) {
|
||||
logger.info(
|
||||
`Newt session already valid. Newt ID: ${newtId}. IP: ${req.ip}.`
|
||||
);
|
||||
}
|
||||
return response<null>(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Token session already valid",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const existingNewtRes = await db
|
||||
.select()
|
||||
.from(newts)
|
||||
.where(eq(newts.newtId, newtId));
|
||||
if (!existingNewtRes || !existingNewtRes.length) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"No newt found with that newtId"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const existingNewt = existingNewtRes[0];
|
||||
|
||||
const validSecret = await verifyPassword(
|
||||
secret,
|
||||
existingNewt.secretHash
|
||||
);
|
||||
if (!validSecret) {
|
||||
if (config.getRawConfig().app.log_failed_attempts) {
|
||||
logger.info(
|
||||
`Newt id or secret is incorrect. Newt: ID ${newtId}. IP: ${req.ip}.`
|
||||
);
|
||||
}
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Secret is incorrect")
|
||||
);
|
||||
}
|
||||
|
||||
const resToken = generateSessionToken();
|
||||
await createNewtSession(resToken, existingNewt.newtId);
|
||||
|
||||
return response<{ token: string }>(res, {
|
||||
data: {
|
||||
token: resToken
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Token created successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Failed to authenticate newt"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
147
server/routers/olm/handleGetConfigMessage.ts
Normal file
147
server/routers/olm/handleGetConfigMessage.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { z } from "zod";
|
||||
import { MessageHandler } from "../ws";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import db from "@server/db";
|
||||
import { olms, Site, sites } from "@server/db/schema";
|
||||
import { eq, isNotNull } from "drizzle-orm";
|
||||
import { findNextAvailableCidr } from "@server/lib/ip";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
const inputSchema = z.object({
|
||||
publicKey: z.string(),
|
||||
endpoint: z.string(),
|
||||
listenPort: z.number()
|
||||
});
|
||||
|
||||
type Input = z.infer<typeof inputSchema>;
|
||||
|
||||
export const handleGetConfigMessage: MessageHandler = async (context) => {
|
||||
const { message, newt, sendToClient } = context;
|
||||
|
||||
logger.debug("Handling Newt get config message!");
|
||||
|
||||
if (!newt) {
|
||||
logger.warn("Newt not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newt.siteId) {
|
||||
logger.warn("Newt has no site!"); // TODO: Maybe we create the site here?
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = inputSchema.safeParse(message.data);
|
||||
if (!parsed.success) {
|
||||
logger.error(
|
||||
"handleGetConfigMessage: Invalid input: " +
|
||||
fromError(parsed.error).toString()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const { publicKey, endpoint, listenPort } = message.data as Input;
|
||||
|
||||
const siteId = newt.siteId;
|
||||
|
||||
const [siteRes] = await db
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(eq(sites.siteId, siteId));
|
||||
|
||||
if (!siteRes) {
|
||||
logger.warn("handleGetConfigMessage: Site not found");
|
||||
return;
|
||||
}
|
||||
|
||||
let site: Site | undefined;
|
||||
if (!site) {
|
||||
const address = await getNextAvailableSubnet();
|
||||
|
||||
// create a new exit node
|
||||
const [updateRes] = await db
|
||||
.update(sites)
|
||||
.set({
|
||||
publicKey,
|
||||
endpoint,
|
||||
address,
|
||||
listenPort
|
||||
})
|
||||
.where(eq(sites.siteId, siteId))
|
||||
.returning();
|
||||
|
||||
site = updateRes;
|
||||
|
||||
logger.info(`Updated site ${siteId} with new WG Newt info`);
|
||||
} else {
|
||||
site = siteRes;
|
||||
}
|
||||
|
||||
if (!site) {
|
||||
logger.error("handleGetConfigMessage: Failed to update site");
|
||||
return;
|
||||
}
|
||||
|
||||
const clientsRes = await db
|
||||
.select()
|
||||
.from(olms)
|
||||
.where(eq(olms.siteId, siteId));
|
||||
|
||||
const peers = await Promise.all(
|
||||
clientsRes.map(async (client) => {
|
||||
return {
|
||||
publicKey: client.pubKey,
|
||||
allowedIps: "0.0.0.0/0"
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const configResponse = {
|
||||
listenPort: site.listenPort, // ?????
|
||||
// ipAddress: exitNode[0].address,
|
||||
peers
|
||||
};
|
||||
|
||||
logger.debug("Sending config: ", configResponse);
|
||||
|
||||
return {
|
||||
message: {
|
||||
type: "olm/wg/connect", // what to make the response type?
|
||||
data: {
|
||||
config: configResponse
|
||||
}
|
||||
},
|
||||
broadcast: false, // Send to all clients
|
||||
excludeSender: false // Include sender in broadcast
|
||||
};
|
||||
};
|
||||
|
||||
async function getNextAvailableSubnet(): Promise<string> {
|
||||
const existingAddresses = await db
|
||||
.select({
|
||||
address: sites.address
|
||||
})
|
||||
.from(sites)
|
||||
.where(isNotNull(sites.address));
|
||||
|
||||
const addresses = existingAddresses
|
||||
.map((a) => a.address)
|
||||
.filter((a) => a) as string[];
|
||||
|
||||
let subnet = findNextAvailableCidr(
|
||||
addresses,
|
||||
config.getRawConfig().wg_site.block_size,
|
||||
config.getRawConfig().wg_site.subnet_group
|
||||
);
|
||||
if (!subnet) {
|
||||
throw new Error("No available subnets remaining in space");
|
||||
}
|
||||
|
||||
// replace the last octet with 1
|
||||
subnet =
|
||||
subnet.split(".").slice(0, 3).join(".") +
|
||||
".1" +
|
||||
"/" +
|
||||
subnet.split("/")[1];
|
||||
return subnet;
|
||||
}
|
||||
93
server/routers/olm/handleOlmRegisterMessage.ts
Normal file
93
server/routers/olm/handleOlmRegisterMessage.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import db from "@server/db";
|
||||
import { MessageHandler } from "../ws";
|
||||
import {
|
||||
exitNodes,
|
||||
resources,
|
||||
sites,
|
||||
Target,
|
||||
targets
|
||||
} from "@server/db/schema";
|
||||
import { eq, and, sql } from "drizzle-orm";
|
||||
import { addPeer, deletePeer } from "../gerbil/peers";
|
||||
import logger from "@server/logger";
|
||||
|
||||
export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
const { message, client, sendToClient } = context;
|
||||
|
||||
const olm = client;
|
||||
|
||||
logger.info("Handling register message!");
|
||||
|
||||
if (!olm) {
|
||||
logger.warn("Olm not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!olm.siteId) {
|
||||
logger.warn("Olm has no site!"); // TODO: Maybe we create the site here?
|
||||
return;
|
||||
}
|
||||
|
||||
const siteId = olm.siteId;
|
||||
|
||||
const { publicKey } = message.data;
|
||||
if (!publicKey) {
|
||||
logger.warn("Public key not provided");
|
||||
return;
|
||||
}
|
||||
|
||||
const [site] = await db
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(eq(sites.siteId, siteId))
|
||||
.limit(1);
|
||||
|
||||
if (!site || !site.exitNodeId) {
|
||||
logger.warn("Site not found or does not have exit node");
|
||||
return;
|
||||
}
|
||||
|
||||
await db
|
||||
.update(sites)
|
||||
.set({
|
||||
pubKey: publicKey
|
||||
})
|
||||
.where(eq(sites.siteId, siteId))
|
||||
.returning();
|
||||
|
||||
const [exitNode] = await db
|
||||
.select()
|
||||
.from(exitNodes)
|
||||
.where(eq(exitNodes.exitNodeId, site.exitNodeId))
|
||||
.limit(1);
|
||||
|
||||
if (site.pubKey && site.pubKey !== publicKey) {
|
||||
logger.info("Public key mismatch. Deleting old peer...");
|
||||
await deletePeer(site.exitNodeId, site.pubKey);
|
||||
}
|
||||
|
||||
if (!site.subnet) {
|
||||
logger.warn("Site has no subnet");
|
||||
return;
|
||||
}
|
||||
|
||||
// add the peer to the exit node
|
||||
await addPeer(site.exitNodeId, {
|
||||
publicKey: publicKey,
|
||||
allowedIps: [site.subnet]
|
||||
});
|
||||
|
||||
return {
|
||||
message: {
|
||||
type: "olm/wg/connect",
|
||||
data: {
|
||||
endpoint: `${exitNode.endpoint}:${exitNode.listenPort}`,
|
||||
publicKey: exitNode.publicKey,
|
||||
serverIP: exitNode.address.split("/")[0],
|
||||
tunnelIP: site.subnet.split("/")[0]
|
||||
}
|
||||
},
|
||||
broadcast: false, // Send to all olms
|
||||
excludeSender: false // Include sender in broadcast
|
||||
};
|
||||
};
|
||||
1
server/routers/olm/index.ts
Normal file
1
server/routers/olm/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./pickOlmDefaults";
|
||||
128
server/routers/olm/pickOlmDefaults.ts
Normal file
128
server/routers/olm/pickOlmDefaults.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { olms, sites } from "@server/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { findNextAvailableCidr } from "@server/lib/ip";
|
||||
import { generateId } from "@server/auth/sessions/app";
|
||||
import config from "@server/lib/config";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
const getSiteSchema = z
|
||||
.object({
|
||||
siteId: z.number().int().positive()
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type PickClientDefaultsResponse = {
|
||||
siteId: number;
|
||||
address: string;
|
||||
publicKey: string;
|
||||
name: string;
|
||||
listenPort: number;
|
||||
endpoint: string;
|
||||
subnet: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
};
|
||||
|
||||
export async function pickOlmDefaults(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = getSiteSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { siteId } = parsedParams.data;
|
||||
|
||||
const [site] = await db
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(eq(sites.siteId, siteId));
|
||||
|
||||
if (!site) {
|
||||
return next(createHttpError(HttpCode.NOT_FOUND, "Site not found"));
|
||||
}
|
||||
|
||||
// make sure all the required fields are present
|
||||
if (
|
||||
!site.address ||
|
||||
!site.publicKey ||
|
||||
!site.listenPort ||
|
||||
!site.endpoint
|
||||
) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Site has no address")
|
||||
);
|
||||
}
|
||||
|
||||
const clientsQuery = await db
|
||||
.select({
|
||||
subnet: olms.subnet
|
||||
})
|
||||
.from(olms)
|
||||
.where(eq(olms.siteId, site.siteId));
|
||||
|
||||
let subnets = clientsQuery.map((client) => client.subnet);
|
||||
|
||||
// exclude the exit node address by replacing after the / with a site block size
|
||||
subnets.push(
|
||||
site.address.replace(
|
||||
/\/\d+$/,
|
||||
`/${config.getRawConfig().wg_site.block_size}`
|
||||
)
|
||||
);
|
||||
const newSubnet = findNextAvailableCidr(
|
||||
subnets,
|
||||
config.getRawConfig().wg_site.block_size,
|
||||
site.address
|
||||
);
|
||||
if (!newSubnet) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"No available subnets"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const clientId = generateId(15);
|
||||
const secret = generateId(48);
|
||||
|
||||
return response<PickClientDefaultsResponse>(res, {
|
||||
data: {
|
||||
siteId: site.siteId,
|
||||
address: site.address,
|
||||
publicKey: site.publicKey,
|
||||
name: site.name,
|
||||
listenPort: site.listenPort,
|
||||
endpoint: site.endpoint,
|
||||
subnet: newSubnet,
|
||||
clientId,
|
||||
clientSecret: secret
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Organization retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user