+963
-1
@@ -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, '&'); | ||
| str = str.replace(/>/g, '>'); | ||
| str = str.replace(/</g, '<'); | ||
| if (isAttr) { | ||
| str = str.replace(/"/g, '"'); | ||
| } | ||
| str = str.replace(/\u00a0/g, ' '); | ||
| 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'); |
-1
| module.exports = require('./src/adapters/fs'); |
-1
| 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; |
-136
| /* 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, '&'); | ||
| str = str.replace(/>/g, '>'); | ||
| str = str.replace(/</g, '<'); | ||
| if (isAttr !== false) { | ||
| str = str.replace(/"/g, '"'); | ||
| } | ||
| str = str.replace(/\u00a0/g, ' '); | ||
| 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); | ||
| }; |
Explicitly Unlicensed Item
LicenseSomething was found which is explicitly marked as unlicensed.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Explicitly Unlicensed Item
LicenseSomething was found which is explicitly marked as unlicensed.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
1
-75%0
-100%6
-45.45%3
-25%26091
-66.14%3
-90%948
-63.64%- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed