simplify application and remove window references
Sonarqube Scanner / Build and analyze (push) Failing after 1m27s
Sonarqube Scanner / Build and analyze (push) Failing after 1m27s
This commit is contained in:
+100
-67
@@ -8,6 +8,25 @@ import { detectAv } from '@file-type/av';
|
||||
import { config } from '../config.js';
|
||||
import { HttpError } from './errors.js';
|
||||
|
||||
const MAX_GPX_PREFIX_BYTES = 131072;
|
||||
const BASE64_PATTERN = /^[A-Za-z0-9+/]*={0,2}$/;
|
||||
const IGNORED_XML_MARKUP = Object.freeze([
|
||||
Object.freeze({ opening: '<!--', closing: '-->' }),
|
||||
Object.freeze({ opening: '<?', closing: '?>' }),
|
||||
Object.freeze({ opening: '<!', closing: '>' })
|
||||
]);
|
||||
|
||||
const ALLOWED_TYPES = Object.freeze({
|
||||
picture: Object.freeze({
|
||||
mime: new Set(['image/jpeg', 'image/png', 'image/webp']),
|
||||
ext: new Set(['jpg', 'png', 'webp'])
|
||||
}),
|
||||
audio: Object.freeze({
|
||||
mime: new Set(['audio/mpeg', 'audio/mp4', 'audio/aac', 'audio/ogg', 'audio/wav', 'audio/webm']),
|
||||
ext: new Set(['mp3', 'm4a', 'aac', 'ogg', 'opus', 'spx', 'wav', 'webm'])
|
||||
})
|
||||
});
|
||||
|
||||
function sanitizeFilename(value) {
|
||||
return String(value).replaceAll(/[^a-zA-Z0-9._-]/g, '_') || 'upload';
|
||||
}
|
||||
@@ -35,38 +54,40 @@ function isXmlNameCharacter(character, first = false) {
|
||||
return !first && ((code >= 48 && code <= 57) || character === '.' || character === '-' || character === ':');
|
||||
}
|
||||
|
||||
function ignoredMarkupEnd(xml, opening) {
|
||||
const markup = IGNORED_XML_MARKUP.find(item => xml.startsWith(item.opening, opening));
|
||||
if (!markup) return null;
|
||||
|
||||
const closing = xml.indexOf(markup.closing, opening + markup.opening.length);
|
||||
return closing < 0 ? -1 : closing + markup.closing.length;
|
||||
}
|
||||
|
||||
function readXmlName(xml, start) {
|
||||
if (start >= xml.length || !isXmlNameCharacter(xml[start], true)) return null;
|
||||
|
||||
let end = start + 1;
|
||||
while (end < xml.length && isXmlNameCharacter(xml[end])) end += 1;
|
||||
return xml.slice(start, end);
|
||||
}
|
||||
|
||||
function rootElementName(xml) {
|
||||
let cursor = 0;
|
||||
|
||||
while (cursor < xml.length) {
|
||||
const opening = xml.indexOf('<', cursor);
|
||||
if (opening < 0 || opening + 1 >= xml.length) return null;
|
||||
|
||||
if (xml.startsWith('<!--', opening)) {
|
||||
const end = xml.indexOf('-->', opening + 4);
|
||||
if (end < 0) return null;
|
||||
cursor = end + 3;
|
||||
const nextCursor = ignoredMarkupEnd(xml, opening);
|
||||
if (nextCursor === -1) return null;
|
||||
if (nextCursor != null) {
|
||||
cursor = nextCursor;
|
||||
continue;
|
||||
}
|
||||
if (xml.startsWith('<?', opening)) {
|
||||
const end = xml.indexOf('?>', opening + 2);
|
||||
if (end < 0) return null;
|
||||
cursor = end + 2;
|
||||
continue;
|
||||
}
|
||||
if (xml.startsWith('<!', opening)) {
|
||||
const end = xml.indexOf('>', opening + 2);
|
||||
if (end < 0) return null;
|
||||
cursor = end + 1;
|
||||
continue;
|
||||
}
|
||||
if (xml.startsWith('</', opening)) return null;
|
||||
|
||||
let end = opening + 1;
|
||||
if (!isXmlNameCharacter(xml[end], true)) return null;
|
||||
end += 1;
|
||||
while (end < xml.length && isXmlNameCharacter(xml[end])) end += 1;
|
||||
return xml.slice(opening + 1, end);
|
||||
if (xml.startsWith('</', opening)) return null;
|
||||
return readXmlName(xml, opening + 1);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -83,19 +104,29 @@ export const upload = multer({
|
||||
limits: { fileSize: config.maxUploadBytes, files: 25 }
|
||||
});
|
||||
|
||||
const ALLOWED_TYPES = Object.freeze({
|
||||
picture: Object.freeze({
|
||||
mime: new Set(['image/jpeg', 'image/png', 'image/webp']),
|
||||
ext: new Set(['jpg', 'png', 'webp'])
|
||||
}),
|
||||
audio: Object.freeze({
|
||||
mime: new Set(['audio/mpeg', 'audio/mp4', 'audio/aac', 'audio/ogg', 'audio/wav', 'audio/webm']),
|
||||
ext: new Set(['mp3', 'm4a', 'aac', 'ogg', 'opus', 'spx', 'wav', 'webm'])
|
||||
})
|
||||
});
|
||||
|
||||
function removeTemporaryFile(file) {
|
||||
if (file?.path && fs.existsSync(file.path)) fs.rmSync(file.path, { force: true });
|
||||
if (!file?.path || !fs.existsSync(file.path)) return;
|
||||
fs.rmSync(file.path, { force: true });
|
||||
}
|
||||
|
||||
function assertValidBase64(compact, fieldName) {
|
||||
if (!BASE64_PATTERN.test(compact) || compact.length % 4 === 1) {
|
||||
throw new HttpError(400, `${fieldName}.base64 enthält keine gültigen Base64-Daten.`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertCanonicalBase64(compact, buffer, fieldName) {
|
||||
const canonicalInput = stripBase64Padding(compact);
|
||||
const canonicalOutput = stripBase64Padding(buffer.toString('base64'));
|
||||
if (canonicalInput !== canonicalOutput) {
|
||||
throw new HttpError(400, `${fieldName}.base64 enthält keine gültigen Base64-Daten.`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertUploadSize(buffer) {
|
||||
if (buffer.length <= config.maxUploadBytes) return;
|
||||
const limitMb = Math.floor(config.maxUploadBytes / 1024 / 1024);
|
||||
throw new HttpError(413, `Die Datei überschreitet das Limit von ${limitMb} MB.`);
|
||||
}
|
||||
|
||||
function decodeBase64(value, fieldName) {
|
||||
@@ -104,19 +135,11 @@ function decodeBase64(value, fieldName) {
|
||||
}
|
||||
|
||||
const compact = value.replaceAll(/\s/g, '');
|
||||
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(compact) || compact.length % 4 === 1) {
|
||||
throw new HttpError(400, `${fieldName}.base64 enthält keine gültigen Base64-Daten.`);
|
||||
}
|
||||
assertValidBase64(compact, fieldName);
|
||||
|
||||
const buffer = Buffer.from(compact, 'base64');
|
||||
const canonicalInput = stripBase64Padding(compact);
|
||||
const canonicalOutput = stripBase64Padding(buffer.toString('base64'));
|
||||
if (canonicalInput !== canonicalOutput) {
|
||||
throw new HttpError(400, `${fieldName}.base64 enthält keine gültigen Base64-Daten.`);
|
||||
}
|
||||
if (buffer.length > config.maxUploadBytes) {
|
||||
throw new HttpError(413, `Die Datei überschreitet das Limit von ${Math.floor(config.maxUploadBytes / 1024 / 1024)} MB.`);
|
||||
}
|
||||
assertCanonicalBase64(compact, buffer, fieldName);
|
||||
assertUploadSize(buffer);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
@@ -126,7 +149,8 @@ function jsonFileDescriptor(value, fieldName) {
|
||||
throw new HttpError(400, `${fieldName} muss ein JSON-Objekt sein.`);
|
||||
}
|
||||
|
||||
const originalname = String(value.filename ?? `${fieldName}.upload`).trim() || `${fieldName}.upload`;
|
||||
const fallbackName = `${fieldName}.upload`;
|
||||
const originalname = String(value.filename ?? fallbackName).trim() || fallbackName;
|
||||
let base64 = value.base64;
|
||||
|
||||
if (value.dataUrl != null) {
|
||||
@@ -174,10 +198,9 @@ function detectGpx(file) {
|
||||
throw new HttpError(415, 'Der Upload ist keine gültig kodierte GPX-Datei.');
|
||||
}
|
||||
|
||||
const prefix = xml.slice(0, 131072);
|
||||
const prefix = xml.slice(0, MAX_GPX_PREFIX_BYTES);
|
||||
const start = prefix.startsWith('\uFEFF') ? prefix.slice(1) : prefix;
|
||||
const rootName = rootElementName(start);
|
||||
const localName = rootName?.split(':').at(-1);
|
||||
const localName = rootElementName(start)?.split(':').at(-1);
|
||||
if (localName !== 'gpx') {
|
||||
throw new HttpError(415, 'Der Upload enthält kein GPX-Dokument.');
|
||||
}
|
||||
@@ -195,25 +218,34 @@ async function detectBinary(file) {
|
||||
}
|
||||
}
|
||||
|
||||
function unsupportedContentMessage(kind) {
|
||||
return kind === 'picture'
|
||||
? 'Der Dateiinhalt ist kein unterstütztes Bildformat.'
|
||||
: 'Der Dateiinhalt ist kein unterstütztes Audioformat.';
|
||||
}
|
||||
|
||||
function unsupportedFormatMessage(kind, mime) {
|
||||
return kind === 'picture'
|
||||
? `Nicht unterstütztes Bildformat: ${mime}`
|
||||
: `Nicht unterstütztes Audioformat: ${mime}`;
|
||||
}
|
||||
|
||||
function validateDetectedType(detected, kind) {
|
||||
if (kind === 'gpx') return;
|
||||
|
||||
const rules = ALLOWED_TYPES[kind];
|
||||
const baseMime = String(detected.mime).split(';', 1)[0].toLowerCase();
|
||||
const extension = String(detected.ext).toLowerCase();
|
||||
if (!rules?.mime.has(baseMime) || !rules.ext.has(extension)) {
|
||||
throw new HttpError(415, unsupportedFormatMessage(kind, detected.mime));
|
||||
}
|
||||
}
|
||||
|
||||
async function detectAndValidate(file, kind) {
|
||||
const detected = kind === 'gpx' ? detectGpx(file) : await detectBinary(file);
|
||||
if (!detected) {
|
||||
throw new HttpError(415, kind === 'picture'
|
||||
? 'Der Dateiinhalt ist kein unterstütztes Bildformat.'
|
||||
: 'Der Dateiinhalt ist kein unterstütztes Audioformat.');
|
||||
}
|
||||
|
||||
if (kind !== 'gpx') {
|
||||
const rules = ALLOWED_TYPES[kind];
|
||||
const baseMime = String(detected.mime).split(';', 1)[0].toLowerCase();
|
||||
const extension = String(detected.ext).toLowerCase();
|
||||
if (!rules?.mime.has(baseMime) || !rules?.ext.has(extension)) {
|
||||
throw new HttpError(415, kind === 'picture'
|
||||
? `Nicht unterstütztes Bildformat: ${detected.mime}`
|
||||
: `Nicht unterstütztes Audioformat: ${detected.mime}`);
|
||||
}
|
||||
}
|
||||
if (!detected) throw new HttpError(415, unsupportedContentMessage(kind));
|
||||
|
||||
validateDetectedType(detected, kind);
|
||||
file.mimetype = detected.mime;
|
||||
file.detectedMime = detected.mime;
|
||||
file.detectedExtension = detected.ext;
|
||||
@@ -225,12 +257,13 @@ export function inspectUpload(fieldName, kind, { allowJson = false } = {}) {
|
||||
try {
|
||||
if (allowJson) materializeJsonUpload(req, fieldName);
|
||||
if (!req.file) return next();
|
||||
|
||||
await detectAndValidate(req.file, kind);
|
||||
next();
|
||||
return next();
|
||||
} catch (error) {
|
||||
removeTemporaryFile(req.file);
|
||||
req.file = undefined;
|
||||
next(error);
|
||||
return next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user