zero-http 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +311 -0
- package/documentation/controllers/cleanup.js +25 -0
- package/documentation/controllers/echo.js +14 -0
- package/documentation/controllers/headers.js +1 -0
- package/documentation/controllers/proxy.js +112 -0
- package/documentation/controllers/root.js +1 -0
- package/documentation/controllers/uploads.js +289 -0
- package/documentation/full-server.js +129 -0
- package/documentation/public/data/api.json +167 -0
- package/documentation/public/data/examples.json +62 -0
- package/documentation/public/data/options.json +13 -0
- package/documentation/public/index.html +414 -0
- package/documentation/public/prism-overrides.css +40 -0
- package/documentation/public/scripts/app.js +44 -0
- package/documentation/public/scripts/data-sections.js +300 -0
- package/documentation/public/scripts/helpers.js +166 -0
- package/documentation/public/scripts/playground.js +71 -0
- package/documentation/public/scripts/proxy.js +98 -0
- package/documentation/public/scripts/ui.js +210 -0
- package/documentation/public/scripts/uploads.js +459 -0
- package/documentation/public/styles.css +310 -0
- package/documentation/public/vendor/icons/fetch.svg +23 -0
- package/documentation/public/vendor/icons/plug.svg +27 -0
- package/documentation/public/vendor/icons/static.svg +35 -0
- package/documentation/public/vendor/icons/stream.svg +22 -0
- package/documentation/public/vendor/icons/zero.svg +21 -0
- package/documentation/public/vendor/prism-copy-to-clipboard.min.js +27 -0
- package/documentation/public/vendor/prism-javascript.min.js +1 -0
- package/documentation/public/vendor/prism-json.min.js +1 -0
- package/documentation/public/vendor/prism-okaidia.css +1 -0
- package/documentation/public/vendor/prism-toolbar.css +27 -0
- package/documentation/public/vendor/prism-toolbar.min.js +41 -0
- package/documentation/public/vendor/prism.min.js +1 -0
- package/index.js +43 -0
- package/lib/app.js +159 -0
- package/lib/body/index.js +14 -0
- package/lib/body/json.js +54 -0
- package/lib/body/multipart.js +310 -0
- package/lib/body/raw.js +40 -0
- package/lib/body/rawBuffer.js +74 -0
- package/lib/body/sendError.js +17 -0
- package/lib/body/text.js +43 -0
- package/lib/body/typeMatch.js +22 -0
- package/lib/body/urlencoded.js +166 -0
- package/lib/cors.js +72 -0
- package/lib/fetch.js +218 -0
- package/lib/logger.js +68 -0
- package/lib/rateLimit.js +64 -0
- package/lib/request.js +76 -0
- package/lib/response.js +165 -0
- package/lib/router.js +87 -0
- package/lib/static.js +196 -0
- package/package.json +44 -0
package/index.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module zero-http
|
|
3
|
+
* @description Public entry point for the zero-http package.
|
|
4
|
+
* Re-exports every middleware, the app factory, and the fetch helper.
|
|
5
|
+
*/
|
|
6
|
+
const App = require('./lib/app');
|
|
7
|
+
const cors = require('./lib/cors');
|
|
8
|
+
const fetch = require('./lib/fetch');
|
|
9
|
+
const body = require('./lib/body');
|
|
10
|
+
const serveStatic = require('./lib/static');
|
|
11
|
+
const rateLimit = require('./lib/rateLimit');
|
|
12
|
+
const logger = require('./lib/logger');
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
/**
|
|
16
|
+
* Create a new application instance.
|
|
17
|
+
* @returns {import('./lib/app')} Fresh App with an empty middleware stack.
|
|
18
|
+
*/
|
|
19
|
+
createApp: () => new App(),
|
|
20
|
+
/** @see module:cors */
|
|
21
|
+
cors,
|
|
22
|
+
/** @see module:fetch */
|
|
23
|
+
fetch,
|
|
24
|
+
// body parsers
|
|
25
|
+
/** @see module:body/json */
|
|
26
|
+
json: body.json,
|
|
27
|
+
/** @see module:body/urlencoded */
|
|
28
|
+
urlencoded: body.urlencoded,
|
|
29
|
+
/** @see module:body/text */
|
|
30
|
+
text: body.text,
|
|
31
|
+
/** @see module:body/raw */
|
|
32
|
+
raw: body.raw,
|
|
33
|
+
/** @see module:body/multipart */
|
|
34
|
+
multipart: body.multipart,
|
|
35
|
+
// serving
|
|
36
|
+
/** @see module:static */
|
|
37
|
+
static: serveStatic,
|
|
38
|
+
// middleware
|
|
39
|
+
/** @see module:rateLimit */
|
|
40
|
+
rateLimit,
|
|
41
|
+
/** @see module:logger */
|
|
42
|
+
logger,
|
|
43
|
+
};
|
package/lib/app.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module app
|
|
3
|
+
* @description Express-like HTTP application with middleware pipeline and
|
|
4
|
+
* method-based routing. Created via `createApp()` in the public API.
|
|
5
|
+
*/
|
|
6
|
+
const http = require('http');
|
|
7
|
+
const Router = require('./router');
|
|
8
|
+
const Request = require('./request');
|
|
9
|
+
const Response = require('./response');
|
|
10
|
+
|
|
11
|
+
class App
|
|
12
|
+
{
|
|
13
|
+
/**
|
|
14
|
+
* Create a new App instance.
|
|
15
|
+
* Initialises an empty middleware stack, a {@link Router}, and binds
|
|
16
|
+
* `this.handler` for direct use with `http.createServer()`.
|
|
17
|
+
*
|
|
18
|
+
* @constructor
|
|
19
|
+
*/
|
|
20
|
+
constructor()
|
|
21
|
+
{
|
|
22
|
+
/** @type {Router} */
|
|
23
|
+
this.router = new Router();
|
|
24
|
+
/** @type {Function[]} */
|
|
25
|
+
this.middlewares = [];
|
|
26
|
+
/** @type {Function|null} */
|
|
27
|
+
this._errorHandler = null;
|
|
28
|
+
|
|
29
|
+
// Bind for use as `http.createServer(app.handler)`
|
|
30
|
+
this.handler = (req, res) => this.handle(req, res);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Register middleware.
|
|
35
|
+
* - `use(fn)` — global middleware applied to every request.
|
|
36
|
+
* - `use('/prefix', fn)` — path-scoped middleware (strips the prefix
|
|
37
|
+
* before calling `fn` so downstream sees relative paths).
|
|
38
|
+
*
|
|
39
|
+
* @param {string|Function} pathOrFn - A path prefix string, or middleware function.
|
|
40
|
+
* @param {Function} [fn] - Middleware function when first arg is a path.
|
|
41
|
+
*/
|
|
42
|
+
use(pathOrFn, fn)
|
|
43
|
+
{
|
|
44
|
+
if (typeof pathOrFn === 'function')
|
|
45
|
+
{
|
|
46
|
+
this.middlewares.push(pathOrFn);
|
|
47
|
+
}
|
|
48
|
+
else if (typeof pathOrFn === 'string' && typeof fn === 'function')
|
|
49
|
+
{
|
|
50
|
+
const prefix = pathOrFn.endsWith('/') ? pathOrFn.slice(0, -1) : pathOrFn;
|
|
51
|
+
this.middlewares.push((req, res, next) =>
|
|
52
|
+
{
|
|
53
|
+
const urlPath = req.url.split('?')[0];
|
|
54
|
+
if (urlPath === prefix || urlPath.startsWith(prefix + '/'))
|
|
55
|
+
{
|
|
56
|
+
// strip prefix from url so downstream sees relative paths
|
|
57
|
+
const origUrl = req.url;
|
|
58
|
+
req.url = req.url.slice(prefix.length) || '/';
|
|
59
|
+
fn(req, res, () => { req.url = origUrl; next(); });
|
|
60
|
+
}
|
|
61
|
+
else
|
|
62
|
+
{
|
|
63
|
+
next();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Register a global error handler.
|
|
71
|
+
* The handler receives `(err, req, res, next)` and is invoked whenever
|
|
72
|
+
* a middleware or route handler throws or passes an error to `next(err)`.
|
|
73
|
+
*
|
|
74
|
+
* @param {Function} fn - Error-handling function `(err, req, res, next) => void`.
|
|
75
|
+
*/
|
|
76
|
+
onError(fn)
|
|
77
|
+
{
|
|
78
|
+
this._errorHandler = fn;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Core request handler. Wraps the raw Node `req`/`res` in
|
|
83
|
+
* {@link Request}/{@link Response} wrappers, runs the middleware
|
|
84
|
+
* pipeline, then falls through to the router.
|
|
85
|
+
*
|
|
86
|
+
* @param {import('http').IncomingMessage} req - Raw Node request.
|
|
87
|
+
* @param {import('http').ServerResponse} res - Raw Node response.
|
|
88
|
+
*/
|
|
89
|
+
handle(req, res)
|
|
90
|
+
{
|
|
91
|
+
const request = new Request(req);
|
|
92
|
+
const response = new Response(res);
|
|
93
|
+
|
|
94
|
+
let idx = 0;
|
|
95
|
+
const run = (err) =>
|
|
96
|
+
{
|
|
97
|
+
if (err)
|
|
98
|
+
{
|
|
99
|
+
if (this._errorHandler) return this._errorHandler(err, request, response, run);
|
|
100
|
+
response.status(500).json({ error: err.message || 'Internal Server Error' });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (idx < this.middlewares.length)
|
|
104
|
+
{
|
|
105
|
+
const mw = this.middlewares[idx++];
|
|
106
|
+
try
|
|
107
|
+
{
|
|
108
|
+
const result = mw(request, response, run);
|
|
109
|
+
// Handle promise-returning middleware
|
|
110
|
+
if (result && typeof result.catch === 'function')
|
|
111
|
+
{
|
|
112
|
+
result.catch(run);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (e)
|
|
116
|
+
{
|
|
117
|
+
run(e);
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
this.router.handle(request, response);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
run();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Start listening for HTTP connections.
|
|
129
|
+
*
|
|
130
|
+
* @param {number} [port=3000] - Port number to bind.
|
|
131
|
+
* @param {Function} [cb] - Callback invoked once the server is listening.
|
|
132
|
+
* @returns {import('http').Server} The underlying Node HTTP server.
|
|
133
|
+
*/
|
|
134
|
+
listen(port = 3000, cb)
|
|
135
|
+
{
|
|
136
|
+
const server = http.createServer(this.handler);
|
|
137
|
+
return server.listen(port, cb);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Register one or more handler functions for a specific HTTP method and path.
|
|
142
|
+
*
|
|
143
|
+
* @param {string} method - HTTP method (GET, POST, etc.) or 'ALL'.
|
|
144
|
+
* @param {string} path - Route pattern (e.g. '/users/:id').
|
|
145
|
+
* @param {...Function} fns - Handler functions `(req, res, next) => void`.
|
|
146
|
+
*/
|
|
147
|
+
route(method, path, ...fns) { this.router.add(method, path, fns); }
|
|
148
|
+
|
|
149
|
+
/** @see App#route — shortcut for GET requests. */ get(path, ...fns) { this.route('GET', path, ...fns); }
|
|
150
|
+
/** @see App#route — shortcut for POST requests. */ post(path, ...fns) { this.route('POST', path, ...fns); }
|
|
151
|
+
/** @see App#route — shortcut for PUT requests. */ put(path, ...fns) { this.route('PUT', path, ...fns); }
|
|
152
|
+
/** @see App#route — shortcut for DELETE requests. */ delete(path, ...fns) { this.route('DELETE', path, ...fns); }
|
|
153
|
+
/** @see App#route — shortcut for PATCH requests. */ patch(path, ...fns) { this.route('PATCH', path, ...fns); }
|
|
154
|
+
/** @see App#route — shortcut for OPTIONS requests.*/ options(path, ...fns) { this.route('OPTIONS', path, ...fns); }
|
|
155
|
+
/** @see App#route — shortcut for HEAD requests. */ head(path, ...fns) { this.route('HEAD', path, ...fns); }
|
|
156
|
+
/** @see App#route — matches every HTTP method. */ all(path, ...fns) { this.route('ALL', path, ...fns); }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = App;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module body
|
|
3
|
+
* @description Barrel export for all body-parsing utilities and middleware.
|
|
4
|
+
*/
|
|
5
|
+
const rawBuffer = require('./rawBuffer');
|
|
6
|
+
const isTypeMatch = require('./typeMatch');
|
|
7
|
+
const sendError = require('./sendError');
|
|
8
|
+
const json = require('./json');
|
|
9
|
+
const urlencoded = require('./urlencoded');
|
|
10
|
+
const text = require('./text');
|
|
11
|
+
const raw = require('./raw');
|
|
12
|
+
const multipart = require('./multipart');
|
|
13
|
+
|
|
14
|
+
module.exports = { rawBuffer, isTypeMatch, sendError, json, urlencoded, text, raw, multipart };
|
package/lib/body/json.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module body/json
|
|
3
|
+
* @description JSON body-parsing middleware.
|
|
4
|
+
* Reads the request body, parses it as JSON, and sets `req.body`.
|
|
5
|
+
*/
|
|
6
|
+
const rawBuffer = require('./rawBuffer');
|
|
7
|
+
const isTypeMatch = require('./typeMatch');
|
|
8
|
+
const sendError = require('./sendError');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create a JSON body-parsing middleware.
|
|
12
|
+
*
|
|
13
|
+
* @param {object} [options]
|
|
14
|
+
* @param {string|number} [options.limit] - Max body size (e.g. `'10kb'`).
|
|
15
|
+
* @param {Function} [options.reviver] - `JSON.parse` reviver function.
|
|
16
|
+
* @param {boolean} [options.strict=true] - When true, reject non-object/array roots.
|
|
17
|
+
* @param {string|Function} [options.type='application/json'] - Content-Type to match.
|
|
18
|
+
* @returns {Function} Async middleware `(req, res, next) => void`.
|
|
19
|
+
*/
|
|
20
|
+
function json(options = {})
|
|
21
|
+
{
|
|
22
|
+
const opts = options || {};
|
|
23
|
+
const limit = opts.limit || null;
|
|
24
|
+
const reviver = opts.reviver;
|
|
25
|
+
const strict = (opts.hasOwnProperty('strict')) ? !!opts.strict : true;
|
|
26
|
+
const typeOpt = opts.type || 'application/json';
|
|
27
|
+
|
|
28
|
+
return async (req, res, next) =>
|
|
29
|
+
{
|
|
30
|
+
const ct = (req.headers['content-type'] || '');
|
|
31
|
+
if (!isTypeMatch(ct, typeOpt)) return next();
|
|
32
|
+
try
|
|
33
|
+
{
|
|
34
|
+
const buf = await rawBuffer(req, { limit });
|
|
35
|
+
const txt = buf.toString('utf8');
|
|
36
|
+
if (!txt) { req.body = null; return next(); }
|
|
37
|
+
let parsed;
|
|
38
|
+
try { parsed = JSON.parse(txt, reviver); } catch (e) { req.body = null; return next(); }
|
|
39
|
+
if (strict && (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed) === false && Object.keys(parsed).length === 0 && !Array.isArray(parsed)))
|
|
40
|
+
{
|
|
41
|
+
// If strict, prefer objects/arrays; allow arrays but reject primitives
|
|
42
|
+
if (typeof parsed !== 'object') { req.body = null; return next(); }
|
|
43
|
+
}
|
|
44
|
+
req.body = parsed;
|
|
45
|
+
} catch (err)
|
|
46
|
+
{
|
|
47
|
+
if (err && err.status === 413) return sendError(res, 413, 'payload too large');
|
|
48
|
+
req.body = null;
|
|
49
|
+
}
|
|
50
|
+
next();
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = json;
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module body/multipart
|
|
3
|
+
* @description Streaming multipart/form-data parser.
|
|
4
|
+
* Writes uploaded files to a temp directory and collects
|
|
5
|
+
* form fields. Sets `req.body = { fields, files }`.
|
|
6
|
+
*/
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const sendError = require('./sendError');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generate a unique filename with an optional prefix.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} [prefix='miniex'] - Filename prefix.
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
function uniqueName(prefix = 'miniex')
|
|
19
|
+
{
|
|
20
|
+
return `${prefix}-${Date.now()}-${Math.floor(Math.random() * 1e9)}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Recursively create a directory if it doesn't exist.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} dir - Directory path.
|
|
27
|
+
*/
|
|
28
|
+
function ensureDir(dir)
|
|
29
|
+
{
|
|
30
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch (e) { }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse raw MIME header text (CRLF-separated) into a plain object.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} headerText - Raw header block.
|
|
37
|
+
* @returns {Object<string, string>} Lower-cased header key/value map.
|
|
38
|
+
*/
|
|
39
|
+
function parseHeaders(headerText)
|
|
40
|
+
{
|
|
41
|
+
const lines = headerText.split('\r\n');
|
|
42
|
+
const obj = {};
|
|
43
|
+
for (const l of lines)
|
|
44
|
+
{
|
|
45
|
+
const idx = l.indexOf(':');
|
|
46
|
+
if (idx === -1) continue;
|
|
47
|
+
const k = l.slice(0, idx).trim().toLowerCase();
|
|
48
|
+
const v = l.slice(idx + 1).trim();
|
|
49
|
+
obj[k] = v;
|
|
50
|
+
}
|
|
51
|
+
return obj;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Extract `name` and `filename` fields from a `Content-Disposition` header.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} cd - Content-Disposition value.
|
|
58
|
+
* @returns {Object<string, string>} Parsed disposition parameters.
|
|
59
|
+
*/
|
|
60
|
+
function parseContentDisposition(cd)
|
|
61
|
+
{
|
|
62
|
+
const m = /form-data;(.*)/i.exec(cd);
|
|
63
|
+
if (!m) return {};
|
|
64
|
+
const parts = m[1].split(';').map(s => s.trim());
|
|
65
|
+
const out = {};
|
|
66
|
+
for (const p of parts)
|
|
67
|
+
{
|
|
68
|
+
const mm = /([^=]+)="?([^"]+)"?/.exec(p);
|
|
69
|
+
if (mm) out[mm[1]] = mm[2];
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Create a streaming multipart/form-data parsing middleware.
|
|
76
|
+
*
|
|
77
|
+
* @param {object} [opts]
|
|
78
|
+
* @param {string} [opts.dir] - Upload directory (default: OS temp dir).
|
|
79
|
+
* @param {number} [opts.maxFileSize] - Maximum file size in bytes.
|
|
80
|
+
* @returns {Function} Async middleware `(req, res, next) => void`.
|
|
81
|
+
*/
|
|
82
|
+
function multipart(opts = {})
|
|
83
|
+
{
|
|
84
|
+
return async (req, res, next) =>
|
|
85
|
+
{
|
|
86
|
+
const ct = req.headers['content-type'] || '';
|
|
87
|
+
const m = /boundary=(?:"([^"]+)"|([^;\s]+))/i.exec(ct);
|
|
88
|
+
if (!m) return next();
|
|
89
|
+
const boundary = (m[1] || m[2] || '').replace(/^"|"$/g, '');
|
|
90
|
+
const dashBoundary = `--${boundary}`;
|
|
91
|
+
const dashBoundaryBuf = Buffer.from('\r\n' + dashBoundary);
|
|
92
|
+
const startBoundaryBuf = Buffer.from(dashBoundary);
|
|
93
|
+
|
|
94
|
+
let tmpDir;
|
|
95
|
+
if (opts.dir)
|
|
96
|
+
{
|
|
97
|
+
tmpDir = path.isAbsolute(opts.dir) ? opts.dir : path.join(process.cwd(), opts.dir);
|
|
98
|
+
} else
|
|
99
|
+
{
|
|
100
|
+
tmpDir = path.join(os.tmpdir(), 'zero-http-uploads');
|
|
101
|
+
}
|
|
102
|
+
const maxFileSize = opts.maxFileSize || null; // bytes
|
|
103
|
+
ensureDir(tmpDir);
|
|
104
|
+
|
|
105
|
+
const fields = {};
|
|
106
|
+
const files = {};
|
|
107
|
+
|
|
108
|
+
let buffer = Buffer.alloc(0);
|
|
109
|
+
let state = 'start'; // start, headers, body
|
|
110
|
+
let current = null; // { headers, name, filename, contentType, writeStream, collectedSize }
|
|
111
|
+
|
|
112
|
+
const pendingWrites = [];
|
|
113
|
+
|
|
114
|
+
function abortFileTooLarge()
|
|
115
|
+
{
|
|
116
|
+
try { current.writeStream.end(); } catch (e) { }
|
|
117
|
+
try { fs.unlinkSync(current.filePath); } catch (e) { }
|
|
118
|
+
if (!req._multipartErrorHandled)
|
|
119
|
+
{
|
|
120
|
+
req._multipartErrorHandled = true;
|
|
121
|
+
sendError(res, 413, 'file too large');
|
|
122
|
+
req.raw.pause && req.raw.pause();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function closeCurrent()
|
|
127
|
+
{
|
|
128
|
+
if (!current) return;
|
|
129
|
+
if (current.writeStream)
|
|
130
|
+
{
|
|
131
|
+
// end the stream and record file after it's flushed to disk
|
|
132
|
+
// capture values so we don't rely on `current` later
|
|
133
|
+
const info = { name: current.name, filename: current.filename, filePath: current.filePath, contentType: current.contentType, size: current.collectedSize };
|
|
134
|
+
const p = new Promise((resolve) =>
|
|
135
|
+
{
|
|
136
|
+
current.writeStream.on('finish', () =>
|
|
137
|
+
{
|
|
138
|
+
files[info.name] = { originalFilename: info.filename, storedName: path.basename(info.filePath), path: info.filePath, contentType: info.contentType, size: info.size };
|
|
139
|
+
resolve();
|
|
140
|
+
});
|
|
141
|
+
current.writeStream.on('error', () =>
|
|
142
|
+
{
|
|
143
|
+
resolve();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
pendingWrites.push(p);
|
|
147
|
+
current.writeStream.end();
|
|
148
|
+
} else
|
|
149
|
+
{
|
|
150
|
+
fields[current.name] = current.value || '';
|
|
151
|
+
}
|
|
152
|
+
current = null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
req.raw.on('data', (chunk) =>
|
|
156
|
+
{
|
|
157
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
158
|
+
|
|
159
|
+
while (true)
|
|
160
|
+
{
|
|
161
|
+
if (state === 'start')
|
|
162
|
+
{
|
|
163
|
+
// look for starting boundary
|
|
164
|
+
const idx = buffer.indexOf(startBoundaryBuf);
|
|
165
|
+
if (idx === -1)
|
|
166
|
+
{
|
|
167
|
+
// boundary not yet found
|
|
168
|
+
if (buffer.length > startBoundaryBuf.length) buffer = buffer.slice(buffer.length - startBoundaryBuf.length);
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
// consume up to after boundary and CRLF
|
|
172
|
+
const after = idx + startBoundaryBuf.length;
|
|
173
|
+
if (buffer.length < after + 2) break; // wait for CRLF
|
|
174
|
+
buffer = buffer.slice(after);
|
|
175
|
+
if (buffer.slice(0, 2).toString() === '\r\n') buffer = buffer.slice(2);
|
|
176
|
+
state = 'headers';
|
|
177
|
+
} else if (state === 'headers')
|
|
178
|
+
{
|
|
179
|
+
const idx = buffer.indexOf('\r\n\r\n');
|
|
180
|
+
if (idx === -1)
|
|
181
|
+
{
|
|
182
|
+
// wait for more
|
|
183
|
+
if (buffer.length > 1024 * 1024)
|
|
184
|
+
{
|
|
185
|
+
// keep buffer bounded
|
|
186
|
+
buffer = buffer.slice(buffer.length - 1024 * 16);
|
|
187
|
+
}
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
const headerText = buffer.slice(0, idx).toString('utf8');
|
|
191
|
+
buffer = buffer.slice(idx + 4);
|
|
192
|
+
const hdrs = parseHeaders(headerText);
|
|
193
|
+
const disp = hdrs['content-disposition'] || '';
|
|
194
|
+
const cd = parseContentDisposition(disp);
|
|
195
|
+
const name = cd.name;
|
|
196
|
+
const filename = cd.filename;
|
|
197
|
+
const contentType = hdrs['content-type'] || null;
|
|
198
|
+
current = { headers: hdrs, name, filename, contentType, collectedSize: 0 };
|
|
199
|
+
if (filename)
|
|
200
|
+
{
|
|
201
|
+
// create temp file; preserve the original extension when possible
|
|
202
|
+
const ext = path.extname(filename) || '';
|
|
203
|
+
const safeExt = ext.replace(/[^a-z0-9.]/gi, '');
|
|
204
|
+
let fname = uniqueName('upload');
|
|
205
|
+
if (safeExt) fname = fname + (safeExt.startsWith('.') ? safeExt : ('.' + safeExt));
|
|
206
|
+
const filePath = path.join(tmpDir, fname);
|
|
207
|
+
current.filePath = filePath;
|
|
208
|
+
current.writeStream = fs.createWriteStream(filePath);
|
|
209
|
+
} else
|
|
210
|
+
{
|
|
211
|
+
current.value = '';
|
|
212
|
+
}
|
|
213
|
+
state = 'body';
|
|
214
|
+
} else if (state === 'body')
|
|
215
|
+
{
|
|
216
|
+
// look for boundary preceded by CRLF
|
|
217
|
+
const idx = buffer.indexOf(dashBoundaryBuf);
|
|
218
|
+
if (idx === -1)
|
|
219
|
+
{
|
|
220
|
+
// keep tail in buffer to match partial boundary
|
|
221
|
+
const keep = Math.max(dashBoundaryBuf.length, 1024);
|
|
222
|
+
const writeLen = buffer.length - keep;
|
|
223
|
+
if (writeLen > 0)
|
|
224
|
+
{
|
|
225
|
+
const toWrite = buffer.slice(0, writeLen);
|
|
226
|
+
if (current.writeStream)
|
|
227
|
+
{
|
|
228
|
+
current.writeStream.write(toWrite);
|
|
229
|
+
current.collectedSize += toWrite.length;
|
|
230
|
+
if (maxFileSize && current.collectedSize > maxFileSize)
|
|
231
|
+
{
|
|
232
|
+
abortFileTooLarge();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
} else
|
|
236
|
+
{
|
|
237
|
+
current.value += toWrite.toString('utf8');
|
|
238
|
+
}
|
|
239
|
+
buffer = buffer.slice(writeLen);
|
|
240
|
+
}
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
// boundary found at idx; data before idx is body chunk (without the leading CRLF)
|
|
244
|
+
const bodyChunk = buffer.slice(0, idx);
|
|
245
|
+
// if bodyChunk starts with CRLF, strip it
|
|
246
|
+
const toWrite = (bodyChunk.slice(0, 2).toString() === '\r\n') ? bodyChunk.slice(2) : bodyChunk;
|
|
247
|
+
if (toWrite.length)
|
|
248
|
+
{
|
|
249
|
+
if (current.writeStream)
|
|
250
|
+
{
|
|
251
|
+
current.writeStream.write(toWrite);
|
|
252
|
+
current.collectedSize += toWrite.length;
|
|
253
|
+
if (maxFileSize && current.collectedSize > maxFileSize)
|
|
254
|
+
{
|
|
255
|
+
abortFileTooLarge();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
} else
|
|
259
|
+
{
|
|
260
|
+
current.value += toWrite.toString('utf8');
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// consume boundary marker
|
|
264
|
+
buffer = buffer.slice(idx + dashBoundaryBuf.length);
|
|
265
|
+
// check for final boundary '--'
|
|
266
|
+
if (buffer.slice(0, 2).toString() === '--')
|
|
267
|
+
{
|
|
268
|
+
// final
|
|
269
|
+
closeCurrent();
|
|
270
|
+
// wait for any pending file flushes then continue
|
|
271
|
+
req.raw.pause && req.raw.pause();
|
|
272
|
+
Promise.all(pendingWrites).then(() =>
|
|
273
|
+
{
|
|
274
|
+
req.body = { fields, files };
|
|
275
|
+
req._multipart = true;
|
|
276
|
+
return next();
|
|
277
|
+
}).catch(() =>
|
|
278
|
+
{
|
|
279
|
+
req.body = { fields, files };
|
|
280
|
+
req._multipart = true;
|
|
281
|
+
return next();
|
|
282
|
+
});
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
// trim leading CRLF if present
|
|
286
|
+
if (buffer.slice(0, 2).toString() === '\r\n') buffer = buffer.slice(2);
|
|
287
|
+
// close current and continue to next headers
|
|
288
|
+
closeCurrent();
|
|
289
|
+
state = 'headers';
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
req.raw.on('end', () =>
|
|
295
|
+
{
|
|
296
|
+
// finish any current
|
|
297
|
+
if (current) closeCurrent();
|
|
298
|
+
req.body = { fields, files };
|
|
299
|
+
req._multipart = true;
|
|
300
|
+
next();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
req.raw.on('error', (err) =>
|
|
304
|
+
{
|
|
305
|
+
next();
|
|
306
|
+
});
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
module.exports = multipart;
|
package/lib/body/raw.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module body/raw
|
|
3
|
+
* @description Raw-buffer body-parsing middleware.
|
|
4
|
+
* Stores the full request body as a Buffer on `req.body`.
|
|
5
|
+
*/
|
|
6
|
+
const rawBuffer = require('./rawBuffer');
|
|
7
|
+
const isTypeMatch = require('./typeMatch');
|
|
8
|
+
const sendError = require('./sendError');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create a raw-buffer body-parsing middleware.
|
|
12
|
+
*
|
|
13
|
+
* @param {object} [options]
|
|
14
|
+
* @param {string|number} [options.limit] - Max body size.
|
|
15
|
+
* @param {string|Function} [options.type='application/octet-stream'] - Content-Type to match.
|
|
16
|
+
* @returns {Function} Async middleware `(req, res, next) => void`.
|
|
17
|
+
*/
|
|
18
|
+
function raw(options = {})
|
|
19
|
+
{
|
|
20
|
+
const opts = options || {};
|
|
21
|
+
const limit = opts.limit || null;
|
|
22
|
+
const typeOpt = opts.type || 'application/octet-stream';
|
|
23
|
+
|
|
24
|
+
return async (req, res, next) =>
|
|
25
|
+
{
|
|
26
|
+
const ct = (req.headers['content-type'] || '');
|
|
27
|
+
if (!isTypeMatch(ct, typeOpt)) return next();
|
|
28
|
+
try
|
|
29
|
+
{
|
|
30
|
+
req.body = await rawBuffer(req, { limit });
|
|
31
|
+
} catch (err)
|
|
32
|
+
{
|
|
33
|
+
if (err && err.status === 413) return sendError(res, 413, 'payload too large');
|
|
34
|
+
req.body = Buffer.alloc(0);
|
|
35
|
+
}
|
|
36
|
+
next();
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = raw;
|