UNPKG

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