Inital Commit
[oweals/finalsclub.git] / node_modules / connect / lib / middleware / static.js
1
2 /*!
3  * Connect - staticProvider
4  * Copyright(c) 2010 Sencha Inc.
5  * Copyright(c) 2011 TJ Holowaychuk
6  * MIT Licensed
7  */
8
9 /**
10  * Module dependencies.
11  */
12
13 var fs = require('fs')
14   , path = require('path')
15   , join = path.join
16   , basename = path.basename
17   , normalize = path.normalize
18   , utils = require('../utils')
19   , Buffer = require('buffer').Buffer
20   , parse = require('url').parse
21   , mime = require('mime');
22
23 /**
24  * Static file server with the given `root` path.
25  *
26  * Examples:
27  *
28  *     var oneDay = 86400000;
29  *
30  *     connect(
31  *       connect.static(__dirname + '/public')
32  *     ).listen(3000);
33  *
34  *     connect(
35  *       connect.static(__dirname + '/public', { maxAge: oneDay })
36  *     ).listen(3000);
37  *
38  * Options:
39  *
40  *    - `maxAge`   Browser cache maxAge in milliseconds. defaults to 0
41  *    - `hidden`   Allow transfer of hidden files. defaults to false
42  *
43  * @param {String} root
44  * @param {Object} options
45  * @return {Function}
46  * @api public
47  */
48
49 exports = module.exports = function static(root, options){
50   options = options || {};
51
52   // root required
53   if (!root) throw new Error('static() root path required');
54   options.root = root;
55
56   return function static(req, res, next) {
57     options.path = req.url;
58     options.getOnly = true;
59     send(req, res, next, options);
60   };
61 };
62
63 /**
64  * Expose mime module.
65  */
66
67 exports.mime = mime;
68
69 /**
70  * Respond with 416  "Requested Range Not Satisfiable"
71  *
72  * @param {ServerResponse} res
73  * @api private
74  */
75
76 function invalidRange(res) {
77   var body = 'Requested Range Not Satisfiable';
78   res.setHeader('Content-Type', 'text/plain');
79   res.setHeader('Content-Length', body.length);
80   res.statusCode = 416;
81   res.end(body);
82 }
83
84 /**
85  * Attempt to tranfer the requseted file to `res`.
86  *
87  * @param {ServerRequest}
88  * @param {ServerResponse}
89  * @param {Function} next
90  * @param {Object} options
91  * @api private
92  */
93
94 var send = exports.send = function(req, res, next, options){
95   options = options || {};
96   if (!options.path) throw new Error('path required');
97
98   // setup
99   var maxAge = options.maxAge || 0
100     , ranges = req.headers.range
101     , head = 'HEAD' == req.method
102     , get = 'GET' == req.method
103     , root = options.root ? normalize(options.root) : null
104     , getOnly = options.getOnly
105     , fn = options.callback
106     , hidden = options.hidden
107     , done;
108
109   // replace next() with callback when available
110   if (fn) next = fn;
111
112   // ignore non-GET requests
113   if (getOnly && !get && !head) return next();
114
115   // parse url
116   var url = parse(options.path)
117     , path = decodeURIComponent(url.pathname)
118     , type;
119
120   // null byte(s)
121   if (~path.indexOf('\0')) return utils.badRequest(res);
122
123   // when root is not given, consider .. malicious
124   if (!root && ~path.indexOf('..')) return utils.forbidden(res);
125
126   // join / normalize from optional root dir
127   path = normalize(join(root, path));
128
129   // malicious path
130   if (root && 0 != path.indexOf(root)) return fn
131     ? fn(new Error('Forbidden'))
132     : utils.forbidden(res);
133
134   // index.html support
135   if ('/' == path[path.length - 1]) path += 'index.html';
136
137   // "hidden" file
138   if (!hidden && '.' == basename(path)[0]) return next();
139
140   fs.stat(path, function(err, stat){
141     // mime type
142     type = mime.lookup(path);
143
144     // ignore ENOENT
145     if (err) {
146       if (fn) return fn(err);
147       return 'ENOENT' == err.code
148         ? next()
149         : next(err);
150     // redirect directory in case index.html is present
151     } else if (stat.isDirectory()) {
152       res.statusCode = 301;
153       res.setHeader('Location', url.pathname + '/');
154       res.end('Redirecting to ' + url.pathname + '/');
155       return;
156     }
157
158     var opts = {};
159
160     // we have a Range request
161     if (ranges) {
162       ranges = utils.parseRange(stat.size, ranges);
163       // valid
164       if (ranges) {
165         // TODO: stream options
166         // TODO: multiple support
167         opts.start = ranges[0].start;
168         opts.end = ranges[0].end;
169         res.statusCode = 206;
170         res.setHeader('Content-Range', 'bytes '
171           + opts.start
172           + '-'
173           + opts.end
174           + '/'
175           + stat.size);
176       // invalid
177       } else {
178         return fn
179           ? fn(new Error('Requested Range Not Satisfiable'))
180           : invalidRange(res);
181       }
182     // stream the entire file
183     } else {
184       res.setHeader('Content-Length', stat.size);
185       if (!res.getHeader('Cache-Control')) res.setHeader('Cache-Control', 'public, max-age=' + (maxAge / 1000));
186       if (!res.getHeader('Last-Modified')) res.setHeader('Last-Modified', stat.mtime.toUTCString());
187       if (!res.getHeader('ETag')) res.setHeader('ETag', utils.etag(stat));
188
189       // conditional GET support
190       if (utils.conditionalGET(req)) {
191         if (!utils.modified(req, res)) {
192           return utils.notModified(res);
193         }
194       }
195     }
196
197     // header fields
198     if (!res.getHeader('content-type')) {
199       var charset = mime.charsets.lookup(type);
200       res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ''));
201     }
202     res.setHeader('Accept-Ranges', 'bytes');
203
204     // transfer
205     if (head) return res.end();
206
207     // stream
208     var stream = fs.createReadStream(path, opts);
209     stream.pipe(res);
210
211     // callback
212     if (fn) {
213       function callback(err) { done || fn(err); done = true }
214       req.on('close', callback);
215       req.socket.on('error', callback);
216       stream.on('end', callback);
217     }
218   });
219 };