Merge pull request #51 from PretendoNetwork/feature-better-config

[Feature] Better config
This commit is contained in:
Jonathan Barrow
2022-10-14 21:27:46 -04:00
committed by GitHub
17 changed files with 597 additions and 89 deletions

3
.gitignore vendored
View File

@@ -63,4 +63,5 @@ t.js
p.js
config.json
certs
/cdn
/cdn
dump.rdb

View File

@@ -24,15 +24,18 @@
},
"from": "Company Name <user@company.net>"
},
"aws": {
"spaces": {
"key": "ACCESS_KEY",
"secret": "ACCESS_SECRET"
}
"s3": {
"endpoint": "nyc3.digitaloceanspaces.com",
"key": "ACCESS_KEY",
"secret": "ACCESS_SECRET"
},
"hcaptcha": {
"secret": "0x0000000000000000000000000000000000000000"
},
"cdn_base": "https://example.com",
"cdn": {
"base_url": "https://local-cdn.example.com",
"subdomain": "local-cdn",
"disk_path": "/home/jon/pretend-cdn"
},
"website_base": "https://example.com"
}

21
example.env Normal file
View File

@@ -0,0 +1,21 @@
PN_ACT_PREFER_ENV_CONFIG=true # Load config from ENV instead of config.json
PN_ACT_CONFIG_HTTP_PORT=7070
PN_ACT_CONFIG_MONGO_URI=mongodb://localhost:27017
PN_ACT_CONFIG_MONGO_DB_NAME=database_name
PN_ACT_CONFIG_MONGOOSE_OPTION_useNewUrlParser=true
PN_ACT_CONFIG_MONGOOSE_OPTION_useUnifiedTopology=true
PN_ACT_CONFIG_REDIS_URL=redis://localhost:6379
PN_ACT_CONFIG_EMAIL_HOST=smtp.gmail.com
PN_ACT_CONFIG_EMAIL_PORT=587
PN_ACT_CONFIG_EMAIL_SECURE=false
PN_ACT_CONFIG_EMAIL_USERNAME=username
PN_ACT_CONFIG_EMAIL_PASSWORD=password
PN_ACT_CONFIG_EMAIL_FROM=Company Name <user@company.net>
PN_ACT_CONFIG_S3_ENDPOINT=nyc3.digitaloceanspaces.com
PN_ACT_CONFIG_S3_ACCESS_KEY=ACCESS_KEY
PN_ACT_CONFIG_S3_ACCESS_SECRET=ACCESS_SECRET
PN_ACT_CONFIG_HCAPTCHA_SECRET=0x0000000000000000000000000000000000000000
PN_ACT_CONFIG_CDN_BASE_URL=https://local-cdn.example.com
PN_ACT_CONFIG_CDN_SUBDOMAIN=local-cdn
PN_ACT_CONFIG_CDN_DISK_PATH=/home/jon/pretend-cdn
PN_ACT_CONFIG_WEBSITE_BASE=https://example.com

30
package-lock.json generated
View File

