diff --git a/.gitignore b/.gitignore index 6ca20ff..ce5d611 100644 --- a/.gitignore +++ b/.gitignore @@ -63,4 +63,5 @@ t.js p.js config.json certs -/cdn \ No newline at end of file +/cdn +dump.rdb \ No newline at end of file diff --git a/example.config.json b/example.config.json index bfa68f4..b8a86b5 100644 --- a/example.config.json +++ b/example.config.json @@ -24,15 +24,18 @@ }, "from": "Company Name " }, - "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" } \ No newline at end of file diff --git a/example.env b/example.env new file mode 100644 index 0000000..84a692a --- /dev/null +++ b/example.env @@ -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 +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 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 00a2f1b..4fa0176 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 1ef4be4..bed4955 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/cache.js b/src/cache.js index 7d263e9..e889483 100644 --- a/src/cache.js +++ b/src/cache.js @@ -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 }; \ No newline at end of file diff --git a/src/config-manager.js b/src/config-manager.js new file mode 100644 index 0000000..3cc79e8 --- /dev/null +++ b/src/config-manager.js @@ -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 +}; \ No newline at end of file diff --git a/src/database.js b/src/database.js index 53b9cdc..860e05c 100644 --- a/src/database.js +++ b/src/database.js @@ -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' } }, diff --git a/src/mailer.js b/src/mailer.js index 6f00665..d8228e2 100644 --- a/src/mailer.js +++ b/src/mailer.js @@ -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 = { diff --git a/src/server.js b/src/server.js index 5711f6d..c4dcf6b 100644 --- a/src/server.js +++ b/src/server.js @@ -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'); diff --git a/src/services/api/routes/v1/register.js b/src/services/api/routes/v1/register.js index 5f91aac..729bc41 100644 --- a/src/services/api/routes/v1/register.js +++ b/src/services/api/routes/v1/register.js @@ -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 === '') { diff --git a/src/services/api/routes/v1/user.js b/src/services/api/routes/v1/user.js index 07c014d..7008548 100644 --- a/src/services/api/routes/v1/user.js +++ b/src/services/api/routes/v1/user.js @@ -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') diff --git a/src/services/local-cdn/index.js b/src/services/local-cdn/index.js new file mode 100644 index 0000000..b0e3985 --- /dev/null +++ b/src/services/local-cdn/index.js @@ -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; \ No newline at end of file diff --git a/src/services/local-cdn/routes/get.js b/src/services/local-cdn/routes/get.js new file mode 100644 index 0000000..7ae69b9 --- /dev/null +++ b/src/services/local-cdn/routes/get.js @@ -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; \ No newline at end of file diff --git a/src/services/local-cdn/routes/index.js b/src/services/local-cdn/routes/index.js new file mode 100644 index 0000000..033fca1 --- /dev/null +++ b/src/services/local-cdn/routes/index.js @@ -0,0 +1,3 @@ +module.exports = { + GET: require('./get.js'), +}; \ No newline at end of file diff --git a/src/services/nnid/routes/miis.js b/src/services/nnid/routes/miis.js index bc99ce9..3c5603c 100644 --- a/src/services/nnid/routes/miis.js +++ b/src/services/nnid/routes/miis.js @@ -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' } ]; diff --git a/src/util.js b/src/util.js index 32f3cab..5550a70 100644 --- a/src/util.js +++ b/src/util.js @@ -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) {