mirror of
https://github.com/PretendoNetwork/account.git
synced 2024-05-17 04:11:03 -04:00
Large restructure
Restructured the codebase a bit. No big changes really. Need to handle registration sessions better
This commit is contained in:
parent
f447d44921
commit
18b96b06c5
128
.gitignore
vendored
128
.gitignore
vendored
|
@ -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
|
|
@ -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);
|
174
generate-keys.js
174
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');
|
||||
}
|
102
logger.js
102
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
|
||||
};
|
150
package-lock.json
generated
150
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
338
src/database.js
338
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,
|
||||
};
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
|
@ -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
|
||||
};
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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
|
||||
};
|
|
@ -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
|
||||
};
|
131
src/server.js
131
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}`);
|
||||
});
|
||||
});
|
22
src/services/3ds/index.js
Normal file
22
src/services/3ds/index.js
Normal file
|
@ -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;
|
0
src/services/3ds/routes/account.js
Normal file
0
src/services/3ds/routes/account.js
Normal file
3
src/services/3ds/routes/index.js
Normal file
3
src/services/3ds/routes/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
ACCOUNT: require('./account')
|
||||
};
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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'),
|
||||
};
|
95
src/services/wiiu/routes/miis.js
Normal file
95
src/services/wiiu/routes/miis.js
Normal file
|
@ -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;
|
|
@ -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;
|
|
@ -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(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><person><accounts><account><attributes><attribute><id>84955611</id><name>ctr_initial_device_account_id</name><updated_by>USER</updated_by><value>195833643</value></attribute><attribute><id>84955489</id><name>environment</name><updated_by>USER</updated_by><value>PROD</value></attribute></attributes><domain>ESHOP.NINTENDO.NET</domain><type>INTERNAL</type><username>327329101</username></account></accounts><active_flag>Y</active_flag><birth_date>1998-09-22</birth_date><country>US</country><create_date>2017-12-29T04:11:24</create_date><device_attributes><device_attribute><created_date>2019-11-24T15:26:06</created_date><name>persistent_id</name><value>80000043</value></device_attribute><device_attribute><created_date>2019-11-24T15:26:06</created_date><name>transferable_id_base</name><value>1200000444b6221d</value></device_attribute><device_attribute><created_date>2019-11-24T15:26:06</created_date><name>transferable_id_base_common</name><value>1180000444b6221d</value></device_attribute><device_attribute><created_date>2019-11-24T15:26:06</created_date><name>uuid_account</name><value>42ef6c46-0ea3-11ea-97fe-010144b6221d</value></device_attribute><device_attribute><created_date>2019-11-24T15:26:06</created_date><name>uuid_common</name><value>3d9b06d8-0ea3-11ea-97fe-010144b6221d</value></device_attribute></device_attributes><gender>M</gender><language>en</language><updated>2019-06-02T04:17:56</updated><marketing_flag>Y</marketing_flag><off_device_flag>Y</off_device_flag><pid>1750087940</pid><email><address>halolink44@gmail.com</address><id>50463196</id><parent>N</parent><primary>Y</primary><reachable>Y</reachable><type>DEFAULT</type><updated_by>INTERNAL WS</updated_by><validated>Y</validated><validated_date>2017-12-29T04:12:32</validated_date></email><mii><status>COMPLETED</status><data>AwBzMOlVognnx0GCk6r2p0D0B2n+cgAA0lJSAGUAZABEAHUAYwBrAHMAAAAAAGQrAAAWAQJoRBgmNEYUgRIXaI0AiiWBSUhQUgBlAGQARAB1AGMAawBzAHMAAAAAAJZY</data><id>1151699634</id><mii_hash>u2jg043u028x</mii_hash><mii_images><mii_image><cached_url>https://mii-secure.account.nintendo.net/u2jg043u028x_standard.tga</cached_url><id>1319591505</id><url>https://mii-secure.account.nintendo.net/u2jg043u028x_standard.tga</url><type>standard</type></mii_image></mii_images><name>RedDucks</name><primary>Y</primary></mii><region>822870016</region><tz_name>America/New_York</tz_name><user_id>RedDuckss</user_id><utc_offset>-18000</utc_offset></person>`);
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* [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(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><person><accounts><account><attributes><attribute><id>84955611</id><name>ctr_initial_device_account_id</name><updated_by>USER</updated_by><value>195833643</value></attribute><attribute><id>84955489</id><name>environment</name><updated_by>USER</updated_by><value>PROD</value></attribute></attributes><domain>ESHOP.NINTENDO.NET</domain><type>INTERNAL</type><username>327329101</username></account></accounts><active_flag>Y</active_flag><birth_date>1998-09-22</birth_date><country>US</country><create_date>2017-12-29T04:11:24</create_date><device_attributes><device_attribute><created_date>2019-11-24T15:26:06</created_date><name>persistent_id</name><value>80000043</value></device_attribute><device_attribute><created_date>2019-11-24T15:26:06</created_date><name>transferable_id_base</name><value>1200000444b6221d</value></device_attribute><device_attribute><created_date>2019-11-24T15:26:06</created_date><name>transferable_id_base_common</name><value>1180000444b6221d</value></device_attribute><device_attribute><created_date>2019-11-24T15:26:06</created_date><name>uuid_account</name><value>42ef6c46-0ea3-11ea-97fe-010144b6221d</value></device_attribute><device_attribute><created_date>2019-11-24T15:26:06</created_date><name>uuid_common</name><value>3d9b06d8-0ea3-11ea-97fe-010144b6221d</value></device_attribute></device_attributes><gender>M</gender><language>en</language><updated>2019-06-02T04:17:56</updated><marketing_flag>Y</marketing_flag><off_device_flag>Y</off_device_flag><pid>1750087940</pid><email><address>halolink44@gmail.com</address><id>50463196</id><parent>N</parent><primary>Y</primary><reachable>Y</reachable><type>DEFAULT</type><updated_by>INTERNAL WS</updated_by><validated>Y</validated><validated_date>2017-12-29T04:12:32</validated_date></email><mii><status>COMPLETED</status><data>AwBzMOlVognnx0GCk6r2p0D0B2n+cgAA0lJSAGUAZABEAHUAYwBrAHMAAAAAAGQrAAAWAQJoRBgmNEYUgRIXaI0AiiWBSUhQUgBlAGQARAB1AGMAawBzAHMAAAAAAJZY</data><id>1151699634</id><mii_hash>u2jg043u028x</mii_hash><mii_images><mii_image><cached_url>https://mii-secure.account.nintendo.net/u2jg043u028x_standard.tga</cached_url><id>1319591505</id><url>https://mii-secure.account.nintendo.net/u2jg043u028x_standard.tga</url><type>standard</type></mii_image></mii_images><name>RedDucks</name><primary>Y</primary></mii><region>822870016</region><tz_name>America/New_York</tz_name><user_id>RedDuckss</user_id><utc_offset>-18000</utc_offset></person>`);
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* [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;
|
|
@ -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;
|
|
@ -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;
|
352
src/util.js
352
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
|
||||
};
|
Loading…
Reference in a new issue