UNPKG

13.4 kBJavaScriptView Raw
1
2var statusCodes = require("http").STATUS_CODES
3, fs = require("fs")
4, zlib = require("zlib")
5, accept = require("./accept.js").accept
6, cookie = require("./cookie.js")
7, getContent = require("./content.js")
8, mime = require("./mime.js")
9, csv = require("../lib/csv.js")
10, util = require("../lib/util.js")
11, events = require("../lib/events")
12, empty = {}
13, defaultOptions = {
14 maxURILength: 2000,
15 maxBodySize: 1e6,
16 memBodySize: 1e6,
17 maxFields: 1000,
18 maxFiles: 1000,
19 maxFieldSize: 1000,
20 maxFileSize: Infinity,
21 negotiateAccept: accept({
22 'application/json;space=;filename=;select=': function(data, negod) {
23 return JSON.stringify(
24 data,
25 negod.select ? negod.select.split(",") : null,
26 +negod.space || negod.space
27 )
28 },
29 'text/csv;headers=no;delimiter=",";NULL=;br="\r\n";fields=;filename=;select=': csv.encode,
30 'application/vnd.ms-excel;headers=no;NULL=;sheet=Sheet1;fields=;filename=file.xls;select=': function(data, negod) {
31 negod.prefix = '<?xml version="1.0" encoding="UTF-8"?>\n<?mso-application progid="Excel.Sheet"?>\n<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet" xmlns:html="https://www.w3.org/TR/html401/">\n<Worksheet ss:Name="' + negod.sheet + '"><Table><Row><Cell><Data ss:Type="String">'
32 negod.delimiter = '</Data></Cell><Cell><Data ss:Type="String">'
33 negod.br = '</Data></Cell></Row><Row><Cell><Data ss:Type="String">'
34 negod.postfix = '</Data></Cell></Row></Table></Worksheet></Workbook>'
35 negod.re = /</
36 negod.esc = /</g
37 negod.escVal = "&lt;"
38 return csv.encode(data, negod)
39 },
40 'application/sql;NULL=NULL;table=table;fields=;filename=;select=': function(data, negod) {
41 negod.re = /\D/
42 negod.br = "),\n("
43 negod.prefix = "INSERT INTO " +
44 negod.table + (negod.fields ? " (" + negod.fields + ")" : "") + " VALUES ("
45 negod.postfix = ");"
46 return csv.encode(data, negod)
47 }
48 }),
49 errors: {
50 // new Error([message[, fileName[, lineNumber]]])
51 // - EvalError - The EvalError object indicates an error regarding the global eval() function.
52 // This exception is not thrown by JavaScript anymore,
53 // however the EvalError object remains for compatibility.
54 // - RangeError - a value is not in the set or range of allowed values.
55 // - ReferenceError - a non-existent variable is referenced
56 // - SyntaxError - trying to interpret syntactically invalid code
57 // - TypeError - a value is not of the expected type
58 // - URIError - a global URI handling function was used in a wrong way
59 "URIError": { code: 400 }
60 }
61}
62
63require("../lib/fn")
64require("../lib/timing")
65
66Object.keys(statusCodes).forEach(function(code) {
67 if (code >= 400) {
68 this[statusCodes[code]] = { code: +code }
69 }
70}, defaultOptions.errors)
71
72module.exports = function createApp(_options) {
73 var uses = []
74 , options = app.options = {}
75
76 util.deepAssign(options, defaultOptions, _options)
77 events.asEmitter(app)
78
79 app.use = function appUse(method, path) {
80 var fn
81 , arr = Array.from(arguments)
82 , len = arr.length
83 , i = 2
84 if (typeof method === "function") {
85 method = path = null
86 i = 0
87 } else if (typeof path === "function") {
88 path = method
89 method = null
90 i = 1
91 }
92 for (; i < len; ) {
93 if (typeof arr[i] !== "function") throw Error("Not a function")
94 uses.push(method, path, arr[i++])
95 }
96 return app
97 }
98
99 app.addMethod = addMethod
100 app.initRequest = initRequest
101 app.readBody = readBody
102 app.static = require("./static.js")
103 app.listen = require("./listen.js")
104
105 addMethod("del", "DELETE")
106 addMethod("get", "GET")
107 addMethod("patch", "PATCH")
108 addMethod("post", "POST")
109 addMethod("put", "PUT")
110
111 return app
112
113 function app(req, res, _next) {
114 var oldPath, oldUrl
115 , tryCatch = true
116 , usePos = 0
117
118 next()
119
120 function next(err) {
121 if (err) {
122 return sendError(res, options, err)
123 }
124 var method = uses[usePos]
125 , path = uses[usePos + 1]
126 , pos = usePos += 3
127
128 if (
129 method && method !== req.method ||
130 path && path !== req.url.slice(0, path.length)
131 ) {
132 next()
133 } else if (uses[pos - 1] === void 0) {
134 if (typeof _next === "function") {
135 _next()
136 } else {
137 res.sendStatus(404)
138 }
139 } else {
140 method = uses[pos - 1]
141 if (path) {
142 oldPath = req.baseUrl
143 oldUrl = req.url
144 req.baseUrl = path
145 req.url = req.url.slice(path.length) || "/"
146 }
147 if (tryCatch === true) {
148 tryCatch = false
149 try {
150 method.call(app, req, res, path ? nextPath : next, options)
151 } catch(e) {
152 return sendError(res, options, e)
153 }
154 } else {
155 method.call(app, req, res, path ? nextPath : next, options)
156 }
157 if (pos === usePos) {
158 tryCatch = true
159 }
160 }
161 }
162 function nextPath(e) {
163 req.baseUrl = oldPath
164 req.url = oldUrl
165 next(e)
166 }
167 }
168
169 function addMethod(method, methodString) {
170 app[method] = function() {
171 var arr = uses.slice.call(arguments)
172 if (typeof arr[0] === "function") {
173 arr.unshift(null)
174 }
175 arr.unshift(methodString)
176 return app.use.apply(app, arr)
177 }
178 }
179}
180
181
182
183function initRequest(req, res, next, opts) {
184 var forwarded = req.headers[opts.ipHeader || "x-forwarded-for"]
185 req.ip = forwarded ? forwarded.split(/[\s,]+/)[0] : req.connection.remoteAddress
186 req.res = res
187 res.req = req
188 req.date = new Date()
189 res.send = send
190 res.sendStatus = sendStatus
191 res.opts = req.opts = opts
192
193 // IE8-10 accept 2083 chars in URL
194 // Sitemaps protocol has a limit of 2048 characters in a URL
195 // Google SERP tool wouldn't cope with URLs longer than 1855 chars
196 if (req.url.length > opts.maxURILength) {
197 return sendError(res, opts, "URI Too Long")
198 // throw "URI Too Long"
199 }
200
201 req.originalUrl = req.url
202 req.cookie = cookie.get
203 req.content = getContent
204
205 res.cookie = cookie.set
206 res.link = setLink
207 res.sendFile = sendFile
208
209 next()
210}
211
212
213function send(body, _opts) {
214 var res = this
215 , head = res.req.headers
216 , negod = res.opts.negotiateAccept(head.accept || head["content-type"] || "*")
217 , format = negod.subtype || "json"
218
219 // Safari 5 and IE9 drop the original URI's fragment if a HTTP/3xx redirect occurs.
220 // If the Location header on the response specifies a fragment, it is used.
221 // IE10+, Chrome 11+, Firefox 4+, and Opera will all "reattach" the original URI's fragment after following a 3xx redirection.
222
223 if (!format) {
224 return res.sendStatus(406)
225 }
226
227 if (typeof body !== "string") {
228 if (negod.filename) {
229 res.setHeader("Content-Disposition", "attachment; filename=" + negod.filename)
230 }
231 negod.select = _opts && _opts.select || res.req.url.split("$select")[1] || ""
232 body = negod.o(body, negod)
233 }
234
235 res.setHeader("Content-Type", mime[format])
236 // Content-Type: application/my-media-type+json; profile=http://example.com/my-hyper-schema#
237 // res.setHeader("Content-Length", body.length)
238
239 // Line and Paragraph separator needing to be escaped in JavaScript but not in JSON,
240 // escape those so the JSON can be evaluated or directly utilized within JSONP.
241 res.end(
242 format === "json" ? body.replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029") :
243 body
244 )
245}
246
247var errIsDir = {
248 name: "EISDIR",
249 code: 403,
250 message: "Is Directory"
251}
252, errBadRange = {
253 name: "ERANGE",
254 code: 416,
255 message: "Range Not Satisfiable"
256}
257, flvMagic = "FLV" + String.fromCharCode(1,5,0,0,0,9,0,0,0,9)
258, ieRe = /\bMSIE (\d+)/
259
260function sendFile(file, _opts, next) {
261 var res = this
262 , opts = _opts || {}
263
264 if (typeof opts === "function") {
265 next = opts
266 opts = {}
267 }
268
269 fs.stat(file, sendFile)
270
271 function sendFile(err, stat) {
272 if (err) {
273 return next && next(err)
274 }
275
276 if (stat.isDirectory()) {
277 return next && next(errIsDir)
278 }
279
280 var tmp
281 , headers = {}
282 , reqMtime = Date.parse(res.req.headers["if-modified-since"])
283
284 if (reqMtime && reqMtime >= stat.mtime) {
285 return res.sendStatus(304)
286 }
287
288 /**
289 , etag = [stat.ino, stat.size, stat.mtime.getTime()].join("-")
290
291 if ( req.headers["if-none-match"] === etag || (reqMtime && reqMtime >= stat.mtime)) {
292 return sendStatus(res, 304)
293 }
294 // If the server finds that its version of the resource is different than that demanded by the client,
295 // it will return a HTTP/412 Precondition Failed response.
296 // If the client sent its ETag using an If-Range header instead of the If-Match,
297 // the server would instead return the full response body if the client’s ETag didn’t match.
298 // Using If-Range saves one network request in the event that the client needs the complete file.
299 headers["ETag"] = etag
300 /*/
301 //*/
302
303 if (typeof opts.maxAge === "number") {
304 tmp = opts.cacheControl && opts.cacheControl[file]
305 if (typeof tmp !== "number") tmp = opts.maxAge
306
307 // max-age=N is relative to the time of the request
308 headers["Cache-Control"] = tmp > 0 ? "public, max-age=" + tmp : "no-cache"
309
310 // Last-Modified header is used only when
311 // Cache-control: max-age=N and Expires header is not present:
312 // max-age = (Date - Last-modified) / 10
313 // headers["Last-Modified"] = stat.mtime.toUTCString()
314 }
315
316 if (opts.download) {
317 headers["Content-Disposition"] = "attachment; filename=" + (
318 opts.download === true ?
319 file.split("/").pop() :
320 opts.download
321 )
322 }
323
324 /*
325 // http://tools.ietf.org/html/rfc3803 Content Duration MIME Header
326 headers["Content-Duration"] = 30
327 Content-Disposition: Attachment; filename=example.html
328 */
329
330
331 // https://tools.ietf.org/html/rfc7233 HTTP/1.1 Range Requests
332
333 if (stat.size > opts.rangeSize) {
334 headers["Accept-Ranges"] = "bytes"
335 }
336
337 var info = {
338 code: 200,
339 start: 0,
340 end: stat.size,
341 size: stat.size
342 }
343 , range = res.req.headers.range
344
345 if (range = range && range.match(/bytes=(\d+)-(\d*)/)) {
346 // If-Range
347 // If the entity tag does not match,
348 // then the server SHOULD return the entire entity using a 200 (OK) response.
349 info.start = +range[1]
350 info.end = +range[2]
351
352 if (info.start > info.end || info.end > info.size) {
353 res.setHeader("Content-Range", "bytes */" + info.size)
354 return next && next(errBadRange)
355 }
356 info.code = 206
357 info.size = info.end - info.start + 1
358 headers["Content-Range"] = "bytes " + info.start + "-" + info.end + "/" + info.size
359 }
360
361 headers["Content-Type"] = mime[ file.split(".").pop() ] || mime["_default"]
362 if (headers["Content-Type"].slice(0, 5) == "text/") {
363 headers["Content-Type"] += "; charset=UTF-8"
364 }
365
366 headers["Content-Length"] = info.size
367
368 if (headers["Content-Type"] == "text/html" && (tmp = ieRe.exec(req.headers["user-agent"]))) {
369 // Chrome Frame works in IE6-9, and was retired on February 25, 2014
370 headers["X-UA-Compatible"] = tmp[1] < 10 ? "IE=edge,chrome=1" : "IE=edge"
371 }
372
373
374 if (opts.headers) {
375 Object.assign(headers, opts.headers["*"])
376 Object.assign(headers, opts.headers[file])
377 }
378
379 res.writeHead(info.code, headers)
380 if (res.req.method == "HEAD") {
381 return res.end()
382 }
383
384 /*
385 * if (cache && qzip) headers["Vary"] = "Accept-Encoding,User-Agent"
386 */
387
388 // Flash videos seem to need this on the front,
389 // even if they start part way through. (JW Player does anyway)
390 if (info.start > 0 && info.mime === "video/x-flv") {
391 res.write(flvMagic)
392 }
393
394 /*
395 <meta http-equiv="x-dns-prefetch-control" content="off">
396
397 To be sure you're using the latest rendering mode for IE.
398 The best practice is an X-UA-Compatible HTTP Header.
399 <meta http-equiv="X-UA-Compatible" content="IE=edge">
400
401 X-Frame-Options: deny
402 Content-Security-Policy: script-src 'self'; frame-src 'none'
403 Strict-Transport-Security: max-age=778000
404 */
405
406 fs.createReadStream(file, {
407 flags: "r",
408 start: info.start,
409 end: info.end
410 }).pipe(res)
411
412 /*/
413 if ( (""+req.headers["accept-encoding"]).indexOf("gzip") > -1) {
414 // Only send a Vary: Accept-Encoding header when you have compressed the content (e.g. Content-Encoding: gzip).
415 res.useChunkedEncodingByDefault = false
416 res.setHeader("Content-Encoding", "gzip")
417 fs.createReadStream(file).pipe(zlib.createGzip()).pipe(res)
418 } else {
419 fs.createReadStream(file).pipe(res)
420 }
421 //*/
422 }
423}
424
425function sendStatus(code, message) {
426 var res = this
427 res.statusCode = code
428 if (code > 199 && code != 204 && code != 304) {
429 res.setHeader("Content-Type", "text/plain")
430 message = (message || statusCodes[code] || code) + "\n"
431 res.setHeader("Content-Length", message.length)
432 if ("HEAD" != res.req.method) {
433 res.write(message)
434 }
435 }
436 res.end()
437}
438
439function sendError(res, opts, e) {
440 var message = typeof e === "string" ? e : e.message
441 , map = opts.errors && (opts.errors[message] || opts.errors[e.name]) || empty
442 , error = {
443 id: Math.random().toString(36).slice(2,10),
444 time: res.req.date,
445 code: map.code || e.code || 500,
446 message: map.message || message
447 }
448 res.statusCode = error.code
449 res.statusMessage = statusCodes[error.code] || message
450
451 res.send(error)
452
453 ;(opts.errorLog || console.error)(
454 (e.stack || (e.name || "Error") + ": " + error.message).replace(":", ":" + error.id)
455 )
456}
457
458function readBody(req, res, next, opts) {
459 if (req.method === "POST" || req.method === "PUT" || req.method === "PATCH") {
460 req.content(next)
461 } else {
462 next()
463 }
464}
465
466function setLink(url, rel) {
467 var res = this
468 , existing = res.getHeader("link") || []
469
470 if (!Array.isArray(existing)) {
471 existing = [ existing ]
472 }
473
474 existing.push('<' + encodeURI(url) + '>; rel="' + rel + '"')
475
476 res.setHeader("Link", existing)
477}
478
479