add basic gatewayt

This commit is contained in:
hippoz 2022-04-10 01:22:07 +03:00
parent 3bc166bfb0
commit a9162c245e
No known key found for this signature in database
GPG key ID: 7C52899193467641
11 changed files with 185 additions and 9 deletions

View file

@ -11,13 +11,16 @@
"express": "^4.17.3", "express": "^4.17.3",
"express-validator": "^6.14.0", "express-validator": "^6.14.0",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"pg": "^8.7.3" "pg": "^8.7.3",
"ws": "^8.5.0"
}, },
"devDependencies": { "devDependencies": {
"@types/bcrypt": "^5.0.0", "@types/bcrypt": "^5.0.0",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/jsonwebtoken": "^8.5.8", "@types/jsonwebtoken": "^8.5.8",
"@types/node": "^17.0.23",
"@types/pg": "^8.6.5", "@types/pg": "^8.6.5",
"@types/ws": "^8.5.3",
"typescript": "^4.6.3" "typescript": "^4.6.3"
} }
} }

View file

@ -6,3 +6,10 @@ export const errors = {
FORBIDDEN_DUE_TO_MISSING_PERMISSIONS: { code: 6005, message: "Forbidden due to missing permission(s)" }, 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" } 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" },
};

View file

@ -0,0 +1,5 @@
export enum GatewayPayloadType {
Hello = 0,
Authenticate,
Ready
}

123
src/gateway/index.ts Normal file
View 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");
}
}
});
});
};

View file

@ -1,15 +1,20 @@
import "dotenv/config"; import "dotenv/config";
import express from "express"; import express from "express";
import { createServer } from "node:http";
import databaseInit from "./database/init"; import databaseInit from "./database/init";
import gateway from "./gateway";
import server from "./server"; import server from "./server";
const port = process.env.PORT || 3000; const port = process.env.PORT || 3000;
const app = express(); const app = express();
server(app); server(app);
const httpServer = createServer(app);
gateway(httpServer);
function serve() { function serve() {
app.listen(port, () => console.log(`listening on port ${port}`)); httpServer.listen(port, () => console.log(`listening on port ${port}`));
} }
async function main() { async function main() {

View file

@ -3,6 +3,8 @@ import usersRouter from "./routes/api/v1/users";
import channelsRouter from "./routes/api/v1/channels"; import channelsRouter from "./routes/api/v1/channels";
export default function(app: Application) { export default function(app: Application) {
app.get("/", (req, res) => res.send("hello!"));
app.use(json()); app.use(json());
app.use("/api/v1/users", usersRouter); app.use("/api/v1/users", usersRouter);
app.use("/api/v1/channels", channelsRouter); app.use("/api/v1/channels", channelsRouter);

6
src/types/gatewayclientstate.d.ts vendored Normal file
View file

@ -0,0 +1,6 @@
interface GatewayClientState {
user?: User;
ready: boolean,
alive: boolean,
lastAliveCheck: number,
}

6
src/types/gatewaypayload.d.ts vendored Normal file
View 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
View file

@ -0,0 +1,7 @@
import ws from 'ws';
declare module 'ws' {
export interface WebSocket extends ws {
state: GatewayClientState;
}
}

View file

@ -19,13 +19,13 @@ content-type: application/json
### ###
GET http://localhost:3000/api/v1/users/self HTTP/1.1 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 POST http://localhost:3000/api/v1/channels HTTP/1.1
content-type: application/json content-type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidHlwZSI6MSwiaWF0IjoxNjQ5MTg3MDc5LCJleHAiOjE2NDkzNTk4Nzl9.tVzJWnBP7IFhA88XRwByKGXQ4cihWdJSoxUkrWHkIVU Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NTI0NDA1LCJleHAiOjE2NDk2OTcyMDV9.4nIDs0K8MCT18GsdlKdicT_GK2KbEqi_P7cND3_aZvE
{ {
"name": "another channel" "name": "another channel"
@ -35,7 +35,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidHlwZSI6M
PUT http://localhost:3000/api/v1/channels/2 HTTP/1.1 PUT http://localhost:3000/api/v1/channels/2 HTTP/1.1
content-type: application/json content-type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidHlwZSI6MSwiaWF0IjoxNjQ5MTg3MDc5LCJleHAiOjE2NDkzNTk4Nzl9.tVzJWnBP7IFhA88XRwByKGXQ4cihWdJSoxUkrWHkIVU Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NTI0NDA1LCJleHAiOjE2NDk2OTcyMDV9.4nIDs0K8MCT18GsdlKdicT_GK2KbEqi_P7cND3_aZvE
#Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5MjU5NDUwLCJleHAiOjE2NDk0MzIyNTB9.JmF9NujFZnln7A-ynNpeyayGBqmR5poAyACYV6RnSQY #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 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 #Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5MjU5NDUwLCJleHAiOjE2NDk0MzIyNTB9.JmF9NujFZnln7A-ynNpeyayGBqmR5poAyACYV6RnSQY
### ###
GET http://localhost:3000/api/v1/channels/1 HTTP/1.1 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 GET http://localhost:3000/api/v1/channels HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidHlwZSI6MSwiaWF0IjoxNjQ5MTg3MDc5LCJleHAiOjE2NDkzNTk4Nzl9.tVzJWnBP7IFhA88XRwByKGXQ4cihWdJSoxUkrWHkIVU Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NTI0NDA1LCJleHAiOjE2NDk2OTcyMDV9.4nIDs0K8MCT18GsdlKdicT_GK2KbEqi_P7cND3_aZvE

View file

@ -70,7 +70,7 @@
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
"@types/node@*": "@types/node@*", "@types/node@^17.0.23":
version "17.0.23" version "17.0.23"
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.23.tgz#3b41a6e643589ac6442bdbd7a4a3ded62f33f7da" resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.23.tgz#3b41a6e643589ac6442bdbd7a4a3ded62f33f7da"
integrity sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw== integrity sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==
@ -102,6 +102,13 @@
"@types/mime" "^1" "@types/mime" "^1"
"@types/node" "*" "@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: abbrev@1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" 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" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= 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: xtend@^4.0.0:
version "4.0.2" version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"