UNPKG

9.87 kBJavaScriptView Raw
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 (!(self.req.socket && self.req.socket._handle)) {
322 self.emit('error', new Error("Connection closed"));
323 return;
324 }
325 if (stat.isDirectory()) return self.redirect(self.path);
326 self.emit('file', path, stat);
327 self.send(path, stat);
328 });
329
330 return res;
331};
332
333/**
334 * Transfer `path`.
335 *
336 * @param {String} path
337 * @api public
338 */
339
340SendStream.prototype.send = function(path, stat){
341 var options = this.options;
342 var len = stat.size;
343 var res = this.res;
344 var req = this.req;
345 var ranges = req.headers.range;
346 var offset = options.start || 0;
347
348 // set header fields
349 this.setHeader(stat);
350
351 // set content-type
352 this.type(path);
353
354 // conditional GET support
355 if (this.isConditionalGET()
356 && this.isCachable()
357 && this.isFresh()) {
358 return this.notModified();
359 }
360
361 // adjust len to start/end options
362 len = Math.max(0, len - offset);
363 if (options.end !== undefined) {
364 var bytes = options.end - offset + 1;
365 if (len > bytes) len = bytes;
366 }
367
368 // Range support
369 if (ranges) {
370 ranges = parseRange(len, ranges);
371
372 // unsatisfiable
373 if (-1 == ranges) {
374 res.setHeader('Content-Range', 'bytes */' + stat.size);
375 return this.error(416);
376 }
377
378 // valid (syntactically invalid ranges are treated as a regular response)
379 if (-2 != ranges) {
380 options.start = offset + ranges[0].start;
381 options.end = offset + ranges[0].end;
382
383 // Content-Range
384 res.statusCode = 206;
385 res.setHeader('Content-Range', 'bytes '
386 + ranges[0].start
387 + '-'
388 + ranges[0].end
389 + '/'
390 + len);
391 len = options.end - options.start + 1;
392 }
393 }
394
395 // content-length
396 res.setHeader('Content-Length', len);
397
398 // HEAD support
399 if ('HEAD' == req.method) return res.end();
400
401 this.stream(path, options);
402};
403
404/**
405 * Stream `path` to the response.
406 *
407 * @param {String} path
408 * @param {Object} options
409 * @api private
410 */
411
412SendStream.prototype.stream = function(path, options){
413 // TODO: this is all lame, refactor meeee
414 var self = this;
415 var res = this.res;
416 var req = this.req;
417
418 // pipe
419 var stream = fs.createReadStream(path, options);
420 this.emit('stream', stream);
421 stream.pipe(res);
422
423 // socket closed, done with the fd
424 req.on('close', stream.destroy.bind(stream));
425
426 // error handling code-smell
427 stream.on('error', function(err){
428 // no hope in responding
429 if (res._header) {
430 console.error(err.stack);
431 req.destroy();
432 return;
433 }
434
435 // 500
436 err.status = 500;
437 self.emit('error', err);
438 });
439
440 // end
441 stream.on('end', function(){
442 self.emit('end');
443 });
444};
445
446/**
447 * Set content-type based on `path`
448 * if it hasn't been explicitly set.
449 *
450 * @param {String} path
451 * @api private
452 */
453
454SendStream.prototype.type = function(path){
455 var res = this.res;
456 if (res.getHeader('Content-Type')) return;
457 var type = mime.lookup(path);
458 var charset = mime.charsets.lookup(type);
459 debug('content-type %s', type);
460 res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ''));
461};
462
463/**
464 * Set reaponse header fields, most
465 * fields may be pre-defined.
466 *
467 * @param {Object} stat
468 * @api private
469 */
470
471SendStream.prototype.setHeader = function(stat){
472 var res = this.res;
473 if (!res.getHeader('Accept-Ranges')) res.setHeader('Accept-Ranges', 'bytes');
474 if (!res.getHeader('ETag')) res.setHeader('ETag', utils.etag(stat));
475 if (!res.getHeader('Date')) res.setHeader('Date', new Date().toUTCString());
476 if (!res.getHeader('Cache-Control')) res.setHeader('Cache-Control', 'public, max-age=' + (this._maxage / 1000));
477 if (!res.getHeader('Last-Modified')) res.setHeader('Last-Modified', stat.mtime.toUTCString());
478};