UNPKG

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