From c10e5be3cd592cac0b87397b48a206ca08e35d42 Mon Sep 17 00:00:00 2001 From: Jonathan Barrow Date: Fri, 11 Apr 2025 18:50:48 -0400 Subject: [PATCH] feat(nnas): centralize error response creation --- src/middleware/nnas-basic-header-check.ts | 160 ++++++++-------- src/middleware/nnas-check-device.ts | 144 +++++++------- src/middleware/pnid.ts | 52 +++-- src/middleware/xml-parser.ts | 13 +- src/services/nnas/create-response.ts | 44 +++++ src/services/nnas/routes/admin.ts | 13 +- src/services/nnas/routes/miis.ts | 13 +- src/services/nnas/routes/oauth.ts | 149 +++++++-------- src/services/nnas/routes/people.ts | 219 ++++++++++------------ src/services/nnas/routes/provider.ts | 134 ++++++------- src/services/nnas/routes/support.ts | 111 +++++------ 11 files changed, 513 insertions(+), 539 deletions(-) create mode 100644 src/services/nnas/create-response.ts diff --git a/src/middleware/nnas-basic-header-check.ts b/src/middleware/nnas-basic-header-check.ts index c6c67c5..8ab1207 100644 --- a/src/middleware/nnas-basic-header-check.ts +++ b/src/middleware/nnas-basic-header-check.ts @@ -1,5 +1,5 @@ import express from 'express'; -import xmlbuilder from 'xmlbuilder'; +import { createNNASErrorResponse } from '@/services/nnas/create-response'; const VALID_CLIENT_ID_SECRET_PAIRS: Record = { // * 'Key' is the client ID, 'Value' is the client secret @@ -50,14 +50,14 @@ function nnasBasicHeaderCheckMiddleware(request: express.Request, response: expr // * 0 = 3DS, 1 = Wii U if (platformID === undefined || (platformID !== '0' && platformID !== '1')) { - response.send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '0002', message: 'platformId format is invalid' } - } - }).end()); + ] + }); return; } @@ -65,80 +65,80 @@ function nnasBasicHeaderCheckMiddleware(request: express.Request, response: expr // * 1 = debug, 2 = retail if (deviceType === undefined || (deviceType !== '1' && deviceType !== '2')) { // TODO - Unsure if this is the right error - response.send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '0002', message: 'Device type format is invalid' } - } - }).end()); + ] + }); return; } if (deviceID === undefined || !DEVICE_ID.test(deviceID)) { - response.send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '0002', message: 'deviceId format is invalid' } - } - }).end()); + ] + }); return; } if (serialNumber === undefined || !SERIAL_REGEX.test(serialNumber)) { - response.send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '0002', message: 'serialNumber format is invalid' } - } - }).end()); + ] + }); return; } // TODO - Should the version check throw SYSTEM_UPDATE_REQUIRED? if (systemVersion === undefined || SYSTEM_VERSIONS[platformID] !== systemVersion) { - response.send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '0002', message: 'version format is invalid' } - } - }).end()); + ] + }); return; } if (region === undefined || !REGIONS.includes(region)) { - response.send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '0002', message: 'X-Nintendo-Region format is invalid' } - } - }).end()); + ] + }); return; } if (country === undefined) { - response.send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '0002', message: 'X-Nintendo-Country format is invalid' } - } - }).end()); + ] + }); return; } @@ -150,112 +150,110 @@ function nnasBasicHeaderCheckMiddleware(request: express.Request, response: expr !VALID_CLIENT_ID_SECRET_PAIRS[clientID] || clientSecret !== VALID_CLIENT_ID_SECRET_PAIRS[clientID] ) { - response.send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { cause: 'client_id', code: '0004', message: 'API application invalid or incorrect application credentials' } - } - }).end()); + ] + }); return; } if (friendsVersion === undefined || friendsVersion !== '0000') { // TODO - Unsure if this is the right error - response.send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '0002', message: 'Friends version is invalid' } - } - }).end()); + ] + }); return; } // TODO - Check this against valid list if (environment === undefined) { - response.send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '1017', message: 'The requested game environment wasn\'t found for the given game server.' } - } - }).end()); + ] + }); return; } if (titleID === undefined) { // TODO - Unsure if this is the right error - response.send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '0002', message: 'Title ID format is invalid' } - } - }).end()); + ] + }); return; } if (uniqueID === undefined) { // TODO - Unsure if this is the right error - response.send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '0002', message: 'Unique ID format is invalid' } - } - }).end()); + ] + }); return; } if (applicationVersion === undefined) { // TODO - Unsure if this is the right error - response.send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '0002', message: 'Application version format is invalid' } - } - }).end()); + ] + }); return; } if (platformID === '0' && model === undefined) { // TODO - Unsure if this is the right error - response.send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '0002', message: 'Model format is invalid' } - } - }).end()); - - return; + ] + }); } if (platformID === '0' && deviceCertificate === undefined) { - response.status(400).send(xmlbuilder.create({ - error: { - code: '0110', - message: 'Unlinked device' - } - }).end()); - - return; + return createNNASErrorResponse(response, { + errors: [ + { + code: '0110', + message: 'Unlinked device' + } + ] + }); } return next(); diff --git a/src/middleware/nnas-check-device.ts b/src/middleware/nnas-check-device.ts index 210b48b..41a9417 100644 --- a/src/middleware/nnas-check-device.ts +++ b/src/middleware/nnas-check-device.ts @@ -1,6 +1,6 @@ import crypto from 'node:crypto'; import express from 'express'; -import xmlbuilder from 'xmlbuilder'; +import { createNNASErrorResponse } from '@/services/nnas/create-response'; import NintendoCertificate from '@/nintendo-certificate'; import { Device } from '@/models/device'; @@ -40,56 +40,56 @@ async function nnasCheckDeviceMiddleware(request: express.Request, response: exp // * 3DS ALWAYS sends the device certificate if (platformID === '0' && deviceCertificate == undefined) { - response.status(400).send(xmlbuilder.create({ - error: { - code: '0110', - message: 'Unlinked device' - } - }).end()); - - return; + return createNNASErrorResponse(response, { + errors: [ + { + code: '0110', + message: 'Unlinked device' + } + ] + }); } const shouldCheckCertificate = deviceCertificate !== undefined || platformID === '0' || REQUIRED_CERT_CHECK_ENDPOINTS.some(regex => regex.test(path)); if (shouldCheckCertificate) { if (deviceCertificate === undefined) { - response.status(400).send(xmlbuilder.create({ - error: { - code: '0110', - message: 'Unlinked device' - } - }).end()); - - return; + return createNNASErrorResponse(response, { + errors: [ + { + code: '0110', + message: 'Unlinked device' + } + ] + }); } const certificate = new NintendoCertificate(deviceCertificate); if (!certificate.valid) { - response.status(400).send(xmlbuilder.create({ - error: { - code: '0110', - message: 'Unlinked device' - } - }).end()); - - return; + return createNNASErrorResponse(response, { + errors: [ + { + code: '0110', + message: 'Unlinked device' + } + ] + }); } const certificateDeviceID = parseInt(certificate.certificateName.slice(2).split('-')[0], 16); if (deviceID !== certificateDeviceID) { // TODO - Change this to a different error - response.status(400).send(xmlbuilder.create({ - error: { - cause: 'Bad Request', - code: '1600', - message: 'Unable to process request' - } - }).end()); - - return; + return createNNASErrorResponse(response, { + errors: [ + { + cause: 'Bad Request', + code: '1600', + message: 'Unable to process request' + } + ] + }); } let device = await Device.findOne({ @@ -102,14 +102,14 @@ async function nnasCheckDeviceMiddleware(request: express.Request, response: exp // * the time the device document was created. Therefore we can // * know that serial tampering happened on the 3DS if this fails // * to find a device document. - response.status(400).send(xmlbuilder.create({ - error: { - code: '0002', - message: 'serialNumber format is invalid' - } - }).end()); - - return; + return createNNASErrorResponse(response, { + errors: [ + { + code: '0002', + message: 'serialNumber format is invalid' + } + ] + }); } // * Update 3DS consoles to sync with the data from NASC @@ -139,15 +139,15 @@ async function nnasCheckDeviceMiddleware(request: express.Request, response: exp if (certificate.consoleType === '3ds') { // * If this happens, something has gone horribly wrong // TODO - Change this to a different error - response.status(400).send(xmlbuilder.create({ - error: { - cause: 'Bad Request', - code: '1600', - message: 'Unable to process request' - } - }).end()); - - return; + return createNNASErrorResponse(response, { + errors: [ + { + cause: 'Bad Request', + code: '1600', + message: 'Unable to process request' + } + ] + }); } // * Assume device is a Wii U we've never seen before @@ -163,15 +163,15 @@ async function nnasCheckDeviceMiddleware(request: express.Request, response: exp if (device.serial !== serialNumber) { // * Spoofed serial. Device ID compared to certificate directly earlier // TODO - Change this to a different error - response.status(400).send(xmlbuilder.create({ - error: { - cause: 'Bad Request', - code: '1600', - message: 'Unable to process request' - } - }).end()); - - return; + return createNNASErrorResponse(response, { + errors: [ + { + cause: 'Bad Request', + code: '1600', + message: 'Unable to process request' + } + ] + }); } request.device = device; @@ -186,30 +186,26 @@ async function nnasCheckDeviceMiddleware(request: express.Request, response: exp }); if (!device) { - response.status(400).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { cause: 'device_id', code: '0113', message: 'Unauthorized device' } - } - }).end()); - - return; + ] + }); } if (device.access_level < 0) { - response.status(400).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '0012', message: 'Device has been banned by game server' // TODO - This is not the right error message } - } - }).end()); - - return; + ] + }); } request.device = device; diff --git a/src/middleware/pnid.ts b/src/middleware/pnid.ts index 9fa7ed9..489625e 100644 --- a/src/middleware/pnid.ts +++ b/src/middleware/pnid.ts @@ -1,6 +1,6 @@ import express from 'express'; -import xmlbuilder from 'xmlbuilder'; import { getValueFromHeaders } from '@/util'; +import { createNNASErrorResponse } from '@/services/nnas/create-response'; import { getPNIDByBasicAuth, getPNIDByNNASAccessToken } from '@/database'; import { HydratedPNIDDocument } from '@/types/mongoose/pnid'; @@ -28,55 +28,49 @@ async function PNIDMiddleware(request: express.Request, response: express.Respon if (!pnid) { if (type === 'Bearer') { - response.status(401).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + status: 401, + errors: [ + { cause: 'access_token', code: '0005', message: 'Invalid access token' } - } - }).end()); - - return; + ] + }); } - response.status(401).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + status: 401, + errors: [ + { code: '1105', message: 'Email address, username, or password, is not valid' } - } - }).end()); - - return; + ] + }); } if (pnid.deleted) { - response.status(400).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '0112', message: pnid.username } - } - }).end()); - - return; + ] + }); } if (pnid.access_level < 0) { - response.status(400).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '0108', message: 'Account has been banned' } - } - }).end()); - - return; + ] + }); } request.pnid = pnid; diff --git a/src/middleware/xml-parser.ts b/src/middleware/xml-parser.ts index 9732053..2700b78 100644 --- a/src/middleware/xml-parser.ts +++ b/src/middleware/xml-parser.ts @@ -1,7 +1,7 @@ import express from 'express'; -import xmlbuilder from 'xmlbuilder'; import { document as xmlParser } from 'xmlbuilder2'; import { getValueFromHeaders, mapToObject } from '@/util'; +import { createNNASErrorResponse } from '@/services/nnas/create-response'; function XMLMiddleware(request: express.Request, response: express.Response, next: express.NextFunction): void { if (request.method == 'POST' || request.method == 'PUT') { @@ -35,14 +35,15 @@ function XMLMiddleware(request: express.Request, response: express.Response, nex request.body = mapToObject(request.body); } catch (error) { // TODO - This is not a real error code, check to see if better one exists - return response.status(401).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + status: 401, + errors: [ + { code: '0004', message: 'XML parse error' } - } - }).end()); + ] + }); } next(); diff --git a/src/services/nnas/create-response.ts b/src/services/nnas/create-response.ts new file mode 100644 index 0000000..7ca5c5e --- /dev/null +++ b/src/services/nnas/create-response.ts @@ -0,0 +1,44 @@ +import express from 'express'; +import xmlbuilder from 'xmlbuilder'; + +// TODO - I'm unsure if this is the best place for this file to be or the best place to put these functions. It's fine for now though +// TODO - Support JSON mode + +type CreateNNASErrorResponseOptions = { + status?: number; + errors: { + cause?: string; + code?: string; + message?: string; + }[]; +} + +type CreateNNASResponseOptions = { + status?: number; + body: Record; +} + +// TODO - This can largely be removed once an upgrade to Express v5 is done, since v5 supports throwing errors in async routes +export function createNNASErrorResponse(response: express.Response, options: CreateNNASErrorResponseOptions): void { + createNNASResponse(response, { + status: options.status || 400, + body: { + // TODO - This is NOT suitable for JSON mode. Hack to get xmlbuilder to properly build XML error lists in the way we need it to. NNAS formats JSON error lists as `{"errors":[{"code":"0107","message":"Account country and device country do not match"}]}` + errors: { + error: options.errors + } + } + }); +} + +export function createNNASResponse(response: express.Response, options: CreateNNASResponseOptions): void { + response.set('Content-Type', 'text/xml'); + response.set('Server', 'Nintendo 3DS (http)'); + response.set('X-Nintendo-Date', new Date().getTime().toString()); + + const body = xmlbuilder + .create(options.body) + .commentBefore('WARNING! DO NOT SHARE ANYTHING IN THIS REQUEST OR RESPONSE WITH UNTRUSTED USERS! REQUESTS AND RESPONSES CONTAIN SENSITIVE INFORMATION ABOUT YOUR DEVICE/ACCOUNT SUCH AS PASSWORDS, EMAILS, CERTIFICATES, ETC!').end(); + + response.status(options.status || 200).send(body); +} \ No newline at end of file diff --git a/src/services/nnas/routes/admin.ts b/src/services/nnas/routes/admin.ts index c08c356..91b69b3 100644 --- a/src/services/nnas/routes/admin.ts +++ b/src/services/nnas/routes/admin.ts @@ -1,6 +1,7 @@ import express from 'express'; import xmlbuilder from 'xmlbuilder'; import { getValueFromQueryString } from '@/util'; +import { createNNASErrorResponse } from '@/services/nnas/create-response'; import { PNID } from '@/models/pnid'; const router = express.Router(); @@ -16,17 +17,15 @@ router.get('/mapped_ids', async (request: express.Request, response: express.Res const input = getValueFromQueryString(request.query, 'input'); if (!inputType || !outputType || !input) { - response.status(400).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { cause: 'Bad Request', code: '1600', message: 'Unable to process request' } - } - }).end()); - - return; + ] + }); } let inputList = input.split(','); diff --git a/src/services/nnas/routes/miis.ts b/src/services/nnas/routes/miis.ts index 85487bc..5182b79 100644 --- a/src/services/nnas/routes/miis.ts +++ b/src/services/nnas/routes/miis.ts @@ -1,6 +1,7 @@ import express from 'express'; import xmlbuilder from 'xmlbuilder'; import { getValueFromQueryString } from '@/util'; +import { createNNASErrorResponse } from '@/services/nnas/create-response'; import { PNID } from '@/models/pnid'; import { config } from '@/config-manager'; import { YesNoBoolString } from '@/types/common/yes-no-bool-string'; @@ -16,17 +17,15 @@ router.get('/', async (request: express.Request, response: express.Response): Pr const input = getValueFromQueryString(request.query, 'pids'); if (!input) { - response.status(400).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { cause: 'Bad Request', code: '1600', message: 'Unable to process request' } - } - }).end()); - - return; + ] + }); } const pids = input.split(',').map(pid => Number(pid)).filter(pid => !isNaN(pid)); diff --git a/src/services/nnas/routes/oauth.ts b/src/services/nnas/routes/oauth.ts index 4cf8eac..2eba4bc 100644 --- a/src/services/nnas/routes/oauth.ts +++ b/src/services/nnas/routes/oauth.ts @@ -3,6 +3,7 @@ import xmlbuilder from 'xmlbuilder'; import bcrypt from 'bcrypt'; import { getPNIDByNNASRefreshToken, getPNIDByUsername } from '@/database'; import { generateOAuthTokens } from '@/util'; +import { createNNASErrorResponse } from '@/services/nnas/create-response'; import { Device } from '@/models/device'; import { SystemType } from '@/types/common/token'; @@ -20,95 +21,97 @@ router.post('/access_token/generate', async (request: express.Request, response: const refreshToken = request.body.refresh_token; if (!['password', 'refresh_token'].includes(grantType)) { - response.status(400).send(xmlbuilder.create({ - error: { - cause: 'grant_type', - code: '0004', - message: 'Invalid Grant Type' - } - }).end()); - - return; + return createNNASErrorResponse(response, { + errors: [ + { + cause: 'grant_type', + code: '0004', + message: 'Invalid Grant Type' + } + ] + }); } let pnid = null; if (grantType === 'password') { if (!username || username.trim() === '') { - response.status(400).send(xmlbuilder.create({ - error: { - cause: 'user_id', - code: '0002', - message: 'user_id format is invalid' - } - }).end()); - - return; + return createNNASErrorResponse(response, { + errors: [ + { + cause: 'user_id', + code: '0002', + message: 'user_id format is invalid' + } + ] + }); } if (!password || password.trim() === '') { - response.status(400).send(xmlbuilder.create({ - error: { - cause: 'password', - code: '0002', - message: 'password format is invalid' - } - }).end()); - - return; + return createNNASErrorResponse(response, { + errors: [ + { + cause: 'password', + code: '0002', + message: 'password format is invalid' + } + ] + }); } pnid = await getPNIDByUsername(username); + // TODO - Client also sends a password_type field, which is always set to "hashed". If field is missing or not "hashed", assume input password is plain-text if (!pnid || !await bcrypt.compare(password, pnid.password)) { - response.status(400).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '0106', message: 'Invalid account ID or password' } - } - }).end({ pretty: true })); - - return; + ] + }); } } else { if (!refreshToken || refreshToken.trim() === '') { - response.status(400).send(xmlbuilder.create({ - error: { - cause: 'refresh_token', - code: '0106', - message: 'Invalid Refresh Token' - } - }).end()); - - return; + return createNNASErrorResponse(response, { + status: 401, + errors: [ + { + cause: 'refresh_token', + code: '0106', + message: 'Invalid Refresh Token' + } + ] + }); } try { pnid = await getPNIDByNNASRefreshToken(refreshToken); if (!pnid) { - response.status(400).send(xmlbuilder.create({ - error: { + return createNNASErrorResponse(response, { + status: 401, + errors: [ + { + cause: 'refresh_token', + code: '0106', + message: 'Invalid Refresh Token' + } + ] + }); + } + } catch (error) { + return createNNASErrorResponse(response, { + status: 401, + errors: [ + { cause: 'refresh_token', code: '0106', message: 'Invalid Refresh Token' } - }).end()); - - return; - } - } catch (error) { - response.status(400).send(xmlbuilder.create({ - error: { - cause: 'refresh_token', - code: '0106', - message: 'Invalid Refresh Token' - } - }).end()); - - return; + ] + }); } } @@ -116,14 +119,14 @@ router.post('/access_token/generate', async (request: express.Request, response: // * 0112 is the "account deleted" error, but unsure if this unlinks the PNID from the user? // * 0143 is the "The link to this Nintendo Network ID has been temporarliy removed" error, // * maybe that is a better error to use here? - response.status(400).send(xmlbuilder.create({ - error: { - code: '0112', - message: pnid.username - } - }).end()); - - return; + return createNNASErrorResponse(response, { + errors: [ + { + code: '0112', + message: pnid.username + } + ] + }); } // * This are set/validated in consoleStatusVerificationMiddleware @@ -139,16 +142,14 @@ router.post('/access_token/generate', async (request: express.Request, response: } if (pnid.access_level < 0) { - response.status(400).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '0108', message: 'Account has been banned' } - } - }).end()); - - return; + ] + }); } try { diff --git a/src/services/nnas/routes/people.ts b/src/services/nnas/routes/people.ts index ecd6075..ba1e7cf 100644 --- a/src/services/nnas/routes/people.ts +++ b/src/services/nnas/routes/people.ts @@ -6,6 +6,7 @@ import moment from 'moment'; import ratelimit from '@/middleware/ratelimit'; import { connection as databaseConnection, doesPNIDExist, getPNIDProfileJSONByPID } from '@/database'; import { getValueFromHeaders, nintendoPasswordHash, sendConfirmationEmail, sendPNIDDeletedEmail } from '@/util'; +import { createNNASErrorResponse } from '@/services/nnas/create-response'; import { PNID } from '@/models/pnid'; import { NEXAccount } from '@/models/nex-account'; import { LOG_ERROR } from '@/logger'; @@ -28,16 +29,14 @@ router.get('/:username', async (request: express.Request, response: express.Resp const userExists = await doesPNIDExist(username); if (userExists) { - response.status(400).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '0100', message: 'Account ID already exists' } - } - }).end()); - - return; + ] + }); } response.send(); @@ -54,16 +53,14 @@ router.post('/', ratelimit, async (request: express.Request, response: express.R const userExists = await doesPNIDExist(person.user_id); if (userExists) { - response.status(400).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '0100', message: 'Account ID already exists' } - } - }).end()); - - return; + ] + }); } const creationDate = moment().format('YYYY-MM-DDTHH:MM:SS'); @@ -168,15 +165,15 @@ router.post('/', ratelimit, async (request: express.Request, response: express.R await session.abortTransaction(); - response.status(400).send(xmlbuilder.create({ - error: { - cause: 'Bad Request', - code: '1600', - message: 'Unable to process request' - } - }).end()); - - return; + return createNNASErrorResponse(response, { + errors: [ + { + cause: 'Bad Request', + code: '1600', + message: 'Unable to process request' + } + ] + }); } finally { // * This runs regardless of failure // * Returning on catch will not prevent this from running @@ -206,34 +203,32 @@ router.get('/@me/profile', async (request: express.Request, response: express.Re if (!pnid) { // TODO - Research this error more - response.status(404).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + status: 404, + errors: [ + { cause: '', code: '0008', message: 'Not Found' } - } - }).end()); - - return; + ] + }); } const person = await getPNIDProfileJSONByPID(pnid.pid); if (!person) { // TODO - Research this error more - response.status(404).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + status: 404, + errors: [ + { cause: '', code: '0008', message: 'Not Found' } - } - }).end()); - - return; + ] + }); } response.send(xmlbuilder.create({ @@ -261,34 +256,32 @@ router.post('/@me/devices', async (request: express.Request, response: express.R if (!pnid) { // TODO - Research this error more - response.status(404).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + status: 404, + errors: [ + { cause: '', code: '0008', message: 'Not Found' } - } - }).end()); - - return; + ] + }); } const person = await getPNIDProfileJSONByPID(pnid.pid); if (!person) { // TODO - Research this error more - response.status(404).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + status: 404, + errors: [ + { cause: '', code: '0008', message: 'Not Found' } - } - }).end()); - - return; + ] + }); } response.send(xmlbuilder.create({ @@ -316,32 +309,29 @@ router.get('/@me/devices', async (request: express.Request, response: express.Re if (!deviceID || !acceptLanguage || !platformID || !region || !serialNumber || !systemVersion) { // TODO - Research these error more - response.status(400).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { cause: 'Bad Request', code: '1600', message: 'Unable to process request' } - } - }).end()); - - return; + ] + }); } if (!pnid) { // TODO - Research this error more - response.status(404).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + status: 404, + errors: [ + { cause: '', code: '0008', message: 'Not Found' } - } - }).end()); - - return; + ] + }); } response.send(xmlbuilder.create({ @@ -379,34 +369,32 @@ router.get('/@me/devices/owner', async (request: express.Request, response: expr if (!pnid) { // TODO - Research this error more - response.status(404).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + status: 404, + errors: [ + { cause: '', code: '0008', message: 'Not Found' } - } - }).end()); - - return; + ] + }); } const person = await getPNIDProfileJSONByPID(pnid.pid); if (!person) { // TODO - Research this error more - response.status(404).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + status: 404, + errors: [ + { cause: '', code: '0008', message: 'Not Found' } - } - }).end()); - - return; + ] + }); } response.send(xmlbuilder.create({ @@ -440,17 +428,16 @@ router.put('/@me/miis/@primary', async (request: express.Request, response: expr if (!pnid) { // TODO - Research this error more - response.status(404).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + status: 404, + errors: [ + { cause: '', code: '0008', message: 'Not Found' } - } - }).end()); - - return; + ] + }); } const mii: { @@ -482,17 +469,15 @@ router.put('/@me/devices/@current/inactivate', async (request: express.Request, const pnid = request.pnid; if (!pnid) { - response.status(400).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { cause: 'access_token', code: '0002', message: 'Invalid access token' } - } - }).end()); - - return; + ] + }); } response.send(); @@ -507,17 +492,15 @@ router.post('/@me/deletion', async (request: express.Request, response: express. const pnid = request.pnid; if (!pnid) { - response.status(400).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { cause: 'access_token', code: '0002', message: 'Invalid access token' } - } - }).end()); - - return; + ] + }); } const email = pnid.email.address; @@ -544,17 +527,15 @@ router.put('/@me', async (request: express.Request, response: express.Response): const person: Person = request.body.person; if (!pnid) { - response.status(400).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { cause: 'access_token', code: '0002', message: 'Invalid access token' } - } - }).end()); - - return; + ] + }); } const gender = person.gender ? person.gender : pnid.gender; @@ -618,17 +599,15 @@ router.get('/@me/emails', async (request: express.Request, response: express.Res const pnid = request.pnid; if (!pnid) { - response.status(400).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { cause: 'access_token', code: '0002', message: 'Invalid access token' } - } - }).end()); - - return; + ] + }); } response.send(xmlbuilder.create({ @@ -663,17 +642,15 @@ router.put('/@me/emails/@primary', async (request: express.Request, response: ex } = request.body.email; if (!pnid || !email || !email.address) { - response.status(400).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { cause: 'access_token', code: '0002', message: 'Invalid access token' } - } - }).end()); - - return; + ] + }); } // TODO - Better email check diff --git a/src/services/nnas/routes/provider.ts b/src/services/nnas/routes/provider.ts index 50c8058..25583cf 100644 --- a/src/services/nnas/routes/provider.ts +++ b/src/services/nnas/routes/provider.ts @@ -2,6 +2,7 @@ import express from 'express'; import xmlbuilder from 'xmlbuilder'; import { getServerByClientID, getServerByGameServerID } from '@/database'; import { generateToken, getValueFromHeaders, getValueFromQueryString, isSystemType } from '@/util'; +import { createNNASErrorResponse } from '@/services/nnas/create-response'; import { NEXAccount } from '@/models/nex-account'; import { SystemType, TokenOptions, TokenType } from '@/types/common/token'; @@ -16,78 +17,68 @@ router.get('/service_token/@me', async (request: express.Request, response: expr const pnid = request.pnid; if (!pnid) { - response.status(400).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { cause: 'access_token', code: '0002', message: 'Invalid access token' } - } - }).end()); - - return; + ] + }); } const clientID = getValueFromQueryString(request.query, 'client_id'); if (!clientID) { // TODO - Research this error more - response.send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '1021', message: 'The requested game server was not found' } - } - }).end()); - - return; + ] + }); } const titleID = getValueFromHeaders(request.headers, 'x-nintendo-title-id'); if (!titleID) { // TODO - Research this error more - response.send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '1021', message: 'The requested game server was not found' } - } - }).end()); - - return; + ] + }); } const serverAccessLevel = pnid.server_access_level; const server = await getServerByClientID(clientID, serverAccessLevel); if (!server || !server.aes_key) { - response.send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '1021', message: 'The requested game server was not found' } - } - }).end()); - - return; + ] + }); } if (server.maintenance_mode) { - response.send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '2002', message: 'The requested game server is under maintenance' } - } - }).end()); - - return; + ] + }); } if (!isSystemType(server.device)) { @@ -125,17 +116,15 @@ router.get('/nex_token/@me', async (request: express.Request, response: express. const pnid = request.pnid; if (!pnid) { - response.status(400).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { cause: 'access_token', code: '0002', message: 'Invalid access token' } - } - }).end()); - - return; + ] + }); } const nexAccount = await NEXAccount.findOne({ @@ -143,77 +132,68 @@ router.get('/nex_token/@me', async (request: express.Request, response: express. }); if (!nexAccount) { - response.status(404).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + status: 404, + errors: [ + { cause: '', code: '0008', message: 'Not Found' } - } - }).end()); - - return; + ] + }); } const gameServerID = getValueFromQueryString(request.query, 'game_server_id'); if (!gameServerID) { - response.send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '0118', message: 'Unique ID and Game Server ID are not linked' } - } - }).end()); - - return; + ] + }); } const serverAccessLevel = pnid.server_access_level; const server = await getServerByGameServerID(gameServerID, serverAccessLevel); if (!server || !server.aes_key) { - response.send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '1021', message: 'The requested game server was not found' } - } - }).end()); - - return; + ] + }); } if (server.maintenance_mode) { - response.send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '2002', message: 'The requested game server is under maintenance' } - } - }).end()); - - return; + ] + }); } const titleID = getValueFromHeaders(request.headers, 'x-nintendo-title-id'); if (!titleID) { // TODO - Research this error more - response.send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '1021', message: 'The requested game server was not found' } - } - }).end()); - - return; + ] + }); } if (!isSystemType(server.device)) { diff --git a/src/services/nnas/routes/support.ts b/src/services/nnas/routes/support.ts index eca842b..5552832 100644 --- a/src/services/nnas/routes/support.ts +++ b/src/services/nnas/routes/support.ts @@ -1,10 +1,10 @@ import dns from 'node:dns'; import express from 'express'; -import xmlbuilder from 'xmlbuilder'; import moment from 'moment'; import { getPNIDByEmailAddress, getPNIDByPID } from '@/database'; -import { Device } from '@/models/device'; import { sendEmailConfirmedEmail, sendConfirmationEmail, sendForgotPasswordEmail, sendEmailConfirmedParentalControlsEmail } from '@/util'; +import { createNNASErrorResponse } from '@/services/nnas/create-response'; +import { Device } from '@/models/device'; // * Middleware to ensure the input device is valid // TODO - Make this available for more routes? This could be useful elsewhere @@ -23,30 +23,26 @@ async function validateDeviceIDMiddleware(request: express.Request, response: ex }); if (!device) { - response.status(400).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { cause: 'device_id', code: '0113', message: 'Unauthorized device' } - } - }).end()); - - return; + ] + }); } if (device.access_level < 0) { - response.status(400).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '0012', message: 'Device has been banned by game server' // TODO - This is not the right error message } - } - }).end()); - - return; + ] + }); } // TODO - Once we push support for linking PNIDs to consoles, also check if the PID is linked or not @@ -65,31 +61,29 @@ router.post('/validate/email', async (request: express.Request, response: expres const email = request.body.email; if (!email) { - response.send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { cause: 'email', code: '0103', message: 'Email format is invalid' } - } - }).end()); - - return; + ] + }); } const domain = email.split('@')[1]; dns.resolveMx(domain, (error: NodeJS.ErrnoException | null) => { if (error) { - return response.send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '1126', - message: 'The domain "' + domain + '" is not accessible.' + message: `The domain "${domain}" is not accessible.` } - } - }).end()); + ] + }); } response.send(); @@ -108,16 +102,14 @@ router.put('/email_confirmation/:pid/:code', async (request: express.Request, re const pnid = await getPNIDByPID(pid); if (!pnid) { - response.status(400).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '0130', message: 'PID has not been registered yet' } - } - }).end()); - - return; + ] + }); } // * If the email is already confirmed don't bother continuing @@ -128,15 +120,14 @@ router.put('/email_confirmation/:pid/:code', async (request: express.Request, re } if (pnid.identification.email_code !== code) { - response.status(400).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '0116', message: 'Missing or invalid verification code' } - } - }).end()); - return; + ] + }); } const validatedDate = moment().format('YYYY-MM-DDTHH:MM:SS'); @@ -164,16 +155,14 @@ router.get('/resend_confirmation', validateDeviceIDMiddleware, async (request: e if (!pnid) { // TODO - Unsure if this is the right error - response.status(400).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '0130', message: 'PID has not been registered yet' } - } - }).end()); - - return; + ] + }); } // * If the email is already confirmed don't bother continuing @@ -200,16 +189,14 @@ router.get('/send_confirmation/pin/:email', async (request: express.Request, res if (!pnid) { // TODO - Unsure if this is the right error - response.status(400).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { code: '0130', message: 'PID has not been registered yet' } - } - }).end()); - - return; + ] + }); } await sendEmailConfirmedParentalControlsEmail(pnid); @@ -226,17 +213,15 @@ router.get('/send_confirmation/pin/:email', async (request: express.Request, res router.get('/forgotten_password/:pid', validateDeviceIDMiddleware, async (request: express.Request, response: express.Response): Promise => { if (!/^\d+$/.test(request.params.pid)) { // * This is what Nintendo sends - response.status(400).send(xmlbuilder.create({ - errors: { - error: { + return createNNASErrorResponse(response, { + errors: [ + { cause: 'Not Found', code: '1600', message: 'Unable to process request' } - } - }).end()); - - return; + ] + }); } const pid = Number(request.params.pid);