1 |
|
2 | var 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 = "<"
|
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 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 | "URIError": { code: 400 }
|
60 | }
|
61 | }
|
62 |
|
63 | require("../lib/fn")
|
64 | require("../lib/timing")
|
65 |
|
66 | Object.keys(statusCodes).forEach(function(code) {
|
67 | if (code >= 400) {
|
68 | this[statusCodes[code]] = { code: +code }
|
69 | }
|
70 | }, defaultOptions.errors)
|
71 |
|
72 | module.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 |
|
183 | function 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 |
|
194 |
|
195 |
|
196 | if (req.url.length > opts.maxURILength) {
|
197 | return sendError(res, opts, "URI Too Long")
|
198 |
|
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 |
|
213 | function 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 |
|
220 |
|
221 |
|
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 |
|
237 |
|
238 |
|
239 |
|
240 |
|
241 | res.end(
|
242 | format === "json" ? body.replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029") :
|
243 | body
|
244 | )
|
245 | }
|
246 |
|
247 | var 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 |
|
260 | function 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 |
|
290 |
|
291 |
|
292 |
|
293 |
|
294 |
|
295 |
|
296 |
|
297 |
|
298 |
|
299 |
|
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 |
|
308 | headers["Cache-Control"] = tmp > 0 ? "public, max-age=" + tmp : "no-cache"
|
309 |
|
310 |
|
311 |
|
312 |
|
313 |
|
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 |
|
326 |
|
327 |
|
328 |
|
329 |
|
330 |
|
331 |
|
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 |
|
347 |
|
348 |
|
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 |
|
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 |
|
386 |
|
387 |
|
388 |
|
389 |
|
390 | if (info.start > 0 && info.mime === "video/x-flv") {
|
391 | res.write(flvMagic)
|
392 | }
|
393 |
|
394 | |
395 |
|
396 |
|
397 |
|
398 |
|
399 |
|
400 |
|
401 |
|
402 |
|
403 |
|
404 |
|
405 |
|
406 | fs.createReadStream(file, {
|
407 | flags: "r",
|
408 | start: info.start,
|
409 | end: info.end
|
410 | }).pipe(res)
|
411 |
|
412 | |
413 |
|
414 |
|
415 |
|
416 |
|
417 |
|
418 |
|
419 |
|
420 |
|
421 |
|
422 | }
|
423 | }
|
424 |
|
425 | function 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 |
|
439 | function 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 |
|
458 | function 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 |
|
466 | function 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 |
|