Merge pull request #26 from PretendoNetwork/feat/logger-and-config
Some checks failed
Build and Publish Docker Image / Build and Publish Docker Image (amd64) (push) Has been cancelled
Build and Publish Docker Image / Build and Publish Docker Image (arm64) (push) Has been cancelled

Add pino logger + add domain configuration
This commit is contained in:
mrjvs 2025-09-03 13:51:43 +02:00 committed by GitHub
commit 1112c135e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 677 additions and 1139 deletions

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

@ -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);

View File

@ -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:'));

View File

@ -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
});

View 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();
};
}

View File

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

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
};
}

View File

@ -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;
}

View File

@ -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);
}
}
};