Registration and server authentification
This commit is contained in:
Generated
+25
@@ -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",
|
||||
|
||||
@@ -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
@@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user