UNPKG

19.5 kBJavaScriptView Raw
1
2var defaultOpts = {
3 maxBodySize: 1e6,
4 maxNameSize: 100,
5 maxFields: 1000,
6 maxFieldSize: 1000,
7 maxFiles: 1000,
8 maxFileSize: Infinity,
9 maxURILength: 2000,
10 log: console,
11 exitTime: 5000,
12 accept: {
13 "application/json;filename=;select=;space=": function(data, negod) {
14 return JSON.stringify(
15 data,
16 negod.select ? negod.select.split(",") : null,
17 +negod.space || negod.space
18 // Line and Paragraph separator needing to be escaped in JavaScript but not in JSON,
19 // escape those so the JSON can be evaluated or directly utilized within JSONP.
20 ).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029")
21 },
22 // RFC 4180 optional parameters: charset, header
23 "text/csv;br=\"\r\n\";delimiter=\",\";fields=;filename=;header=;NULL=;select=": require("./csv.js").encode,
24 "application/sql;fields=;filename=;NULL=NULL;select=;table=table": function(data, negod) {
25 negod.re = /\D/
26 negod.br = "),\n("
27 negod.prefix = "INSERT INTO " +
28 negod.table + (negod.fields ? " (" + negod.fields + ")" : "") + " VALUES ("
29 negod.postfix = ");"
30 return require("./csv.js").encode(data, negod)
31 }
32 },
33 bodyRe: /^(?:PATCH|POST|PUT)$/i,
34 catch: sendError,
35 charset: "UTF-8",
36 compress: false,
37 encoding: {
38 "deflate;q=0.1": "createDeflate",
39 "gzip;q=0.2": "createGzip",
40 "br": "createBrotliCompress"
41 },
42 error: {
43 "URIError": { code: 400 }
44 },
45 method: {
46 "DELETE": "del",
47 "GET": "get",
48 "PATCH": "patch",
49 "POST": "post",
50 "PUT": "put"
51 },
52 mime: {
53 "asf": "video/x-ms-asf",
54 "asx": "video/x-ms-asx",
55 "avi": "video/x-msvideo",
56 "css": "text/css",
57 "csv": "text/csv",
58 "cur": "image/vnd.microsoft.icon",
59 "doc": "application/msword",
60 "drw": "application/drafting",
61 "dvi": "application/x-dvi",
62 "dwg": "application/acad",
63 "dxf": "application/dxf",
64 "gif": "image/gif",
65 "gz": "application/x-gzip",
66 "htm": "text/html",
67 "html": "text/html",
68 "ico": "image/x-icon",
69 "jar": "application/java-archive",
70 "jpeg": "image/jpeg",
71 "jpg": "image/jpeg",
72 "js": "text/javascript",
73 "json": "application/json",
74 "m3u": "audio/x-mpegurl",
75 "manifest": "text/cache-manifest",
76 "midi": "audio/midi",
77 "mjs": "text/javascript",
78 "mp3": "audio/mpeg",
79 "mp4": "video/mp4",
80 "mpeg": "video/mpeg",
81 "mpg": "video/mpeg",
82 "mpga": "audio/mpeg",
83 "pdf": "application/pdf",
84 "pgp": "application/pgp",
85 "png": "image/png",
86 "ppz": "application/vnd.ms-powerpoint",
87 "ps": "application/postscript",
88 "qt": "video/quicktime",
89 "ra": "audio/x-realaudio",
90 "rar": "application/x-rar-compressed",
91 "rm": "audio/x-pn-realaudio",
92 "rtf": "text/rtf",
93 "rtx": "text/richtext",
94 "sgml": "text/sgml",
95 "sh": "application/x-sh",
96 "snd": "audio/basic",
97 "sql": "application/sql",
98 "svg": "image/svg+xml",
99 "tex": "application/x-tex",
100 "tgz": "application/x-tar-gz",
101 "tiff": "image/tiff",
102 "tsv": "text/tab-separated-values",
103 "txt": "text/plain",
104 "wav": "audio/x-wav",
105 "wma": "audio/x-ms-wma",
106 "wmv": "video/x-ms-wmv",
107 "xls": "application/vnd.ms-excel",
108 "xlw": "application/vnd.ms-excel",
109 "xml": "text/xml",
110 "zip": "application/zip"
111 },
112 rangeSize: 500 * 1024,
113 status: {
114 200: "OK",
115 201: "Created",
116 202: "Accepted",
117 203: "Non-Authoritative Information",
118 204: "No Content",
119 205: "Reset Content",
120 206: "Partial Content",
121 207: "Multi-Status",
122 208: "Already Reported",
123 226: "IM Used",
124 300: "Multiple Choices",
125 301: "Moved Permanently",
126 302: "Found",
127 303: "See Other",
128 304: "Not Modified",
129 305: "Use Proxy",
130 307: "Temporary Redirect",
131 308: "Permanent Redirect",
132 400: "Bad Request",
133 401: "Unauthorized",
134 402: "Payment Required",
135 403: "Forbidden",
136 404: "Not Found",
137 405: "Method Not Allowed",
138 406: "Not Acceptable",
139 407: "Proxy Authentication Required",
140 408: "Request Timeout",
141 409: "Conflict",
142 410: "Gone",
143 411: "Length Required",
144 412: "Precondition Failed",
145 413: "Payload Too Large",
146 414: "URI Too Long",
147 415: "Unsupported Media Type",
148 416: "Range Not Satisfiable",
149 417: "Expectation Failed",
150 421: "Misdirected Request",
151 422: "Unprocessable Entity",
152 423: "Locked",
153 424: "Failed Dependency",
154 425: "Too Early",
155 426: "Upgrade Required",
156 428: "Precondition Required",
157 429: "Too Many Requests",
158 451: "Unavailable For Legal Reasons",
159 500: "Internal Server Error",
160 501: "Not Implemented",
161 502: "Bad Gateway",
162 503: "Service Unavailable",
163 504: "Gateway Timeout",
164 505: "HTTP Version Not Supported",
165 506: "Variant Also Negotiates",
166 507: "Insufficient Storage",
167 508: "Loop Detected",
168 509: "Bandwidth Limit Exceeded",
169 510: "Not Extended",
170 511: "Network Authentication Required"
171 },
172 tmp: (
173 process.env.TMPDIR ||
174 process.env.TEMP ||
175 process.env.TMP ||
176 (
177 process.platform === "win32"
178 /* istanbul ignore next */
179 ? (process.env.SystemRoot || process.env.windir) + "\\temp"
180 : "/tmp"
181 )
182 ).replace(/([^:])[/\\]+$/, "$1") + "/up-" + process.pid + "-",
183 http: {
184 port: 8080
185 }
186}
187, fs = require("fs")
188, content = require("./content")
189, event = require("./event")
190, path = require("./path")
191, util = require("./util")
192, hasOwn = defaultOpts.hasOwnProperty
193, cookieRe = /[^!#-~]|[%,;\\]/g
194, rangeRe = /^bytes=(\d*)-(\d*)^/
195, tmpDate = new Date()
196
197module.exports = createApp
198createApp.getCookie = getCookie
199createApp.setCookie = setCookie
200createApp.static = createStatic
201
202function createApp(opts_) {
203 var uses = []
204 , opts = util.deepAssign(app.opts = {defaults: defaultOpts}, defaultOpts, opts_)
205
206 event.asEmitter(app)
207 if (!opts._accept) opts._accept = require("./accept").accept(opts.accept)
208 if (!opts._encoding) opts._encoding = require("./accept").accept(opts.encoding)
209
210 Object.keys(opts.method).forEach(function(method) {
211 app[opts.method[method]] = function() {
212 var arr = uses.slice.call(arguments)
213 if (typeof arr[0] === "function") {
214 arr.unshift(null)
215 }
216 arr.unshift(method)
217 return use.apply(app, arr)
218 }
219 })
220
221 app.listen = listen
222 app.readBody = readBody
223 // TODO:2021-08-24:lauri:Remove on next major
224 app.static = createStatic
225 app.use = use
226
227 return app
228
229 function app(req, res, _next) {
230 var oldPath, oldUrl
231 , tryCatch = true
232 , usePos = 0
233 , forwarded = req.headers[opts.ipHeader || "x-forwarded-for"]
234
235 if (!res.send) {
236 req.date = new Date()
237 req.ip = forwarded ? forwarded.split(/[\s,]+/)[0] : req.socket && req.socket.remoteAddress
238 req.opts = res.opts = opts
239 req.res = res
240 res.req = req
241 res.send = send
242 res.sendStatus = sendStatus
243
244 // IE8-10 accept 2083 chars in URL
245 // Sitemaps protocol has a limit of 2048 characters in a URL
246 // Google SERP tool wouldn't cope with URLs longer than 1855 chars
247 if (req.url.length > opts.maxURILength) {
248 return opts.catch("URI Too Long", req, res, opts)
249 }
250
251 req.content = content
252 req.cookie = getCookie
253 req.originalUrl = req.url
254
255 res.cookie = setCookie
256 res.link = setLink
257 res.sendFile = sendFile
258 }
259
260 next()
261
262 function next(err) {
263 if (err) {
264 return opts.catch(err, req, res, opts)
265 }
266 var method = uses[usePos]
267 , path = uses[usePos + 1]
268 , pos = usePos += 3
269
270 if (method && method !== req.method || path && path !== req.url.slice(0, path.length)) {
271 next()
272 } else if (uses[pos - 1] === void 0) {
273 if (typeof _next === "function") {
274 _next()
275 } else {
276 opts.catch(404, req, res, opts)
277 }
278 } else {
279 method = uses[pos - 1]
280 if (path) {
281 oldPath = req.baseUrl
282 oldUrl = req.url
283 req.baseUrl = path
284 req.url = req.url === path ? "/" : req.url.slice(path.slice(-1) === "/" ? path.length - 1 : path.length)
285 }
286 if (tryCatch === true) {
287 tryCatch = false
288 try {
289 method.call(app, req, res, path ? nextPath : next, opts)
290 } catch(e) {
291 return opts.catch(e, req, res, opts)
292 }
293 } else {
294 method.call(app, req, res, path ? nextPath : next, opts)
295 }
296 if (pos === usePos) {
297 tryCatch = true
298 }
299 }
300 }
301 function nextPath(e) {
302 req.baseUrl = oldPath
303 req.url = oldUrl
304 next(e)
305 }
306 }
307
308 function use(method, path) {
309 var arr = Array.from(arguments)
310 , len = arr.length
311 , i = 2
312 if (typeof method === "function") {
313 method = path = null
314 i = 0
315 } else if (typeof path === "function") {
316 path = method
317 method = null
318 i = 1
319 }
320 for (; i < len; ) {
321 if (typeof arr[i] !== "function") throw Error("Not a function")
322 uses.push(method, path, arr[i++])
323 }
324 return app
325 }
326}
327
328function createStatic(root, opts) {
329 root = path.resolve(root)
330 opts = util.deepAssign({
331 fallthrough: true,
332 index: "index.html",
333 maxAge: "1 year",
334 cache: {
335 "cache.manifest": 0,
336 "worker.js": 0
337 }
338 }, opts)
339
340 resolveFile("cache", util.num)
341 resolveFile("headers")
342
343 return function(req, res, next) {
344 var file
345
346 if (req.method !== "GET" && req.method !== "HEAD") {
347 if (opts.fallthrough !== true) res.setHeader("Allow", "GET, HEAD")
348 return fall(405) // Method not allowed
349 }
350
351 if (req.url === "/") {
352 if (!opts.index) return fall(404)
353 if (typeof opts.index === "function") return opts.index(req, res, next, opts)
354 file = path.resolve(root, opts.index)
355 } else try {
356 file = path.resolve(root, "." + decodeURIComponent(req.url.split("?")[0].replace(/\+/g, " ")))
357 } catch (e) {
358 return fall(400)
359 }
360
361 if (file.slice(0, root.length) !== root) {
362 return fall(404)
363 }
364 sendFile.call(res, file, opts, fall)
365 function fall(err) {
366 next(opts.fallthrough === true ? null : err)
367 }
368 }
369
370 function resolveFile(name, util) {
371 if (!opts[name]) return
372 var file
373 , map = opts[name]
374 opts[name] = {}
375 for (file in map) if (hasOwn.call(map, file)) {
376 opts[name][file === "*" ? file : path.resolve(root, file)] = util ? util(map[file]) : map[file]
377 }
378 }
379}
380
381function readBody(req, res, next, opts) {
382 if (req.body === void 0 && opts.bodyRe.test(req.method)) {
383 req.content(next)
384 } else {
385 next()
386 }
387}
388
389function send(body, opts_) {
390 var tmp
391 , res = this
392 , reqHead = res.req.headers
393 , resHead = {}
394 , negod = res.opts._accept(reqHead.accept || reqHead["content-type"] || "*/*")
395 , opts = Object.assign({
396 statusCode: res.statusCode,
397 mimeType: negod.rule
398 }, res.opts, negod, opts_)
399 , outStream = opts.stream || res
400
401 if (!negod.match) {
402 return res.sendStatus(406) // Not Acceptable
403 }
404
405 tmp = util.num(opts.cache && opts.filename && opts.cache[opts.filename], opts.maxAge)
406 if (typeof tmp === "number") {
407 // max-age=N is relative to the time of the request
408 resHead["Cache-Control"] = tmp > 0 ? "public, max-age=" + (0|(tmp/1000)) : "no-cache, max-age=0"
409 }
410
411 if (opts.mtime && opts.mtime > Date.parse(reqHead["if-modified-since"])) {
412 return res.sendStatus(304)
413 }
414
415 if (typeof body !== "string") {
416 negod.select = opts && opts.select || res.req.url.split("$select")[1] || ""
417 body = negod.o(body, negod)
418 }
419
420 resHead["Content-Type"] = opts.mimeType + (
421 opts.charset && opts.mimeType.slice(0, 5) === "text/" ? "; charset=" + opts.charset : ""
422 )
423
424 if (opts.size >= 0) {
425 resHead["Content-Length"] = opts.size
426 if (opts.size > opts.rangeSize) {
427 resHead["Accept-Ranges"] = "bytes"
428 resHead["Content-Length"] = opts.size
429
430 if ((tmp = reqHead.range && !reqHead["if-range"] && rangeRe.exec(reqHead.range))) {
431 opts.start = tmp[1] ? +tmp[1] : tmp[2] ? opts.size - tmp[2] - 1 : 0
432 opts.end = tmp[1] && tmp[2] ? +tmp[2] : opts.size - 1
433
434 if (opts.start > opts.end || opts.end >= opts.size) {
435 opts.start = 0
436 opts.end = opts.size - 1
437 } else {
438 opts.statusCode = 206
439 resHead["Content-Length"] = opts.end - opts.start
440 resHead["Content-Range"] = "bytes " + opts.start + "-" + opts.end + "/" + opts.size
441 }
442 }
443 }
444 }
445
446 if (opts.filename) {
447 resHead["Content-Disposition"] = "attachment; filename=" + (
448 typeof opts.filename === "function" ? opts.filename() : opts.filename
449 )
450 }
451
452 negod = opts.compress && opts._encoding(reqHead["accept-encoding"])
453 if (negod.match) {
454 // Server may choose not to compress the body, if:
455 // - data is already compressed (some image format)
456 // - server is overloaded and cannot afford the computational overhead.
457 // Microsoft recommends not to compress if a server uses more than 80% of its computational power.
458 delete resHead["Content-Length"]
459 resHead["Content-Encoding"] = negod.match
460 resHead.Vary = "Accept-Encoding"
461 outStream = typeof negod.o === "string" ? require("zlib")[negod.o]() : negod.o()
462 outStream.pipe(res)
463 }
464
465 if (opts.headers) Object.assign(resHead, opts.headers["*"], opts.headers[opts.sendfile || res.req.url])
466 res.writeHead(opts.statusCode || 200, resHead)
467
468 if (res.req.method === "HEAD") {
469 return res.end()
470 }
471
472 if (opts.sendfile) {
473 fs.createReadStream(opts.sendfile, {start: opts.start, end: opts.end}).pipe(outStream)
474 } else {
475 outStream.end(body)
476 }
477}
478
479function sendFile(file, opts_, next_) {
480 var res = this
481 , opts = typeof opts_ === "function" ? (next_ = opts_) && {} : opts_ || {}
482 , next = typeof next_ === "function" ? next_ : function(code) {
483 sendStatus.call(res, code)
484 }
485
486 fs.stat(file, function(err, stat) {
487 if (err) return next(404)
488 if (stat.isDirectory()) return next(403)
489
490 opts.mtime = stat.mtime
491 opts.size = stat.size
492 opts.filename = opts.download === true ? file.split("/").pop() : opts.download
493 opts.mimeType = res.opts.mime[ file.split(".").pop() ] || "application/octet-stream"
494 opts.sendfile = file
495
496 send.call(res, file, opts)
497 })
498}
499
500function sendStatus(code, message) {
501 var res = this
502 res.statusCode = code
503 if (code === 204 || code === 205 || code === 304) {
504 res.setHeader("Content-Length", 0)
505 } else if (code > 199) {
506 message = (message || res.opts.status[code] || code) + "\n"
507 res.setHeader("Content-Length", message.length)
508 res.setHeader("Content-Type", "text/plain")
509 if ("HEAD" !== res.req.method) {
510 res.write(message)
511 }
512 }
513 res.end()
514}
515
516function sendError(e, req, res, opts) {
517 var message = typeof e === "string" ? e : e.message
518 , map = opts.error && (opts.error[message] || opts.error[e.name]) || {}
519 , error = {
520 id: Math.random().toString(36).slice(2, 10),
521 time: req.date,
522 code: map.code || e.code || 500,
523 message: map.message || message
524 }
525 res.statusCode = error.code
526 res.statusMessage = opts.status[error.code] || message
527
528 res.send(error)
529
530 opts.log.error(
531 (Array.isArray(e.stack) ? e.stack.join("\n") : e.stack || (e.name || "Error") + ": " + error.message).replace("Error:", "Error:" + error.id)
532 )
533}
534
535function setLink(url, rel) {
536 var res = this
537 , existing = res.getHeader("link") || []
538
539 if (!Array.isArray(existing)) {
540 existing = [ existing ]
541 }
542
543 existing.push("<" + encodeURI(url) + ">; rel=\"" + rel + "\"")
544
545 res.setHeader("Link", existing)
546}
547
548function listen() {
549 var exiting
550 , server = this
551 , opts = server.opts
552
553 process.on("uncaughtException", function(e) {
554 if (opts.log) opts.log.error(
555 "\nUNCAUGHT EXCEPTION!\n" +
556 (Array.isArray(e.stack) ? e.message + "\n" + e.stack.join("\n") : e.stack || (e.name || "Error") + ": " + (e.message || e))
557 )
558 else throw e
559 ;(opts.exit || exit).call(server, 1)
560 })
561
562 process.on("SIGINT", function() {
563 if (exiting) {
564 opts.log.info("\nKilling from SIGINT (got Ctrl-C twice)")
565 return process.exit()
566 }
567 exiting = true
568 opts.log.info("\nGracefully shutting down from SIGINT (Ctrl-C)")
569 ;(opts.exit || exit).call(server, 0)
570 })
571
572 process.on("SIGTERM", function() {
573 opts.log.info("Gracefully shutting down from SIGTERM (kill)")
574 ;(opts.exit || exit).call(server, 0)
575 })
576
577 process.on("SIGHUP", function() {
578 opts.log.info("Reloading configuration from SIGHUP")
579 server.listen(true)
580 server.emit("reload")
581 })
582
583 server.listen = opts.listen || function() {
584 ["http", "https", "http2"].forEach(createNet)
585 }
586
587 server.listen()
588
589 return server
590
591 function createNet(proto) {
592 var map = opts[proto]
593 , net = server["_" + proto] && !server["_" + proto].close()
594 if (!map || !map.port) return
595 net = server["_" + proto] = (
596 proto === "http" ?
597 require(proto).createServer(map.redirect ? forceHttps : server) :
598 require(proto).createSecureServer(map, map.redirect ? forceHttps : server)
599 )
600 .listen(map.port, map.host || "0.0.0.0", function() {
601 var addr = this.address()
602 opts.log.info("Listening %s at %s:%s", proto, addr.address, addr.port)
603 this.on("close", function() {
604 opts.log.info("Stop listening %s at %s:%s", proto, addr.address, addr.port)
605 })
606 if (map.noDelay) this.on("connection", setNoDelay)
607 })
608 if (map.sessionReuse) {
609 var sessionStore = {}
610
611 net
612 .on("newSession", function(id, data, cb) {
613 sessionStore[id] = data
614 cb()
615 })
616 .on("resumeSession", function(id, cb) {
617 cb(null, sessionStore[id] || null)
618 })
619 }
620 }
621
622 function setNoDelay(socket) {
623 if (socket.setNoDelay) socket.setNoDelay(true)
624 }
625
626 function forceHttps(req, res) {
627 // Safari 5 and IE9 drop the original URI's fragment if a HTTP/3xx redirect occurs.
628 // If the Location header on the response specifies a fragment, it is used.
629 // IE10+, Chrome 11+, Firefox 4+, and Opera will all "reattach" the original URI's fragment after following a 3xx redirection.
630 var port = opts.https && opts.https.port || 8443
631 , host = (req.headers.host || "localhost").split(":")[0]
632 , url = "https://" + (port === 443 ? host : host + ":" + port) + req.url
633
634 res.writeHead(301, {"Content-Type": "text/html", "Location": url})
635 res.end("Redirecting to <a href=\"" + url + "\">" + url + "</a>")
636 }
637
638 function exit(code) {
639 var softKill = util.wait(function() {
640 opts.log.info("Everything closed cleanly")
641 process.exit(code)
642 }, 1)
643
644 server.emit("beforeExit", softKill)
645
646 try {
647 if (server._http) server._http.close(softKill.wait()).unref()
648 if (server._https) server._https.close(softKill.wait()).unref()
649 if (server._http2) server._http2.close(softKill.wait()).unref()
650 } catch(e) {}
651
652 setTimeout(function() {
653 opts.log.warn("Kill (timeout)")
654 process.exit(code)
655 }, opts.exitTime).unref()
656
657 softKill()
658 }
659}
660
661function setCookie(opts, value) {
662 var res = this
663 , existing = res.getHeader("set-cookie")
664 , cookie = (typeof opts === "string" ? (opts = { name: opts }) : opts).name +
665 ("=" + value).replace(cookieRe, encodeURIComponent) +
666 (opts.maxAge ? "; Expires=" + (tmpDate.setTime(opts.maxAge < 1 ? 0 : Date.now() + util.num(opts.maxAge)), tmpDate).toUTCString() : "") +
667 (opts.path ? "; Path=" + opts.path : "") +
668 (opts.domain ? "; Domain=" + opts.domain : "") +
669 (opts.secure ? "; Secure" : "") +
670 (opts.httpOnly ? "; HttpOnly" : "") +
671 (opts.sameSite ? "; SameSite=" + opts.sameSite : "")
672
673 if (Array.isArray(existing)) {
674 existing.push(cookie)
675 } else {
676 res.setHeader("Set-Cookie", existing ? [existing, cookie] : cookie)
677 }
678}
679
680function getCookie(opts) {
681 var req = this
682 , name = (typeof opts === "string" ? (opts = { name: opts }) : opts).name
683 , junks = ("; " + req.headers.cookie).split("; " + name + "=")
684
685 if (junks.length > 2) {
686 (opts.path || "").split("/").map(function(val, key, arr) {
687 var map = {
688 name: name,
689 maxAge: -1,
690 path: arr.slice(0, key + 1).join("/")
691 }
692 , domain = opts.domain
693 req.res.cookie(map, "")
694 if (domain) {
695 map.domain = domain
696 req.res.cookie(map, "")
697
698 if (domain !== (domain = domain.replace(/^[^.]+(?=\.(?=.+\.))/, "*"))) {
699 map.domain = domain
700 req.res.cookie(map, "")
701 }
702 }
703 })
704 req.opts.log.warn("Cookie fixation detected: %s", req.headers.cookie)
705 } else try {
706 return decodeURIComponent((junks[1] || "").split(";")[0])
707 } catch(e) {
708 req.opts.log.warn("Invalid cookie '%s' in: %s", name, req.headers.cookie)
709 }
710 return ""
711}
712
713