From 6a19bbb7b1416fc90ff7418deed46257566dc8c3 Mon Sep 17 00:00:00 2001 From: Natalie Rose Date: Thu, 21 Aug 2025 23:16:56 +1000 Subject: [PATCH] Implement v2 api argument mapping --- docker-compose.yml | 1 - etc-docker/nginx.conf | 1 + v2-api/index.ts | 103 +++++++++++++++++++++++++++++++++++++-- v2-api/package-lock.json | 34 +++++++++++++ v2-api/package.json | 2 + 5 files changed, 136 insertions(+), 5 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 504b0e48..99c4a9f5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -83,7 +83,6 @@ services: - 'lacuna-server' ports: - '8080:80' - - '8443:443' volumes: - ./etc-docker/nginx.conf:/etc/nginx/nginx.conf - ./etc-docker:/home/lacuna/server/etc diff --git a/etc-docker/nginx.conf b/etc-docker/nginx.conf index f1c4fdc2..033fc5cc 100644 --- a/etc-docker/nginx.conf +++ b/etc-docker/nginx.conf @@ -72,6 +72,7 @@ http { location ~ /.* { proxy_pass http://lacuna-server:5000; proxy_set_header X-Real-IP $remote_addr; + add_header X-Served-By 'Lacuna' always; } } } diff --git a/v2-api/index.ts b/v2-api/index.ts index 6b6eb2fe..d3da3e7a 100644 --- a/v2-api/index.ts +++ b/v2-api/index.ts @@ -1,24 +1,86 @@ import express from 'express'; import morgan from 'morgan'; import bodyParser from 'body-parser'; +import cors from 'cors'; + +const errorCodes = [ + 1000, // Name not available. + 1001, // Invalid password. + 1002, // Object does not exist. + 1003, // Too much information. + 1004, // Password incorrect. + 1005, // Contains invalid characters. + 1006, // Session expired. + 1007, // Overspend. + 1008, // Underspend. + 1009, // Invalid range. + 1010, // Insufficient privileges. + 1011, // Not enough resources in storage. + 1012, // Not enough resources in production. + 1013, // Missing prerequisites. + 1014, // Captcha not valid. + 1015, // Restricted for sitter logins. + 1016, // Needs to solve a captcha. + 1017, // Pending Parliamentary Vote + 1018, // Already Voted + 1100, // Empire not founded. + 1101, // Empire not founded, and you tried to create it, but had the wrong password. + 1200, // Game Over. +] as const; + +type ReqModule = string; +type ReqMethod = string; +type MethodRef = `/${ReqModule}/${ReqMethod}`; + +type Argument = { + name: string; + required: boolean; +}; + +type RPCRequest = { + id: number; + jsonrpc: '2.0'; + method: ReqMethod; + params: any; +}; + +type RPCError = { + jsonrpc: '2.0'; + id: number; + error: { + code: typeof errorCodes[number]; + message: string; + data?: any; + }; +}; + +const required = (name: string): Argument => ({ name, required: true }); + +const namedArgumentMapper: Record = { + '/empire/login': [required('empire_name'), required('password'), required('api_key')], + '/empire/view_boosts': [], + '/empire/view_public_profile': [required('empire_id')] +}; const port = 5999 const app = express(); app.set('trust proxy', 'uniquelocal'); +app.use(cors()); app.use(morgan('common', { immediate: true })); app.use(bodyParser.json()); app.post('/v2/:module/:method', async (req, res) => { const { module: reqModule, method: reqMethod } = req.params; + const methodRef: MethodRef = `/${reqModule}/${reqMethod}`; const token = req.headers['authorization']?.startsWith('Token') ? req.headers['authorization'].split(' ')[1] : undefined; - const prepareBody = (data: any) => { + const prepareBody = (data: any): RPCRequest | RPCError => { console.log('handling body', data); - const body = { + const body: RPCRequest = { id: 1, jsonrpc: '2.0', method: reqMethod, @@ -32,7 +94,34 @@ app.post('/v2/:module/:method', async (req, res) => { body.params = data; } - if (!(reqModule === 'empire' && reqMethod === 'login') && !!token && body.params !== null) { + // Use the mapping to convert named args into legacy positional arguments + // This quasi-proxy approach allows us to maintain backwards compatibility with + // previous iterations of the game/scripts as well as inject some validation when helpful. + if (!Array.isArray(body.params) && !!namedArgumentMapper[methodRef]) { + const newArgList = [...namedArgumentMapper[methodRef]]; + + for (const arg of newArgList) { + if (arg.required && typeof body.params[arg.name] === 'undefined' || body.params[arg.name] === null) { + return { + id: 1, + jsonrpc: '2.0', + error: { + code: 1002, + message: `Parameter ${arg.name} required by v2 API but not provided.`, + data: { + methodRef, + schema: newArgList, + }, + }, + }; + } + } + + body.params = [...newArgList.map(({name}) => body.params[name])]; + } + + // TODO: improve session id mapping + if (methodRef !== '/empire/login' && !!token && body.params !== null) { body.params = Array.isArray(body.params) ? [token, ...body.params] : { session_id: token, ...body.params}; @@ -41,7 +130,13 @@ app.post('/v2/:module/:method', async (req, res) => { return body; } - const body = prepareBody(req.body); + const body = prepareBody(req.body ?? {}); + + if ('error' in body) { + console.error('Processing/validation error', body); + return res.status(500).json(body); + } + console.log(`hitting /${reqModule} with:`, body); const response = await fetch(`http://lacuna-server:5000/${reqModule}`, { method: 'POST', diff --git a/v2-api/package-lock.json b/v2-api/package-lock.json index 86c80c88..b652f0e5 100644 --- a/v2-api/package-lock.json +++ b/v2-api/package-lock.json @@ -9,10 +9,12 @@ "version": "1.0.0", "dependencies": { "body-parser": "^2.2.0", + "cors": "^2.8.5", "express": "^5.1.0", "morgan": "^1.10.1" }, "devDependencies": { + "@types/cors": "^2.8.19", "@types/express": "^5.0.3", "@types/morgan": "^1.9.10", "typescript": "^5.9.2" @@ -39,6 +41,16 @@ "@types/node": "*" } }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/express": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", @@ -263,6 +275,19 @@ "node": ">=6.6.0" } }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -689,6 +714,15 @@ "node": ">= 0.6" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", diff --git a/v2-api/package.json b/v2-api/package.json index 5e922c93..41c12afd 100644 --- a/v2-api/package.json +++ b/v2-api/package.json @@ -8,10 +8,12 @@ "main": "index.js", "dependencies": { "body-parser": "^2.2.0", + "cors": "^2.8.5", "express": "^5.1.0", "morgan": "^1.10.1" }, "devDependencies": { + "@types/cors": "^2.8.19", "@types/express": "^5.0.3", "@types/morgan": "^1.9.10", "typescript": "^5.9.2" -- 2.51.2