UNPKG

@auth70/bodyguard

Version:

Fetch API compatible streaming JSON and form data body parser and guard

308 lines (307 loc) 15.5 kB
import { ERRORS, MAX_DEPTH, MAX_KEYS, MAX_KEY_LENGTH, MAX_SIZE } from "./lib.js"; import { FormDataParser, JSONParser, TextParser, URLParamsParser } from "./parser.js"; export { ERRORS, MAX_DEPTH, MAX_KEYS, MAX_KEY_LENGTH, MAX_SIZE, FormDataParser, JSONParser, TextParser, URLParamsParser }; export class Bodyguard { config; /** * Constructs a Bodyguard instance with the provided configuration or defaults to preset values. * @param {BodyguardConfig} config - Configuration settings to initialize the Bodyguard instance. * @param {number} config.maxKeys - Maximum number of keys. * @param {number} config.maxDepth - Maximum depth of an object or array. * @param {number} config.maxSize - Maximum size of a Request or Response body in bytes. * @param {number} config.maxKeyLength - Maximum length of a key in characters. * @param {boolean} config.castBooleans - Whether to cast boolean values to boolean type. * @param {boolean} config.castNumbers - Whether to cast numeric values to number type. * @param {boolean} config.convertPluses - Whether to convert plus signs to spaces in urlencoded form data. * @param {number} config.maxFiles - Maximum number of files; used only for multipart form data. * @param {number} config.maxFilenameLength - Maximum length of a filename; used only for multipart form data. * @param {string[]} config.allowedContentTypes - Allow list for content types; used only for multipart form data. * @example * const bodyguard = new Bodyguard({ * maxKeys: 100, // Maximum number of keys. * maxDepth: 10, // Maximum depth of an object or array. * maxSize: 1024 * 1024, // Maximum size of a Request or Response body in bytes. * maxKeyLength: 100, // Maximum length of a key in characters. * castBooleans: false, // Whether to cast boolean values to boolean type. * castNumbers: false, // Whether to cast numeric values to number type. * }); */ constructor(config) { this.config = { maxKeys: config?.maxKeys && typeof config.maxKeys === 'number' && config.maxKeys > 0 ? config.maxKeys : MAX_KEYS, maxDepth: config?.maxDepth && typeof config.maxDepth === 'number' && config.maxDepth > 0 ? config.maxDepth : MAX_DEPTH, maxSize: config?.maxSize && typeof config.maxSize === 'number' && config.maxSize > 0 ? config.maxSize : MAX_SIZE, maxKeyLength: config?.maxKeyLength && typeof config.maxKeyLength === 'number' && config.maxKeyLength > 0 ? config.maxKeyLength : MAX_KEY_LENGTH, castBooleans: config?.castBooleans !== undefined && typeof config.castBooleans === 'boolean' ? config.castBooleans : false, castNumbers: config?.castNumbers !== undefined && typeof config.castNumbers === 'boolean' ? config.castNumbers : false, convertPluses: config?.convertPluses !== undefined && typeof config.convertPluses === 'boolean' ? config.convertPluses : false, maxFiles: typeof config?.maxFiles === 'number' && config.maxFiles > -1 ? config.maxFiles : Infinity, maxFilenameLength: typeof config?.maxFilenameLength === 'number' && config.maxFilenameLength > 0 ? config.maxFilenameLength : 255, allowedContentTypes: config?.allowedContentTypes && Array.isArray(config.allowedContentTypes) ? config.allowedContentTypes : undefined, }; } /** * Attempts to parse a Request or Response body. Returns the parsed object in case of success and * an error object in case of failure. * @param {Request | Response} input - Request or Response to parse the body from. * @param {BodyguardValidator} validator - Optional validator to validate the parsed body against. * @param {Partial<BodyguardConfig | BodyguardFormConfig>} config - Optional configuration to override the default configuration. * @returns {Promise<BodyguardResult<E, K>>} - Result of the parsing operation. */ async softPat(input, validator, config) { try { const res = await this.pat(input, validator, config); return { success: true, value: res }; } catch (e) { return { success: false, error: e }; } } /** * Attempts to parse a Request or Response body. Returns the parsed object in case of success and * an error object in case of failure. * @param {Request | Response} input - Request or Response to parse the body from. * @param {BodyguardValidator} validator - Optional validator to validate the parsed body against. * @param {Partial<BodyguardConfig | BodyguardFormConfig>} config - Optional configuration to override the default configuration. * @returns {Promise<K>} - Result of the parsing operation. * @throws {Error} - If content-type is not present or is invalid, or the body is invalid, it throws an error. */ async pat(input, validator, config) { const contentType = input.headers.get("content-type"); if (!contentType || contentType === '') throw new Error(ERRORS.NO_CONTENT_TYPE); if (contentType === "application/x-www-form-urlencoded") { return await this.form(input, validator, config); } else if (contentType.startsWith("multipart/form-data")) { return await this.form(input, validator, config); } else if (contentType === "application/json") { return await this.json(input, validator, config); } else if (contentType === "text/plain") { return await this.text(input, validator, config); } else { throw new Error(ERRORS.INVALID_CONTENT_TYPE); } } async formInternal(input, config) { if (!input.body) throw new Error(ERRORS.BODY_NOT_AVAILABLE); const instanceConfig = this.constructConfig(config || {}); const contentType = input.headers.get("content-type"); if (!contentType || contentType === '') throw new Error(ERRORS.NO_CONTENT_TYPE); const bodyType = contentType === "application/x-www-form-urlencoded" ? "params" : "formdata"; let boundary = ""; if (contentType.includes("boundary")) { const match = contentType.match(/boundary=(.*)/); if (!match || !match[1]) { throw new Error(ERRORS.INVALID_CONTENT_TYPE); } boundary = match[1]; } if (bodyType === "formdata" && !boundary) throw new Error(ERRORS.INVALID_CONTENT_TYPE); const parser = bodyType === "params" ? new URLParamsParser(instanceConfig) : new FormDataParser(instanceConfig, boundary); const ret = await parser.parse(input.body); return ret; } /** * Attempts to parse a form from a Request or Response. Returns the parsed object in case of success and * an error object in case of failure. * @param {Request | Response} input - Request or Response to parse the form from. * @param {BodyguardValidator} validator - Optional validator to validate the parsed form against. * @param {Partial<BodyguardFormConfig>} config - Optional configuration to override the default configuration. * @return {Promise<BodyguardResult<E, K>>} - Result of the parsing operation. */ async softForm(input, validator, config) { try { const ret = await this.formInternal(input, config); try { if (validator) { return { success: true, value: await Promise.resolve(validator(ret)) }; } return { success: true, value: ret }; } catch (err) { return { success: false, error: err, value: ret }; } } catch (e) { return { success: false, error: e }; } } /** * Parses a form from a Request or Response. Form could be urlencoded or multipart. * @param {Request | Response} input - Request or Response to parse the form from. * @param {BodyguardValidator} validator - Optional validator to validate the parsed form against. * @param {Partial<BodyguardFormConfig>} config - Optional configuration to override the default configuration. * @param {boolean} soft - Whether to throw an error or return an error object in case of failure. * @return {Promise<K>} - Parsed form from the Request or Response. * @throws {Error} - If content-type is not present or is invalid, or the form data is invalid, it throws an error. */ async form(input, validator, config, soft) { const ret = await this.formInternal(input, config); if (validator) { return await Promise.resolve(validator(ret)); } return ret; } async jsonInternal(input, config) { if (!input.body) throw new Error(ERRORS.BODY_NOT_AVAILABLE); const instanceConfig = this.constructConfig(config || {}); const parser = new JSONParser(instanceConfig); const ret = await parser.parse(input.body); return ret; } /** * Attempts to parse JSON from a Request or Response. Returns the parsed JSON in case of success and * an error object in case of failure. * @param {Request | Response} input - Request or Response to parse the JSON from. * @param {BodyguardValidator} validator - Optional validator to validate the parsed JSON against. * @param {BodyguardConfig} config - Optional configuration to override the default configuration. * @return {Promise<BodyguardResult<E, K>>} - Result of the parsing operation. */ async softJson(input, validator, config) { try { const ret = await this.jsonInternal(input, config); try { if (validator) { return { success: true, value: await Promise.resolve(validator(ret)) }; } return { success: true, value: ret }; } catch (err) { return { success: false, error: err, value: ret }; } } catch (e) { return { success: false, error: e }; } } /** * Parses JSON from a Request or Response. * @param {Request | Response} input - Request or Response to parse the JSON from. * @param {BodyguardValidator} validator - Optional validator to validate the parsed JSON against. * @param {BodyguardConfig} config - Optional configuration to override the default configuration. * @return {Promise<K>} - Parsed JSON from the Request or Response. * @throws {Error} - If JSON parsing fails, it throws an error. */ async json(input, validator, config) { const ret = await this.jsonInternal(input, config); if (validator) { return await Promise.resolve(validator(ret)); } return ret; } async textInternal(input, config) { if (!input.body) throw new Error(ERRORS.BODY_NOT_AVAILABLE); const instanceConfig = this.constructConfig(config || {}); const parser = new TextParser(instanceConfig); const ret = await parser.parse(input.body); return ret; } /** * Attempts to parse text from a Request or Response. Returns the parsed text in case of success and * an error object in case of failure. * @param {Request | Response} input - Request or Response to parse the text from. * @param {BodyguardValidator} validator - Optional validator to validate the parsed text against. * @param {BodyguardConfig} config - Optional configuration to override the default configuration. * @returns {Promise<BodyguardResult<K>>} - Result of the parsing operation. */ async softText(input, validator, config) { try { const ret = await this.textInternal(input, config); try { if (validator) { return { success: true, value: await Promise.resolve(validator(ret)) }; } return { success: true, value: ret }; } catch (err) { return { success: false, error: err, value: ret }; } } catch (e) { return { success: false, error: e }; } } /** * Parses text from a Request or Response. * @param {Request | Response} input - Request or Response to parse the text from. * @param {BodyguardValidator} validator - Optional validator to validate the parsed text against. * @param {BodyguardConfig} config - Optional configuration to override the default configuration. * @returns {Promise<K>} - Parsed text from the Request or Response. * @throws {Error} - If text parsing fails, it throws an error. */ async text(input, validator, config) { const ret = await this.textInternal(input, config); if (validator) { return await Promise.resolve(validator(ret)); } return ret; } constructConfig(config) { return { maxKeys: config?.maxKeys && typeof config.maxKeys === 'number' && config.maxKeys > 0 ? config.maxKeys : this.config.maxKeys, maxDepth: config?.maxDepth && typeof config.maxDepth === 'number' && config.maxDepth > 0 ? config.maxDepth : this.config.maxDepth, maxSize: config?.maxSize && typeof config.maxSize === 'number' && config.maxSize > 0 ? config.maxSize : this.config.maxSize, maxKeyLength: config?.maxKeyLength && typeof config.maxKeyLength === 'number' && config.maxKeyLength > 0 ? config.maxKeyLength : this.config.maxKeyLength, castBooleans: config?.castBooleans !== undefined && typeof config.castBooleans === 'boolean' ? config.castBooleans : this.config.castBooleans, castNumbers: config?.castNumbers !== undefined && typeof config.castNumbers === 'boolean' ? config.castNumbers : this.config.castNumbers, convertPluses: config?.convertPluses !== undefined && typeof config.convertPluses === 'boolean' ? config.convertPluses : false, maxFiles: typeof config?.maxFiles === 'number' && config.maxFiles > -1 ? config.maxFiles : this.config.maxFiles, maxFilenameLength: typeof config?.maxFilenameLength === 'number' ? config.maxFilenameLength : this.config.maxFilenameLength, allowedContentTypes: config?.allowedContentTypes && Array.isArray(config.allowedContentTypes) ? config.allowedContentTypes : this.config.allowedContentTypes, }; } }