chore: replace dicer with formidable

This commit is contained in:
mrjvs 2025-09-17 12:45:24 +02:00
parent f432d2d3dc
commit 28f77dcf77
3 changed files with 134 additions and 102 deletions

133
package-lock.json generated
View File

@ -14,9 +14,9 @@
"@pretendonetwork/grpc": "^1.0.6", "@pretendonetwork/grpc": "^1.0.6",
"@typegoose/auto-increment": "^4.13.0", "@typegoose/auto-increment": "^4.13.0",
"commander": "^14.0.0", "commander": "^14.0.0",
"dicer": "^0.3.1",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"express": "^5.1.0", "express": "^5.1.0",
"formidable": "^3.5.4",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"moment": "^2.30.1", "moment": "^2.30.1",
"mongoose": "^8.18.1", "mongoose": "^8.18.1",
@ -33,6 +33,7 @@
"@smithy/types": "^4.0.0", "@smithy/types": "^4.0.0",
"@types/dicer": "^0.2.4", "@types/dicer": "^0.2.4",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/formidable": "^3.4.5",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"@types/node": "^22.10.5", "@types/node": "^22.10.5",
"axios": "^1.7.9", "axios": "^1.7.9",
@ -1917,6 +1918,18 @@
"@tybys/wasm-util": "^0.10.0" "@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": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -2000,6 +2013,15 @@
"node": ">=8.0" "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": { "node_modules/@pkgjs/parseargs": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@ -3198,6 +3220,16 @@
"@types/send": "*" "@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": { "node_modules/@types/fs-extra": {
"version": "11.0.4", "version": "11.0.4",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz",
@ -4187,6 +4219,12 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/async-function": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
@ -4722,15 +4760,14 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/dicer": { "node_modules/dezalgo": {
"version": "0.3.1", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.1.tgz", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
"integrity": "sha512-ObioMtXnmjYs3aRtpIJt9rgQSPCIhKVkFPip+E9GUDyWl8N435znUxK/JfNwGZJ2wnn5JKQ7Ly3vOK5Q5dylGA==", "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
"license": "ISC",
"dependencies": { "dependencies": {
"streamsearch": "^1.1.0" "asap": "^2.0.0",
}, "wrappy": "1"
"engines": {
"node": ">=10.0.0"
} }
}, },
"node_modules/doctrine": { "node_modules/doctrine": {
@ -5997,6 +6034,23 @@
"node": ">= 6" "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": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -8749,14 +8803,6 @@
"node": ">= 0.4" "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": { "node_modules/string-width": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@ -11123,6 +11169,11 @@
"@tybys/wasm-util": "^0.10.0" "@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": { "@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -11185,6 +11236,14 @@
"integrity": "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ==", "integrity": "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ==",
"dev": true "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": { "@pkgjs/parseargs": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@ -12051,6 +12110,15 @@
"@types/send": "*" "@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": { "@types/fs-extra": {
"version": "11.0.4", "version": "11.0.4",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz",
@ -12671,6 +12739,11 @@
"is-array-buffer": "^3.0.4" "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": { "async-function": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", "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", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
}, },
"dicer": { "dezalgo": {
"version": "0.3.1", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.1.tgz", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
"integrity": "sha512-ObioMtXnmjYs3aRtpIJt9rgQSPCIhKVkFPip+E9GUDyWl8N435znUxK/JfNwGZJ2wnn5JKQ7Ly3vOK5Q5dylGA==", "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
"requires": { "requires": {
"streamsearch": "^1.1.0" "asap": "^2.0.0",
"wrappy": "1"
} }
}, },
"doctrine": { "doctrine": {
@ -13935,6 +14009,16 @@
"mime-types": "^2.1.12" "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": { "forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -15747,11 +15831,6 @@
"internal-slot": "^1.1.0" "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": { "string-width": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",

View File

@ -18,9 +18,9 @@
"@pretendonetwork/grpc": "^1.0.6", "@pretendonetwork/grpc": "^1.0.6",
"@typegoose/auto-increment": "^4.13.0", "@typegoose/auto-increment": "^4.13.0",
"commander": "^14.0.0", "commander": "^14.0.0",
"dicer": "^0.3.1",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"express": "^5.1.0", "express": "^5.1.0",
"formidable": "^3.5.4",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"moment": "^2.30.1", "moment": "^2.30.1",
"mongoose": "^8.18.1", "mongoose": "^8.18.1",
@ -37,6 +37,7 @@
"@smithy/types": "^4.0.0", "@smithy/types": "^4.0.0",
"@types/dicer": "^0.2.4", "@types/dicer": "^0.2.4",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/formidable": "^3.4.5",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"@types/node": "^22.10.5", "@types/node": "^22.10.5",
"axios": "^1.7.9", "axios": "^1.7.9",

View File

@ -1,87 +1,38 @@
import crypto from 'node:crypto'; 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 express from 'express';
import Dicer from 'dicer';
import { getDuplicateCECData, getRandomCECData } from '@/database'; import { getDuplicateCECData, getRandomCECData } from '@/database';
import { getFriends } from '@/util'; import { getFriends } from '@/util';
import { CECData } from '@/models/cec-data'; import { CECData } from '@/models/cec-data';
import { CECSlot } from '@/models/cec-slot'; import { CECSlot } from '@/models/cec-slot';
import { SendMode } from '@/types/common/spr-slot'; import { SendMode } from '@/types/common/spr-slot';
import RequestException from '@/request-exception';
import { config } from '@/config-manager'; import { config } from '@/config-manager';
import { restrictHostnames } from '@/middleware/host-limit'; import { restrictHostnames } from '@/middleware/host-limit';
import { logger } from '@/logger'; import { logger } from '@/logger';
import { getCDNFileAsBuffer, uploadCDNFile } from '@/cdn'; 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'; import type { SPRSlot } from '@/types/common/spr-slot';
const spr = express.Router(); const spr = express.Router();
function multipartParser(request: express.Request, response: express.Response, next: express.NextFunction): void { async function parseMultipart(request: Request): Promise<Record<string, File>> {
const RE_BOUNDARY = /^multipart\/.+?(?:; boundary=(?:(?:"(.+)")|(?:([^\s]+))))$/i; const form = formidable({
const RE_FILE_NAME = /name="(.*)"/; multiples: false
});
const contentType = request.header('content-type'); const parsedForm = await form.parse(request).catch((err: Error) => {
throw new RequestException(err.message, 400);
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<string, Buffer> = {};
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));
});
}); });
dicer.on('finish', function () { const entries = Object.entries(parsedForm[1]);
request.files = files; const entriesWithSinglefile = entries.map(v => [v[0], (v[1] ?? [])[0]]);
return next(); return Object.fromEntries(entriesWithSinglefile);
});
Stream.pipeline(request, dicer, (error: Error | null) => {
if (error) {
return next(new RequestException(error.message, 400));
}
});
} }
spr.post('/relay/0', multipartParser, async (request, response) => { spr.post('/relay/0', async (request, response) => {
if (!request.files) { const files = await parseMultipart(request);
response.sendStatus(400);
return;
}
if (!request.pid || !request.nexAccount) { if (!request.pid || !request.nexAccount) {
response.sendStatus(401); 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 // * Check that the account is a 3DS and isn't banned
if (!request.nexAccount.friendCode || request.nexAccount.accessLevel < 0) { 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); response.sendStatus(403);
return; return;
} }
const sprMetadataBuffer: Buffer | undefined = request.files['spr-meta']; const sprMetadataFile: File | undefined = files['spr-meta'];
if (!sprMetadataBuffer) { if (!sprMetadataFile) {
logger.warn(`{request.pid}: Missing spr-meta file`); logger.warn(`${request.pid}: Missing spr-meta file`);
response.sendStatus(400); response.sendStatus(400);
return; return;
} }
@ -106,7 +57,7 @@ spr.post('/relay/0', multipartParser, async (request, response) => {
const sprSlots: SPRSlot[] = []; const sprSlots: SPRSlot[] = [];
// * Check spr-meta metadata headers // * 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 const metadataHeaders = sprMetadata.split('\r\n'); // * Split header lines
if (metadataHeaders.length < 1) { if (metadataHeaders.length < 1) {
@ -177,15 +128,15 @@ spr.post('/relay/0', multipartParser, async (request, response) => {
let data: Buffer = Buffer.alloc(0); let data: Buffer = Buffer.alloc(0);
if (size > 0 && sendMode !== SendMode.RecvOnly) { if (size > 0 && sendMode !== SendMode.RecvOnly) {
const slot = i.toString().padStart(2, '0'); 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`); logger.warn(`${request.pid}: Missing slot data file`);
response.sendStatus(400); response.sendStatus(400);
return; return;
} }
if (slotData.length !== size) { if (slotDataFile.size !== size) {
logger.warn(`${request.pid}: Invalid slot data size`); logger.warn(`${request.pid}: Invalid slot data size`);
response.sendStatus(400); response.sendStatus(400);
return; 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) // * 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 // * 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`); logger.warn(`${request.pid}: Slot is too short`);
response.sendStatus(400); response.sendStatus(400);
return; return;
} }
const slotData = await readFile(slotDataFile.filepath);
if (slotData.readUInt32LE() !== 0x6161) { if (slotData.readUInt32LE() !== 0x6161) {
logger.warn(`${request.pid}: Slot header missmatch`); logger.warn(`${request.pid}: Slot header missmatch`);
response.sendStatus(400); response.sendStatus(400);