Seperate servers

This commit is contained in:
Owen Schwartz 2024-12-07 22:07:13 -05:00
parent ef7723561e
commit 37f51bec9b
No known key found for this signature in database
GPG key ID: 8271FDFFD9E0CCBD
8 changed files with 214 additions and 189 deletions

56
server/apiServer.ts Normal file
View file

@ -0,0 +1,56 @@
import express, { Request, Response } from "express";
import cors from "cors";
import cookieParser from "cookie-parser";
import config from "@server/config";
import logger from "@server/logger";
import { errorHandlerMiddleware, notFoundMiddleware, rateLimitMiddleware } from "@server/middlewares";
import { authenticated, unauthenticated } from "@server/routers/external";
import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws";
import { logIncomingMiddleware } from "./middlewares/logIncoming";
const dev = process.env.ENVIRONMENT !== "prod";
const externalPort = config.server.external_port;
export function createApiServer() {
const apiServer = express();
// Middleware setup
apiServer.set("trust proxy", 1);
apiServer.use(cors());
apiServer.use(cookieParser());
apiServer.use(express.json());
if (!dev) {
apiServer.use(
rateLimitMiddleware({
windowMin: config.rate_limit.window_minutes,
max: config.rate_limit.max_requests,
type: "IP_ONLY",
})
);
}
// API routes
const prefix = `/api/v1`;
apiServer.use(logIncomingMiddleware);
apiServer.use(prefix, unauthenticated);
apiServer.use(prefix, authenticated);
// WebSocket routes
apiServer.use(`/ws`, wsRouter);
// Error handling
apiServer.use(notFoundMiddleware);
apiServer.use(errorHandlerMiddleware);
// Create HTTP server
const httpServer = apiServer.listen(externalPort, (err?: any) => {
if (err) throw err;
logger.info(`API server is running on http://localhost:${externalPort}`);
});
// Handle WebSocket upgrades
handleWSUpgrade(httpServer);
return httpServer;
}

View file

@ -21,6 +21,7 @@ const environmentSchema = z.object({
server: z.object({
external_port: portSchema,
internal_port: portSchema,
next_port: portSchema,
internal_hostname: z.string(),
secure_cookies: z.boolean(),
signup_secret: z.string().optional(),

View file

@ -1,107 +1,35 @@
import config from "@server/config";
import express, { Request, Response } from "express";
import next from "next";
import { parse } from "url";
import logger from "@server/logger";
import helmet from "helmet";
import cors from "cors";
import {
errorHandlerMiddleware,
notFoundMiddleware,
rateLimitMiddleware,
} from "@server/middlewares";
import internal from "@server/routers/internal";
import { authenticated, unauthenticated } from "@server/routers/external";
import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws";
import cookieParser from "cookie-parser";
import { User, UserOrg } from "@server/db/schema";
import { ensureActions } from "./db/ensureActions";
import { logIncomingMiddleware } from "./middlewares/logIncoming";
import { createApiServer } from "./apiServer";
import { createNextServer } from "./nextServer";
import { createInternalServer } from "./internalServer";
import { User, UserOrg } from "./db/schema";
const dev = process.env.ENVIRONMENT !== "prod";
async function startServers() {
await ensureActions();
// Start all servers
const apiServer = createApiServer();
const nextServer = await createNextServer();
const internalServer = createInternalServer();
const app = next({ dev });
const handle = app.getRequestHandler();
const externalPort = config.server.external_port;
const internalPort = config.server.internal_port;
app.prepare().then(() => {
ensureActions(); // This loads the actions into the database
// External server
const externalServer = express();
externalServer.set("trust proxy", 1);
// externalServer.use(helmet()); // Disabled because causes issues with Next.js
externalServer.use(cors());
externalServer.use(cookieParser());
externalServer.use(express.json());
if (!dev) {
externalServer.use(
rateLimitMiddleware({
windowMin: config.rate_limit.window_minutes,
max: config.rate_limit.max_requests,
type: "IP_ONLY",
})
);
}
const prefix = `/api/v1`;
externalServer.use(logIncomingMiddleware);
externalServer.use(prefix, unauthenticated);
externalServer.use(prefix, authenticated);
// externalServer.use(`${prefix}/ws`, wsRouter);
externalServer.use(notFoundMiddleware);
// We are using NEXT from here on
externalServer.all("*", (req: Request, res: Response) => {
const parsedUrl = parse(req.url!, true);
handle(req, res, parsedUrl);
});
const httpServer = externalServer.listen(externalPort, (err?: any) => {
if (err) throw err;
logger.info(
`Main server is running on http://localhost:${externalPort}`
);
});
// handleWSUpgrade(httpServer);
externalServer.use(errorHandlerMiddleware);
// Internal server
const internalServer = express();
internalServer.use(helmet());
internalServer.use(cors());
internalServer.use(cookieParser());
internalServer.use(express.json());
internalServer.use(prefix, internal);
internalServer.listen(internalPort, (err?: any) => {
if (err) throw err;
logger.info(
`Internal server is running on http://localhost:${internalPort}`
);
});
internalServer.use(notFoundMiddleware);
internalServer.use(errorHandlerMiddleware);
});
declare global {
// TODO: eventually make seperate types that extend express.Request
namespace Express {
interface Request {
user?: User;
userOrg?: UserOrg;
userOrgRoleId?: number;
userOrgId?: string;
userOrgIds?: string[];
}
}
return {
apiServer,
nextServer,
internalServer
};
}
// Types
declare global {
namespace Express {
interface Request {
user?: User;
userOrg?: UserOrg;
userOrgRoleId?: number;
userOrgId?: string;
userOrgIds?: string[];
}
}
}
startServers().catch(console.error);

32
server/internalServer.ts Normal file
View file

@ -0,0 +1,32 @@
import express from "express";
import helmet from "helmet";
import cors from "cors";
import cookieParser from "cookie-parser";
import config from "@server/config";
import logger from "@server/logger";
import { errorHandlerMiddleware, notFoundMiddleware } from "@server/middlewares";
import internal from "@server/routers/internal";
const internalPort = config.server.internal_port;
export function createInternalServer() {
const internalServer = express();
internalServer.use(helmet());
internalServer.use(cors());
internalServer.use(cookieParser());
internalServer.use(express.json());
const prefix = `/api/v1`;
internalServer.use(prefix, internal);
internalServer.use(notFoundMiddleware);
internalServer.use(errorHandlerMiddleware);
internalServer.listen(internalPort, (err?: any) => {
if (err) throw err;
logger.info(`Internal server is running on http://localhost:${internalPort}`);
});
return internalServer;
}

29
server/nextServer.ts Normal file
View file

@ -0,0 +1,29 @@
import next from "next";
import express from "express";
import { parse } from "url";
import logger from "@server/logger";
import config from "@server/config";
const nextPort = config.server.next_port;
export async function createNextServer() {
// const app = next({ dev });
const app = next({ dev: process.env.ENVIRONMENT !== "prod" });
const handle = app.getRequestHandler();
await app.prepare();
const nextServer = express();
nextServer.all("*", (req, res) => {
const parsedUrl = parse(req.url!, true);
return handle(req, res, parsedUrl);
});
nextServer.listen(nextPort, (err?: any) => {
if (err) throw err;
logger.info(`Next.js server is running on http://localhost:${nextPort}`);
});
return nextServer;
}

View file

@ -17,7 +17,6 @@ interface WebSocketRequest extends IncomingMessage {
interface AuthenticatedWebSocket extends WebSocket {
newt?: Newt;
isAlive?: boolean;
}
interface TokenPayload {
@ -124,77 +123,23 @@ const verifyToken = async (token: string): Promise<TokenPayload | null> => {
return { newt: existingNewt[0], session };
} catch (error) {
console.error("Token verification failed:", error);
logger.error("Token verification failed:", error);
return null;
}
};
// Router endpoint (unchanged)
router.get("/ws", (req: Request, res: Response) => {
res.status(200).send("WebSocket endpoint");
});
const setupConnection = (ws: AuthenticatedWebSocket, newt: Newt): void => {
logger.info("Establishing websocket connection");
// WebSocket upgrade handler
const handleWSUpgrade = (server: HttpServer): void => {
server.on("upgrade", async (request: WebSocketRequest, socket: Socket, head: Buffer) => {
try {
const token = request.url?.includes("?")
? new URLSearchParams(request.url.split("?")[1]).get("token") || ""
: request.headers["sec-websocket-protocol"];
if (!token) {
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
socket.destroy();
return;
}
const tokenPayload = await verifyToken(token);
if (!tokenPayload) {
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
socket.destroy();
return;
}
request.token = token;
wss.handleUpgrade(request, socket, head, (ws: AuthenticatedWebSocket) => {
ws.newt = tokenPayload.newt;
ws.isAlive = true;
wss.emit("connection", ws, request);
});
} catch (error) {
console.error("Upgrade error:", error);
socket.write("HTTP/1.1 500 Internal Server Error\r\n\r\n");
socket.destroy();
}
});
};
// WebSocket connection handler
wss.on("connection", (ws: AuthenticatedWebSocket, request: WebSocketRequest) => {
const newtId = ws.newt?.newtId;
if (!newtId) {
console.error("Connection attempt without newt ID");
if (!newt) {
logger.error("Connection attempt without newt");
return ws.terminate();
}
ws.newt = newt;
// Add client to tracking
addClient(newtId, ws);
// Set up ping-pong for connection health check
const pingInterval = setInterval(() => {
if (ws.isAlive === false) {
clearInterval(pingInterval);
removeClient(newtId, ws);
return ws.terminate();
}
ws.isAlive = false;
ws.ping();
}, 30000);
ws.on("pong", () => {
ws.isAlive = true;
});
addClient(newt.newtId, ws);
ws.on("message", async (data) => {
try {
@ -226,7 +171,7 @@ wss.on("connection", (ws: AuthenticatedWebSocket, request: WebSocketRequest) =>
if (response) {
if (response.broadcast) {
// Broadcast to all clients except sender if specified
broadcastToAllExcept(response.message, response.excludeSender ? newtId : undefined);
broadcastToAllExcept(response.message, response.excludeSender ? newt.newtId : undefined);
} else if (response.targetNewtId) {
// Send to specific client if targetNewtId is provided
sendToClient(response.targetNewtId, response.message);
@ -235,9 +180,9 @@ wss.on("connection", (ws: AuthenticatedWebSocket, request: WebSocketRequest) =>
ws.send(JSON.stringify(response.message));
}
}
} catch (error) {
console.error("Message handling error:", error);
logger.error("Message handling error:", error);
ws.send(JSON.stringify({
type: "error",
data: {
@ -247,18 +192,58 @@ wss.on("connection", (ws: AuthenticatedWebSocket, request: WebSocketRequest) =>
}));
}
});
ws.on("close", () => {
clearInterval(pingInterval);
removeClient(newtId, ws);
logger.info(`Client disconnected - Newt ID: ${newtId}`);
removeClient(newt.newtId, ws);
logger.info(`Client disconnected - Newt ID: ${newt.newtId}`);
});
ws.on("error", (error: Error) => {
logger.error(`WebSocket error for Newt ID ${newt.newtId}:`, error);
});
ws.on("error", (error: Error) => {
console.error(`WebSocket error for Newt ID ${newtId}:`, error);
});
logger.info(`WebSocket connection established - Newt ID: ${newt.newtId}`);
};
// Router endpoint (unchanged)
router.get("/ws", (req: Request, res: Response) => {
res.status(200).send("WebSocket endpoint");
});
// WebSocket upgrade handler
const handleWSUpgrade = (server: HttpServer): void => {
server.on("upgrade", async (request: WebSocketRequest, socket: Socket, head: Buffer) => {
try {
const token = request.url?.includes("?")
? new URLSearchParams(request.url.split("?")[1]).get("token") || ""
: request.headers["sec-websocket-protocol"];
if (!token) {
logger.warn("Unauthorized connection attempt: no token...");
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
socket.destroy();
return;
}
const tokenPayload = await verifyToken(token);
if (!tokenPayload) {
logger.warn("Unauthorized connection attempt: invalid token...");
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
socket.destroy();
return;
}
wss.handleUpgrade(request, socket, head, (ws: AuthenticatedWebSocket) => {
setupConnection(ws, tokenPayload.newt);
});
} catch (error) {
logger.error("WebSocket upgrade error:", error);
socket.write("HTTP/1.1 500 Internal Server Error\r\n\r\n");
socket.destroy();
}
});
};
export {
router,
handleWSUpgrade,

View file

@ -1,12 +0,0 @@
"use client";
export function NewtConfig() {
const config = `curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh`;
return (
<pre className="mt-2 w-full rounded-md bg-slate-950 p-4 overflow-x-auto">
<code className="text-white whitespace-pre-wrap">{config}</code>
</pre>
);
}

View file

@ -174,7 +174,13 @@ Endpoint = ${siteDefaults.endpoint}:${siteDefaults.listenPort}
PersistentKeepalive = 5`
: "";
const newtConfig = `newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret}`;
// am I at http or https?
let proto = "http:";
if (typeof window !== "undefined") {
proto = window.location.protocol;
}
const newtConfig = `newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${proto}//${siteDefaults?.endpoint}`;
return (
<>