diff --git a/package-lock.json b/package-lock.json index dfbbb63..230fa8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,9 +14,9 @@ "@pretendonetwork/grpc": "^1.0.6", "@typegoose/auto-increment": "^4.13.0", "commander": "^14.0.0", - "dicer": "^0.3.1", "dotenv": "^16.4.7", "express": "^5.1.0", + "formidable": "^3.5.4", "fs-extra": "^11.2.0", "moment": "^2.30.1", "mongoose": "^8.18.1", @@ -33,6 +33,7 @@ "@smithy/types": "^4.0.0", "@types/dicer": "^0.2.4", "@types/express": "^4.17.21", + "@types/formidable": "^3.4.5", "@types/fs-extra": "^11.0.4", "@types/node": "^22.10.5", "axios": "^1.7.9", @@ -1917,6 +1918,18 @@ "@tybys/wasm-util": "^0.10.0" } }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2000,6 +2013,15 @@ "node": ">=8.0" } }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -3198,6 +3220,16 @@ "@types/send": "*" } }, + "node_modules/@types/formidable": { + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/@types/formidable/-/formidable-3.4.5.tgz", + "integrity": "sha512-s7YPsNVfnsng5L8sKnG/Gbb2tiwwJTY1conOkJzTMRvJAlLFW1nEua+ADsJQu8N1c0oTHx9+d5nqg10WuT9gHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/fs-extra": { "version": "11.0.4", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", @@ -4187,6 +4219,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "license": "MIT" + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -4722,15 +4760,14 @@ "node": ">= 0.8" } }, - "node_modules/dicer": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.1.tgz", - "integrity": "sha512-ObioMtXnmjYs3aRtpIJt9rgQSPCIhKVkFPip+E9GUDyWl8N435znUxK/JfNwGZJ2wnn5JKQ7Ly3vOK5Q5dylGA==", + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "license": "ISC", "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.0.0" + "asap": "^2.0.0", + "wrappy": "1" } }, "node_modules/doctrine": { @@ -5997,6 +6034,23 @@ "node": ">= 6" } }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -8749,14 +8803,6 @@ "node": ">= 0.4" } }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -11123,6 +11169,11 @@ "@tybys/wasm-util": "^0.10.0" } }, + "@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==" + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -11185,6 +11236,14 @@ "integrity": "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ==", "dev": true }, + "@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "requires": { + "@noble/hashes": "^1.1.5" + } + }, "@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -12051,6 +12110,15 @@ "@types/send": "*" } }, + "@types/formidable": { + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/@types/formidable/-/formidable-3.4.5.tgz", + "integrity": "sha512-s7YPsNVfnsng5L8sKnG/Gbb2tiwwJTY1conOkJzTMRvJAlLFW1nEua+ADsJQu8N1c0oTHx9+d5nqg10WuT9gHQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/fs-extra": { "version": "11.0.4", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", @@ -12671,6 +12739,11 @@ "is-array-buffer": "^3.0.4" } }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + }, "async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -13025,12 +13098,13 @@ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" }, - "dicer": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.1.tgz", - "integrity": "sha512-ObioMtXnmjYs3aRtpIJt9rgQSPCIhKVkFPip+E9GUDyWl8N435znUxK/JfNwGZJ2wnn5JKQ7Ly3vOK5Q5dylGA==", + "dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", "requires": { - "streamsearch": "^1.1.0" + "asap": "^2.0.0", + "wrappy": "1" } }, "doctrine": { @@ -13935,6 +14009,16 @@ "mime-types": "^2.1.12" } }, + "formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "requires": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + } + }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -15747,11 +15831,6 @@ "internal-slot": "^1.1.0" } }, - "streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" - }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", diff --git a/package.json b/package.json index 03e34ca..82775c6 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,9 @@ "@pretendonetwork/grpc": "^1.0.6", "@typegoose/auto-increment": "^4.13.0", "commander": "^14.0.0", - "dicer": "^0.3.1", "dotenv": "^16.4.7", "express": "^5.1.0", + "formidable": "^3.5.4", "fs-extra": "^11.2.0", "moment": "^2.30.1", "mongoose": "^8.18.1", @@ -37,6 +37,7 @@ "@smithy/types": "^4.0.0", "@types/dicer": "^0.2.4", "@types/express": "^4.17.21", + "@types/formidable": "^3.4.5", "@types/fs-extra": "^11.0.4", "@types/node": "^22.10.5", "axios": "^1.7.9", diff --git a/src/services/spr.ts b/src/services/spr.ts index 57dd1e1..370ed17 100644 --- a/src/services/spr.ts +++ b/src/services/spr.ts @@ -1,87 +1,38 @@ import crypto from 'node:crypto'; -import { Stream } from 'node:stream'; +import { readFile } from 'node:fs/promises'; +import { formidable } from 'formidable'; import express from 'express'; -import Dicer from 'dicer'; import { getDuplicateCECData, getRandomCECData } from '@/database'; import { getFriends } from '@/util'; 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 { getCDNFileAsBuffer, uploadCDNFile } from '@/cdn'; +import RequestException from '@/request-exception'; +import type { File } from 'formidable'; +import type { Request } from 'express'; import type { SPRSlot } from '@/types/common/spr-slot'; const spr = express.Router(); -function multipartParser(request: express.Request, response: express.Response, next: express.NextFunction): void { - const RE_BOUNDARY = /^multipart\/.+?(?:; boundary=(?:(?:"(.+)")|(?:([^\s]+))))$/i; - const RE_FILE_NAME = /name="(.*)"/; - - const contentType = request.header('content-type'); - - if (!contentType) { - return next(); - } - - const boundary = RE_BOUNDARY.exec(contentType); - - if (!boundary) { - return next(); - } - - const dicer = new Dicer({ boundary: boundary[1] || boundary[2] }); - const files: Record = {}; - - dicer.on('part', (part: Dicer.PartStream) => { - let fileBuffer = Buffer.alloc(0); - let fileName = ''; - - part.on('header', (header) => { - const contentDisposition = header['content-disposition' as keyof object]; - const regexResult = RE_FILE_NAME.exec(contentDisposition); - - if (regexResult) { - fileName = regexResult[1]; - } - }); - - part.on('data', (data: Buffer | string) => { - if (typeof data === 'string') { - data = Buffer.from(data); - } - - fileBuffer = Buffer.concat([fileBuffer, data]); - }); - - part.on('end', () => { - files[fileName] = fileBuffer; - }); - - part.on('error', (error: Error) => { - return next(new RequestException(error.message, 400)); - }); +async function parseMultipart(request: Request): Promise> { + const form = formidable({ + multiples: false + }); + const parsedForm = await form.parse(request).catch((err: Error) => { + throw new RequestException(err.message, 400); }); - dicer.on('finish', function () { - request.files = files; - return next(); - }); - - Stream.pipeline(request, dicer, (error: Error | null) => { - if (error) { - return next(new RequestException(error.message, 400)); - } - }); + const entries = Object.entries(parsedForm[1]); + const entriesWithSinglefile = entries.map(v => [v[0], (v[1] ?? [])[0]]); + return Object.fromEntries(entriesWithSinglefile); } -spr.post('/relay/0', multipartParser, async (request, response) => { - if (!request.files) { - response.sendStatus(400); - return; - } +spr.post('/relay/0', async (request, response) => { + const files = await parseMultipart(request); if (!request.pid || !request.nexAccount) { response.sendStatus(401); @@ -90,15 +41,15 @@ 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) { - logger.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; } - const sprMetadataBuffer: Buffer | undefined = request.files['spr-meta']; + const sprMetadataFile: File | undefined = files['spr-meta']; - if (!sprMetadataBuffer) { - logger.warn(`{request.pid}: Missing spr-meta file`); + if (!sprMetadataFile) { + logger.warn(`${request.pid}: Missing spr-meta file`); response.sendStatus(400); return; } @@ -106,7 +57,7 @@ spr.post('/relay/0', multipartParser, async (request, response) => { const sprSlots: SPRSlot[] = []; // * Check spr-meta metadata headers - const sprMetadata = sprMetadataBuffer.toString(); + const sprMetadata = await readFile(sprMetadataFile.filepath, 'utf-8'); const metadataHeaders = sprMetadata.split('\r\n'); // * Split header lines if (metadataHeaders.length < 1) { @@ -177,15 +128,15 @@ spr.post('/relay/0', multipartParser, async (request, response) => { let data: Buffer = Buffer.alloc(0); if (size > 0 && sendMode !== SendMode.RecvOnly) { const slot = i.toString().padStart(2, '0'); - const slotData: Buffer | undefined = request.files['spr-slot' + slot]; + const slotDataFile: File | undefined = files['spr-slot' + slot]; - if (!slotData) { + if (!slotDataFile) { logger.warn(`${request.pid}: Missing slot data file`); response.sendStatus(400); return; } - if (slotData.length !== size) { + if (slotDataFile.size !== size) { logger.warn(`${request.pid}: Invalid slot data size`); response.sendStatus(400); return; @@ -200,12 +151,13 @@ spr.post('/relay/0', multipartParser, async (request, response) => { // * This is then followed by a CecMessageHeader (see https://github.com/NarcolepticK/CECDocs/blob/master/Structs/CecMessageHeader.md) // * Check that we at least have enough size for the StreetPass header - if (slotData.length < 0x12) { + if (slotDataFile.size < 0x12) { logger.warn(`${request.pid}: Slot is too short`); response.sendStatus(400); return; } + const slotData = await readFile(slotDataFile.filepath); if (slotData.readUInt32LE() !== 0x6161) { logger.warn(`${request.pid}: Slot header missmatch`); response.sendStatus(400);