Skip to content

Commit

Permalink
Merge pull request #192 from FoxxMD/GH-191/plex-formdata-fix
Browse files Browse the repository at this point in the history
fix(plex): Refactor plex webhook formdata parsing to be more defensive
  • Loading branch information
FoxxMD authored Sep 17, 2024
2 parents b74f23f + 29c8d62 commit 6196874
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 21 deletions.
59 changes: 38 additions & 21 deletions src/backend/sources/PlexSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { childLogger, Logger } from "@foxxmd/logging";
import concatStream from 'concat-stream';
import dayjs from "dayjs";
import EventEmitter from "events";
import formidable from 'formidable';
import formidable, { Files, File } from 'formidable';
import { file } from "jscodeshift";
import { PlayObject } from "../../core/Atomic.js";
import { truncateStringToLength } from "../../core/StringUtils.js";
import { FormatPlayObjectOptions, InternalConfig, SourceType } from "../common/infrastructure/Atomic.js";
import { PlexSourceConfig } from "../common/infrastructure/config/source/plex.js";
import { combinePartsToString } from "../utils.js";
import { getFileIdentifier, getValidMultipartJsonFile } from "../utils/RequestUtils.js";
import AbstractSource from "./AbstractSource.js";

const shortDeviceId = truncateStringToLength(10, '');
Expand Down Expand Up @@ -265,7 +267,7 @@ export const plexRequestMiddle = (logger: Logger) => {
plexLog.debug('Receiving request from Plex...');

return new Promise((resolve, reject) => {
form.parse(req, (err: any, fields: any, files: any) => {
form.parse(req, (err: any, fields: any, files: Files | File) => {
if (err) {
plexLog.error('Error occurred while parsing formdata');
plexLog.error(err);
Expand All @@ -274,20 +276,35 @@ export const plexRequestMiddle = (logger: Logger) => {
return;
}

let validFile = null;
for (const namedFile of Object.values(files)) {
// @ts-expect-error TS(2571): Object is of type 'unknown'.
if (namedFile.mimetype.includes('json')) {
validFile = namedFile;
break;
}
let validFile,
fileResults;
try {
const [vf, fr] = getValidMultipartJsonFile(files);
validFile = vf;
fileResults = fr;
} catch (e) {
const parseError = new Error('Could not parse plex webhook formdata to valid files', {cause: e});
plexLog.error(parseError)
next(parseError);
reject(parseError);
return;
}
if (validFile === null) {
// @ts-expect-error TS(2571): Object is of type 'unknown'.
const err = new Error(`No files parsed from formdata had a mimetype that included 'json'. Found files:\n ${Object.entries(files).map(([k, v]) => `${k}: ${v.mimetype}`).join('\n')}`);
plexLog.error(err);
next(err);
reject(err);

if (validFile === undefined) {
const validError = new Error(`No files parsed from formdata had a mimetype that included 'json' => ${fileResults.join('\n')}`);
plexLog.error(validError);
next(validError);
reject(validError);
return;
} else {
plexLog.debug(`formdata file results => ${fileResults.join('\n')}`);
}

if(!('buffer' in validFile)) {
const buffErr = new Error(`${getFileIdentifier(validFile as unknown as File)} file should have had buffer but it did not!`);
plexLog.error(buffErr);
next(buffErr);
reject(buffErr);
return;
}

Expand All @@ -297,13 +314,13 @@ export const plexRequestMiddle = (logger: Logger) => {
payload = JSON.parse(payloadRaw);
req.payload = payload;
next();
// @ts-expect-error TS(2794): Expected 1 arguments, but got 0. Did you forget to... Remove this comment to see the full error message
resolve();
resolve(undefined);
} catch (e) {
plexLog.error(`Error occurred while trying to parse Plex file payload to json. Raw text:\n${payloadRaw}`);
plexLog.error(e);
next(e);
reject(e);
const jsonParseError = new Error(`Error occurred while trying to parse Plex formdata file ${getFileIdentifier(validFile as unknown as File)} to json. Raw text:\n${payloadRaw}`, {cause: e});
plexLog.error(jsonParseError);
next(jsonParseError);
reject(jsonParseError);
return;
}
});
});
Expand Down
71 changes: 71 additions & 0 deletions src/backend/utils/RequestUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Files, File } from "formidable";
import VolatileFile from "formidable/VolatileFile.js";

// typings from Formidable are all nuts.
// VolatileFile is missing buffer and also does not extend File even though it should

export const getValidMultipartJsonFile = (files: Files | File): [VolatileFile, string[]?] => {

const logs: string[] = [];

try {

if (isVolatileFile(files)) {
if ('mimetype' in files && files.mimetype !== undefined) {
if (files.mimetype.includes('json')) {
logs.push(`Found ${getFileIdentifier(files)} with mimetype '${files.mimetype}'`)
return [files as unknown as VolatileFile, logs];
} else {
logs.push(`${getFileIdentifier(files)} mimetype '${files.mimetype}' does not include 'json'`);
}
} else {
logs.push(`${getFileIdentifier(files)} had no mimetype`)
}
} else {
for (const [partName, namedFile] of Object.entries(files)) {
if (Array.isArray(namedFile)) {
for (const [index, file] of Object.entries(namedFile)) {
if ('mimetype' in file && file.mimetype !== undefined) {
if (file.mimetype.includes('json')) {
logs.push(`Found ${partName}.${index}.${getFileIdentifier(file)} with mimetype '${file.mimetype}'`)
return [file as unknown as VolatileFile, logs];
} else {
logs.push(`${partName}.${index}.${getFileIdentifier(file)} mimetype '${file.mimetype}' does not include 'json'`);
}
} else {
logs.push(`${partName}.${index}.${getFileIdentifier(file)} had no mimetype`)
}
}
} else {
// this shouldn't happen but it was happening so...
const singleFile = namedFile as File;
if (typeof singleFile === 'object' && 'mimetype' in singleFile && singleFile.mimetype !== undefined) {
if (singleFile.mimetype.includes('json')) {
logs.push(`Found ${partName}.${getFileIdentifier(singleFile)} with mimetype '${singleFile.mimetype}'`);
return [namedFile as unknown as VolatileFile, logs];
} else {
logs.push(`${partName}.${getFileIdentifier(singleFile)} mimetype '${singleFile.mimetype}' does not include 'json'`);
}
} else {
logs.push(`${partName}.${getFileIdentifier(singleFile)} had no mimetype`)
}
}
}
}
} catch (e) {
throw new Error('Unexpected error occurred while trying to find valid json file in formdata', {cause: e});
}

return [undefined, logs];
}

const isVolatileFile = (val: unknown): val is File => {
return typeof val === 'object'
&& val !== null
&& 'size' in val
&& 'filepath' in val;
}

export const getFileIdentifier = (f: File): string => {
return f.originalFilename === null ? f.newFilename : f.originalFilename;
}

0 comments on commit 6196874

Please sign in to comment.