UNPKG

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