Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove subdomain routing for the API service #116

Merged
merged 13 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,819 changes: 3,225 additions & 594 deletions package-lock.json

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@
"description": "",
"main": "./dist/server.js",
"scripts": {
"lint": "npx eslint .",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"build": "npm run lint && npm run clean && npx tsc && npx tsc-alias && npm run copy-static",
"clean": "rimraf ./dist",
"copy-static": "npm run copy-assets && npm run copy-timezones && npm run copy-views",
"copy-assets": "cp -r ./src/assets ./dist/assets",
"copy-views": "cp -r ./src/views ./dist/views",
"copy-timezones": "cp ./src/services/nnas/timezones.json ./dist/services/nnas/timezones.json",
"copy-static": "copyfiles -e \"src/**/*.ts\" -u 1 \"src/**/*\" dist",
"start": "node .",
"start:dev": "NODE_ENV=development node ."
"start:dev": "cross-env NODE_ENV=development node ."
},
"repository": {
"type": "git",
Expand All @@ -26,9 +24,9 @@
},
"homepage": "https://github.com/PretendoNetwork/account#readme",
"dependencies": {
"@aws-sdk/client-s3": "^3.657.0",
"@aws-sdk/client-ses": "^3.515.0",
"@pretendonetwork/grpc": "^1.0.5",
"aws-sdk": "^2.978.0",
"bcrypt": "^5.0.0",
"buffer-crc32": "^0.2.13",
"colors": "^1.4.0",
Expand All @@ -40,11 +38,11 @@
"email-validator": "^2.0.4",
"express": "^4.17.1",
"express-rate-limit": "^6.7.0",
"express-subdomain": "^1.0.5",
"fs-extra": "^8.1.0",
"got": "^11.8.2",
"hcaptcha": "^0.1.0",
"image-pixels": "^1.1.1",
"is-valid-hostname": "^1.0.2",
"joi": "^17.8.3",
"mii-js": "github:PretendoNetwork/mii-js",
"moment": "^2.29.4",
Expand Down Expand Up @@ -80,6 +78,8 @@
"@types/validator": "^13.7.14",
"@typescript-eslint/eslint-plugin": "^5.54.1",
"@typescript-eslint/parser": "^5.54.1",
"copyfiles": "^2.4.1",
"cross-env": "^7.0.3",
"eslint": "^8.35.0",
"ndarray": "^1.0.19",
"typescript": "^4.9.5"
Expand Down
91 changes: 68 additions & 23 deletions src/config-manager.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import fs from 'fs-extra';
import mongoose from 'mongoose';
import dotenv from 'dotenv';
import { LOG_INFO, LOG_WARN, LOG_ERROR } from '@/logger';
import { Config } from '@/types/common/config';
import { LOG_INFO, LOG_WARN, LOG_ERROR, formatHostnames } from '@/logger';
import { Config, domainServices } from '@/types/common/config';
import isValidHostname from 'is-valid-hostname';
binaryoverload marked this conversation as resolved.
Show resolved Hide resolved

dotenv.config();

Expand Down Expand Up @@ -60,7 +61,7 @@ export const config: Config = {
secret: process.env.PN_ACT_CONFIG_HCAPTCHA_SECRET || ''
},
cdn: {
subdomain: process.env.PN_ACT_CONFIG_CDN_SUBDOMAIN || '',
subdomain: process.env.PN_ACT_CONFIG_CDN_SUBDOMAIN,
disk_path: process.env.PN_ACT_CONFIG_CDN_DISK_PATH || '',
base_url: process.env.PN_ACT_CONFIG_CDN_BASE_URL || ''
},
Expand All @@ -76,6 +77,16 @@ export const config: Config = {
server_environment: process.env.PN_ACT_CONFIG_SERVER_ENVIRONMENT || '',
datastore: {
signature_secret: process.env.PN_ACT_CONFIG_DATASTORE_SIGNATURE_SECRET || ''
},
domains: {
api: (process.env.PN_ACT_CONFIG_DOMAINS_API || 'api.pretendo.cc').split(','),
assets: (process.env.PN_ACT_CONFIG_DOMAINS_ASSETS || 'assets.pretendo.cc').split(','),
cbvc: (process.env.PN_ACT_CONFIG_DOMAINS_CBVC || 'cbvc.cdn.pretendo.cc').split(','),
conntest: (process.env.PN_ACT_CONFIG_DOMAINS_CONNTEST || 'conntest.pretendo.cc').split(','),
datastore: (process.env.PN_ACT_CONFIG_DOMAINS_DATASTORE || 'datastore.pretendo.cc').split(','),
cdn: (process.env.PN_ACT_CONFIG_DOMAINS_CDN || '').split(',').filter(d => d),
binaryoverload marked this conversation as resolved.
Show resolved Hide resolved
nasc: (process.env.PN_ACT_CONFIG_DOMAINS_NASC || 'nasc.pretendo.cc').split(','),
nnas: (process.env.PN_ACT_CONFIG_DOMAINS_NNAS || 'c.account.pretendo.cc,account.pretendo.cc').split(','),
}
};

Expand All @@ -85,21 +96,50 @@ if (process.env.PN_ACT_CONFIG_STRIPE_SECRET_KEY) {
};
}

