feat(nnas): centralize error response creation

This commit is contained in:
Jonathan Barrow
2025-04-11 18:50:48 -04:00
parent b47bba5870
commit c10e5be3cd
11 changed files with 513 additions and 539 deletions

View File

@@ -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<string, string> = {
// * '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();

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();

View File

@@ -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<string, any>;
}
// 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);
}

View File

@@ -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(',');

View File

@@ -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));

View File

@@ -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 {

View File

@@ -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

View File

@@ -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)) {

View File

@@ -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<void> => {
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);