mirror of
https://github.com/PretendoNetwork/account.git
synced 2026-08-27 05:14:40 -05:00
Merge branch 'dev'
This commit is contained in:
131
.gitignore
vendored
131
.gitignore
vendored
@@ -1,65 +1,66 @@
|
||||
# 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
|
||||
# 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
|
||||
/cdn
|
||||
@@ -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 database = require('./src/database');
|
||||
const { PNID } = require('./src/models/pnid');
|
||||
|
||||
prompt.message = '';
|
||||
|
||||
const properties = [
|
||||
'username',
|
||||
'email',
|
||||
{
|
||||
name: 'password',
|
||||
hidden: true
|
||||
}
|
||||
];
|
||||
|
||||
prompt.get(properties, (error, { username, email, password }) => {
|
||||
const date = new Date().toISOString();
|
||||
// Sample Mii data
|
||||
const miiData = 'AwAAQOlVognnx0GC2X0LLQOzuI0n2QAAAUBiAGUAbABsAGEAAABFAAAAAAAAAEBAEgCBAQRoQxggNEYUgRIXaA0AACkDUkhQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP6G';
|
||||
|
||||
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: crypto.randomBytes(4).readUInt32LE()
|
||||
},
|
||||
region: 0x310B0000,
|
||||
timezone: {
|
||||
name: 'America/New_York',
|
||||
offset: -14400
|
||||
},
|
||||
mii: {
|
||||
name: 'bella',
|
||||
primary: true,
|
||||
data: miiData,
|
||||
id: crypto.randomBytes(4).readUInt32LE(),
|
||||
hash: crypto.randomBytes(7).toString('hex'),
|
||||
image_url: '', // Deprecated, will be removed
|
||||
image_id: crypto.randomBytes(4).readUInt32LE()
|
||||
},
|
||||
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: 1,
|
||||
email_token: ''
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
};
|
||||
4143
package-lock.json
generated
4143
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@@ -2,7 +2,7 @@
|
||||
"name": "account",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "./src/server",
|
||||
"main": "./src/server.js",
|
||||
"scripts": {
|
||||
"lint": "./node_modules/.bin/eslint .",
|
||||
"start": "node .",
|
||||
@@ -20,21 +20,26 @@
|
||||
},
|
||||
"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",
|
||||
"got": "^11.8.2",
|
||||
"image-pixels": "^2.2.2",
|
||||
"kaitai-struct": "^0.9.0",
|
||||
"moment": "^2.24.0",
|
||||
"moment-timezone": "^0.5.27",
|
||||
"mongoose": "^5.8.3",
|
||||
"mongoose-unique-validator": "^2.0.3",
|
||||
"morgan": "^1.9.1",
|
||||
"node-rsa": "^1.0.7",
|
||||
"nodemailer": "^6.4.2",
|
||||
"prompt": "^1.0.0",
|
||||
"tga": "^1.0.4",
|
||||
"xmlbuilder": "^13.0.2",
|
||||
"xmlbuilder2": "0.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"node-rsa": "^1.0.7",
|
||||
"prompt": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
339
src/database.js
339
src/database.js
@@ -1,170 +1,171 @@
|
||||
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: 'INTERNAL WS', // 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').replace(/(\r\n|\n|\r)/gm, ''),
|
||||
id: user.get('mii.id'),
|
||||
mii_hash: user.get('mii.hash'),
|
||||
mii_images: {
|
||||
mii_image: {
|
||||
// Images MUST be loaded over HTTPS or console ignores them
|
||||
cached_url: `https://mii-images.cdn.pretendo.cc/${user.pid}/standard.tga`,
|
||||
id: user.get('mii.image_id'),
|
||||
url: `https://mii-images.cdn.pretendo.cc/${user.pid}/standard.tga`,
|
||||
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;
|
||||
214
src/mii.js
Normal file
214
src/mii.js
Normal file
@@ -0,0 +1,214 @@
|
||||
const KaitaiStream = require('kaitai-struct/KaitaiStream');
|
||||
|
||||
class Mii extends KaitaiStream {
|
||||
constructor(arrayBuffer, byteOffset) {
|
||||
super(arrayBuffer, byteOffset);
|
||||
|
||||
this.decode();
|
||||
}
|
||||
|
||||
decode() {
|
||||
// Decode raw data
|
||||
// A lot of this goes unused
|
||||
this.unknown1 = this.readU1();
|
||||
this.characterSet = this.readBitsIntBe(2);
|
||||
this.regionLock = this.readBitsIntBe(2);
|
||||
this.profanityFlag = this.readBitsIntBe(1) !== 0;
|
||||
this.copying = this.readBitsIntBe(1) !== 0;
|
||||
this.unknown2 = this.readBitsIntBe(2);
|
||||
this.slotIndex = this.readBitsIntBe(4);
|
||||
this.pageIndex = this.readBitsIntBe(4);
|
||||
this.version = this.readBitsIntBe(4);
|
||||
this.unknown3 = this.readBitsIntBe(4);
|
||||
this.systemId = Array(8).fill().map(() => this.readU1());
|
||||
this.avatarId = Array(4).fill().map(() => this.readU1());
|
||||
this.clientId = Array(6).fill().map(() => this.readU1());
|
||||
this.padding = this.readU2le();
|
||||
this.miiMetaData = this.readU2le();
|
||||
this.miiName = Buffer.from(this.readBytes(20)).toString('utf16le');
|
||||
this.height = this.readU1();
|
||||
this.build = this.readU1();
|
||||
this.faceColor = this.readBitsIntBe(3);
|
||||
this.faceType = this.readBitsIntBe(4);
|
||||
this.mingle = this.readBitsIntBe(1) !== 0;
|
||||
this.faceMakeup = this.readBitsIntBe(4);
|
||||
this.faceWrinkles = this.readBitsIntBe(4);
|
||||
this.alignToByte();
|
||||
this.hairType = this.readU1();
|
||||
this.unknown5 = this.readBitsIntBe(4);
|
||||
this.hairFlip = this.readBitsIntBe(1) !== 0;
|
||||
this.hairColor = this.readBitsIntBe(3);
|
||||
this.alignToByte();
|
||||
this.eyeData = this.readU4le();
|
||||
this.eyebrowData = this.readU4le();
|
||||
this.noseData = this.readU2le();
|
||||
this.mouthData = this.readU2le();
|
||||
this.mouthData2 = this.readU2le();
|
||||
this.facialHairData = this.readU2le();
|
||||
this.glassesData = this.readU2le();
|
||||
this.moleData = this.readU2le();
|
||||
this.creatorName = Buffer.from(this.readBytes(20)).toString('utf16le');
|
||||
this.padding2 = this.readU2le();
|
||||
this.checksum = this.readU2le();
|
||||
|
||||
// Carve out more specific data from the above values
|
||||
// TODO: read these bits directly instead of getting them later
|
||||
|
||||
this.gender = (this.miiMetaData & 1);
|
||||
this.birthMonth = ((this.miiMetaData >> 1) & 15);
|
||||
this.birthDay = ((this.miiMetaData >> 5) & 31);
|
||||
this.favoriteColor = ((this.miiMetaData >> 10) & 15);
|
||||
this.favorite = ((this.miiMetaData >> 14) & 1);
|
||||
|
||||
this.eyeType = (this.eyeData & 63);
|
||||
this.eyeColor = ((this.eyeData >> 6) & 7);
|
||||
this.eyeSize = ((this.eyeData >> 9) & 7);
|
||||
this.eyeStretch = ((this.eyeData >> 13) & 7);
|
||||
this.eyeRotation = ((this.eyeData >> 16) & 31);
|
||||
this.eyeHorizontal = ((this.eyeData >> 21) & 15);
|
||||
this.eyeVertical = ((this.eyeData >> 25) & 31);
|
||||
|
||||
this.eyebrowType = (this.eyebrowData & 31);
|
||||
this.eyebrowColor = ((this.eyebrowData >> 5) & 7);
|
||||
this.eyebrowSize = ((this.eyebrowData >> 8) & 15);
|
||||
this.eyebrowStretch = ((this.eyebrowData >> 12) & 7);
|
||||
this.eyebrowRotation = ((this.eyebrowData >> 16) & 15);
|
||||
this.eyebrowHorizontal = ((this.eyebrowData >> 21) & 15);
|
||||
this.eyebrowVertical = ((this.eyebrowData >> 25) & 31);
|
||||
|
||||
this.noseType = (this.noseData & 31);
|
||||
this.noseSize = ((this.noseData >> 5) & 15);
|
||||
this.noseVertical = ((this.noseData >> 9) & 31);
|
||||
|
||||
|
||||
this.mouthType = (this.mouthData & 63);
|
||||
this.mouthColor = ((this.mouthData >> 6) & 7);
|
||||
this.mouthSize = ((this.mouthData >> 9) & 15);
|
||||
this.mouthStretch = ((this.mouthData >> 13) & 7);
|
||||
|
||||
this.mouthVertical = (this.mouthData2 & 31);
|
||||
this.facialHairMustache = ((this.mouthData2 >> 5) & 7);
|
||||
|
||||
this.facialHairType = (this.facialHairData & 7);
|
||||
this.facialHairColor = ((this.facialHairData >> 3) & 7);
|
||||
this.facialHairSize = ((this.facialHairData >> 6) & 15);
|
||||
this.facialHairVertical = ((this.facialHairData >> 10) & 31);
|
||||
|
||||
this.glassesType = (this.glassesData & 15);
|
||||
this.glassesColor = (this.glassesData >> 4) & 7;
|
||||
this.glassesSize = (this.glassesData >> 7) & 15;
|
||||
this.glassesVertical = (this.glassesData >> 11) & 15;
|
||||
|
||||
this.moleEnable = (this.moleData >> 15);
|
||||
this.moleSize = ((this.moleData >> 1) & 15);
|
||||
this.moleHorizontal = ((this.moleData >> 5) & 31);
|
||||
this.moleVertical = ((this.moleData >> 10) & 31);
|
||||
}
|
||||
|
||||
toStudioMii() {
|
||||
/*
|
||||
Can also disable randomization with:
|
||||
|
||||
let miiStudioData = Buffer.alloc(0x2F);
|
||||
let next = 256;
|
||||
|
||||
and removing "randomizer" and the "miiStudioData.writeUInt8(randomizer);" call
|
||||
*/
|
||||
const miiStudioData = Buffer.alloc(0x2F);
|
||||
const randomizer = Math.floor(256 * Math.random());
|
||||
let next = randomizer;
|
||||
let pos = 1;
|
||||
|
||||
function encodeMiiPart(partValue) {
|
||||
const encoded = (7 + (partValue ^ next)) % 256;
|
||||
next = encoded;
|
||||
|
||||
miiStudioData.writeUInt8(encoded, pos);
|
||||
pos++;
|
||||
}
|
||||
|
||||
miiStudioData.writeUInt8(randomizer);
|
||||
|
||||
if (this.facialHairColor === 0) {
|
||||
encodeMiiPart(8);
|
||||
} else {
|
||||
encodeMiiPart(this.facialHairColor);
|
||||
}
|
||||
|
||||
encodeMiiPart(this.facialHairType);
|
||||
encodeMiiPart(this.build);
|
||||
encodeMiiPart(this.eyeStretch);
|
||||
encodeMiiPart(this.eyeColor + 8);
|
||||
encodeMiiPart(this.eyeRotation);
|
||||
encodeMiiPart(this.eyeSize);
|
||||
encodeMiiPart(this.eyeType);
|
||||
encodeMiiPart(this.eyeHorizontal);
|
||||
encodeMiiPart(this.eyeVertical);
|
||||
encodeMiiPart(this.eyebrowStretch);
|
||||
|
||||
if (this.eyebrowColor === 0) {
|
||||
encodeMiiPart(8);
|
||||
} else {
|
||||
encodeMiiPart(this.eyebrowColor);
|
||||
}
|
||||
|
||||
encodeMiiPart(this.eyebrowRotation);
|
||||
encodeMiiPart(this.eyebrowSize);
|
||||
encodeMiiPart(this.eyebrowType);
|
||||
encodeMiiPart(this.eyebrowHorizontal);
|
||||
encodeMiiPart(this.eyebrowVertical);
|
||||
encodeMiiPart(this.faceColor);
|
||||
encodeMiiPart(this.faceMakeup);
|
||||
encodeMiiPart(this.faceType);
|
||||
encodeMiiPart(this.faceWrinkles);
|
||||
encodeMiiPart(this.favoriteColor);
|
||||
encodeMiiPart(this.gender);
|
||||
|
||||
if (this.glassesColor == 0) {
|
||||
encodeMiiPart(8);
|
||||
} else if (this.glassesColor < 6) {
|
||||
encodeMiiPart(this.glassesColor + 13);
|
||||
} else {
|
||||
encodeMiiPart(0);
|
||||
}
|
||||
|
||||
encodeMiiPart(this.glassesSize);
|
||||
encodeMiiPart(this.glassesType);
|
||||
encodeMiiPart(this.glassesVertical);
|
||||
|
||||
if (this.hairColor == 0) {
|
||||
encodeMiiPart(8);
|
||||
} else {
|
||||
encodeMiiPart(this.hairColor);
|
||||
}
|
||||
|
||||
encodeMiiPart(this.hairFlip ? 1 : 0);
|
||||
encodeMiiPart(this.hairType);
|
||||
encodeMiiPart(this.height);
|
||||
encodeMiiPart(this.moleSize);
|
||||
encodeMiiPart(this.moleEnable);
|
||||
encodeMiiPart(this.moleHorizontal);
|
||||
encodeMiiPart(this.moleVertical);
|
||||
encodeMiiPart(this.mouthStretch);
|
||||
|
||||
if (this.mouthColor < 4) {
|
||||
encodeMiiPart(this.mouthColor + 19);
|
||||
} else {
|
||||
encodeMiiPart(0);
|
||||
}
|
||||
|
||||
encodeMiiPart(this.mouthSize);
|
||||
encodeMiiPart(this.mouthType);
|
||||
encodeMiiPart(this.mouthVertical);
|
||||
encodeMiiPart(this.facialHairSize);
|
||||
encodeMiiPart(this.facialHairMustache);
|
||||
encodeMiiPart(this.facialHairVertical);
|
||||
encodeMiiPart(this.noseSize);
|
||||
encodeMiiPart(this.noseType);
|
||||
encodeMiiPart(this.noseVertical);
|
||||
|
||||
return miiStudioData;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Mii;
|
||||
@@ -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,253 @@
|
||||
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 fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const imagePixels = require('image-pixels');
|
||||
const TGA = require('tga');
|
||||
const got = require('got');
|
||||
const util = require('../util');
|
||||
const { DeviceSchema } = require('./device');
|
||||
const Mii = require('../mii');
|
||||
|
||||
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);
|
||||
this.set('mii.hash', crypto.randomBytes(7).toString('hex'));
|
||||
this.set('mii.id', crypto.randomBytes(4).readUInt32LE());
|
||||
this.set('mii.image_id', crypto.randomBytes(4).readUInt32LE());
|
||||
|
||||
const studioMii = new Mii(Buffer.from(data, 'base64'));
|
||||
const converted = studioMii.toStudioMii();
|
||||
const encodedStudioMiiData = converted.toString('hex');
|
||||
const miiStudioUrl = `https://studio.mii.nintendo.com/miis/image.png?data=${encodedStudioMiiData}&type=face&width=128&instanceCount=1`;
|
||||
const miiStudioNormalFaceImageData = await got(miiStudioUrl).buffer();
|
||||
const pngData = await imagePixels(miiStudioNormalFaceImageData);
|
||||
const tga = TGA.createTgaBuffer(pngData.width, pngData.height, pngData.data);
|
||||
|
||||
const userMiiPath = path.normalize(`${__dirname}/../../cdn/${this.get('pid')}/miis`);
|
||||
fs.ensureDirSync(userMiiPath);
|
||||
fs.writeFileSync(`${userMiiPath}/standard.tga`, tga);
|
||||
fs.writeFileSync(`${userMiiPath}/normal_face.png`, miiStudioNormalFaceImageData);
|
||||
|
||||
const expressions = ['frustrated', 'smile_open_mouth', 'wink_left', 'sorrow', 'surprise_open_mouth'];
|
||||
for (const expression of expressions) {
|
||||
const miiStudioExpressionUrl = `https://studio.mii.nintendo.com/miis/image.png?data=${encodedStudioMiiData}&type=face&expression=${expression}&width=128&instanceCount=1`;
|
||||
const miiStudioExpressionImageData = await got(miiStudioExpressionUrl).buffer();
|
||||
fs.writeFileSync(`${userMiiPath}/${expression}.png`, miiStudioExpressionImageData);
|
||||
}
|
||||
|
||||
const miiStudioBodyUrl = `https://studio.mii.nintendo.com/miis/image.png?data=${encodedStudioMiiData}&type=all_body&width=270&instanceCount=1`;
|
||||
const miiStudioBodyImageData = await got(miiStudioBodyUrl).buffer();
|
||||
fs.writeFileSync(`${userMiiPath}/body.png`, miiStudioBodyImageData);
|
||||
|
||||
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
|
||||
};
|
||||
133
src/server.js
133
src/server.js
@@ -1,59 +1,76 @@
|
||||
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');
|
||||
const cdnService = require('./services/cdn');
|
||||
|
||||
// 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);
|
||||
app.use(cdnService);
|
||||
|
||||
// 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')
|
||||
};
|
||||
20
src/services/cdn/index.js
Normal file
20
src/services/cdn/index.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const express = require('express');
|
||||
const subdomain = require('express-subdomain');
|
||||
const logger = require('../../../logger');
|
||||
const routes = require('./routes');
|
||||
|
||||
// Router to handle the subdomain
|
||||
const miiImagesCDN = express.Router();
|
||||
|
||||
// Setup routes
|
||||
logger.info('[CDN] Applying imported routes');
|
||||
miiImagesCDN.use(routes.MII_IMAGES);
|
||||
|
||||
// Main router for endpoints
|
||||
const router = express.Router();
|
||||
|
||||
// Create subdomains
|
||||
logger.info('[CDN] Creating \'mii-images.cdn\' subdomain');
|
||||
router.use(subdomain('mii-images.cdn', miiImagesCDN));
|
||||
|
||||
module.exports = router;
|
||||
3
src/services/cdn/routes/index.js
Normal file
3
src/services/cdn/routes/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
MII_IMAGES: require('./mii-images.js'),
|
||||
};
|
||||
17
src/services/cdn/routes/mii-images.js
Normal file
17
src/services/cdn/routes/mii-images.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const router = require('express').Router();
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
|
||||
router.get('/:pid/:image', async (request, response) => {
|
||||
const { pid, image } = request.params;
|
||||
const userMiiImagePath = path.normalize(`${__dirname}/../../../../cdn/${pid}/miis/${image}`);
|
||||
console.log(userMiiImagePath);
|
||||
|
||||
if (fs.existsSync(userMiiImagePath)) {
|
||||
response.sendFile(userMiiImagePath);
|
||||
} else {
|
||||
response.sendStatus(404);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -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,163 @@
|
||||
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
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
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
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
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'),
|
||||
};
|
||||
94
src/services/wiiu/routes/miis.js
Normal file
94
src/services/wiiu/routes/miis.js
Normal file
@@ -0,0 +1,94 @@
|
||||
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.pretendo.cc/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 = [];
|
||||
|
||||
for (const user of results) {
|
||||
const { mii } = user;
|
||||
|
||||
const miiImages = [
|
||||
{
|
||||
cached_url: `http://mii-images.cdn.pretendo.cc/${user.pid}/normal_face.png`,
|
||||
id: mii.id,
|
||||
url: `http://mii-images.cdn.pretendo.cc/${user.pid}/normal_face.png`,
|
||||
type: 'standard'
|
||||
},
|
||||
{
|
||||
cached_url: `http://mii-images.cdn.pretendo.cc/${user.pid}/frustrated.png`,
|
||||
id: mii.id,
|
||||
url: `http://mii-images.cdn.pretendo.cc/${user.pid}/frustrated.png`,
|
||||
type: 'frustrated_face'
|
||||
},
|
||||
{
|
||||
cached_url: `http://mii-images.cdn.pretendo.cc/${user.pid}/smile_open_mouth.png`,
|
||||
id: mii.id,
|
||||
url: `http://mii-images.cdn.pretendo.cc/${user.pid}/smile_open_mouth.png`,
|
||||
type: 'happy_face'
|
||||
},
|
||||
{
|
||||
cached_url: `http://mii-images.cdn.pretendo.cc/${user.pid}/wink_left.png`,
|
||||
id: mii.id,
|
||||
url: `http://mii-images.cdn.pretendo.cc/${user.pid}/wink_left.png`,
|
||||
type: 'like_face'
|
||||
},
|
||||
{
|
||||
cached_url: `http://mii-images.cdn.pretendo.cc/${user.pid}/normal_face.png`,
|
||||
id: mii.id,
|
||||
url: `http://mii-images.cdn.pretendo.cc/${user.pid}/normal_face.png`,
|
||||
type: 'normal_face'
|
||||
},
|
||||
{
|
||||
cached_url: `http://mii-images.cdn.pretendo.cc/${user.pid}/sorrow.png`,
|
||||
id: mii.id,
|
||||
url: `http://mii-images.cdn.pretendo.cc/${user.pid}/sorrow.png`,
|
||||
type: 'puzzled_face'
|
||||
},
|
||||
{
|
||||
cached_url: `http://mii-images.cdn.pretendo.cc/${user.pid}/surprised_open_mouth.png`,
|
||||
id: mii.id,
|
||||
url: `http://mii-images.cdn.pretendo.cc/${user.pid}/surprised_open_mouth.png`,
|
||||
type: 'surprised_face'
|
||||
},
|
||||
{
|
||||
cached_url: `http://mii-images.cdn.pretendo.cc/${user.pid}/body.png`,
|
||||
id: mii.id,
|
||||
url: `http://mii-images.cdn.pretendo.cc/${user.pid}/body.png`,
|
||||
type: 'whole_body'
|
||||
}
|
||||
];
|
||||
|
||||
miis.push({
|
||||
data: mii.data.replace(/(\r\n|\n|\r)/gm, ''),
|
||||
id: mii.id,
|
||||
images: {
|
||||
image: miiImages
|
||||
},
|
||||
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,280 @@
|
||||
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
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
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: crypto.randomBytes(4).readUInt32LE()
|
||||
},
|
||||
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: crypto.randomBytes(4).readUInt32LE(),
|
||||
hash: crypto.randomBytes(7).toString('hex'),
|
||||
image_url: '', // deprecated, will be removed in the future
|
||||
image_id: crypto.randomBytes(4).readUInt32LE()
|
||||
},
|
||||
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
|
||||
}, { separateArrayItems: true }).end());
|
||||
});
|
||||
|
||||
/**
|
||||
* [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,56 @@
|
||||
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());
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
request.session.registration_status = 3;
|
||||
|
||||
response.status(200);
|
||||
response.end();
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
346
src/util.js
346
src/util.js
@@ -1,172 +1,176 @@
|
||||
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');
|
||||
|
||||
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 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) {
|
||||
const protocol = request.protocol;
|
||||
const host = request.host;
|
||||
const path = request.originalUrl;
|
||||
|
||||
return `${protocol}://${host}${path}`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
nintendoPasswordHash,
|
||||
generateToken,
|
||||
decryptToken,
|
||||
unpackToken,
|
||||
fullUrl
|
||||
};
|
||||
Reference in New Issue
Block a user