connect
Version:
High performance middleware framework
236 lines (199 loc) • 5.71 kB
JavaScript
/*!
* Ext JS Connect
* Copyright(c) 2010 Sencha Inc.
* MIT Licensed
*/
/**
* Module dependencies.
*/
var fs = require('fs'),
Path = require('path'),
utils = require('../utils'),
Buffer = require('buffer').Buffer,
parseUrl = require('url').parse,
queryString = require('querystring');
/**
* File buffer cache.
*/
var _cache = {};
/**
* Static file server.
*
* Options:
*
* - `root` Root path from which to serve static files.
* - `maxAge` Browser cache maxAge in milliseconds, defaults to 0
* - `cache` When true cache files in memory indefinitely,
* until invalidated by a conditional GET request.
* When given, maxAge will be derived from this value.
*
* @param {Object} options
* @return {Function}
* @api public
*/
module.exports = function staticProvider(options){
var cache, maxAge, root;
// Support options object and root string
if (typeof options == 'string') {
root = options;
maxAge = 0;
} else {
options = options || {};
maxAge = options.maxAge;
root = process.connectEnv.staticRoot || options.root || process.cwd();
cache = options.cache;
if (cache && !maxAge) maxAge = cache;
maxAge = maxAge || 0;
}
return function staticProvider(req, res, next) {
if (req.method != 'GET' && req.method != 'HEAD') return next();
var hit,
head = req.method == 'HEAD',
filename, url = parseUrl(req.url);
// Potentially malicious path
if (~url.pathname.indexOf('..')) {
return forbidden(res);
}
// Absolute path
filename = Path.join(root, queryString.unescape(url.pathname));
// Index.html support
if (filename[filename.length - 1] === '/') {
filename += "index.html";
}
// Cache hit
if (cache && !conditionalGET(req) && (hit = _cache[req.url])) {
res.writeHead(200, hit.headers);
res.end(head ? undefined : hit.body);
return;
}
fs.stat(filename, function(err, stat){
// Pass through for missing files, thow error for other problems
if (err) {
return err.errno === process.ENOENT
? next()
: next(err);
} else if (stat.isDirectory()) {
return next();
}
// Serve the file directly using buffers
function onRead(err, data) {
if (err) return next(err);
// Response headers
var headers = {
"Content-Type": utils.mime.type(filename),
"Content-Length": stat.size,
"Last-Modified": stat.mtime.toUTCString(),
"Cache-Control": "public max-age=" + (maxAge / 1000),
"ETag": etag(stat)
};
// Conditional GET
if (!modified(req, headers)) {
return notModified(res, headers);
}
res.writeHead(200, headers);
res.end(head ? undefined : data);
// Cache support
if (cache) {
_cache[req.url] = {
headers: headers,
body: data
};
}
}
fs.readFile(filename, onRead);
});
};
};
/**
* Check if `req` and response `headers`.
*
* @param {IncomingMessage} req
* @param {Object} headers
* @return {Boolean}
* @api private
*/
function modified(req, headers) {
var modifiedSince = req.headers['if-modified-since'],
lastModified = headers['Last-Modified'],
noneMatch = req.headers['if-none-match'],
etag = headers['ETag'];
// Check If-None-Match
if (noneMatch && etag && noneMatch == etag) {
return false;
}
// Check If-Modified-Since
if (modifiedSince && lastModified) {
modifiedSince = new Date(modifiedSince);
lastModified = new Date(lastModified);
// Ignore invalid dates
if (!isNaN(modifiedSince.getTime())) {
if (lastModified <= modifiedSince) return false;
}
}
return true;
}
/**
* Check if `req` is a conditional GET request.
*
* @param {IncomingMessage} req
* @return {Boolean}
* @api private
*/
function conditionalGET(req) {
return req.headers['if-modified-since']
|| req.headers['if-none-match'];
}
/**
* Return an ETag in the form of size-mtime.
*
* @param {Object} stat
* @return {String}
* @api private
*/
function etag(stat) {
return stat.size + '-' + Number(stat.mtime);
}
/**
* Respond with 304 "Not Modified".
*
* @param {ServerResponse} res
* @param {Object} headers
* @api private
*/
function notModified(res, headers) {
// Strip Content-* headers
Object.keys(headers).forEach(function(field){
if (0 == field.indexOf('Content')) {
delete headers[field];
}
});
res.writeHead(304, headers);
res.end();
}
/**
* Respond with 403 "Forbidden".
*
* @param {ServerResponse} res
* @api private
*/
function forbidden(res) {
var body = 'Forbidden';
res.writeHead(403, {
'Content-Type': 'text/plain',
'Content-Length': body.length
});
res.end(body);
}
/**
* Clear the memory cache for `key` or the entire store.
*
* @param {String} key
* @api public
*/
exports.clearCache = function(key){
if (key) {
delete _cache[key];
} else {
_cache = {};
}
};