diff --git a/package-lock.json b/package-lock.json index dc8a274..3c28cac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "nodemailer": "^6.4.2", "redis": "^4.3.1", "tga": "^1.0.4", + "typescript-is": "^0.19.0", "validator": "^13.7.0", "xmlbuilder": "^13.0.2", "xmlbuilder2": "0.0.4" @@ -48,6 +49,7 @@ "@types/node": "^18.14.4", "@types/node-rsa": "^1.1.1", "@types/nodemailer": "^6.4.7", + "@types/qs": "^6.9.7", "@types/validator": "^13.7.14", "@typescript-eslint/eslint-plugin": "^5.54.1", "@typescript-eslint/parser": "^5.54.1", @@ -5465,6 +5467,11 @@ "node": ">= 0.6" } }, + "node_modules/nested-error-stacks": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.1.tgz", + "integrity": "sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw==" + }, "node_modules/next-tick": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", @@ -5976,6 +5983,12 @@ "@redis/time-series": "1.0.4" } }, + "node_modules/reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", + "optional": true + }, "node_modules/regexpp": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", @@ -6679,14 +6692,12 @@ "node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "devOptional": true + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "node_modules/tsutils": { "version": "3.21.0", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, "dependencies": { "tslib": "^1.8.1" }, @@ -6762,7 +6773,6 @@ "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6771,6 +6781,24 @@ "node": ">=4.2.0" } }, + "node_modules/typescript-is": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/typescript-is/-/typescript-is-0.19.0.tgz", + "integrity": "sha512-SAJEx2cxbQZhfOjDEjPnQJt1qRS1M3wrKbUwvsywVHWGbMgM1dcIf9gPWNDS1/dgTa/7Iexk2mmAHHsP9MeCsA==", + "dependencies": { + "nested-error-stacks": "^2", + "tsutils": "^3.17.1" + }, + "engines": { + "node": ">=6.14.4" + }, + "optionalDependencies": { + "reflect-metadata": ">=0.1.12" + }, + "peerDependencies": { + "typescript": "^4.1.5" + } + }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -11517,6 +11545,11 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" }, + "nested-error-stacks": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.1.tgz", + "integrity": "sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw==" + }, "next-tick": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", @@ -11892,6 +11925,12 @@ "@redis/time-series": "1.0.4" } }, + "reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", + "optional": true + }, "regexpp": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", @@ -12440,14 +12479,12 @@ "tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "devOptional": true + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "tsutils": { "version": "3.21.0", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, "requires": { "tslib": "^1.8.1" } @@ -12501,8 +12538,17 @@ "typescript": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==" + }, + "typescript-is": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/typescript-is/-/typescript-is-0.19.0.tgz", + "integrity": "sha512-SAJEx2cxbQZhfOjDEjPnQJt1qRS1M3wrKbUwvsywVHWGbMgM1dcIf9gPWNDS1/dgTa/7Iexk2mmAHHsP9MeCsA==", + "requires": { + "nested-error-stacks": "^2", + "reflect-metadata": ">=0.1.12", + "tsutils": "^3.17.1" + } }, "universalify": { "version": "0.1.2", diff --git a/package.json b/package.json index 5ca8b49..8e2b0dc 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "nodemailer": "^6.4.2", "redis": "^4.3.1", "tga": "^1.0.4", + "typescript-is": "^0.19.0", "validator": "^13.7.0", "xmlbuilder": "^13.0.2", "xmlbuilder2": "0.0.4" @@ -64,6 +65,7 @@ "@types/node": "^18.14.4", "@types/node-rsa": "^1.1.1", "@types/nodemailer": "^6.4.7", + "@types/qs": "^6.9.7", "@types/validator": "^13.7.14", "@typescript-eslint/eslint-plugin": "^5.54.1", "@typescript-eslint/parser": "^5.54.1", diff --git a/src/middleware/api.ts b/src/middleware/api.ts index e4fa82a..c196d0b 100644 --- a/src/middleware/api.ts +++ b/src/middleware/api.ts @@ -1,9 +1,10 @@ import express from 'express'; +import { getValueFromHeaders } from '@/util'; import { getUserBearer } from '@/database'; import { HydratedPNIDDocument } from '@/types/mongoose/pnid'; async function APIMiddleware(request: express.Request, _response: express.Response, next: express.NextFunction): Promise { - const authHeader: string | undefined = request.headers.authorization; + const authHeader: string | undefined = getValueFromHeaders(request.headers, 'authorization'); if (!authHeader || !(authHeader.startsWith('Bearer'))) { return next(); diff --git a/src/middleware/client-header.ts b/src/middleware/client-header.ts index 18b21fd..fc9c1e5 100644 --- a/src/middleware/client-header.ts +++ b/src/middleware/client-header.ts @@ -1,5 +1,6 @@ import express from 'express'; import xmlbuilder from 'xmlbuilder'; +import { getValueFromHeaders } from '@/util'; const VALID_CLIENT_ID_SECRET_PAIRS: { [key: string]: string } = { // * 'Key' is the client ID, 'Value' is the client secret @@ -14,8 +15,8 @@ function nintendoClientHeaderCheck(request: express.Request, response: express.R response.set('Server', 'Nintendo 3DS (http)'); response.set('X-Nintendo-Date', new Date().getTime().toString()); - const clientId: string = request.headers['x-nintendo-client-id'] as string; - const clientSecret: string = request.headers['x-nintendo-client-secret'] as string; + const clientId: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-client-id'); + const clientSecret: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-client-secret'); if ( !clientId || diff --git a/src/middleware/device-certificate.ts b/src/middleware/device-certificate.ts index e74c11c..9e7d224 100644 --- a/src/middleware/device-certificate.ts +++ b/src/middleware/device-certificate.ts @@ -1,8 +1,9 @@ import express from 'express'; import NintendoCertificate from '@/nintendo-certificate'; +import { getValueFromHeaders } from '@/util'; function deviceCertificateMiddleware(request: express.Request, _response: express.Response, next: express.NextFunction): void { - const certificate: string = request.headers['x-nintendo-device-cert'] as string; + const certificate: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-device-cert'); if (!certificate) { return next(); diff --git a/src/middleware/pnid.ts b/src/middleware/pnid.ts index 96f58cd..c1e0933 100644 --- a/src/middleware/pnid.ts +++ b/src/middleware/pnid.ts @@ -1,10 +1,11 @@ import express from 'express'; import xmlbuilder from 'xmlbuilder'; +import { getValueFromHeaders } from '@/util'; import { getUserBasic, getUserBearer } from '@/database'; import { HydratedPNIDDocument } from '@/types/mongoose/pnid'; async function PNIDMiddleware(request: express.Request, response: express.Response, next: express.NextFunction): Promise { - const authHeader: string | undefined = request.headers.authorization; + const authHeader: string | undefined = getValueFromHeaders(request.headers, 'authorization'); if (!authHeader || !(authHeader.startsWith('Bearer') || authHeader.startsWith('Basic'))) { return next(); diff --git a/src/middleware/ratelimit.ts b/src/middleware/ratelimit.ts index e6909f2..0cc8b38 100644 --- a/src/middleware/ratelimit.ts +++ b/src/middleware/ratelimit.ts @@ -1,12 +1,18 @@ import crypto from 'node:crypto'; import express from 'express'; import ratelimit from 'express-rate-limit'; +import { getValueFromHeaders } from '@/util'; export default ratelimit({ windowMs: 60 * 1000, max: 1, keyGenerator: (request: express.Request) => { - const data: string = request.headers['x-nintendo-device-cert'] as string; + let data: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-device-cert'); + + if (!data) { + data = request.ip; + } + return crypto.createHash('md5').update(data).digest('hex'); } }); \ No newline at end of file diff --git a/src/middleware/xml-parser.ts b/src/middleware/xml-parser.ts index cf1335e..31bb8b4 100644 --- a/src/middleware/xml-parser.ts +++ b/src/middleware/xml-parser.ts @@ -1,11 +1,12 @@ import express from 'express'; import xmlbuilder from 'xmlbuilder'; import { document as xmlParser } from 'xmlbuilder2'; +import { getValueFromHeaders } from '@/util'; function XMLMiddleware(request: express.Request, response: express.Response, next: express.NextFunction): void { if (request.method == 'POST' || request.method == 'PUT') { - const contentType: string | undefined = request.headers['content-type']; - const contentLength: string | undefined = request.headers['content-length']; + const contentType: string | undefined = getValueFromHeaders(request.headers, 'content-type'); + const contentLength: string | undefined = getValueFromHeaders(request.headers, 'content-length'); let body: string = ''; if ( diff --git a/src/nintendo-certificate.ts b/src/nintendo-certificate.ts index 92dc9f0..4cc3105 100644 --- a/src/nintendo-certificate.ts +++ b/src/nintendo-certificate.ts @@ -42,7 +42,7 @@ const CTR_LFCS_B_PUB = Buffer.from([ ]); // Signature options -const SIGNATURE_SIZES: { [key: string]: SignatureSize } = { +const SIGNATURE_SIZES = { RSA_4096_SHA1: { SIZE: 0x200, PADDING_SIZE: 0x3C @@ -67,7 +67,7 @@ const SIGNATURE_SIZES: { [key: string]: SignatureSize } = { SIZE: 0x3C, PADDING_SIZE: 0x40 } -}; +} as const; class NintendoCertificate { _certificate: Buffer; diff --git a/src/server.ts b/src/server.ts index 52fe75a..c18941f 100644 --- a/src/server.ts +++ b/src/server.ts @@ -9,7 +9,7 @@ import morgan from 'morgan'; import xmlparser from '@/middleware/xml-parser'; import { connect as connectCache } from '@/cache'; import { connect as connectDatabase } from '@/database'; -import { fullUrl } from '@/util'; +import { fullUrl, getValueFromHeaders } from '@/util'; import { LOG_INFO, LOG_SUCCESS, LOG_WARN } from '@/logger'; import conntest from '@/services/conntest'; @@ -49,7 +49,11 @@ app.use(assets); LOG_INFO('Creating 404 status handler'); app.use((request: express.Request, response: express.Response) => { const url: string = fullUrl(request); - const deviceId: string = request.headers['X-Nintendo-Device-ID'] as string || 'Unknown'; + let deviceId: string | undefined = getValueFromHeaders(request.headers, 'X-Nintendo-Device-ID'); + + if (!deviceId) { + deviceId = 'Unknown'; + } LOG_WARN(`HTTP 404 at ${url} from ${deviceId}`); @@ -66,7 +70,11 @@ LOG_INFO('Creating non-404 status handler'); app.use((error: any, request: express.Request, response: express.Response, _next: express.NextFunction) => { const status: number = error.status || 500; const url: string = fullUrl(request); - const deviceId: string = request.headers['X-Nintendo-Device-ID'] as string || 'Unknown'; + let deviceId: string | undefined = getValueFromHeaders(request.headers, 'X-Nintendo-Device-ID'); + + if (!deviceId) { + deviceId = 'Unknown'; + } LOG_WARN(`HTTP ${status} at ${url} from ${deviceId}: ${error.message}`); diff --git a/src/services/api/routes/v1/email.ts b/src/services/api/routes/v1/email.ts index a7c6eb7..2db0fd6 100644 --- a/src/services/api/routes/v1/email.ts +++ b/src/services/api/routes/v1/email.ts @@ -1,13 +1,13 @@ import express from 'express'; import moment from 'moment'; import { PNID } from '@/models/pnid'; -import { sendEmailConfirmedEmail } from '@/util'; +import { getValueFromQueryString, sendEmailConfirmedEmail } from '@/util'; import { HydratedPNIDDocument } from '@/types/mongoose/pnid'; const router: express.Router = express.Router(); router.get('/verify', async (request: express.Request, response: express.Response) => { - const token: string = request.query.token as string; + const token: string | undefined = getValueFromQueryString(request.query, 'token'); if (!token || token.trim() == '') { return response.status(400).json({ diff --git a/src/services/local-cdn/routes/get.ts b/src/services/local-cdn/routes/get.ts index ff18990..6c98b71 100644 --- a/src/services/local-cdn/routes/get.ts +++ b/src/services/local-cdn/routes/get.ts @@ -4,7 +4,7 @@ import { getLocalCDNFile } from '@/cache'; const router: express.Router = express.Router(); router.get('/*', async (request: express.Request, response: express.Response) => { - const filePath: string = request.params[0] as string; + const filePath: string = request.params[0]; const file: Buffer = await getLocalCDNFile(filePath); diff --git a/src/services/nnid/routes/admin.ts b/src/services/nnid/routes/admin.ts index 6a4437c..f1b63b4 100644 --- a/src/services/nnid/routes/admin.ts +++ b/src/services/nnid/routes/admin.ts @@ -1,5 +1,6 @@ import express from 'express'; import xmlbuilder from 'xmlbuilder'; +import { getValueFromQueryString } from '@/util'; import { PNID } from '@/models/pnid'; import { HydratedPNIDDocument } from '@/types/mongoose/pnid'; @@ -11,19 +12,26 @@ const router: express.Router = express.Router(); * Description: Maps between NNID usernames and PIDs */ router.get('/mapped_ids', async (request: express.Request, response: express.Response) => { - const inputType: string = request.query.input_type as string; - const outputType: string = request.query.output_type as string; - let inputList: string[]; + const inputType: string | undefined = getValueFromQueryString(request.query, 'input_type'); + const outputType: string | undefined = getValueFromQueryString(request.query, 'output_type'); + const input: string | undefined = getValueFromQueryString(request.query, 'input'); + + if (!inputType || !outputType || !input) { + return response.status(400).send(xmlbuilder.create({ + errors: { + error: { + cause: 'Bad Request', + code: '1600', + message: 'Unable to process request' + } + } + }).end()); + } + + let inputList: string[] = input.split(','); let queryInput: string; let queryOutput: string; - if (Array.isArray(request.query.input)) { - inputList = request.query.input as string[]; - } else { - const input = request.query.input as string; - inputList = input.split(','); - } - inputList = inputList.filter(input => input); // * Remove null inputs if (inputType === 'user_id') { diff --git a/src/services/nnid/routes/miis.ts b/src/services/nnid/routes/miis.ts index 368cb9e..4333e29 100644 --- a/src/services/nnid/routes/miis.ts +++ b/src/services/nnid/routes/miis.ts @@ -1,5 +1,6 @@ import express from 'express'; import xmlbuilder from 'xmlbuilder'; +import { getValueFromQueryString } from '@/util'; import { PNID } from '@/models/pnid'; import { config } from '@/config-manager'; import { HydratedPNIDDocument } from '@/types/mongoose/pnid'; @@ -13,15 +14,22 @@ const router: express.Router = express.Router(); * Description: Returns a list of NNID miis */ router.get('/', async (request: express.Request, response: express.Response) => { - let pids: number[]; + const input: string | undefined = getValueFromQueryString(request.query, 'pids'); - if (Array.isArray(request.query.pids)) { - pids = request.query.pids.map(pid => Number(pid)); - } else { - const input = request.query.pids as string; - pids = input.split(',').map(pid => Number(pid)); + if (!input) { + return response.status(400).send(xmlbuilder.create({ + errors: { + error: { + cause: 'Bad Request', + code: '1600', + message: 'Unable to process request' + } + } + }).end()); } + const pids: number[] = input.split(',').map(pid => Number(pid)); + const results: HydratedPNIDDocument[] = await PNID.where('pid', pids); const miis: { data: string; diff --git a/src/services/nnid/routes/people.ts b/src/services/nnid/routes/people.ts index 3258375..c145759 100644 --- a/src/services/nnid/routes/people.ts +++ b/src/services/nnid/routes/people.ts @@ -7,7 +7,7 @@ import mongoose from 'mongoose'; import deviceCertificateMiddleware from '@/middleware/device-certificate'; import ratelimit from '@/middleware/ratelimit'; import { connection as databaseConnection, doesUserExist, getUserProfileJSONByPID } from '@/database'; -import { nintendoPasswordHash, sendConfirmationEmail } from '@/util'; +import { getValueFromHeaders, nintendoPasswordHash, sendConfirmationEmail } from '@/util'; import { PNID } from '@/models/pnid'; import { NEXAccount } from '@/models/nex-account'; import { LOG_ERROR } from '@/logger'; @@ -292,12 +292,25 @@ router.get('/@me/devices', async (request: express.Request, response: express.Re response.set('X-Nintendo-Date', new Date().getTime().toString()); const pnid: HydratedPNIDDocument | null = request.pnid; - const deviceId: string = request.headers['x-nintendo-device-id'] as string; - const acceptLanguage: string = request.headers['accept-language'] as string; - const platformId: string = request.headers['x-nintendo-platform-id'] as string; - const region: string = request.headers['x-nintendo-region'] as string; - const serialNumber: string = request.headers['x-nintendo-serial-number'] as string; - const systemVersion: string = request.headers['x-nintendo-system-version'] as string; + const deviceId: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-device-id'); + const acceptLanguage: string | undefined = getValueFromHeaders(request.headers, 'accept-language'); + const platformId: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-platform-id'); + const region: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-region'); + const serialNumber: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-serial-number'); + const systemVersion: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-system-version'); + + if (!deviceId || !acceptLanguage || !platformId || !region || !serialNumber || !systemVersion) { + // TODO - Research these error more + return response.status(400).send(xmlbuilder.create({ + errors: { + error: { + cause: 'Bad Request', + code: '1600', + message: 'Unable to process request' + } + } + }).end()); + } if (!pnid) { // TODO - Research this error more diff --git a/src/services/nnid/routes/provider.ts b/src/services/nnid/routes/provider.ts index e2cfa4d..793739c 100644 --- a/src/services/nnid/routes/provider.ts +++ b/src/services/nnid/routes/provider.ts @@ -2,7 +2,7 @@ import express from 'express'; import xmlbuilder from 'xmlbuilder'; import fs from 'fs-extra'; import { getServerByTitleId, getServer } from '@/database'; -import { generateToken } from '@/util'; +import { generateToken, getValueFromHeaders, getValueFromQueryString } from '@/util'; import { getServicePublicKey, getServiceSecretKey, getNEXPublicKey, getNEXSecretKey } from '@/cache'; import { NEXAccount } from '@/models/nex-account'; import { CryptoOptions } from '@/types/common/crypto-options'; @@ -35,7 +35,20 @@ router.get('/service_token/@me', async (request: express.Request, response: expr }).end()); } - const titleId: string = request.headers['x-nintendo-title-id'] as string; + const titleId: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-title-id'); + + if (!titleId) { + // TODO - Research this error more + return response.send(xmlbuilder.create({ + errors: { + error: { + code: '1021', + message: 'The requested game server was not found' + } + } + }).end()); + } + const serverAccessLevel: string = pnid.get('server_access_level'); const server: HydratedServerDocument | null = await getServerByTitleId(titleId, serverAccessLevel); @@ -121,7 +134,7 @@ router.get('/nex_token/@me', async (request: express.Request, response: express. }).end()); } - const gameServerID: string = request.query.game_server_id as string; + const gameServerID: string | undefined = getValueFromQueryString(request.query, 'game_server_id'); if (!gameServerID) { return response.send(xmlbuilder.create({ @@ -152,7 +165,19 @@ router.get('/nex_token/@me', async (request: express.Request, response: express. const ip: string = server.ip; const port: number = server.port; const device: number = server.device; - const titleId: string = request.headers['x-nintendo-title-id'] as string; + const titleId: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-title-id'); + + if (!titleId) { + // TODO - Research this error more + return response.send(xmlbuilder.create({ + errors: { + error: { + code: '1021', + message: 'The requested game server was not found' + } + } + }).end()); + } const cryptoPath: string = `${__dirname}/../../../../certs/nex/${serverName}`; diff --git a/src/types/common/safe-qs.ts b/src/types/common/safe-qs.ts new file mode 100644 index 0000000..a17df29 --- /dev/null +++ b/src/types/common/safe-qs.ts @@ -0,0 +1,3 @@ +export interface SafeQs { + [key: string]: string | undefined +} \ No newline at end of file diff --git a/src/util.ts b/src/util.ts index 5a79d1f..d791840 100644 --- a/src/util.ts +++ b/src/util.ts @@ -5,6 +5,7 @@ import aws from 'aws-sdk'; import fs from 'fs-extra'; import express from 'express'; import mongoose from 'mongoose'; +import { ParsedQs } from 'qs'; import { sendMail } from '@/mailer'; import { getServiceAESKey, getServicePrivateKey, getServiceSecretKey, getServicePublicKey } from '@/cache'; import { config, disabledFeatures } from '@/config-manager'; @@ -13,6 +14,8 @@ import { TokenOptions } from '@/types/common/token-options'; import { Token } from '@/types/common/token'; import { IPNID, IPNIDMethods } from '@/types/mongoose/pnid'; import { MailerOptions } from '@/types/common/mailer-options'; +import { SafeQs } from '@/types/common/safe-qs'; +import { IncomingHttpHeaders } from 'node:http'; let s3: aws.S3; @@ -320,4 +323,55 @@ export async function sendForgotPasswordEmail(pnid: mongoose.HydratedDocumentproperty); + value = (property)[key]; + } else { + value = property; + } + } + + return value; +} + +export function getValueFromHeaders(headers: IncomingHttpHeaders, key: string): string | undefined { + let header: string | string[] | undefined = headers[key]; + let value: string | undefined; + + if (header) { + if (Array.isArray(header)) { + header = header[0]; + } + + value = header; + } + + return value; } \ No newline at end of file