mirror of
https://github.com/PretendoNetwork/BOSS.git
synced 2026-04-24 23:17:47 -05:00
Merge pull request #26 from PretendoNetwork/feat/logger-and-config
Add pino logger + add domain configuration
This commit is contained in:
commit
1112c135e5
48
README.md
48
README.md
|
|
@ -1,5 +1,47 @@
|
|||
# BOSS
|
||||
### Pretendo BOSS server implementation
|
||||
|
||||
## About
|
||||
Handles all BOSS (SpotPass on 3DS) related tasks for the WiiU and 3DS
|
||||
Handles all BOSS (Background Online Storage Service) related tasks for the Pretendo network.
|
||||
|
||||
## What does BOSS handle?
|
||||
- SpotPass on 3DS
|
||||
- Tasksheets and policy files for both WiiU and 3DS
|
||||
- Streetpass relay
|
||||
|
||||
## Configuration
|
||||
|
||||
Configurations are loaded through environment variables. `.env` files are supported.
|
||||
|
||||
| Environment variable | Description | Default |
|
||||
| -------------------------------------------- | ----------------------------------------------------------------- | --------------------------------------------- |
|
||||
| `PN_BOSS_CONFIG_HTTP_PORT` | The HTTP port the server listens on | None |
|
||||
| `PN_BOSS_CONFIG_LOG_FORMAT` | What logging format to use, possible options: `pretty` or `json` | `pretty` |
|
||||
| `PN_BOSS_CONFIG_LOG_LEVEL` | What log level to use | `info` |
|
||||
| `PN_BOSS_CONFIG_BOSS_WIIU_AES_KEY` | The BOSS WiiU AES key, needs to be dumped from a console | None |
|
||||
| `PN_BOSS_CONFIG_BOSS_WIIU_HMAC_KEY` | The BOSS WiiU HMAC key, needs to be dumped from a console | None |
|
||||
| `PN_BOSS_CONFIG_BOSS_3DS_AES_KEY` | The BOSS 3DS AES key, needs to be dumped from a console | None |
|
||||
| `PN_BOSS_CONFIG_MONGO_CONNECTION_STRING` | MongoDB connection string | None |
|
||||
| `PN_BOSS_CONFIG_GRPC_BOSS_SERVER_ADDRESS` | Address for the GRPC server to listen on | None |
|
||||
| `PN_BOSS_CONFIG_GRPC_BOSS_SERVER_PORT` | Port for the GRPC server to listen on | None |
|
||||
| `PN_BOSS_CONFIG_GRPC_BOSS_SERVER_API_KEY` | API key that services will use to connect to the BOSS GRPC server | None |
|
||||
| `PN_BOSS_CONFIG_GRPC_ACCOUNT_SERVER_ADDRESS` | Address of the account GRPC server | None |
|
||||
| `PN_BOSS_CONFIG_GRPC_ACCOUNT_SERVER_PORT` | Port of the account GRPC server | None |
|
||||
| `PN_BOSS_CONFIG_GRPC_ACCOUNT_SERVER_API_KEY` | API key of the account GRPC server | None |
|
||||
| `PN_BOSS_CONFIG_GRPC_FRIENDS_SERVER_ADDRESS` | Address of the friends GRPC server | None |
|
||||
| `PN_BOSS_CONFIG_GRPC_FRIENDS_SERVER_PORT` | Port of the friends GRPC server | None |
|
||||
| `PN_BOSS_CONFIG_GRPC_FRIENDS_SERVER_API_KEY` | API key of the friends GRPC server | None |
|
||||
| `PN_BOSS_CONFIG_S3_ENDPOINT` | S3 server endpoint | None |
|
||||
| `PN_BOSS_CONFIG_S3_REGION` | S3 server region | None |
|
||||
| `PN_BOSS_CONFIG_S3_BUCKET` | S3 server bucket | None |
|
||||
| `PN_BOSS_CONFIG_S3_ACCESS_KEY` | S3 access key | None |
|
||||
| `PN_BOSS_CONFIG_S3_ACCESS_SECRET` | S3 access key secret | None |
|
||||
| `PN_BOSS_CONFIG_CDN_DISK_PATH` | Storage path for the CDN, use as alternative for S3 | None |
|
||||
| `PN_BOSS_CONFIG_STREETPASS_RELAY_ENABLED` | Should Streetpass Relay be enabled? | `false` |
|
||||
| `PN_BOSS_CONFIG_DOMAINS_NPDI` | What domain should the NPDI component use? | `npdi.cdn.pretendo.cc` |
|
||||
| `PN_BOSS_CONFIG_DOMAINS_NPDL` | What domain should the NPDL component use? | `npdl.cdn.pretendo.cc` |
|
||||
| `PN_BOSS_CONFIG_DOMAINS_NPFL` | What domain should the NPFL component use? | `npfl.c.app.pretendo.cc` |
|
||||
| `PN_BOSS_CONFIG_DOMAINS_NPPL` | What domain should the NPPL component use? | `nppl.app.pretendo.cc,nppl.c.app.pretendo.cc` |
|
||||
| `PN_BOSS_CONFIG_DOMAINS_NPTS` | What domain should the NPTS component use? | `npts.app.pretendo.cc` |
|
||||
| `PN_BOSS_CONFIG_DOMAINS_SPR` | What domain should the SPR component use? | `service.spr.app.pretendo.cc` |
|
||||
|
||||
## S3 server
|
||||
The S3 server is optional, you can set `PN_BOSS_CONFIG_CDN_DISK_PATH` if you want to use a local folder as CDN source instead.
|
||||
|
|
|
|||
1414
package-lock.json
generated
1414
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
|
|
@ -1,35 +1,32 @@
|
|||
{
|
||||
"name": "boss",
|
||||
"description": "Pretendo BOSS server implementation",
|
||||
"version": "2.0.0",
|
||||
"description": "",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "dist/server.js",
|
||||
"scripts": {
|
||||
"dev": "tsup --watch --onSuccess \"node dist/server.js\"",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"build": "npm run lint && tsup",
|
||||
"build": "tsup && tsc --noEmit",
|
||||
"start": "node --enable-source-maps dist/server.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.723.0",
|
||||
"@pretendonetwork/boss-crypto": "^1.0.0",
|
||||
"@pretendonetwork/grpc": "^1.0.6",
|
||||
"@typegoose/auto-increment": "^3.6.1",
|
||||
"boss-js": "github:PretendoNetwork/boss-js",
|
||||
"cacache": "^19.0.1",
|
||||
"colors": "^1.4.0",
|
||||
"dicer": "^0.3.1",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^5.1.0",
|
||||
"express-subdomain": "^1.0.6",
|
||||
"fs-extra": "^11.2.0",
|
||||
"moment": "^2.30.1",
|
||||
"mongoose": "~7.6.1",
|
||||
"morgan": "^1.10.0",
|
||||
"nice-grpc": "^2.1.10",
|
||||
"pino": "^9.9.1",
|
||||
"pino-http": "^10.5.0",
|
||||
"pino-pretty": "^13.1.1",
|
||||
"xml-js": "^1.6.11",
|
||||
"xmlbuilder": "^15.1.1"
|
||||
},
|
||||
|
|
@ -39,11 +36,9 @@
|
|||
"@types/dicer": "^0.2.4",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/morgan": "^1.9.9",
|
||||
"@types/node": "^22.10.5",
|
||||
"axios": "^1.7.9",
|
||||
"eslint": "^9.17.0",
|
||||
"tsc-alias": "^1.8.10",
|
||||
"tsup": "^8.3.5",
|
||||
"typescript": "^5.7.2",
|
||||
"xmlbuilder2": "^3.1.1"
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ import crypto from 'node:crypto';
|
|||
import path from 'node:path';
|
||||
import fs from 'fs-extra';
|
||||
import dotenv from 'dotenv';
|
||||
import { LOG_INFO, LOG_WARN, LOG_ERROR } from '@/logger';
|
||||
import type mongoose from 'mongoose';
|
||||
import type { DisabledFeatures, Config } from '@/types/common/config';
|
||||
import pinoPretty from 'pino-pretty';
|
||||
import { pino } from 'pino';
|
||||
|
||||
// temporary logger - just for configuration (as log level and format is not yet known in this file)
|
||||
const logger = pino(pinoPretty());
|
||||
|
||||
dotenv.config();
|
||||
|
||||
|
|
@ -17,28 +19,22 @@ const BOSS_WIIU_AES_KEY_MD5_HASH = '5202ce5099232c3d365e28379790a919';
|
|||
const BOSS_WIIU_HMAC_KEY_MD5_HASH = 'b4482fef177b0100090ce0dbeb8ce977';
|
||||
const BOSS_3DS_AES_KEY_MD5_HASH = '86fbc2bb4cb703b2a4c6cc9961319926';
|
||||
|
||||
LOG_INFO('Loading config');
|
||||
|
||||
const warnings: string[] = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
let mongooseConnectOptionsMain: mongoose.ConnectOptions = {};
|
||||
|
||||
if (process.env.PN_BOSS_CONFIG_MONGOOSE_CONNECT_OPTIONS_PATH?.trim()) {
|
||||
mongooseConnectOptionsMain = fs.readJSONSync(process.env.PN_BOSS_CONFIG_MONGOOSE_CONNECT_OPTIONS_PATH?.trim());
|
||||
} else {
|
||||
warnings.push('No Mongoose connection options found for main connection. To add connection options, set PN_BOSS_CONFIG_MONGOOSE_CONNECT_OPTIONS_PATH to the path of your options JSON file');
|
||||
}
|
||||
|
||||
export const disabledFeatures: DisabledFeatures = {
|
||||
export const disabledFeatures = {
|
||||
s3: false,
|
||||
spr: false
|
||||
};
|
||||
|
||||
export const config: Config = {
|
||||
export const config = {
|
||||
http: {
|
||||
port: Number(process.env.PN_BOSS_CONFIG_HTTP_PORT?.trim() || '')
|
||||
},
|
||||
log: {
|
||||
format: process.env.PN_BOSS_CONFIG_LOG_FORMAT?.trim() || 'pretty',
|
||||
level: process.env.PN_BOSS_CONFIG_LOG_LEVEL?.trim() || 'info'
|
||||
},
|
||||
crypto: {
|
||||
wup: {
|
||||
aes_key: process.env.PN_BOSS_CONFIG_BOSS_WIIU_AES_KEY?.trim() || '',
|
||||
|
|
@ -66,11 +62,9 @@ export const config: Config = {
|
|||
}
|
||||
},
|
||||
mongoose: {
|
||||
connection_string: process.env.PN_BOSS_CONFIG_MONGO_CONNECTION_STRING?.trim() || '',
|
||||
options: mongooseConnectOptionsMain
|
||||
connection_string: process.env.PN_BOSS_CONFIG_MONGO_CONNECTION_STRING?.trim() || ''
|
||||
},
|
||||
cdn: {
|
||||
download_url: process.env.PN_BOSS_CONFIG_CDN_DOWNLOAD_URL?.trim() || '',
|
||||
s3: {
|
||||
endpoint: process.env.PN_BOSS_CONFIG_S3_ENDPOINT?.trim() || '',
|
||||
region: process.env.PN_BOSS_CONFIG_S3_REGION?.trim() || '',
|
||||
|
|
@ -82,15 +76,31 @@ export const config: Config = {
|
|||
},
|
||||
spr: {
|
||||
enabled: process.env.PN_BOSS_CONFIG_STREETPASS_RELAY_ENABLED?.trim().toLowerCase() === 'true'
|
||||
},
|
||||
domains: {
|
||||
npdi: (process.env.PN_BOSS_CONFIG_DOMAINS_NPDI || 'npdi.cdn.pretendo.cc').split(','),
|
||||
npdl: (process.env.PN_BOSS_CONFIG_DOMAINS_NPDL || 'npdl.cdn.pretendo.cc').split(','),
|
||||
npfl: (process.env.PN_BOSS_CONFIG_DOMAINS_NPFL || 'npfl.c.app.pretendo.cc').split(','),
|
||||
nppl: (process.env.PN_BOSS_CONFIG_DOMAINS_NPPL || 'nppl.app.pretendo.cc,nppl.c.app.pretendo.cc').split(','),
|
||||
npts: (process.env.PN_BOSS_CONFIG_DOMAINS_NPTS || 'npts.app.pretendo.cc').split(','),
|
||||
spr: (process.env.PN_BOSS_CONFIG_DOMAINS_SPR || 'service.spr.app.pretendo.cc').split(',')
|
||||
}
|
||||
};
|
||||
|
||||
LOG_INFO('Config loaded, checking integrity');
|
||||
|
||||
if (!config.http.port) {
|
||||
errors.push('Failed to find HTTP port. Set the PN_BOSS_CONFIG_HTTP_PORT environment variable');
|
||||
}
|
||||
|
||||
const possibleConfigFormats = ['pretty', 'json'];
|
||||
if (!possibleConfigFormats.includes(config.log.format)) {
|
||||
errors.push(`Invalid log format, possible values: ${possibleConfigFormats.join(', ')}`);
|
||||
}
|
||||
|
||||
const possibleconfigLevels = ['error', 'warn', 'info', 'debug', 'trace'];
|
||||
if (!possibleconfigLevels.includes(config.log.level)) {
|
||||
errors.push(`Invalid log level, possible values: ${possibleConfigFormats.join(', ')}`);
|
||||
}
|
||||
|
||||
if (md5(config.crypto.wup.aes_key) !== BOSS_WIIU_AES_KEY_MD5_HASH) {
|
||||
warnings.push('Invalid BOSS WiiU AES key. Uploading and encrypting new BOSS content for the Wii U won\'t work! Set or correct the PN_BOSS_CONFIG_BOSS_WIIU_AES_KEY environment variable');
|
||||
}
|
||||
|
|
@ -143,16 +153,6 @@ if (!config.mongoose.connection_string) {
|
|||
errors.push('Failed to find MongoDB connection string. Set the PN_BOSS_CONFIG_MONGO_CONNECTION_STRING environment variable');
|
||||
}
|
||||
|
||||
if (!config.cdn.download_url) {
|
||||
errors.push('Failed to find CDN content download URL. Set the PN_BOSS_CONFIG_CDN_DOWNLOAD_URL environment variable');
|
||||
} else {
|
||||
const parsedURL = new URL(config.cdn.download_url);
|
||||
|
||||
if (!parsedURL.hostname.startsWith('npdi.cdn')) {
|
||||
errors.push('CDN content download URL *MUST* use the subdomain `npdi.cdn`');
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.cdn.s3.endpoint) {
|
||||
warnings.push('Failed to find s3 endpoint config. Disabling feature. To enable feature set the PN_BOSS_CONFIG_S3_ENDPOINT environment variable');
|
||||
disabledFeatures.s3 = true;
|
||||
|
|
@ -194,12 +194,12 @@ if (disabledFeatures.s3) {
|
|||
}
|
||||
|
||||
for (const warning of warnings) {
|
||||
LOG_WARN(warning);
|
||||
logger.warn(warning);
|
||||
}
|
||||
|
||||
if (errors.length !== 0) {
|
||||
for (const error of errors) {
|
||||
LOG_ERROR(error);
|
||||
logger.error(error);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
|
|
|
|||
|
|
@ -10,12 +10,11 @@ import type { HydratedTaskDocument, ITask } from '@/types/mongoose/task';
|
|||
import type { HydratedFileDocument, IFile } from '@/types/mongoose/file';
|
||||
|
||||
const connection_string: string = config.mongoose.connection_string;
|
||||
const options: mongoose.ConnectOptions = config.mongoose.options;
|
||||
|
||||
let _connection: mongoose.Connection;
|
||||
|
||||
export async function connect(): Promise<void> {
|
||||
await mongoose.connect(connection_string, options);
|
||||
await mongoose.connect(connection_string);
|
||||
|
||||
_connection = mongoose.connection;
|
||||
_connection.on('error', console.error.bind(console, 'connection error:'));
|
||||
|
|
|
|||
|
|
@ -1,55 +1,46 @@
|
|||
import fs from 'fs-extra';
|
||||
import colors from 'colors';
|
||||
import { pino } from 'pino';
|
||||
import pinoPretty from 'pino-pretty';
|
||||
import { pinoHttp } from 'pino-http';
|
||||
import { config } from '@/config-manager';
|
||||
import type { SerializedRequest, SerializedResponse } from 'pino';
|
||||
|
||||
colors.enable();
|
||||
const pretty = config.log.format == 'pretty'
|
||||
? pinoPretty({
|
||||
customPrettifiers: {
|
||||
// Clean up Express types for developer eyes
|
||||
req(inputData, _key, _log, { colors }) {
|
||||
const req = inputData as SerializedRequest;
|
||||
return `${colors.bold(req.method)} ${req.headers.host}${req.url} (${req.remoteAddress}:${req.remotePort})`;
|
||||
},
|
||||
res(inputData, _key, _log, { colors }) {
|
||||
const res = inputData as SerializedResponse;
|
||||
const color = ((): (val: any) => string => {
|
||||
if (res.statusCode >= 500) {
|
||||
return colors.red;
|
||||
} else if (res.statusCode >= 400) {
|
||||
return colors.yellow;
|
||||
} else if (res.statusCode >= 200) {
|
||||
return colors.green;
|
||||
} else {
|
||||
return colors.reset;
|
||||
}
|
||||
})();
|
||||
|
||||
const root = process.env.PN_BOSS_CONFIG_LOGGER_PATH ? process.env.PN_BOSS_CONFIG_LOGGER_PATH : `${__dirname}/..`;
|
||||
fs.ensureDirSync(`${root}/logs`);
|
||||
return `${color(res.statusCode)} (${res.headers['content-length']} bytes)`;
|
||||
}
|
||||
}
|
||||
})
|
||||
: undefined;
|
||||
|
||||
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`)
|
||||
} as const;
|
||||
// Main logger object
|
||||
export const logger = pino({
|
||||
level: config.log.level,
|
||||
|
||||
function getCurrentTimestamp(): string {
|
||||
const date = new Date();
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0'); // * Months are 0-indexed
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
customLevels: {
|
||||
success: 35 // between INFO and WARN
|
||||
}
|
||||
}, pretty);
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
export function LOG_SUCCESS(input: string): void {
|
||||
input = `[${getCurrentTimestamp()}] [SUCCESS]: ${input}`;
|
||||
streams.success.write(`${input}\n`);
|
||||
|
||||
console.log(`${input}`.green.bold);
|
||||
}
|
||||
|
||||
export function LOG_ERROR(input: string): void {
|
||||
input = `[${getCurrentTimestamp()}] [ERROR]: ${input}`;
|
||||
streams.error.write(`${input}\n`);
|
||||
|
||||
console.error(`${input}`.red.bold);
|
||||
}
|
||||
|
||||
export function LOG_WARN(input: string): void {
|
||||
input = `[${getCurrentTimestamp()}] [WARN]: ${input}`;
|
||||
streams.warn.write(`${input}\n`);
|
||||
|
||||
console.log(`${input}`.yellow.bold);
|
||||
}
|
||||
|
||||
export function LOG_INFO(input: string): void {
|
||||
input = `[${getCurrentTimestamp()}] [INFO]: ${input}`;
|
||||
streams.info.write(`${input}\n`);
|
||||
|
||||
console.log(`${input}`.cyan.bold);
|
||||
}
|
||||
export const loggerHttp = pinoHttp({
|
||||
logger: logger
|
||||
});
|
||||
|
|
|
|||
14
src/middleware/host-limit.ts
Normal file
14
src/middleware/host-limit.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import type express from 'express';
|
||||
|
||||
export function restrictHostnames<TFn extends express.Router>(
|
||||
allowedHostnames: string[],
|
||||
fn: TFn
|
||||
): (request: express.Request, response: express.Response, next: () => void) => void | TFn {
|
||||
return (request: express.Request, response: express.Response, next: () => void) => {
|
||||
if (allowedHostnames.includes(request.hostname)) {
|
||||
return fn(request, response, next);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
}
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
import express from 'express';
|
||||
import morgan from 'morgan';
|
||||
import { connect as connectDatabase } from '@/database';
|
||||
import { startGRPCServer } from '@/services/grpc/server';
|
||||
import RequestException from '@/request-exception';
|
||||
import { LOG_INFO, LOG_SUCCESS } from '@/logger';
|
||||
import { logger, loggerHttp } from '@/logger';
|
||||
import { config } from '@/config-manager';
|
||||
import parseUserAgentMiddleware from '@/middleware/parse-user-agent';
|
||||
import authenticationMiddleware from '@/middleware/authentication';
|
||||
|
|
@ -21,8 +20,8 @@ process.on('SIGTERM', () => {
|
|||
|
||||
const app = express();
|
||||
|
||||
LOG_INFO('Setting up Middleware');
|
||||
app.use(morgan('dev'));
|
||||
logger.info('Setting up Middleware');
|
||||
app.use(loggerHttp);
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({
|
||||
extended: true
|
||||
|
|
@ -38,7 +37,7 @@ app.use(npfl);
|
|||
app.use(npdl);
|
||||
app.use(spr);
|
||||
|
||||
LOG_INFO('Creating 404 status handler');
|
||||
logger.info('Creating 404 status handler');
|
||||
app.use((_request, response) => {
|
||||
response.status(404);
|
||||
response.json({
|
||||
|
|
@ -48,7 +47,7 @@ app.use((_request, response) => {
|
|||
});
|
||||
});
|
||||
|
||||
LOG_INFO('Creating non-404 status handler');
|
||||
logger.info('Creating non-404 status handler');
|
||||
app.use((error: unknown, _request: express.Request, response: express.Response, _next: express.NextFunction) => {
|
||||
let status: number = 500;
|
||||
let message: string = 'Unknown error';
|
||||
|
|
@ -69,16 +68,16 @@ app.use((error: unknown, _request: express.Request, response: express.Response,
|
|||
});
|
||||
|
||||
async function main(): Promise<void> {
|
||||
LOG_INFO('Starting server');
|
||||
logger.info('Starting server');
|
||||
|
||||
await connectDatabase();
|
||||
LOG_SUCCESS('Database connected');
|
||||
logger.success('Database connected');
|
||||
|
||||
await startGRPCServer();
|
||||
LOG_SUCCESS(`gRPC server started at address ${config.grpc.boss.address}:${config.grpc.boss.port}`);
|
||||
logger.success(`gRPC server started at address ${config.grpc.boss.address}:${config.grpc.boss.port}`);
|
||||
|
||||
app.listen(config.http.port, () => {
|
||||
LOG_SUCCESS(`HTTP server started on port ${config.http.port}`);
|
||||
logger.success(`HTTP server started on port ${config.http.port}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import path from 'node:path';
|
||||
import express from 'express';
|
||||
import subdomain from 'express-subdomain';
|
||||
import { fileErrCallback } from '@/util';
|
||||
import { __appRoot } from '@/app-root';
|
||||
import { restrictHostnames } from '@/middleware/host-limit';
|
||||
import { config } from '@/config-manager';
|
||||
|
||||
const npdi = express.Router();
|
||||
|
||||
|
|
@ -22,6 +23,6 @@ npdi.get('/p01/data/1/:titleHash/:dataID/:fileHash', (request, response) => {
|
|||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(subdomain('npdi.cdn', npdi));
|
||||
router.use(restrictHostnames(config.domains.npdi, npdi));
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { Stream } from 'node:stream';
|
||||
import express from 'express';
|
||||
import subdomain from 'express-subdomain';
|
||||
import { getTaskFile } from '@/database';
|
||||
import { getCDNFileStream } from '@/util';
|
||||
import { LOG_ERROR } from '@/logger';
|
||||
import { logger } from '@/logger';
|
||||
import { config } from '@/config-manager';
|
||||
import { restrictHostnames } from '@/middleware/host-limit';
|
||||
|
||||
const npdl = express.Router();
|
||||
|
||||
|
|
@ -42,7 +43,7 @@ npdl.get([
|
|||
|
||||
Stream.pipeline(readStream, response, (err) => {
|
||||
if (err) {
|
||||
LOG_ERROR('Error with response stream: ' + err.message);
|
||||
logger.error('Error with response stream: ' + err.message);
|
||||
response.end();
|
||||
}
|
||||
});
|
||||
|
|
@ -50,6 +51,6 @@ npdl.get([
|
|||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(subdomain('npdl.cdn', npdl));
|
||||
router.use(restrictHostnames(config.domains.npdl, npdl));
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import crypto from 'node:crypto';
|
||||
import express from 'express';
|
||||
import subdomain from 'express-subdomain';
|
||||
import { getTaskFilesWithAttributes } from '@/database';
|
||||
import { restrictHostnames } from '@/middleware/host-limit';
|
||||
import { config } from '@/config-manager';
|
||||
|
||||
const ALLOWED_QUERY_PARMS = [
|
||||
'c', 'l',
|
||||
|
|
@ -109,6 +110,6 @@ npfl.get('/p01/filelist/:appID/:taskID', async (request: express.Request<{
|
|||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(subdomain('npfl.c.app', npfl));
|
||||
router.use(restrictHostnames(config.domains.npfl, npfl));
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import xmlbuilder from 'xmlbuilder';
|
||||
import moment from 'moment';
|
||||
import express from 'express';
|
||||
import subdomain from 'express-subdomain';
|
||||
import { disabledFeatures } from '@/config-manager';
|
||||
import { config, disabledFeatures } from '@/config-manager';
|
||||
import { restrictHostnames } from '@/middleware/host-limit';
|
||||
import type { PolicyList } from '@/types/common/policylist';
|
||||
|
||||
const nppl = express.Router();
|
||||
|
|
@ -183,7 +183,8 @@ function getWiiUPolicyList(countryCode: string, majorVersion: string): { PolicyL
|
|||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(subdomain('nppl.c.app', nppl)); // * 3DS
|
||||
router.use(subdomain('nppl.app', nppl)); // * WiiU
|
||||
// 3DS hosts on nppl.c.app
|
||||
// WiiU hosts on nppl.app
|
||||
router.use(restrictHostnames(config.domains.nppl, nppl));
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import path from 'node:path';
|
||||
import express from 'express';
|
||||
import subdomain from 'express-subdomain';
|
||||
import { fileErrCallback } from '@/util';
|
||||
import { __appRoot } from '@/app-root';
|
||||
import { config } from '@/config-manager';
|
||||
import { restrictHostnames } from '@/middleware/host-limit';
|
||||
|
||||
const npts = express.Router();
|
||||
|
||||
|
|
@ -30,6 +31,6 @@ npts.get('/p01/tasksheet/:id/:hash/:subfolder/:fileName', (request, response) =>
|
|||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(subdomain('npts.app', npts));
|
||||
router.use(restrictHostnames(config.domains.npts, npts));
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import crypto from 'node:crypto';
|
||||
import { Stream } from 'node:stream';
|
||||
import express from 'express';
|
||||
import subdomain from 'express-subdomain';
|
||||
import Dicer from 'dicer';
|
||||
import { getDuplicateCECData, getRandomCECData } from '@/database';
|
||||
import { getFriends } from '@/util';
|
||||
|
|
@ -9,8 +8,10 @@ import { CECData } from '@/models/cec-data';
|
|||
import { CECSlot } from '@/models/cec-slot';
|
||||
import { SendMode } from '@/types/common/spr-slot';
|
||||
import RequestException from '@/request-exception';
|
||||
import { config } from '@/config-manager';
|
||||
import { restrictHostnames } from '@/middleware/host-limit';
|
||||
import { logger } from '@/logger';
|
||||
import type { SPRSlot } from '@/types/common/spr-slot';
|
||||
import { LOG_WARN, LOG_INFO } from '@/logger';
|
||||
|
||||
const spr = express.Router();
|
||||
|
||||
|
|
@ -88,7 +89,7 @@ spr.post('/relay/0', multipartParser, async (request, response) => {
|
|||
|
||||
// * Check that the account is a 3DS and isn't banned
|
||||
if (!request.nexAccount.friendCode || request.nexAccount.accessLevel < 0) {
|
||||
LOG_INFO(`{request.pid}: User is not a 3DS or is banned`);
|
||||
logger.info(`{request.pid}: User is not a 3DS or is banned`);
|
||||
response.sendStatus(403);
|
||||
return;
|
||||
}
|
||||
|
|
@ -96,7 +97,7 @@ spr.post('/relay/0', multipartParser, async (request, response) => {
|
|||
const sprMetadataBuffer: Buffer | undefined = request.files['spr-meta'];
|
||||
|
||||
if (!sprMetadataBuffer) {
|
||||
LOG_WARN(`{request.pid}: Missing spr-meta file`);
|
||||
logger.warn(`{request.pid}: Missing spr-meta file`);
|
||||
response.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
|
@ -108,7 +109,7 @@ spr.post('/relay/0', multipartParser, async (request, response) => {
|
|||
const metadataHeaders = sprMetadata.split('\r\n'); // * Split header lines
|
||||
|
||||
if (metadataHeaders.length < 1) {
|
||||
LOG_WARN(`{request.pid}: spr-meta file is too short / empty`);
|
||||
logger.warn(`{request.pid}: spr-meta file is too short / empty`);
|
||||
response.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
|
@ -117,7 +118,7 @@ spr.post('/relay/0', multipartParser, async (request, response) => {
|
|||
const metadataHeader = metadataHeaders[i];
|
||||
const [header, value] = metadataHeader.split(': '); // * Split header and value
|
||||
if (!header || !value) {
|
||||
LOG_WARN(`{request.pid}: Bad spr-meta entry`);
|
||||
logger.warn(`{request.pid}: Bad spr-meta entry`);
|
||||
response.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
|
@ -126,7 +127,7 @@ spr.post('/relay/0', multipartParser, async (request, response) => {
|
|||
// * we can guarantee that i must match with the slot we are looking at except for 0, which will be the slotsize
|
||||
if (i === 0) {
|
||||
if (header !== 'slotsize') {
|
||||
LOG_WARN(`{request.pid}: spr-meta missing slotsize`);
|
||||
logger.warn(`{request.pid}: spr-meta missing slotsize`);
|
||||
response.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
|
@ -136,14 +137,14 @@ spr.post('/relay/0', multipartParser, async (request, response) => {
|
|||
try {
|
||||
slotsize = parseInt(value);
|
||||
} catch {
|
||||
LOG_WARN(`{request.pid}: Invalid spr-meta slotsize`);
|
||||
logger.warn(`{request.pid}: Invalid spr-meta slotsize`);
|
||||
response.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
||||
// * We don't count the slotsize header itself in the slot count
|
||||
if (slotsize !== (metadataHeaders.length - 1)) {
|
||||
LOG_WARN(`{request.pid}: Bad spr-meta slotsize`);
|
||||
logger.warn(`{request.pid}: Bad spr-meta slotsize`);
|
||||
response.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
|
@ -154,7 +155,7 @@ spr.post('/relay/0', multipartParser, async (request, response) => {
|
|||
const metadata = value.split(','); // * Split the value to get the metadata
|
||||
|
||||
if (metadata.length !== 3) {
|
||||
LOG_WARN(`{request.pid}: Bad spr-meta entry param count`);
|
||||
logger.warn(`{request.pid}: Bad spr-meta entry param count`);
|
||||
response.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
|
@ -167,7 +168,7 @@ spr.post('/relay/0', multipartParser, async (request, response) => {
|
|||
gameID = parseInt(metadata[1], 16);
|
||||
size = parseInt(metadata[2]);
|
||||
} catch {
|
||||
LOG_WARN(`{request.pid}: Invalid spr-meta entry params`);
|
||||
logger.warn(`{request.pid}: Invalid spr-meta entry params`);
|
||||
response.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
|
@ -178,13 +179,13 @@ spr.post('/relay/0', multipartParser, async (request, response) => {
|
|||
const slotData: Buffer | undefined = request.files['spr-slot' + slot];
|
||||
|
||||
if (!slotData) {
|
||||
LOG_WARN(`{request.pid}: Missing slot data file`);
|
||||
logger.warn(`{request.pid}: Missing slot data file`);
|
||||
response.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (slotData.length !== size) {
|
||||
LOG_WARN(`{request.pid}: Invalid slot data size`);
|
||||
logger.warn(`{request.pid}: Invalid slot data size`);
|
||||
response.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
|
@ -199,25 +200,25 @@ spr.post('/relay/0', multipartParser, async (request, response) => {
|
|||
|
||||
// * Check that we at least have enough size for the StreetPass header
|
||||
if (slotData.length < 0x12) {
|
||||
LOG_WARN(`{request.pid}: Slot is too short`);
|
||||
logger.warn(`{request.pid}: Slot is too short`);
|
||||
response.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (slotData.readUInt32LE() !== 0x6161) {
|
||||
LOG_WARN(`{request.pid}: Slot header missmatch`);
|
||||
logger.warn(`{request.pid}: Slot header missmatch`);
|
||||
response.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (slotData.readUInt32LE(4) !== size) {
|
||||
LOG_WARN(`{request.pid}: Slot bad size`);
|
||||
logger.warn(`{request.pid}: Slot bad size`);
|
||||
response.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (slotData.readUInt32LE(8) !== gameID) {
|
||||
LOG_WARN(`{request.pid}: Slot bad gameID`);
|
||||
logger.warn(`{request.pid}: Slot bad gameID`);
|
||||
response.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
|
@ -296,6 +297,6 @@ spr.post('/relay/0', multipartParser, async (request, response) => {
|
|||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(subdomain('service.spr.app', spr));
|
||||
router.use(restrictHostnames(config.domains.spr, spr));
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
import type mongoose from 'mongoose';
|
||||
|
||||
export interface DisabledFeatures {
|
||||
s3: boolean;
|
||||
spr: boolean;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
http: {
|
||||
port: number;
|
||||
};
|
||||
crypto: {
|
||||
wup: {
|
||||
aes_key: string;
|
||||
hmac_key: string;
|
||||
};
|
||||
ctr: {
|
||||
aes_key: Buffer;
|
||||
};
|
||||
};
|
||||
grpc: {
|
||||
boss: {
|
||||
address: string;
|
||||
port: number;
|
||||
api_key: string;
|
||||
};
|
||||
account: {
|
||||
address: string;
|
||||
port: number;
|
||||
api_key: string;
|
||||
};
|
||||
friends: {
|
||||
address: string;
|
||||
port: number;
|
||||
api_key: string;
|
||||
};
|
||||
};
|
||||
mongoose: {
|
||||
connection_string: string;
|
||||
options: mongoose.ConnectOptions;
|
||||
};
|
||||
cdn: {
|
||||
download_url: string;
|
||||
s3: {
|
||||
endpoint: string;
|
||||
region: string;
|
||||
bucket: string;
|
||||
key: string;
|
||||
secret: string;
|
||||
};
|
||||
disk_path: string;
|
||||
};
|
||||
spr: {
|
||||
enabled: boolean;
|
||||
};
|
||||
}
|
||||
16
src/types/express-subdomain.d.ts
vendored
16
src/types/express-subdomain.d.ts
vendored
|
|
@ -1,16 +0,0 @@
|
|||
// * Credit to https://github.com/bmullan91/express-subdomain/pull/61 for the types!
|
||||
|
||||
declare module 'express-subdomain' {
|
||||
import type { Request, Response, Router } from 'express';
|
||||
|
||||
/**
|
||||
* @description The subdomain function.
|
||||
* @param subdomain The subdomain to listen on.
|
||||
* @param fn The listener function, takes a response and request.
|
||||
* @returns A function call to the value passed as FN, or void (the next function).
|
||||
*/
|
||||
export default function subdomain(
|
||||
subdomain: string,
|
||||
fn: Router | ((req: Request, res: Response) => void | any)
|
||||
): (req: Request, res: Response, next: () => void) => void | typeof fn;
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ import { GetObjectCommand, PutObjectCommand, S3 } from '@aws-sdk/client-s3';
|
|||
import { AccountDefinition } from '@pretendonetwork/grpc/account/account_service';
|
||||
import { FriendsDefinition } from '@pretendonetwork/grpc/friends/friends_service';
|
||||
import { config, disabledFeatures } from '@/config-manager';
|
||||
import { LOG_ERROR } from '@/logger';
|
||||
import { logger } from './logger';
|
||||
import type { FriendsClient } from '@pretendonetwork/grpc/friends/friends_service';
|
||||
import type { AccountClient } from '@pretendonetwork/grpc/account/account_service';
|
||||
import type { S3Client } from '@aws-sdk/client-s3';
|
||||
|
|
@ -72,7 +72,7 @@ export function fileErrCallback(response: Response) {
|
|||
if (!response.headersSent) {
|
||||
response.status(500).send('Server Error');
|
||||
}
|
||||
LOG_ERROR('Error in sending file: ' + err.message);
|
||||
logger.error('Error in sending file: ' + err.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user