raspberryGPIOhtmlserver/node_modules/connect/node_modules/send/lib/send.js
node_modules/connect/node_modules/send/lib/send.jsRaw
1
2/**
3 * Module dependencies.
4 */
5
6var debug = require('debug')('send')
7 , parseRange = require('range-parser')
8 , Stream = require('stream')
9 , mime = require('mime')
10 , fresh = require('fresh')
11 , path = require('path')
12 , http = require('http')
13 , fs = require('fs')
14 , basename = path.basename
15 , normalize = path.normalize
16 , join = path.join
17 , utils = require('./utils');
18
19/**
20 * Expose `send`.
21 */
22
23exports = module.exports = send;
24
25/**
26 * Expose mime module.
27 */
28
29exports.mime = mime;
30
31/**
32 * Return a `SendStream` for `req` and `path`.
33 *
34 * @param {Request} req
35 * @param {String} path
36 * @param {Object} options
37 * @return {SendStream}
38 * @api public
39 */
40
41function send(req, path, options) {
42 return new SendStream(req, path, options);
43}
44
45/**
46 * Initialize a `SendStream` with the given `path`.
47 *
48 * Events:
49 *
50 * - `error` an error occurred
51 * - `stream` file streaming has started
52 * - `end` streaming has completed
53 * - `directory` a directory was requested
54 *
55 * @param {Request} req
56 * @param {String} path
57 * @param {Object} options
58 * @api private
59 */
60
61function SendStream(req, path, options) {
62 var self = this;
63 this.req = req;
64 this.path = path;
65 this.options = options || {};
66 this.maxage(0);
67 this.hidden(false);
68 this.index('index.html');
69}
70
71/**
72 * Inherits from `Stream.prototype`.
73 */
74
75SendStream.prototype.__proto__ = Stream.prototype;
76
77/**
78 * Enable or disable "hidden" (dot) files.
79 *
80 * @param {Boolean} path
81 * @return {SendStream}
82 * @api public
83 */
84
85SendStream.prototype.hidden = function(val){
86 debug('hidden %s', val);
87 this._hidden = val;
88 return this;
89};
90
91/**
92 * Set index `path`, set to a falsy
93 * value to disable index support.
94 *
95 * @param {String|Boolean} path
96 * @return {SendStream}
97 * @api public
98 */
99
100SendStream.prototype.index = function(path){
101 debug('index %s', path);
102 this._index = path;
103 return this;
104};
105
106/**
107 * Set root `path`.
108 *
109 * @param {String} path
110 * @return {SendStream}
111 * @api public
112 */
113
114SendStream.prototype.root =
115SendStream.prototype.from = function(path){
116 this._root = normalize(path);
117 return this;
118};
119
120/**
121 * Set max-age to `ms`.
122 *
123 * @param {Number} ms
124 * @return {SendStream}
125 * @api public
126 */
127
128SendStream.prototype.maxage = function(ms){
129 if (Infinity == ms) ms = 60 * 60 * 24 * 365 * 1000;
130 debug('max-age %d', ms);
131 this._maxage = ms;
132 return this;
133};
134
135/**
136 * Emit error with `status`.
137 *
138 * @param {Number} status
139 * @api private
140 */
141
142SendStream.prototype.error = function(status, err){
143 var res = this.res;
144 var msg = http.STATUS_CODES[status];
145 err = err || new Error(msg);
146 err.status = status;
147 if (this.listeners('error').length) return this.emit('error', err);
148 res.statusCode = err.status;
149 res.end(msg);
150};
151
152/**
153 * Check if the pathname is potentially malicious.
154 *
155 * @return {Boolean}
156 * @api private
157 */
158
159SendStream.prototype.isMalicious = function(){
160 return !this._root && ~this.path.indexOf('..');
161};
162
163/**
164 * Check if the pathname ends with "/".
165 *
166 * @return {Boolean}
167 * @api private
168 */
169
170SendStream.prototype.hasTrailingSlash = function(){
171 return '/' == this.path[this.path.length - 1];
172};
173
174/**
175 * Check if the basename leads with ".".
176 *
177 * @return {Boolean}
178 * @api private
179 */
180
181SendStream.prototype.hasLeadingDot = function(){
182 return '.' == basename(this.path)[0];
183};
184
185/**
186 * Check if this is a conditional GET request.
187 *
188 * @return {Boolean}
189 * @api private
190 */
191
192SendStream.prototype.isConditionalGET = function(){
193 return this.req.headers['if-none-match']
194 || this.req.headers['if-modified-since'];
195};
196
197/**
198 * Strip content-* header fields.
199 *
200 * @api private
201 */
202
203SendStream.prototype.removeContentHeaderFields = function(){
204 var res = this.res;
205 Object.keys(res._headers).forEach(function(field){
206 if (0 == field.indexOf('content')) {
207 res.removeHeader(field);
208 }
209 });
210};
211
212/**
213 * Respond with 304 not modified.
214 *
215 * @api private
216 */
217
218SendStream.prototype.notModified = function(){
219 var res = this.res;
220 debug('not modified');
221 this.removeContentHeaderFields();
222 res.statusCode = 304;
223 res.end();
224};
225
226/**
227 * Check if the request is cacheable, aka
228 * responded with 2xx or 304 (see RFC 2616 section 14.2{5,6}).
229 *
230 * @return {Boolean}
231 * @api private
232 */
233
234SendStream.prototype.isCachable = function(){
235 var res = this.res;
236 return (res.statusCode >= 200 && res.statusCode < 300) || 304 == res.statusCode;
237};
238
239/**
240 * Handle stat() error.
241 *
242 * @param {Error} err
243 * @api private
244 */
245
246SendStream.prototype.onStatError = function(err){
247 var notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR'];
248 if (~notfound.indexOf(err.code)) return this.error(404, err);
249 this.error(500, err);
250};
251
252/**
253 * Check if the cache is fresh.
254 *
255 * @return {Boolean}
256 * @api private
257 */
258
259SendStream.prototype.isFresh = function(){
260 return fresh(this.req.headers, this.res._headers);
261};
262
263/**
264 * Redirect to `path`.
265 *
266 * @param {String} path
267 * @api private
268 */
269
270SendStream.prototype.redirect = function(path){
271 if (this.listeners('directory').length) return this.emit('directory');
272 var res = this.res;
273 path += '/';
274 res.statusCode = 301;
275 res.setHeader('Location', path);
276 res.end('Redirecting to ' + utils.escape(path));
277};
278
279/**
280 * Pipe to `res.
281 *
282 * @param {Stream} res
283 * @return {Stream} res
284 * @api public
285 */
286
287SendStream.prototype.pipe = function(res){
288 var self = this
289 , args = arguments
290 , path = this.path
291 , root = this._root;
292
293 // references
294 this.res = res;
295
296 // invalid request uri
297 path = utils.decode(path);
298 if (-1 == path) return this.error(400);
299
300 // null byte(s)
301 if (~path.indexOf('\0')) return this.error(400);
302
303 // join / normalize from optional root dir
304 if (root) path = normalize(join(this._root, path));
305
306 // ".." is malicious without "root"
307 if (this.isMalicious()) return this.error(403);
308
309 // malicious path
310 if (root && 0 != path.indexOf(root)) return this.error(403);
311
312 // hidden file support
313 if (!this._hidden && this.hasLeadingDot()) return this.error(404);
314
315 // index file support
316 if (this._index && this.hasTrailingSlash()) path += this._index;
317
318 debug('stat "%s"', path);
319 fs.stat(path, function(err, stat){
320 if (err) return self.onStatError(err);
321 if (stat.isDirectory()) return self.redirect(self.path);
322 self.emit('file', path, stat);
323 self.send(path, stat);
324 });
325
326 return res;
327};
328
329/**
330 * Transfer `path`.
331 *
332 * @param {String} path
333 * @api public
334 */
335
336SendStream.prototype.send = function(path, stat){
337 var options = this.options;
338 var len = stat.size;
339 var res = this.res;
340 var req = this.req;
341 var ranges = req.headers.range;
342 var offset = options.start || 0;
343
344 // set header fields
345 this.setHeader(stat);
346
347 // set content-type
348 this.type(path);
349
350 // conditional GET support
351 if (this.isConditionalGET()
352 && this.isCachable()
353 && this.isFresh()) {
354 return this.notModified();
355 }
356
357 // adjust len to start/end options
358 len = Math.max(0, len - offset);
359 if (options.end !== undefined) {
360 var bytes = options.end - offset + 1;
361 if (len > bytes) len = bytes;
362 }
363
364 // Range support
365 if (ranges) {
366 ranges = parseRange(len, ranges);
367
368 // unsatisfiable
369 if (-1 == ranges) {
370 res.setHeader('Content-Range', 'bytes */' + stat.size);
371 return this.error(416);
372 }
373
374 // valid (syntactically invalid ranges are treated as a regular response)
375 if (-2 != ranges) {
376 options.start = offset + ranges[0].start;
377 options.end = offset + ranges[0].end;
378
379 // Content-Range
380 res.statusCode = 206;
381 res.setHeader('Content-Range', 'bytes '
382 + ranges[0].start
383 + '-'
384 + ranges[0].end
385 + '/'
386 + len);
387 len = options.end - options.start + 1;
388 }
389 }
390
391 // content-length
392 res.setHeader('Content-Length', len);
393
394 // HEAD support
395 if ('HEAD' == req.method) return res.end();
396
397 this.stream(path, options);
398};
399
400/**
401 * Stream `path` to the response.
402 *
403 * @param {String} path
404 * @param {Object} options
405 * @api private
406 */
407
408SendStream.prototype.stream = function(path, options){
409 // TODO: this is all lame, refactor meeee
410 var self = this;
411 var res = this.res;
412 var req = this.req;
413
414 // pipe
415 var stream = fs.createReadStream(path, options);
416 this.emit('stream', stream);
417 stream.pipe(res);
418
419 // socket closed, done with the fd
420 req.on('close', stream.destroy.bind(stream));
421
422 // error handling code-smell
423 stream.on('error', function(err){
424 // no hope in responding
425 if (res._header) {
426 console.error(err.stack);
427 req.destroy();
428 return;
429 }
430
431 // 500
432 err.status = 500;
433 self.emit('error', err);
434 });
435
436 // end
437 stream.on('end', function(){
438 self.emit('end');
439 });
440};
441
442/**
443 * Set content-type based on `path`
444 * if it hasn't been explicitly set.
445 *
446 * @param {String} path
447 * @api private
448 */
449
450SendStream.prototype.type = function(path){
451 var res = this.res;
452 if (res.getHeader('Content-Type')) return;
453 var type = mime.lookup(path);
454 var charset = mime.charsets.lookup(type);
455 debug('content-type %s', type);
456 res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ''));
457};
458
459/**
460 * Set reaponse header fields, most
461 * fields may be pre-defined.
462 *
463 * @param {Object} stat
464 * @api private
465 */
466
467SendStream.prototype.setHeader = function(stat){
468 var res = this.res;
469 if (!res.getHeader('Accept-Ranges')) res.setHeader('Accept-Ranges', 'bytes');
470 if (!res.getHeader('ETag')) res.setHeader('ETag', utils.etag(stat));
471 if (!res.getHeader('Date')) res.setHeader('Date', new Date().toUTCString());
472 if (!res.getHeader('Cache-Control')) res.setHeader('Cache-Control', 'public, max-age=' + (this._maxage / 1000));
473 if (!res.getHeader('Last-Modified')) res.setHeader('Last-Modified', stat.mtime.toUTCString());
474};