/*! * Connect - staticProvider * Copyright(c) 2010 Sencha Inc. * Copyright(c) 2011 TJ Holowaychuk * MIT Licensed */ /** * Module dependencies. */ var fs = require('fs') , path = require('path') , join = path.join , basename = path.basename , normalize = path.normalize , utils = require('../utils') , Buffer = require('buffer').Buffer , parse = require('url').parse , mime = require('mime'); /** * Static file server with the given `root` path. * * Examples: * * var oneDay = 86400000; * * connect( * connect.static(__dirname + '/public') * ).listen(3000); * * connect( * connect.static(__dirname + '/public', { maxAge: oneDay }) * ).listen(3000); * * Options: * * - `maxAge` Browser cache maxAge in milliseconds. defaults to 0 * - `hidden` Allow transfer of hidden files. defaults to false * - `redirect` Redirect to trailing "/" when the pathname is a dir * * @param {String} root * @param {Object} options * @return {Function} * @api public */ exports = module.exports = function static(root, options){ options = options || {}; // root required if (!root) throw new Error('static() root path required'); options.root = root; return function static(req, res, next) { options.path = req.url; options.getOnly = true; send(req, res, next, options); }; }; /** * Expose mime module. */ exports.mime = mime; /** * Respond with 416 "Requested Range Not Satisfiable" * * @param {ServerResponse} res * @api private */ function invalidRange(res) { var body = 'Requested Range Not Satisfiable'; res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Length', body.length); res.statusCode = 416; res.end(body); } /** * Attempt to tranfer the requseted file to `res`. * * @param {ServerRequest} * @param {ServerResponse} * @param {Function} next * @param {Object} options * @api private */ var send = exports.send = function(req, res, next, options){ options = options || {}; if (!options.path) throw new Error('path required'); // setup var maxAge = options.maxAge || 0 , ranges = req.headers.range , head = 'HEAD' == req.method , get = 'GET' == req.method , root = options.root ? normalize(options.root) : null , redirect = false === options.redirect ? false : true , getOnly = options.getOnly , fn = options.callback , hidden = options.hidden , done; // replace next() with callback when available if (fn) next = fn; // ignore non-GET requests if (getOnly && !get && !head) return next(); // parse url var url = parse(options.path) , path = decodeURIComponent(url.pathname) , type; // null byte(s) if (~path.indexOf('\0')) return utils.badRequest(res); // when root is not given, consider .. malicious if (!root && ~path.indexOf('..')) return utils.forbidden(res); // join / normalize from optional root dir path = normalize(join(root, path)); // malicious path if (root && 0 != path.indexOf(root)) return fn ? fn(new Error('Forbidden')) : utils.forbidden(res); // index.html support if (normalize('/') == path[path.length - 1]) path += 'index.html'; // "hidden" file if (!hidden && '.' == basename(path)[0]) return next(); fs.stat(path, function(err, stat){ // mime type type = mime.lookup(path); // ignore ENOENT if (err) { if (fn) return fn(err); return 'ENOENT' == err.code ? next() : next(err); // redirect directory in case index.html is present } else if (stat.isDirectory()) { if (!redirect) return next(); res.statusCode = 301; res.setHeader('Location', url.pathname + '/'); res.end('Redirecting to ' + url.pathname + '/'); return; } // header fields if (!res.getHeader('Date')) res.setHeader('Date', new Date().toUTCString()); if (!res.getHeader('Cache-Control')) res.setHeader('Cache-Control', 'public, max-age=' + (maxAge / 1000)); if (!res.getHeader('Last-Modified')) res.setHeader('Last-Modified', stat.mtime.toUTCString()); if (!res.getHeader('ETag')) res.setHeader('ETag', utils.etag(stat)); if (!res.getHeader('content-type')) { var charset = mime.charsets.lookup(type); res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : '')); } res.setHeader('Accept-Ranges', 'bytes'); // conditional GET support if (utils.conditionalGET(req)) { if (!utils.modified(req, res)) { req.emit('static'); return utils.notModified(res); } } var opts = {}; var chunkSize = stat.size; // we have a Range request if (ranges) { ranges = utils.parseRange(stat.size, ranges); // valid if (ranges) { // TODO: stream options // TODO: multiple support opts.start = ranges[0].start; opts.end = ranges[0].end; chunkSize = opts.end - opts.start + 1; res.statusCode = 206; res.setHeader('Content-Range', 'bytes ' + opts.start + '-' + opts.end + '/' + stat.size); // invalid } else { return fn ? fn(new Error('Requested Range Not Satisfiable')) : invalidRange(res); } } res.setHeader('Content-Length', chunkSize); // transfer if (head) return res.end(); // stream var stream = fs.createReadStream(path, opts); req.emit('static', stream); stream.pipe(res); // callback if (fn) { function callback(err) { done || fn(err); done = true } req.on('close', callback); stream.on('end', callback); } }); };