UNPKG

18.8 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 )
25 },
26 // RFC 4180 optional parameters: charset, header
27 'text/csv;br="\r\n";delimiter=",";fields=;filename=;header=;NULL=;select=': require("./csv.js").encode,
28 'application/sql;fields=;filename=;NULL=NULL;select=;table=table': function(data, negod) {
29 negod.re = /\D/
30 negod.br = "),\n("
31 negod.prefix = "INSERT INTO " +
32 negod.table + (negod.fields ? " (" + negod.fields + ")" : "") + " VALUES ("
33 negod.postfix = ");"
34 return csv.encode(data, negod)
35 }
36 },
37 charset: "UTF-8",
38 compress: false,
39 encoding: {
40 "deflate;q=0.1": "createDeflate",
41 "gzip;q=0.2": "createGzip",
42 "br": "createBrotliCompress"
43 },
44 error: {
45 "URIError": { code: 400 }
46 },
47 method: {
48 "DELETE": "del",
49 "GET": "get",
50 "PATCH": "patch",
51 "POST": "post",
52 "PUT": "put"
53 },
54 mime: {
55 "asf": "video/x-ms-asf",
56 "asx": "video/x-ms-asx",
57 "avi": "video/x-msvideo",
58 "css": "text/css",
59 "csv": "text/csv",
60 "cur": "image/vnd.microsoft.icon",
61 "doc": "application/msword",
62 "drw": "application/drafting",
63 "dvi": "application/x-dvi",
64 "dwg": "application/acad",
65 "dxf": "application/dxf",
66 "gif": "image/gif",
67 "gz": "application/x-gzip",
68 "htm": "text/html",
69 "html": "text/html",
70 "ico": "image/x-icon",
71 "jar": "application/java-archive",
72 "jpeg": "image/jpeg",
73 "jpg": "image/jpeg",
74 "js": "text/javascript",
75 "json": "application/json",
76 "m3u": "audio/x-mpegurl",
77 "manifest": "text/cache-manifest",
78 "midi": "audio/midi",
79 "mjs": "text/javascript",
80 "mp3": "audio/mpeg",
81 "mp4": "video/mp4",
82 "mpeg": "video/mpeg",
83 "mpg": "video/mpeg",
84 "mpga": "audio/mpeg",
85 "pdf": "application/pdf",
86 "pgp": "application/pgp",
87 "png": "image/png",
88 "ppz": "application/vnd.ms-powerpoint",
89 "ps": "application/postscript",
90 "qt": "video/quicktime",
91 "ra": "audio/x-realaudio",
92 "rar": "application/x-rar-compressed",
93 "rm": "audio/x-pn-realaudio",
94 "rtf": "text/rtf",
95 "rtx": "text/richtext",
96 "sgml": "text/sgml",
97 "sh": "application/x-sh",
98 "snd": "audio/basic",
99 "sql": "application/sql",
100 "svg": "image/svg+xml",
101 "tex": "application/x-tex",
102 "tgz": "application/x-tar-gz",
103 "tiff": "image/tiff",
104 "tsv": "text/tab-separated-values",
105 "txt": "text/plain",
106 "wav": "audio/x-wav",
107 "wma": "audio/x-ms-wma",
108 "wmv": "video/x-ms-wmv",
109 "xls": "application/vnd.ms-excel",
110 "xlw": "application/vnd.ms-excel",
111 "xml": "text/xml",
112 "zip": "application/zip"
113 },
114 mimeType: "application/octet-stream",
115 rangeSize: 500 * 1024,
116 status: {
117 200: "OK",
118 201: "Created",
119 202: "Accepted",
120 203: "Non-Authoritative Information",
121 204: "No Content",
122 205: "Reset Content",
123 206: "Partial Content",
124 207: "Multi-Status",
125 208: "Already Reported",
126 226: "IM Used",
127 300: "Multiple Choices",
128 301: "Moved Permanently",
129 302: "Found",
130 303: "See Other",
131 304: "Not Modified",
132 305: "Use Proxy",
133 307: "Temporary Redirect",
134 308: "Permanent Redirect",
135 400: "Bad Request",
136 401: "Unauthorized",
137 402: "Payment Required",
138 403: "Forbidden",
139 404: "Not Found",
140 405: "Method Not Allowed",
141 406: "Not Acceptable",
142 407: "Proxy Authentication Required",
143 408: "Request Timeout",
144 409: "Conflict",
145 410: "Gone",
146 411: "Length Required",
147 412: "Precondition Failed",
148 413: "Payload Too Large",
149 414: "URI Too Long",
150 415: "Unsupported Media Type",
151 416: "Range Not Satisfiable",
152 417: "Expectation Failed",
153 421: "Misdirected Request",
154 422: "Unprocessable Entity",
155 423: "Locked",
156 424: "Failed Dependency",
157 425: "Too Early",
158 426: "Upgrade Required",
159 428: "Precondition Required",
160 429: "Too Many Requests",
161 451: "Unavailable For Legal Reasons",
162 500: "Internal Server Error",
163 501: "Not Implemented",
164 502: "Bad Gateway",
165 503: "Service Unavailable",
166 504: "Gateway Timeout",
167 505: "HTTP Version Not Supported",
168 506: "Variant Also Negotiates",
169 507: "Insufficient Storage",
170 508: "Loop Detected",
171 509: "Bandwidth Limit Exceeded",
172 510: "Not Extended",
173 511: "Network Authentication Required"
174 },
175 statusCode: 200,
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({}, res.opts, negod, opts_)
387 , format = negod.subtype || "json"
388 , outStream = opts.stream || res
389
390 if (!format) {
391 return res.sendStatus(406) // Not Acceptable
392 }
393
394 tmp = opts.cache && opts.filename && opts.cache[opts.filename] || opts.maxAge
395 if (typeof tmp === "number") {
396 // max-age=N is relative to the time of the request
397 resHead["Cache-Control"] = tmp > 0 ? "public, max-age=" + tmp : "no-cache, max-age=0"
398 }
399
400 if (opts.mtime && opts.mtime > Date.parse(reqHead["if-modified-since"])) {
401 return res.sendStatus(304)
402 }
403
404 if (typeof body !== "string") {
405 negod.select = opts && opts.select || res.req.url.split("$select")[1] || ""
406 body = negod.o(body, negod)
407 opts.mimeType = negod.rule
408 }
409
410 resHead["Content-Type"] = opts.mimeType + (
411 opts.charset && opts.mimeType.slice(0, 5) === "text/" ? "; charset=" + opts.charset : ""
412 )
413
414 if (opts.size > 0 || opts.size === 0) {
415 resHead["Content-Length"] = opts.size
416 if (opts.size > opts.rangeSize) {
417 resHead["Accept-Ranges"] = "bytes"
418 resHead["Content-Length"] = opts.size
419
420 if (tmp = reqHead.range && !reqHead["if-range"] && rangeRe.exec(reqHead.range)) {
421 opts.start = range[1] ? +range[1] : range[2] ? opts.size - range[2] - 1 : 0
422 opts.end = range[1] && range[2] ? +range[2] : opts.size - 1
423
424 if (opts.start > opts.end || opts.end >= opts.size) {
425 opts.start = 0
426 opts.end = opts.size - 1
427 } else {
428 opts.statusCode = 206
429 resHead["Content-Length"] = opts.end - opts.start
430 resHead["Content-Range"] = "bytes " + opts.start + "-" + opts.end + "/" + opts.size
431 }
432 }
433 }
434 }
435
436 if (opts.filename) {
437 resHead["Content-Disposition"] = "attachment; filename=" + (
438 typeof opts.filename === "function" ? opts.filename() : opts.filename
439 )
440 }
441
442 negod = opts.compress && opts._encoding(reqHead["accept-encoding"])
443 if (negod.match) {
444 // Server may choose not to compress the body, if:
445 // - data is already compressed (some image format)
446 // - server is overloaded and cannot afford the computational overhead.
447 // Microsoft recommends not to compress if a server uses more than 80% of its computational power.
448 delete resHead["Content-Length"]
449 resHead["Content-Encoding"] = negod.match
450 resHead["Vary"] = "Accept-Encoding"
451 outStream = typeof negod.o === "string" ? require("zlib")[negod.o]() : negod.o()
452 outStream.pipe(res)
453 }
454
455 if (opts.headers) Object.assign(resHead, opts.headers["*"], opts.headers[opts.url || res.req.url])
456 res.writeHead(opts.statusCode, resHead)
457
458 if (res.req.method == "HEAD") {
459 return res.end()
460 }
461
462 if (opts.sendfile) {
463 fs.createReadStream(opts.sendfile, {start: opts.start, end: opts.end}).pipe(outStream)
464 } else {
465 // Line and Paragraph separator needing to be escaped in JavaScript but not in JSON,
466 // escape those so the JSON can be evaluated or directly utilized within JSONP.
467 outStream.end(
468 format === "json" ? body.replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029") :
469 body
470 )
471 }
472}
473
474function sendFile(file, opts_, next_) {
475 var res = this
476 , opts = typeof opts_ === "function" ? (next = opts_) && {} : opts_ || {}
477 , next = typeof next_ === "function" ? next_ : function(code) {
478 res.sendStatus(code)
479 }
480
481 fs.stat(file, function(err, stat) {
482 if (err) return next(404)
483 if (stat.isDirectory()) return next(403)
484
485 opts.mtime = stat.mtime
486 opts.size = stat.size
487 opts.filename = opts.download === true ? file.split("/").pop() : opts.download
488 opts.mimeType = res.opts.mime[ file.split(".").pop() ] || res.opts.mimeType
489 opts.sendfile = file
490
491 res.send(file, opts)
492 })
493}
494
495function sendStatus(code, message) {
496 var res = this
497 res.statusCode = code
498 if (code > 199 && code != 204 && code != 304) {
499 res.setHeader("Content-Type", "text/plain")
500 message = (message || res.opts.status[code] || code) + "\n"
501 res.setHeader("Content-Length", message.length)
502 if ("HEAD" != res.req.method) {
503 res.write(message)
504 }
505 }
506 res.end()
507}
508
509function sendError(res, opts, e) {
510 var message = typeof e === "string" ? e : e.message
511 , map = opts.error && (opts.error[message] || opts.error[e.name]) || {}
512 , error = {
513 id: Math.random().toString(36).slice(2,10),
514 time: res.req.date,
515 code: map.code || e.code || 500,
516 message: map.message || message
517 }
518 res.statusCode = error.code
519 res.statusMessage = opts.status[error.code] || message
520
521 res.send(error)
522
523 opts.log.error(
524 (e.stack || (e.name || "Error") + ": " + error.message).replace(":", ":" + error.id)
525 )
526}
527
528function setLink(url, rel) {
529 var res = this
530 , existing = res.getHeader("link") || []
531
532 if (!Array.isArray(existing)) {
533 existing = [ existing ]
534 }
535
536 existing.push('<' + encodeURI(url) + '>; rel="' + rel + '"')
537
538 res.setHeader("Link", existing)
539}
540
541function listen() {
542 var exiting
543 , server = this
544 , opts = server.opts
545
546 process.on("uncaughtException", function(e) {
547 if (opts.log) opts.log.error(
548 "\nUNCAUGHT EXCEPTION!\n" +
549 (e.stack || (e.name || "Error") + ": " + (e.message || e))
550 )
551 else throw e
552 ;(opts.exit || exit).call(server, 1)
553 })
554
555 process.on("SIGINT", function() {
556 if (exiting) {
557 opts.log.info("\nKilling from SIGINT (got Ctrl-C twice)")
558 return process.exit()
559 }
560 exiting = true
561 opts.log.info("\nGracefully shutting down from SIGINT (Ctrl-C)")
562 ;(opts.exit || exit).call(server, 0)
563 })
564
565 process.on("SIGTERM", function() {
566 opts.log.info("Gracefully shutting down from SIGTERM (kill)")
567 ;(opts.exit || exit).call(server, 0)
568 })
569
570 process.on("SIGHUP", function() {
571 opts.log.info("Reloading configuration from SIGHUP")
572 server.listen(true)
573 server.emit("reload")
574 })
575
576 server.listen = opts.listen || function() {
577 ;["http", "https", "http2"].forEach(createNet)
578 }
579
580 server.listen()
581
582 return server
583
584 function createNet(proto) {
585 var map = opts[proto]
586 , net = server["_" + proto] && !server["_" + proto].close()
587 if (!map || !map.port) return
588 net = server["_" + proto] = (
589 proto == "http" ?
590 require(proto).createServer(map.redirect ? forceHttps : server) :
591 require(proto).createSecureServer(map, map.redirect ? forceHttps : server)
592 )
593 .listen(map.port, map.host || "0.0.0.0", function() {
594 var addr = this.address()
595 opts.log.info("Listening %s at %s:%s", proto, addr.address, addr.port)
596 this.on("close", function() {
597 opts.log.info("Stop listening %s at %s:%s", proto, addr.address, addr.port)
598 })
599 if (map.noDelay) this.on("connection", setNoDelay)
600 })
601 if (map.sessionReuse) {
602 var sessionStore = {}
603 , timeout = map.sessionTimeout || 300
604
605 net
606 .on("newSession", function(id, data, cb) {
607 sessionStore[id] = data
608 cb()
609 })
610 .on("resumeSession", function(id, cb) {
611 cb(null, sessionStore[id] || null)
612 })
613 }
614 }
615
616 function setNoDelay(socket) {
617 if (socket.setNoDelay) socket.setNoDelay(true)
618 }
619
620 function forceHttps(req, res) {
621 // Safari 5 and IE9 drop the original URI's fragment if a HTTP/3xx redirect occurs.
622 // If the Location header on the response specifies a fragment, it is used.
623 // IE10+, Chrome 11+, Firefox 4+, and Opera will all "reattach" the original URI's fragment after following a 3xx redirection.
624 var port = opts.https && opts.https.port || 8443
625 , host = (req.headers.host || "localhost").split(":")[0]
626 , url = "https://" + (port == 443 ? host : host + ":" + port) + req.url
627
628 res.writeHead(301, {"Content-Type": "text/html", "Location": url})
629 res.end('Redirecting to <a href="' + url + '">' + url + '</a>')
630 }
631
632 function exit(code) {
633 var softKill = util.wait(function() {
634 opts.log.info("Everything closed cleanly")
635 process.exit(code)
636 }, 1)
637
638 server.emit("beforeExit", softKill)
639
640 try {
641 if (server._http) server._http.close(softKill.wait()).unref()
642 if (server._https) server._https.close(softKill.wait()).unref()
643 if (server._http2) server._http2.close(softKill.wait()).unref()
644 } catch(e) {}
645
646 setTimeout(function() {
647 opts.log.warn("Kill (timeout)")
648 process.exit(code)
649 }, opts.exitTime).unref()
650
651 softKill()
652 }
653}
654
655
656function setCookie(opts, value) {
657 var res = this
658 , existing = res.getHeader("set-cookie")
659 , cookie = (typeof opts === "string" ? (opts = { name: opts }) : opts).name
660 + ("=" + value).replace(cookieRe, encodeURIComponent)
661 + (opts.maxAge ? "; Expires=" + new Date(opts.maxAge > 0 ? Date.now() + (opts.maxAge*1000) : 0).toUTCString() : "")
662 + (opts.path ? "; Path=" + opts.path : "")
663 + (opts.domain ? "; Domain=" + opts.domain : "")
664 + (opts.secure ? "; Secure" : "")
665 + (opts.httpOnly ? "; HttpOnly" : "")
666 + (opts.sameSite ? "; SameSite=" + opts.sameSite : "")
667
668 if (Array.isArray(existing)) {
669 existing.push(cookie)
670 } else {
671 res.setHeader("Set-Cookie", existing ? [existing, cookie] : cookie)
672 }
673}
674
675function getCookie(opts) {
676 var req = this
677 , name = (typeof opts === "string" ? (opts = { name: opts }) : opts).name
678 , junks = ("; " + req.headers.cookie).split("; " + name + "=")
679
680 if (junks.length > 2) {
681 ;(opts.path || "").split("/").map(function(val, key, arr) {
682 var map = {
683 name: name,
684 maxAge: -1,
685 path: arr.slice(0, key + 1).join("/")
686 }
687 , domain = opts.domain
688 req.res.cookie(map, "")
689 if (domain) {
690 map.domain = domain
691 req.res.cookie(map, "")
692
693 if (domain !== (domain = domain.replace(/^[^.]+(?=\.(?=.+\.))/, "*"))) {
694 map.domain = domain
695 req.res.cookie(map, "")
696 }
697 }
698 })
699 req.opts.log.warn("Cookie fixation detected: %s", req.headers.cookie)
700 } else try {
701 return decodeURIComponent((junks[1] || "").split(";")[0])
702 } catch(e) {
703 req.opts.log.warn("Invalid cookie '%s' in: %s", name, req.headers.cookie)
704 }
705 return ""
706}
707
708