tsoa
This commit is contained in:
458
node_modules/@hapi/subtext/lib/index.js
generated
vendored
Executable file
458
node_modules/@hapi/subtext/lib/index.js
generated
vendored
Executable file
@@ -0,0 +1,458 @@
|
||||
'use strict';
|
||||
|
||||
const Fs = require('fs');
|
||||
const Fsp = require('fs/promises');
|
||||
const Os = require('os');
|
||||
const Querystring = require('querystring');
|
||||
const Stream = require('stream');
|
||||
const Zlib = require('zlib');
|
||||
|
||||
const Boom = require('@hapi/boom');
|
||||
const Bourne = require('@hapi/bourne');
|
||||
const Content = require('@hapi/content');
|
||||
const File = require('@hapi/file');
|
||||
const Hoek = require('@hapi/hoek');
|
||||
const Pez = require('@hapi/pez');
|
||||
const Wreck = require('@hapi/wreck');
|
||||
|
||||
|
||||
const internals = {
|
||||
kSubtext: Symbol('subtext'),
|
||||
decoders: {
|
||||
gzip: (options) => Zlib.createGunzip(options),
|
||||
deflate: (options) => Zlib.createInflate(options)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
exports.parse = async function (req, tap, options) {
|
||||
|
||||
Hoek.assert(options, 'Missing options');
|
||||
Hoek.assert(options.parse !== undefined, 'Missing parse option setting');
|
||||
Hoek.assert(options.output !== undefined, 'Missing output option setting');
|
||||
|
||||
// Content size
|
||||
|
||||
const contentLength = req.headers['content-length'];
|
||||
if (options.maxBytes !== undefined &&
|
||||
contentLength &&
|
||||
parseInt(contentLength, 10) > options.maxBytes) {
|
||||
|
||||
throw Boom.entityTooLarge('Payload content length greater than maximum allowed: ' + options.maxBytes);
|
||||
}
|
||||
|
||||
// Content type
|
||||
|
||||
const contentType = Content.type(options.override || req.headers['content-type'] || options.defaultContentType || 'application/octet-stream');
|
||||
try {
|
||||
if (options.allow &&
|
||||
options.allow.indexOf(contentType.mime) === -1) {
|
||||
|
||||
throw Boom.unsupportedMediaType();
|
||||
}
|
||||
|
||||
const parsed = { mime: contentType.mime };
|
||||
|
||||
// Parse: true
|
||||
|
||||
if (options.parse === true) {
|
||||
parsed.payload = await internals.parse(req, tap, options, contentType);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
// Parse: false, 'gunzip'
|
||||
|
||||
parsed.payload = await internals.raw(req, tap, options);
|
||||
return parsed;
|
||||
}
|
||||
catch (err) {
|
||||
err.mime = contentType.mime;
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
internals.parse = async function (req, tap, options, contentType) {
|
||||
|
||||
const output = options.output; // Output: 'data', 'stream', 'file'
|
||||
let source = internals.decoder(req, options);
|
||||
|
||||
// Tap request
|
||||
|
||||
if (tap) {
|
||||
[source] = internals.pipe(source, tap);
|
||||
}
|
||||
|
||||
// Multipart
|
||||
|
||||
if (contentType.mime === 'multipart/form-data') {
|
||||
if (options.multipart === false) { // Defaults to true
|
||||
throw Boom.unsupportedMediaType();
|
||||
}
|
||||
|
||||
return await internals.multipart(req, options, source, contentType);
|
||||
}
|
||||
|
||||
// Output: 'stream'
|
||||
|
||||
if (output === 'stream') {
|
||||
return source;
|
||||
}
|
||||
|
||||
// Output: 'file'
|
||||
|
||||
if (output === 'file') {
|
||||
const file = await internals.writeFile(req, options, source);
|
||||
return file.item;
|
||||
}
|
||||
|
||||
// Output: 'data'
|
||||
|
||||
const payload = await Wreck.read(source, { timeout: options.timeout, maxBytes: options.maxBytes });
|
||||
return internals.object(options, payload, contentType.mime);
|
||||
};
|
||||
|
||||
|
||||
internals.decoder = function (source, options) {
|
||||
|
||||
const contentEncoding = source.headers['content-encoding'];
|
||||
const decoders = options.decoders ?? internals.decoders;
|
||||
if (!decoders.hasOwnProperty(contentEncoding)) {
|
||||
return source;
|
||||
}
|
||||
|
||||
const decoderOptions = options.compression?.[contentEncoding] ?? null;
|
||||
const stream = decoders[contentEncoding](decoderOptions);
|
||||
|
||||
const orig = stream.emit;
|
||||
stream.emit = (event, ...args) => {
|
||||
|
||||
if (event === 'error') {
|
||||
args = [Boom.badRequest('Invalid compressed payload', args[0])];
|
||||
}
|
||||
|
||||
return orig.call(stream, event, ...args);
|
||||
};
|
||||
|
||||
[source] = internals.pipe(source, stream);
|
||||
|
||||
return source;
|
||||
};
|
||||
|
||||
|
||||
internals.raw = async function (req, tap, options) {
|
||||
|
||||
const output = options.output; // Output: 'data', 'stream', 'file'
|
||||
let source = req;
|
||||
|
||||
// Content-encoding (optional)
|
||||
|
||||
if (options.parse === 'gunzip') {
|
||||
source = internals.decoder(source, options);
|
||||
}
|
||||
|
||||
// Setup source
|
||||
|
||||
if (tap) {
|
||||
[source] = internals.pipe(source, tap);
|
||||
}
|
||||
|
||||
// Output: 'stream'
|
||||
|
||||
if (output === 'stream') {
|
||||
return source;
|
||||
}
|
||||
|
||||
// Output: 'file'
|
||||
|
||||
if (output === 'file') {
|
||||
const file = await internals.writeFile(req, options, source);
|
||||
return file.item;
|
||||
}
|
||||
|
||||
// Output: 'data'
|
||||
|
||||
return await Wreck.read(source, { timeout: options.timeout, maxBytes: options.maxBytes });
|
||||
};
|
||||
|
||||
|
||||
internals.object = function (options, payload, mime) {
|
||||
|
||||
// Binary
|
||||
|
||||
if (mime === 'application/octet-stream') {
|
||||
return payload.length ? payload : null;
|
||||
}
|
||||
|
||||
// Text
|
||||
|
||||
if (mime.match(/^text\/.+$/)) {
|
||||
return payload.toString('utf8');
|
||||
}
|
||||
|
||||
// JSON
|
||||
|
||||
if (/^application\/(?:.+\+)?json$/.test(mime)) {
|
||||
if (!payload.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return Bourne.parse(payload.toString('utf8'), { protoAction: options.protoAction });
|
||||
}
|
||||
catch (err) {
|
||||
const error = Boom.badRequest('Invalid request payload JSON format', err);
|
||||
error.raw = payload;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Form-encoded
|
||||
|
||||
if (mime === 'application/x-www-form-urlencoded') {
|
||||
const parse = options.querystring ?? Querystring.parse;
|
||||
return payload.length ? parse(payload.toString('utf8')) : {};
|
||||
}
|
||||
|
||||
const error = Boom.unsupportedMediaType();
|
||||
error.raw = payload;
|
||||
throw error;
|
||||
};
|
||||
|
||||
|
||||
internals.multipart = function (req, options, source, contentType) {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
// Set stream timeout
|
||||
|
||||
const clientTimeout = options.timeout;
|
||||
const clientTimeoutId = clientTimeout ? setTimeout(() => reject(Boom.clientTimeout()), clientTimeout) : null;
|
||||
|
||||
// Create parser
|
||||
|
||||
const dispenserOptions = Hoek.applyToDefaults(contentType, {
|
||||
maxBytes: options.maxBytes,
|
||||
maxParts: options.maxParts
|
||||
});
|
||||
const dispenser = new Pez.Dispenser(dispenserOptions);
|
||||
|
||||
const data = {};
|
||||
const pendingFiles = [];
|
||||
|
||||
const onError = (err) => {
|
||||
|
||||
const cleanup = internals.cleanupFiles(pendingFiles);
|
||||
cleanup.catch(Hoek.ignore); // Prevent triggering node's PromiseRejectionHandledWarning
|
||||
|
||||
reject(Boom.badRequest('Invalid multipart payload format', err));
|
||||
};
|
||||
|
||||
dispenser.once('error', onError);
|
||||
|
||||
const set = (name, value) => {
|
||||
|
||||
if (!data.hasOwnProperty(name)) {
|
||||
data[name] = value;
|
||||
}
|
||||
else if (Array.isArray(data[name])) {
|
||||
data[name].push(value);
|
||||
}
|
||||
else {
|
||||
data[name] = [data[name], value];
|
||||
}
|
||||
};
|
||||
|
||||
const finalize = async () => {
|
||||
|
||||
// Clean up
|
||||
|
||||
clearTimeout(clientTimeoutId);
|
||||
dispenser.removeListener('error', onError);
|
||||
dispenser.removeListener('part', onPart);
|
||||
dispenser.removeListener('field', onField);
|
||||
dispenser.removeListener('close', onClose);
|
||||
|
||||
// Wait for files
|
||||
|
||||
try {
|
||||
const files = await Promise.all(pendingFiles);
|
||||
for (const { item, name } of files) {
|
||||
set(name, item);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(data);
|
||||
};
|
||||
|
||||
const output = typeof options.multipart === 'object' ? options.multipart.output : options.output; // options.multipart can be true or object
|
||||
|
||||
const onPart = (part) => {
|
||||
|
||||
if (output === 'file') { // Output: 'file'
|
||||
pendingFiles.push(internals.writeFile(req, options, part));
|
||||
}
|
||||
else {
|
||||
internals.part(part, output, set, options); // Output: 'data' / 'stream'
|
||||
}
|
||||
};
|
||||
|
||||
dispenser.on('part', onPart);
|
||||
|
||||
const onField = (name, value) => set(name, value);
|
||||
dispenser.on('field', onField);
|
||||
|
||||
const onClose = () => finalize();
|
||||
dispenser.once('close', onClose);
|
||||
|
||||
source.pipe(dispenser);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
internals.writeFile = function (req, options, stream) {
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
|
||||
const path = File.uniqueFilename(options.uploads ?? Os.tmpdir());
|
||||
const file = Fs.createWriteStream(path, { flags: 'wx' });
|
||||
const counter = new internals.Counter(options);
|
||||
|
||||
const finalize = (err) => {
|
||||
|
||||
req.removeListener('aborted', onAbort);
|
||||
file.removeListener('close', finalize);
|
||||
file.removeListener('error', finalize);
|
||||
|
||||
if (err) {
|
||||
unpipeStreamToCounter();
|
||||
unpipeCounterToFile();
|
||||
|
||||
file.close();
|
||||
Fs.unlink(path, (/* fsErr */) => reject(err)); // Ignore unlink errors
|
||||
return;
|
||||
}
|
||||
|
||||
const result = {
|
||||
item: {
|
||||
path,
|
||||
bytes: counter.bytes
|
||||
}
|
||||
};
|
||||
|
||||
if (stream.name) { // Multipart
|
||||
result.name = stream.name;
|
||||
result.item.filename = stream.filename;
|
||||
result.item.headers = stream.headers;
|
||||
}
|
||||
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
file.once('close', finalize);
|
||||
file.once('error', finalize);
|
||||
|
||||
const onAbort = () => finalize(Boom.badRequest('Client connection aborted'));
|
||||
req.once('aborted', onAbort);
|
||||
|
||||
const [, unpipeStreamToCounter] = internals.pipe(stream, counter);
|
||||
const [, unpipeCounterToFile] = internals.pipe(counter, file);
|
||||
});
|
||||
|
||||
promise.catch(Hoek.ignore); // Prevent triggering node's PromiseRejectionHandledWarning
|
||||
return promise;
|
||||
};
|
||||
|
||||
internals.cleanupFiles = async (pendingFiles) => {
|
||||
|
||||
const results = await Promise.allSettled(pendingFiles);
|
||||
|
||||
await Promise.all(results.map(async (result) => {
|
||||
|
||||
if (result.value) {
|
||||
await Fsp.unlink(result.value.item.path);
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
internals.part = async function (part, output, set, options) {
|
||||
|
||||
const payload = await Wreck.read(part); // Error handled by dispenser.once('error')
|
||||
|
||||
if (output === 'stream') { // Output: 'stream'
|
||||
const item = Wreck.toReadableStream(payload);
|
||||
item.hapi = {
|
||||
filename: part.filename,
|
||||
headers: part.headers
|
||||
};
|
||||
|
||||
return set(part.name, item);
|
||||
}
|
||||
|
||||
const ct = part.headers['content-type'] || ''; // Output: 'data'
|
||||
const mime = ct.split(';')[0].trim().toLowerCase();
|
||||
const annotate = (value) => set(part.name, output === 'annotated' ? { filename: part.filename, headers: part.headers, payload: value } : value);
|
||||
|
||||
if (!mime) {
|
||||
return annotate(payload);
|
||||
}
|
||||
|
||||
if (!payload.length) {
|
||||
return annotate({});
|
||||
}
|
||||
|
||||
try {
|
||||
const object = internals.object(options, payload, mime);
|
||||
annotate(object);
|
||||
}
|
||||
catch (err) {
|
||||
annotate(payload);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
internals.pipe = function (from, to) {
|
||||
|
||||
const forwardError = (err) => {
|
||||
|
||||
unpipe();
|
||||
to.emit('error', err);
|
||||
};
|
||||
|
||||
const unpipe = () => {
|
||||
|
||||
from.removeListener('error', forwardError);
|
||||
return from.unpipe(to);
|
||||
};
|
||||
|
||||
from.once('error', forwardError);
|
||||
|
||||
return [from.pipe(to), unpipe];
|
||||
};
|
||||
|
||||
internals.Counter = class extends Stream.Transform {
|
||||
|
||||
constructor(options) {
|
||||
|
||||
super();
|
||||
this.bytes = 0;
|
||||
this._maxBytes = options.maxBytes;
|
||||
}
|
||||
|
||||
_transform(chunk, encoding, next) {
|
||||
|
||||
this.bytes = this.bytes + chunk.length;
|
||||
|
||||
if (this._maxBytes !== undefined &&
|
||||
this.bytes > this._maxBytes) {
|
||||
|
||||
return next(Boom.entityTooLarge('Payload content length greater than maximum allowed: ' + this._maxBytes));
|
||||
}
|
||||
|
||||
return next(null, chunk);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user