diff --git a/.gitignore b/.gitignore index bc708df..2e8b69a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,65 +1,65 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Typescript v1 declaration files -typings/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env - -# custom -sign.js -t.js -config.json -servers.json +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# custom +sign.js +t.js +config.json +servers.json certs \ No newline at end of file diff --git a/create-test-user.js b/create-test-user.js index 9597b0f..494d22e 100644 --- a/create-test-user.js +++ b/create-test-user.js @@ -1,79 +1,79 @@ -const prompt = require('prompt'); -const crypto = require('crypto'); -const util = require('./src/util'); -const database = require('./src/database'); -const { PNID } = require('./src/models/pnid'); - -prompt.message = ''; - -const properties = [ - 'username', - 'email', - { - name: 'password', - hidden: true - } -]; - -prompt.get(properties, function (error, { username, email, password }) { - const date = new Date().toISOString(); - const miiHash = crypto.createHash('md5').update(date).digest('hex'); - - const document = { - pid: 1, - creation_date: date.split('.')[0], - updated: date, - username: username, - password: password, - birthdate: '1990-01-01', - gender: 'M', - country: 'US', - language: 'en', - email: { - address: email, - primary: true, - parent: true, - reachable: true, - validated: true, - id: util.generateRandomInt(10) - }, - region: 0x310B0000, - timezone: { - name: 'America/New_York', - offset: -14400 - }, - mii: { - name: 'UserMii', - primary: true, - data: 'AwAAQIhluwTgxEAA2NlGWQOzuI0n2QAAAEBsAG8AZwBpAG4AdABlAHMAdAAAAEBAAAAhAQJoRBgmNEYUgRIXaA0AACkAUkhQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMw7', // hardcoded for now. currently testing - id: util.generateRandomInt(10), - hash: miiHash, - image_url: 'https://mii-secure.account.nintendo.net/2rtgf01lztoqo_standard.tga', - image_id: util.generateRandomInt(10) - }, - flags: { - active: true, - marketing: false, - off_device: true - }, - validation: { - // These values are temp and will be overwritten before the document saves - // These values are only being defined to get around the `E11000 duplicate key error collection` error - email_code: Date.now(), - email_token: Date.now().toString() - } - }; - - const newUser = new PNID(document); - - newUser.save(async (error, newUser) => { - if (error) { - throw error; - } - - console.log(newUser); - console.log('New user created'); - }); -}); - +const prompt = require('prompt'); +const crypto = require('crypto'); +const util = require('./src/util'); +const database = require('./src/database'); +const { PNID } = require('./src/models/pnid'); + +prompt.message = ''; + +const properties = [ + 'username', + 'email', + { + name: 'password', + hidden: true + } +]; + +prompt.get(properties, function (error, { username, email, password }) { + const date = new Date().toISOString(); + const miiHash = crypto.createHash('md5').update(date).digest('hex'); + + const document = { + pid: 1, + creation_date: date.split('.')[0], + updated: date, + username: username, + password: password, + birthdate: '1990-01-01', + gender: 'M', + country: 'US', + language: 'en', + email: { + address: email, + primary: true, + parent: true, + reachable: true, + validated: true, + id: util.generateRandomInt(10) + }, + region: 0x310B0000, + timezone: { + name: 'America/New_York', + offset: -14400 + }, + mii: { + name: 'bella', + primary: true, + data: 'AwAAQOlVognnx0GC2X0LLQOzuI0n2QAAAUBiAGUAbABsAGEAAABFAAAAAAAAAEBAEgCBAQRoQxggNEYUgRIXaA0AACkDUkhQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP6G', // hardcoded for now. currently testing + id: 1330180812, + hash: '1o54mkuuxfg8t', + image_url: 'https://mii-secure.account.nintendo.net/1o54mkuuxfg8t_standard.tga', + image_id: 1330180887 + }, + flags: { + active: true, + marketing: false, + off_device: true + }, + validation: { + // These values are temp and will be overwritten before the document saves + // These values are only being defined to get around the `E11000 duplicate key error collection` error + email_code: Date.now(), + email_token: Date.now().toString() + } + }; + + const newUser = new PNID(document); + + newUser.save(async (error, newUser) => { + if (error) { + throw error; + } + + console.log(newUser); + console.log('New user created'); + }); +}); + database.connect().then(prompt.start); \ No newline at end of file diff --git a/generate-keys.js b/generate-keys.js index b6e7b98..09fce5c 100644 --- a/generate-keys.js +++ b/generate-keys.js @@ -1,88 +1,88 @@ -const NodeRSA = require('node-rsa'); -const crypto = require('crypto'); -const fs = require('fs-extra'); -require('colors'); - -const args = process.argv.slice(2); - -if (args.length < 1) { - usage(); - return; -} - -const [type, name] = args; - -if (!['nex', 'service', 'access'].includes(type)) { - usage(); - return; -} - -if (type !== 'access' && (!name || name.trim() === '')) { - usage(); - return; -} - -let path; - -if (type === 'access') { - path = `${__dirname}/certs/${type}`; -} else { - path = `${__dirname}/certs/${type}/${name}`; -} - -// Ensure the output directories exist -console.log('Creating output directories'.brightGreen); -fs.ensureDirSync(path); - -// Generate new AES key -console.log('Generating AES key'.brightGreen); -const aesKey = crypto.randomBytes(16); - -// Saving AES key -fs.writeFileSync(`${path}/aes.key`, aesKey.toString('hex')); -console.log(`Saved AES key to file ${path}/aes.key`.brightBlue); - -const key = new NodeRSA({ b: 1024}, null, { - environment: 'browser', - encryptionScheme: { - 'hash': 'sha256', - } -}); - -// Generate new key pair -console.log('Generating RSA key pair'.brightGreen, '(this may take a while)'.yellow.bold); -key.generateKeyPair(1024); - -// Export the keys -console.log('Exporting public key'.brightGreen); -const publickKey = key.exportKey('public'); - -// Saving public key -fs.writeFileSync(`${path}/public.pem`, publickKey); -console.log(`Saved public key to file ${path}/public.pem`.brightBlue); - -console.log('Exporting private key'.brightGreen); -const privatekKey = key.exportKey('private'); - -// Saving private key -fs.writeFileSync(`${path}/private.pem`, privatekKey); -console.log(`Saved public key to file ${path}/private.pem`.brightBlue); - -// Create HMAC secret key -console.log('Generating HMAC secret'.brightGreen); -const secret = crypto.randomBytes(16); -fs.writeFileSync(`${path}/secret.key`, secret.toString('hex')); - -console.log(`Saved HMAC secret to file ${path}/secret.key`.brightBlue); - -// Display usage information -function usage() { - console.log('Usage: node generate-keys.js type [name]'); - - console.log('Types:'); - console.log(' - nex'); - console.log(' - service'); - console.log(' - access'); - - console.log('Name: service or nex server name. Not used in access type'); +const NodeRSA = require('node-rsa'); +const crypto = require('crypto'); +const fs = require('fs-extra'); +require('colors'); + +const args = process.argv.slice(2); + +if (args.length < 1) { + usage(); + return; +} + +const [type, name] = args; + +if (!['nex', 'service', 'access'].includes(type)) { + usage(); + return; +} + +if (type !== 'access' && (!name || name.trim() === '')) { + usage(); + return; +} + +let path; + +if (type === 'access') { + path = `${__dirname}/certs/${type}`; +} else { + path = `${__dirname}/certs/${type}/${name}`; +} + +// Ensure the output directories exist +console.log('Creating output directories'.brightGreen); +fs.ensureDirSync(path); + +// Generate new AES key +console.log('Generating AES key'.brightGreen); +const aesKey = crypto.randomBytes(16); + +// Saving AES key +fs.writeFileSync(`${path}/aes.key`, aesKey.toString('hex')); +console.log(`Saved AES key to file ${path}/aes.key`.brightBlue); + +const key = new NodeRSA({ b: 1024}, null, { + environment: 'browser', + encryptionScheme: { + 'hash': 'sha256', + } +}); + +// Generate new key pair +console.log('Generating RSA key pair'.brightGreen, '(this may take a while)'.yellow.bold); +key.generateKeyPair(1024); + +// Export the keys +console.log('Exporting public key'.brightGreen); +const publickKey = key.exportKey('public'); + +// Saving public key +fs.writeFileSync(`${path}/public.pem`, publickKey); +console.log(`Saved public key to file ${path}/public.pem`.brightBlue); + +console.log('Exporting private key'.brightGreen); +const privatekKey = key.exportKey('private'); + +// Saving private key +fs.writeFileSync(`${path}/private.pem`, privatekKey); +console.log(`Saved public key to file ${path}/private.pem`.brightBlue); + +// Create HMAC secret key +console.log('Generating HMAC secret'.brightGreen); +const secret = crypto.randomBytes(16); +fs.writeFileSync(`${path}/secret.key`, secret.toString('hex')); + +console.log(`Saved HMAC secret to file ${path}/secret.key`.brightBlue); + +// Display usage information +function usage() { + console.log('Usage: node generate-keys.js type [name]'); + + console.log('Types:'); + console.log(' - nex'); + console.log(' - service'); + console.log(' - access'); + + console.log('Name: service or nex server name. Not used in access type'); } \ No newline at end of file diff --git a/logger.js b/logger.js index eda058e..07215de 100644 --- a/logger.js +++ b/logger.js @@ -1,52 +1,52 @@ -const fs = require('fs-extra'); -require('colors'); - -const root = __dirname; -fs.ensureDirSync(`${root}/logs`); - -const streams = { - latest: fs.createWriteStream(`${root}/logs/latest.log`), - success: fs.createWriteStream(`${root}/logs/success.log`), - error: fs.createWriteStream(`${root}/logs/error.log`), - warn: fs.createWriteStream(`${root}/logs/warn.log`), - info: fs.createWriteStream(`${root}/logs/info.log`) -}; - -function success(input) { - const time = new Date(); - input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [SUCCESS]: ${input}`; - streams.success.write(`${input}\n`); - - console.log(`${input}`.green.bold); -} - -function error(input) { - const time = new Date(); - input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [ERROR]: ${input}`; - streams.error.write(`${input}\n`); - - console.log(`${input}`.red.bold); -} - -function warn(input) { - const time = new Date(); - input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [WARN]: ${input}`; - streams.warn.write(`${input}\n`); - - console.log(`${input}`.yellow.bold); -} - -function info(input) { - const time = new Date(); - input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [INFO]: ${input}`; - streams.info.write(`${input}\n`); - - console.log(`${input}`.cyan.bold); -} - -module.exports = { - success, - error, - warn, - info +const fs = require('fs-extra'); +require('colors'); + +const root = __dirname; +fs.ensureDirSync(`${root}/logs`); + +const streams = { + latest: fs.createWriteStream(`${root}/logs/latest.log`), + success: fs.createWriteStream(`${root}/logs/success.log`), + error: fs.createWriteStream(`${root}/logs/error.log`), + warn: fs.createWriteStream(`${root}/logs/warn.log`), + info: fs.createWriteStream(`${root}/logs/info.log`) +}; + +function success(input) { + const time = new Date(); + input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [SUCCESS]: ${input}`; + streams.success.write(`${input}\n`); + + console.log(`${input}`.green.bold); +} + +function error(input) { + const time = new Date(); + input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [ERROR]: ${input}`; + streams.error.write(`${input}\n`); + + console.log(`${input}`.red.bold); +} + +function warn(input) { + const time = new Date(); + input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [WARN]: ${input}`; + streams.warn.write(`${input}\n`); + + console.log(`${input}`.yellow.bold); +} + +function info(input) { + const time = new Date(); + input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [INFO]: ${input}`; + streams.info.write(`${input}\n`); + + console.log(`${input}`.cyan.bold); +} + +module.exports = { + success, + error, + warn, + info }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index cdc154f..c6bdfc4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -154,12 +154,12 @@ } }, "bcrypt": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-3.0.7.tgz", - "integrity": "sha512-K5UglF9VQvBMHl/1elNyyFvAfOY9Bj+rpKrCSR9sFwcW8FywAYJSRwTURNej5TaAK2TEJkcJ6r6lh1YPmspx5Q==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.0.tgz", + "integrity": "sha512-jB0yCBl4W/kVHM2whjfyqnxTmOHkCX4kHEa5nYKSoGeYe8YrjTYTc87/6bwt1g8cmV0QrbhKriETg9jWtcREhg==", "requires": { - "nan": "2.14.0", - "node-pre-gyp": "0.13.0" + "node-addon-api": "^3.0.0", + "node-pre-gyp": "0.15.0" } }, "bluebird": { @@ -204,9 +204,9 @@ "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" }, "chownr": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz", - "integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "code-point-at": { "version": "1.1.0", @@ -356,33 +356,6 @@ "vary": "~1.1.2" } }, - "express-session": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.0.tgz", - "integrity": "sha512-t4oX2z7uoSqATbMfsxWMbNjAL0T5zpvcJCk3Z9wnPPN7ibddhnmDZXHfEcoBMG2ojKXZoCyPMc5FbtK+G7SoDg==", - "requires": { - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~2.0.0", - "on-headers": "~1.0.2", - "parseurl": "~1.3.3", - "safe-buffer": "5.2.0", - "uid-safe": "~2.1.5" - }, - "dependencies": { - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, - "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" - } - } - }, "express-subdomain": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/express-subdomain/-/express-subdomain-1.0.5.tgz", @@ -526,9 +499,9 @@ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, "ipaddr.js": { "version": "1.9.0", @@ -624,9 +597,9 @@ } }, "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "minipass": { "version": "2.9.0", @@ -646,11 +619,11 @@ } }, "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "requires": { - "minimist": "0.0.8" + "minimist": "^1.2.5" } }, "moment": { @@ -765,20 +738,15 @@ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" }, - "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" - }, "ncp": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ncp/-/ncp-1.0.1.tgz", "integrity": "sha1-0VNn5cuHQyuhF9K/gP30Wuz7QkY=" }, "needle": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", - "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.6.0.tgz", + "integrity": "sha512-KKYdza4heMsEfSWD7VPUIz3zX2XDwOyX2d+geb4vrERZMT5RMU6ujjaD+I5Yr54uZxQ2w6XRTAhHBbSCyovZBg==", "requires": { "debug": "^3.2.6", "iconv-lite": "^0.4.4", @@ -786,17 +754,17 @@ }, "dependencies": { "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "requires": { "ms": "^2.1.1" } }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" } } }, @@ -805,21 +773,26 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, + "node-addon-api": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz", + "integrity": "sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==" + }, "node-pre-gyp": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.13.0.tgz", - "integrity": "sha512-Md1D3xnEne8b/HGVQkZZwV27WUi1ZRuZBij24TNaZwUPU3ZAFtvT6xxJGaUVillfmMKnn5oD1HoGsp2Ftik7SQ==", + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.15.0.tgz", + "integrity": "sha512-7QcZa8/fpaU/BKenjcaeFF9hLz2+7S9AqyXFhlH/rilsQ/hPZKK32RtR5EQHJElgu+q5RfbJ34KriI79UWaorA==", "requires": { "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", + "mkdirp": "^0.5.3", + "needle": "^2.5.0", "nopt": "^4.0.1", "npm-packlist": "^1.1.6", "npmlog": "^4.0.2", "rc": "^1.2.7", "rimraf": "^2.6.1", "semver": "^5.3.0", - "tar": "^4" + "tar": "^4.4.2" } }, "node-rsa": { @@ -836,9 +809,9 @@ "integrity": "sha512-g0n4nH1ONGvqYo1v72uSWvF/MRNnnq1LzmSzXb/6EPF3LFb51akOhgG3K2+aETAsJx90/Q5eFNTntu4vBCwyQQ==" }, "nopt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", "requires": { "abbrev": "1", "osenv": "^0.1.4" @@ -858,12 +831,13 @@ "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" }, "npm-packlist": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.7.tgz", - "integrity": "sha512-vAj7dIkp5NhieaGZxBJB8fF4R0078rqsmhJcAfXZ6O7JJhjhPK96n5Ry1oZcfLXgfun0GWTZPOxaEyqv8GBykQ==", + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", + "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", "requires": { "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" + "npm-bundled": "^1.0.1", + "npm-normalize-package-bin": "^1.0.1" } }, "npmlog": { @@ -984,11 +958,6 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, - "random-bytes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", - "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" - }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -1014,13 +983,6 @@ "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } } }, "read": { @@ -1032,9 +994,9 @@ } }, "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -1160,9 +1122,9 @@ "integrity": "sha512-oqD7PMJ+uO6jV9EQCl0LrRw1OwsiPsiFQR5AR30heR+4Dl7jBBbDLnNvWiak20tzZlSE1H7RB30SX/1j/YYT7g==" }, "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" }, "sliced": { "version": "1.0.1", @@ -1247,14 +1209,6 @@ "mime-types": "~2.1.24" } }, - "uid-safe": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", - "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", - "requires": { - "random-bytes": "~1.0.0" - } - }, "universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", diff --git a/package.json b/package.json index a76e42d..c44b7fa 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "account", "version": "1.0.0", "description": "", - "main": "./src/server", + "main": "./src/server.js", "scripts": { "lint": "./node_modules/.bin/eslint .", "start": "node .", @@ -20,10 +20,9 @@ }, "homepage": "https://github.com/PretendoNetwork/account#readme", "dependencies": { - "bcrypt": "^3.0.7", + "bcrypt": "^5.0.0", "colors": "^1.4.0", "express": "^4.17.1", - "express-session": "^1.17.0", "express-subdomain": "^1.0.5", "fs-extra": "^8.1.0", "moment": "^2.24.0", diff --git a/src/database.js b/src/database.js index fa3d9ab..a483710 100644 --- a/src/database.js +++ b/src/database.js @@ -1,170 +1,170 @@ -const mongoose = require('mongoose'); -const bcrypt = require('bcrypt'); -const util = require('./util'); -const { PNID } = require('./models/pnid'); -const { mongoose: mongooseConfig } = require('./config.json'); -const { uri, database, options } = mongooseConfig; - -let connection; - -async function connect() { - await mongoose.connect(`${uri}/${database}`, options); - - connection = mongoose.connection; - connection.on('error', console.error.bind(console, 'connection error:')); -} - -function verifyConnected() { - if (!connection) { - throw new Error('Cannot make database requets without being connected'); - } -} - -async function getUserByUsername(username) { - verifyConnected(); - - if (typeof username !== 'string') { - return null; - } - - const user = await PNID.findOne({ - usernameLower: username.toLowerCase() - }); - - return user; -} - -async function getUserByPID(pid) { - verifyConnected(); - - const user = await PNID.findOne({ - pid - }); - - return user; -} - -async function doesUserExist(username) { - verifyConnected(); - - return !!await this.getUserByUsername(username); -} - -async function getUserBasic(token) { - verifyConnected(); - - const [username, password] = Buffer.from(token, 'base64').toString().split(' '); - const user = await this.getUserByUsername(username); - - if (!user) { - return null; - } - - const hashedPassword = util.nintendoPasswordHash(password, user.pid); - - if (!bcrypt.compareSync(hashedPassword, user.password)) { - return null; - } - - return user; -} - -async function getUserBearer(token) { - verifyConnected(); - - const decryptedToken = util.decryptToken(Buffer.from(token, 'base64')); - const unpackedToken = util.unpackToken(decryptedToken); - - const user = await getUserByPID(unpackedToken.pid); - - if (user) { - const expireTime = Math.floor((Number(unpackedToken.date) / 1000) + 3600); - - if (Math.floor(Date.now()/1000) > expireTime) { - return null; - } - } - - return user; -} - -async function getUserProfileJSONByPID(pid) { - verifyConnected(); - - const user = await this.getUserByPID(pid); - const device = user.get('devices')[0]; // Just grab the first device - let device_attributes; - - if (device) { - device_attributes = device.get('device_attributes').map(({name, value, created_date}) => { - const deviceAttributeDocument = { - name, - value - }; - - if (created_date) { - deviceAttributeDocument.created_date = created_date; - } - - return { - device_attribute: deviceAttributeDocument - }; - }); - } - - return { - //accounts: {}, We need to figure this out, no idea what these values mean or what they do - active_flag: user.get('flags.active') ? 'Y' : 'N', - birth_date: user.get('birthdate'), - country: user.get('country'), - create_date: user.get('creation_date'), - device_attributes: device_attributes, - gender: user.get('gender'), - language: user.get('language'), - updated: user.get('updated'), - marketing_flag: user.get('flags.marketing') ? 'Y' : 'N', - off_device_flag: user.get('flags.off_device') ? 'Y' : 'N', - pid: user.get('pid'), - email: { - address: user.get('email.address'), - id: user.get('email.id'), - parent: user.get('email.parent') ? 'Y' : 'N', - primary: user.get('email.primary') ? 'Y' : 'N', - reachable: user.get('email.reachable') ? 'Y' : 'N', - type: 'DEFAULT', - updated_by: 'USER', // Can also be INTERNAL WS, don't know the difference - validated: user.get('email.validated') ? 'Y' : 'N', - //validated_date: user.get('email.validated_date') // not used atm - }, - mii: { - status: 'COMPLETED', - data: user.get('mii.data'), - id: user.get('mii.id'), - mii_hash: user.get('mii.hash'), - mii_images: { - mii_image: { - cached_url: user.get('mii.image_url'), - id: user.get('mii.image_id'), - url: user.get('mii.image_url'), - type: 'standard' - } - }, - name: user.get('mii.name'), - primary: user.get('mii.primary') ? 'Y' : 'N', - }, - region: user.get('region'), - tz_name: user.get('timezone.name'), - user_id: user.get('username'), - utc_offset: user.get('timezone.offset') - }; -} - -module.exports = { - connect, - getUserByUsername, - getUserByPID, - doesUserExist, - getUserBasic, - getUserBearer, - getUserProfileJSONByPID, +const mongoose = require('mongoose'); +const bcrypt = require('bcrypt'); +const util = require('./util'); +const { PNID } = require('./models/pnid'); +const { mongoose: mongooseConfig } = require('./config.json'); +const { uri, database, options } = mongooseConfig; + +let connection; + +async function connect() { + await mongoose.connect(`${uri}/${database}`, options); + + connection = mongoose.connection; + connection.on('error', console.error.bind(console, 'connection error:')); +} + +function verifyConnected() { + if (!connection) { + throw new Error('Cannot make database requets without being connected'); + } +} + +async function getUserByUsername(username) { + verifyConnected(); + + if (typeof username !== 'string') { + return null; + } + + const user = await PNID.findOne({ + usernameLower: username.toLowerCase() + }); + + return user; +} + +async function getUserByPID(pid) { + verifyConnected(); + + const user = await PNID.findOne({ + pid + }); + + return user; +} + +async function doesUserExist(username) { + verifyConnected(); + + return !!await this.getUserByUsername(username); +} + +async function getUserBasic(token) { + verifyConnected(); + + const [username, password] = Buffer.from(token, 'base64').toString().split(' '); + const user = await this.getUserByUsername(username); + + if (!user) { + return null; + } + + const hashedPassword = util.nintendoPasswordHash(password, user.pid); + + if (!bcrypt.compareSync(hashedPassword, user.password)) { + return null; + } + + return user; +} + +async function getUserBearer(token) { + verifyConnected(); + + const decryptedToken = util.decryptToken(Buffer.from(token, 'base64')); + const unpackedToken = util.unpackToken(decryptedToken); + + const user = await getUserByPID(unpackedToken.pid); + + if (user) { + const expireTime = Math.floor((Number(unpackedToken.date) / 1000) + 3600); + + if (Math.floor(Date.now()/1000) > expireTime) { + return null; + } + } + + return user; +} + +async function getUserProfileJSONByPID(pid) { + verifyConnected(); + + const user = await this.getUserByPID(pid); + const device = user.get('devices')[0]; // Just grab the first device + let device_attributes; + + if (device) { + device_attributes = device.get('device_attributes').map(({name, value, created_date}) => { + const deviceAttributeDocument = { + name, + value + }; + + if (created_date) { + deviceAttributeDocument.created_date = created_date; + } + + return { + device_attribute: deviceAttributeDocument + }; + }); + } + + return { + //accounts: {}, We need to figure this out, no idea what these values mean or what they do + active_flag: user.get('flags.active') ? 'Y' : 'N', + birth_date: user.get('birthdate'), + country: user.get('country'), + create_date: user.get('creation_date'), + device_attributes: device_attributes, + gender: user.get('gender'), + language: user.get('language'), + updated: user.get('updated'), + marketing_flag: user.get('flags.marketing') ? 'Y' : 'N', + off_device_flag: user.get('flags.off_device') ? 'Y' : 'N', + pid: user.get('pid'), + email: { + address: user.get('email.address'), + id: user.get('email.id'), + parent: user.get('email.parent') ? 'Y' : 'N', + primary: user.get('email.primary') ? 'Y' : 'N', + reachable: user.get('email.reachable') ? 'Y' : 'N', + type: 'DEFAULT', + updated_by: 'USER', // Can also be INTERNAL WS, don't know the difference + validated: user.get('email.validated') ? 'Y' : 'N', + //validated_date: user.get('email.validated_date') // not used atm + }, + mii: { + status: 'COMPLETED', + data: user.get('mii.data'), + id: user.get('mii.id'), + mii_hash: user.get('mii.hash'), + mii_images: { + mii_image: { + cached_url: user.get('mii.image_url'), + id: user.get('mii.image_id'), + url: user.get('mii.image_url'), + type: 'standard' + } + }, + name: user.get('mii.name'), + primary: user.get('mii.primary') ? 'Y' : 'N', + }, + region: user.get('region'), + tz_name: user.get('timezone.name'), + user_id: user.get('username'), + utc_offset: user.get('timezone.offset') + }; +} + +module.exports = { + connect, + getUserByUsername, + getUserByPID, + doesUserExist, + getUserBasic, + getUserBearer, + getUserProfileJSONByPID, }; \ No newline at end of file diff --git a/src/example.config.json b/src/example.config.json index cb63e1d..b3a7e36 100644 --- a/src/example.config.json +++ b/src/example.config.json @@ -1,19 +1,19 @@ -{ - "http": { - "port": 7070 - }, - "mongoose": { - "uri": "mongodb://localhost:27017", - "database": "database_name", - "options": { - "useNewUrlParser": true - } - }, - "email": { - "service": "gmail", - "auth": { - "user": "email@address.com", - "pass": "password" - } - } +{ + "http": { + "port": 7070 + }, + "mongoose": { + "uri": "mongodb://localhost:27017", + "database": "database_name", + "options": { + "useNewUrlParser": true + } + }, + "email": { + "service": "gmail", + "auth": { + "user": "email@address.com", + "pass": "password" + } + } } \ No newline at end of file diff --git a/src/example.servers.json b/src/example.servers.json index 88ae5e2..be3e344 100644 --- a/src/example.servers.json +++ b/src/example.servers.json @@ -1,24 +1,24 @@ -[ - { - "title_ids": [ - "000500301001500A", - "000500301001510A", - "000500301001520A", - "0005001010001C00" - ], - "server_id": "00003200", - "ip": "192.168.0.27", - "port": "60000", - "system": 1, - "crypto": { - "service": { - "cek": "", - "hmac_secret": "" - }, - "nex": { - "cek": "", - "hmac_secret": "" - } - } - } +[ + { + "title_ids": [ + "000500301001500A", + "000500301001510A", + "000500301001520A", + "0005001010001C00" + ], + "server_id": "00003200", + "ip": "192.168.0.27", + "port": "60000", + "system": 1, + "crypto": { + "service": { + "cek": "", + "hmac_secret": "" + }, + "nex": { + "cek": "", + "hmac_secret": "" + } + } + } ] \ No newline at end of file diff --git a/src/mailer.js b/src/mailer.js index 68accbf..e0d93ca 100644 --- a/src/mailer.js +++ b/src/mailer.js @@ -1,33 +1,33 @@ -const nodemailer = require('nodemailer'); -const config = require('./config'); - -const transporter = nodemailer.createTransport(config.email); - -/** - * Sends an email with the specified subject and message to an email address - * @param {String} email the destination email address - * @param {String} subject The Subject of the email - * @param {String} message The body of the email - */ -async function send(email, subject = 'No email subject provided', message = 'No email body provided') { - const options = { - from: config.email.auth.user, - to: email, - subject: subject, - html: message - }; - - return new Promise(resolve => { - transporter.sendMail(options, (error) => { - if (error) { - console.warn(error); - } - - return resolve(); - }); - }); -} - -module.exports = { - send: send +const nodemailer = require('nodemailer'); +const config = require('./config'); + +const transporter = nodemailer.createTransport(config.email); + +/** + * Sends an email with the specified subject and message to an email address + * @param {String} email the destination email address + * @param {String} subject The Subject of the email + * @param {String} message The body of the email + */ +async function send(email, subject = 'No email subject provided', message = 'No email body provided') { + const options = { + from: config.email.auth.user, + to: email, + subject: subject, + html: message + }; + + return new Promise(resolve => { + transporter.sendMail(options, (error) => { + if (error) { + console.warn(error); + } + + return resolve(); + }); + }); +} + +module.exports = { + send: send }; \ No newline at end of file diff --git a/src/middleware/client-header.js b/src/middleware/client-header.js index e722968..1999223 100644 --- a/src/middleware/client-header.js +++ b/src/middleware/client-header.js @@ -1,38 +1,38 @@ -const xmlbuilder = require('xmlbuilder'); - -const VALID_CLIENT_ID_SECRET_PAIRS = { - // 'Key' is the client ID, 'Value' is the client secret - 'a2efa818a34fa16b8afbc8a74eba3eda': 'c91cdb5658bd4954ade78533a339cf9a', // Possibly WiiU exclusive? - 'daf6227853bcbdce3d75baee8332b': '3eff548eac636e2bf45bb7b375e7b6b0', // Possibly 3DS exclusive? - 'ea25c66c26b403376b4c5ed94ab9cdea': 'd137be62cb6a2b831cad8c013b92fb55', // Possibly 3DS exclusive? -}; - - -function nintendoClientHeaderCheck(request, response, next) { - response.set('Content-Type', 'text/xml'); - response.set('Server', 'Nintendo 3DS (http)'); - response.set('X-Nintendo-Date', new Date().getTime()); - - const {headers} = request; - - if ( - !headers['x-nintendo-client-id'] || - !headers['x-nintendo-client-secret'] || - !VALID_CLIENT_ID_SECRET_PAIRS[headers['x-nintendo-client-id']] || - headers['x-nintendo-client-secret'] !== VALID_CLIENT_ID_SECRET_PAIRS[headers['x-nintendo-client-id']] - ) { - return response.send(xmlbuilder.create({ - errors: { - error: { - cause: 'client_id', - code: '0004', - message: 'API application invalid or incorrect application credentials' - } - } - }).end()); - } - - return next(); -} - +const xmlbuilder = require('xmlbuilder'); + +const VALID_CLIENT_ID_SECRET_PAIRS = { + // 'Key' is the client ID, 'Value' is the client secret + 'a2efa818a34fa16b8afbc8a74eba3eda': 'c91cdb5658bd4954ade78533a339cf9a', // Possibly WiiU exclusive? + 'daf6227853bcbdce3d75baee8332b': '3eff548eac636e2bf45bb7b375e7b6b0', // Possibly 3DS exclusive? + 'ea25c66c26b403376b4c5ed94ab9cdea': 'd137be62cb6a2b831cad8c013b92fb55', // Possibly 3DS exclusive? +}; + + +function nintendoClientHeaderCheck(request, response, next) { + response.set('Content-Type', 'text/xml'); + response.set('Server', 'Nintendo 3DS (http)'); + response.set('X-Nintendo-Date', new Date().getTime()); + + const {headers} = request; + + if ( + !headers['x-nintendo-client-id'] || + !headers['x-nintendo-client-secret'] || + !VALID_CLIENT_ID_SECRET_PAIRS[headers['x-nintendo-client-id']] || + headers['x-nintendo-client-secret'] !== VALID_CLIENT_ID_SECRET_PAIRS[headers['x-nintendo-client-id']] + ) { + return response.send(xmlbuilder.create({ + errors: { + error: { + cause: 'client_id', + code: '0004', + message: 'API application invalid or incorrect application credentials' + } + } + }).end()); + } + + return next(); +} + module.exports = nintendoClientHeaderCheck; \ No newline at end of file diff --git a/src/middleware/pnid.js b/src/middleware/pnid.js index 5d47cde..4a9946f 100644 --- a/src/middleware/pnid.js +++ b/src/middleware/pnid.js @@ -1,50 +1,50 @@ -const xmlbuilder = require('xmlbuilder'); -const database = require('../database'); - -async function PNIDMiddleware(request, response, next) { - const { headers } = request; - - if (!headers.authorization || !(headers.authorization.startsWith('Bearer') || headers.authorization.startsWith('Basic'))) { - return next(); - } - - const [type, token] = headers.authorization.split(' '); - let user; - - if (type === 'Basic') { - user = await database.getUserBasic(token); - } else { - user = await database.getUserBearer(token); - } - - if (!user) { - response.status(401); - - if (type === 'Bearer') { - return response.send(xmlbuilder.create({ - errors: { - error: { - cause: 'access_token', - code: '0005', - message: 'Invalid access token' - } - } - }).end()); - } - - return response.send(xmlbuilder.create({ - errors: { - error: { - code: '1105', - message: 'Email address, username, or password, is not valid' - } - } - }).end()); - } - - request.pnid = user; - - return next(); -} - +const xmlbuilder = require('xmlbuilder'); +const database = require('../database'); + +async function PNIDMiddleware(request, response, next) { + const { headers } = request; + + if (!headers.authorization || !(headers.authorization.startsWith('Bearer') || headers.authorization.startsWith('Basic'))) { + return next(); + } + + const [type, token] = headers.authorization.split(' '); + let user; + + if (type === 'Basic') { + user = await database.getUserBasic(token); + } else { + user = await database.getUserBearer(token); + } + + if (!user) { + response.status(401); + + if (type === 'Bearer') { + return response.send(xmlbuilder.create({ + errors: { + error: { + cause: 'access_token', + code: '0005', + message: 'Invalid access token' + } + } + }).end()); + } + + return response.send(xmlbuilder.create({ + errors: { + error: { + code: '1105', + message: 'Email address, username, or password, is not valid' + } + } + }).end()); + } + + request.pnid = user; + + return next(); +} + module.exports = PNIDMiddleware; \ No newline at end of file diff --git a/src/middleware/session.js b/src/middleware/session.js index e8b663f..93e383d 100644 --- a/src/middleware/session.js +++ b/src/middleware/session.js @@ -1,23 +1,23 @@ -// super basic and there's probably a much better way to do this - -// this will only be used during the registration process, to track the progress of the user -// express-session uses cookies which the WiiU does not support during the registration process - -// temp, in-memory session storage -const sessionStore = {}; - -function sessionMiddlware(request, response, next) { - const ip = request.headers['x-forwarded-for'] || request.connection.remoteAddress; - - if (!sessionStore[ip]) { - sessionStore[ip] = {}; - } - - const session = sessionStore[ip]; - - request.session = session; - - return next(); -} - +// super basic and there's probably a much better way to do this + +// this will only be used during the registration process, to track the progress of the user +// express-session uses cookies which the WiiU does not support during the registration process + +// temp, in-memory session storage +const sessionStore = {}; + +function sessionMiddlware(request, response, next) { + const ip = request.headers['x-forwarded-for'] || request.connection.remoteAddress; + + if (!sessionStore[ip]) { + sessionStore[ip] = {}; + } + + const session = sessionStore[ip]; + + request.session = session; + + return next(); +} + module.exports = sessionMiddlware; \ No newline at end of file diff --git a/src/middleware/xml-parser.js b/src/middleware/xml-parser.js index 12fb614..5ed1578 100644 --- a/src/middleware/xml-parser.js +++ b/src/middleware/xml-parser.js @@ -1,35 +1,35 @@ -const { document: xmlParser } = require('xmlbuilder2'); - -function XMLMiddleware(request, response, next) { - if (request.method == 'POST' || request.method == 'PUT') { - const headers = request.headers; - let body = ''; - - if ( - !headers['content-type'] || - !headers['content-type'].toLowerCase().includes('xml') - ) { - return next(); - } - - request.setEncoding('utf-8'); - request.on('data', (chunk) => { - body += chunk; - }); - - request.on('end', () => { - try { - request.body = xmlParser(body); - request.body = request.body.toObject(); - } catch (error) { - return next(); - } - - next(); - }); - } else { - next(); - } -} - +const { document: xmlParser } = require('xmlbuilder2'); + +function XMLMiddleware(request, response, next) { + if (request.method == 'POST' || request.method == 'PUT') { + const headers = request.headers; + let body = ''; + + if ( + !headers['content-type'] || + !headers['content-type'].toLowerCase().includes('xml') + ) { + return next(); + } + + request.setEncoding('utf-8'); + request.on('data', (chunk) => { + body += chunk; + }); + + request.on('end', () => { + try { + request.body = xmlParser(body); + request.body = request.body.toObject(); + } catch (error) { + return next(); + } + + next(); + }); + } else { + next(); + } +} + module.exports = XMLMiddleware; \ No newline at end of file diff --git a/src/models/device.js b/src/models/device.js index 4a1c4bd..c04816d 100644 --- a/src/models/device.js +++ b/src/models/device.js @@ -1,37 +1,37 @@ -const { Schema, model } = require('mongoose'); - -const DeviceAttributeSchema = new Schema({ - created_date: String, - name: String, - value: String, -}); - -const DeviceAttribute = model('DeviceAttribute', DeviceAttributeSchema); - -const DeviceSchema = new Schema({ - is_emulator: { - type: Boolean, - default: false - }, - console_type: { - type: String, - enum: ['wup', 'ctr', 'spr', 'ftr', 'ktr', 'red', 'jan'] // wup is WiiU, the rest are the 3DS family. Only wup is used atm - }, - device_id: Number, - device_type: Number, - serial: String, - device_attributes: [DeviceAttributeSchema], - soap: { - token: String, - account_id: Number, - } -}); - -const Device = model('Device', DeviceSchema); - -module.exports = { - DeviceSchema, - Device, - DeviceAttributeSchema, - DeviceAttribute +const { Schema, model } = require('mongoose'); + +const DeviceAttributeSchema = new Schema({ + created_date: String, + name: String, + value: String, +}); + +const DeviceAttribute = model('DeviceAttribute', DeviceAttributeSchema); + +const DeviceSchema = new Schema({ + is_emulator: { + type: Boolean, + default: false + }, + console_type: { + type: String, + enum: ['wup', 'ctr', 'spr', 'ftr', 'ktr', 'red', 'jan'] // wup is WiiU, the rest are the 3DS family. Only wup is used atm + }, + device_id: Number, + device_type: Number, + serial: String, + device_attributes: [DeviceAttributeSchema], + soap: { + token: String, + account_id: Number, + } +}); + +const Device = model('Device', DeviceSchema); + +module.exports = { + DeviceSchema, + Device, + DeviceAttributeSchema, + DeviceAttribute }; \ No newline at end of file diff --git a/src/models/pnid.js b/src/models/pnid.js index b642553..5834318 100644 --- a/src/models/pnid.js +++ b/src/models/pnid.js @@ -1,220 +1,220 @@ -const { Schema, model } = require('mongoose'); -const uniqueValidator = require('mongoose-unique-validator'); -const bcrypt = require('bcrypt'); -const crypto = require('crypto'); -const util = require('../util'); -const { DeviceSchema } = require('./device'); - -const PNIDSchema = new Schema({ - access_level: { - type: Number, - default: 0 // Standard user - }, - pid: { - type: Number, - unique: true - }, - creation_date: String, - updated: String, - username: { - type: String, - unique: true, - minlength: 6, - maxlength: 16 - }, - usernameLower: { - type: String, - unique: true - }, - password: String, - birthdate: String, - gender: String, - country: String, - language: String, - email: { - address: String, - primary: Boolean, - parent: Boolean, - reachable: Boolean, - validated: Boolean, - validated_date: String, - id: { - type: Number, - unique: true - } - }, - region: Number, - timezone: { - name: String, - offset: Number - }, - mii: { - name: String, - primary: Boolean, - data: String, - id: { - type: Number, - unique: true - }, - hash: { - type: String, - unique: true - }, - image_url: String, - image_id: { - type: Number, - unique: true - }, - }, - flags: { // not entirely sure what these are used for - active: Boolean, // Is the account active? Like, not deleted maybe? - marketing: Boolean, - off_device: Boolean - }, - devices: [DeviceSchema], - nex: { - password: String, - token: String - }, - identification: { // user identification tokens - email_code: { - type: String, - unique: true - }, - email_token: { - type: String, - unique: true - }, - access_token: { - value: String, - ttl: Number - }, - refresh_token: { - value: String, - ttl: Number - } - } -}); - -PNIDSchema.plugin(uniqueValidator, {message: '{PATH} already in use.'}); - -/* - According to http://pf2m.com/tools/rank.php Nintendo PID's start at 1,800,000,000 and count down with each account - This means the max PID is 1799999999 and hard-limits the number of potential accounts to 1,800,000,000 - The author of that site does not give any information on how they found this out, but it does seem to hold true - https://account.nintendo.net/v1/api/admin/mapped_ids?input_type=pid&output_type=user_id&input=1800000000 returns nothing - https://account.nintendo.net/v1/api/admin/mapped_ids?input_type=pid&output_type=user_id&input=1799999999 returns `prodtest1` - and the next few accounts counting down seem to be admin, service and internal test accounts -*/ -PNIDSchema.methods.generatePID = async function() { - const min = 1000000000; // The console (WiiU) seems to not accept PIDs smaller than this - const max = 1799999999; - - let pid = Math.floor(Math.random() * (max - min + 1) + min); - - const inuse = await PNID.findOne({ - pid - }); - - pid = (inuse ? await PNID.generatePID() : pid); - - this.set('pid', pid); -}; - -PNIDSchema.methods.generateNEXPassword = function() { - function character() { - const offset = Math.floor(Math.random() * 62); - if (offset < 10) return offset; - if (offset < 36) return String.fromCharCode(offset + 55); - return String.fromCharCode(offset + 61); - } - - const output = []; - - while (output.length < 16) { - output.push(character()); - } - - this.set('nex.password', output.join('')); -}; - -PNIDSchema.methods.generateEmailValidationCode = async function() { - let code = Math.random().toFixed(6).split('.')[1]; // Dirty one-liner to generate numbers of 6 length and padded 0 - - const inuse = await PNID.findOne({ - 'identification.email_code': code - }); - - code = (inuse ? await PNID.generateEmailValidationCode() : code); - - this.set('identification.email_code', code); -}; - -PNIDSchema.methods.generateEmailValidationToken = async function() { - let token = crypto.randomBytes(32).toString('hex'); - - const inuse = await PNID.findOne({ - 'identification.email_token': token - }); - - token = (inuse ? await PNID.generateEmailValidationToken() : token); - - this.set('identification.email_token', token); -}; - -PNIDSchema.methods.getDevice = async function(document) { - const devices = this.get('devices'); - - return devices.find(device => { - return ( - (device.device_id === document.device_id) && - (device.device_type === document.device_type) && - (device.serial === document.serial) - ); - }); -}; - -PNIDSchema.methods.addDevice = async function(device) { - this.devices.push(device); - - await this.save(); -}; - -PNIDSchema.methods.removeDevice = async function(device) { - this.devices = this.devices.filter(({ _id }) => _id !== device._id); - - await this.save(); -}; - -PNIDSchema.methods.updateMii = async function({name, primary, data}) { - this.set('mii.name', name); - this.set('mii.primary', primary === 'Y'); - this.set('mii.data', data); - - await this.save(); -}; - -PNIDSchema.pre('save', async function(next) { - if (!this.isModified('password')) { - return next(); - } - - this.set('usernameLower', this.get('username').toLowerCase()); - await this.generatePID(); - await this.generateNEXPassword(); - await this.generateEmailValidationCode(); - await this.generateEmailValidationToken(); - - const primaryHash = util.nintendoPasswordHash(this.get('password'), this.get('pid')); - const hash = bcrypt.hashSync(primaryHash, 10); - - this.set('password', hash); - next(); -}); - -const PNID = model('PNID', PNIDSchema); - -module.exports = { - PNIDSchema, - PNID +const { Schema, model } = require('mongoose'); +const uniqueValidator = require('mongoose-unique-validator'); +const bcrypt = require('bcrypt'); +const crypto = require('crypto'); +const util = require('../util'); +const { DeviceSchema } = require('./device'); + +const PNIDSchema = new Schema({ + access_level: { + type: Number, + default: 0 // Standard user + }, + pid: { + type: Number, + unique: true + }, + creation_date: String, + updated: String, + username: { + type: String, + unique: true, + minlength: 6, + maxlength: 16 + }, + usernameLower: { + type: String, + unique: true + }, + password: String, + birthdate: String, + gender: String, + country: String, + language: String, + email: { + address: String, + primary: Boolean, + parent: Boolean, + reachable: Boolean, + validated: Boolean, + validated_date: String, + id: { + type: Number, + unique: true + } + }, + region: Number, + timezone: { + name: String, + offset: Number + }, + mii: { + name: String, + primary: Boolean, + data: String, + id: { + type: Number, + unique: true + }, + hash: { + type: String, + unique: true + }, + image_url: String, + image_id: { + type: Number, + unique: true + }, + }, + flags: { // not entirely sure what these are used for + active: Boolean, // Is the account active? Like, not deleted maybe? + marketing: Boolean, + off_device: Boolean + }, + devices: [DeviceSchema], + nex: { + password: String, + token: String + }, + identification: { // user identification tokens + email_code: { + type: String, + unique: true + }, + email_token: { + type: String, + unique: true + }, + access_token: { + value: String, + ttl: Number + }, + refresh_token: { + value: String, + ttl: Number + } + } +}); + +PNIDSchema.plugin(uniqueValidator, {message: '{PATH} already in use.'}); + +/* + According to http://pf2m.com/tools/rank.php Nintendo PID's start at 1,800,000,000 and count down with each account + This means the max PID is 1799999999 and hard-limits the number of potential accounts to 1,800,000,000 + The author of that site does not give any information on how they found this out, but it does seem to hold true + https://account.nintendo.net/v1/api/admin/mapped_ids?input_type=pid&output_type=user_id&input=1800000000 returns nothing + https://account.nintendo.net/v1/api/admin/mapped_ids?input_type=pid&output_type=user_id&input=1799999999 returns `prodtest1` + and the next few accounts counting down seem to be admin, service and internal test accounts +*/ +PNIDSchema.methods.generatePID = async function() { + const min = 1000000000; // The console (WiiU) seems to not accept PIDs smaller than this + const max = 1799999999; + + let pid = Math.floor(Math.random() * (max - min + 1) + min); + + const inuse = await PNID.findOne({ + pid + }); + + pid = (inuse ? await PNID.generatePID() : pid); + + this.set('pid', pid); +}; + +PNIDSchema.methods.generateNEXPassword = function() { + function character() { + const offset = Math.floor(Math.random() * 62); + if (offset < 10) return offset; + if (offset < 36) return String.fromCharCode(offset + 55); + return String.fromCharCode(offset + 61); + } + + const output = []; + + while (output.length < 16) { + output.push(character()); + } + + this.set('nex.password', output.join('')); +}; + +PNIDSchema.methods.generateEmailValidationCode = async function() { + let code = Math.random().toFixed(6).split('.')[1]; // Dirty one-liner to generate numbers of 6 length and padded 0 + + const inuse = await PNID.findOne({ + 'identification.email_code': code + }); + + code = (inuse ? await PNID.generateEmailValidationCode() : code); + + this.set('identification.email_code', code); +}; + +PNIDSchema.methods.generateEmailValidationToken = async function() { + let token = crypto.randomBytes(32).toString('hex'); + + const inuse = await PNID.findOne({ + 'identification.email_token': token + }); + + token = (inuse ? await PNID.generateEmailValidationToken() : token); + + this.set('identification.email_token', token); +}; + +PNIDSchema.methods.getDevice = async function(document) { + const devices = this.get('devices'); + + return devices.find(device => { + return ( + (device.device_id === document.device_id) && + (device.device_type === document.device_type) && + (device.serial === document.serial) + ); + }); +}; + +PNIDSchema.methods.addDevice = async function(device) { + this.devices.push(device); + + await this.save(); +}; + +PNIDSchema.methods.removeDevice = async function(device) { + this.devices = this.devices.filter(({ _id }) => _id !== device._id); + + await this.save(); +}; + +PNIDSchema.methods.updateMii = async function({name, primary, data}) { + this.set('mii.name', name); + this.set('mii.primary', primary === 'Y'); + this.set('mii.data', data); + + await this.save(); +}; + +PNIDSchema.pre('save', async function(next) { + if (!this.isModified('password')) { + return next(); + } + + this.set('usernameLower', this.get('username').toLowerCase()); + await this.generatePID(); + await this.generateNEXPassword(); + await this.generateEmailValidationCode(); + await this.generateEmailValidationToken(); + + const primaryHash = util.nintendoPasswordHash(this.get('password'), this.get('pid')); + const hash = bcrypt.hashSync(primaryHash, 10); + + this.set('password', hash); + next(); +}); + +const PNID = model('PNID', PNIDSchema); + +module.exports = { + PNIDSchema, + PNID }; \ No newline at end of file diff --git a/src/server.js b/src/server.js index 3ec6a57..ef672dc 100644 --- a/src/server.js +++ b/src/server.js @@ -1,59 +1,74 @@ -process.title = 'Pretendo - Account'; - -const express = require('express'); -const morgan = require('morgan'); -const xmlparser = require('./middleware/xml-parser'); -const database = require('./database'); -const logger = require('../logger'); -const config = require('./config.json'); - -const { http: { port } } = config; -const app = express(); - -const account = require('./services/account'); - -// START APPLICATION -app.set('etag', false); -app.disable('x-powered-by'); - -// Create router -logger.info('Setting up Middleware'); -app.use(morgan('dev')); -app.use(express.json()); -app.use(express.urlencoded({ - extended: true -})); -app.use(xmlparser); - -// import the servers into one -app.use(account); - -// 404 handler -logger.info('Creating 404 status handler'); -app.use((request, response) => { - response.status(404); - response.send(); -}); - -// non-404 error handler -logger.info('Creating non-404 status handler'); -app.use((error, request, response) => { - const status = error.status || 500; - - response.status(status); - - response.json({ - app: 'api', - status, - error: error.message - }); -}); - -// Starts the server -logger.info('Starting server'); - -database.connect().then(() => { - app.listen(port, () => { - logger.success(`Server started on port ${port}`); - }); +process.title = 'Pretendo - Account'; + +const express = require('express'); +const morgan = require('morgan'); +const xmlparser = require('./middleware/xml-parser'); +const database = require('./database'); +const util = require('./util'); +const logger = require('../logger'); +const config = require('./config.json'); + +const { http: { port } } = config; +const app = express(); + +const accountWiiU = require('./services/wiiu'); +//const account3DS = require('./services/3ds'); + +// START APPLICATION +app.set('etag', false); +app.disable('x-powered-by'); + +// Create router +logger.info('Setting up Middleware'); +app.use(morgan('dev')); +app.use(express.json()); +app.use(express.urlencoded({ + extended: true +})); +app.use(xmlparser); + +// import the servers into one +app.use(accountWiiU); +//app.use(account3DS); + +// 404 handler +logger.info('Creating 404 status handler'); +app.use((request, response) => { + const fullUrl = util.fullUrl(request); + const deviceId = request.headers['X-Nintendo-Device-ID'] || 'Unknown'; + + logger.warn(`HTTP 404 at ${fullUrl} from ${deviceId}`); + + response.status(404); + response.json({ + app: 'api', + status: 404, + error: 'Route not found' + }); +}); + +// non-404 error handler +logger.info('Creating non-404 status handler'); +app.use((error, request, response) => { + const status = error.status || 500; + const fullUrl = util.fullUrl(request); + const deviceId = request.headers['X-Nintendo-Device-ID'] || 'Unknown'; + + logger.warn(`HTTP ${status} at ${fullUrl} from ${deviceId}: ${error.message}`); + + response.status(status); + response.json({ + app: 'api', + status, + error: error.message + }); +}); + +// Starts the server +logger.info('Starting server'); + +database.connect().then(() => { + app.listen(port, () => { + logger.success(`Server started on port ${port}`); + }); }); \ No newline at end of file diff --git a/src/services/3ds/index.js b/src/services/3ds/index.js new file mode 100644 index 0000000..43134f1 --- /dev/null +++ b/src/services/3ds/index.js @@ -0,0 +1,22 @@ +// handles NASC endpoints + +const express = require('express'); +const subdomain = require('express-subdomain'); +const logger = require('../../../logger'); +const routes = require('./routes'); + +// Main router for endpoints +const router = express.Router(); + +// Router to handle the subdomain restriction +const nasc = express.Router(); + +// Create subdomains +logger.info('[ACCOUNT - 3DS] Creating \'nasc\' subdomain'); +router.use(subdomain('nasc', nasc)); + +// Setup routes +logger.info('[ACCOUNT - 3DS] Applying imported routes'); +nasc.use('/ac', routes.ACCOUNT); + +module.exports = router; \ No newline at end of file diff --git a/src/services/3ds/routes/account.js b/src/services/3ds/routes/account.js new file mode 100644 index 0000000..e69de29 diff --git a/src/services/3ds/routes/index.js b/src/services/3ds/routes/index.js new file mode 100644 index 0000000..669919e --- /dev/null +++ b/src/services/3ds/routes/index.js @@ -0,0 +1,3 @@ +module.exports = { + ACCOUNT: require('./account') +}; \ No newline at end of file diff --git a/src/services/account/index.js b/src/services/wiiu/index.js similarity index 75% rename from src/services/account/index.js rename to src/services/wiiu/index.js index 635c68b..e75ae4e 100644 --- a/src/services/account/index.js +++ b/src/services/wiiu/index.js @@ -1,32 +1,35 @@ -const express = require('express'); -const subdomain = require('express-subdomain'); -const sessionMiddleware = require('../../middleware/session'); -const pnidMiddleware = require('../../middleware/pnid'); -const logger = require('../../../logger'); -const routes = require('./routes'); - -// Main router for endpoints -const router = express.Router(); - -// Router to handle the subdomain restriction -const account = express.Router(); - -// Create subdomains -logger.info('[ACCOUNT] Creating \'account\' subdomain'); -router.use(subdomain('account', account)); - -logger.info('[ACCOUNT] Importing middleware'); -account.use(sessionMiddleware); -account.use(pnidMiddleware); - -// Setup routes -logger.info('[ACCOUNT] Applying imported routes'); -account.use('/v1/api/admin', routes.ADMIN); -account.use('/v1/api/content', routes.CONTENT); -account.use('/v1/api/devices', routes.DEVICES); -account.use('/v1/api/oauth20', routes.OAUTH); -account.use('/v1/api/people', routes.PEOPLE); -account.use('/v1/api/provider', routes.PROVIDER); -account.use('/v1/api/support', routes.SUPPORT); - +// handles "account.nintendo.net" endpoints + +const express = require('express'); +const subdomain = require('express-subdomain'); +const sessionMiddleware = require('../../middleware/session'); +const pnidMiddleware = require('../../middleware/pnid'); +const logger = require('../../../logger'); +const routes = require('./routes'); + +// Router to handle the subdomain restriction +const account = express.Router(); + +logger.info('[ACCOUNT - WiiU] Importing middleware'); +account.use(sessionMiddleware); +account.use(pnidMiddleware); + +// Setup routes +logger.info('[ACCOUNT - WiiU] Applying imported routes'); +account.use('/v1/api/admin', routes.ADMIN); +account.use('/v1/api/content', routes.CONTENT); +account.use('/v1/api/devices', routes.DEVICES); +account.use('/v1/api/miis', routes.MIIS); +account.use('/v1/api/oauth20', routes.OAUTH); +account.use('/v1/api/people', routes.PEOPLE); +account.use('/v1/api/provider', routes.PROVIDER); +account.use('/v1/api/support', routes.SUPPORT); + +// Main router for endpoints +const router = express.Router(); + +// Create subdomains +logger.info('[ACCOUNT - WiiU] Creating \'account\' subdomain'); +router.use(subdomain('account', account)); + module.exports = router; \ No newline at end of file diff --git a/src/services/account/routes/admin.js b/src/services/wiiu/routes/admin.js similarity index 96% rename from src/services/account/routes/admin.js rename to src/services/wiiu/routes/admin.js index 118602b..12ea600 100644 --- a/src/services/account/routes/admin.js +++ b/src/services/wiiu/routes/admin.js @@ -1,39 +1,39 @@ -const router = require('express').Router(); -const xmlbuilder = require('xmlbuilder'); -const { PNID } = require('../../../models/pnid'); -const clientHeaderCheck = require('../../../middleware/client-header'); - -/** - * [GET] - * Replacement for: https://account.nintendo.net/v1/api/admin/mapped_ids - * Description: Maps given input to expected output - */ -router.get('/mapped_ids', clientHeaderCheck, async (request, response) => { - - let { input: inputList, input_type: inputType, output_type: outputType } = request.query; - inputList = inputList.split(','); - - if (inputType === 'user_id') { - inputType = 'usernameLower'; - - inputList = inputList.map(name => name.toLowerCase()); - } - - if (outputType === 'user_id') { - outputType = 'username'; - } - - let results = await PNID.where(inputType, inputList); - results = results.map(user => ({ - in_id: user.get(inputType), - out_id: user.get(outputType) || '' - })); - - response.send(xmlbuilder.create({ - mapped_ids: { - mapped_id: results - } - }).end()); -}); - +const router = require('express').Router(); +const xmlbuilder = require('xmlbuilder'); +const { PNID } = require('../../../models/pnid'); +const clientHeaderCheck = require('../../../middleware/client-header'); + +/** + * [GET] + * Replacement for: https://account.nintendo.net/v1/api/admin/mapped_ids + * Description: Maps given input to expected output + */ +router.get('/mapped_ids', clientHeaderCheck, async (request, response) => { + + let { input: inputList, input_type: inputType, output_type: outputType } = request.query; + inputList = inputList.split(','); + + if (inputType === 'user_id') { + inputType = 'usernameLower'; + + inputList = inputList.map(name => name.toLowerCase()); + } + + if (outputType === 'user_id') { + outputType = 'username'; + } + + let results = await PNID.where(inputType, inputList); + results = results.map(user => ({ + in_id: user.get(inputType), + out_id: user.get(outputType) || '' + })); + + response.send(xmlbuilder.create({ + mapped_ids: { + mapped_id: results + } + }).end()); +}); + module.exports = router; \ No newline at end of file diff --git a/src/services/account/routes/content.js b/src/services/wiiu/routes/content.js similarity index 96% rename from src/services/account/routes/content.js rename to src/services/wiiu/routes/content.js index 1a5c052..394a793 100644 --- a/src/services/account/routes/content.js +++ b/src/services/wiiu/routes/content.js @@ -1,161 +1,161 @@ -const router = require('express').Router(); -const xmlbuilder = require('xmlbuilder'); -const timezones = require('../timezones.json'); -const clientHeaderCheck = require('../../../middleware/client-header'); - -/** - * [GET] - * Replacement for: https://account.nintendo.net/v1/api/content/agreements/TYPE/REGION/VERSION - * Description: Sends the client requested agreement - */ -router.get('/agreements/:type/:region/:version', clientHeaderCheck, (request, response) => { - response.set('Content-Type', 'text/xml'); - response.set('Server', 'Nintendo 3DS (http)'); - response.set('X-Nintendo-Date', new Date().getTime()); - - // Registration process has started - request.session.registration_status = 0; - - response.send(xmlbuilder.create({ - agreements: { - agreement: [ - { - country: 'US', - language: 'en', - language_name: 'English', - publish_date: '2014-09-29T20:07:35', - texts: { - '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', - '@xsi:type': 'chunkedStoredAgreementText', - - main_title: { - '#cdata': 'Pretendo Network Services Agreement' - }, - agree_text: { - '#cdata': 'I Accept' - }, - non_agree_text: { - '#cdata': 'I Decline' - }, - main_text: { - '@index': '1', - '#cdata': 'Dont be dumb' - } - }, - type: 'NINTENDO-NETWORK-EULA', - version: '0300', - }, - { - country: 'US', - language: 'en', - language_name: 'Español', - publish_date: '2014-09-29T20:07:35', - texts: { - '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', - '@xsi:type': 'chunkedStoredAgreementText', - - main_title: { - '#cdata': 'Pretendo Network Services Agreement' - }, - agree_text: { - '#cdata': 'I Accept' - }, - non_agree_text: { - '#cdata': 'I Decline' - }, - main_text: { - '@index': '1', - '#cdata': 'Dont be dumb' - } - }, - type: 'NINTENDO-NETWORK-EULA', - version: '0300', - }, - { - country: 'US', - language: 'en', - language_name: 'Français', - publish_date: '2014-09-29T20:07:35', - texts: { - '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', - '@xsi:type': 'chunkedStoredAgreementText', - - main_title: { - '#cdata': 'Pretendo Network Services Agreement' - }, - agree_text: { - '#cdata': 'I Accept' - }, - non_agree_text: { - '#cdata': 'I Decline' - }, - main_text: { - '@index': '1', - '#cdata': 'Dont be dumb' - } - }, - type: 'NINTENDO-NETWORK-EULA', - version: '0300', - } - ] - } - }).end()); -}); - -/** - * [GET] - * Replacement for: https://account.nintendo.net/v1/api/content/time_zones/COUNTRY/LANGUAGE - * Description: Sends the client the requested timezones - */ -router.get('/time_zones/:countryCode/:language', clientHeaderCheck, (request, response) => { - response.set('Content-Type', 'text/xml'); - response.set('Server', 'Nintendo 3DS (http)'); - response.set('X-Nintendo-Date', new Date().getTime()); - - // Status should be 0 from previous request in registration process - if (request.session.registration_status !== 0) { - response.status(400); - - return response.send(xmlbuilder.create({ - error: { - cause: 'Bad Request', - code: '1600', - message: 'Unable to process request' - } - }).end()); - } - - /* - // Old method. Crashes WiiU when sending a list with over 32 entries, but otherwise works - // countryTimezones is "countries-and-timezones" module - - const country = countryTimezones.getCountry(countryCode); - const timezones = country.timezones.map((timezone, index) => { - const data = countryTimezones.getTimezone(timezone); - - return { - area: data.name, - language, - name: data.name, - utc_offset: data.utcOffset * 6 * 10, - order: index+1 - }; - }); - */ - - const { countryCode, language } = request.params; - - const regionLanguages = timezones[countryCode]; - const regionTimezones = regionLanguages[language] ? regionLanguages[language] : Object.values(regionLanguages)[0]; - - // Bump status to allow access to next endpoint - request.session.registration_status = 1; - - response.send(xmlbuilder.create({ - timezones: { - timezone: regionTimezones - } - }).end()); -}); - +const router = require('express').Router(); +const xmlbuilder = require('xmlbuilder'); +const timezones = require('../timezones.json'); +const clientHeaderCheck = require('../../../middleware/client-header'); + +/** + * [GET] + * Replacement for: https://account.nintendo.net/v1/api/content/agreements/TYPE/REGION/VERSION + * Description: Sends the client requested agreement + */ +router.get('/agreements/:type/:region/:version', clientHeaderCheck, (request, response) => { + response.set('Content-Type', 'text/xml'); + response.set('Server', 'Nintendo 3DS (http)'); + response.set('X-Nintendo-Date', new Date().getTime()); + + // Registration process has started + request.session.registration_status = 0; + + response.send(xmlbuilder.create({ + agreements: { + agreement: [ + { + country: 'US', + language: 'en', + language_name: 'English', + publish_date: '2014-09-29T20:07:35', + texts: { + '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + '@xsi:type': 'chunkedStoredAgreementText', + + main_title: { + '#cdata': 'Pretendo Network Services Agreement' + }, + agree_text: { + '#cdata': 'I Accept' + }, + non_agree_text: { + '#cdata': 'I Decline' + }, + main_text: { + '@index': '1', + '#cdata': 'Dont be dumb' + } + }, + type: 'NINTENDO-NETWORK-EULA', + version: '0300', + }, + { + country: 'US', + language: 'en', + language_name: 'Español', + publish_date: '2014-09-29T20:07:35', + texts: { + '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + '@xsi:type': 'chunkedStoredAgreementText', + + main_title: { + '#cdata': 'Pretendo Network Services Agreement' + }, + agree_text: { + '#cdata': 'I Accept' + }, + non_agree_text: { + '#cdata': 'I Decline' + }, + main_text: { + '@index': '1', + '#cdata': 'Dont be dumb' + } + }, + type: 'NINTENDO-NETWORK-EULA', + version: '0300', + }, + { + country: 'US', + language: 'en', + language_name: 'Français', + publish_date: '2014-09-29T20:07:35', + texts: { + '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + '@xsi:type': 'chunkedStoredAgreementText', + + main_title: { + '#cdata': 'Pretendo Network Services Agreement' + }, + agree_text: { + '#cdata': 'I Accept' + }, + non_agree_text: { + '#cdata': 'I Decline' + }, + main_text: { + '@index': '1', + '#cdata': 'Dont be dumb' + } + }, + type: 'NINTENDO-NETWORK-EULA', + version: '0300', + } + ] + } + }).end()); +}); + +/** + * [GET] + * Replacement for: https://account.nintendo.net/v1/api/content/time_zones/COUNTRY/LANGUAGE + * Description: Sends the client the requested timezones + */ +router.get('/time_zones/:countryCode/:language', clientHeaderCheck, (request, response) => { + response.set('Content-Type', 'text/xml'); + response.set('Server', 'Nintendo 3DS (http)'); + response.set('X-Nintendo-Date', new Date().getTime()); + + // Status should be 0 from previous request in registration process + if (request.session.registration_status !== 0) { + response.status(400); + + return response.send(xmlbuilder.create({ + error: { + cause: 'Bad Request', + code: '1600', + message: 'Unable to process request' + } + }).end()); + } + + /* + // Old method. Crashes WiiU when sending a list with over 32 entries, but otherwise works + // countryTimezones is "countries-and-timezones" module + + const country = countryTimezones.getCountry(countryCode); + const timezones = country.timezones.map((timezone, index) => { + const data = countryTimezones.getTimezone(timezone); + + return { + area: data.name, + language, + name: data.name, + utc_offset: data.utcOffset * 6 * 10, + order: index+1 + }; + }); + */ + + const { countryCode, language } = request.params; + + const regionLanguages = timezones[countryCode]; + const regionTimezones = regionLanguages[language] ? regionLanguages[language] : Object.values(regionLanguages)[0]; + + // Bump status to allow access to next endpoint + request.session.registration_status = 1; + + response.send(xmlbuilder.create({ + timezones: { + timezone: regionTimezones + } + }).end()); +}); + module.exports = router; \ No newline at end of file diff --git a/src/services/account/routes/devices.js b/src/services/wiiu/routes/devices.js similarity index 96% rename from src/services/account/routes/devices.js rename to src/services/wiiu/routes/devices.js index d666937..3c7c398 100644 --- a/src/services/account/routes/devices.js +++ b/src/services/wiiu/routes/devices.js @@ -1,16 +1,16 @@ -const router = require('express').Router(); -const xmlbuilder = require('xmlbuilder'); -const clientHeaderCheck = require('../../../middleware/client-header'); - -/** - * [GET] - * Replacement for: https://account.nintendo.net/v1/api/devices/@current/status - * Description: Unknown use - */ -router.get('/@current/status', clientHeaderCheck, async (request, response) => { - response.send(xmlbuilder.create({ - device: '' - }).end()); -}); - +const router = require('express').Router(); +const xmlbuilder = require('xmlbuilder'); +const clientHeaderCheck = require('../../../middleware/client-header'); + +/** + * [GET] + * Replacement for: https://account.nintendo.net/v1/api/devices/@current/status + * Description: Unknown use + */ +router.get('/@current/status', clientHeaderCheck, async (request, response) => { + response.send(xmlbuilder.create({ + device: '' + }).end()); +}); + module.exports = router; \ No newline at end of file diff --git a/src/services/account/routes/index.js b/src/services/wiiu/routes/index.js similarity index 87% rename from src/services/account/routes/index.js rename to src/services/wiiu/routes/index.js index 32493af..b5becf4 100644 --- a/src/services/account/routes/index.js +++ b/src/services/wiiu/routes/index.js @@ -1,9 +1,10 @@ -module.exports = { - ADMIN: require('./admin'), - CONTENT: require('./content'), - DEVICES: require('./devices'), - OAUTH: require('./oauth'), - PEOPLE: require('./people'), - PROVIDER: require('./provider'), - SUPPORT: require('./support'), +module.exports = { + ADMIN: require('./admin'), + CONTENT: require('./content'), + DEVICES: require('./devices'), + MIIS: require('./miis'), + OAUTH: require('./oauth'), + PEOPLE: require('./people'), + PROVIDER: require('./provider'), + SUPPORT: require('./support'), }; \ No newline at end of file diff --git a/src/services/wiiu/routes/miis.js b/src/services/wiiu/routes/miis.js new file mode 100644 index 0000000..5297875 --- /dev/null +++ b/src/services/wiiu/routes/miis.js @@ -0,0 +1,95 @@ +const router = require('express').Router(); +const xmlbuilder = require('xmlbuilder'); +const { PNID } = require('../../../models/pnid'); +const clientHeaderCheck = require('../../../middleware/client-header'); + +/** + * [GET] + * Replacement for: https://account.nintendo.net/v1/api/miis + * Description: Returns a list of NNID miis + */ +router.get('/', clientHeaderCheck, async (request, response) => { + + const { pids } = request.query; + + const results = await PNID.where('pid', pids); + const miis = []; + + // We don't have a Mii renderer yet so hard code the images + const hardCodedImages = [ + { + cached_url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_standard.png', + id: 1292086302, + url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_standard.png', + type: 'standard' + }, + { + cached_url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_frustrated_face.png', + id: 1292086302, + url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_frustrated_face.png', + type: 'frustrated_face' + }, + { + cached_url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_happy_face.png', + id: 1292086302, + url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_happy_face.png', + type: 'happy_face' + }, + { + cached_url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_like_face.png', + id: 1292086302, + url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_like_face.png', + type: 'like_face' + }, + { + cached_url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_normal_face.png', + id: 1292086302, + url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_normal_face.png', + type: 'normal_face' + }, + { + cached_url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_puzzled_face.png', + id: 1292086302, + url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_puzzled_face.png', + type: 'puzzled_face' + }, + { + cached_url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_surprised_face.png', + id: 1292086302, + url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_surprised_face.png', + type: 'surprised_face' + }, + { + cached_url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_whole_body.png', + id: 1292086302, + url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_whole_body.png', + type: 'whole_body' + } + ]; + + for (const user of results) { + const { mii } = user; + + miis.push({ + data: mii.data.replace(/(\r\n|\n|\r)/gm, ''), + id: mii.id, + images: { + image: hardCodedImages + }, + name: mii.name, + pid: user.pid, + primary: mii.primary ? 'Y' : 'N', + user_id: user.username + }); + } + + //console.log(results[0].mii.data.replace(/(\r\n|\n|\r)/gm, '')); + + response.send(xmlbuilder.create({ + miis: { + mii: miis + } + }).end()); +}); + +module.exports = router; \ No newline at end of file diff --git a/src/services/account/routes/oauth.js b/src/services/wiiu/routes/oauth.js similarity index 96% rename from src/services/account/routes/oauth.js rename to src/services/wiiu/routes/oauth.js index 79e3321..55f6506 100644 --- a/src/services/account/routes/oauth.js +++ b/src/services/wiiu/routes/oauth.js @@ -1,116 +1,116 @@ -const router = require('express').Router(); -const xmlbuilder = require('xmlbuilder'); -const bcrypt = require('bcrypt'); -const fs = require('fs-extra'); -const clientHeaderCheck = require('../../../middleware/client-header'); -const database = require('../../../database'); -const util = require('../../../util'); - -/** - * [POST] - * Replacement for: https://account.nintendo.net/v1/api/oauth20/access_token/generate - * Description: Generates an access token for a user - */ -router.post('/access_token/generate', clientHeaderCheck, async (request, response) => { - const titleId = request.headers['x-nintendo-title-id']; - const { body } = request; - const { grant_type, user_id, password } = body; - - if (!['password', 'refresh_token'].includes(grant_type)) { - response.status(400); - return response.send(xmlbuilder.create({ - error: { - cause: 'grant_type', - code: '0004', - message: 'Invalid Grant Type' - } - }).end()); - } - - if (!user_id || user_id.trim() === '') { - response.status(400); - return response.send(xmlbuilder.create({ - error: { - cause: 'user_id', - code: '0002', - message: 'user_id format is invalid' - } - }).end()); - } - - if (!password || password.trim() === '') { - response.status(400); - return response.send(xmlbuilder.create({ - error: { - cause: 'password', - code: '0002', - message: 'password format is invalid' - } - }).end()); - } - - const pnid = await database.getUserByUsername(user_id); - - if (!pnid || !bcrypt.compareSync(password, pnid.password)) { - response.status(400); - return response.send(xmlbuilder.create({ - error: { - code: '0106', - message: 'Invalid account ID or password' - } - }).end()); - } - - const cryptoPath = `${__dirname}/../../../../certs/access`; - - if (!fs.pathExistsSync(cryptoPath)) { - // Need to generate keys - return response.send(xmlbuilder.create({ - errors: { - error: { - code: '0000', - message: 'Could not find access key crypto path' - } - } - }).end()); - } - - const publicKey = fs.readFileSync(`${cryptoPath}/public.pem`); - const hmacSecret = fs.readFileSync(`${cryptoPath}/secret.key`); - - const cryptoOptions = { - public_key: publicKey, - hmac_secret: hmacSecret - }; - - const accessTokenOptions = { - system_type: 0x1, // WiiU - token_type: 0x1, // OAuth Access, - pid: pnid.get('pid'), - title_id: BigInt(parseInt(titleId, 16)), - date: BigInt(Date.now()) - }; - - const refreshTokenOptions = { - system_type: 0x1, // WiiU - token_type: 0x2, // OAuth Refresh, - pid: pnid.get('pid'), - title_id: BigInt(parseInt(titleId, 16)), - date: BigInt(Date.now()) - }; - - const accessToken = util.generateToken(cryptoOptions, accessTokenOptions); - const refreshToken = util.generateToken(cryptoOptions, refreshTokenOptions); - - response.send(xmlbuilder.create({ - OAuth20: { - access_token: { - token: accessToken, - refresh_token: refreshToken, - expires_in: 3600 - } - } - }).end()); -}); - +const router = require('express').Router(); +const xmlbuilder = require('xmlbuilder'); +const bcrypt = require('bcrypt'); +const fs = require('fs-extra'); +const clientHeaderCheck = require('../../../middleware/client-header'); +const database = require('../../../database'); +const util = require('../../../util'); + +/** + * [POST] + * Replacement for: https://account.nintendo.net/v1/api/oauth20/access_token/generate + * Description: Generates an access token for a user + */ +router.post('/access_token/generate', clientHeaderCheck, async (request, response) => { + const titleId = request.headers['x-nintendo-title-id']; + const { body } = request; + const { grant_type, user_id, password } = body; + + if (!['password', 'refresh_token'].includes(grant_type)) { + response.status(400); + return response.send(xmlbuilder.create({ + error: { + cause: 'grant_type', + code: '0004', + message: 'Invalid Grant Type' + } + }).end()); + } + + if (!user_id || user_id.trim() === '') { + response.status(400); + return response.send(xmlbuilder.create({ + error: { + cause: 'user_id', + code: '0002', + message: 'user_id format is invalid' + } + }).end()); + } + + if (!password || password.trim() === '') { + response.status(400); + return response.send(xmlbuilder.create({ + error: { + cause: 'password', + code: '0002', + message: 'password format is invalid' + } + }).end()); + } + + const pnid = await database.getUserByUsername(user_id); + + if (!pnid || !bcrypt.compareSync(password, pnid.password)) { + response.status(400); + return response.send(xmlbuilder.create({ + error: { + code: '0106', + message: 'Invalid account ID or password' + } + }).end()); + } + + const cryptoPath = `${__dirname}/../../../../certs/access`; + + if (!fs.pathExistsSync(cryptoPath)) { + // Need to generate keys + return response.send(xmlbuilder.create({ + errors: { + error: { + code: '0000', + message: 'Could not find access key crypto path' + } + } + }).end()); + } + + const publicKey = fs.readFileSync(`${cryptoPath}/public.pem`); + const hmacSecret = fs.readFileSync(`${cryptoPath}/secret.key`); + + const cryptoOptions = { + public_key: publicKey, + hmac_secret: hmacSecret + }; + + const accessTokenOptions = { + system_type: 0x1, // WiiU + token_type: 0x1, // OAuth Access, + pid: pnid.get('pid'), + title_id: BigInt(parseInt(titleId, 16)), + date: BigInt(Date.now()) + }; + + const refreshTokenOptions = { + system_type: 0x1, // WiiU + token_type: 0x2, // OAuth Refresh, + pid: pnid.get('pid'), + title_id: BigInt(parseInt(titleId, 16)), + date: BigInt(Date.now()) + }; + + const accessToken = util.generateToken(cryptoOptions, accessTokenOptions); + const refreshToken = util.generateToken(cryptoOptions, refreshTokenOptions); + + response.send(xmlbuilder.create({ + OAuth20: { + access_token: { + token: accessToken, + refresh_token: refreshToken, + expires_in: 3600 + } + } + }).end()); +}); + module.exports = router; \ No newline at end of file diff --git a/src/services/account/routes/people.js b/src/services/wiiu/routes/people.js similarity index 97% rename from src/services/account/routes/people.js rename to src/services/wiiu/routes/people.js index 7673962..3fffab5 100644 --- a/src/services/account/routes/people.js +++ b/src/services/wiiu/routes/people.js @@ -1,283 +1,283 @@ -const router = require('express').Router(); -const xmlbuilder = require('xmlbuilder'); -const moment = require('moment'); -const crypto = require('crypto'); -const { PNID } = require('../../../models/pnid'); -const clientHeaderCheck = require('../../../middleware/client-header'); -const database = require('../../../database'); -const mailer = require('../../../mailer'); -require('moment-timezone'); - -router.get('/:username', clientHeaderCheck, async (request, response) => { - // Status should be 1 from previous request in registration process - if (request.session.registration_status !== 1) { - response.status(400); - - return response.send(xmlbuilder.create({ - error: { - cause: 'Bad Request', - code: '1600', - message: 'Unable to process request' - } - }).end()); - } - - const { username } = request.params; - - const userExists = await database.doesUserExist(username); - - if (userExists) { - response.status(400); - - return response.end(xmlbuilder.create({ - errors: { - error: { - code: '0100', - message: 'Account ID already exists' - } - } - }).end()); - } - - // Bump status to allow access to next endpoint - request.session.registration_status = 2; - - response.status(200); - response.end(); -}); - -router.post('/', clientHeaderCheck, async (request, response) => { - // Status should be 3 from previous request in registration process - if (request.session.registration_status !== 3) { - response.status(400); - - return response.send(xmlbuilder.create({ - error: { - cause: 'Bad Request', - code: '1600', - message: 'Unable to process request' - } - }).end()); - } - - const person = request.body.get('person'); - - const userExists = await database.doesUserExist(person.get('user_id')); - - if (userExists) { - response.status(400); - - return response.end(xmlbuilder.create({ - errors: { - error: { - code: '0100', - message: 'Account ID already exists' - } - } - }).end()); - } - - const creationDate = moment().format('YYYY-MM-DDTHH:MM:SS'); - - const document = { - pid: 1, // will be overwritten before saving - creation_date: creationDate, - updated: creationDate, - username: person.get('user_id'), - password: person.get('password'), // will be hashed before saving - birthdate: person.get('birth_date'), - gender: person.get('gender'), - country: person.get('country'), - language: person.get('language'), - email: { - address: person.get('email').get('address'), - primary: person.get('email').get('primary') === 'Y', - parent: person.get('email').get('parent') === 'Y', - reachable: false, - validated: person.get('email').get('validated') === 'Y', - id: Math.floor(Math.random(10000000000)*10000000000) - }, - region: person.get('region'), - timezone: { - name: person.get('tz_name'), - offset: (moment.tz(person.get('tz_name')).utcOffset() * 60) - }, - mii: { - name: person.get('mii').get('name'), - primary: person.get('mii').get('name') === 'Y', - data: person.get('mii').get('data'), - id: Math.floor(Math.random(10000000000)*10000000000), - hash: crypto.createHash('md5').update(person.get('mii').get('data')).digest('hex'), - image_url: 'https://mii-secure.account.nintendo.net/2rtgf01lztoqo_standard.tga', - image_id: Math.floor(Math.random(10000000000)*10000000000) - }, - flags: { - active: true, - marketing: person.get('marketing_flag') === 'Y', - off_device: person.get('off_device_flag') === 'Y' - }, - validation: { - email_code: 1, // will be overwritten before saving - email_token: '' // will be overwritten before saving - } - }; - - const newUser = new PNID(document); - - newUser.save() - .catch(error => { - console.log(error); - - response.status(400); - - return response.send(xmlbuilder.create({ - error: { - cause: 'Bad Request', - code: '1600', - message: 'Unable to process request' - } - }).end()); - }) - .then(async newUser => { - await mailer.send( - newUser.get('email'), - '[Prentendo Network] Please confirm your e-mail address', - `Hello, - - Your Prentendo Network ID activation is almost complete. Please click the link below to confirm your e-mail address and complete the activation process. - - https://account.pretendo.cc/account/email-confirmation?token=` + newUser.get('identification.email_token') + ` - - If you are unable to connect to the above URL, please enter the following confirmation code on the device to which your Prentendo Network ID is linked. - - <<Confirmation code: ` + newUser.get('identification.email_code') + '>>' - ); - - delete request.session.registration_status; - - response.send(xmlbuilder.create({ - person: { - pid: newUser.get('pid') - } - }).end()); - }); -}); - -/** - * [GET] - * Replacement for: https://account.nintendo.net/v1/api/people/@me/profile - * Description: Gets a users profile - */ -router.get('/@me/profile', clientHeaderCheck, async (request, response) => { - response.set('Content-Type', 'text/xml'); - response.set('Server', 'Nintendo 3DS (http)'); - response.set('X-Nintendo-Date', new Date().getTime()); - - - const { pnid } = request; - - const person = await database.getUserProfileJSONByPID(pnid.get('pid')); - - response.send(xmlbuilder.create({ - person - }).end()); - - - //response.send(`84955611ctr_initial_device_account_idUSER19583364384955489environmentUSERPRODESHOP.NINTENDO.NETINTERNAL327329101Y1998-09-22US2017-12-29T04:11:242019-11-24T15:26:06persistent_id800000432019-11-24T15:26:06transferable_id_base1200000444b6221d2019-11-24T15:26:06transferable_id_base_common1180000444b6221d2019-11-24T15:26:06uuid_account42ef6c46-0ea3-11ea-97fe-010144b6221d2019-11-24T15:26:06uuid_common3d9b06d8-0ea3-11ea-97fe-010144b6221dMen2019-06-02T04:17:56YY1750087940
halolink44@gmail.com
50463196NYYDEFAULTINTERNAL WSY2017-12-29T04:12:32
COMPLETEDAwBzMOlVognnx0GCk6r2p0D0B2n+cgAA0lJSAGUAZABEAHUAYwBrAHMAAAAAAGQrAAAWAQJoRBgmNEYUgRIXaI0AiiWBSUhQUgBlAGQARAB1AGMAawBzAHMAAAAAAJZY1151699634u2jg043u028xhttps://mii-secure.account.nintendo.net/u2jg043u028x_standard.tga1319591505https://mii-secure.account.nintendo.net/u2jg043u028x_standard.tgastandardRedDucksY822870016America/New_YorkRedDuckss-18000
`); - -}); - -/** - * [POST] - * Replacement for: https://account.nintendo.net/v1/api/people/@me/devices - * Description: Gets user profile, seems to be the same as https://account.nintendo.net/v1/api/people/@me/profile - */ -router.post('/@me/devices', clientHeaderCheck, async (request, response) => { - response.set('Content-Type', 'text/xml'); - response.set('Server', 'Nintendo 3DS (http)'); - response.set('X-Nintendo-Date', new Date().getTime()); - - // We don't care about the device attributes - // The console ignores them and PNIDs are not tied to consoles anyway - // So the server also ignores them and does not save the ones posted here - - const { pnid } = request; - - const person = await database.getUserProfileJSONByPID(pnid.get('pid')); - - response.send(xmlbuilder.create({ - person - }).end()); -}); - -/** - * [GET] - * Replacement for: https://account.nintendo.net/v1/api/people/@me/devices - * Description: Returns only user devices - */ -router.get('/@me/devices', clientHeaderCheck, async (request, response) => { - response.set('Content-Type', 'text/xml'); - response.set('Server', 'Nintendo 3DS (http)'); - response.set('X-Nintendo-Date', new Date().getTime()); - - const { pnid, headers } = request; - - response.send(xmlbuilder.create({ - devices: [ - { - device: { - device_id: headers['x-nintendo-device-id'], - language: headers['accept-language'], - updated: moment().format('YYYY-MM-DDTHH:MM:SS'), - pid: pnid.get('pid'), - platform_id: headers['x-nintendo-platform-id'], - region: headers['x-nintendo-region'], - serial_number: headers['x-nintendo-serial-number'], - status: 'ACTIVE', - system_version: headers['x-nintendo-system-version'], - type: 'RETAIL', - updated_by: 'USER' - } - } - ] - }).end()); -}); - -/** - * [GET] - * Replacement for: https://account.nintendo.net/v1/api/people/@me/devices/owner - * Description: Gets user profile, seems to be the same as https://account.nintendo.net/v1/api/people/@me/profile - */ -router.get('/@me/devices/owner', clientHeaderCheck, async (request, response) => { - response.set('Content-Type', 'text/xml'); - response.set('Server', 'Nintendo 3DS (http)'); - response.set('X-Nintendo-Date', moment().add(5, 'h')); - - const { pnid } = request; - - const person = await database.getUserProfileJSONByPID(pnid.get('pid')); - - response.send(xmlbuilder.create({ - person - }).end()); -}); - - -/** - * [PUT] - * Replacement for: https://account.nintendo.net/v1/api/people/@me/miis/@primary - * Description: Updates a users Mii - */ -router.put('/@me/miis/@primary', clientHeaderCheck, async (request, response) => { - const { pnid } = request; - - const mii = request.body.get('mii'); - - const [name, primary, data] = [mii.get('name'), mii.get('primary'), mii.get('data')] - - await pnid.updateMii({name, primary, data}); - - response.send(''); -}); - +const router = require('express').Router(); +const xmlbuilder = require('xmlbuilder'); +const moment = require('moment'); +const crypto = require('crypto'); +const { PNID } = require('../../../models/pnid'); +const clientHeaderCheck = require('../../../middleware/client-header'); +const database = require('../../../database'); +const mailer = require('../../../mailer'); +require('moment-timezone'); + +router.get('/:username', clientHeaderCheck, async (request, response) => { + // Status should be 1 from previous request in registration process + if (request.session.registration_status !== 1) { + response.status(400); + + return response.send(xmlbuilder.create({ + error: { + cause: 'Bad Request', + code: '1600', + message: 'Unable to process request' + } + }).end()); + } + + const { username } = request.params; + + const userExists = await database.doesUserExist(username); + + if (userExists) { + response.status(400); + + return response.end(xmlbuilder.create({ + errors: { + error: { + code: '0100', + message: 'Account ID already exists' + } + } + }).end()); + } + + // Bump status to allow access to next endpoint + request.session.registration_status = 2; + + response.status(200); + response.end(); +}); + +router.post('/', clientHeaderCheck, async (request, response) => { + // Status should be 3 from previous request in registration process + if (request.session.registration_status !== 3) { + response.status(400); + + return response.send(xmlbuilder.create({ + error: { + cause: 'Bad Request', + code: '1600', + message: 'Unable to process request' + } + }).end()); + } + + const person = request.body.get('person'); + + const userExists = await database.doesUserExist(person.get('user_id')); + + if (userExists) { + response.status(400); + + return response.end(xmlbuilder.create({ + errors: { + error: { + code: '0100', + message: 'Account ID already exists' + } + } + }).end()); + } + + const creationDate = moment().format('YYYY-MM-DDTHH:MM:SS'); + + const document = { + pid: 1, // will be overwritten before saving + creation_date: creationDate, + updated: creationDate, + username: person.get('user_id'), + password: person.get('password'), // will be hashed before saving + birthdate: person.get('birth_date'), + gender: person.get('gender'), + country: person.get('country'), + language: person.get('language'), + email: { + address: person.get('email').get('address'), + primary: person.get('email').get('primary') === 'Y', + parent: person.get('email').get('parent') === 'Y', + reachable: false, + validated: person.get('email').get('validated') === 'Y', + id: Math.floor(Math.random(10000000000)*10000000000) + }, + region: person.get('region'), + timezone: { + name: person.get('tz_name'), + offset: (moment.tz(person.get('tz_name')).utcOffset() * 60) + }, + mii: { + name: person.get('mii').get('name'), + primary: person.get('mii').get('name') === 'Y', + data: person.get('mii').get('data'), + id: Math.floor(Math.random(10000000000)*10000000000), + hash: crypto.createHash('md5').update(person.get('mii').get('data')).digest('hex'), + image_url: 'https://mii-secure.account.nintendo.net/2rtgf01lztoqo_standard.tga', + image_id: Math.floor(Math.random(10000000000)*10000000000) + }, + flags: { + active: true, + marketing: person.get('marketing_flag') === 'Y', + off_device: person.get('off_device_flag') === 'Y' + }, + validation: { + email_code: 1, // will be overwritten before saving + email_token: '' // will be overwritten before saving + } + }; + + const newUser = new PNID(document); + + newUser.save() + .catch(error => { + console.log(error); + + response.status(400); + + return response.send(xmlbuilder.create({ + error: { + cause: 'Bad Request', + code: '1600', + message: 'Unable to process request' + } + }).end()); + }) + .then(async newUser => { + await mailer.send( + newUser.get('email'), + '[Prentendo Network] Please confirm your e-mail address', + `Hello, + + Your Prentendo Network ID activation is almost complete. Please click the link below to confirm your e-mail address and complete the activation process. + + https://account.pretendo.cc/account/email-confirmation?token=` + newUser.get('identification.email_token') + ` + + If you are unable to connect to the above URL, please enter the following confirmation code on the device to which your Prentendo Network ID is linked. + + <<Confirmation code: ` + newUser.get('identification.email_code') + '>>' + ); + + delete request.session.registration_status; + + response.send(xmlbuilder.create({ + person: { + pid: newUser.get('pid') + } + }).end()); + }); +}); + +/** + * [GET] + * Replacement for: https://account.nintendo.net/v1/api/people/@me/profile + * Description: Gets a users profile + */ +router.get('/@me/profile', clientHeaderCheck, async (request, response) => { + response.set('Content-Type', 'text/xml'); + response.set('Server', 'Nintendo 3DS (http)'); + response.set('X-Nintendo-Date', new Date().getTime()); + + + const { pnid } = request; + + const person = await database.getUserProfileJSONByPID(pnid.get('pid')); + + response.send(xmlbuilder.create({ + person + }).end()); + + + //response.send(`84955611ctr_initial_device_account_idUSER19583364384955489environmentUSERPRODESHOP.NINTENDO.NETINTERNAL327329101Y1998-09-22US2017-12-29T04:11:242019-11-24T15:26:06persistent_id800000432019-11-24T15:26:06transferable_id_base1200000444b6221d2019-11-24T15:26:06transferable_id_base_common1180000444b6221d2019-11-24T15:26:06uuid_account42ef6c46-0ea3-11ea-97fe-010144b6221d2019-11-24T15:26:06uuid_common3d9b06d8-0ea3-11ea-97fe-010144b6221dMen2019-06-02T04:17:56YY1750087940
halolink44@gmail.com
50463196NYYDEFAULTINTERNAL WSY2017-12-29T04:12:32
COMPLETEDAwBzMOlVognnx0GCk6r2p0D0B2n+cgAA0lJSAGUAZABEAHUAYwBrAHMAAAAAAGQrAAAWAQJoRBgmNEYUgRIXaI0AiiWBSUhQUgBlAGQARAB1AGMAawBzAHMAAAAAAJZY1151699634u2jg043u028xhttps://mii-secure.account.nintendo.net/u2jg043u028x_standard.tga1319591505https://mii-secure.account.nintendo.net/u2jg043u028x_standard.tgastandardRedDucksY822870016America/New_YorkRedDuckss-18000
`); + +}); + +/** + * [POST] + * Replacement for: https://account.nintendo.net/v1/api/people/@me/devices + * Description: Gets user profile, seems to be the same as https://account.nintendo.net/v1/api/people/@me/profile + */ +router.post('/@me/devices', clientHeaderCheck, async (request, response) => { + response.set('Content-Type', 'text/xml'); + response.set('Server', 'Nintendo 3DS (http)'); + response.set('X-Nintendo-Date', new Date().getTime()); + + // We don't care about the device attributes + // The console ignores them and PNIDs are not tied to consoles anyway + // So the server also ignores them and does not save the ones posted here + + const { pnid } = request; + + const person = await database.getUserProfileJSONByPID(pnid.get('pid')); + + response.send(xmlbuilder.create({ + person + }).end()); +}); + +/** + * [GET] + * Replacement for: https://account.nintendo.net/v1/api/people/@me/devices + * Description: Returns only user devices + */ +router.get('/@me/devices', clientHeaderCheck, async (request, response) => { + response.set('Content-Type', 'text/xml'); + response.set('Server', 'Nintendo 3DS (http)'); + response.set('X-Nintendo-Date', new Date().getTime()); + + const { pnid, headers } = request; + + response.send(xmlbuilder.create({ + devices: [ + { + device: { + device_id: headers['x-nintendo-device-id'], + language: headers['accept-language'], + updated: moment().format('YYYY-MM-DDTHH:MM:SS'), + pid: pnid.get('pid'), + platform_id: headers['x-nintendo-platform-id'], + region: headers['x-nintendo-region'], + serial_number: headers['x-nintendo-serial-number'], + status: 'ACTIVE', + system_version: headers['x-nintendo-system-version'], + type: 'RETAIL', + updated_by: 'USER' + } + } + ] + }).end()); +}); + +/** + * [GET] + * Replacement for: https://account.nintendo.net/v1/api/people/@me/devices/owner + * Description: Gets user profile, seems to be the same as https://account.nintendo.net/v1/api/people/@me/profile + */ +router.get('/@me/devices/owner', clientHeaderCheck, async (request, response) => { + response.set('Content-Type', 'text/xml'); + response.set('Server', 'Nintendo 3DS (http)'); + response.set('X-Nintendo-Date', moment().add(5, 'h')); + + const { pnid } = request; + + const person = await database.getUserProfileJSONByPID(pnid.get('pid')); + + response.send(xmlbuilder.create({ + person + }).end()); +}); + + +/** + * [PUT] + * Replacement for: https://account.nintendo.net/v1/api/people/@me/miis/@primary + * Description: Updates a users Mii + */ +router.put('/@me/miis/@primary', clientHeaderCheck, async (request, response) => { + const { pnid } = request; + + const mii = request.body.get('mii'); + + const [name, primary, data] = [mii.get('name'), mii.get('primary'), mii.get('data')] + + await pnid.updateMii({name, primary, data}); + + response.send(''); +}); + module.exports = router; \ No newline at end of file diff --git a/src/services/account/routes/provider.js b/src/services/wiiu/routes/provider.js similarity index 93% rename from src/services/account/routes/provider.js rename to src/services/wiiu/routes/provider.js index 5f902cb..c55eb5d 100644 --- a/src/services/account/routes/provider.js +++ b/src/services/wiiu/routes/provider.js @@ -1,149 +1,151 @@ -const router = require('express').Router(); -const xmlbuilder = require('xmlbuilder'); -const fs = require('fs-extra'); -const util = require('../../../util'); -const servers = require('../../../servers.json'); - -/** - * [GET] - * Replacement for: https://account.nintendo.net/v1/api/provider/service_token/@me - * Description: Gets a service token - */ -router.get('/service_token/@me', async (request, response) => { - const { pnid } = request; - - const titleId = request.headers['x-nintendo-title-id']; - const server = servers.find(({ title_ids }) => title_ids.includes(titleId)); - - if (!server) { - return response.send(xmlbuilder.create({ - errors: { - error: { - code: '1021', - message: 'The requested game server was not found' - } - } - }).end()); - } - - const { name, system } = server; - - const cryptoPath = `${__dirname}/../../../../certs/nex/${name}`; - - if (!fs.pathExistsSync(cryptoPath)) { - // Need to generate keys - return response.send(xmlbuilder.create({ - errors: { - error: { - code: '1021', - message: 'The requested game server was not found' - } - } - }).end()); - } - - const publicKey = fs.readFileSync(`${cryptoPath}/public.pem`); - const hmacSecret = fs.readFileSync(`${cryptoPath}/secret.key`); - - const cryptoOptions = { - public_key: publicKey, - hmac_secret: hmacSecret - }; - - const tokenOptions = { - system_type: system, - token_type: 0x4, // service token, - pid: pnid.get('pid'), - title_id: BigInt(parseInt(titleId, 16)), - date: BigInt(Date.now()) - }; - - const serviceToken = util.generateToken(cryptoOptions, tokenOptions); - - response.send(xmlbuilder.create({ - service_token: { - token: serviceToken - } - }).end()); -}); - -/** - * [GET] - * Replacement for: https://account.nintendo.net/v1/api/provider/nex_token/@me - * Description: Gets a NEX server address and token - */ -router.get('/nex_token/@me', async (request, response) => { - const { game_server_id: gameServerID } = request.query; - const { pnid } = request; - - if (!gameServerID) { - return response.send(xmlbuilder.create({ - errors: { - error: { - code: '0118', - message: 'Unique ID and Game Server ID are not linked' - } - } - }).end()); - } - - const server = servers.find(({ server_id }) => server_id === gameServerID); - - if (!server) { - return response.send(xmlbuilder.create({ - errors: { - error: { - code: '1021', - message: 'The requested game server was not found' - } - } - }).end()); - } - - const { name, ip, port, system } = server; - const titleId = request.headers['x-nintendo-title-id']; - - const cryptoPath = `${__dirname}/../../../../certs/nex/${name}`; - - if (!fs.pathExistsSync(cryptoPath)) { - // Need to generate keys - return response.send(xmlbuilder.create({ - errors: { - error: { - code: '1021', - message: 'The requested game server was not found' - } - } - }).end()); - } - - const publicKey = fs.readFileSync(`${cryptoPath}/public.pem`); - const hmacSecret = fs.readFileSync(`${cryptoPath}/secret.key`); - - const cryptoOptions = { - public_key: publicKey, - hmac_secret: hmacSecret - }; - - const tokenOptions = { - system_type: system, - token_type: 0x3, // nex token, - pid: pnid.get('pid'), - title_id: BigInt(parseInt(titleId, 16)), - date: BigInt(Date.now()) - }; - - const nexToken = util.generateToken(cryptoOptions, tokenOptions); - - response.send(xmlbuilder.create({ - nex_token: { - host: ip, - nex_password: pnid.get('nex.password'), - pid: pnid.get('pid'), - port: port, - token: nexToken - } - }).end()); -}); - +const router = require('express').Router(); +const xmlbuilder = require('xmlbuilder'); +const fs = require('fs-extra'); +const util = require('../../../util'); +const servers = require('../../../servers.json'); + +/** + * [GET] + * Replacement for: https://account.nintendo.net/v1/api/provider/service_token/@me + * Description: Gets a service token + */ +router.get('/service_token/@me', async (request, response) => { + const { pnid } = request; + + const titleId = request.headers['x-nintendo-title-id']; + const server = servers.find(({ title_ids }) => title_ids.includes(titleId)); + + console.log(titleId); + + if (!server) { + return response.send(xmlbuilder.create({ + errors: { + error: { + code: '1021', + message: 'The requested game server was not found' + } + } + }).end()); + } + + const { name, system } = server; + + const cryptoPath = `${__dirname}/../../../../certs/service/${name}`; + + if (!fs.pathExistsSync(cryptoPath)) { + // Need to generate keys + return response.send(xmlbuilder.create({ + errors: { + error: { + code: '1021', + message: 'The requested game server was not found' + } + } + }).end()); + } + + const publicKey = fs.readFileSync(`${cryptoPath}/public.pem`); + const hmacSecret = fs.readFileSync(`${cryptoPath}/secret.key`); + + const cryptoOptions = { + public_key: publicKey, + hmac_secret: hmacSecret + }; + + const tokenOptions = { + system_type: system, + token_type: 0x4, // service token, + pid: pnid.get('pid'), + title_id: BigInt(parseInt(titleId, 16)), + date: BigInt(Date.now()) + }; + + const serviceToken = util.generateToken(cryptoOptions, tokenOptions); + + response.send(xmlbuilder.create({ + service_token: { + token: serviceToken + } + }).end()); +}); + +/** + * [GET] + * Replacement for: https://account.nintendo.net/v1/api/provider/nex_token/@me + * Description: Gets a NEX server address and token + */ +router.get('/nex_token/@me', async (request, response) => { + const { game_server_id: gameServerID } = request.query; + const { pnid } = request; + + if (!gameServerID) { + return response.send(xmlbuilder.create({ + errors: { + error: { + code: '0118', + message: 'Unique ID and Game Server ID are not linked' + } + } + }).end()); + } + + const server = servers.find(({ server_id }) => server_id === gameServerID); + + if (!server) { + return response.send(xmlbuilder.create({ + errors: { + error: { + code: '1021', + message: 'The requested game server was not found' + } + } + }).end()); + } + + const { name, ip, port, system } = server; + const titleId = request.headers['x-nintendo-title-id']; + + const cryptoPath = `${__dirname}/../../../../certs/nex/${name}`; + + if (!fs.pathExistsSync(cryptoPath)) { + // Need to generate keys + return response.send(xmlbuilder.create({ + errors: { + error: { + code: '1021', + message: 'The requested game server was not found' + } + } + }).end()); + } + + const publicKey = fs.readFileSync(`${cryptoPath}/public.pem`); + const hmacSecret = fs.readFileSync(`${cryptoPath}/secret.key`); + + const cryptoOptions = { + public_key: publicKey, + hmac_secret: hmacSecret + }; + + const tokenOptions = { + system_type: system, + token_type: 0x3, // nex token, + pid: pnid.get('pid'), + title_id: BigInt(parseInt(titleId, 16)), + date: BigInt(Date.now()) + }; + + const nexToken = util.generateToken(cryptoOptions, tokenOptions); + + response.send(xmlbuilder.create({ + nex_token: { + host: ip, + nex_password: pnid.get('nex.password'), + pid: pnid.get('pid'), + port: port, + token: nexToken + } + }).end()); +}); + module.exports = router; \ No newline at end of file diff --git a/src/services/account/routes/support.js b/src/services/wiiu/routes/support.js similarity index 95% rename from src/services/account/routes/support.js rename to src/services/wiiu/routes/support.js index 069824d..322d3ba 100644 --- a/src/services/account/routes/support.js +++ b/src/services/wiiu/routes/support.js @@ -1,55 +1,55 @@ -const router = require('express').Router(); -const dns = require('dns'); -const xmlbuilder = require('xmlbuilder'); -const clientHeaderCheck = require('../../../middleware/client-header'); - -router.post('/validate/email', clientHeaderCheck, async (request, response) => { - // Status should be 2 from previous request in registration process - if (request.session.registration_status !== 2) { - response.status(400); - - return response.send(xmlbuilder.create({ - error: { - cause: 'Bad Request', - code: '1600', - message: 'Unable to process request' - } - }).end()); - } - - const { email } = request.body; - - if (!email) { - return response.send(xmlbuilder.create({ - errors: { - error: { - cause: 'email', - code: '0103', - message: 'Email format is invalid' - } - } - }).end()); - } - - const domain = email.split('@')[1]; - - dns.resolveMx(domain, (error) => { - if (error) { - return response.send(xmlbuilder.create({ - errors: { - error: { - code: '1126', - message: 'The domain "' + domain + '" is not accessible.' - } - } - }).end()); - } - - request.session.registration_status = 3; - - response.status(200); - response.end(); - }); -}); - +const router = require('express').Router(); +const dns = require('dns'); +const xmlbuilder = require('xmlbuilder'); +const clientHeaderCheck = require('../../../middleware/client-header'); + +router.post('/validate/email', clientHeaderCheck, async (request, response) => { + // Status should be 2 from previous request in registration process + if (request.session.registration_status !== 2) { + response.status(400); + + return response.send(xmlbuilder.create({ + error: { + cause: 'Bad Request', + code: '1600', + message: 'Unable to process request' + } + }).end()); + } + + const { email } = request.body; + + if (!email) { + return response.send(xmlbuilder.create({ + errors: { + error: { + cause: 'email', + code: '0103', + message: 'Email format is invalid' + } + } + }).end()); + } + + const domain = email.split('@')[1]; + + dns.resolveMx(domain, (error) => { + if (error) { + return response.send(xmlbuilder.create({ + errors: { + error: { + code: '1126', + message: 'The domain "' + domain + '" is not accessible.' + } + } + }).end()); + } + + request.session.registration_status = 3; + + response.status(200); + response.end(); + }); +}); + module.exports = router; \ No newline at end of file diff --git a/src/services/account/timezones.json b/src/services/wiiu/timezones.json similarity index 100% rename from src/services/account/timezones.json rename to src/services/wiiu/timezones.json diff --git a/src/util.js b/src/util.js index 2e9596c..f3128b5 100644 --- a/src/util.js +++ b/src/util.js @@ -1,172 +1,182 @@ -const crypto = require('crypto'); -const NodeRSA = require('node-rsa'); -const fs = require('fs-extra'); - -function nintendoPasswordHash(password, pid) { - const pidBuffer = Buffer.alloc(4); - pidBuffer.writeUInt32LE(pid); - - const unpacked = Buffer.concat([ - pidBuffer, - Buffer.from('\x02\x65\x43\x46'), - Buffer.from(password) - ]); - const hashed = crypto.createHash('sha256').update(unpacked).digest().toString('hex'); - - return hashed; -} - -function generateRandomInt(length = 4) { - return Math.floor(Math.pow(10, length-1) + Math.random() * 9 * Math.pow(10, length-1)); -} - -function generateToken(cryptoOptions, tokenOptions) { - - // Access and refresh tokens use a different format since they must be much smaller - if ([0x1, 0x2].includes(tokenOptions.token_type)) { - const cryptoPath = `${__dirname}/../certs/access`; - const aesKey = Buffer.from(fs.readFileSync(`${cryptoPath}/aes.key`, { encoding: 'utf8' }), 'hex'); - const dataBuffer = Buffer.alloc(4 + 8); - - dataBuffer.writeUInt32LE(tokenOptions.pid, 0x0); - dataBuffer.writeBigUInt64LE(tokenOptions.date, 0x4); - - const iv = Buffer.alloc(16); - const cipher = crypto.createCipheriv('aes-128-cbc', aesKey, iv); - - let encryptedBody = cipher.update(dataBuffer); - encryptedBody = Buffer.concat([encryptedBody, cipher.final()]); - - return encryptedBody.toString('base64'); - } - - const publicKey = new NodeRSA(cryptoOptions.public_key, 'pkcs8-public-pem', { - environment: 'browser', - encryptionScheme: { - 'hash': 'sha256', - } - }); - - // Create the buffer containing the token data - const dataBuffer = Buffer.alloc(1 + 1 + 4 + 8 + 8); - - dataBuffer.writeUInt8(tokenOptions.system_type, 0x0); - dataBuffer.writeUInt8(tokenOptions.token_type, 0x1); - dataBuffer.writeUInt32LE(tokenOptions.pid, 0x2); - dataBuffer.writeBigUInt64LE(tokenOptions.title_id, 0x6); - dataBuffer.writeBigUInt64LE(tokenOptions.date, 0xE); - - // Calculate the signature of the token body - const hmac = crypto.createHmac('sha1', cryptoOptions.hmac_secret).update(dataBuffer); - const signature = hmac.digest(); - - // Generate random AES key and IV - const key = crypto.randomBytes(16); - const iv = crypto.randomBytes(16); - - // Encrypt the token body with AES - const cipher = crypto.createCipheriv('aes-128-cbc', key, iv); - - let encryptedBody = cipher.update(dataBuffer); - encryptedBody = Buffer.concat([encryptedBody, cipher.final()]); - - // Encrypt the AES key with RSA public key - const encryptedKey = publicKey.encrypt(key); - - // Create crypto config token section - const cryptoConfig = Buffer.concat([ - encryptedKey, - iv - ]); - - // Build the token - const token = Buffer.concat([ - cryptoConfig, - signature, - encryptedBody - ]); - - return token.toString('base64'); // Encode to base64 for transport -} - -function decryptToken(token) { - - // Access and refresh tokens use a different format since they must be much smaller - // Assume a small length means access or refresh token - if (token.length <= 32) { - const cryptoPath = `${__dirname}/../certs/access`; - const aesKey = Buffer.from(fs.readFileSync(`${cryptoPath}/aes.key`, { encoding: 'utf8' }), 'hex'); - - const iv = Buffer.alloc(16); - - const decipher = crypto.createDecipheriv('aes-128-cbc', aesKey, iv); - - let decryptedBody = decipher.update(token); - decryptedBody = Buffer.concat([decryptedBody, decipher.final()]); - - return decryptedBody; - } - - const cryptoPath = `${__dirname}/../certs/access`; - - const cryptoOptions = { - private_key: fs.readFileSync(`${cryptoPath}/private.pem`), - hmac_secret: fs.readFileSync(`${cryptoPath}/secret.key`) - }; - - const privateKey = new NodeRSA(cryptoOptions.private_key, 'pkcs1-private-pem', { - environment: 'browser', - encryptionScheme: { - 'hash': 'sha256', - } - }); - - const cryptoConfig = token.subarray(0, 0x90); - const signature = token.subarray(0x90, 0xA4); - const encryptedBody = token.subarray(0xA4); - - const encryptedAESKey = cryptoConfig.subarray(0, 128); - const iv = cryptoConfig.subarray(128); - - const decryptedAESKey = privateKey.decrypt(encryptedAESKey); - - const decipher = crypto.createDecipheriv('aes-128-cbc', decryptedAESKey, iv); - - let decryptedBody = decipher.update(encryptedBody); - decryptedBody = Buffer.concat([decryptedBody, decipher.final()]); - - const hmac = crypto.createHmac('sha1', cryptoOptions.hmac_secret).update(decryptedBody); - const calculatedSignature = hmac.digest(); - - if (calculatedSignature !== signature) { - console.log('Token signature did not match'); - return null; - } - - return decryptedBody; -} - -function unpackToken(token) { - if (token.length <= 32) { - return { - pid: token.readUInt32LE(0x0), - date: token.readBigUInt64LE(0x2) - }; - } - - return { - system_type: token.readUInt8(0x0), - token_type: token.readUInt8(0x1), - pid: token.readUInt32LE(0x2), - title_id: token.readBigUInt64LE(0x6), - date: token.readBigUInt64LE(0xE) - }; -} - -module.exports = { - nintendoPasswordHash, - generateRandomInt, - generateToken, - decryptToken, - unpackToken +const crypto = require('crypto'); +const NodeRSA = require('node-rsa'); +const fs = require('fs-extra'); +const url = require('url'); + +function nintendoPasswordHash(password, pid) { + const pidBuffer = Buffer.alloc(4); + pidBuffer.writeUInt32LE(pid); + + const unpacked = Buffer.concat([ + pidBuffer, + Buffer.from('\x02\x65\x43\x46'), + Buffer.from(password) + ]); + const hashed = crypto.createHash('sha256').update(unpacked).digest().toString('hex'); + + return hashed; +} + +function generateRandomInt(length = 4) { + return Math.floor(Math.pow(10, length-1) + Math.random() * 9 * Math.pow(10, length-1)); +} + +function generateToken(cryptoOptions, tokenOptions) { + + // Access and refresh tokens use a different format since they must be much smaller + if ([0x1, 0x2].includes(tokenOptions.token_type)) { + const cryptoPath = `${__dirname}/../certs/access`; + const aesKey = Buffer.from(fs.readFileSync(`${cryptoPath}/aes.key`, { encoding: 'utf8' }), 'hex'); + const dataBuffer = Buffer.alloc(4 + 8); + + dataBuffer.writeUInt32LE(tokenOptions.pid, 0x0); + dataBuffer.writeBigUInt64LE(tokenOptions.date, 0x4); + + const iv = Buffer.alloc(16); + const cipher = crypto.createCipheriv('aes-128-cbc', aesKey, iv); + + let encryptedBody = cipher.update(dataBuffer); + encryptedBody = Buffer.concat([encryptedBody, cipher.final()]); + + return encryptedBody.toString('base64'); + } + + const publicKey = new NodeRSA(cryptoOptions.public_key, 'pkcs8-public-pem', { + environment: 'browser', + encryptionScheme: { + 'hash': 'sha256', + } + }); + + // Create the buffer containing the token data + const dataBuffer = Buffer.alloc(1 + 1 + 4 + 8 + 8); + + dataBuffer.writeUInt8(tokenOptions.system_type, 0x0); + dataBuffer.writeUInt8(tokenOptions.token_type, 0x1); + dataBuffer.writeUInt32LE(tokenOptions.pid, 0x2); + dataBuffer.writeBigUInt64LE(tokenOptions.title_id, 0x6); + dataBuffer.writeBigUInt64LE(tokenOptions.date, 0xE); + + // Calculate the signature of the token body + const hmac = crypto.createHmac('sha1', cryptoOptions.hmac_secret).update(dataBuffer); + const signature = hmac.digest(); + + // Generate random AES key and IV + const key = crypto.randomBytes(16); + const iv = crypto.randomBytes(16); + + // Encrypt the token body with AES + const cipher = crypto.createCipheriv('aes-128-cbc', key, iv); + + let encryptedBody = cipher.update(dataBuffer); + encryptedBody = Buffer.concat([encryptedBody, cipher.final()]); + + // Encrypt the AES key with RSA public key + const encryptedKey = publicKey.encrypt(key); + + // Create crypto config token section + const cryptoConfig = Buffer.concat([ + encryptedKey, + iv + ]); + + // Build the token + const token = Buffer.concat([ + cryptoConfig, + signature, + encryptedBody + ]); + + return token.toString('base64'); // Encode to base64 for transport +} + +function decryptToken(token) { + + // Access and refresh tokens use a different format since they must be much smaller + // Assume a small length means access or refresh token + if (token.length <= 32) { + const cryptoPath = `${__dirname}/../certs/access`; + const aesKey = Buffer.from(fs.readFileSync(`${cryptoPath}/aes.key`, { encoding: 'utf8' }), 'hex'); + + const iv = Buffer.alloc(16); + + const decipher = crypto.createDecipheriv('aes-128-cbc', aesKey, iv); + + let decryptedBody = decipher.update(token); + decryptedBody = Buffer.concat([decryptedBody, decipher.final()]); + + return decryptedBody; + } + + const cryptoPath = `${__dirname}/../certs/access`; + + const cryptoOptions = { + private_key: fs.readFileSync(`${cryptoPath}/private.pem`), + hmac_secret: fs.readFileSync(`${cryptoPath}/secret.key`) + }; + + const privateKey = new NodeRSA(cryptoOptions.private_key, 'pkcs1-private-pem', { + environment: 'browser', + encryptionScheme: { + 'hash': 'sha256', + } + }); + + const cryptoConfig = token.subarray(0, 0x90); + const signature = token.subarray(0x90, 0xA4); + const encryptedBody = token.subarray(0xA4); + + const encryptedAESKey = cryptoConfig.subarray(0, 128); + const iv = cryptoConfig.subarray(128); + + const decryptedAESKey = privateKey.decrypt(encryptedAESKey); + + const decipher = crypto.createDecipheriv('aes-128-cbc', decryptedAESKey, iv); + + let decryptedBody = decipher.update(encryptedBody); + decryptedBody = Buffer.concat([decryptedBody, decipher.final()]); + + const hmac = crypto.createHmac('sha1', cryptoOptions.hmac_secret).update(decryptedBody); + const calculatedSignature = hmac.digest(); + + if (calculatedSignature !== signature) { + console.log('Token signature did not match'); + return null; + } + + return decryptedBody; +} + +function unpackToken(token) { + if (token.length <= 32) { + return { + pid: token.readUInt32LE(0x0), + date: token.readBigUInt64LE(0x2) + }; + } + + return { + system_type: token.readUInt8(0x0), + token_type: token.readUInt8(0x1), + pid: token.readUInt32LE(0x2), + title_id: token.readBigUInt64LE(0x6), + date: token.readBigUInt64LE(0xE) + }; +} + +function fullUrl(request) { + return url.format({ + protocol: request.protocol, + host: request.get('host'), + pathname: request.originalUrl + }); +} + +module.exports = { + nintendoPasswordHash, + generateRandomInt, + generateToken, + decryptToken, + unpackToken, + fullUrl }; \ No newline at end of file