From 27ef1dda6b0a728ba945910531b381f910d66516 Mon Sep 17 00:00:00 2001 From: Jonathan Barrow Date: Sat, 19 Nov 2022 11:15:55 -0500 Subject: [PATCH] Moved all validation to middleware and replaced per-title routes with configs --- src/middleware/title-code.js | 13 +++ src/middleware/validate-multipart.js | 40 +++++++ src/routes/index.js | 3 + src/routes/post.js | 27 +++++ src/server.js | 11 +- src/titles/index.js | 3 + src/titles/splatoon.js | 132 ++++++++++++++++++++++ src/titles/splatoon/index.js | 20 ---- src/titles/splatoon/post.js | 163 --------------------------- 9 files changed, 225 insertions(+), 187 deletions(-) create mode 100644 src/middleware/title-code.js create mode 100644 src/middleware/validate-multipart.js create mode 100644 src/routes/index.js create mode 100644 src/routes/post.js create mode 100644 src/titles/index.js create mode 100644 src/titles/splatoon.js delete mode 100644 src/titles/splatoon/index.js delete mode 100644 src/titles/splatoon/post.js diff --git a/src/middleware/title-code.js b/src/middleware/title-code.js new file mode 100644 index 0000000..c2664ab --- /dev/null +++ b/src/middleware/title-code.js @@ -0,0 +1,13 @@ +const titles = require('../titles'); + +function titleCodeMiddleware(request, response, next) { + request.titleCode = request.subdomains.pop(); + + if (!titles[request.titleCode]) { + return next(`No valid title config set for title code ${request.titleCode}`); + } + + next(); +} + +module.exports = titleCodeMiddleware; \ No newline at end of file diff --git a/src/middleware/validate-multipart.js b/src/middleware/validate-multipart.js new file mode 100644 index 0000000..b806d30 --- /dev/null +++ b/src/middleware/validate-multipart.js @@ -0,0 +1,40 @@ +const titles = require('../titles'); + +function validateMultipartMiddleware(request, response, next) { + const title = titles[request.titleCode]; + + const multipartValidator = title.multipart_validator; + const validationSchema = title.validation_schema; + + multipartValidator(request.copy, response, error => { + if (error) { + return next(error); + } + + const resultData = { + ...request.copy.body, + }; + + for (const key in request.copy.files) { + if (Object.hasOwnProperty.call(request.copy.files, key)) { + const field = request.copy.files[key]; + + for (const file of field) { + resultData[file.fieldname] = file.buffer; + } + } + } + + const validationResult = validationSchema.validate(resultData); + + if (validationResult.error) { + return next(validationResult.error); + } + + request.resultData = validationResult.value; + + next(); + }); +} + +module.exports = validateMultipartMiddleware; \ No newline at end of file diff --git a/src/routes/index.js b/src/routes/index.js new file mode 100644 index 0000000..447eaaf --- /dev/null +++ b/src/routes/index.js @@ -0,0 +1,3 @@ +module.exports = { + post: require('./post') +}; \ No newline at end of file diff --git a/src/routes/post.js b/src/routes/post.js new file mode 100644 index 0000000..b71ce15 --- /dev/null +++ b/src/routes/post.js @@ -0,0 +1,27 @@ +const router = require('express').Router(); +const titles = require('../titles'); + +router.post('/post', async (request, response, next) => { + const title = titles[request.titleCode]; + + const resultType = title.type; + const ResultTypeModel = title.result_model; + + const result = new ResultTypeModel({ + type: resultType, + bossUniqueId: request.headers['x-boss-uniqueid'], + bossDigest: request.headers['x-boss-digest'], + resultData: request.resultData + }); + + try { + await result.save(); + } catch (error) { + return next(error); + } + + + return response.send('success'); +}); + +module.exports = router; \ No newline at end of file diff --git a/src/server.js b/src/server.js index c31c99b..9d670d8 100644 --- a/src/server.js +++ b/src/server.js @@ -1,21 +1,24 @@ const express = require('express'); const morgan = require('morgan'); +const routes = require('./routes') +const titleCodeMiddleware = require('./middleware/title-code'); const rawBodyMiddleware = require('./middleware/raw-body'); const validateBOSSDigestMiddleware = require('./middleware/validate-boss-digest'); const copyRequestStreamMiddleware = require('./middleware/copy-request-stream'); +const validateMultipartMiddleware = require('./middleware/validate-multipart'); const database = require('./database'); const config = require('../config.json'); -const splatoon = require('./titles/splatoon'); - const app = express(); const { port } = config.http; app.use(morgan('dev')); +app.use(titleCodeMiddleware); app.use(rawBodyMiddleware); app.use(validateBOSSDigestMiddleware); app.use(copyRequestStreamMiddleware); -app.use(splatoon); +app.use(validateMultipartMiddleware); +app.use(routes.post); // * 404 error handler app.use((request, response) => { @@ -35,7 +38,7 @@ app.use((error, request, response, next) => { console.log(error); // * 5XX means a server error, but this would be a 4XX client error // * (wrong body or hash provided) - // * Real server sends this, however, so leave as is + // * The real server sends 500 for all errors, however, so leave as is return response.status(500).send('error'); }); diff --git a/src/titles/index.js b/src/titles/index.js new file mode 100644 index 0000000..80cdfc4 --- /dev/null +++ b/src/titles/index.js @@ -0,0 +1,3 @@ +module.exports = { + 'wup-agmj': require('./splatoon') +}; \ No newline at end of file diff --git a/src/titles/splatoon.js b/src/titles/splatoon.js new file mode 100644 index 0000000..c23c341 --- /dev/null +++ b/src/titles/splatoon.js @@ -0,0 +1,132 @@ +const multer = require('.multer'); +const { joi } = require('../util'); +const { SplatfestResult } = require('../models/splatfest_result'); + +module.exports = { + type: 'splatfest', + result_model: SplatfestResult, + validation_schema: joi.object({ + ServerEnv: joi.string(), + PId: joi.numberstring(), + MiiName: joi.string(), + Model: joi.numberstring(), + Skin: joi.numberstring(), + EyeColor: joi.numberstring(), + Weapon: joi.numberstring(), + SumPaint: joi.numberstring(), + Gear_Shoes: joi.numberstring(), + Gear_Shoes_Skill0: joi.numberstring(), + Gear_Shoes_Skill1: joi.numberstring(), + Gear_Shoes_Skill2: joi.numberstring(), + Gear_Clothes: joi.numberstring(), + Gear_Clothes_Skill0: joi.numberstring(), + Gear_Clothes_Skill1: joi.numberstring(), + Gear_Clothes_Skill2: joi.numberstring(), + Gear_Head: joi.numberstring(), + Gear_Head_Skill0: joi.numberstring(), + Gear_Head_Skill1: joi.numberstring(), + Gear_Head_Skill2: joi.numberstring(), + Rank: joi.numberstring(), + Udemae: joi.numberstring(), + RegularKillSum: joi.numberstring(), + WinSum: joi.numberstring(), + LoseSum: joi.numberstring(), + TodaysCondition: joi.numberstring(), + Region: joi.string(), + Area: joi.numberstring(), + FesID: joi.numberstring(), + FesState: joi.numberstring(), + FesTeam: joi.numberstring(), + FesGrade: joi.numberstring(), + FesPoint: joi.numberstring(), + FesPower: joi.numberstring(), + BestFesPower: joi.numberstring(), + Money: joi.numberstring(), + Shell: joi.numberstring(), + TotalBonusShell: joi.numberstring(), + MatchingTime: joi.numberstring(), + IsRematch: joi.numberstring(), + SaveDataCorrupted: joi.numberstring(), + DisconnectedPId: joi.numberstring(), + DisconnectedMemHash: joi.numberstring(), + SessionID: joi.numberstring(), + StartNetworkTime: joi.numberstring(), + GameMode: joi.numberstring(), + Rule: joi.numberstring(), + Stage: joi.numberstring(), + Team: joi.numberstring(), + IsWinGame: joi.numberstring(), + Kill: joi.numberstring(), + Death: joi.numberstring(), + Paint: joi.numberstring(), + IsNetworkBurst: joi.numberstring(), + BottleneckPlayerNum: joi.numberstring(), + MaxSilenceFrame: joi.numberstring(), + MemoryHash: joi.numberstring(), + Paint_Alpha: joi.numberstring(), + Paint_Bravo: joi.numberstring(), + FaceImg: joi.binary() + }).options({ presence: 'required' }).required(), + multipart_validator: multer().fields([ + { name: 'ServerEnv' }, + { name: 'PId' }, + { name: 'MiiName' }, + { name: 'Model' }, + { name: 'Skin' }, + { name: 'EyeColor' }, + { name: 'Weapon' }, + { name: 'SumPaint' }, + { name: 'Gear_Shoes' }, + { name: 'Gear_Shoes_Skill0' }, + { name: 'Gear_Shoes_Skill1' }, + { name: 'Gear_Shoes_Skill2' }, + { name: 'Gear_Clothes' }, + { name: 'Gear_Clothes_Skill0' }, + { name: 'Gear_Clothes_Skill1' }, + { name: 'Gear_Clothes_Skill2' }, + { name: 'Gear_Head' }, + { name: 'Gear_Head_Skill0' }, + { name: 'Gear_Head_Skill1' }, + { name: 'Gear_Head_Skill2' }, + { name: 'Rank' }, + { name: 'Udemae' }, + { name: 'RegularKillSum' }, + { name: 'WinSum' }, + { name: 'LoseSum' }, + { name: 'TodaysCondition' }, + { name: 'Region' }, + { name: 'Area' }, + { name: 'FesID' }, + { name: 'FesState' }, + { name: 'FesTeam' }, + { name: 'FesGrade' }, + { name: 'FesPoint' }, + { name: 'FesPower' }, + { name: 'BestFesPower' }, + { name: 'Money' }, + { name: 'Shell' }, + { name: 'TotalBonusShell' }, + { name: 'MatchingTime' }, + { name: 'IsRematch' }, + { name: 'SaveDataCorrupted' }, + { name: 'DisconnectedPId' }, + { name: 'DisconnectedMemHash' }, + { name: 'SessionID' }, + { name: 'StartNetworkTime' }, + { name: 'GameMode' }, + { name: 'Rule' }, + { name: 'Stage' }, + { name: 'Team' }, + { name: 'IsWinGame' }, + { name: 'Kill' }, + { name: 'Death' }, + { name: 'Paint' }, + { name: 'IsNetworkBurst' }, + { name: 'BottleneckPlayerNum' }, + { name: 'MaxSilenceFrame' }, + { name: 'MemoryHash' }, + { name: 'Paint_Alpha' }, + { name: 'Paint_Bravo' }, + { name: 'FaceImg' } + ]) +}; \ No newline at end of file diff --git a/src/titles/splatoon/index.js b/src/titles/splatoon/index.js deleted file mode 100644 index 9afed2f..0000000 --- a/src/titles/splatoon/index.js +++ /dev/null @@ -1,20 +0,0 @@ -const express = require('express'); -const subdomain = require('express-subdomain'); - -const post = require('./post'); - -// Router to handle the subdomain -const splatoon = express.Router(); - -// Setup routes -//logger.info('[SPLATOON] Applying imported routes'); -splatoon.use(post); - -// Main router for endpoints -const router = express.Router(); - -// Create subdomains -//logger.info('[SPLATOON] Creating wup-agmj subdomain'); -router.use(subdomain('wup-agmj.app', splatoon)); - -module.exports = router; \ No newline at end of file diff --git a/src/titles/splatoon/post.js b/src/titles/splatoon/post.js deleted file mode 100644 index 3cf30cd..0000000 --- a/src/titles/splatoon/post.js +++ /dev/null @@ -1,163 +0,0 @@ -const router = require('express').Router(); -const multer = require('multer'); -const joi = require('joi'); -const { SplatfestResult } = require('../../models/splatfest_result'); - -const resultDataSchema = joi.object({ - ServerEnv: joi.string(), - PId: joi.number(), - MiiName: joi.string(), - Model: joi.number(), - Skin: joi.number(), - EyeColor: joi.number(), - Weapon: joi.number(), - SumPaint: joi.number(), - Gear_Shoes: joi.number(), - Gear_Shoes_Skill0: joi.number(), - Gear_Shoes_Skill1: joi.number(), - Gear_Shoes_Skill2: joi.number(), - Gear_Clothes: joi.number(), - Gear_Clothes_Skill0: joi.number(), - Gear_Clothes_Skill1: joi.number(), - Gear_Clothes_Skill2: joi.number(), - Gear_Head: joi.number(), - Gear_Head_Skill0: joi.number(), - Gear_Head_Skill1: joi.number(), - Gear_Head_Skill2: joi.number(), - Rank: joi.number(), - Udemae: joi.number(), - RegularKillSum: joi.number(), - WinSum: joi.number(), - LoseSum: joi.number(), - TodaysCondition: joi.number(), - Region: joi.string(), - Area: joi.number(), - FesID: joi.number(), - FesState: joi.number(), - FesTeam: joi.number(), - FesGrade: joi.number(), - FesPoint: joi.number(), - FesPower: joi.number(), - BestFesPower: joi.number(), - Money: joi.number(), - Shell: joi.number(), - TotalBonusShell: joi.number(), - MatchingTime: joi.number(), - IsRematch: joi.number(), - SaveDataCorrupted: joi.number(), - DisconnectedPId: joi.number().default(0).empty(''), // * Is sometimes an empty string. Turn to 0 - DisconnectedMemHash: joi.number().default(0).empty(''), // * Is sometimes an empty string. Turn to 0 - SessionID: joi.number(), - StartNetworkTime: joi.number(), - GameMode: joi.number(), - Rule: joi.number(), - Stage: joi.number(), - Team: joi.number(), - IsWinGame: joi.number(), - Kill: joi.number(), - Death: joi.number(), - Paint: joi.number(), - IsNetworkBurst: joi.number(), - BottleneckPlayerNum: joi.number(), - MaxSilenceFrame: joi.number(), - MemoryHash: joi.number(), - Paint_Alpha: joi.number(), - Paint_Bravo: joi.number(), - FaceImg: joi.binary() -}); - - -const multipart = multer().fields([ - { name: 'ServerEnv' }, - { name: 'PId' }, - { name: 'MiiName' }, - { name: 'Model' }, - { name: 'Skin' }, - { name: 'EyeColor' }, - { name: 'Weapon' }, - { name: 'SumPaint' }, - { name: 'Gear_Shoes' }, - { name: 'Gear_Shoes_Skill0' }, - { name: 'Gear_Shoes_Skill1' }, - { name: 'Gear_Shoes_Skill2' }, - { name: 'Gear_Clothes' }, - { name: 'Gear_Clothes_Skill0' }, - { name: 'Gear_Clothes_Skill1' }, - { name: 'Gear_Clothes_Skill2' }, - { name: 'Gear_Head' }, - { name: 'Gear_Head_Skill0' }, - { name: 'Gear_Head_Skill1' }, - { name: 'Gear_Head_Skill2' }, - { name: 'Rank' }, - { name: 'Udemae' }, - { name: 'RegularKillSum' }, - { name: 'WinSum' }, - { name: 'LoseSum' }, - { name: 'TodaysCondition' }, - { name: 'Region' }, - { name: 'Area' }, - { name: 'FesID' }, - { name: 'FesState' }, - { name: 'FesTeam' }, - { name: 'FesGrade' }, - { name: 'FesPoint' }, - { name: 'FesPower' }, - { name: 'BestFesPower' }, - { name: 'Money' }, - { name: 'Shell' }, - { name: 'TotalBonusShell' }, - { name: 'MatchingTime' }, - { name: 'IsRematch' }, - { name: 'SaveDataCorrupted' }, - { name: 'DisconnectedPId' }, - { name: 'DisconnectedMemHash' }, - { name: 'SessionID' }, - { name: 'StartNetworkTime' }, - { name: 'GameMode' }, - { name: 'Rule' }, - { name: 'Stage' }, - { name: 'Team' }, - { name: 'IsWinGame' }, - { name: 'Kill' }, - { name: 'Death' }, - { name: 'Paint' }, - { name: 'IsNetworkBurst' }, - { name: 'BottleneckPlayerNum' }, - { name: 'MaxSilenceFrame' }, - { name: 'MemoryHash' }, - { name: 'Paint_Alpha' }, - { name: 'Paint_Bravo' }, - { name: 'FaceImg' } -]); - -router.post('/post', async (request, response, next) => { - multipart(request.copy, response, async error => { - if (error) { - return next(error); - } - - const resultData = { - ...request.copy.body, - FaceImg: request.copy?.files?.FaceImg[0]?.buffer - }; - - const valid = resultDataSchema.validate(resultData); - - if (valid.error) { - return next(valid.error); - } - - const result = new SplatfestResult({ - type: 'splatfest', - bossUniqueId: request.headers['x-boss-uniqueid'], - bossDigest: request.headers['x-boss-digest'], - resultData: valid.value - }); - - await result.save(); - - return response.send('success'); - }); -}); - -module.exports = router; \ No newline at end of file