@@ -14,6 +14,7 @@
"colors": "^1.4.0",
"cors": "^2.8.5",
"dicer": "^0.2.5",
"dotenv": "^16.0.3",
"email-validator": "^2.0.4",
"express": "^4.17.1",
"express-form-data": "^2.0.17",
@@ -26,6 +27,8 @@
"image-pixels": "^1.1.1",
"joi": "^17.6.1",
"kaitai-struct": "^0.9.0",
"lodash.get": "^4.4.2",
"lodash.set": "^4.3.2",
"mii-js": "github:PretendoNetwork/mii-js",
"moment": "^2.24.0",
"moment-timezone": "^0.5.27",
@@ -1083,6 +1086,14 @@
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
},
"node_modules/dotenv": {
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
"integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==",
"engines": {
"node": ">=12"
}
},
"node_modules/dtype": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dtype/-/dtype-2.0.0.tgz",
@@ -2044,7 +2055,12 @@
"node_modules/lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="
},
"node_modules/lodash.set": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
"integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg=="
},
"node_modules/lowercase-keys": {
"version": "2.0.0",
@@ -4540,6 +4556,11 @@
}
}
},
"dotenv": {
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
"integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ=="
},
"dtype": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dtype/-/dtype-2.0.0.tgz",
@@ -5334,7 +5355,12 @@
"lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="
},
"lodash.set": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
"integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg=="
},
"lowercase-keys": {
"version": "2.0.0",

View File

@@ -25,6 +25,7 @@
"colors": "^1.4.0",
"cors": "^2.8.5",
"dicer": "^0.2.5",
"dotenv": "^16.0.3",
"email-validator": "^2.0.4",
"express": "^4.17.1",
"express-form-data": "^2.0.17",
@@ -37,6 +38,8 @@
"image-pixels": "^1.1.1",
"joi": "^17.6.1",
"kaitai-struct": "^0.9.0",
"lodash.get": "^4.4.2",
"lodash.set": "^4.3.2",
"mii-js": "github:PretendoNetwork/mii-js",
"moment": "^2.24.0",
"moment-timezone": "^0.5.27",

View File

@@ -1,30 +1,45 @@
const fs = require('fs-extra');
const redis = require('redis');
const config = require('../config.json');
const { config, disabledFeatures } = require('./config-manager');
let client;
const memoryCache = {};
const SERVICE_CERTS_BASE = `${__dirname}/../certs/service`;
const NEX_CERTS_BASE = `${__dirname}/../certs/nex`;
const LOCAL_CDN_BASE = `${__dirname}/../cdn`;
async function connect() {
client = redis.createClient(config.redis.client);
client.on('error', (err) => console.log('Redis Client Error', err));
if (!disabledFeatures.redis) {
client = redis.createClient(config.redis.client);
client.on('error', (err) => console.log('Redis Client Error', err));
await client.connect();
await client.connect();
}
}
async function setCachedFile(type, name, fileName, value) {
await client.set(`${type}:${name}:${fileName}`, value);
async function setCachedFile(fileName, value) {
if (disabledFeatures.redis) {
memoryCache[fileName] = value;
} else {
await client.set(fileName, value);
}
}
async function getCachedFile(type, name, fileName, encoding) {
const cachedFile = await client.get(`${type}:${name}:${fileName}`);
async function getCachedFile(fileName, encoding) {
let cachedFile;
if (disabledFeatures.redis) {
cachedFile = memoryCache[fileName];
} else {
cachedFile = await client.get(fileName);
}
if (cachedFile !== null) {
return Buffer.from(cachedFile, encoding);
} else {
return cachedFile;
cachedFile = Buffer.from(cachedFile, encoding);
}
return cachedFile;
}
// NEX server cache functions
@@ -76,19 +91,19 @@ async function getNEXAESKey(name, encoding) {
}
async function setNEXPublicKey(name, value) {
await setCachedFile('nex', name, 'public_key', value);
await setCachedFile(`nex:${name}:public_key`, value);
}
async function setNEXPrivateKey(name, value) {
await setCachedFile('nex', name, 'private_key', value);
await setCachedFile(`nex:${name}:private_key`, value);
}
async function setNEXSecretKey(name, value) {
await setCachedFile('nex', name, 'secret_key', value);
await setCachedFile(`nex:${name}:secret_key`, value);
}
async function setNEXAESKey(name, value) {
await setCachedFile('nex', name, 'aes_key', value);
await setCachedFile(`nex:${name}:aes_key`, value);
}
// 3rd party service cache functions
@@ -140,19 +155,38 @@ async function getServiceAESKey(name, encoding) {
}
async function setServicePublicKey(name, value) {
await setCachedFile('service', name, 'public_key', value);
await setCachedFile(`service:${name}:public_key`, value);
}
async function setServicePrivateKey(name, value) {
await setCachedFile('service', name, 'private_key', value);
await setCachedFile(`service:${name}:private_key`, value);
}
async function setServiceSecretKey(name, value) {
await setCachedFile('service', name, 'secret_key', value);
await setCachedFile(`service:${name}:secret_key`, value);
}
async function setServiceAESKey(name, value) {
await setCachedFile('service', name, 'aes_key', value);
await setCachedFile(`service:${name}:aes_key`, value);
}
// Local CDN cache functions
async function getLocalCDNFile(name, encoding) {
let file = await getCachedFile(`local_cdn:${name}`, encoding);
if (file === null) {
if (await fs.pathExists(`${LOCAL_CDN_BASE}/${name}`)) {
file = await fs.readFile(`${LOCAL_CDN_BASE}/${name}`, { encoding });
await setLocalCDNFile(name, file);
}
}
return file;
}
async function setLocalCDNFile(name, value) {
await setCachedFile(`local_cdn:${name}`, value);
}
module.exports = {
@@ -173,4 +207,6 @@ module.exports = {
setServicePrivateKey,
setServiceSecretKey,
setServiceAESKey,
getLocalCDNFile,
setLocalCDNFile
};

339
src/config-manager.js Normal file
View File

@@ -0,0 +1,339 @@
const fs = require('fs-extra');
const get = require('lodash.get');
const set = require('lodash.set');
const logger = require('../logger');
require('dotenv').config();
/**
* @typedef {Object} Config
* @property {object} http HTTP server settings
* @property {number} http.port HTTP port the server will listen on
* @property {object} mongoose Mongose connection settings
* @property {string} mongoose.uri URI Mongoose will connect to
* @property {string} mongoose.database MongoDB database name
* @property {object} mongoose.options MongoDB connection options
* @property {object} [redis] redis settings
* @property {string} [redis.client] redis client settings
* @property {string} [redis.client.url] redis server URL
* @property {object} [email] node-mailer client settings
* @property {string} [email.host] SMTP server address
* @property {number} [email.port] SMTP server port
* @property {boolean} [email.secure] Secure SMTP
* @property {string} [email.from] Email 'from' name/address
* @property {object} [email.auth] Email authentication settings
* @property {string} [email.auth.user] Email username
* @property {string} [email.auth.pass] Email password
* @property {object} [s3] s3 client settings
* @property {object} [s3.endpoint] s3 endpoint URL
* @property {string} [s3.key] s3 access key
* @property {string} [s3.secret] s3 access secret
* @property {object} [hcaptcha] hCaptcha settings
* @property {string} [hcaptcha.secret] hCaptcha secret
* @property {object} cdn CDN config settings
* @property {object} [cdn.subdomain] Subdomain used for serving CDN contents when s3 is disabled
* @property {string} [cdn.disk_path] Fully qualified file system path for storing and reading local CDN contents
* @property {string} cdn.base_url Base URL for CDN server
* @property {string} website_base Base URL for service website (used with emails)
*/
/**
* @type {Config}
*/
let config = {};
/**
* @typedef {Object} DisabledFeatures
* @property {boolean} redis true if redis is disabled
* @property {boolean} email true if email sending is disabled
* @property {boolean} captcha true if captcha verification is disabled
* @property {boolean} s3 true if s3 services is disabled
*/
/**
* @type {DisabledFeatures}
*/
const disabledFeatures = {
redis: false,
email: false,
captcha: false,
s3: false
};
const requiredFields = [
['http.port', 'PN_ACT_CONFIG_HTTP_PORT', Number],
['mongoose.uri', 'PN_ACT_CONFIG_MONGO_URI'],
['mongoose.database', 'PN_ACT_CONFIG_MONGO_DB_NAME'],
['cdn.base_url', 'PN_ACT_CONFIG_CDN_BASE_URL']
];
function configure() {
const usingEnv = process.env.PN_ACT_PREFER_ENV_CONFIG === 'true';
if (usingEnv) {
logger.info('Loading config from environment variable');
config = {
http: {
port: Number(process.env.PN_ACT_CONFIG_HTTP_PORT)
},
mongoose: {
uri: process.env.PN_ACT_CONFIG_MONGO_URI,
database: process.env.PN_ACT_CONFIG_MONGO_DB_NAME,
options: Object.keys(process.env)
.filter(key => key.startsWith('PN_ACT_CONFIG_MONGOOSE_OPTION_'))
.reduce((obj, key) => {
obj[key.split('_').pop()] = process.env[key];
return obj;
}, {})
},
redis: {
client: {
url: process.env.PN_ACT_CONFIG_REDIS_URL
}
},
email: {
host: process.env.PN_ACT_CONFIG_EMAIL_HOST,
port: Number(process.env.PN_ACT_CONFIG_EMAIL_PORT),
secure: Boolean(process.env.PN_ACT_CONFIG_EMAIL_SECURE),
auth: {
user: process.env.PN_ACT_CONFIG_EMAIL_USERNAME,
pass: process.env.PN_ACT_CONFIG_EMAIL_PASSWORD
},
from: process.env.PN_ACT_CONFIG_EMAIL_FROM
},
s3: {
endpoint: process.env.PN_ACT_CONFIG_S3_ENDPOINT,
key: process.env.PN_ACT_CONFIG_S3_ACCESS_KEY,
secret: process.env.PN_ACT_CONFIG_S3_ACCESS_SECRET
},
hcaptcha: {
secret: process.env.PN_ACT_CONFIG_HCAPTCHA_SECRET
},
cdn: {
subdomain: process.env.PN_ACT_CONFIG_CDN_BASE,
disk_path: process.env.PN_ACT_CONFIG_CDN_BASE,
base_url: process.env.PN_ACT_CONFIG_CDN_BASE
},
website_base: process.env.PN_ACT_CONFIG_WEBSITE_BASE
};
} else {
logger.info('Loading config from config.json');
if (!fs.pathExistsSync(`${__dirname}/../config.json`)) {
logger.error('Failed to locate config.json file');
process.exit(0);
}
config = require(`${__dirname}/../config.json`);
}
logger.info('Config loaded, checking integrity');
// * Check for required settings
for (const requiredField of requiredFields) {
const [keyPath, env, convertType] = requiredField;
const configValue = get(config, keyPath);
const envValue = get(process.env, keyPath);
if (!configValue || (typeof configValue === 'string' && configValue.trim() === '')) {
if (!envValue || envValue.trim() === '') {
logger.error(`Failed to locate required field ${keyPath}. Set ${keyPath} in config.json or the ${env} environment variable`);
process.exit(0);
} else {
logger.info(`${keyPath} not found in config, using environment variable ${env}`);
const newValue = envValue;
set(config, keyPath, convertType ? convertType(newValue) : newValue);
}
}
}
// * Check for optional settings
const redisCheck = get(config, 'redis.client.url');
if (!redisCheck || redisCheck.trim() === '') {
if (usingEnv) {
logger.warn('Failed to find Redis connection url. Disabling feature and using in-memory cache. To enable feature set the PN_ACT_CONFIG_REDIS_URL environment variable');
} else {
logger.warn('Failed to find Redis connection url. Disabling feature and using in-memory cache. To enable feature set redis.client.url in your config.json');
}
disabledFeatures.redis = true;
}
const emailHostCheck = get(config, 'email.host');
if (!emailHostCheck || emailHostCheck.trim() === '') {
if (usingEnv) {
logger.warn('Failed to find email SMTP host. Disabling feature. To enable feature set the PN_ACT_CONFIG_EMAIL_HOST environment variable');
} else {
logger.warn('Failed to find email SMTP host. Disabling feature. To enable feature set email.host in your config.json');
}
disabledFeatures.email = true;
}
const emailPortCheck = get(config, 'email.port');
if (!emailPortCheck) {
if (usingEnv) {
logger.warn('Failed to find email SMTP port. Disabling feature. To enable feature set the PN_ACT_CONFIG_EMAIL_PORT environment variable');
} else {
logger.warn('Failed to find email SMTP port. Disabling feature. To enable feature set email.port in your config.json');
}
disabledFeatures.email = true;
}
const emailSecureCheck = get(config, 'email.secure');
if (emailSecureCheck === undefined) {
if (usingEnv) {
logger.warn('Failed to find email SMTP secure flag. Disabling feature. To enable feature set the PN_ACT_CONFIG_EMAIL_SECURE environment variable');
} else {
logger.warn('Failed to find email SMTP secure flag. Disabling feature. To enable feature set email.secure in your config.json');
}
disabledFeatures.email = true;
}
const emailUsernameCheck = get(config, 'email.auth.user');
if (!emailUsernameCheck || emailUsernameCheck.trim() === '') {
if (usingEnv) {
logger.warn('Failed to find email account username. Disabling feature. To enable feature set the auth.user environment variable');
} else {
logger.warn('Failed to find email account username. Disabling feature. To enable feature set email.auth.user in your config.json');
}
disabledFeatures.email = true;
}
const emailPasswordCheck = get(config, 'email.auth.pass');
if (!emailPasswordCheck || emailPasswordCheck.trim() === '') {
if (usingEnv) {
logger.warn('Failed to find email account password. Disabling feature. To enable feature set the PN_ACT_CONFIG_EMAIL_PASSWORD environment variable');
} else {
logger.warn('Failed to find email account password. Disabling feature. To enable feature set email.auth.pass in your config.json');
}
disabledFeatures.email = true;
}
const emailFromCheck = get(config, 'email.from');
if (!emailFromCheck || emailFromCheck.trim() === '') {
if (usingEnv) {
logger.warn('Failed to find email from config. Disabling feature. To enable feature set the PN_ACT_CONFIG_EMAIL_FROM environment variable');
} else {
logger.warn('Failed to find email from config. Disabling feature. To enable feature set email.from in your config.json');
}
disabledFeatures.email = true;
}
if (!disabledFeatures.email) {
const websiteBaseCheck = get(config, 'website_base');
if (!websiteBaseCheck || websiteBaseCheck.trim() === '') {
if (usingEnv) {
logger.error('Email sending is enabled and no website base was configured. Set the PN_ACT_CONFIG_WEBSITE_BASE environment variable');
} else {
logger.error('Email sending is enabled and no website base was configured. Set website_base in your config.json');
}
process.exit(0);
}
}
const captchaSecretCheck = get(config, 'hcaptcha.secret');
if (!captchaSecretCheck || captchaSecretCheck.trim() === '') {
if (usingEnv) {
logger.warn('Failed to find captcha secret config. Disabling feature. To enable feature set the PN_ACT_CONFIG_HCAPTCHA_SECRET environment variable');
} else {
logger.warn('Failed to find captcha secret config. Disabling feature. To enable feature set hcaptcha.secret in your config.json');
}
disabledFeatures.captcha = true;
}
const s3EndpointCheck = get(config, 's3.endpoint');
if (!s3EndpointCheck || s3EndpointCheck.trim() === '') {
if (usingEnv) {
logger.warn('Failed to find s3 endpoint config. Disabling feature. To enable feature set the PN_ACT_CONFIG_S3_ENDPOINT environment variable');
} else {
logger.warn('Failed to find s3 endpoint config. Disabling feature. To enable feature set s3.endpoint in your config.json');
}
disabledFeatures.s3 = true;
} else {
}
const s3AccessKeyCheck = get(config, 's3.key');
if (!s3AccessKeyCheck || s3AccessKeyCheck.trim() === '') {
if (usingEnv) {
logger.warn('Failed to find s3 access key config. Disabling feature. To enable feature set the PN_ACT_CONFIG_S3_ACCESS_KEY environment variable');
} else {
logger.warn('Failed to find s3 access key config. Disabling feature. To enable feature set s3.key in your config.json');
}
disabledFeatures.s3 = true;
}
const s3SecretKeyCheck = get(config, 's3.secret');
if (!s3SecretKeyCheck || s3SecretKeyCheck.trim() === '') {
if (usingEnv) {
logger.warn('Failed to find s3 secret key config. Disabling feature. To enable feature set the PN_ACT_CONFIG_S3_ACCESS_SECRET environment variable');
} else {
logger.warn('Failed to find s3 secret key config. Disabling feature. To enable feature set s3.secret in your config.json');
}
disabledFeatures.s3 = true;
}
if (disabledFeatures.s3) {
const cdnSubdomainCheck = get(config, 'cdn.subdomain');
if (!cdnSubdomainCheck || cdnSubdomainCheck.trim() === '') {
if (usingEnv) {
logger.error('s3 file storage is disabled and no CDN subdomain was set. Set the PN_ACT_CONFIG_CDN_SUBDOMAIN environment variable');
} else {
logger.error('s3 file storage is disabled and no CDN subdomain was set. Set cdn.subdomain in your config.json');
}
process.exit(0);
}
if (disabledFeatures.redis) {
logger.warn('Both s3 and Redis are disabled. Large CDN files will use the in-memory cache, which may result in high memory use. Please enable s3 if you\'re running a production server.');
}
logger.warn(`s3 file storage disabled. Using disk-based file storage. Please ensure cdn.base_url config or PN_ACT_CONFIG_CDN_BASE env variable is set to point to this server with the subdomain being ${config.cdn.subdomain}`);
}
module.exports.config = config;
}
module.exports = {
configure,
config,
disabledFeatures
};

View File

@@ -5,7 +5,7 @@ const util = require('./util');
const { PNID } = require('./models/pnid');
const { Server } = require('./models/server');
const logger = require('../logger');
const config = require('../config.json');
const { config } = require('./config-manager');
const { uri, database, options } = config.mongoose;
// TODO: Extend this later with more settings
@@ -172,9 +172,9 @@ async function getUserProfileJSONByPID(pid) {
mii_image: {
// Images MUST be loaded over HTTPS or console ignores them
// Bunny CDN is the only CDN which seems to support TLS 1.0/1.1 (required)
cached_url: `${config.cdn_base}/mii/${user.pid}/standard.tga`,
cached_url: `${config.cdn.base_url}/mii/${user.pid}/standard.tga`,
id: user.get('mii.image_id'),
url: `${config.cdn_base}/mii/${user.pid}/standard.tga`,
url: `${config.cdn.base_url}/mii/${user.pid}/standard.tga`,
type: 'standard'
}
},

View File

@@ -1,12 +1,18 @@
const nodemailer = require('nodemailer');
const config = require('../config.json');
const { config, disabledFeatures } = require('./config-manager');
const transporter = nodemailer.createTransport(config.email);
let transporter;
if (!disabledFeatures.email) {
transporter = nodemailer.createTransport(config.email);
}
async function sendMail(options) {
options.from = config.email.from;
if (!disabledFeatures.email) {
options.from = config.email.from;
await transporter.sendMail(options);
await transporter.sendMail(options);
}
}
module.exports = {

View File

@@ -1,5 +1,9 @@
process.title = 'Pretendo - Account';
const configManager = require('./config-manager');
configManager.configure();
const express = require('express');
const morgan = require('morgan');
const xmlparser = require('./middleware/xml-parser');
@@ -7,7 +11,8 @@ const cache = require('./cache');
const database = require('./database');
const util = require('./util');
const logger = require('../logger');
const config = require('../config.json');
const { config } = configManager;
const { http: { port } } = config;
const app = express();
@@ -17,10 +22,9 @@ const nnid = require('./services/nnid');
const nasc = require('./services/nasc');
const datastore = require('./services/datastore');
const api = require('./services/api');
const localcdn = require('./services/local-cdn');
// START APPLICATION
app.set('etag', false);
app.disable('x-powered-by');
// Create router
logger.info('Setting up Middleware');
@@ -37,6 +41,7 @@ app.use(nnid);
app.use(nasc);
app.use(datastore);
app.use(api);
app.use(localcdn);
// 404 handler
logger.info('Creating 404 status handler');

View File

@@ -12,7 +12,7 @@ const database = require('../../../../database');
const cache = require('../../../../cache');
const util = require('../../../../util');
const logger = require('../../../../../logger');
const config = require('../../../../../config.json');
const { config, disabledFeatures } = require('../../../../config-manager');
const PNID_VALID_CHARACTERS_REGEX = /^[\w\-\.]*$/gm;
const PNID_PUNCTUATION_START_REGEX = /^[\_\-\.]/gm;
@@ -42,22 +42,24 @@ router.post('/', async (request, response) => {
const passwordConfirm = body.password_confirm?.trim();
const hCaptchaResponse = body.hCaptchaResponse?.trim();
if (!hCaptchaResponse || hCaptchaResponse === '') {
return response.status(400).json({
app: 'api',
status: 400,
error: 'Must fill in captcha'
});
}
if (!disabledFeatures.captcha) {
if (!hCaptchaResponse || hCaptchaResponse === '') {
return response.status(400).json({
app: 'api',
status: 400,
error: 'Must fill in captcha'
});
}
const captchaVerify = await hcaptcha.verify(config.hcaptcha.secret, hCaptchaResponse);
const captchaVerify = await hcaptcha.verify(config.hcaptcha.secret, hCaptchaResponse);
if (!captchaVerify.success) {
return response.status(400).json({
app: 'api',
status: 400,
error: 'Captcha verification failed'
});
if (!captchaVerify.success) {
return response.status(400).json({
app: 'api',
status: 400,
error: 'Captcha verification failed'
});
}
}
if (!email || email === '') {

View File

@@ -1,7 +1,7 @@
const router = require('express').Router();
const joi = require('joi');
const { PNID } = require('../../../../models/pnid');
const config = require('../../../../../config.json');
const { config } = require('../../../../config-manager');
// TODO: Extend this later with more settings
const userSchema = joi.object({
@@ -47,7 +47,7 @@ router.get('/', async (request, response) => {
mii: {
data: pnid.get('mii.data'),
name: pnid.get('mii.name'),
image_url: `${config.cdn_base}/mii/${pnid.get('pid')}/normal_face.png`
image_url: `${config.cdn.base_url}/mii/${pnid.get('pid')}/normal_face.png`
},
flags: {
marketing: pnid.get('flags.marketing')
@@ -111,7 +111,7 @@ router.post('/', async (request, response) => {
mii: {
data: pnid.get('mii.data'),
name: pnid.get('mii.name'),
image_url: `${config.cdn_base}/mii/${pnid.get('pid')}/normal_face.png`
image_url: `${config.cdn.base_url}/mii/${pnid.get('pid')}/normal_face.png`
},
flags: {
marketing: pnid.get('flags.marketing')

View File

@@ -0,0 +1,30 @@
const express = require('express');
const subdomain = require('express-subdomain');
const logger = require('../../../logger');
const { config, disabledFeatures } = require('../../config-manager');
if (!disabledFeatures.s3) {
// * s3 enabled, no need for this
module.exports = express.Router();
return;
}
const routes = require('./routes');
// Router to handle the subdomain
const localcdn = express.Router();
// Setup routes
logger.info('[LOCAL-CDN] Applying imported routes');
localcdn.use(routes.GET);
// Main router for endpoints
const router = express.Router();
// Create subdomains
logger.info(`[LOCAL-CDN] Creating '${config.cdn.subdomain}' subdomain`);
router.use(subdomain(config.cdn.subdomain, localcdn));
module.exports = router;

View File

@@ -0,0 +1,16 @@
const router = require('express').Router();
const cache = require('../../../cache');
router.get('/*', async (request, response) => {
const filePath = request.params[0];
const file = await cache.getLocalCDNFile(filePath);
if (file) {
response.send(file);
} else {
response.sendStatus(404);
}
});
module.exports = router;

View File

@@ -0,0 +1,3 @@
module.exports = {
GET: require('./get.js'),
};

View File

@@ -1,7 +1,7 @@
const router = require('express').Router();
const xmlbuilder = require('xmlbuilder');
const { PNID } = require('../../../models/pnid');
const config = require('../../../../config.json');
const { config } = require('../../../config-manager');
/**
* [GET]
@@ -20,51 +20,51 @@ router.get('/', async (request, response) => {
const miiImages = [
{
cached_url: `${config.cdn_base}/mii/${user.pid}/normal_face.png`,
cached_url: `${config.cdn.base_url}/mii/${user.pid}/normal_face.png`,
id: mii.id,
url: `${config.cdn_base}/mii/${user.pid}/normal_face.png`,
url: `${config.cdn.base_url}/mii/${user.pid}/normal_face.png`,
type: 'standard'
},
{
cached_url: `${config.cdn_base}/mii/${user.pid}/frustrated.png`,
cached_url: `${config.cdn.base_url}/mii/${user.pid}/frustrated.png`,
id: mii.id,
url: `${config.cdn_base}/mii/${user.pid}/frustrated.png`,
url: `${config.cdn.base_url}/mii/${user.pid}/frustrated.png`,
type: 'frustrated_face'
},
{
cached_url: `${config.cdn_base}/mii/${user.pid}/smile_open_mouth.png`,
cached_url: `${config.cdn.base_url}/mii/${user.pid}/smile_open_mouth.png`,
id: mii.id,
url: `${config.cdn_base}/mii/${user.pid}/smile_open_mouth.png`,
url: `${config.cdn.base_url}/mii/${user.pid}/smile_open_mouth.png`,
type: 'happy_face'
},
{
cached_url: `${config.cdn_base}/mii/${user.pid}/wink_left.png`,
cached_url: `${config.cdn.base_url}/mii/${user.pid}/wink_left.png`,
id: mii.id,
url: `${config.cdn_base}/mii/${user.pid}/wink_left.png`,
url: `${config.cdn.base_url}/mii/${user.pid}/wink_left.png`,
type: 'like_face'
},
{
cached_url: `${config.cdn_base}/mii/${user.pid}/normal_face.png`,
cached_url: `${config.cdn.base_url}/mii/${user.pid}/normal_face.png`,
id: mii.id,
url: `${config.cdn_base}/mii/${user.pid}/normal_face.png`,
url: `${config.cdn.base_url}/mii/${user.pid}/normal_face.png`,
type: 'normal_face'
},
{
cached_url: `${config.cdn_base}/mii/${user.pid}/sorrow.png`,
cached_url: `${config.cdn.base_url}/mii/${user.pid}/sorrow.png`,
id: mii.id,
url: `${config.cdn_base}/mii/${user.pid}/sorrow.png`,
url: `${config.cdn.base_url}/mii/${user.pid}/sorrow.png`,
type: 'puzzled_face'
},
{
cached_url: `${config.cdn_base}/mii/${user.pid}/surprised_open_mouth.png`,
cached_url: `${config.cdn.base_url}/mii/${user.pid}/surprised_open_mouth.png`,
id: mii.id,
url: `${config.cdn_base}/mii/${user.pid}/surprised_open_mouth.png`,
url: `${config.cdn.base_url}/mii/${user.pid}/surprised_open_mouth.png`,
type: 'surprised_face'
},
{
cached_url: `${config.cdn_base}/mii/${user.pid}/body.png`,
cached_url: `${config.cdn.base_url}/mii/${user.pid}/body.png`,
id: mii.id,
url: `${config.cdn_base}/mii/${user.pid}/body.png`,
url: `${config.cdn.base_url}/mii/${user.pid}/body.png`,
type: 'whole_body'
}
];

View File

@@ -1,16 +1,21 @@
const crypto = require('crypto');
const path = require('path');
const NodeRSA = require('node-rsa');
const aws = require('aws-sdk');
const fs = require('fs-extra');
const mailer = require('./mailer');
const cache = require('./cache');
const config = require('../config.json');
const { config, disabledFeatures } = require('./config-manager');
const spacesEndpoint = new aws.Endpoint('nyc3.digitaloceanspaces.com');
const s3 = new aws.S3({
endpoint: spacesEndpoint,
accessKeyId: config.aws.spaces.key,
secretAccessKey: config.aws.spaces.secret
});
let s3;
if (!disabledFeatures.s3) {
s3 = new aws.S3({
endpoint: new aws.Endpoint(config.s3.endpoint),
accessKeyId: config.s3.key,
secretAccessKey: config.s3.secret
});
}
function nintendoPasswordHash(password, pid) {
const pidBuffer = Buffer.alloc(4);
@@ -203,20 +208,32 @@ function unpackToken(token) {
function fullUrl(request) {
const protocol = request.protocol;
const host = request.host;
const path = request.originalUrl;
const opath = request.originalUrl;
return `${protocol}://${host}${path}`;
return `${protocol}://${host}${opath}`;
}
async function uploadCDNAsset(bucket, key, data, acl) {
const awsPutParams = {
Body: data,
Key: key,
Bucket: bucket,
ACL: acl
};
if (disabledFeatures.s3) {
await writeLocalCDNFile(key, data);
} else {
const awsPutParams = {
Body: data,
Key: key,
Bucket: bucket,
ACL: acl
};
await s3.putObject(awsPutParams).promise();
await s3.putObject(awsPutParams).promise();
}
}
async function writeLocalCDNFile(key, data) {
const filePath = config.cdn.disk_path;
const folder = path.dirname(filePath);
await fs.ensureDir(folder);
await fs.writeFile(filePath, data);
}
function nascError(errorCode) {