socketioMEANseed/www/node_modules/ecstatic/lib/ecstatic.js
www/node_modules/ecstatic/lib/ecstatic.jsRaw
1#! /usr/bin/env node
2
3var path = require('path'),
4 fs = require('fs'),
5 url = require('url'),
6 mime = require('mime'),
7 urlJoin = require('url-join'),
8 showDir = require('./ecstatic/showdir'),
9 version = JSON.parse(
10 fs.readFileSync(__dirname + '/../package.json').toString()
11 ).version,
12 status = require('./ecstatic/status-handlers'),
13 generateEtag = require('./ecstatic/etag'),
14 optsParser = require('./ecstatic/opts');
15
16var ecstatic = module.exports = function (dir, options) {
17 if (typeof dir !== 'string') {
18 options = dir;
19 dir = options.root;
20 }
21
22 var root = path.join(path.resolve(dir), '/'),
23 opts = optsParser(options),
24 cache = opts.cache,
25 autoIndex = opts.autoIndex,
26 baseDir = opts.baseDir,
27 defaultExt = opts.defaultExt,
28 handleError = opts.handleError,
29 headers = opts.headers,
30 serverHeader = opts.serverHeader,
31 weakEtags = opts.weakEtags,
32 handleOptionsMethod = opts.handleOptionsMethod;
33
34 opts.root = dir;
35 if (defaultExt && /^\./.test(defaultExt)) defaultExt = defaultExt.replace(/^\./, '');
36
37 // Support hashes and .types files in mimeTypes @since 0.8
38 if (opts.mimeTypes) {
39 try {
40 // You can pass a JSON blob here---useful for CLI use
41 opts.mimeTypes = JSON.parse(opts.mimeTypes);
42 } catch (e) {}
43 if (typeof opts.mimeTypes === 'string') {
44 mime.load(opts.mimeTypes);
45 }
46 else if (typeof opts.mimeTypes === 'object') {
47 mime.define(opts.mimeTypes);
48 }
49 }
50
51
52 return function middleware (req, res, next) {
53
54 // Strip any null bytes from the url
55 while(req.url.indexOf('%00') !== -1) {
56 req.url = req.url.replace(/\%00/g, '');
57 }
58 // Figure out the path for the file from the given url
59 var parsed = url.parse(req.url);
60 try {
61 decodeURIComponent(req.url); // check validity of url
62 var pathname = decodePathname(parsed.pathname);
63 }
64 catch (err) {
65 return status[400](res, next, { error: err });
66 }
67
68 var file = path.normalize(
69 path.join(root,
70 path.relative(
71 path.join('/', baseDir),
72 pathname
73 )
74 )
75 ),
76 gzipped = file + '.gz';
77
78 if(serverHeader !== false) {
79 // Set common headers.
80 res.setHeader('server', 'ecstatic-'+version);
81 }
82 Object.keys(headers).forEach(function (key) {
83 res.setHeader(key, headers[key])
84 })
85
86 if (req.method === 'OPTIONS' && handleOptionsMethod) {
87 return res.end();
88 }
89
90 // TODO: This check is broken, which causes the 403 on the
91 // expected 404.
92 if (file.slice(0, root.length) !== root) {
93 return status[403](res, next);
94 }
95
96 if (req.method && (req.method !== 'GET' && req.method !== 'HEAD' )) {
97 return status[405](res, next);
98 }
99
100 function statFile() {
101 fs.stat(file, function (err, stat) {
102 if (err && (err.code === 'ENOENT' || err.code === 'ENOTDIR')) {
103 if (req.statusCode == 404) {
104 // This means we're already trying ./404.html and can not find it.
105 // So send plain text response with 404 status code
106 status[404](res, next);
107 }
108 else if (!path.extname(parsed.pathname).length && defaultExt) {
109 // If there is no file extension in the path and we have a default
110 // extension try filename and default extension combination before rendering 404.html.
111 middleware({
112 url: parsed.pathname + '.' + defaultExt + ((parsed.search)? parsed.search:'')
113 }, res, next);
114 }
115 else {
116 // Try to serve default ./404.html
117 middleware({
118 url: (handleError ? ('/' + path.join(baseDir, '404.' + defaultExt)) : req.url),
119 statusCode: 404
120 }, res, next);
121 }
122 }
123 else if (err) {
124 status[500](res, next, { error: err });
125 }
126 else if (stat.isDirectory()) {
127 // 302 to / if necessary
128 if (!parsed.pathname.match(/\/$/)) {
129 res.statusCode = 302;
130 res.setHeader('location', parsed.pathname + '/' +
131 (parsed.query? ('?' + parsed.query):'')
132 );
133 return res.end();
134 }
135
136 if (autoIndex) {
137 return middleware({
138 url: urlJoin(encodeURIComponent(pathname), '/index.' + defaultExt)
139 }, res, function (err) {
140 if (err) {
141 return status[500](res, next, { error: err });
142 }
143 if (opts.showDir) {
144 return showDir(opts, stat)(req, res);
145 }
146
147 return status[403](res, next);
148 });
149 }
150
151 if (opts.showDir) {
152 return showDir(opts, stat)(req, res);
153 }
154
155 status[404](res, next);
156
157 }
158 else {
159 serve(stat);
160 }
161 });
162 }
163
164 // Look for a gzipped file if this is turned on
165 if (opts.gzip && shouldCompress(req)) {
166 fs.stat(gzipped, function (err, stat) {
167 if (!err && stat.isFile()) {
168 file = gzipped;
169 return serve(stat);
170 } else {
171 statFile();
172 }
173 });
174 } else {
175 statFile();
176 }
177
178 function serve(stat) {
179 // Do a MIME lookup, fall back to octet-stream and handle gzip
180 // special case.
181 var defaultType = opts.contentType || 'application/octet-stream',
182 contentType = mime.lookup(file, defaultType),
183 charSet;
184
185 if (contentType) {
186 charSet = mime.charsets.lookup(contentType, 'utf-8');
187 if (charSet) {
188 contentType += '; charset=' + charSet;
189 }
190 }
191
192 if (path.extname(file) === '.gz') {
193 res.setHeader('Content-Encoding', 'gzip');
194
195 // strip gz ending and lookup mime type
196 contentType = mime.lookup(path.basename(file, ".gz"), defaultType);
197 }
198
199 var range = (req.headers && req.headers['range']);
200 if (range) {
201 var total = stat.size;
202 var parts = range.replace(/bytes=/, "").split("-");
203 var partialstart = parts[0];
204 var partialend = parts[1];
205 var start = parseInt(partialstart, 10);
206 var end = Math.min(total-1, partialend ? parseInt(partialend, 10) : total-1);
207 var chunksize = (end-start)+1;
208 if (start > end || isNaN(start) || isNaN(end)) {
209 return status['416'](res, next);
210 }
211 var fstream = fs.createReadStream(file, {start: start, end: end});
212 fstream.on('error', function (err) {
213 status['500'](res, next, { error: err });
214 });
215 res.on('close', function () {
216 fstream.destroy();
217 });
218 res.writeHead(206, {
219 'Content-Range': 'bytes ' + start + '-' + end + '/' + total,
220 'Accept-Ranges': 'bytes',
221 'Content-Length': chunksize,
222 'Content-Type': contentType
223 });
224 fstream.pipe(res);
225 return;
226 }
227
228 // TODO: Helper for this, with default headers.
229 var lastModified = (new Date(stat.mtime)).toUTCString(),
230 etag = generateEtag(stat, weakEtags);
231 res.setHeader('last-modified', lastModified);
232 res.setHeader('etag', etag);
233
234 if (typeof cache === 'function') {
235 var requestSpecificCache = cache(pathname);
236 if (typeof requestSpecificCache === 'number') {
237 requestSpecificCache = 'max-age=' + requestSpecificCache;
238 }
239 res.setHeader('cache-control', requestSpecificCache);
240 } else {
241 res.setHeader('cache-control', cache);
242 }
243
244 // Return a 304 if necessary
245 if (shouldReturn304(req, lastModified, etag)) {
246 return status[304](res, next);
247 }
248
249 res.setHeader('content-length', stat.size);
250 res.setHeader('content-type', contentType);
251
252 // set the response statusCode if we have a request statusCode.
253 // This only can happen if we have a 404 with some kind of 404.html
254 // In all other cases where we have a file we serve the 200
255 res.statusCode = req.statusCode || 200;
256
257 if (req.method === "HEAD") {
258 return res.end();
259 }
260
261 var stream = fs.createReadStream(file);
262
263 stream.pipe(res);
264 stream.on('error', function (err) {
265 status['500'](res, next, { error: err });
266 });
267 }
268
269 function shouldReturn304(req, serverLastModified, serverEtag) {
270 if (!req || !req.headers) {
271 return false;
272 }
273
274 var clientModifiedSince = req.headers['if-modified-since'],
275 clientEtag = req.headers['if-none-match'];
276
277 if (!clientModifiedSince && !clientEtag) {
278 // Client did not provide any conditional caching headers
279 return false;
280 }
281
282 if (clientModifiedSince) {
283 // Catch "illegal access" dates that will crash v8
284 // https://github.com/jfhbrook/node-ecstatic/pull/179
285 try {
286 var clientModifiedDate = new Date(Date.parse(clientModifiedSince));
287 }
288 catch (err) { return false }
289
290 if (clientModifiedDate.toString() === 'Invalid Date') {
291 return false;
292 }
293 // If the client's copy is older than the server's, don't return 304
294 if (clientModifiedDate < new Date(serverLastModified)) {
295 return false;
296 }
297 }
298
299 if (clientEtag) {
300 // Do a strong or weak etag comparison based on setting
301 // https://www.ietf.org/rfc/rfc2616.txt Section 13.3.3
302 if (opts.weakCompare && clientEtag !== serverEtag
303 && clientEtag !== ('W/' + serverEtag) && ('W/' + clientEtag) !== serverEtag) {
304 return false;
305 } else if (!opts.weakCompare && (clientEtag !== serverEtag || clientEtag.indexOf('W/') === 0)) {
306 return false;
307 }
308 }
309
310 return true;
311 }
312 };
313};
314
315ecstatic.version = version;
316ecstatic.showDir = showDir;
317
318// Check to see if we should try to compress a file with gzip.
319function shouldCompress(req) {
320 var headers = req.headers;
321
322 return headers && headers['accept-encoding'] &&
323 headers['accept-encoding']
324 .split(",")
325 .some(function (el) {
326 return ['*','compress', 'gzip', 'deflate'].indexOf(el) != -1;
327 })
328 ;
329}
330
331// See: https://github.com/jesusabdullah/node-ecstatic/issues/109
332function decodePathname(pathname) {
333 var pieces = pathname.replace(/\\/g,"/").split('/');
334
335 return pieces.map(function (piece) {
336 piece = decodeURIComponent(piece);
337
338 if (process.platform === 'win32' && /\\/.test(piece)) {
339 throw new Error('Invalid forward slash character');
340 }
341
342 return piece;
343 }).join('/');
344}
345
346if (!module.parent) {
347 var defaults = require('./ecstatic/defaults.json')
348 var http = require('http'),
349 opts = require('minimist')(process.argv.slice(2), {
350 alias: require('./ecstatic/aliases.json'),
351 default: defaults,
352 boolean: Object.keys(defaults).filter(function (key) {
353 return typeof defaults[key] === 'boolean'
354 })
355 }),
356 envPORT = parseInt(process.env.PORT, 10),
357 port = envPORT > 1024 && envPORT <= 65536 ? envPORT : opts.port || opts.p || 8000,
358 dir = opts.root || opts._[0] || process.cwd();
359
360 if (opts.help || opts.h) {
361 var u = console.error;
362 u('usage: ecstatic [dir] {options} --port PORT');
363 u('see https://npm.im/ecstatic for more docs');
364 return;
365 }
366
367 http.createServer(ecstatic(dir, opts))
368 .listen(port, function () {
369 console.log('ecstatic serving ' + dir + ' at http://0.0.0.0:' + port);
370 });
371}