add basic gatewayt
This commit is contained in:
parent
3bc166bfb0
commit
a9162c245e
11 changed files with 185 additions and 9 deletions
|
@ -11,13 +11,16 @@
|
|||
"express": "^4.17.3",
|
||||
"express-validator": "^6.14.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"pg": "^8.7.3"
|
||||
"pg": "^8.7.3",
|
||||
"ws": "^8.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/jsonwebtoken": "^8.5.8",
|
||||
"@types/node": "^17.0.23",
|
||||
"@types/pg": "^8.6.5",
|
||||
"@types/ws": "^8.5.3",
|
||||
"typescript": "^4.6.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,3 +6,10 @@ export const errors = {
|
|||
FORBIDDEN_DUE_TO_MISSING_PERMISSIONS: { code: 6005, message: "Forbidden due to missing permission(s)" },
|
||||
GOT_NO_DATABASE_DATA: { code: 7001, message: "Unexpectedly got no data from database" }
|
||||
};
|
||||
|
||||
export const gatewayErrors = {
|
||||
BAD_PAYLOAD: { code: 4001, message: "Bad payload" },
|
||||
BAD_AUTH: { code: 4002, message: "Bad authentication" },
|
||||
AUTHENTICATION_TIMEOUT: { code: 4003, message: "Authentication timeout" },
|
||||
NO_PING: { code: 4004, message: "No ping" },
|
||||
};
|
||||
|
|
5
src/gateway/gatewaypayloadtype.ts
Normal file
5
src/gateway/gatewaypayloadtype.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export enum GatewayPayloadType {
|
||||
Hello = 0,
|
||||
Authenticate,
|
||||
Ready
|
||||
}
|
123
src/gateway/index.ts
Normal file
123
src/gateway/index.ts
Normal file
|
@ -0,0 +1,123 @@
|
|||
import { Server } from "node:http";
|
||||
import { performance } from "node:perf_hooks";
|
||||
import WebSocket, { WebSocketServer } from "ws";
|
||||
import { decodeTokenOrNull } from "../auth";
|
||||
import { gatewayErrors } from "../errors";
|
||||
import { GatewayPayload } from "../types/gatewaypayload";
|
||||
import { GatewayPayloadType } from "./gatewaypayloadtype";
|
||||
|
||||
const GATEWAY_BATCH_INTERVAL = 25000 || process.env.GATEWAY_BATCH_INTERVAL;
|
||||
const GATEWAY_PING_INTERVAL = 20000 || process.env.GATEWAY_PING_INTERVAL;
|
||||
|
||||
function closeWithError(ws: WebSocket, { code, message }: { code: number, message: string }) {
|
||||
return ws.close(1000, `(${code}) ${message}`);
|
||||
}
|
||||
|
||||
function closeWithBadPayload(ws: WebSocket, hint: string) {
|
||||
return ws.close(gatewayErrors.BAD_PAYLOAD.code, `${gatewayErrors.BAD_PAYLOAD.message}: ${hint}`);
|
||||
}
|
||||
|
||||
function parseJsonOrNull(payload: string): any {
|
||||
try {
|
||||
return JSON.parse(payload);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// The function below ensures `payload` is of the GatewayPayload
|
||||
// interface payload. If it does not match, null is returned.
|
||||
function ensureFormattedGatewayPayload(payload: any): GatewayPayload | null {
|
||||
if (!payload) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let foundT = false;
|
||||
let foundD = false;
|
||||
for (const [k, v] of Object.entries(payload)) {
|
||||
if (k === "t" && typeof v === "number") {
|
||||
foundT = true;
|
||||
} else if (k === "d") {
|
||||
foundD = true;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (!foundT || !foundD) {
|
||||
return null;
|
||||
}
|
||||
const asPayload = payload as GatewayPayload;
|
||||
return asPayload;
|
||||
}
|
||||
|
||||
function sendPayload(ws: WebSocket, payload: GatewayPayload) {
|
||||
ws.send(JSON.stringify(payload));
|
||||
}
|
||||
|
||||
export default function(server: Server) {
|
||||
const wss = new WebSocketServer({ server });
|
||||
|
||||
setInterval(() => {
|
||||
wss.clients.forEach((e) => {
|
||||
const now = performance.now();
|
||||
if (e.state && (now - e.state.lastAliveCheck) >= GATEWAY_PING_INTERVAL) {
|
||||
if (!e.state.ready) {
|
||||
return closeWithError(e, gatewayErrors.AUTHENTICATION_TIMEOUT);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, GATEWAY_BATCH_INTERVAL);
|
||||
|
||||
wss.on("connection", (ws) => {
|
||||
ws.state = {
|
||||
user: undefined,
|
||||
alive: false,
|
||||
ready: false,
|
||||
lastAliveCheck: performance.now()
|
||||
};
|
||||
|
||||
sendPayload(ws, {
|
||||
t: GatewayPayloadType.Hello,
|
||||
d: {
|
||||
pingInterval: GATEWAY_PING_INTERVAL
|
||||
}
|
||||
});
|
||||
|
||||
ws.on("message", async (rawData, isBinary) => {
|
||||
if (isBinary) {
|
||||
return closeWithBadPayload(ws, "Binary messages are not supported");
|
||||
}
|
||||
|
||||
const payload = ensureFormattedGatewayPayload(parseJsonOrNull(rawData.toString()));
|
||||
if (!payload) {
|
||||
return closeWithBadPayload(ws, "Invalid JSON or message does not match schema");
|
||||
}
|
||||
|
||||
switch (payload.t) {
|
||||
case GatewayPayloadType.Authenticate: {
|
||||
const token = payload.d;
|
||||
if (typeof token !== "string") {
|
||||
return closeWithBadPayload(ws, "d: expected string");
|
||||
}
|
||||
const user = await decodeTokenOrNull(token);
|
||||
if (!user) {
|
||||
return closeWithError(ws, gatewayErrors.BAD_AUTH);
|
||||
}
|
||||
ws.state.user = user;
|
||||
ws.state.ready = true;
|
||||
|
||||
sendPayload(ws, {
|
||||
t: GatewayPayloadType.Ready,
|
||||
d: {
|
||||
user: ws.state.user,
|
||||
}
|
||||
})
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return closeWithBadPayload(ws, "t: unknown type");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
|
@ -1,15 +1,20 @@
|
|||
import "dotenv/config";
|
||||
import express from "express";
|
||||
import { createServer } from "node:http";
|
||||
import databaseInit from "./database/init";
|
||||
import gateway from "./gateway";
|
||||
import server from "./server";
|
||||
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
const app = express();
|
||||
server(app);
|
||||
const httpServer = createServer(app);
|
||||
gateway(httpServer);
|
||||
|
||||
|
||||
function serve() {
|
||||
app.listen(port, () => console.log(`listening on port ${port}`));
|
||||
httpServer.listen(port, () => console.log(`listening on port ${port}`));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
|
|
|
@ -3,6 +3,8 @@ import usersRouter from "./routes/api/v1/users";
|
|||
import channelsRouter from "./routes/api/v1/channels";
|
||||
|
||||
export default function(app: Application) {
|
||||
app.get("/", (req, res) => res.send("hello!"));
|
||||
|
||||
app.use(json());
|
||||
app.use("/api/v1/users", usersRouter);
|
||||
app.use("/api/v1/channels", channelsRouter);
|
||||
|
|
6
src/types/gatewayclientstate.d.ts
vendored
Normal file
6
src/types/gatewayclientstate.d.ts
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
interface GatewayClientState {
|
||||
user?: User;
|
||||
ready: boolean,
|
||||
alive: boolean,
|
||||
lastAliveCheck: number,
|
||||
}
|
6
src/types/gatewaypayload.d.ts
vendored
Normal file
6
src/types/gatewaypayload.d.ts
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { GatewayPayloadType } from "../gateway/gatewaypayloadtype";
|
||||
|
||||
declare interface GatewayPayload {
|
||||
t: GatewayPayloadType;
|
||||
d: any;
|
||||
}
|
7
src/types/ws.d.ts
vendored
Normal file
7
src/types/ws.d.ts
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
import ws from 'ws';
|
||||
|
||||
declare module 'ws' {
|
||||
export interface WebSocket extends ws {
|
||||
state: GatewayClientState;
|
||||
}
|
||||
}
|
12
test.rest
12
test.rest
|
@ -19,13 +19,13 @@ content-type: application/json
|
|||
###
|
||||
|
||||
GET http://localhost:3000/api/v1/users/self HTTP/1.1
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidHlwZSI6MSwiaWF0IjoxNjQ5MTg3MDc5LCJleHAiOjE2NDkzNTk4Nzl9.tVzJWnBP7IFhA88XRwByKGXQ4cihWdJSoxUkrWHkIVU
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NTI0NDA1LCJleHAiOjE2NDk2OTcyMDV9.4nIDs0K8MCT18GsdlKdicT_GK2KbEqi_P7cND3_aZvE
|
||||
|
||||
###
|
||||
|
||||
POST http://localhost:3000/api/v1/channels HTTP/1.1
|
||||
content-type: application/json
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidHlwZSI6MSwiaWF0IjoxNjQ5MTg3MDc5LCJleHAiOjE2NDkzNTk4Nzl9.tVzJWnBP7IFhA88XRwByKGXQ4cihWdJSoxUkrWHkIVU
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NTI0NDA1LCJleHAiOjE2NDk2OTcyMDV9.4nIDs0K8MCT18GsdlKdicT_GK2KbEqi_P7cND3_aZvE
|
||||
|
||||
{
|
||||
"name": "another channel"
|
||||
|
@ -35,7 +35,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidHlwZSI6M
|
|||
|
||||
PUT http://localhost:3000/api/v1/channels/2 HTTP/1.1
|
||||
content-type: application/json
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidHlwZSI6MSwiaWF0IjoxNjQ5MTg3MDc5LCJleHAiOjE2NDkzNTk4Nzl9.tVzJWnBP7IFhA88XRwByKGXQ4cihWdJSoxUkrWHkIVU
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NTI0NDA1LCJleHAiOjE2NDk2OTcyMDV9.4nIDs0K8MCT18GsdlKdicT_GK2KbEqi_P7cND3_aZvE
|
||||
#Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5MjU5NDUwLCJleHAiOjE2NDk0MzIyNTB9.JmF9NujFZnln7A-ynNpeyayGBqmR5poAyACYV6RnSQY
|
||||
|
||||
{
|
||||
|
@ -45,15 +45,15 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidHlwZSI6M
|
|||
###
|
||||
|
||||
DELETE http://localhost:3000/api/v1/channels/1 HTTP/1.1
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidHlwZSI6MSwiaWF0IjoxNjQ5MTg3MDc5LCJleHAiOjE2NDkzNTk4Nzl9.tVzJWnBP7IFhA88XRwByKGXQ4cihWdJSoxUkrWHkIVU
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NTI0NDA1LCJleHAiOjE2NDk2OTcyMDV9.4nIDs0K8MCT18GsdlKdicT_GK2KbEqi_P7cND3_aZvE
|
||||
#Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5MjU5NDUwLCJleHAiOjE2NDk0MzIyNTB9.JmF9NujFZnln7A-ynNpeyayGBqmR5poAyACYV6RnSQY
|
||||
|
||||
###
|
||||
|
||||
GET http://localhost:3000/api/v1/channels/1 HTTP/1.1
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidHlwZSI6MSwiaWF0IjoxNjQ5MTg3MDc5LCJleHAiOjE2NDkzNTk4Nzl9.tVzJWnBP7IFhA88XRwByKGXQ4cihWdJSoxUkrWHkIVU
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NTI0NDA1LCJleHAiOjE2NDk2OTcyMDV9.4nIDs0K8MCT18GsdlKdicT_GK2KbEqi_P7cND3_aZvE
|
||||
|
||||
###
|
||||
|
||||
GET http://localhost:3000/api/v1/channels HTTP/1.1
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidHlwZSI6MSwiaWF0IjoxNjQ5MTg3MDc5LCJleHAiOjE2NDkzNTk4Nzl9.tVzJWnBP7IFhA88XRwByKGXQ4cihWdJSoxUkrWHkIVU
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NTI0NDA1LCJleHAiOjE2NDk2OTcyMDV9.4nIDs0K8MCT18GsdlKdicT_GK2KbEqi_P7cND3_aZvE
|
||||
|
|
14
yarn.lock
14
yarn.lock
|
@ -70,7 +70,7 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
|
||||
integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
|
||||
|
||||
"@types/node@*":
|
||||
"@types/node@*", "@types/node@^17.0.23":
|
||||
version "17.0.23"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.23.tgz#3b41a6e643589ac6442bdbd7a4a3ded62f33f7da"
|
||||
integrity sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==
|
||||
|
@ -102,6 +102,13 @@
|
|||
"@types/mime" "^1"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/ws@^8.5.3":
|
||||
version "8.5.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d"
|
||||
integrity sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
abbrev@1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
||||
|
@ -1010,6 +1017,11 @@ wrappy@1:
|
|||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
|
||||
|
||||
ws@^8.5.0:
|
||||
version "8.5.0"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f"
|
||||
integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==
|
||||
|
||||
xtend@^4.0.0:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||
|
|
Loading…
Reference in a new issue