mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-19 16:59:45 +02:00
Seperate servers
This commit is contained in:
parent
ef7723561e
commit
37f51bec9b
8 changed files with 214 additions and 189 deletions
56
server/apiServer.ts
Normal file
56
server/apiServer.ts
Normal 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;
|
||||||
|
}
|
|
@ -21,6 +21,7 @@ const environmentSchema = z.object({
|
||||||
server: z.object({
|
server: z.object({
|
||||||
external_port: portSchema,
|
external_port: portSchema,
|
||||||
internal_port: portSchema,
|
internal_port: portSchema,
|
||||||
|
next_port: portSchema,
|
||||||
internal_hostname: z.string(),
|
internal_hostname: z.string(),
|
||||||
secure_cookies: z.boolean(),
|
secure_cookies: z.boolean(),
|
||||||
signup_secret: z.string().optional(),
|
signup_secret: z.string().optional(),
|
||||||
|
|
108
server/index.ts
108
server/index.ts
|
@ -1,100 +1,26 @@
|
||||||
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 { 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();
|
||||||
|
|
||||||
const app = next({ dev });
|
// Start all servers
|
||||||
const handle = app.getRequestHandler();
|
const apiServer = createApiServer();
|
||||||
|
const nextServer = await createNextServer();
|
||||||
|
const internalServer = createInternalServer();
|
||||||
|
|
||||||
const externalPort = config.server.external_port;
|
return {
|
||||||
const internalPort = config.server.internal_port;
|
apiServer,
|
||||||
|
nextServer,
|
||||||
app.prepare().then(() => {
|
internalServer
|
||||||
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`;
|
// Types
|
||||||
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 {
|
declare global {
|
||||||
// TODO: eventually make seperate types that extend express.Request
|
|
||||||
namespace Express {
|
namespace Express {
|
||||||
interface Request {
|
interface Request {
|
||||||
user?: User;
|
user?: User;
|
||||||
|
@ -105,3 +31,5 @@ declare global {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
startServers().catch(console.error);
|
||||||
|
|
32
server/internalServer.ts
Normal file
32
server/internalServer.ts
Normal 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
29
server/nextServer.ts
Normal 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;
|
||||||
|
}
|
|
@ -17,7 +17,6 @@ interface WebSocketRequest extends IncomingMessage {
|
||||||
|
|
||||||
interface AuthenticatedWebSocket extends WebSocket {
|
interface AuthenticatedWebSocket extends WebSocket {
|
||||||
newt?: Newt;
|
newt?: Newt;
|
||||||
isAlive?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TokenPayload {
|
interface TokenPayload {
|
||||||
|
@ -124,77 +123,23 @@ const verifyToken = async (token: string): Promise<TokenPayload | null> => {
|
||||||
|
|
||||||
return { newt: existingNewt[0], session };
|
return { newt: existingNewt[0], session };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Token verification failed:", error);
|
logger.error("Token verification failed:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Router endpoint (unchanged)
|
const setupConnection = (ws: AuthenticatedWebSocket, newt: Newt): void => {
|
||||||
router.get("/ws", (req: Request, res: Response) => {
|
logger.info("Establishing websocket connection");
|
||||||
res.status(200).send("WebSocket endpoint");
|
|
||||||
});
|
|
||||||
|
|
||||||
// WebSocket upgrade handler
|
if (!newt) {
|
||||||
const handleWSUpgrade = (server: HttpServer): void => {
|
logger.error("Connection attempt without newt");
|
||||||
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");
|
|
||||||
return ws.terminate();
|
return ws.terminate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ws.newt = newt;
|
||||||
|
|
||||||
// Add client to tracking
|
// Add client to tracking
|
||||||
addClient(newtId, ws);
|
addClient(newt.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;
|
|
||||||
});
|
|
||||||
|
|
||||||
ws.on("message", async (data) => {
|
ws.on("message", async (data) => {
|
||||||
try {
|
try {
|
||||||
|
@ -226,7 +171,7 @@ wss.on("connection", (ws: AuthenticatedWebSocket, request: WebSocketRequest) =>
|
||||||
if (response) {
|
if (response) {
|
||||||
if (response.broadcast) {
|
if (response.broadcast) {
|
||||||
// Broadcast to all clients except sender if specified
|
// 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) {
|
} else if (response.targetNewtId) {
|
||||||
// Send to specific client if targetNewtId is provided
|
// Send to specific client if targetNewtId is provided
|
||||||
sendToClient(response.targetNewtId, response.message);
|
sendToClient(response.targetNewtId, response.message);
|
||||||
|
@ -237,7 +182,7 @@ wss.on("connection", (ws: AuthenticatedWebSocket, request: WebSocketRequest) =>
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Message handling error:", error);
|
logger.error("Message handling error:", error);
|
||||||
ws.send(JSON.stringify({
|
ws.send(JSON.stringify({
|
||||||
type: "error",
|
type: "error",
|
||||||
data: {
|
data: {
|
||||||
|
@ -249,16 +194,56 @@ wss.on("connection", (ws: AuthenticatedWebSocket, request: WebSocketRequest) =>
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on("close", () => {
|
ws.on("close", () => {
|
||||||
clearInterval(pingInterval);
|
removeClient(newt.newtId, ws);
|
||||||
removeClient(newtId, ws);
|
logger.info(`Client disconnected - Newt ID: ${newt.newtId}`);
|
||||||
logger.info(`Client disconnected - Newt ID: ${newtId}`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on("error", (error: Error) => {
|
ws.on("error", (error: Error) => {
|
||||||
console.error(`WebSocket error for Newt ID ${newtId}:`, error);
|
logger.error(`WebSocket error for Newt ID ${newt.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 {
|
export {
|
||||||
router,
|
router,
|
||||||
handleWSUpgrade,
|
handleWSUpgrade,
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -174,7 +174,13 @@ Endpoint = ${siteDefaults.endpoint}:${siteDefaults.listenPort}
|
||||||
PersistentKeepalive = 5`
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue