Registration and server authentification
This commit is contained in:
Generated
+25
@@ -8,6 +8,7 @@
|
|||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"express-basic-auth": "^1.2.1",
|
||||||
"express-validator": "^6.15.0",
|
"express-validator": "^6.15.0",
|
||||||
"kysely": "^0.23.4",
|
"kysely": "^0.23.4",
|
||||||
"pg": "^8.9.0",
|
"pg": "^8.9.0",
|
||||||
@@ -3469,6 +3470,22 @@
|
|||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/body-parser": {
|
||||||
"version": "1.20.1",
|
"version": "1.20.1",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
|
||||||
@@ -4634,6 +4651,14 @@
|
|||||||
"node": ">= 0.10.0"
|
"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": {
|
"node_modules/express-validator": {
|
||||||
"version": "6.15.0",
|
"version": "6.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.15.0.tgz",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"express-basic-auth": "^1.2.1",
|
||||||
"express-validator": "^6.15.0",
|
"express-validator": "^6.15.0",
|
||||||
"kysely": "^0.23.4",
|
"kysely": "^0.23.4",
|
||||||
"pg": "^8.9.0",
|
"pg": "^8.9.0",
|
||||||
|
|||||||
+11
-9
@@ -1,7 +1,7 @@
|
|||||||
import http from 'http'
|
import http from 'http'
|
||||||
import https from 'https'
|
import https from 'https'
|
||||||
|
|
||||||
export async function GetRequest(url: string) {
|
export function GetRequest(url: string) {
|
||||||
return new Promise<string>((resolve, reject) => {
|
return new Promise<string>((resolve, reject) => {
|
||||||
let handler = (resp: http.IncomingMessage) => {
|
let handler = (resp: http.IncomingMessage) => {
|
||||||
let data = ''
|
let data = ''
|
||||||
@@ -15,15 +15,17 @@ export async function GetRequest(url: string) {
|
|||||||
resp.on('end', () => {
|
resp.on('end', () => {
|
||||||
resolve(data)
|
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)
|
reject(err)
|
||||||
})
|
})
|
||||||
}
|
.end()
|
||||||
|
|
||||||
if (url.startsWith('https')) {
|
|
||||||
https.get(url, handler)
|
|
||||||
} else {
|
|
||||||
http.get(url, handler)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,38 @@ export async function FindServer({ name }: { name: string }) {
|
|||||||
.where('server.name', '=', name)
|
.where('server.name', '=', name)
|
||||||
.executeTakeFirst()
|
.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() {
|
async function demo() {
|
||||||
const { id } = await db
|
const { id } = await db
|
||||||
|
|||||||
@@ -30,7 +30,12 @@ export async function up(db: Kysely<any>): Promise<void> {
|
|||||||
.addColumn('id', 'serial', (col) => col.primaryKey())
|
.addColumn('id', 'serial', (col) => col.primaryKey())
|
||||||
.addColumn('name', 'varchar', (col) => col.unique())
|
.addColumn('name', 'varchar', (col) => col.unique())
|
||||||
.addColumn('description', 'varchar')
|
.addColumn('description', 'varchar')
|
||||||
.addColumn('token', 'varchar')
|
.addColumn('token', 'varchar', (col) =>
|
||||||
|
col
|
||||||
|
.notNull()
|
||||||
|
.unique()
|
||||||
|
.defaultTo(sql`gen_random_uuid()`)
|
||||||
|
)
|
||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
await db.schema
|
await db.schema
|
||||||
|
|||||||
+2
-1
@@ -57,9 +57,10 @@ interface MapTable {
|
|||||||
image: string
|
image: string
|
||||||
}
|
}
|
||||||
interface ServerTable {
|
interface ServerTable {
|
||||||
id: string
|
id: Generated<number>
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
|
token: Generated<string>
|
||||||
}
|
}
|
||||||
interface Database {
|
interface Database {
|
||||||
kill: KillTable
|
kill: KillTable
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ import express from 'express'
|
|||||||
import cors from 'cors'
|
import cors from 'cors'
|
||||||
import client from './client'
|
import client from './client'
|
||||||
import { dbReady } from './db/db'
|
import { dbReady } from './db/db'
|
||||||
import server from './server'
|
import server from './server/server'
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
const port = 3001
|
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 { Router } from 'express'
|
||||||
import db, { CreateKillRecord, FindServer } from './db/db'
|
import expressBasicAuth from 'express-basic-auth'
|
||||||
import { body, validationResult } from 'express-validator'
|
import { body, header, validationResult } from 'express-validator'
|
||||||
import { GetRequest } from './common'
|
import register from './register'
|
||||||
|
import { CreateKillRecord, CheckServerToken } from '../db/db'
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
const verificationString = 'I am a northstar server!'
|
router.use('/', register)
|
||||||
const masterServerURL = 'https://northstar.tf'
|
|
||||||
//auth middleware, maybe some timeout ?
|
|
||||||
router.post('/*', (req, res, next) => {
|
|
||||||
next()
|
|
||||||
})
|
|
||||||
|
|
||||||
|
//auth middleware
|
||||||
router.post(
|
router.post(
|
||||||
'/servers/register',
|
'/servers/:serverId/kill',
|
||||||
body(['name', 'description']).isString(),
|
header('authorization')
|
||||||
body('auth_endpoint').isURL(),
|
.exists({ checkFalsy: true })
|
||||||
async (req, res, next) => {
|
.withMessage('Missing Authorization Header')
|
||||||
|
.bail()
|
||||||
|
.contains('Basic')
|
||||||
|
.withMessage('Authorization Token is not Basic'),
|
||||||
|
(req, res, next) => {
|
||||||
const errors = validationResult(req)
|
const errors = validationResult(req)
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
console.log(JSON.stringify(errors))
|
console.log(JSON.stringify(errors))
|
||||||
return res.status(400).json({ errors: errors.array() })
|
return res.status(403).json({ errors: errors.array() })
|
||||||
}
|
}
|
||||||
try {
|
next()
|
||||||
//Check if server name isn't already in database
|
},
|
||||||
if (await FindServer(req.body.name)) {
|
//Huge mess to retrieve server id from expressBasicAuth. We probably should fix it.
|
||||||
return res
|
(req, res, next) => {
|
||||||
.status(403)
|
if (!req) res.send(500)
|
||||||
.json({ error: 'Server already exists in the database' })
|
return expressBasicAuth({
|
||||||
}
|
authorizeAsync: true,
|
||||||
|
authorizer: CheckServerToken.bind(req),
|
||||||
//Check if server is in masterserver's list
|
unauthorizedResponse: { error: 'invalid credentials' }
|
||||||
const masterServerList = JSON.parse(
|
})(req as any, res, next)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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(
|
router.post(
|
||||||
'/servers/:serverId/kill',
|
'/servers/:serverId/kill',
|
||||||
body([
|
body([
|
||||||
@@ -108,7 +110,7 @@ router.post(
|
|||||||
min: 0
|
min: 0
|
||||||
}),
|
}),
|
||||||
body(['cause_of_death', 'victim_id'], 'mandatory').exists().notEmpty(),
|
body(['cause_of_death', 'victim_id'], 'mandatory').exists().notEmpty(),
|
||||||
(req, res, next) => {
|
(req, res) => {
|
||||||
const errors = validationResult(req)
|
const errors = validationResult(req)
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
console.log(JSON.stringify(errors))
|
console.log(JSON.stringify(errors))
|
||||||
@@ -185,7 +187,7 @@ router.post(
|
|||||||
distance
|
distance
|
||||||
})
|
})
|
||||||
.then((e) => {
|
.then((e) => {
|
||||||
res.send(200)
|
res.send(201)
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
res.send(500)
|
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