// Add the old config option for backwards compatibility
binaryoverload marked this conversation as resolved.
Show resolved Hide resolved
if (config.cdn.subdomain) {
config.domains.cdn.push(config.cdn.subdomain);
}

let configValid = true;

LOG_INFO('Config loaded, checking integrity');

for (const service of domainServices) {
const validDomains: string[] = [];
const invalidDomains: string[] = [];

const uniqueDomains = [...new Set(config.domains[service])];

for (const domain of uniqueDomains) {
isValidHostname(domain) ? validDomains.push(domain) : invalidDomains.push(domain);
}

if (validDomains.length === 0) {
LOG_ERROR(`No valid domains found for ${service}. Set the PN_ACT_CONFIG_DOMAINS_${service.toUpperCase()} environment variable to a valid domain`);
configValid = false;
}

if (invalidDomains.length) {
LOG_WARN(`Invalid domain(s) skipped for ${service}: ${formatHostnames(invalidDomains)}`);
}

config.domains[service] = validDomains;
}

if (!config.http.port) {
LOG_ERROR('Failed to find HTTP port. Set the PN_ACT_CONFIG_HTTP_PORT environment variable');
process.exit(0);
configValid = false;
}

if (!config.mongoose.connection_string) {
LOG_ERROR('Failed to find MongoDB connection string. Set the PN_ACT_CONFIG_MONGO_CONNECTION_STRING environment variable');
process.exit(0);
configValid = false;
}

if (!config.cdn.base_url) {
LOG_ERROR('Failed to find asset CDN base URL. Set the PN_ACT_CONFIG_CDN_BASE_URL environment variable');
process.exit(0);
configValid = false;
}

