New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details
Socket
Book a DemoSign in
Socket

drift

Package Overview
Dependencies
Maintainers
1
Versions
33
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

drift - npm Package Compare versions

Comparing version
15.1.0
to
16.0.0
+963
-1
index.js

@@ -1,1 +0,963 @@

module.exports = require('./src/server.js');
const _http = require('http');
const _https = require('https');
const Fiber = require('fibers');
function sendError(res, error) {
console.log(error.stack || error.toString());
if (!res.headersSent) {
const status = 500;
const headers = { 'Content-Type': 'text/plain' };
const body = error.stack;
res.writeHead(status, 'Internal Error', headers);
res.write(body + '\n');
}
res.end();
}
class EventEmitter {
listenerMap = {};
on(key, listener) {
return this.addListener(key, listener);
}
addListener(key, listener) {
const { listenerMap } = this;
const listeners = listenerMap[key] ?? (listenerMap[key] = new Set());
listeners.add(listener);
return () => {
listeners.delete(listener);
};
}
off(key, listener) {
return this.removeListener(key, listener);
}
removeListener(key, listener) {
const listeners = this.listenerMap[key];
if (listeners) {
listeners.delete(listener);
}
}
emit(key, ...args) {
const listeners = this.listenerMap[key];
if (listeners) {
for (const listener of listeners) {
listener(...args);
}
}
}
}
const RE_VERB = /^([A-Z]+):(.*)/;
class Router {
_routes = [];
addRoute(pattern, handler) {
this._routes.push(parseRoute(pattern, handler));
}
route(method, url, ...routeArgs) {
for (const route of this._routes) {
if (route.method !== '*' && route.method !== method) {
continue;
}
const captures = route.matcher(url);
if (captures) {
route.handler(routeArgs, captures);
}
}
}
}
function parseRoute(rawPattern, fn) {
const match = RE_VERB.exec(rawPattern);
const method = match ? String(match[1]) : '*';
const pattern = match ? String(match[2]) : rawPattern;
return {
method,
matcher: getMatcher(pattern),
handler: (routeArgs, captures) => {
return fn(routeArgs[0], routeArgs[1], ...captures);
},
};
}
function getMatcher(pattern) {
const patternSegments = pattern.slice(1).split('/');
return (url) => {
const urlSegments = url.slice(1).split('/');
if (patternSegments.length !== urlSegments.length) {
return null;
}
const captures = [];
for (let i = 0; i < urlSegments.length; i++) {
const patternSegment = patternSegments[i] ?? '';
const urlSegment = urlSegments[i] ?? '';
if (patternSegment.charAt(0) === ':') {
captures.push(urlSegment);
} else if (patternSegment !== urlSegment) {
return null;
}
}
return captures;
};
}
const CHARS = /[^\w!$'()*,-.\/:;@[\\\]^{|}~]+/g;
const qs = {
escape: function (value) {
return String(value).replace(CHARS, (s) => encodeURIComponent(s));
},
unescape: function (value) {
const s = String(value).replace(/\+/g, ' ');
try {
return decodeURIComponent(s);
} catch (e) {
return unescape(s);
}
},
stringify: function (obj) {
const arr = [];
const keys = Object.keys(obj);
for (const key of keys) {
const name = qs.escape(key);
const value = obj[key];
if (Array.isArray(value)) {
for (const val of value) {
arr.push(name + '=' + qs.escape(val));
}
} else {
arr.push(name + '=' + qs.escape(value));
}
}
return arr.join('&');
},
parse: function (str, opts = {}) {
const { lcase = true, flatten = true } = opts;
const obj = {};
if (typeof str === 'string') {
if (str.charAt(0) === '?') {
str = str.slice(1);
}
for (const part of str.split('&')) {
let pos = part.indexOf('=');
if (pos < 0) {
pos = part.length;
}
let key = part.slice(0, pos);
const val = part.slice(pos + 1);
if (!key) {
continue;
}
key = qs.unescape(key);
if (lcase) {
key = key.toLowerCase();
}
const existing = obj[key];
if (Array.isArray(existing)) {
existing.push(qs.unescape(val));
} else {
obj[key] = [qs.unescape(val)];
}
}
}
if (flatten) {
qs.flatten(obj);
}
return obj;
},
flatten: function (obj) {
for (const [key, value] of Object.entries(obj)) {
obj[key] = Array.isArray(value) ? value.join(', ') : value;
}
},
encode: (value) => {
return qs.escape(value);
},
decode: (value) => {
return qs.unescape(value);
},
};
function abortCurrentFiber() {
const fiber = Fiber.current;
process.nextTick(() => {
fiber?.reset();
});
Fiber.yield();
}
function fiberize(fn) {
const arity = fn.length;
return function (...args) {
const fiber = Fiber.current;
let err;
let result;
let yielded = false;
let syncCallbackCalled = false;
const syncCallback = function (callbackError, ...results) {
if (syncCallbackCalled) {
return;
}
syncCallbackCalled = true;
if (callbackError) {
err = callbackError;
} else {
result = results.length > 1 ? results : results[0];
}
if (yielded) {
fiber?.run();
}
};
if (args.length + 1 < arity) {
args[arity - 1] = syncCallback;
} else {
args.push(syncCallback);
}
fn.apply(this, args);
if (!syncCallbackCalled) {
yielded = true;
Fiber.yield();
}
if (err) {
throw err;
}
return result;
};
}
const MAX_BUFFER_SIZE = process.env.REQ_BODY_MAX_BUFFER_SIZE
? parseInt(process.env.REQ_BODY_MAX_BUFFER_SIZE, 10)
: 6291456;
class BodyParser extends EventEmitter {
headers;
readStream;
opts;
parse;
constructor(headers, readStream, opts = {}) {
super();
this.headers = headers;
this.readStream = readStream;
this.opts = opts;
const bodyParser = this;
this.parse = fiberize(function (_, callback) {
bodyParser.parseCallback(callback);
});
}
bufferReqBody(callback) {
const { headers, readStream, opts } = this;
const buffer = [];
let size = 0;
const expected = getContentLength(headers);
readStream.on('data', (data) => {
size += data.length;
if (size > MAX_BUFFER_SIZE || (expected != null && size > expected)) {
readStream.pause();
callback(new Error('413 Request Entity Too Large'));
return;
}
buffer.push(data);
});
readStream.on('error', (err) => {
callback(err);
});
readStream.on('end', () => {
const data = Buffer.concat(buffer);
callback(null, data.toString(opts.encoding || 'utf8'));
});
}
processFormBody() {
this.bufferReqBody((err, body) => {
if (err) {
this.emit('error', err);
return;
}
const parsed = qs.parse(body ?? '');
this.emit('end', parsed);
});
}
processJSONBody() {
this.bufferReqBody((err, body) => {
if (err) {
this.emit('error', err);
return;
}
let parsed;
try {
parsed = JSON.parse(body ?? 'null');
} catch (e) {
this.emit('error', new Error('Invalid JSON Body'));
return;
}
this.emit('end', isPrimitive$1(parsed) ? { '': parsed } : parsed);
});
}
parseCallback(callback) {
this.on('error', (err) => {
callback(err);
});
this.on('end', (parsed) => {
callback(null, parsed);
});
const headers = this.headers;
const expectedLength = getContentLength(headers);
if (expectedLength === 0) {
this.emit('end', {});
return;
}
const contentType = (headers['content-type'] || '').toString();
const normalizedContentType = contentType.toLowerCase().split(';')[0];
if (!normalizedContentType) {
this.emit('error', new Error('415 Content-Type Required'));
return;
}
switch (normalizedContentType) {
case 'application/x-www-form-urlencoded': {
this.processFormBody();
break;
}
case 'application/json': {
this.processJSONBody();
break;
}
default: {
callback(new Error(`Invalid content type "${normalizedContentType}"`));
}
}
this.readStream.resume();
}
}
function getContentLength(headers) {
const contentLength = headers['content-length'];
if (typeof contentLength === 'string') {
return parseInt(contentLength, 10) || 0;
}
return undefined;
}
function isPrimitive$1(obj) {
return obj !== Object(obj);
}
const HTTP_METHODS = { GET: 1, HEAD: 1, POST: 1, PUT: 1, DELETE: 1 };
const BODY_ALLOWED$1 = { POST: 1, PUT: 1 };
class Request$1 extends EventEmitter {
_super;
_headers;
_cookies;
_url;
_method;
_query;
_body;
res;
constructor(req) {
super();
this._super = req;
req.on('end', () => {
this.emit('end');
});
}
url(part) {
const url = this._url || (this._url = parseURL(this._super.getURL()));
return part ? url[part] : url.raw;
}
getMethod() {
const override = (
this.headers('X-HTTP-Method-Override') ||
this.query('_method') ||
''
).toUpperCase();
const method = this._super.getMethod() ?? '';
return override in HTTP_METHODS ? override : method.toUpperCase();
}
method(...args) {
const method = this._method || (this._method = this.getMethod());
if (args.length) {
const [n] = args;
return n.toUpperCase() === method;
} else {
return method;
}
}
getRemoteIP() {
return this._super.getRemoteAddress();
}
headers(...args) {
const headers = this._headers || (this._headers = this._super.getHeaders());
if (args.length) {
const [n] = args;
return normalizeHeaderValue(headers[n.toLowerCase()]) || '';
} else {
return headers;
}
}
cookies(n) {
const cookies =
this._cookies || (this._cookies = parseCookies(this.headers('cookie')));
if (n !== undefined) {
return cookies[n.toLowerCase()] || '';
} else {
return cookies;
}
}
query(...args) {
const query = this._query || (this._query = qs.parse(this.url('qs')));
if (args.length) {
const [n] = args;
return query[n.toLowerCase()] || '';
} else {
return query;
}
}
body(...args) {
const body = this._body || (this._body = this._maybeParseBody());
if (args.length) {
const [n] = args;
return body[n.toLowerCase()];
} else {
return body;
}
}
_maybeParseBody() {
let body;
try {
body = this.method() in BODY_ALLOWED$1 ? this._parseBody() : {};
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e));
this.emit('parse-error', error);
if (error.message.match(/^\d{3}\b/)) {
this.res?.die(error.message);
} else {
this.res?.die(400, {
error: 'Unable to parse request body; ' + error.message,
});
}
return {};
}
return body;
}
_parseBody() {
const parser = new BodyParser(this.headers(), this._super.getReadStream());
return parser.parse();
}
}
const REG_COOKIE_SEP = /[;,] */;
function parseURL(url) {
const pos = url.indexOf('?');
const search = pos > 0 ? url.slice(pos) : '';
const rawPath = search ? url.slice(0, pos) : url;
return {
raw: url,
rawPath: rawPath,
path: qs.unescape(rawPath),
search: search,
qs: search.slice(1),
};
}
function parseCookies(str) {
str = str == null ? '' : String(str);
const cookies = {};
for (const part of str.split(REG_COOKIE_SEP)) {
let index = part.indexOf('=');
if (index < 0) {
index = part.length;
}
const key = part.slice(0, index).trim().toLowerCase();
if (!key) {
continue;
}
let value = part.slice(index + 1).trim();
if (value[0] === '"') {
value = value.slice(1, -1);
}
value = qs.unescape(value);
cookies[key] = cookies[key] ? cookies[key] + ', ' + value : value;
}
return cookies;
}
function normalizeHeaderValue(value) {
return Array.isArray(value) ? value.join(', ') : value;
}
const RE_CONTENT_TYPE = /^[\w-]+\/[\w-]+$/;
const RE_STATUS = /^\d{3}\b/;
const TEXT_CONTENT_TYPES = /^text\/|\/json$/i;
const httpResHeadersStr =
'Accept-Ranges Age Allow Cache-Control Connection Content-Encoding Content-Language ' +
'Content-Length Content-Location Content-MD5 Content-Disposition Content-Range Content-Type Date ETag Expires ' +
'Last-Modified Link Location P3P Pragma Proxy-Authenticate Refresh Retry-After Server Set-Cookie ' +
'Strict-Transport-Security Trailer Transfer-Encoding Vary Via Warning WWW-Authenticate X-Frame-Options ' +
'X-XSS-Protection X-Content-Type-Options X-Forwarded-Proto Front-End-Https X-Powered-By X-UA-Compatible';
const httpResHeaders = httpResHeadersStr
.split(' ')
.reduce((headers, header) => {
headers[header.toLowerCase()] = header;
return headers;
}, {});
const statusCodes = {
100: 'Continue',
101: 'Switching Protocols',
102: 'Processing',
200: 'OK',
201: 'Created',
202: 'Accepted',
203: 'Non-Authoritative Information',
204: 'No Content',
205: 'Reset Content',
206: 'Partial Content',
207: 'Multi-Status',
300: 'Multiple Choices',
301: 'Moved Permanently',
302: 'Moved Temporarily',
303: 'See Other',
304: 'Not Modified',
305: 'Use Proxy',
307: 'Temporary Redirect',
400: 'Bad Request',
401: 'Unauthorized',
402: 'Payment Required',
403: 'Forbidden',
404: 'Not Found',
405: 'Method Not Allowed',
406: 'Not Acceptable',
407: 'Proxy Authentication Required',
408: 'Request Time-out',
409: 'Conflict',
410: 'Gone',
411: 'Length Required',
412: 'Precondition Failed',
413: 'Request Entity Too Large',
414: 'Request-URI Too Large',
415: 'Unsupported Media Type',
416: 'Requested Range Not Satisfiable',
417: 'Expectation Failed',
422: 'Unprocessable Entity',
423: 'Locked',
424: 'Failed Dependency',
425: 'Unordered Collection',
426: 'Upgrade Required',
428: 'Precondition Required',
429: 'Too Many Requests',
431: 'Request Header Fields Too Large',
500: 'Internal Server Error',
501: 'Not Implemented',
502: 'Bad Gateway',
503: 'Service Unavailable',
504: 'Gateway Time-out',
505: 'HTTP Version not supported',
506: 'Variant Also Negotiates',
507: 'Insufficient Storage',
509: 'Bandwidth Limit Exceeded',
510: 'Not Extended',
511: 'Network Authentication Required',
};
const allowMulti = { 'Set-Cookie': 1 };
const htmlRedirect = [
'<html>',
'<head><title>Redirecting ...</title><meta http-equiv="refresh" content="0;url=URL"></head>',
'<body onload="location.replace(document.getElementsByTagName(\'meta\')[0].content.slice(6))">',
'<noscript><p>If you are not redirected, <a href="URL">Click Here</a></p></noscript>',
new Array(15).join('<' + '!-- PADDING --' + '>'),
'</body>',
'</html>',
].join('\r\n');
function createResponseDesc(type, status) {
return {
status: status || '200 OK',
headers: { 'Content-Type': type || 'text/plain' },
charset: 'utf-8',
cookies: {},
body: [],
};
}
function normalizeHeaderName(name) {
return httpResHeaders[name.toLowerCase()] || name;
}
function stringifyHeaderValue(value) {
return Array.isArray(value) ? value.join('; ') : value;
}
class Response$1 {
_super;
buffer;
req;
constructor(res) {
this._super = res;
this.buffer = createResponseDesc();
}
clear(type, status) {
this.buffer = createResponseDesc(type, status);
}
status(status) {
if (status !== undefined) {
status = String(status);
if (status.match(RE_STATUS) && status.slice(0, 3) in statusCodes) {
this.buffer.status = status;
}
return this;
}
return this.buffer.status;
}
charset(charset) {
if (charset !== undefined) {
return (this.buffer.charset = charset);
} else {
return this.buffer.charset;
}
}
headers(...args) {
const headers = this.buffer.headers;
if (args.length === 0) {
return headers;
}
if (args.length === 1) {
if (typeof args[0] === 'string') {
const name = normalizeHeaderName(args[0]);
return stringifyHeaderValue(headers[name]);
} else {
const obj = args[0];
for (const [n, val] of Object.entries(obj)) {
this.headers(n, val);
}
return this;
}
}
const name = normalizeHeaderName(args[0]);
if (args[1] === null) {
delete headers[name];
return this;
}
const value = args[1] ?? '';
if (name in allowMulti && name in headers) {
const existing = headers[name];
if (Array.isArray(existing)) {
existing.push(value);
} else {
headers[name] = [existing, value];
}
} else {
headers[name] = value;
}
return this;
}
write(data) {
if (this.req?.method('head')) {
return;
}
if (isPrimitive(data)) {
this.buffer.body.push(String(data));
} else if (Buffer.isBuffer(data)) {
this.buffer.body.push(data);
} else {
this.buffer.body.push(JSON.stringify(data) || '');
}
}
contentType(type) {
this.headers('Content-Type', type);
}
cookies(...args) {
const cookies = this.buffer.cookies;
if (args.length === 0) {
return cookies;
}
if (args.length === 1) {
if (typeof args[0] === 'string') {
const name = args[0];
return cookies[name];
} else {
const obj = args[0];
for (const [n, val] of Object.entries(obj)) {
this.cookies(n, val);
}
return this;
}
}
const [name, value] = args;
if (value === null) {
delete cookies[name];
return this;
}
const cookie = typeof value === 'string' ? { value } : value;
cookies[name] = cookie;
return this;
}
_prepHeaders() {
const cookies = this.buffer.cookies;
for (const [name, cookie] of Object.entries(cookies)) {
this.headers('Set-Cookie', serializeCookie(name, cookie));
}
const contentType = buildContentType(
this.buffer.charset,
this.headers('Content-Type'),
);
this.headers('Content-Type', contentType);
if (this.headers('Cache-Control') == null) {
this.headers('Cache-Control', 'Private');
}
}
_writeHead() {
this._prepHeaders();
const status = parseStatus(this.buffer.status);
this._super.writeHead(status.code, status.reason, this.buffer.headers);
}
end(...args) {
if (args.length > 1 && typeof args[0] === 'string') {
const maybeStatus = args[0];
if (RE_STATUS.test(maybeStatus)) {
this.status(maybeStatus);
args.shift();
}
}
if (args.length > 1 && typeof args[0] === 'string') {
const maybeContentType = args[0];
if (RE_CONTENT_TYPE.test(maybeContentType)) {
this.contentType(maybeContentType);
args.shift();
}
}
for (const chunk of args) {
this.write(chunk);
}
this._writeHead();
for (const chunk of this.buffer.body) {
this._super.write(chunk);
}
this._super.end();
}
die(...args) {
this.clear();
this.end(...args);
}
redirect(url, type = '302') {
if (type === 'html') {
this.htmlRedirect(url);
}
if (type === '301') {
this.status('301 Moved Permanently');
} else if (type === '303') {
this.status('303 See Other');
} else {
this.status('302 Moved');
}
this.headers('Location', url);
this.end();
}
htmlRedirect(url) {
const html = htmlRedirect.replace(/URL/g, htmlEnc(url));
this.end('text/html', html);
}
}
function parseStatus(status) {
const statusCode = status.slice(0, 3);
return {
code: Number(statusCode),
reason: status.slice(4) || statusCodes[statusCode] || '',
};
}
function serializeCookie(name, cookie) {
const out = [];
out.push(name + '=' + encodeURIComponent(cookie.value));
if (cookie.domain) {
out.push('Domain=' + cookie.domain);
}
out.push('Path=' + (cookie.path || '/'));
if (cookie.expires) {
out.push('Expires=' + toGMTString(cookie.expires));
}
if (cookie.httpOnly) {
out.push('HttpOnly');
}
if (cookie.secure) {
out.push('Secure');
}
return out.join('; ');
}
function buildContentType(charset, contentType) {
const normalizedContentType = (contentType ?? '').split(';')[0] ?? '';
const normalizedCharset = charset ? charset.toUpperCase() : '';
return charset && TEXT_CONTENT_TYPES.test(normalizedContentType)
? normalizedContentType + '; charset=' + normalizedCharset
: normalizedContentType;
}
function isPrimitive(obj) {
return obj !== Object(obj);
}
function toGMTString(date) {
const a = date.toUTCString().split(' ');
if (a[1]?.length === 1) {
a[1] = '0' + a[1];
}
return a.join(' ').replace(/UTC$/i, 'GMT');
}
function htmlEnc(str, isAttr = true) {
str = String(str);
str = str.replace(/&/g, '&amp;');
str = str.replace(/>/g, '&gt;');
str = str.replace(/</g, '&lt;');
if (isAttr) {
str = str.replace(/"/g, '&quot;');
}
str = str.replace(/\u00a0/g, '&nbsp;');
return str;
}
class Request extends EventEmitter {
_super;
res;
constructor(req) {
super();
this._super = req;
req.pause();
}
getMethod() {
return this._super.method ?? '';
}
getURL() {
return this._super.url ?? '';
}
getHeaders() {
return this._super.headers;
}
getRemoteAddress() {
return this._super.connection.remoteAddress;
}
getReadStream() {
return this._super;
}
read(_bytes) {
throw new Error('Body Parser: request.read() not implemented');
}
}
class Response {
_super;
req;
constructor(httpRes) {
this._super = httpRes;
}
writeHead(statusCode, statusReason, headers) {
this._super.writeHead(statusCode, statusReason || '', headers);
}
write(data) {
this._super.write(Buffer.isBuffer(data) ? data : Buffer.from(data));
}
end() {
this._super.end();
this.req?.emit('end');
abortCurrentFiber();
}
}
class App extends EventEmitter {
router = new Router();
routeRequest = (adapterRequest, adapterResponse) => {
const req = new Request$1(adapterRequest);
const res = new Response$1(adapterResponse);
req.res = res;
res.req = req;
this.emit('request', req, res);
const path = req.url('rawPath');
this.router.route(req.method(), path, req, res);
req.emit('no-route');
res.end('404', 'text/plain', JSON.stringify({ error: '404 Not Found' }));
res.end('404', 'text/plain', JSON.stringify({ error: '404 Not Found' }));
};
route(pattern, handler) {
this.router.addRoute(pattern, handler);
}
getRequestHandler() {
const fiberWorker = (http) => {
const req = new Request(http.req);
const res = new Response(http.res);
req.res = res;
res.req = req;
this.routeRequest(req, res);
throw new Error('Router returned without handling request.');
};
return (req, res) => {
Object(req).res = res;
Object(res).req = req;
const fiber = new Fiber(fiberWorker);
try {
fiber.run({ req, res });
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e));
sendError(res, error);
}
};
}
}
function createApp() {
return new App();
}
const BODY_ALLOWED = { POST: 1, PUT: 1 };
const httpReqHeadersStr =
'Accept Accept-Charset Accept-Encoding Accept-Language Accept-Datetime Authorization ' +
'Cache-Control Connection Cookie Content-Length Content-MD5 Content-Type Date Expect From Host If-Match ' +
'If-Modified-Since If-None-Match If-Range If-Unmodified-Since Max-Forwards Pragma Proxy-Authorization ' +
'Range Referer TE Upgrade User-Agent Via Warning X-Requested-With X-Do-Not-Track X-Forwarded-For ' +
'X-ATT-DeviceId X-Wap-Profile';
const httpReqHeaders = httpReqHeadersStr
.split(' ')
.reduce((headers, header) => {
headers[header.toLowerCase()] = header;
return headers;
}, {});
const request = fiberize(function (opts, callback) {
if (opts.params) {
opts.path =
opts.path +
(~opts.path.indexOf('?') ? '&' : '?') +
qs.stringify(opts.params);
}
opts.headers = opts.headers || {};
opts.method = opts.method ? opts.method.toUpperCase() : 'GET';
const headers = {};
if (opts.headers) {
for (const [name, value] of Object.entries(opts.headers)) {
headers[httpReqHeaders[name.toLowerCase()] || name] = value;
}
}
opts.headers = headers;
let body;
if (opts.method in BODY_ALLOWED) {
const maybeBody = opts.body || opts.data;
if (Buffer.isBuffer(maybeBody) || typeof maybeBody === 'string') {
body = maybeBody;
} else {
body = qs.stringify(maybeBody || {});
}
if (!headers['Content-Type']) {
headers['Content-Type'] = 'application/x-www-form-urlencoded';
}
if (!headers['Content-Length']) {
headers['Content-Length'] = String(body.length);
}
}
const request = opts.protocol === 'https:' ? _https.request : _http.request;
const req = request(opts, (res) => {
const chunks = [];
res.on('data', (data) => {
chunks.push(data);
});
res.on('end', () => {
const body = opts.enc
? Buffer.concat(chunks).toString(opts.enc)
: Buffer.concat(chunks);
const data = {
statusCode: res.statusCode ?? 200,
headers: res.headers,
body,
};
callback(null, data);
});
});
req.on('error', (err) => {
callback(err);
});
if (body) {
req.write(body);
}
req.end();
});
const http = /*#__PURE__*/ Object.freeze({
__proto__: null,
request: request,
});
exports.createApp = createApp;
exports.http = http;
+4
-30
{
"name": "drift",
"version": "15.1.0",
"description": "",
"version": "16.0.0",
"repository": "github:marketwurks/drift",
"main": "index.js",
"files": [
"src/",
"crypto.js",
"fs.js",
"http.js"
"index.js"
],
"scripts": {
"check-format": "prettier --check \"**/*.js\"",
"format": "prettier --write \"**/*.js\"",
"lint": "eslint . --max-warnings 0",
"test-src": "mocha test",
"test": "yarn lint && yarn test-src"
},
"dependencies": {
"fibers": "^4.0.2",
"formidable": "~1.0.11",
"mkdirp": "^0.5.0",
"rimraf": "^2.4.3"
"fibers": "^4.0.2"
},
"devDependencies": {
"babel-eslint": "^10.1.0",
"eslint": "^7.26.0",
"eslint-plugin-babel": "^5.3.1",
"expect.js": "^0.3.1",
"mocha": "^2.5.3",
"prettier": "^1.19.1"
},
"prettier": {
"singleQuote": true,
"trailingComma": "all",
"arrowParens": "always",
"endOfLine": "lf"
},
"license": "UNLICENSED"
}
module.exports = require('./src/adapters/crypto');
module.exports = require('./src/adapters/fs');
module.exports = require('./src/adapters/http');
/* eslint-disable one-var */
'use strict';
const _fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const formidable = require('formidable');
const EventEmitter = require('events').EventEmitter;
const { toFiber } = require('../toFiber');
const { mapPath } = require('../mapPath');
const qs = require('../system/qs');
const util = require('../system/util');
const fs = require('./fs');
const MAX_BUFFER_SIZE = process.env.REQ_BODY_MAX_BUFFER_SIZE
? parseInt(process.env.REQ_BODY_MAX_BUFFER_SIZE, 10)
: 6291456;
const join = path.join;
const hasOwnProperty = Object.hasOwnProperty;
//src can be either a readStream or a path to file
function BodyParser(headers, src, opts) {
EventEmitter.call(this);
this.init(src);
this.headers = headers;
opts = opts || {};
this.hashType = opts.hash === false ? null : opts.hash || 'md5';
this.opts = opts;
this.parsed = {};
//to work properly with formidable
if (!this.readStream.headers) {
this.readStream.headers = headers;
}
this.on('end', function() {
this._finished = true;
});
}
util.inherits(BodyParser, EventEmitter);
BodyParser.prototype.init = toFiber(function(src, callback) {
if (typeof src === 'string') {
var readStream = (this.readStream = _fs.createReadStream(mapPath(src)));
readStream.on('error', callback);
readStream.on('open', function() {
callback();
});
//pause immediately since we may not attach data listener until later
readStream.pause();
} else {
this.readStream = src;
callback();
}
});
BodyParser.prototype.parse = toFiber(function(callback) {
//enable callback syntax
this.on('error', function(err) {
callback(err);
});
this.on('end', function() {
callback(null, this.parsed);
});
//process based on headers
var headers = this.headers;
this.length = parseInt(headers['content-length'], 10);
if (isNaN(this.length)) {
this.emit('error', '411 Length Required');
return;
} else if (this.length === 0) {
//nothing to parse
this.emit('end');
return;
}
this.type = headers['content-type'] || '';
this.type = this.type.toLowerCase().split(';')[0];
if (!this.type) {
this.emit('error', '415 Content-Type Required');
return;
}
switch (this.type) {
case 'application/x-www-form-urlencoded':
this.processFormBody();
break;
case 'application/json':
this.processJSONBody();
break;
case 'multipart/form-data':
this.processMultiPartBody();
break;
default:
this.processBinaryBody();
}
this.readStream.resume();
});
BodyParser.prototype.bufferReqBody = function(callback) {
var readStream = this.readStream,
opts = this.opts;
if (this.length > MAX_BUFFER_SIZE) {
callback('413 Request Entity Too Large');
return;
}
var buffer = [],
size = 0,
expected = this.length;
readStream.on('data', function(data) {
if (size > expected) {
readStream.pause();
callback('413 Request Entity Too Large');
return;
}
buffer.push(data);
size += data.length;
});
readStream.on('error', function(err) {
callback(err);
});
readStream.on('end', function() {
var data = Buffer.concat(buffer);
callback(null, data.toString(opts.encoding || 'utf8'));
});
};
BodyParser.prototype.processFormBody = function() {
var self = this;
this.bufferReqBody(function(err, body) {
if (err) {
self.emit('error', err);
return;
}
Object.assign(self.parsed, qs.parse(body));
self.emit('end');
});
};
BodyParser.prototype.processJSONBody = function() {
var self = this;
this.bufferReqBody(function(err, body) {
if (err) {
self.emit('error', err);
return;
}
try {
var parsed = JSON.parse(body);
} catch (e) {
self.emit('error', new Error('Invalid JSON Body'));
return;
}
if (parsed !== Object(parsed)) {
parsed = { '': parsed };
}
Object.assign(self.parsed, parsed);
self.emit('end');
});
};
BodyParser.prototype.processMultiPartBody = function() {
var self = this,
readStream = this.readStream,
opts = this.opts;
//todo: we should use formidable.MultipartParser directly
var parser = new formidable.IncomingForm();
parser.hash = this.hashType;
parser.maxFieldsSize = MAX_BUFFER_SIZE;
if (opts.autoSavePath) {
parser.uploadDir = mapPath(opts.autoSavePath);
}
parser.on('field', function(name, val) {
var parsed = self.parsed;
var key = qs.unescape(name).toLowerCase();
if (hasOwnProperty.call(parsed, key)) {
parsed[key] += ', ' + qs.unescape(val);
} else {
parsed[key] = qs.unescape(val);
}
});
parser.on('fileBegin', function(name, _file) {
var id = getID();
var key = qs.unescape(name).toLowerCase();
if (opts.autoSavePath) {
_file.path = join(parser.uploadDir, id);
} else {
//hacky way to prevent formidable from saving files to disk
_file.open = function() {
_file._writeStream = new DummyWriteStream();
};
}
var file = new File();
file.id = id;
file.name = name; //field name
file.fileName = _file.name; //original file name as uploaded
file.contentType = _file.type;
file.size = 0;
file.md5 = null;
self.emit('file', file);
_file.on('data', function(data) {
file.size += data.length;
file.emit('data', data);
});
_file.on('end', function() {
file.hash = _file.hash;
if (_file.path && _file.size === 0) {
_fs.unlink(_file.path);
} else if (_file.path) {
file.fullpath = _file.path;
}
file.emit('end');
});
var exists = hasOwnProperty.call(self.parsed, key);
if (exists) {
key = getUniqueKey(self.parsed, key);
}
self.parsed[key] = file;
});
//parser.on('error', function() {});
//todo: socket timeout or close
//parser.on('aborted', function() {});
parser.parse(readStream, function(err) {
if (err) {
self.emit('error', err);
return;
}
self.emit('end');
});
};
BodyParser.prototype.processBinaryBody = function() {
var self = this,
readStream = self.readStream,
headers = this.headers,
opts = self.opts;
if (this.hashType) {
var hash = crypto.createHash(this.hashType);
}
var contentDisp = util.parseHeaderValue(headers['content-disposition']);
var fieldName = contentDisp.name || headers['x-name'] || 'file';
var file = (self.parsed[fieldName] = new File());
file.id = getID();
file.name = fieldName;
file.fileName = contentDisp.filename || headers['x-file-name'] || 'upload';
file.contentType =
headers['content-description'] || headers['x-content-type'] || self.type;
file.size = 0;
file.md5 = null;
self.emit('file', file);
if (opts.autoSavePath) {
var path = (file.fullpath = join(opts.autoSavePath, getID()));
var outStream = _fs.createWriteStream(mapPath(path));
outStream.on('error', function(err) {
self.emit('error', err);
});
readStream.pipe(outStream);
}
readStream.on('data', function(data) {
if (hash) hash.update(data);
file.size += data.length;
file.emit('data', data);
});
readStream.on('end', function() {
if (hash) {
file.hash = hash.digest('hex');
file[self.hashType] = file.hash;
}
file.emit('end');
self.emit('end');
});
};
function File() {
this.type = 'file';
}
util.inherits(File, EventEmitter);
File.prototype.toString = function() {
return typeof this.fileName === 'string' ? this.fileName : '';
};
File.prototype.toJSON = function() {
return {
id: this.id,
name: this.name,
fileName: this.fileName,
contentType: this.contentType,
size: this.size,
md5: this.md5,
hash: this.hash,
fullpath: this.fullpath,
};
};
File.prototype.saveTo = function(path) {
fs.moveFile(this.fullpath, path);
this.fullpath = path;
};
//todo: remove this and use formidable.MultipartParser directly (or dicer)
function DummyWriteStream() {}
DummyWriteStream.prototype.write = function(data, callback) {
callback();
};
DummyWriteStream.prototype.end = function(callback) {
callback();
};
//Helper functions
function isUpload(item) {
return item instanceof File;
}
function getID() {
var chars = '';
for (var i = 0; i < 32; i++) {
chars += Math.floor(Math.random() * 16).toString(16);
}
return chars;
}
function getUniqueKey(obj, key) {
var id = 0;
key = key.replace(/\d+$/, function(key, num) {
id = parseInt(num, 10);
return '';
});
id += 1;
while (hasOwnProperty.call(obj, key + id)) id += 1;
return key + id;
}
module.exports = BodyParser;
BodyParser.isUpload = isUpload;
const crypto = require('crypto');
module.exports = {
createHash: crypto.createHash.bind(crypto),
hash: function(type, data, enc) {
let hasher = crypto.createHash(type);
hasher.update(data, enc);
return hasher.digest();
},
createHmac: crypto.createHmac.bind(crypto),
hmac: function(type, key, data, enc) {
let hasher = crypto.createHmac(type, key, enc);
hasher.update(data, enc);
return hasher.digest();
},
};
'use strict';
const fs = require('fs');
const mkdirp = require('mkdirp');
const rimraf = require('rimraf'); //recursive rmdir
const { eventify } = require('../eventify');
const { mapPath } = require('../mapPath');
const path = require('../system/path');
const { toFiber } = require('../toFiber');
var join = path.join;
var basename = path.basename;
var ERROR_NO = {
EXDEV: 52,
EISDIR: 28, //illegal operation on a directory
EEXIST: 47,
};
exports.isFile = toFiber(function(path, callback) {
path = mapPath(path);
fs.stat(path, function(err, stat) {
callback(null, !err && stat.isFile());
});
});
exports.isDir = toFiber(function(path, callback) {
path = mapPath(path);
fs.stat(path, function(err, stat) {
callback(null, !err && stat.isDirectory());
});
});
exports.copyFile = toFiber(function(src, dest, callback) {
src = mapPath(src);
dest = mapPath(dest);
checkCopyFile(src, dest, function(err, src, dest) {
if (err) return callback(err);
copyFile(src, dest, callback);
});
});
exports.moveFile = toFiber(function(src, dest, callback) {
src = mapPath(src);
dest = mapPath(dest);
checkCopyFile(src, dest, function(err, src, dest) {
if (err) return callback(err);
fs.rename(src, dest, function(err) {
//tried to rename across devices
if (err && err.code === 'EXDEV') {
return moveFileSlow(src, dest, callback);
}
callback(err);
});
});
});
exports.deleteFile = toFiber(function(path, callback) {
path = mapPath(path);
fs.unlink(path, callback);
});
exports.deleteFileIfExists = toFiber(function(path, callback) {
path = mapPath(path);
fs.unlink(path, function(err) {
var wasRemoved = !!err;
if (err && err.code === 'ENOENT') {
err = null;
}
callback(err, wasRemoved);
});
});
exports.createDir = toFiber(function(path, deep, callback) {
path = mapPath(path);
if (deep) {
mkdirp(path, callback);
} else {
fs.mkdir(path, callback);
}
});
exports.removeDir = toFiber(function(path, deep, callback) {
path = mapPath(path);
rmdir(path, deep, callback);
});
exports.removeDirIfExists = toFiber(function(path, deep, callback) {
path = mapPath(path);
rmdir(path, deep, function(err) {
var wasRemoved = !!err;
if (err && err.code === 'ENOENT') {
err = null;
}
callback(err, wasRemoved);
});
});
//note: does not support move across devices
// if destination is a directory:
// - overwrite if empty; else throw `ENOTEMPTY, directory not empty`
// if destination is a file:
// - throw `ENOTDIR, not a directory`
exports.moveDir = toFiber(function(src, dest, callback) {
src = mapPath(src);
dest = mapPath(dest);
fs.stat(src, function(err, stat) {
if (err) return callback(err);
if (!stat.isDirectory()) {
return callback(posixError('ENOENT', { path: src }));
}
fs.rename(src, dest, callback);
});
});
exports.getDirContents = toFiber(function(path, callback) {
path = mapPath(path);
fs.readdir(path, callback);
});
/**
* Walks directory, depth-first, calling fn for each subdirectory and
* file and passing `info` object and `prefix` which can be prepended to
* info.name to get relative path.
*/
exports.walk = toFiber(function(path, fn, callback) {
var fullPath = mapPath(path);
getInfo(fullPath, true, function(err, info) {
if (err) return callback(err);
if (info.type !== 'directory') {
//todo: posix
return callback(new Error('Not a directory: ' + path));
}
walkDeep(info, fn, '');
callback();
});
});
exports.getInfo = toFiber(function(path, deep, callback) {
var fullPath = mapPath(path);
getInfo(fullPath, deep, callback);
});
exports.getFileInfo = toFiber(function(path, callback) {
var fullPath = mapPath(path);
getInfo(fullPath, false, function(err, info) {
if (!err && info.type !== 'file') {
err = posixError('ENOENT', { path: fullPath });
}
callback(err, info);
});
});
exports.readFile = toFiber(function(path, callback) {
path = mapPath(path);
fs.readFile(path, callback);
});
exports.readTextFile = toFiber(function(path, enc, callback) {
path = mapPath(path);
enc = enc || 'utf8';
fs.readFile(path, enc, callback);
});
exports.writeFile = toFiber(function(path, data, opts, callback) {
path = mapPath(path);
writeFile(path, data, opts, callback);
});
exports.writeTextToFile = toFiber(function(path, text, opts, callback) {
path = mapPath(path);
if (typeof text !== 'string') {
text =
text == null || typeof text.toString !== 'function'
? Object.prototype.toString.call(text)
: text.toString();
}
writeFile(path, text, opts, callback);
});
exports.createReadStream = function(path, opts) {
return new FileReadStream(path, opts);
};
exports.createWriteStream = function(path, opts) {
opts = opts || {};
//default is to append
opts.append = opts.append !== false;
//overwrite option will override append
if (opts.overwrite === true) opts.append = false;
return new FileWriteStream(path, opts);
};
function FileReadStream(path, opts) {
this.path = path;
opts = opts || {};
this.opts = opts;
this.init();
}
exports.FileReadStream = FileReadStream;
eventify(FileReadStream.prototype);
Object.assign(FileReadStream.prototype, {
setEncoding: function(enc) {
this.opts.encoding = enc;
},
size: function() {
return this._bytesTotal;
},
init: toFiber(function(callback) {
var path = mapPath(this.path);
this._bytesRead = 0;
fs.stat(
path,
function(err, stat) {
if (err) return callback(err);
this._bytesTotal = stat.size;
callback();
}.bind(this),
);
}),
read: toFiber(function(callback) {
var path = mapPath(this.path);
var opts = { encoding: this.opts.encoding };
var self = this;
var stream = fs.createReadStream(path, opts);
stream.on('error', callback);
stream.on('open', function() {
stream.on('readable', drain);
stream.on('end', function() {
self.emit('end');
callback();
});
drain();
});
//drain the bytes in the buffer (resets readable flag)
var drain = function() {
var chunk;
while (null !== (chunk = stream.read())) {
// eslint-disable-line yoda
self.emit('data', chunk);
}
};
}),
readAll: function() {
if (this.opts.encoding) {
return exports.readTextFile(this.path, this.opts.encoding);
} else {
return exports.readFile(this.path);
}
},
});
function FileWriteStream(path, opts) {
this.path = path;
this.opts = opts || {};
}
exports.FileWriteStream = FileWriteStream;
Object.assign(FileWriteStream.prototype, {
setEncoding: function(enc) {
this.opts.encoding = enc;
},
write: toFiber(function(data, enc, callback) {
if (this._finished) {
callback();
} else if (this._stream) {
this._stream.write(data, enc, callback);
} else {
openWriteStream(
mapPath(this.path),
this.opts,
function(err, stream) {
if (err) return callback(err);
this._stream = stream;
this._stream.write(data, enc, callback);
}.bind(this),
);
}
}),
end: toFiber(function(callback) {
if (this._finished) return;
this._finished = true;
this._stream.end(callback);
}),
});
//helpers
function rmdir(path, deep, callback) {
if (deep) {
rimraf(path, callback);
} else {
//todo: unlink?
fs.rmdir(path, callback);
}
}
//todo: what if it's not a file or directory?
function getInfo(path, deep, callback) {
fs.stat(path, function(err, stat) {
if (err) return callback(err);
var info = fileInfo(basename(path), stat);
if (deep && info.type === 'directory') {
var fullPath = join(path, info.name);
// eslint-disable-next-line handle-callback-err
getChildrenInfo(fullPath, deep, function(err, children) {
info.children = children;
children.forEach(function(childInfo) {
info.size += childInfo.size;
});
callback(null, info);
});
} else {
callback(null, info);
}
});
}
function getChildrenInfo(path, deep, callback) {
// eslint-disable-next-line handle-callback-err
fs.readdir(path, function(err, names) {
var files = [];
var directories = [];
var errors = [];
var results = {};
var list = new AsyncList(names);
list.forEach(function(name, done) {
var pathName = join(path, name);
fs.stat(pathName, function(err, stat) {
if (err) {
errors.push(name);
} else if (stat.isFile()) {
files.push(name);
} else if (stat.isDirectory()) {
directories.push(name);
}
results[name] = err || stat;
done();
});
});
list.on('done', function() {
var children = [];
directories.forEach(function(name) {
children.push(fileInfo(name, results[name]));
});
files.forEach(function(name) {
children.push(fileInfo(name, results[name]));
});
if (!deep) return callback(null, children);
var list = new AsyncList(children);
list.forEach(function(childInfo, done) {
if (childInfo.type !== 'directory') return done();
var fullPath = join(path, childInfo.name);
// eslint-disable-next-line handle-callback-err
getChildrenInfo(fullPath, deep, function(err, children) {
childInfo.children = children;
});
});
list.on('done', function() {
callback(null, children);
});
});
});
}
function fileInfo(name, file) {
var isDirectory = file.isDirectory();
return {
name: name,
dateCreated: file.ctime,
dateLastAccessed: file.atime,
dateLastModified: file.mtime,
type: isDirectory ? 'directory' : 'file',
size: isDirectory ? 0 : file.size,
};
}
function walkDeep(info, fn, prefix) {
if (info.children) {
info.children.forEach(function(childInfo) {
walkDeep(childInfo, fn, prefix + info.name + '/');
});
}
fn(info, prefix);
}
function openWriteStream(path, opts, callback) {
var flags = opts.append ? 'a' : 'w';
var encoding = opts.encoding || 'utf8';
var stream = fs.createWriteStream(path, {
flags: flags,
encoding: encoding,
});
stream.on('error', function(err) {
//if trying to append file, but it doesn't exist, create it
if (opts.append && err.code === 'ENOENT') {
openWriteStream(path, { encoding: encoding }, callback);
} else {
callback(err);
}
});
stream.on('open', function() {
callback(null, stream);
});
}
function writeFile(path, data, opts, callback) {
opts = opts || {};
opts.encoding = opts.encoding || opts.enc || 'utf8';
if (opts.overwrite) {
fs.writeFile(path, data, opts, callback);
} else {
fs.appendFile(path, data, opts, callback);
}
}
//make sure src is a file and destination either doesn't exist or is a
// directory; if destination is a directory, append src filename
function checkCopyFile(src, dest, callback) {
fs.stat(src, function(err, stat) {
if (err) return callback(err);
if (!stat.isFile()) {
var errCode = stat.isDirectory() ? 'EISDIR' : 'ENOENT';
return callback(posixError(errCode, { path: src }));
}
fs.stat(dest, function(err, stat) {
if (err && err.code !== 'ENOENT') {
return callback(err);
}
if (!err) {
//destination exists
if (stat.isDirectory()) {
dest = join(dest, basename(src));
} else {
return callback(posixError('EEXIST', { path: dest }));
}
}
callback(null, src, dest);
});
});
}
function copyFile(srcPath, destPath, callback) {
var src = fs.createReadStream(srcPath);
var dest = fs.createWriteStream(destPath);
src.on('error', callback);
src.on('close', function() {
callback();
});
src.pipe(dest);
}
function moveFileSlow(srcPath, destPath, callback) {
copyFile(srcPath, destPath, function(err) {
if (err) {
callback(err);
} else {
fs.unlink(srcPath, callback);
}
});
}
function posixError(code, opts) {
var message = code;
if (opts.path) {
message += ', ' + (opts.syscall ? opts.syscall + ' ' : '') + opts.path;
}
var e = new Error(message);
e.code = code;
e.errno = ERROR_NO[code];
if (opts.path) e.path = opts.path;
if (opts.syscall) e.syscall = opts.syscall;
return e;
}
/**
* very simple abstraction to do a list of things in parallel and emit 'done'
* event when all have completed
*/
function AsyncList(list) {
this.list = list;
}
eventify(AsyncList.prototype);
AsyncList.prototype.forEach = function(fn) {
var list = this.list;
var doneCount = 0;
var done = this.emit.bind(this, 'done');
var defer = true; //defer the done event
var callback = function() {
doneCount += 1;
if (doneCount === list.length) {
if (defer) {
process.nextTick(done);
} else {
done();
}
}
};
list.forEach(function(item) {
fn(item, callback);
});
defer = false;
};
/* eslint-disable one-var */
'use strict';
const _http = require('http');
const _https = require('https');
const qs = require('../system/qs');
const url = require('../system/url');
const { toFiber } = require('../toFiber');
//url helpers
var parseUrl = url.parse;
var BODY_ALLOWED = { POST: 1, PUT: 1 };
var httpReqHeaders =
'Accept Accept-Charset Accept-Encoding Accept-Language Accept-Datetime Authorization ' +
'Cache-Control Connection Cookie Content-Length Content-MD5 Content-Type Date Expect From Host If-Match ' +
'If-Modified-Since If-None-Match If-Range If-Unmodified-Since Max-Forwards Pragma Proxy-Authorization ' +
'Range Referer TE Upgrade User-Agent Via Warning X-Requested-With X-Do-Not-Track X-Forwarded-For ' +
'X-ATT-DeviceId X-Wap-Profile';
//index headers by lowercase
httpReqHeaders = httpReqHeaders.split(' ').reduce(function(headers, header) {
headers[header.toLowerCase()] = header;
return headers;
}, {});
var request = (exports.request = toFiber(function(opts, callback) {
//todo: organize into ClientRequest and ClientResponse
if (opts.params) {
opts.path =
opts.path +
(~opts.path.indexOf('?') ? '&' : '?') +
qs.stringify(opts.params);
}
opts.headers = opts.headers || {};
opts.method = opts.method ? opts.method.toUpperCase() : 'GET';
//normalize header case
var headers = {};
for (var n in opts.headers) {
if (opts.headers.hasOwnProperty(n)) {
headers[httpReqHeaders[n.toLowerCase()] || n] = opts.headers[n];
}
}
opts.headers = headers;
//set length and default content type
if (opts.method in BODY_ALLOWED) {
//opts.data as alias for opts.body
opts.body = opts.body || opts.data;
//url encode if body is a plain object
if (!Buffer.isBuffer(opts.body) && typeof opts.body !== 'string') {
opts.body = qs.stringify(opts.body || {});
}
if (!headers['Content-Type']) {
headers['Content-Type'] = 'application/x-www-form-urlencoded';
}
if (!headers['Content-Length']) {
headers['Content-Length'] = String(opts.body.length);
}
}
var http = opts.protocol == 'https:' ? _https : _http;
var req = http.request(opts, function(res) {
var body = [];
res.on('data', function(data) {
body.push(data);
});
res.on('end', function() {
res.body = Buffer.concat(body);
if (opts.enc) {
res.body = res.body.toString(opts.enc);
}
var data = {
statusCode: res.statusCode,
headers: res.headers,
body: res.body,
};
callback(null, data);
});
});
req.on('error', function(err) {
callback(err);
});
if (opts.body) {
req.write(opts.body);
}
// Allow caller to stream a request body. In this case the caller will be
// responsible for calling .end()
if (typeof opts.onReady === 'function') {
opts.onReady(req);
} else {
req.end();
}
}));
exports.get = toFiber(function(opts, callback) {
if (typeof opts == 'string') {
opts = { url: opts };
}
if (opts.url) {
Object.assign(opts, parseUrl(opts.url));
}
opts.method = 'GET';
request(opts, callback);
});
exports.post = toFiber(function(opts, callback) {
if (opts.url) {
Object.assign(opts, url.parse(opts.url));
}
opts.method = 'POST';
request(opts, callback);
});
'use strict';
const { eventify } = require('../eventify');
const BodyParser = require('./body-parser');
function Request(req) {
//node's incoming http request
this._super = req;
//pause so that we can use the body parser later
req.pause();
}
eventify(Request.prototype);
Object.assign(Request.prototype, {
getMethod: function() {
return this._super.method;
},
getURL: function() {
return this._super.url;
},
getHeaders: function() {
return this._super.headers;
},
getRemoteAddress: function() {
return this._super.connection.remoteAddress;
},
getBodyParser: function(opts) {
return new BodyParser(this.getHeaders(), this._super, opts);
},
// eslint-disable-next-line no-unused-vars
read: function(bytes) {
throw new Error('Body Parser: request.read() not implemented');
},
});
module.exports = Request;
'use strict';
const _fs = require('fs');
const Fiber = require('../lib/fiber');
const { mapPath } = require('../mapPath');
const fs = require('./fs');
function Response(httpRes) {
this._super = httpRes;
}
Object.assign(Response.prototype, {
writeHead: function(statusCode, statusReason, headers) {
this._super.writeHead(statusCode, statusReason || '', headers);
},
write: function(data) {
this._super.write(Buffer.isBuffer(data) ? data : Buffer.from(data));
},
end: function() {
this._super.end();
//fire end event after we have finished the response to perform things like cleanup
this.req.emit('end');
Fiber.current.abort();
},
streamFile: function(statusCode, statusReason, headers, path) {
var _super = this._super;
var info = fs.getFileInfo(path);
headers['Content-Length'] = info.size;
this.writeHead(statusCode, statusReason, headers);
process.nextTick(function() {
var fullpath = mapPath(path);
var readStream = _fs.createReadStream(fullpath);
readStream.pipe(_super);
});
this.req.emit('end');
Fiber.current.abort();
},
});
module.exports = Response;
//this is the project path; used in server.js and mapPath.js
exports.BASE_PATH = process.cwd();
const emitter = {
on: function(name, fn) {
let events = this._events || (this._events = {});
let list = events[name] || (events[name] = []);
list.push(fn);
},
emit: function(name, ...args) {
let events = this._events || {};
let list = events[name] || [];
for (let fn of list) {
fn.call(this, ...args);
}
},
};
// Make an object into a basic Event Emitter
exports.eventify = function(obj) {
obj.on = emitter.on;
obj.emit = emitter.emit;
return obj;
};
'use strict';
var Fiber = require('fibers');
var slice = Array.prototype.slice;
//patch fiber.run() to send errors to fiber.onError()
//todo: skip this if some flag is set on app/adapter (from a command-line flag)
var _run = Fiber.prototype.run;
Fiber.prototype.run = function() {
try {
return _run.apply(this, arguments);
} catch (e) {
if (this.onError) {
this.onError(e);
} else {
throw e;
}
}
};
Fiber.prototype.abort = function(callback) {
var fiber = Fiber.current;
process.nextTick(function() {
if (callback) callback();
fiber.reset();
});
Fiber.yield();
};
/** Fiber.fiberize() turns an asynchronous function to a fiberized one */
Fiber.fiberize = function(fn) {
var arity = fn.length;
return function() {
var fiber = Fiber.current;
var err;
var result;
var yielded = false;
var args = slice.call(arguments);
// virtual callback
function syncCallback(callbackError, callbackResult) {
// forbid to call twice
if (syncCallback.called) return;
syncCallback.called = true;
if (callbackError) {
err = callbackError;
} else {
// Handle situation when callback returns many values
if (arguments.length > 2) {
callbackResult = slice.call(arguments, 1);
}
// Assign callback result
result = callbackResult;
}
// Resume fiber if yielding
if (yielded) fiber.run();
}
// in case of optional arguments, make sure the callback is at the index expected
if (args.length + 1 < arity) {
args[arity - 1] = syncCallback;
} else {
args.push(syncCallback);
}
// call async function
fn.apply(this, args);
// wait for result
if (!syncCallback.called) {
yielded = true;
Fiber.yield();
}
// Throw if err
if (err) throw err;
return result;
};
};
module.exports = Fiber;
/* eslint-disable quote-props */
var extensions = {
'3gp': 'video/3gpp',
'7z': 'application/x-7z-compressed',
ace: 'application/x-ace-compressed',
ai: 'application/postscript',
aif: 'audio/x-aiff',
aiff: 'audio/x-aiff',
appcache: 'text/cache-manifest',
asx: 'video/x-ms-asf',
asf: 'video/x-ms-asf',
au: 'audio/basic',
avi: 'video/x-msvideo',
bin: 'application/octet-stream',
bmp: 'image/bmp',
bz2: 'application/x-bzip2',
cab: 'application/vnd.ms-cab-compressed',
cbr: 'application/x-cbr',
chm: 'application/vnd.ms-htmlhelp',
css: 'text/css',
dmg: 'application/x-apple-diskimage',
doc: 'application/msword',
docx:
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
dotx:
'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
dtd: 'application/xml-dtd',
dwg: 'image/vnd.dwg',
eml: 'message/rfc822',
eot: 'application/vnd.ms-fontobject',
eps: 'application/postscript',
flv: 'video/x-flv',
gif: 'image/gif',
hqx: 'application/mac-binhex40',
htm: 'text/html',
html: 'text/html',
ico: 'image/x-icon',
iso: 'application/x-iso9660-image',
jar: 'application/java-archive',
jpeg: 'image/jpeg',
jpg: 'image/jpeg',
js: 'application/javascript',
json: 'application/javascript',
lnk: 'application/x-ms-shortcut',
log: 'text/plain',
m4a: 'audio/mp4',
m4p: 'application/mp4',
m4v: 'video/x-m4v',
map: 'application/json',
mcd: 'application/vnd.mcd',
mdb: 'application/x-msaccess',
mid: 'audio/midi',
midi: 'audio/midi',
mov: 'video/quicktime',
mp3: 'audio/mpeg',
mp4: 'video/mp4',
mpeg: 'video/mpeg',
mpg: 'video/mpeg',
ogg: 'audio/ogg',
otf: 'font/opentype',
pdf: 'application/pdf',
png: 'image/png',
potx: 'application/vnd.openxmlformats-officedocument.presentationml.template',
pps: 'application/vnd.ms-powerpoint',
ppsx:
'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
ppt: 'application/vnd.ms-powerpoint',
pptx:
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
ps: 'application/postscript',
psd: 'image/vnd.adobe.photoshop',
pub: 'application/x-mspublisher',
qxd: 'application/vnd.quark.quarkxpress',
ra: 'audio/x-pn-realaudio',
ram: 'audio/x-pn-realaudio',
rar: 'application/x-rar-compressed',
rdf: 'application/rdf+xml',
rm: 'application/vnd.rn-realmedia',
rmvb: 'application/vnd.rn-realmedia-vbr',
rtf: 'application/rtf',
sass: 'text/plain',
scss: 'text/plain',
sgml: 'text/sgml',
sit: 'application/x-stuffit',
sitx: 'application/x-stuffitx',
sldx: 'application/vnd.openxmlformats-officedocument.presentationml.slide',
svg: 'image/svg+xml',
swf: 'application/x-shockwave-flash',
tar: 'application/x-tar',
tiff: 'image/tiff',
tif: 'image/tiff',
torrent: 'application/x-bittorrent',
tsv: 'text/tab-separated-values',
ttf: 'application/x-font-ttf',
txt: 'text/plain',
vcd: 'application/x-cdlink',
wav: 'audio/x-wav',
wma: 'audio/x-ms-wma',
wmv: 'video/x-ms-wmv',
woff: 'application/font-woff',
woff2: 'application/font-woff2',
wpd: 'application/vnd.wordperfect',
wps: 'application/vnd.ms-works',
xlam: 'application/vnd.ms-excel.addin.macroenabled.12',
xls: 'application/vnd.ms-excel',
xlsb: 'application/vnd.ms-excel.sheet.binary.macroenabled.12',
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
xltx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
xml: 'application/xml',
zip: 'application/zip',
};
var types = Object.keys(extensions).reduce(function(types, key) {
if (!types[key]) {
types[key] = [];
}
types[key].push(extensions[key]);
return types;
}, {});
exports.getExtension = function getExtension(type) {
return types[type] && types[type][0];
};
exports.getMime = function getMime(path) {
path = path.toLowerCase().trim();
var index = path.lastIndexOf('/');
if (index >= 0) {
path = path.substr(index + 1);
}
index = path.lastIndexOf('.');
if (index >= 0) {
path = path.substr(index + 1);
}
return extensions[path];
};
const { join } = require('path');
const { BASE_PATH } = require('./constants');
exports.mapPath = (...args) => {
return join(BASE_PATH, ...args);
};
'use strict';
const { BASE_PATH } = require('./constants');
const { eventify } = require('./eventify');
const Router = require('./system/router');
const Request = require('./system/request');
const Response = require('./system/response');
const { tryStaticPath } = require('./support/tryStaticPath');
const AdapterRequest = require('./adapters/request');
const AdapterResponse = require('./adapters/response');
//patch some built-in methods
require('./support/patch');
const Fiber = require('./lib/fiber');
exports.createApp = () => {
const app = {};
eventify(app);
const router = new Router();
app.route = (pattern, handler) => {
router.addRoute(pattern, handler);
};
const routeRequest = (adapterRequest, adapterResponse) => {
let req = new Request(adapterRequest);
let res = new Response(adapterResponse);
//cross-reference request and response
req.res = res;
res.req = req;
app.emit('request', req, res);
let path = req.url('rawPath');
router.route(req.method(), path, req, res);
// If we get to this point and the fiber has not aborted then there was no
// route that handled this request.
req.emit('no-route');
res.end('404', 'text/plain', JSON.stringify({ error: '404 Not Found' }));
};
app.getRequestHandler = () => {
//this function only runs within a fiber
const fiberWorker = (http) => {
let req = new AdapterRequest(http.req);
let res = new AdapterResponse(http.res);
//cross-reference adapter-request and adapter-response
req.res = res;
res.req = req;
routeRequest(req, res);
throw new Error('Router returned without handling request.');
};
return (req, res) => {
//cross-reference request and response
req.res = res;
res.req = req;
//attempt to serve static file
let staticPaths = ['/assets/'];
tryStaticPath(req, res, BASE_PATH, staticPaths, () => {
let fiber = new Fiber(fiberWorker);
fiber.onError = res.sendError.bind(res);
fiber.run({ req, res });
});
};
};
return app;
};
//for debugging
// var sleep = function(ms) {
// var fiber = Fiber.current;
// setTimeout(function() {
// fiber.run();
// }, ms);
// Fiber.yield();
// };
/* eslint-disable one-var */
'use strict';
/**
* Check `req` and `res` to see if it has been modified.
*
* @param {IncomingMessage} req
* @param {ServerResponse} res
* @return {Boolean}
* @api private
*/
exports.modified = function(req, res) {
var headers = res.getHeaders() || {};
var modifiedSince = req.headers['if-modified-since'],
lastModified = headers['last-modified'],
noneMatch = req.headers['if-none-match'],
etag = headers.etag;
if (noneMatch) noneMatch = noneMatch.split(/ *, */);
// check If-None-Match
if (noneMatch && etag && ~noneMatch.indexOf(etag)) {
return false;
}
// check If-Modified-Since
if (modifiedSince && lastModified) {
modifiedSince = new Date(modifiedSince);
lastModified = new Date(lastModified);
// Ignore invalid dates
if (!isNaN(modifiedSince.getTime())) {
if (lastModified <= modifiedSince) return false;
}
}
return true;
};
/**
* Strip `Content-*` headers from `res`.
*
* @param {ServerResponse} res
* @api private
*/
exports.removeContentHeaders = function(res) {
res.getHeaderNames().forEach(function(field) {
if (field.indexOf('content') === 0) {
res.removeHeader(field);
}
});
};
/**
* Check if `req` is a conditional GET request.
*
* @param {IncomingMessage} req
* @return {Boolean}
* @api private
*/
exports.conditionalGET = function(req) {
return req.headers['if-modified-since'] || req.headers['if-none-match'];
};
/**
* Respond with 304 "Not Modified".
*
* @param {ServerResponse} res
* @param {Object} headers
* @api private
*/
exports.notModified = function(res) {
exports.removeContentHeaders(res);
res.statusCode = 304;
res.end();
};
/**
* Parse "Range" header `str` relative to the given file `size`.
*
* @param {Number} size
* @param {String} str
* @return {Array}
* @api private
*/
exports.parseRange = function(size, str) {
var valid = true;
var arr = str
.substr(6)
.split(',')
.map(function(range) {
range = range.split('-');
var start = parseInt(range[0], 10),
end = parseInt(range[1], 10);
// -500
if (isNaN(start)) {
start = size - end;
end = size - 1;
// 500-
} else if (isNaN(end)) {
end = size - 1;
}
// Invalid
if (isNaN(start) || isNaN(end) || start > end || start < 0) valid = false;
return {
start: start,
end: end,
};
});
return valid ? arr : null;
};
/**
* List compiled from taking the most popular file-types (that have known mime-types) and adding
* - the @font-face types
* - ico
* - appcache
* - sass, scss, map (for css source-maps)
* - json
* sources:
* http://reference.sitepoint.com/html/mime-types
* http://www.mailbigfile.com/101-most-popular-file-types/
*
* In the case of reverse (mime -> extension) the last file extension should be used. To make
* this work, a couple of these are out of alphabetical order.
*
*/
module.exports = {
'3gp': 'video/3gpp',
'7z': 'application/x-7z-compressed',
ace: 'application/x-ace-compressed',
ai: 'application/postscript',
aif: 'audio/x-aiff',
aiff: 'audio/x-aiff',
appcache: 'text/cache-manifest',
asx: 'video/x-ms-asf',
asf: 'video/x-ms-asf',
au: 'audio/basic',
avi: 'video/x-msvideo',
bin: 'application/octet-stream',
bmp: 'image/bmp',
bz2: 'application/x-bzip2',
cab: 'application/vnd.ms-cab-compressed',
cbr: 'application/x-cbr',
chm: 'application/vnd.ms-htmlhelp',
css: 'text/css',
dmg: 'application/x-apple-diskimage',
doc: 'application/msword',
docx:
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
dotx:
'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
dtd: 'application/xml-dtd',
dwg: 'image/vnd.dwg',
eml: 'message/rfc822',
eot: 'application/vnd.ms-fontobject',
eps: 'application/postscript',
flv: 'video/x-flv',
gif: 'image/gif',
hqx: 'application/mac-binhex40',
htm: 'text/html',
html: 'text/html',
ico: 'image/x-icon',
iso: 'application/x-iso9660-image',
jar: 'application/java-archive',
jpeg: 'image/jpeg',
jpg: 'image/jpeg',
js: 'application/javascript',
json: 'application/json',
lnk: 'application/x-ms-shortcut',
log: 'text/plain',
m4a: 'audio/mp4',
m4p: 'application/mp4',
m4v: 'video/x-m4v',
map: 'application/json',
mcd: 'application/vnd.mcd',
mdb: 'application/x-msaccess',
mid: 'audio/midi',
midi: 'audio/midi',
mov: 'video/quicktime',
mp3: 'audio/mpeg',
mp4: 'video/mp4',
mpeg: 'video/mpeg',
mpg: 'video/mpeg',
ogg: 'audio/ogg',
otf: 'font/opentype',
pdf: 'application/pdf',
png: 'image/png',
potx: 'application/vnd.openxmlformats-officedocument.presentationml.template',
pps: 'application/vnd.ms-powerpoint',
ppsx:
'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
ppt: 'application/vnd.ms-powerpoint',
pptx:
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
ps: 'application/postscript',
psd: 'image/vnd.adobe.photoshop',
pub: 'application/x-mspublisher',
qxd: 'application/vnd.quark.quarkxpress',
ra: 'audio/x-pn-realaudio',
ram: 'audio/x-pn-realaudio',
rar: 'application/x-rar-compressed',
rdf: 'application/rdf+xml',
rm: 'application/vnd.rn-realmedia',
rmvb: 'application/vnd.rn-realmedia-vbr',
rtf: 'application/rtf',
sass: 'text/plain',
scss: 'text/plain',
sgml: 'text/sgml',
sit: 'application/x-stuffit',
sitx: 'application/x-stuffitx',
sldx: 'application/vnd.openxmlformats-officedocument.presentationml.slide',
svg: 'image/svg+xml',
swf: 'application/x-shockwave-flash',
tar: 'application/x-tar',
tiff: 'image/tiff',
tif: 'image/tiff',
torrent: 'application/x-bittorrent',
tsv: 'text/tab-separated-values',
ttf: 'application/x-font-ttf',
txt: 'text/plain',
vcd: 'application/x-cdlink',
wav: 'audio/x-wav',
wma: 'audio/x-ms-wma',
wmv: 'video/x-ms-wmv',
woff: 'application/font-woff',
woff2: 'application/font-woff2',
wpd: 'application/vnd.wordperfect',
wps: 'application/vnd.ms-works',
xlam: 'application/vnd.ms-excel.addin.macroenabled.12',
xls: 'application/vnd.ms-excel',
xlsb: 'application/vnd.ms-excel.sheet.binary.macroenabled.12',
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
xltx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
xml: 'application/xml',
zip: 'application/zip',
};
/* eslint-disable consistent-this */
'use strict';
const { ServerResponse } = require('http');
//log/report exception (http 50x)
//todo: don't send full file paths in response
ServerResponse.prototype.sendError = function(err) {
var res = this;
console.log(err.stack || err.toString());
if (!res.headersSent) {
var status = 500;
var headers = { 'Content-Type': 'text/plain' };
var body = err.stack;
res.writeHead(status, 'Internal Error', headers);
res.write(body + '\n');
}
res.end();
};
'use strict';
const http = require('http');
const fs = require('fs');
const qs = require('querystring');
const { join, basename, normalize } = require('path');
const mimeTypes = require('../lib/mime');
const utils = require('./http-utils');
//const INVALID_CHARS = /[\x00-\x1F\\\/:*?<>|&%",\u007E-\uFFFF]/g;
const INVALID_CHARS = /[^\w\d!#$'()+,\-;=@\[\]^`{}~]/g;
exports.tryStaticPath = (req, res, basePath, paths, callback) => {
var url = req.url.split('?')[0];
var tryStatic = [];
paths.forEach(function(path) {
var assetPrefix = join('/', path, '/').toLowerCase();
if (url.toLowerCase().indexOf(assetPrefix) === 0) {
//root here is filesystem path
tryStatic.push({ root: basePath, path: url });
}
});
if (!tryStatic.length) {
return callback();
}
var i = 0;
(function next() {
if (tryStatic[i]) {
serveAsset(req, res, tryStatic[i++], next);
} else {
callback();
}
})();
};
function serveAsset(req, res, opts, fallback) {
if (!opts.path) throw new Error('path required');
var isGet = req.method == 'GET';
var isHead = req.method == 'HEAD';
// ignore non-GET requests
if (opts.getOnly && !isGet && !isHead) {
return fallback();
}
// parse url
var path = qs.unescape(opts.path);
// null byte(s)
if (~path.indexOf('\0')) {
return sendHttpError(res, 400);
}
var root = opts.root ? normalize(opts.root) : null;
// when root is not given, consider .. malicious
if (!root && ~path.indexOf('..')) {
return sendHttpError(res, 403);
}
// join / normalize from optional root dir
path = normalize(join(root, path));
// malicious path
if (root && path.indexOf(root) !== 0) {
return sendHttpError(res, 403);
}
var hidden = opts.hidden;
// "hidden" file
if (!hidden && basename(path)[0] == '.') {
return fallback();
}
opts.path = path;
opts.charset = false; //don't assume any charset for asset
opts.enableRanges = true;
opts.enableCaching = true;
sendFile(req, res, opts, fallback);
}
function sendFile(req, res, opts, fallback) {
fs.stat(opts.path, function(err, stat) {
// ignore ENOENT
if (err) {
if (fallback && (err.code == 'ENOENT' || err.code == 'ENAMETOOLONG')) {
fallback();
} else {
res.sendError(err);
}
return;
} else if (stat.isDirectory()) {
if (fallback) {
fallback();
} else {
res.sendError(new Error('Specified resource is a directory'));
}
return;
}
// header fields
if (!res.getHeader('Date')) {
res.setHeader('Date', new Date().toUTCString());
}
//caching
if (opts.enableCaching) {
var maxAge = opts.maxAge || 0;
var cacheControl =
opts.cacheControl || 'public, max-age=' + maxAge / 1000;
//opts.cacheControl === false disables this header completely
if (!res.getHeader('Cache-Control') && opts.cacheControl !== false) {
res.setHeader('Cache-Control', cacheControl);
}
if (!res.getHeader('Last-Modified')) {
res.setHeader('Last-Modified', stat.mtime.toUTCString());
}
}
//ranges (download partial/resuming)
if (opts.enableRanges) {
res.setHeader('Accept-Ranges', 'bytes');
}
// mime/content-type
if (!res.getHeader('Content-Type')) {
var contentType =
opts.contentType ||
mimeTypes.getMime(opts.path) ||
'application/octet-stream';
//opts.charset === false disables charset completely
//if (opts.charset !== false) {
// var charset = opts.charset || mimeTypes.charsets.lookup(contentType);
// if (charset) contentType += '; charset=' + charset;
//}
res.setHeader('Content-Type', contentType);
}
var contentDisp = [];
if (opts.attachment) {
contentDisp.push('attachment');
}
if (opts.filename) {
contentDisp.push('filename="' + stripFilename(opts.filename) + '"');
}
if (contentDisp.length) {
res.setHeader('Content-Disposition', contentDisp.join('; '));
}
// conditional GET support
if (opts.enableCaching && utils.conditionalGET(req)) {
if (!utils.modified(req, res)) {
return utils.notModified(res);
}
}
var streamOpts = {};
var len = stat.size;
// we have a Range request
var ranges = req.headers.range;
if (
opts.enableRanges &&
ranges &&
(ranges = utils.parseRange(len, ranges))
) {
streamOpts.start = ranges[0].start;
streamOpts.end = ranges[0].end;
// unsatisfiable range
if (streamOpts.start > len - 1) {
res.setHeader('Content-Range', 'bytes */' + stat.size);
return sendHttpError(res, 416);
}
// limit last-byte-pos to current length
if (streamOpts.end > len - 1) streamOpts.end = len - 1;
// Content-Range
len = streamOpts.end - streamOpts.start + 1;
res.statusCode = 206;
res.setHeader(
'Content-Range',
'bytes ' + streamOpts.start + '-' + streamOpts.end + '/' + stat.size,
);
}
res.setHeader('Content-Length', len);
// transfer
if (req.method == 'HEAD') {
return res.end();
}
// stream
var stream = fs.createReadStream(opts.path, streamOpts);
req.on('close', stream.destroy.bind(stream));
stream.pipe(res);
stream.on('error', function(err) {
if (res.headersSent) {
console.error(err.stack);
req.destroy();
} else {
res.sendError(err);
}
});
});
}
/*!
* Helpers
*
*/
/** Send an http error (40x, except 404) */
function sendHttpError(res, code) {
if (!res.headersSent) {
var headers = { 'Content-Type': 'text/plain' };
res.writeHead(code, null, headers);
res.write(code + ' ' + http.STATUS_CODES[code]);
}
res.end();
}
//simplified version of util.stripFilename()
function stripFilename(filename) {
filename = String(filename);
return filename.replace(INVALID_CHARS, function(c) {
return encodeURIComponent(c);
});
}
/* eslint-disable one-var */
'use strict';
var RE_SLASHES = /\/+/g;
var RE_DOTSLASH = /\/.\//g;
var RE_DOTDOTSLASH = /[^\/]+\/\.\.\//g;
var RE_TRAILING_SLASHES = /\/+$/;
/*
* Join one or more paths using forward-slash
* path.join('assets/', 'scripts', 'file.js')
*/
exports.join = function() {
var a = [],
args = Array.from(arguments);
args.forEach(function(s) {
if (s) a.push(s);
});
return exports.normalize(a.join('/'));
};
/*
* Normalize a path removing '../', '//', etc
*/
exports.normalize = function(path) {
path = path.replace(RE_SLASHES, '/');
path = path.replace(RE_DOTSLASH, '/');
path = path.replace(RE_DOTDOTSLASH, '');
path = path.replace(RE_TRAILING_SLASHES, '');
return path;
};
/*
* Get the directory part of a path
* /data/file.txt -> /data/
*/
exports.dirname = function(path) {
var split = path.replace(RE_TRAILING_SLASHES, '').split('/');
split.pop();
return split.join('/');
};
/*
* Get the file part of a path
* /data/file.txt -> file.txt
*/
exports.basename = function(path) {
return path
.replace(RE_TRAILING_SLASHES, '')
.split('/')
.pop();
};
/**
* todo: alt flatten function to rename keys a=1&a=2 => {a[0]: 1, a[1]: 2)
*/
/* eslint-disable one-var */
'use strict';
var CHARS = /[^\w!$'()*,-.\/:;@[\\\]^{|}~]+/g;
var hasOwn = Object.hasOwnProperty;
var qs = (module.exports = {
escape: function(s) {
return String(s).replace(CHARS, function(s) {
return encodeURIComponent(s);
});
},
unescape: function(s) {
s = String(s).replace(/\+/g, ' ');
try {
return decodeURIComponent(s);
} catch (e) {
return unescape(s);
}
},
stringify: function(obj) {
var arr = [],
keys = Object.keys(obj),
len = keys.length;
for (var i = 0; i < len; i++) {
var key = keys[i],
name = qs.escape(key),
val = obj[key];
if (Array.isArray(val)) {
for (var j = 0; j < val.length; j++) {
arr.push(name + '=' + qs.escape(val[j]));
}
} else {
arr.push(name + '=' + qs.escape(val));
}
}
return arr.join('&');
},
parse: function(str, opts) {
opts = opts || {};
var obj = {};
if (typeof str == 'string') {
if (str.charAt(0) == '?') str = str.slice(1);
var split = str.split('&');
for (var i = 0, len = split.length; i < len; i++) {
var part = split[i],
pos = part.indexOf('=');
if (pos < 0) {
pos = part.length;
}
var key = part.slice(0, pos),
val = part.slice(pos + 1);
if (!key) continue;
key = qs.unescape(key);
//default to lowercase keys
if (opts.lcase !== false) {
key = key.toLowerCase();
}
if (hasOwn.call(obj, key)) {
obj[key].push(qs.unescape(val));
} else {
obj[key] = [qs.unescape(val)];
}
}
}
//flatten defaults to true (duplicates have their values concatenated with ', ')
if (opts.flatten !== false) {
qs.flatten(obj);
}
return obj;
},
flatten: function(obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
obj[key] = obj[key].join(', ');
}
},
});
//aliases
qs.encode = qs.escape;
qs.decode = qs.unescape;
'use strict';
const { eventify } = require('../eventify');
const BodyParser = require('../adapters/body-parser');
const qs = require('./qs');
const util = require('./util');
const HTTP_METHODS = { GET: 1, HEAD: 1, POST: 1, PUT: 1, DELETE: 1 };
const BODY_ALLOWED = { POST: 1, PUT: 1 };
function Request(req) {
this._super = req;
util.propagateEvents(req, this, 'end');
}
eventify(Request.prototype);
Object.assign(Request.prototype, {
url: function(part) {
var url = this._url || (this._url = parseURL(this._super.getURL()));
if (part) {
return url[part];
} else {
return url.raw;
}
},
method: function(s) {
if (!this._method) {
//method override (for JSONP and platforms that don't support PUT/DELETE)
//todo: this query param (_method) should be specified/disabled in config
var override = (
this.headers('X-HTTP-Method-Override') || this.query('_method')
).toUpperCase();
this._method =
override in HTTP_METHODS
? override
: this._super.getMethod().toUpperCase();
}
return typeof s == 'string'
? s.toUpperCase() == this._method
: this._method;
},
getRemoteIP: function() {
return this._super.getRemoteAddress();
},
headers: function(n) {
var headers =
this._headers || (this._headers = parseHeaders(this._super.getHeaders()));
if (arguments.length) {
return headers[n.toLowerCase()] || '';
} else {
return headers;
}
},
cookies: function(n) {
var cookies =
this._cookies || (this._cookies = parseCookies(this.headers('cookie')));
if (arguments.length) {
return cookies[n.toLowerCase()] || '';
} else {
return cookies;
}
},
query: function(n) {
var query = this._query || (this._query = qs.parse(this.url('qs')));
if (arguments.length) {
return query[n.toLowerCase()] || '';
} else {
return query;
}
},
body: function(n) {
var body = this._body || (this._body = this._parseBody());
if (arguments.length) {
return body[n.toLowerCase()];
} else {
return body;
}
},
_parseBody: function() {
try {
//body-parser events will be propagated to this
var body = this.method() in BODY_ALLOWED ? parseReqBody(this) : {};
} catch (e) {
this.emit('parse-error', e);
if (typeof e == 'string' && e.match(/^\d{3}\b/)) {
this.res.die(e);
} else {
this.res.die(400, {
error: 'Unable to parse request body; ' + e.message,
});
}
}
return body;
},
isUpload: function(item) {
return BodyParser.isUpload(item);
},
});
//Helpers
var REG_COOKIE_SEP = /[;,] */;
function parseURL(url) {
var pos = url.indexOf('?');
var search = pos > 0 ? url.slice(pos) : '';
var rawPath = search ? url.slice(0, pos) : url;
//todo: normalize rawPath: rawPath.split('/').map(decode).map(encode).join('/')
return {
raw: url,
rawPath: rawPath,
path: qs.unescape(rawPath),
search: search,
qs: search.slice(1),
};
}
function parseHeaders(input) {
//headers might already be parsed by _super.getHeaders()
if (typeof input !== 'string') {
return input;
}
return util.parseHeaders(input);
}
function parseCookies(str) {
str = str == null ? '' : String(str);
var cookies = {};
var parts = str.split(REG_COOKIE_SEP);
for (var i = 0, len = parts.length; i < len; i++) {
var part = parts[i];
var index = part.indexOf('=');
if (index < 0) {
index = part.length;
}
var key = part
.slice(0, index)
.trim()
.toLowerCase();
// no empty keys
if (!key) continue;
var value = part.slice(index + 1).trim();
// quoted values
if (value[0] == '"') value = value.slice(1, -1);
value = qs.unescape(value);
cookies[key] = cookies[key] ? cookies[key] + ', ' + value : value;
}
return cookies;
}
function parseReqBody(req) {
var _super = req._super;
var opts = {
//this allows us to turn on auto save at runtime before calling req.body()
autoSavePath: 'autoSavePath' in req ? req.autoSavePath : null,
};
//allow adapter request to instantiate its own parser
if (_super.getBodyParser) {
var parser = _super.getBodyParser(opts);
} else {
parser = new BodyParser(req.headers(), _super.read.bind(_super), opts);
}
util.propagateEvents(parser, req, 'file upload-progress');
return parser.parse();
}
module.exports = Request;
/* eslint-disable consistent-this */
'use strict';
const mimeTypes = require('../support/mime-types');
const util = require('./util');
const RE_CTYPE = /^[\w-]+\/[\w-]+$/;
const RE_STATUS = /^\d{3}\b/;
const TEXT_CTYPES = /^text\/|\/json$/i;
var httpResHeaders =
'Accept-Ranges Age Allow Cache-Control Connection Content-Encoding Content-Language ' +
'Content-Length Content-Location Content-MD5 Content-Disposition Content-Range Content-Type Date ETag Expires ' +
'Last-Modified Link Location P3P Pragma Proxy-Authenticate Refresh Retry-After Server Set-Cookie ' +
'Strict-Transport-Security Trailer Transfer-Encoding Vary Via Warning WWW-Authenticate X-Frame-Options ' +
'X-XSS-Protection X-Content-Type-Options X-Forwarded-Proto Front-End-Https X-Powered-By X-UA-Compatible';
//index headers by lowercase
httpResHeaders = httpResHeaders.split(' ').reduce(function(headers, header) {
headers[header.toLowerCase()] = header;
return headers;
}, {});
var statusCodes = {
100: 'Continue',
101: 'Switching Protocols',
102: 'Processing',
200: 'OK',
201: 'Created',
202: 'Accepted',
203: 'Non-Authoritative Information',
204: 'No Content',
205: 'Reset Content',
206: 'Partial Content',
207: 'Multi-Status',
300: 'Multiple Choices',
301: 'Moved Permanently',
302: 'Moved Temporarily',
303: 'See Other',
304: 'Not Modified',
305: 'Use Proxy',
307: 'Temporary Redirect',
400: 'Bad Request',
401: 'Unauthorized',
402: 'Payment Required',
403: 'Forbidden',
404: 'Not Found',
405: 'Method Not Allowed',
406: 'Not Acceptable',
407: 'Proxy Authentication Required',
408: 'Request Time-out',
409: 'Conflict',
410: 'Gone',
411: 'Length Required',
412: 'Precondition Failed',
413: 'Request Entity Too Large',
414: 'Request-URI Too Large',
415: 'Unsupported Media Type',
416: 'Requested Range Not Satisfiable',
417: 'Expectation Failed',
422: 'Unprocessable Entity',
423: 'Locked',
424: 'Failed Dependency',
425: 'Unordered Collection',
426: 'Upgrade Required',
428: 'Precondition Required',
429: 'Too Many Requests',
431: 'Request Header Fields Too Large',
500: 'Internal Server Error',
501: 'Not Implemented',
502: 'Bad Gateway',
503: 'Service Unavailable',
504: 'Gateway Time-out',
505: 'HTTP Version not supported',
506: 'Variant Also Negotiates',
507: 'Insufficient Storage',
509: 'Bandwidth Limit Exceeded',
510: 'Not Extended',
511: 'Network Authentication Required',
};
//headers that allow multiple
var allowMulti = { 'Set-Cookie': 1 };
var htmlRedirect = [
'<html>',
'<head><title>Redirecting ...</title><meta http-equiv="refresh" content="0;url=URL"></head>',
'<body onload="location.replace(document.getElementsByTagName(\'meta\')[0].content.slice(6))">',
'<noscript><p>If you are not redirected, <a href="URL">Click Here</a></p></noscript>',
//add padding to prevent "friendly" error messages in certain browsers
new Array(15).join('<' + '!-- PADDING --' + '>'),
'</body>',
'</html>',
].join('\r\n');
function Response(res) {
this._super = res;
this.clear();
}
Object.assign(Response.prototype, {
clear: function(type, status) {
//reset response buffer
this.buffer = {
status: status || '200 OK',
headers: { 'Content-Type': type || 'text/plain' },
charset: 'utf-8',
cookies: {},
body: [],
};
},
//these methods manipulate the response buffer
status: function(status) {
if (arguments.length) {
status = String(status);
if (status.match(RE_STATUS) && status.slice(0, 3) in statusCodes) {
this.buffer.status = status;
}
return this;
}
return this.buffer.status;
},
charset: function(charset) {
if (arguments.length) {
return (this.buffer.charset = charset);
} else {
return this.buffer.charset;
}
},
headers: function(name, value) {
var headers = this.buffer.headers;
//return headers
if (arguments.length === 0) {
return headers;
}
//set multiple from name/value pairs
if (name && typeof name == 'object') {
var res = this;
for (let [n, val] of Object.entries(name)) {
res.headers(n, val);
}
return this;
}
name = String(name);
name = httpResHeaders[name.toLowerCase()] || name;
if (arguments.length == 1) {
value = headers[name];
//certain headers allow multiple, so are saved as an array
return Array.isArray(value) ? value.join('; ') : value;
}
if (value === null) {
delete headers[name];
return this;
}
value = value ? String(value) : '';
if (name in allowMulti && name in headers) {
var existing = headers[name];
if (Array.isArray(existing)) {
existing.push(value);
} else {
headers[name] = [existing, value];
}
} else {
headers[name] = value;
}
return this;
},
write: function(data) {
//don't write anything for head requests
if (this.req.method('head')) return;
if (isPrimitive(data)) {
this.buffer.body.push(String(data));
} else if (Buffer.isBuffer(data)) {
this.buffer.body.push(data);
} else {
//stringify returns undefined in some cases
this.buffer.body.push(JSON.stringify(data) || '');
}
},
//these use the methods above to manipulate the response buffer
contentType: function(type) {
this.headers('Content-Type', type);
},
cookies: function(name, value) {
//cookies are a case-sensitive collection that will be serialized into
// Set-Cookie header(s) when response is sent
var cookies = this.buffer.cookies;
if (arguments.length === 0) {
return cookies;
}
//set multiple from name/value pairs
if (name && typeof name == 'object') {
var res = this;
for (let [n, val] of Object.entries(name)) {
res.cookies(n, val);
}
return this;
}
name = String(name);
if (arguments.length == 1) {
return cookies[name];
}
if (value === null) {
delete cookies[name];
return this;
}
var cookie = typeof value == 'object' ? value : { value: value };
cookie = cookie || {};
cookie.value = String(cookie.value);
cookies[name] = cookie;
return this;
},
//this preps the headers to be sent using _writeHead or _streamFile
_prepHeaders: function() {
var res = this;
var cookies = this.buffer.cookies;
for (let [name, cookie] of Object.entries(cookies)) {
res.headers('Set-Cookie', serializeCookie(name, cookie));
}
var contentType = buildContentType(
this.buffer.charset,
res.headers('Content-Type'),
);
res.headers('Content-Type', contentType);
if (res.headers('Cache-Control') == null) {
res.headers('Cache-Control', 'Private');
}
},
//these methods interface with the adapter (_super)
_writeHead: function() {
this._prepHeaders();
var status = parseStatus(this.buffer.status);
this._super.writeHead(status.code, status.reason, this.buffer.headers);
},
_streamFile: function(path, headers) {
var _super = this._super;
//todo: check file exists
this.headers(headers);
this._prepHeaders();
var status = parseStatus(this.buffer.status);
_super.streamFile(status.code, status.reason, this.buffer.headers, path);
},
end: function() {
var args = Array.from(arguments);
if (args.length > 1 && RE_STATUS.test(args[0])) {
this.status(args.shift());
}
if (args.length > 1 && RE_CTYPE.test(args[0])) {
this.contentType(args.shift());
}
for (var i = 0; i < args.length; i++) {
this.write(args[i]);
}
this._writeHead();
//write the buffered response
var _super = this._super;
this.buffer.body.forEach(function(chunk) {
_super.write(chunk);
});
_super.end();
},
//these build on the methods above
die: function() {
this.clear();
this.end.apply(this, arguments);
},
getWriteStream: function() {
return new ResponseWriteStream(this.req, this);
},
sendFile: function(opts) {
if (isPrimitive(opts)) {
opts = { file: String(opts) };
}
if (!opts.contentType && mimeTypes) {
var filename =
typeof opts.filename == 'string'
? opts.filename
: opts.file.split('/').pop();
var ext = getFileExt(filename);
opts.contentType = mimeTypes[ext];
}
var headers = opts.headers || {};
headers['Content-Type'] = opts.contentType || 'application/octet-stream';
var contentDisp = [];
if (opts.attachment) contentDisp.push('attachment');
if (opts.filename) {
//strip double-quote and comma, replacing other invalid chars with ~
filename = util.stripFilename(opts.filename, '~', { '"': '', ',': '' });
contentDisp.push('filename="' + filename + '"');
}
if (contentDisp.length) {
headers['Content-Disposition'] = contentDisp.join('; ');
}
this._streamFile(opts.file, headers);
},
redirect: function(url, type) {
if (type == 'html') {
this.htmlRedirect(url);
}
if (type == '301') {
this.status('301 Moved Permanently');
} else if (type == '303') {
this.status('303 See Other');
} else {
this.status('302 Moved');
}
this.headers('Location', url);
this.end();
},
htmlRedirect: function(url) {
var html = htmlRedirect.replace(/URL/g, util.htmlEnc(url));
this.end('text/html', html);
},
});
function ResponseWriteStream(req, res) {
this.req = req;
this.res = res;
}
Object.assign(ResponseWriteStream.prototype, {
write: function(data) {
if (!this.started) {
this.res._writeHead();
this.started = true;
}
this.res._super.write(data);
},
end: function() {
this.res._super.end();
},
});
function parseStatus(status) {
var statusCode = status.slice(0, 3);
return {
code: statusCode,
reason: status.slice(4) || statusCodes[statusCode],
};
}
function serializeCookie(name, cookie) {
var out = [];
out.push(name + '=' + encodeURIComponent(cookie.value));
if (cookie.domain) {
out.push('Domain=' + cookie.domain);
}
out.push('Path=' + (cookie.path || '/'));
if (cookie.expires) {
out.push('Expires=' + toGMTString(cookie.expires));
}
if (cookie.httpOnly) {
out.push('HttpOnly');
}
if (cookie.secure) {
out.push('Secure');
}
return out.join('; ');
}
function getFileExt(filename) {
var parts = filename
.split('/')
.pop()
.split('.');
return parts.length > 1 ? parts.pop().toLowerCase() : '';
}
function buildContentType(charset, contentType) {
//contentType may already have charset
contentType = contentType.split(';')[0];
charset = charset ? charset.toUpperCase() : '';
return charset && TEXT_CTYPES.test(contentType)
? contentType + '; charset=' + charset
: contentType;
}
function isPrimitive(obj) {
return obj !== Object(obj);
}
function toGMTString() {
var a = this.toUTCString().split(' ');
if (a[1].length == 1) a[1] = '0' + a[1];
return a.join(' ').replace(/UTC$/i, 'GMT');
}
module.exports = Response;
'use strict';
const RE_VERB = /^([A-Z]+):(.*)/;
function Router(routes) {
if (!(this instanceof Router)) {
return new Router(routes);
}
this._routes = [];
if (routes) {
for (let [pattern, handler] of routes) {
this.addRoute(pattern, handler);
}
}
}
Router.prototype.addRoute = function(pattern, handler) {
this._routes.push(parseRoute(pattern, handler));
};
Router.prototype.route = function(method, url, ...routeArgs) {
for (let route of this._routes) {
if (route.method !== '*' && route.method !== method) {
continue;
}
let captures = route.matcher(url);
if (captures) {
route.handler(routeArgs, captures);
}
}
};
function parseRoute(rawPattern, fn) {
let match = RE_VERB.exec(rawPattern);
let method = match ? match[1] : '*';
let pattern = match ? match[2] : rawPattern;
return {
method,
matcher: getMatcher(pattern),
handler: (routeArgs, captures) => {
return fn(...routeArgs, ...captures);
},
};
}
function getMatcher(pattern) {
let patternSegments = pattern.slice(1).split('/');
return (url) => {
let urlSegments = url.slice(1).split('/');
if (patternSegments.length !== urlSegments.length) {
return null;
}
let captures = [];
for (let i = 0; i < urlSegments.length; i++) {
let patternSegment = patternSegments[i];
let urlSegment = urlSegments[i];
if (patternSegment.charAt(0) === ':') {
captures.push(urlSegment);
} else if (patternSegment !== urlSegment) {
return null;
}
}
return captures;
};
}
module.exports = Router;
/* eslint-disable one-var */
'use strict';
exports.parse = function(url) {
var parts = url.match(
/^ *((https?):\/\/)?([^:\/]+)(:([0-9]+))?([^\?]*)(\?.*)?$/,
);
var parsed = {
protocol: parts[2] ? parts[2].toLowerCase() + ':' : 'http:',
hostname: parts[3] ? parts[3].toLowerCase() : '',
port: parts[5],
pathname: parts[6] || '/',
search: parts[7] || '',
};
parsed.port = parsed.port || (parts[2] == 'https' ? 443 : 80);
parsed.host = parsed.hostname + ':' + parsed.port;
parsed.path = parsed.pathname + parsed.search;
return parsed;
};
exports.resolve = function(oldUrl, newUrl) {
var ret;
if (~newUrl.indexOf('://')) {
//absolute URI, standards compliant
ret = newUrl;
} else {
var i =
newUrl.charAt(0) == '/'
? oldUrl.indexOf('/', 8)
: oldUrl.lastIndexOf('/') + 1;
ret = oldUrl.slice(0, i) + newUrl;
ret = exports.normalize(ret);
}
return ret;
};
exports.normalize = function(url) {
var base = '',
path = url,
search = '',
pos;
if (~url.indexOf('://')) {
if (~(pos = url.indexOf('/', 8))) {
base = url.slice(0, pos);
path = url.slice(pos);
} else {
//has no path
base = url;
path = '/';
}
}
if (~(pos = path.indexOf('?'))) {
search = path.slice(pos);
path = path.slice(0, pos - 1);
}
var oldPath;
while (path !== oldPath) {
//console.log((oldPath || '') + ' || ' + path);
oldPath = path;
path = path.replace(/\/+/g, '/');
path = path.replace(/\/\.(\/|$)/g, '$1');
path = path.replace(/(\/[^\/]+)?\/\.\.(\/|$)/g, '/');
}
return base + path + search;
};
/* eslint-disable one-var */
'use strict';
//regex for decoding percent-encoded strings
var PCT_SEQUENCE = /(%[0-9a-f]{2})+/gi;
exports.propagateEvents = function(src, dest, events) {
events = Array.isArray(events) ? events : String(events).split(' ');
events.forEach(function(event) {
src.on(event, function() {
dest.emit.apply(dest, [event].concat(Array.from(arguments)));
});
});
};
exports.inherits = function(ctor, parent) {
ctor.super_ = parent;
ctor.prototype = Object.create(parent.prototype, {
constructor: {
value: ctor,
enumerable: false,
writable: true,
configurable: true,
},
});
};
//parse a header value (e.g. Content-Disposition) accounting for
// various formats such as rfc5987: field*=UTF-8'en'a%20b
// todo: something like: ["multipart/alternative", {"boundary": "eb663d73ae0a4d6c9153cc0aec8b7520"}]
exports.parseHeaderValue = function(str) {
//replace quoted strings with encoded contents
str = String(str).replace(/"(.*?)"/g, function(_, str) {
return encodeURIComponent(str.replace(PCT_SEQUENCE, decode));
});
var results = {};
str.split(';').forEach(function(pair) {
var split = pair.trim().split('=');
var name = split[0],
value = split[1] || '';
if (name.slice(-1) == '*') {
name = name.slice(0, -1);
value = value.replace(/^[\w-]+'.*?'/, '');
}
if (name) {
results[name] = value.replace(PCT_SEQUENCE, decode);
}
});
return results;
};
//parse a set of HTTP headers
// todo: multi-line headers
exports.parseHeaders = function(input) {
//input = input.replace(/[ \t]*(\r\n)[ \t]+/g, ' ');
var headers = {};
var lines = input
.split('\r\n')
.join('\n')
.split('\n');
for (var i = 0, len = lines.length; i < len; i++) {
var line = lines[i];
var index = line.indexOf(':');
//discard lines without a :
if (index < 0) continue;
var key = line
.slice(0, index)
.trim()
.toLowerCase();
// no empty keys
if (!key) continue;
var value = line.slice(index + 1).trim();
headers[key] = headers[key] ? headers[key] + ', ' + value : value;
}
return headers;
};
//strip a filename to be ascii-safe
// used in Content-Disposition header
// will not encode space or: !#$'()+-.;=@[]^_`{}
exports.stripFilename = function(filename, ch, map) {
ch = ch || '';
var safe = String(filename);
//optional map of pre-substitutions (e.g. " -> ')
Object.keys(map || {}).forEach(function(ch) {
safe = safe.split(ch).join(map[ch]);
});
//control characters
// eslint-disable-next-line no-control-regex
safe = safe.replace(/[\x00-\x1F]+/g, ch);
//these are generally unsafe at the OS level
safe = safe.replace(/[\\\/:*?<>|&]+/g, ch);
//these have special meaning in Content-Disposition header
safe = safe.replace(/[%",]+/g, ch);
//ascii "del" and unicode characters
safe = safe.replace(/[\u007E-\uFFFF]+/g, ch);
if (ch) {
//replace duplicate separators
while (~safe.indexOf(ch + ch)) {
safe = safe.replace(ch + ch, ch);
}
}
return safe.trim();
};
exports.htmlEnc = function(str, /**Boolean=true*/ isAttr) {
str = String(str);
str = str.replace(/&/g, '&amp;');
str = str.replace(/>/g, '&gt;');
str = str.replace(/</g, '&lt;');
if (isAttr !== false) {
str = str.replace(/"/g, '&quot;');
}
str = str.replace(/\u00a0/g, '&nbsp;');
return str;
};
//decode a sequence of percent-encoded entities
// (similar to qs.decode or urlDecode)
function decode(str) {
try {
return decodeURIComponent(str);
} catch (e) {
return unescape(str);
}
}
const Fiber = require('./lib/fiber');
exports.toFiber = (fn) => {
return Fiber.fiberize(fn);
};