Convert to TypeScript

This commit is contained in:
Jonathan Barrow 2023-08-17 22:32:40 -04:00
parent 3b5aeb222c
commit 38739fef7e
No known key found for this signature in database
GPG Key ID: E86E9FE9049C741F
26 changed files with 4247 additions and 445 deletions

View File

@ -1 +1,2 @@
node_modules
dist
*.js

View File

@ -1,43 +0,0 @@
{
"env": {
"node": true,
"commonjs": true,
"es6": true
},
"parserOptions": {
"ecmaVersion": 2020
},
"globals": {
"BigInt": true
},
"extends": "eslint:recommended",
"rules": {
"require-atomic-updates": "warn",
"no-case-declarations": "off",
"no-empty": "off",
"no-console": "off",
"linebreak-style": "off",
"no-global-assign": "off",
"prefer-const": "error",
"no-var": "error",
"one-var": [
"error",
"never"
],
"indent": [
"error",
"tab",
{
"SwitchCase": 1
}
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
]
}
}

60
.eslintrc.json Normal file
View File

@ -0,0 +1,60 @@
{
"env": {
"node": true,
"commonjs": true,
"es6": true
},
"parser": "@typescript-eslint/parser",
"globals": {
"BigInt": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"plugins": [
"@typescript-eslint"
],
"rules": {
"require-atomic-updates": "warn",
"no-case-declarations": "off",
"no-empty": "off",
"no-console": "off",
"linebreak-style": "off",
"no-global-assign": "off",
"prefer-const": "error",
"no-var": "error",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"no-extra-semi": "off",
"@typescript-eslint/no-extra-semi": "error",
"@typescript-eslint/no-empty-interface": "warn",
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/typedef": "error",
"@typescript-eslint/explicit-function-return-type": "error",
"@typescript-eslint/no-explicit-any": "warn",
"keyword-spacing": "off",
"@typescript-eslint/keyword-spacing": "error",
"curly": "error",
"brace-style": "error",
"one-var": [
"error",
"never"
],
"indent": [
"error",
"tab",
{
"SwitchCase": 1
}
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
]
}
}

2
.gitignore vendored
View File

@ -59,4 +59,4 @@ typings/
# custom
.vscode
config.json
dist

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<TaskSheet>
<TitleId>0005003010016100</TitleId>
<TitleId>0005003010016200</TitleId>
<TaskId>olvinfo</TaskId>
<ServiceStatus>open</ServiceStatus>
<Files>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<TaskSheet>
<TitleId>0005003010016100</TitleId>
<TitleId>0005003010016000</TitleId>
<TaskId>olvinfo</TaskId>
<ServiceStatus>open</ServiceStatus>
<Files>

View File

@ -8,8 +8,8 @@
<Filename>Festival.byaml</Filename>
<DataId>1490</DataId>
<Type>AppData</Type>
<Url>https://npdi.cdn.pretendo.cc/p01/data/1/bb6tOEckvgZ50ciH/1490/3d12d0e157820defea888023e06fbc3d</Url>
<Size>18784</Size>
<Url>https://npdi.cdn.pretendo.cc/p01/data/1/bb6tOEckvgZ50ciH/1490/2ff2eebe2d8a9c2d93eb289ef27449cf</Url>
<Size>17924</Size>
<Notify>
<New>app</New>
<LED>false</LED>
@ -19,8 +19,8 @@
<Filename>HapTexture.bfres</Filename>
<DataId>1491</DataId>
<Type>AppData</Type>
<Url>https://npdi.cdn.pretendo.cc/p01/data/1/bb6tOEckvgZ50ciH/1491/fdb1f5522d7e8247fd5308fa7f0d5b76</Url>
<Size>361536</Size>
<Url>https://npdi.cdn.pretendo.cc/p01/data/1/bb6tOEckvgZ50ciH/1491/c05ad953c1111bf2dbbe0a191a401e3e</Url>
<Size>364608</Size>
<Notify>
<New>app</New>
<LED>false</LED>
@ -30,8 +30,8 @@
<Filename>PanelTexture.bfres</Filename>
<DataId>1492</DataId>
<Type>AppData</Type>
<Url>https://npdi.cdn.pretendo.cc/p01/data/1/bb6tOEckvgZ50ciH/1492/d6b6ed30ea52d4b176e2656d7dc94e42</Url>
<Size>475712</Size>
<Url>https://npdi.cdn.pretendo.cc/p01/data/1/bb6tOEckvgZ50ciH/1492/fa99f04d9bb81cf6205c76c479852916</Url>
<Size>479296</Size>
<Notify>
<New>app</New>
<LED>false</LED>

View File

@ -8,8 +8,8 @@
<Filename>Festival.byaml</Filename>
<DataId>1348</DataId>
<Type>AppData</Type>
<Url>https://npdi.cdn.pretendo.cc/p01/data/1/rjVlM7hUXPxmYQJh/1348/bee225fded2f9f4adae8f58c640ca9ae</Url>
<Size>18784</Size>
<Url>https://npdi.cdn.pretendo.cc/p01/data/1/rjVlM7hUXPxmYQJh/1348/6d2b6c4df1a666832452385cbc7f77f5</Url>
<Size>24304</Size>
<Notify>
<New>app</New>
<LED>false</LED>
@ -19,8 +19,8 @@
<Filename>HapTexture.bfres</Filename>
<DataId>1347</DataId>
<Type>AppData</Type>
<Url>https://npdi.cdn.pretendo.cc/p01/data/1/rjVlM7hUXPxmYQJh/1347/73d21a6ec3f893b61671f34941bde0a4</Url>
<Size>361536</Size>
<Url>https://npdi.cdn.pretendo.cc/p01/data/1/rjVlM7hUXPxmYQJh/1347/734e5be62755592478fc5e8ca1cc34cd</Url>
<Size>364608</Size>
<Notify>
<New>app</New>
<LED>false</LED>
@ -30,8 +30,8 @@
<Filename>PanelTexture.bfres</Filename>
<DataId>1349</DataId>
<Type>AppData</Type>
<Url>https://npdi.cdn.pretendo.cc/p01/data/1/rjVlM7hUXPxmYQJh/1349/ad76fc5663bc6d27f85d8497970f6b14</Url>
<Size>475712</Size>
<Url>https://npdi.cdn.pretendo.cc/p01/data/1/rjVlM7hUXPxmYQJh/1349/2283065f6cb4c57bf6881de3e7afe37f</Url>
<Size>479296</Size>
<Notify>
<New>app</New>
<LED>false</LED>

View File

@ -8,8 +8,8 @@
<Filename>Festival.byaml</Filename>
<DataId>1357</DataId>
<Type>AppData</Type>
<Url>https://npdi.cdn.pretendo.cc/p01/data/1/zvGSM4kOrXpkKnpT/1357/a5eee49b5c57b9749b4cd61b2c18ec75</Url>
<Size>18784</Size>
<Url>https://npdi.cdn.pretendo.cc/p01/data/1/zvGSM4kOrXpkKnpT/1357/7faa9748451ccee23eaabd1dfd19c982</Url>
<Size>23184</Size>
<Notify>
<New>app</New>
<LED>false</LED>
@ -19,8 +19,8 @@
<Filename>HapTexture.bfres</Filename>
<DataId>1358</DataId>
<Type>AppData</Type>
<Url>https://npdi.cdn.pretendo.cc/p01/data/1/zvGSM4kOrXpkKnpT/1358/e08b705678a3cea02d38f89b569fb971</Url>
<Size>361536</Size>
<Url>https://npdi.cdn.pretendo.cc/p01/data/1/zvGSM4kOrXpkKnpT/1358/827a01e39f4cd9208b230f5b74d7ede6</Url>
<Size>364608</Size>
<Notify>
<New>app</New>
<LED>false</LED>
@ -30,8 +30,8 @@
<Filename>PanelTexture.bfres</Filename>
<DataId>1356</DataId>
<Type>AppData</Type>
<Url>https://npdi.cdn.pretendo.cc/p01/data/1/zvGSM4kOrXpkKnpT/1356/df0e9021d339ab02880120d273e2ab83</Url>
<Size>475712</Size>
<Url>https://npdi.cdn.pretendo.cc/p01/data/1/zvGSM4kOrXpkKnpT/1356/f30e8e623db8cd8f5eb4005d35d7c8bc</Url>
<Size>479296</Size>
<Notify>
<New>app</New>
<LED>false</LED>

View File

@ -4,8 +4,7 @@ const fs = require('fs-extra');
const crypto = require('crypto');
require('dotenv').config();
// PROVIDE THESE KEYS YOURSELF
const { BOSS_AES_KEY, BOSS_HMAC_KEY } = process.env;
const { PN_BOSS_CONFIG_BOSS_WIIU_AES_KEY, PN_BOSS_CONFIG_BOSS_WIIU_HMAC_KEY } = process.env;
const decryptedFilePath = path.normalize(path.resolve(__dirname, process.argv[2]));
const encryptedFolderName = path.basename(path.dirname(decryptedFilePath));
@ -13,7 +12,7 @@ const encryptedFolderPath = path.normalize(path.resolve(decryptedFilePath, '../.
fs.ensureDirSync(encryptedFolderPath);
const encryptedContents = BOSS.encryptWiiU(decryptedFilePath, BOSS_AES_KEY, BOSS_HMAC_KEY);
const encryptedContents = BOSS.encryptWiiU(decryptedFilePath, PN_BOSS_CONFIG_BOSS_WIIU_AES_KEY, PN_BOSS_CONFIG_BOSS_WIIU_HMAC_KEY);
const hash = crypto.createHash('md5').update(encryptedContents).digest('hex');
const encryptedFilePath = path.normalize(path.resolve(encryptedFolderPath, hash));

View File

@ -1,5 +0,0 @@
{
"http": {
"port": 8080
}
}

4162
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,13 @@
{
"name": "BOSS",
"name": "boss",
"version": "1.0.0",
"description": "",
"main": "./src/server.js",
"main": "dist/server.js",
"scripts": {
"start": "node ."
"lint": "npx eslint .",
"build": "npm run lint && npm run clean && npx tsc && npx tsc-alias",
"clean": "rimraf ./dist",
"start": "node --enable-source-maps ."
},
"keywords": [],
"author": "",
@ -18,8 +21,17 @@
"morgan": "^1.10.0"
},
"devDependencies": {
"@types/express": "^4.17.17",
"@types/fs-extra": "^11.0.1",
"@types/morgan": "^1.9.4",
"@types/node": "^20.5.0",
"@typescript-eslint/eslint-plugin": "^6.4.0",
"@typescript-eslint/parser": "^6.4.0",
"axios": "^0.27.2",
"eslint": "^8.47.0",
"fs-extra": "^10.1.0",
"tsc-alias": "^1.8.7",
"typescript": "^5.1.6",
"xmlbuilder2": "^3.0.2"
}
}

View File

@ -65,6 +65,14 @@ const TASKSHEETS = [
id: 'VFoY6V7u7UUq1EG5',
files: ['olvinfo']
},
{
id: 'WJDaV6ePVgrS0TRa',
files: ['olvinfo']
},
{
id: '8MNOVprfNVAJjfCM',
files: ['olvinfo']
},
{
id: '07E3nY6lAwlwrQRo',
files: ['wood1', 'woodBGM']

49
src/config-manager.ts Normal file
View File

@ -0,0 +1,49 @@
import dotenv from 'dotenv';
import { md5 } from '@/util';
import { LOG_INFO, LOG_ERROR } from '@/logger';
import { Config } from '@/types/common/config';
dotenv.config();
const BOSS_WIIU_AES_KEY_HASH = Buffer.from('5202ce5099232c3d365e28379790a919', 'hex');
const BOSS_WIIU_HMAC_KEY_HASH = Buffer.from('b4482fef177b0100090ce0dbeb8ce977', 'hex');
const BOSS_3DS_AES_KEY_HASH = Buffer.from('86fbc2bb4cb703b2a4c6cc9961319926', 'hex');
LOG_INFO('Loading config');
export const config: Config = {
http: {
port: Number(process.env.PN_BOSS_CONFIG_HTTP_PORT || '')
},
crypto: {
wup: {
aes_key: Buffer.from(process.env.PN_BOSS_CONFIG_BOSS_WIIU_AES_KEY || ''),
hmac_key: Buffer.from(process.env.PN_BOSS_CONFIG_BOSS_WIIU_HMAC_KEY || ''),
},
ctr: {
aes_key: Buffer.from(process.env.PN_BOSS_CONFIG_BOSS_3DS_AES_KEY || '', 'hex')
}
}
};
LOG_INFO('Config loaded, checking integrity');
if (!config.http.port) {
LOG_ERROR('Failed to find HTTP port. Set the PN_BOSS_CONFIG_HTTP_PORT environment variable');
process.exit(0);
}
if (!BOSS_WIIU_AES_KEY_HASH.equals(md5(config.crypto.wup.aes_key))) {
LOG_ERROR('Invalid BOSS WiiU AES key. Set or correct the PN_BOSS_CONFIG_BOSS_WIIU_AES_KEY environment variable');
process.exit(0);
}
if (!BOSS_WIIU_HMAC_KEY_HASH.equals(md5(config.crypto.wup.hmac_key))) {
LOG_ERROR('Invalid BOSS WiiU HMAC key. Set or correct the PN_BOSS_CONFIG_BOSS_WIIU_HMAC_KEY environment variable');
process.exit(0);
}
if (!BOSS_3DS_AES_KEY_HASH.equals(md5(config.crypto.ctr.aes_key))) {
LOG_ERROR('Invalid BOSS 3DS AES key. Set or correct the PN_BOSS_CONFIG_BOSS_3DS_AES_KEY environment variable');
process.exit(0);
}

47
src/logger.ts Normal file
View File

@ -0,0 +1,47 @@
import fs from 'fs-extra';
import colors from 'colors';
colors.enable();
const root = process.env.PN_BOSS_CONFIG_LOGGER_PATH ? process.env.PN_BOSS_CONFIG_LOGGER_PATH : `${__dirname}/..`;
fs.ensureDirSync(`${root}/logs`);
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;
export function LOG_SUCCESS(input: string): void {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [SUCCESS]: ${input}`;
streams.success.write(`${input}\n`);
console.log(`${input}`.green.bold);
}
export function LOG_ERROR(input: string): void {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [ERROR]: ${input}`;
streams.error.write(`${input}\n`);
console.log(`${input}`.red.bold);
}
export function LOG_WARN(input: string): void {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [WARN]: ${input}`;
streams.warn.write(`${input}\n`);
console.log(`${input}`.yellow.bold);
}
export function LOG_INFO(input: string): void {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [INFO]: ${input}`;
streams.info.write(`${input}\n`);
console.log(`${input}`.cyan.bold);
}

View File

@ -1,60 +0,0 @@
process.title = 'Pretendo - BOSS';
const express = require('express');
const morgan = require('morgan');
const logger = require('../logger');
const config = require('../config.json');
const { http: { port } } = config;
const app = express();
const nptsService = require('./services/npts');
const npplService = require('./services/nppl');
const npdiService = require('./services/npdi');
// START APPLICATION
app.set('etag', false);
app.disable('x-powered-by');
// Create router
logger.info('Setting up Middleware');
app.use(morgan('dev'));
app.use(express.json());
app.use(express.urlencoded({
extended: true
}));
app.use(nptsService);
app.use(npplService);
app.use(npdiService);
// 404 handler
logger.info('Creating 404 status handler');
app.use((request, response) => {
response.status(404);
response.json({
app: 'api',
status: 404,
error: 'Route not found'
});
});
// non-404 error handler
logger.info('Creating non-404 status handler');
app.use((error, request, response) => {
const status = error.status || 500;
response.status(status);
response.json({
app: 'api',
status,
error: error.message
});
});
// Starts the server
logger.info('Starting server');
app.listen(port, () => {
logger.success(`Server started on port ${port}`);
});

53
src/server.ts Normal file
View File

@ -0,0 +1,53 @@
process.title = 'Pretendo - BOSS';
import express from 'express';
import morgan from 'morgan';
import { LOG_INFO, LOG_SUCCESS } from '@/logger';
import { config } from '@/config-manager';
import nppl from '@/services/nppl';
import npts from '@/services/npts';
import npdi from '@/services/npdi';
const app = express();
app.set('etag', false);
app.disable('x-powered-by');
LOG_INFO('Setting up Middleware');
app.use(morgan('dev'));
app.use(express.json());
app.use(express.urlencoded({
extended: true
}));
app.use(nppl);
app.use(npts);
app.use(npdi);
LOG_INFO('Creating 404 status handler');
app.use((_request, response) => {
response.status(404);
response.json({
app: 'api',
status: 404,
error: 'Route not found'
});
});
LOG_INFO('Creating non-404 status handler');
app.use((error: any, _request: express.Request, response: express.Response, _next: express.NextFunction) => {
const status: number = error.status || 500;
response.status(status);
response.json({
app: 'boss',
status: status,
error: error
});
});
LOG_INFO('Starting server');
app.listen(config.http.port, () => {
LOG_SUCCESS(`Server started on port ${config.http.port}`);
});

View File

@ -1,12 +1,10 @@
const path = require('path');
const fs = require('fs-extra');
const express = require('express');
const subdomain = require('express-subdomain');
import path from 'node:path';
import fs from 'fs-extra';
import express from 'express';
import subdomain from 'express-subdomain';
// Router to handle the subdomain restriction
const npdi = express.Router();
// Setup routes
npdi.get('/p01/data/1/:titleHash/:dataID/:fileHash', (request, response) => {
const { titleHash, fileHash } = request.params;
const contentPath = path.normalize(`${__dirname}/../../cdn/content/encrypted/${titleHash}/${fileHash}`);
@ -22,10 +20,8 @@ npdi.get('/p01/data/1/:titleHash/:dataID/:fileHash', (request, response) => {
}
});
// Main router for endpoints
const router = express.Router();
// Create subdomain
router.use(subdomain('npdi.cdn', npdi));
module.exports = router;
export default router;

View File

@ -1,28 +0,0 @@
const path = require('path');
const fs = require('fs-extra');
const express = require('express');
const subdomain = require('express-subdomain');
// Router to handle the subdomain restriction
const npts = express.Router();
// Setup routes
npts.get('/p01/policylist/1/1/:region', (request, response) => {
//const { region } = request.params;
const policylistPath = path.normalize(`${__dirname}/../../cdn/policylist/policylist.xml`);
if (fs.existsSync(policylistPath)) {
response.set('Content-Type', 'application/xml; charset=utf-8');
response.sendFile(policylistPath);
} else {
response.sendStatus(404);
}
});
// Main router for endpoints
const router = express.Router();
// Create subdomain
router.use(subdomain('nppl.app', npts));
module.exports = router;

23
src/services/nppl.ts Normal file
View File

@ -0,0 +1,23 @@
import path from 'node:path';
import fs from 'fs-extra';
import express from 'express';
import subdomain from 'express-subdomain';
const nppl = express.Router();
nppl.get('/p01/policylist/1/1/:region', (_request, response) => {
const policylistPath = path.normalize(`${__dirname}/../../cdn/policylist/policylist.xml`);
if (fs.existsSync(policylistPath)) {
response.set('Content-Type', 'application/xml; charset=utf-8');
response.sendFile(policylistPath);
} else {
response.sendStatus(404);
}
});
const router = express.Router();
router.use(subdomain('nppl.app', nppl));
export default router;

View File

@ -1,12 +1,10 @@
const path = require('path');
const fs = require('fs-extra');
const express = require('express');
const subdomain = require('express-subdomain');
import path from 'node:path';
import fs from 'fs-extra';
import express from 'express';
import subdomain from 'express-subdomain';
// Router to handle the subdomain restriction
const npts = express.Router();
// Setup routes
npts.get('/p01/tasksheet/:id/:hash/:fileName', (request, response) => {
const { id, hash, fileName } = request.params;
const tasksheetPath = path.normalize(`${__dirname}/../../cdn/tasksheet/${id}/${hash}/${fileName}`);
@ -31,10 +29,8 @@ npts.get('/p01/tasksheet/:id/:hash/:subfolder/:fileName', (request, response) =>
}
});
// Main router for endpoints
const router = express.Router();
// Create subdomain
router.use(subdomain('npts.app', npts));
module.exports = router;
export default router;

View File

@ -0,0 +1,14 @@
export interface Config {
http: {
port: number;
};
crypto: {
wup: {
aes_key: Buffer;
hmac_key: Buffer;
};
ctr: {
aes_key: Buffer;
};
};
}

16
src/types/express-subdomain.d.ts vendored Normal file
View File

@ -0,0 +1,16 @@
// * 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;
}

5
src/util.ts Normal file
View File

@ -0,0 +1,5 @@
import crypto from 'node:crypto';
export function md5(input: crypto.BinaryLike): Buffer {
return crypto.createHash('md5').update(input).digest();
}

23
tsconfig.json Normal file
View File

@ -0,0 +1,23 @@
{
"compilerOptions": {
"strict": true,
"sourceMap": true,
"resolveJsonModule": true,
"module": "commonjs",
"esModuleInterop": true,
"moduleResolution": "node",
"baseUrl": "src",
"outDir": "dist",
"allowJs": true,
"target": "es2022",
"noEmitOnError": true,
"paths": {
"@/*": ["./*"]
},
"typeRoots": [
"node_modules/@types",
"node_modules/@hcaptcha"
]
},
"include": ["src"]
}