if (!config.redis.client.url) {
Expand Down Expand Up @@ -130,7 +170,7 @@ if (!config.email.from) {
if (!disabledFeatures.email) {
if (!config.website_base) {
LOG_ERROR('Email sending is enabled and no website base was configured. Set the PN_ACT_CONFIG_WEBSITE_BASE environment variable');
process.exit(0);
configValid = false;
}
}

Expand All @@ -140,17 +180,17 @@ if (!config.hcaptcha.secret) {
}

if (!config.s3.endpoint) {
LOG_WARN('Failed to find s3 endpoint config. Disabling feature. To enable feature set the PN_ACT_CONFIG_S3_ENDPOINT environment variable');
LOG_WARN('Failed to find S3 endpoint config. Disabling feature. To enable feature set the PN_ACT_CONFIG_S3_ENDPOINT environment variable');
disabledFeatures.s3 = true;
}

if (!config.s3.key) {
LOG_WARN('Failed to find s3 access key config. Disabling feature. To enable feature set the PN_ACT_CONFIG_S3_ACCESS_KEY environment variable');
LOG_WARN('Failed to find S3 access key config. Disabling feature. To enable feature set the PN_ACT_CONFIG_S3_ACCESS_KEY environment variable');
disabledFeatures.s3 = true;
}

if (!config.s3.secret) {
LOG_WARN('Failed to find s3 secret key config. Disabling feature. To enable feature set the PN_ACT_CONFIG_S3_ACCESS_SECRET environment variable');
LOG_WARN('Failed to find S3 secret key config. Disabling feature. To enable feature set the PN_ACT_CONFIG_S3_ACCESS_SECRET environment variable');
disabledFeatures.s3 = true;
}

Expand All @@ -160,41 +200,41 @@ if (!config.server_environment) {
}

if (disabledFeatures.s3) {
if (!config.cdn.subdomain) {
LOG_ERROR('s3 file storage is disabled and no CDN subdomain was set. Set the PN_ACT_CONFIG_CDN_SUBDOMAIN environment variable');
process.exit(0);
if (config.domains.cdn.length === 0) {
LOG_ERROR('S3 file storage is disabled and no CDN subdomain was set. Set the PN_ACT_CONFIG_DOMAINS_CDN environment variable');
configValid = false;
}

if (!config.cdn.disk_path) {
LOG_ERROR('s3 file storage is disabled and no CDN disk path was set. Set the PN_ACT_CONFIG_CDN_DISK_PATH environment variable');
process.exit(0);
LOG_ERROR('S3 file storage is disabled and no CDN disk path was set. Set the PN_ACT_CONFIG_CDN_DISK_PATH environment variable');
configValid = false;
}

LOG_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}`);
LOG_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}`);

if (disabledFeatures.redis) {
LOG_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.');
LOG_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.');
}
}

if (!config.aes_key) {
LOG_ERROR('Token AES key is not set. Set the PN_ACT_CONFIG_AES_KEY environment variable to your AES-256-CBC key');
process.exit(0);
configValid = false;
}

if (!config.grpc.master_api_keys.account) {
LOG_ERROR('Master gRPC API key for the account service is not set. Set the PN_ACT_CONFIG_GRPC_MASTER_API_KEY_ACCOUNT environment variable');
process.exit(0);
configValid = false;
}

if (!config.grpc.master_api_keys.api) {
LOG_ERROR('Master gRPC API key for the api service is not set. Set the PN_ACT_CONFIG_GRPC_MASTER_API_KEY_API environment variable');
process.exit(0);
configValid = false;
}

if (!config.grpc.port) {
LOG_ERROR('Failed to find gRPC port. Set the PN_ACT_CONFIG_GRPC_PORT environment variable');
process.exit(0);
configValid = false;
}

if (!config.stripe?.secret_key) {
Expand All @@ -203,9 +243,14 @@ if (!config.stripe?.secret_key) {

if (!config.datastore.signature_secret) {
LOG_ERROR('Datastore signature secret key is not set. Set the PN_ACT_CONFIG_DATASTORE_SIGNATURE_SECRET environment variable');
process.exit(0);
configValid = false;
}
if (config.datastore.signature_secret.length !== 32 || !hexadecimalStringRegex.test(config.datastore.signature_secret)) {
LOG_ERROR('Datastore signature secret key must be a 32-character hexadecimal string.');
process.exit(0);
configValid = false;
}
binaryoverload marked this conversation as resolved.
Show resolved Hide resolved

if (!configValid) {
LOG_ERROR('Config is invalid. Exiting');
process.exit(0);
}
4 changes: 4 additions & 0 deletions src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,8 @@ export function LOG_INFO(input: string): void {
streams.info.write(`${input}\n`);

console.log(`${input}`.cyan.bold);
}

export function formatHostnames(hostnames: string[]): string {
return hostnames.map(d => `'${d}'`).join(', ');
}
14 changes: 14 additions & 0 deletions src/middleware/host-limit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import express from 'express';

export function restrictHostnames<TFn extends express.Router>(
allowedHostnames: string[],
fn: TFn
): (req: express.Request, res: express.Response, next: () => void) => void | TFn {
return (req: express.Request, res: express.Response, next: () => void) => {
if (!allowedHostnames.includes(req.hostname)) {
return fn(req, res, next);
binaryoverload marked this conversation as resolved.
Show resolved Hide resolved
}

return next();
};
}
11 changes: 6 additions & 5 deletions src/services/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import express from 'express';
import subdomain from 'express-subdomain';
import cors from 'cors';
import APIMiddleware from '@/middleware/api';
import { LOG_INFO } from '@/logger';
import { formatHostnames, LOG_INFO } from '@/logger';

import { V1 } from '@/services/api/routes';
import { config } from '@/config-manager';
import { restrictHostnames } from '@/middleware/host-limit';

// * Router to handle the subdomain restriction
const api = express.Router();
Expand All @@ -28,8 +29,8 @@ api.use('/v1/user', V1.USER);
// * Main router for endpoints
const router = express.Router();

// * Create subdomains
LOG_INFO('[USER API] Creating \'api\' subdomain');
router.use(subdomain('api', api));
// * Create domains
LOG_INFO(`[USER API] Registering api router with domains: ${formatHostnames(config.domains.api)}`);
router.use(restrictHostnames(config.domains.api, api));

export default router;
11 changes: 6 additions & 5 deletions src/services/assets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

import path from 'node:path';
import express from 'express';
import subdomain from 'express-subdomain';
import { LOG_INFO } from '@/logger';
import { LOG_INFO, formatHostnames } from '@/logger';
import { config } from '@/config-manager';
import { restrictHostnames } from '@/middleware/host-limit';

// * Router to handle the subdomain restriction
const assets = express.Router();
Expand All @@ -15,8 +16,8 @@ assets.use(express.static(path.join(__dirname, '../../assets')));
// * Main router for endpoints
const router = express.Router();

// * Create subdomains
LOG_INFO('[conntest] Creating \'assets\' subdomain');
router.use(subdomain('assets', assets));
// * Create domains
LOG_INFO(`[assets] Creating assets router with domains: ${formatHostnames(config.domains.assets)}`);
router.use(restrictHostnames(config.domains.assets, assets));

export default router;
13 changes: 7 additions & 6 deletions src/services/cbvc/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
// * handles CBVC (CTR Browser Version Check?) endpoints

import express from 'express';
import subdomain from 'express-subdomain';
import { LOG_INFO } from '@/logger';
import { LOG_INFO, formatHostnames } from '@/logger';
import { config } from '@/config-manager';
import { restrictHostnames } from '@/middleware/host-limit';

// * Router to handle the subdomain restriction
const cbvc = express.Router();

// * Setup route
LOG_INFO('[cbvc] Applying imported routes');
LOG_INFO('[CBVC] Applying imported routes');
cbvc.get('/:consoleType/:unknown/:region', (request: express.Request, response: express.Response): void => {
response.set('Content-Type', 'text/plain');

Expand All @@ -24,8 +25,8 @@ cbvc.get('/:consoleType/:unknown/:region', (request: express.Request, response:
// * Main router for endpoints
const router = express.Router();

// * Create subdomains
LOG_INFO('[cbvc] Creating \'cbvc\' subdomain');
router.use(subdomain('cbvc.cdn', cbvc));
// * Create domains
LOG_INFO(`[CBVC] Creating cbvc router with domains: ${formatHostnames(config.domains.cbvc)}`);
router.use(restrictHostnames(config.domains.cbvc, cbvc));

export default router;
11 changes: 6 additions & 5 deletions src/services/conntest/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// * handles conntest endpoints

import express from 'express';
import subdomain from 'express-subdomain';
import { LOG_INFO } from '@/logger';
import { LOG_INFO, formatHostnames } from '@/logger';
import { restrictHostnames } from '@/middleware/host-limit';
import { config } from '@/config-manager';

// * Router to handle the subdomain restriction
const conntest = express.Router();
Expand All @@ -29,8 +30,8 @@ This is test.html page
// * Main router for endpoints
const router = express.Router();

// * Create subdomains
LOG_INFO('[conntest] Creating \'conntest\' subdomain');
router.use(subdomain('conntest', conntest));
// * Create domains
LOG_INFO(`[conntest] Creating conntest router with domains: ${formatHostnames(config.domains.conntest)}`);
router.use(restrictHostnames(config.domains.conntest, conntest));

export default router;
Loading