Registration and server authentification

This commit is contained in:
2023-03-03 16:08:50 +01:00
parent eac66702de
commit 9a677511e5
10 changed files with 238 additions and 58 deletions
+25
View File
@@ -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",
+1
View File
@@ -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",
+11 -9
View File
@@ -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<string>((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()
})
}
+32
View File
@@ -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
+6 -1
View File
@@ -30,7 +30,12 @@ export async function up(db: Kysely<any>): Promise<void> {
.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
+2 -1
View File
@@ -57,9 +57,10 @@ interface MapTable {
image: string
}
interface ServerTable {
id: string
id: Generated<number>
name: string
description: string
token: Generated<string>
}
interface Database {
kill: KillTable
+1 -1
View File
@@ -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
+84
View File
@@ -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<any>
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
+48 -46
View File
@@ -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<any>
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)
+28
View File
@@ -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)
})
})