UNPKG

18.7 kBJavaScriptView Raw
1
2var fs = require("fs")
3, accept = require("./accept").accept
4, content = require("./content")
5, event = require("./event")
6, path = require("./path")
7, util = require("./util")
8, defaultOpts = {
9 maxBodySize: 1e6,
10 maxNameSize: 100,
11 maxFields: 1000,
12 maxFieldSize: 1000,
13 maxFiles: 1000,
14 maxFileSize: Infinity,
15 maxURILength: 2000,
16 log: console,
17 exitTime: 5000,
18 accept: {
19 'application/json;filename=;select=;space=': function(data, negod) {
20 return JSON.stringify(
21 data,
22 negod.select ? negod.select.split(",") : null,
23 +negod.space || negod.space
24 // Line and Paragraph separator needing to be escaped in JavaScript but not in JSON,
25 // escape those so the JSON can be evaluated or directly utilized within JSONP.
26 ).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029")
27 },
28 // RFC 4180 optional parameters: charset, header
29 'text/csv;br="\r\n";delimiter=",";fields=;filename=;header=;NULL=;select=': require("./csv.js").encode,
30 'application/sql;fields=;filename=;NULL=NULL;select=;table=table': function(data, negod) {
31 negod.re = /\D/
32 negod.br = "),\n("
33 negod.prefix = "INSERT INTO " +
34 negod.table + (negod.fields ? " (" + negod.fields + ")" : "") + " VALUES ("
35 negod.postfix = ");"
36 return csv.encode(data, negod)
37 }
38 },
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 opts._accept = accept(opts.accept)
206 opts._encoding = 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.static = createStatic
221 app.use = use
222
223 return app
224
225 function app(req, res, _next) {
226 var oldPath, oldUrl
227 , tryCatch = true
228 , usePos = 0
229 , forwarded = req.headers[opts.ipHeader || "x-forwarded-for"]
230
231 if (!res.send) {
232 req.date = new Date()
233 req.ip = forwarded ? forwarded.split(/[\s,]+/)[0] : req.connection && req.connection.remoteAddress
234 req.opts = res.opts = opts
235 req.res = res
236 res.req = req
237 res.send = send
238 res.sendStatus = sendStatus
239
240 // IE8-10 accept 2083 chars in URL
241 // Sitemaps protocol has a limit of 2048 characters in a URL
242 // Google SERP tool wouldn't cope with URLs longer than 1855 chars
243 if (req.url.length > opts.maxURILength) {
244 return sendError(res, opts, "URI Too Long")
245 }
246
247 req.content = content
248 req.cookie = getCookie
249 req.originalUrl = req.url
250
251 res.cookie = setCookie
252 res.link = setLink
253 res.sendFile = sendFile
254 }
255
256 next()
257
258 function next(err) {
259 if (err) {
260 return sendError(res, opts, err)
261 }
262 var method = uses[usePos]
263 , path = uses[usePos + 1]
264 , pos = usePos += 3
265
266 if (method && method !== req.method || path && path !== req.url.slice(0, path.length)) {
267 next()
268 } else if (uses[pos - 1] === void 0) {
269 if (typeof _next === "function") {
270 _next()
271 } else {
272 res.sendStatus(404)
273 }
274 } else {
275 method = uses[pos - 1]
276 if (path) {
277 oldPath = req.baseUrl
278 oldUrl = req.url
279 req.baseUrl = path
280 req.url = req.url.slice(path.length) || "/"
281 }
282 if (tryCatch === true) {
283 tryCatch = false
284 try {
285 method.call(app, req, res, path ? nextPath : next, opts)
286 } catch(e) {
287 return sendError(res, opts, e)
288 }
289 } else {
290 method.call(app, req, res, path ? nextPath : next, opts)
291 }
292 if (pos === usePos) {
293 tryCatch = true
294 }
295 }
296 }
297 function nextPath(e) {
298 req.baseUrl = oldPath
299 req.url = oldUrl
300 next(e)
301 }
302 }
303
304 function use(method, path) {
305 var fn
306 , arr = Array.from(arguments)
307 , len = arr.length
308 , i = 2
309 if (typeof method === "function") {
310 method = path = null
311 i = 0
312 } else if (typeof path === "function") {
313 path = method
314 method = null
315 i = 1
316 }
317 for (; i < len; ) {
318 if (typeof arr[i] !== "function") throw Error("Not a function")
319 uses.push(method, path, arr[i++])
320 }
321 return app
322 }
323}
324
325function createStatic(root_, opts_) {
326 var root = path.resolve(root_)
327 , opts = util.deepAssign({
328 index: "index.html",
329 maxAge: 31536000, // One year
330 cache: {
331 "cache.manifest": 0,
332 "worker.js": 0
333 }
334 }, opts_)
335
336 resolveFile("cache")
337 resolveFile("headers")
338
339 return function(req, res, next) {
340 var file
341
342 if (req.method !== "GET" && req.method !== "HEAD") {
343 res.setHeader("Allow", "GET, HEAD")
344 return res.sendStatus(405) // Method not allowed
345 }
346
347 if (req.url === "/" && !opts.index) {
348 return res.sendStatus(404)
349 }
350
351 try {
352 file = opts.url = path.resolve(root, (
353 req.url === "/" ?
354 opts.index :
355 "." + decodeURIComponent(req.url.split("?")[0].replace(/\+/g, " "))
356 ))
357 } catch (e) {
358 return res.sendStatus(400)
359 }
360
361 if (file.slice(0, root.length) !== root) {
362 return res.sendStatus(404)
363 }
364 res.sendFile(file, opts, function(err) {
365 next()
366 })
367 }
368
369 function resolveFile(name) {
370 if (!opts[name]) return
371 var file
372 , map = opts[name]
373 opts[name] = {}
374 for (file in map) if (hasOwn.call(map, file)) {
375 opts[name][file === "*" ? file : path.resolve(root, file)] = map[file]
376 }
377 }
378}
379
380function send(body, opts_) {
381 var tmp
382 , res = this
383 , reqHead = res.req.headers
384 , resHead = {}
385 , negod = res.opts._accept(reqHead.accept || reqHead["content-type"] || "*/*")
386 , opts = Object.assign({statusCode: res.statusCode}, res.opts, negod, opts_)
387 , outStream = opts.stream || res
388
389 if (!negod.match) {
390 return res.sendStatus(406) // Not Acceptable
391 }
392
393 tmp = opts.cache && opts.filename && opts.cache[opts.filename] || opts.maxAge
394 if (typeof tmp === "number") {
395 // max-age=N is relative to the time of the request
396 resHead["Cache-Control"] = tmp > 0 ? "public, max-age=" + tmp : "no-cache, max-age=0"
397 }
398
399 if (opts.mtime && opts.mtime > Date.parse(reqHead["if-modified-since"])) {
400 return res.sendStatus(304)
401 }
402
403 if (typeof body !== "string") {
404 negod.select = opts && opts.select || res.req.url.split("$select")[1] || ""
405 body = negod.o(body, negod)
406 opts.mimeType = negod.rule
407 }
408
409 resHead["Content-Type"] = opts.mimeType + (
410 opts.charset && opts.mimeType.slice(0, 5) === "text/" ? "; charset=" + opts.charset : ""
411 )
412
413 if (opts.size > 0 || opts.size === 0) {
414 resHead["Content-Length"] = opts.size
415 if (opts.size > opts.rangeSize) {
416 resHead["Accept-Ranges"] = "bytes"
417 resHead["Content-Length"] = opts.size
418
419 if (tmp = reqHead.range && !reqHead["if-range"] && rangeRe.exec(reqHead.range)) {
420 opts.start = range[1] ? +range[1] : range[2] ? opts.size - range[2] - 1 : 0
421 opts.end = range[1] && range[2] ? +range[2] : opts.size - 1
422
423 if (opts.start > opts.end || opts.end >= opts.size) {
424 opts.start = 0
425 opts.end = opts.size - 1
426 } else {
427 opts.statusCode = 206
428 resHead["Content-Length"] = opts.end - opts.start
429 resHead["Content-Range"] = "bytes " + opts.start + "-" + opts.end + "/" + opts.size
430 }
431 }
432 }
433 }
434
435 if (opts.filename) {
436 resHead["Content-Disposition"] = "attachment; filename=" + (
437 typeof opts.filename === "function" ? opts.filename() : opts.filename
438 )
439 }
440
441 negod = opts.compress && opts._encoding(reqHead["accept-encoding"])
442 if (negod.match) {
443 // Server may choose not to compress the body, if:
444 // - data is already compressed (some image format)
445 // - server is overloaded and cannot afford the computational overhead.
446 // Microsoft recommends not to compress if a server uses more than 80% of its computational power.
447 delete resHead["Content-Length"]
448 resHead["Content-Encoding"] = negod.match
449 resHead["Vary"] = "Accept-Encoding"
450 outStream = typeof negod.o === "string" ? require("zlib")[negod.o]() : negod.o()
451 outStream.pipe(res)
452 }
453
454 if (opts.headers) Object.assign(resHead, opts.headers["*"], opts.headers[opts.url || res.req.url])
455 res.writeHead(opts.statusCode || 200, resHead)
456
457 if (res.req.method == "HEAD") {
458 return res.end()
459 }
460
461 if (opts.sendfile) {
462 fs.createReadStream(opts.sendfile, {start: opts.start, end: opts.end}).pipe(outStream)
463 } else {
464 outStream.end(body)
465 }
466}
467
468function sendFile(file, opts_, next_) {
469 var res = this
470 , opts = typeof opts_ === "function" ? (next = opts_) && {} : opts_ || {}
471 , next = typeof next_ === "function" ? next_ : function(code) {
472 res.sendStatus(code)
473 }
474
475 fs.stat(file, function(err, stat) {
476 if (err) return next(404)
477 if (stat.isDirectory()) return next(403)
478
479 opts.mtime = stat.mtime
480 opts.size = stat.size
481 opts.filename = opts.download === true ? file.split("/").pop() : opts.download
482 opts.mimeType = res.opts.mime[ file.split(".").pop() ] || "application/octet-stream"
483 opts.sendfile = file
484
485 res.send(file, opts)
486 })
487}
488
489function sendStatus(code, message) {
490 var res = this
491 res.statusCode = code
492 if (code > 199 && code != 204 && code != 304) {
493 res.setHeader("Content-Type", "text/plain")
494 message = (message || res.opts.status[code] || code) + "\n"
495 res.setHeader("Content-Length", message.length)
496 if ("HEAD" != res.req.method) {
497 res.write(message)
498 }
499 }
500 res.end()
501}
502
503function sendError(res, opts, e) {
504 var message = typeof e === "string" ? e : e.message
505 , map = opts.error && (opts.error[message] || opts.error[e.name]) || {}
506 , error = {
507 id: Math.random().toString(36).slice(2,10),
508 time: res.req.date,
509 code: map.code || e.code || 500,
510 message: map.message || message
511 }
512 res.statusCode = error.code
513 res.statusMessage = opts.status[error.code] || message
514
515 res.send(error)
516
517 opts.log.error(
518 (e.stack || (e.name || "Error") + ": " + error.message).replace(":", ":" + error.id)
519 )
520}
521
522function setLink(url, rel) {
523 var res = this
524 , existing = res.getHeader("link") || []
525
526 if (!Array.isArray(existing)) {
527 existing = [ existing ]
528 }
529
530 existing.push('<' + encodeURI(url) + '>; rel="' + rel + '"')
531
532 res.setHeader("Link", existing)
533}
534
535function listen() {
536 var exiting
537 , server = this
538 , opts = server.opts
539
540 process.on("uncaughtException", function(e) {
541 if (opts.log) opts.log.error(
542 "\nUNCAUGHT EXCEPTION!\n" +
543 (e.stack || (e.name || "Error") + ": " + (e.message || e))
544 )
545 else throw e
546 ;(opts.exit || exit).call(server, 1)
547 })
548
549 process.on("SIGINT", function() {
550 if (exiting) {
551 opts.log.info("\nKilling from SIGINT (got Ctrl-C twice)")
552 return process.exit()
553 }
554 exiting = true
555 opts.log.info("\nGracefully shutting down from SIGINT (Ctrl-C)")
556 ;(opts.exit || exit).call(server, 0)
557 })
558
559 process.on("SIGTERM", function() {
560 opts.log.info("Gracefully shutting down from SIGTERM (kill)")
561 ;(opts.exit || exit).call(server, 0)
562 })
563
564 process.on("SIGHUP", function() {
565 opts.log.info("Reloading configuration from SIGHUP")
566 server.listen(true)
567 server.emit("reload")
568 })
569
570 server.listen = opts.listen || function() {
571 ;["http", "https", "http2"].forEach(createNet)
572 }
573
574 server.listen()
575
576 return server
577
578 function createNet(proto) {
579 var map = opts[proto]
580 , net = server["_" + proto] && !server["_" + proto].close()
581 if (!map || !map.port) return
582 net = server["_" + proto] = (
583 proto == "http" ?
584 require(proto).createServer(map.redirect ? forceHttps : server) :
585 require(proto).createSecureServer(map, map.redirect ? forceHttps : server)
586 )
587 .listen(map.port, map.host || "0.0.0.0", function() {
588 var addr = this.address()
589 opts.log.info("Listening %s at %s:%s", proto, addr.address, addr.port)
590 this.on("close", function() {
591 opts.log.info("Stop listening %s at %s:%s", proto, addr.address, addr.port)
592 })
593 if (map.noDelay) this.on("connection", setNoDelay)
594 })
595 if (map.sessionReuse) {
596 var sessionStore = {}
597 , timeout = map.sessionTimeout || 300
598
599 net
600 .on("newSession", function(id, data, cb) {
601 sessionStore[id] = data
602 cb()
603 })
604 .on("resumeSession", function(id, cb) {
605 cb(null, sessionStore[id] || null)
606 })
607 }
608 }
609
610 function setNoDelay(socket) {
611 if (socket.setNoDelay) socket.setNoDelay(true)
612 }
613
614 function forceHttps(req, res) {
615 // Safari 5 and IE9 drop the original URI's fragment if a HTTP/3xx redirect occurs.
616 // If the Location header on the response specifies a fragment, it is used.
617 // IE10+, Chrome 11+, Firefox 4+, and Opera will all "reattach" the original URI's fragment after following a 3xx redirection.
618 var port = opts.https && opts.https.port || 8443
619 , host = (req.headers.host || "localhost").split(":")[0]
620 , url = "https://" + (port == 443 ? host : host + ":" + port) + req.url
621
622 res.writeHead(301, {"Content-Type": "text/html", "Location": url})
623 res.end('Redirecting to <a href="' + url + '">' + url + '</a>')
624 }
625
626 function exit(code) {
627 var softKill = util.wait(function() {
628 opts.log.info("Everything closed cleanly")
629 process.exit(code)
630 }, 1)
631
632 server.emit("beforeExit", softKill)
633
634 try {
635 if (server._http) server._http.close(softKill.wait()).unref()
636 if (server._https) server._https.close(softKill.wait()).unref()
637 if (server._http2) server._http2.close(softKill.wait()).unref()
638 } catch(e) {}
639
640 setTimeout(function() {
641 opts.log.warn("Kill (timeout)")
642 process.exit(code)
643 }, opts.exitTime).unref()
644
645 softKill()
646 }
647}
648
649
650function setCookie(opts, value) {
651 var res = this
652 , existing = res.getHeader("set-cookie")
653 , cookie = (typeof opts === "string" ? (opts = { name: opts }) : opts).name
654 + ("=" + value).replace(cookieRe, encodeURIComponent)
655 + (opts.maxAge ? "; Expires=" + new Date(opts.maxAge > 0 ? Date.now() + (opts.maxAge*1000) : 0).toUTCString() : "")
656 + (opts.path ? "; Path=" + opts.path : "")
657 + (opts.domain ? "; Domain=" + opts.domain : "")
658 + (opts.secure ? "; Secure" : "")
659 + (opts.httpOnly ? "; HttpOnly" : "")
660 + (opts.sameSite ? "; SameSite=" + opts.sameSite : "")
661
662 if (Array.isArray(existing)) {
663 existing.push(cookie)
664 } else {
665 res.setHeader("Set-Cookie", existing ? [existing, cookie] : cookie)
666 }
667}
668
669function getCookie(opts) {
670 var req = this
671 , name = (typeof opts === "string" ? (opts = { name: opts }) : opts).name
672 , junks = ("; " + req.headers.cookie).split("; " + name + "=")
673
674 if (junks.length > 2) {
675 ;(opts.path || "").split("/").map(function(val, key, arr) {
676 var map = {
677 name: name,
678 maxAge: -1,
679 path: arr.slice(0, key + 1).join("/")
680 }
681 , domain = opts.domain
682 req.res.cookie(map, "")
683 if (domain) {
684 map.domain = domain
685 req.res.cookie(map, "")
686
687 if (domain !== (domain = domain.replace(/^[^.]+(?=\.(?=.+\.))/, "*"))) {
688 map.domain = domain
689 req.res.cookie(map, "")
690 }
691 }
692 })
693 req.opts.log.warn("Cookie fixation detected: %s", req.headers.cookie)
694 } else try {
695 return decodeURIComponent((junks[1] || "").split(";")[0])
696 } catch(e) {
697 req.opts.log.warn("Invalid cookie '%s' in: %s", name, req.headers.cookie)
698 }
699 return ""
700}
701
702