diff --git a/package-lock.json b/package-lock.json index 4b7093f..d04ec47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "cors": "^2.8.5", "dotenv": "^16.0.3", "express": "^4.18.2", + "express-basic-auth": "^1.2.1", "express-validator": "^6.15.0", "kysely": "^0.23.4", "pg": "^8.9.0", @@ -3469,6 +3470,22 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/body-parser": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", @@ -4634,6 +4651,14 @@ "node": ">= 0.10.0" } }, + "node_modules/express-basic-auth": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.2.1.tgz", + "integrity": "sha512-L6YQ1wQ/mNjVLAmK3AG1RK6VkokA1BIY6wmiH304Xtt/cLTps40EusZsU1Uop+v9lTDPxdtzbFmdXfFO3KEnwA==", + "dependencies": { + "basic-auth": "^2.0.1" + } + }, "node_modules/express-validator": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.15.0.tgz", diff --git a/package.json b/package.json index 63ee53a..4bbd712 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "cors": "^2.8.5", "dotenv": "^16.0.3", "express": "^4.18.2", + "express-basic-auth": "^1.2.1", "express-validator": "^6.15.0", "kysely": "^0.23.4", "pg": "^8.9.0", diff --git a/src/common.ts b/src/common.ts index 7210dcd..15f0f01 100644 --- a/src/common.ts +++ b/src/common.ts @@ -1,7 +1,7 @@ import http from 'http' import https from 'https' -export async function GetRequest(url: string) { +export function GetRequest(url: string) { return new Promise((resolve, reject) => { let handler = (resp: http.IncomingMessage) => { let data = '' @@ -15,15 +15,17 @@ export async function GetRequest(url: string) { resp.on('end', () => { resolve(data) }) - resp.on('error', (err) => { + } + let req + if (url.startsWith('https')) { + req = https.get(url, handler) + } else { + req = http.get(url, handler) + } + req + .on('error', (err) => { reject(err) }) - } - - if (url.startsWith('https')) { - https.get(url, handler) - } else { - http.get(url, handler) - } + .end() }) } diff --git a/src/db/db.ts b/src/db/db.ts index 7443dbe..f74f866 100644 --- a/src/db/db.ts +++ b/src/db/db.ts @@ -37,6 +37,38 @@ export async function FindServer({ name }: { name: string }) { .where('server.name', '=', name) .executeTakeFirst() } + +export async function CreateServer({ + name, + description +}: { + name: string + description: string +}) { + return await db + .insertInto('server') + .values({ name, description }) + .returning(['id', 'token']) + .executeTakeFirstOrThrow() +} + +//tokens are stored in raw... maybe we should use something better in the future +//Using callback for express-basic-auth +export function CheckServerToken( + this: { body: any }, + name: string, + password: string, + cb: (error: Error | null, success: boolean) => void +) { + db.selectFrom('server') + .where('id', '=', Number(name)) + .where('token', '=', password) + .executeTakeFirst() + .then((result) => { + if (!!result) this.body.serverId = name + cb(null, !!result) + }) +} /* async function demo() { const { id } = await db diff --git a/src/db/migrations/2023-02-28-01.ts b/src/db/migrations/2023-02-28-01.ts index 9340f83..22ac874 100644 --- a/src/db/migrations/2023-02-28-01.ts +++ b/src/db/migrations/2023-02-28-01.ts @@ -30,7 +30,12 @@ export async function up(db: Kysely): Promise { .addColumn('id', 'serial', (col) => col.primaryKey()) .addColumn('name', 'varchar', (col) => col.unique()) .addColumn('description', 'varchar') - .addColumn('token', 'varchar') + .addColumn('token', 'varchar', (col) => + col + .notNull() + .unique() + .defaultTo(sql`gen_random_uuid()`) + ) .execute() await db.schema diff --git a/src/db/model.ts b/src/db/model.ts index 66d1b1f..d9d1fba 100644 --- a/src/db/model.ts +++ b/src/db/model.ts @@ -57,9 +57,10 @@ interface MapTable { image: string } interface ServerTable { - id: string + id: Generated name: string description: string + token: Generated } interface Database { kill: KillTable diff --git a/src/index.ts b/src/index.ts index 7190dff..c042bc1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ import express from 'express' import cors from 'cors' import client from './client' import { dbReady } from './db/db' -import server from './server' +import server from './server/server' const app = express() const port = 3001 diff --git a/src/server/register.ts b/src/server/register.ts new file mode 100644 index 0000000..ed08106 --- /dev/null +++ b/src/server/register.ts @@ -0,0 +1,84 @@ +import { Router } from 'express' +import { GetRequest } from '../common' +import { FindServer, CreateServer } from '../db/db' +import { body, validationResult } from 'express-validator' + +const verificationString = 'I am a northstar server!' +const masterServerURL = 'https://northstar.tf' + +const router = Router() + +const hostsCount: { [id: string]: number } = {} +const hostsTimeout: { [id: string]: NodeJS.Timeout } = {} +//Very simple rate limiting. max 2 registers per IP every 5 mins. Maybe 2 is a bit few ? +router.post('/servers/register', (req, res, next) => { + let ip = + req.header('x-forwarded-for') || req.socket.remoteAddress || 'undefined' + if (hostsCount[ip] > 2) { + return res.status(429).json({ + error: + 'too many requests. Please wait 5 minutes before requesting a register again.' + }) + } + clearTimeout(hostsTimeout[ip]) + hostsCount[ip] = hostsCount[ip] ?? (hostsCount[ip] + 1) | 1 + hostsTimeout[ip] = setTimeout(() => { + hostsCount[ip] = 0 + }, 300000) + + next() +}) + +router.post( + '/servers/register', + body(['name', 'description']).isString(), + body('auth_endpoint').isURL(), + async (req, res, next) => { + const errors = validationResult(req) + if (!errors.isEmpty()) { + console.log(JSON.stringify(errors)) + return res.status(400).json({ errors: errors.array() }) + } + try { + //Check if server name isn't already in database + if (!!(await FindServer({ name: req.body.name }))) { + return res + .status(403) + .json({ error: 'Server already exists in the database' }) + } + + //Check if server is in masterserver's list + const masterServerList = JSON.parse( + await GetRequest(masterServerURL + '/client/servers') + ) as Array + if (!masterServerList.find((e) => e.name == req.body.name)) { + return res + .status(403) + .json({ error: 'Server not listed in masterserver' }) + } + + //Send request to verify server. Not very useful for now, but maybe a future method for auth ? + //Maybe should set a blacklist here for local domain ? + if ((await GetRequest(req.body.auth_endpoint)) != verificationString) { + return res.status(400).json({ + error: "Couldn't reach gameserver at " + req.body.auth_endpoint + }) + } + + //send token here + res.status(201).json( + await CreateServer({ + name: req.body.name, + description: req.body.description + }) + ) + } catch (e) { + console.log(e) + return res.status(400).json({ + error: "Server encountered an error, Couldn't register gameserver." + }) + } + } +) + +export default router diff --git a/src/server.ts b/src/server/server.ts similarity index 68% rename from src/server.ts rename to src/server/server.ts index 3568e31..40d5ac7 100644 --- a/src/server.ts +++ b/src/server/server.ts @@ -1,59 +1,61 @@ import { Router } from 'express' -import db, { CreateKillRecord, FindServer } from './db/db' -import { body, validationResult } from 'express-validator' -import { GetRequest } from './common' +import expressBasicAuth from 'express-basic-auth' +import { body, header, validationResult } from 'express-validator' +import register from './register' +import { CreateKillRecord, CheckServerToken } from '../db/db' + const router = Router() -const verificationString = 'I am a northstar server!' -const masterServerURL = 'https://northstar.tf' -//auth middleware, maybe some timeout ? -router.post('/*', (req, res, next) => { - next() -}) +router.use('/', register) +//auth middleware router.post( - '/servers/register', - body(['name', 'description']).isString(), - body('auth_endpoint').isURL(), - async (req, res, next) => { + '/servers/:serverId/kill', + header('authorization') + .exists({ checkFalsy: true }) + .withMessage('Missing Authorization Header') + .bail() + .contains('Basic') + .withMessage('Authorization Token is not Basic'), + (req, res, next) => { const errors = validationResult(req) if (!errors.isEmpty()) { console.log(JSON.stringify(errors)) - return res.status(400).json({ errors: errors.array() }) + return res.status(403).json({ errors: errors.array() }) } - try { - //Check if server name isn't already in database - if (await FindServer(req.body.name)) { - return res - .status(403) - .json({ error: 'Server already exists in the database' }) - } - - //Check if server is in masterserver's list - const masterServerList = JSON.parse( - await GetRequest(masterServerURL + '/client/servers') - ) as Array - if (!masterServerList.find((e) => e.name == req.body.name)) { - return res - .status(403) - .json({ error: 'Server not listed in masterserver' }) - } - - //Send request to verify server. Not very useful for now, but maybe a future method for auth ? - if ((await GetRequest(req.body.auth_endpoint)) != verificationString) { - //Maybe should set a blacklist here for local domain ? - return res.status(400).json({ error: "Couldn't verify gameserver" }) - } - } catch (e) { - console.log(e) - return res.status(400).json({ - error: "Server encountered an error, Couldn't verify gameserver" - }) - } - //register server here + next() + }, + //Huge mess to retrieve server id from expressBasicAuth. We probably should fix it. + (req, res, next) => { + if (!req) res.send(500) + return expressBasicAuth({ + authorizeAsync: true, + authorizer: CheckServerToken.bind(req), + unauthorizedResponse: { error: 'invalid credentials' } + })(req as any, res, next) } ) +const serversCount: { [id: string]: number } = {} +const serversTimeout: { [id: string]: NodeJS.Timeout } = {} + +//same rate limiting code as register. max 10 kills per server every 1 sec. should be enough. +router.post('/servers/:serverId/kill', (req, res, next) => { + let serverId = req.body.serverId || 'undefined' + if (serversCount[serverId] > 2) { + return res.status(429).json({ + error: 'too many requests. Are players really making that much kills ?' + }) + } + clearTimeout(serversTimeout[serverId]) + serversCount[serverId] = + serversCount[serverId] ?? (serversCount[serverId] + 1) | 1 + serversTimeout[serverId] = setTimeout(() => { + serversCount[serverId] = 0 + }, 1000) + next() +}) + router.post( '/servers/:serverId/kill', body([ @@ -108,7 +110,7 @@ router.post( min: 0 }), body(['cause_of_death', 'victim_id'], 'mandatory').exists().notEmpty(), - (req, res, next) => { + (req, res) => { const errors = validationResult(req) if (!errors.isEmpty()) { console.log(JSON.stringify(errors)) @@ -185,7 +187,7 @@ router.post( distance }) .then((e) => { - res.send(200) + res.send(201) }) .catch((e) => { res.send(500) diff --git a/tests/registration.test.ts b/tests/registration.test.ts new file mode 100644 index 0000000..02ba755 --- /dev/null +++ b/tests/registration.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, test } from '@jest/globals' + +describe('registration', () => { + test('garbage body', () => { + expect(3).toBe(3) + }) + test('incorrect data format', () => { + expect(3).toBe(3) + }) + test('duplicate server', () => { + expect(3).toBe(3) + }) + test('invisible server', () => { + expect(3).toBe(3) + }) + test('wrong verification', () => { + expect(3).toBe(3) + }) + test('masterserver unreachable', () => { + expect(3).toBe(3) + }) + test('registration successful', () => { + expect(3).toBe(3) + }) + test('server deletion', () => { + expect(3).toBe(3) + }) +})