rc-web@69: var fs = require('fs') rc-web@69: , events = require('events') rc-web@69: , buffer = require('buffer') rc-web@69: , http = require('http') rc-web@69: , url = require('url') rc-web@69: , path = require('path') rc-web@69: , mime = require('mime') rc-web@69: , util = require('./node-static/util'); rc-web@69: rc-web@69: // Current version rob@76: var version = [0, 7, 3]; rc-web@69: rc-web@69: Server = function (root, options) { rc-web@69: if (root && (typeof(root) === 'object')) { options = root; root = null } rc-web@69: rc-web@69: this.root = path.resolve(root || '.'); rc-web@69: this.options = options || {}; rc-web@69: this.cache = 3600; rc-web@69: rc-web@69: this.defaultHeaders = {}; rc-web@69: this.options.headers = this.options.headers || {}; rc-web@69: rc-web@69: if ('cache' in this.options) { rc-web@69: if (typeof(this.options.cache) === 'number') { rc-web@69: this.cache = this.options.cache; rc-web@69: } else if (! this.options.cache) { rc-web@69: this.cache = false; rc-web@69: } rc-web@69: } rc-web@69: rc-web@69: if ('serverInfo' in this.options) { rc-web@69: this.serverInfo = this.options.serverInfo.toString(); rc-web@69: } else { rc-web@69: this.serverInfo = 'node-static/' + version.join('.'); rc-web@69: } rc-web@69: rc-web@69: this.defaultHeaders['server'] = this.serverInfo; rc-web@69: rc-web@69: if (this.cache !== false) { rc-web@69: this.defaultHeaders['cache-control'] = 'max-age=' + this.cache; rc-web@69: } rc-web@69: rc-web@69: for (var k in this.defaultHeaders) { rc-web@69: this.options.headers[k] = this.options.headers[k] || rc-web@69: this.defaultHeaders[k]; rc-web@69: } rc-web@69: }; rc-web@69: rc-web@69: Server.prototype.serveDir = function (pathname, req, res, finish) { rc-web@69: var htmlIndex = path.join(pathname, 'index.html'), rc-web@69: that = this; rc-web@69: rc-web@69: fs.stat(htmlIndex, function (e, stat) { rc-web@69: if (!e) { rc-web@69: var status = 200; rc-web@69: var headers = {}; rc-web@69: var originalPathname = decodeURI(url.parse(req.url).pathname); rc-web@69: if (originalPathname.length && originalPathname.charAt(originalPathname.length - 1) !== '/') { rc-web@69: return finish(301, { 'Location': originalPathname + '/' }); rc-web@69: } else { rc-web@69: that.respond(null, status, headers, [htmlIndex], stat, req, res, finish); rc-web@69: } rc-web@69: } else { rc-web@69: // Stream a directory of files as a single file. rc-web@69: fs.readFile(path.join(pathname, 'index.json'), function (e, contents) { rc-web@69: if (e) { return finish(404, {}) } rc-web@69: var index = JSON.parse(contents); rc-web@69: streamFiles(index.files); rc-web@69: }); rc-web@69: } rc-web@69: }); rc-web@69: function streamFiles(files) { rc-web@69: util.mstat(pathname, files, function (e, stat) { rc-web@69: if (e) { return finish(404, {}) } rc-web@69: that.respond(pathname, 200, {}, files, stat, req, res, finish); rc-web@69: }); rc-web@69: } rc-web@69: }; rc-web@69: rc-web@69: Server.prototype.serveFile = function (pathname, status, headers, req, res) { rc-web@69: var that = this; rc-web@69: var promise = new(events.EventEmitter); rc-web@69: rc-web@69: pathname = this.resolve(pathname); rc-web@69: rc-web@69: fs.stat(pathname, function (e, stat) { rc-web@69: if (e) { rc-web@69: return promise.emit('error', e); rc-web@69: } rc-web@69: that.respond(null, status, headers, [pathname], stat, req, res, function (status, headers) { rc-web@69: that.finish(status, headers, req, res, promise); rc-web@69: }); rc-web@69: }); rc-web@69: return promise; rc-web@69: }; rc-web@69: rc-web@69: Server.prototype.finish = function (status, headers, req, res, promise, callback) { rc-web@69: var result = { rc-web@69: status: status, rc-web@69: headers: headers, rc-web@69: message: http.STATUS_CODES[status] rc-web@69: }; rc-web@69: rc-web@69: headers['server'] = this.serverInfo; rc-web@69: rc-web@69: if (!status || status >= 400) { rc-web@69: if (callback) { rc-web@69: callback(result); rc-web@69: } else { rc-web@69: if (promise.listeners('error').length > 0) { rc-web@69: promise.emit('error', result); rc-web@69: } rc-web@69: else { rc-web@69: res.writeHead(status, headers); rc-web@69: res.end(); rc-web@69: } rc-web@69: } rc-web@69: } else { rc-web@69: // Don't end the request here, if we're streaming; rc-web@69: // it's taken care of in `prototype.stream`. rc-web@69: if (status !== 200 || req.method !== 'GET') { rc-web@69: res.writeHead(status, headers); rc-web@69: res.end(); rc-web@69: } rc-web@69: callback && callback(null, result); rc-web@69: promise.emit('success', result); rc-web@69: } rc-web@69: }; rc-web@69: rc-web@69: Server.prototype.servePath = function (pathname, status, headers, req, res, finish) { rc-web@69: var that = this, rc-web@69: promise = new(events.EventEmitter); rc-web@69: rc-web@69: pathname = this.resolve(pathname); rc-web@69: rc-web@69: // Make sure we're not trying to access a rc-web@69: // file outside of the root. rc-web@69: if (pathname.indexOf(that.root) === 0) { rc-web@69: fs.stat(pathname, function (e, stat) { rc-web@69: if (e) { rc-web@69: finish(404, {}); rc-web@69: } else if (stat.isFile()) { // Stream a single file. rc-web@69: that.respond(null, status, headers, [pathname], stat, req, res, finish); rc-web@69: } else if (stat.isDirectory()) { // Stream a directory of files. rc-web@69: that.serveDir(pathname, req, res, finish); rc-web@69: } else { rc-web@69: finish(400, {}); rc-web@69: } rc-web@69: }); rc-web@69: } else { rc-web@69: // Forbidden rc-web@69: finish(403, {}); rc-web@69: } rc-web@69: return promise; rc-web@69: }; rc-web@69: rc-web@69: Server.prototype.resolve = function (pathname) { rc-web@69: return path.resolve(path.join(this.root, pathname)); rc-web@69: }; rc-web@69: rc-web@69: Server.prototype.serve = function (req, res, callback) { rc-web@69: var that = this, rc-web@69: promise = new(events.EventEmitter), rc-web@69: pathname; rc-web@69: rc-web@69: var finish = function (status, headers) { rc-web@69: that.finish(status, headers, req, res, promise, callback); rc-web@69: }; rc-web@69: rc-web@69: try { rc-web@69: pathname = decodeURI(url.parse(req.url).pathname); rc-web@69: } rc-web@69: catch(e) { rc-web@69: return process.nextTick(function() { rc-web@69: return finish(400, {}); rc-web@69: }); rc-web@69: } rc-web@69: rc-web@69: process.nextTick(function () { rc-web@69: that.servePath(pathname, 200, {}, req, res, finish).on('success', function (result) { rc-web@69: promise.emit('success', result); rc-web@69: }).on('error', function (err) { rc-web@69: promise.emit('error'); rc-web@69: }); rc-web@69: }); rc-web@69: if (! callback) { return promise } rc-web@69: }; rc-web@69: rc-web@69: /* Check if we should consider sending a gzip version of the file based on the rc-web@69: * file content type and client's Accept-Encoding header value. rc-web@69: */ rc-web@69: Server.prototype.gzipOk = function(req, contentType) { rc-web@69: var enable = this.options.gzip; rc-web@69: if(enable && rc-web@69: (typeof enable === 'boolean' || rc-web@69: (contentType && (enable instanceof RegExp) && enable.test(contentType)))) { rc-web@69: var acceptEncoding = req.headers['accept-encoding']; rc-web@69: return acceptEncoding && acceptEncoding.indexOf("gzip") >= 0; rc-web@69: } rc-web@69: return false; rc-web@69: } rc-web@69: rc-web@69: /* Send a gzipped version of the file if the options and the client indicate gzip is enabled and rc-web@69: * we find a .gz file mathing the static resource requested. rc-web@69: */ rc-web@69: Server.prototype.respondGzip = function(pathname, status, contentType, _headers, files, stat, req, res, finish) { rc-web@69: var that = this; rc-web@69: if(files.length == 1 && this.gzipOk(req, contentType)) { rc-web@69: var gzFile = files[0] + ".gz"; rc-web@69: fs.stat(gzFile, function(e, gzStat) { rc-web@69: if(!e && gzStat.isFile()) { rc-web@69: //console.log('Serving', gzFile, 'to gzip-capable client instead of', files[0], 'new size is', gzStat.size, 'uncompressed size', stat.size); rc-web@69: var vary = _headers['Vary']; rc-web@69: _headers['Vary'] = (vary && vary != 'Accept-Encoding'?vary+', ':'')+'Accept-Encoding'; rc-web@69: _headers['Content-Encoding'] = 'gzip'; rc-web@69: stat.size = gzStat.size; rc-web@69: files = [gzFile]; rc-web@69: } else { rc-web@69: //console.log('gzip file not found or error finding it', gzFile, String(e), stat.isFile()); rc-web@69: } rc-web@69: that.respondNoGzip(pathname, status, contentType, _headers, files, stat, req, res, finish); rc-web@69: }); rc-web@69: } else { rc-web@69: // Client doesn't want gzip or we're sending multiple files rc-web@69: that.respondNoGzip(pathname, status, contentType, _headers, files, stat, req, res, finish); rc-web@69: } rc-web@69: } rc-web@69: rc-web@69: Server.prototype.respondNoGzip = function (pathname, status, contentType, _headers, files, stat, req, res, finish) { rc-web@69: var mtime = Date.parse(stat.mtime), rc-web@69: key = pathname || files[0], rc-web@69: headers = {}, rc-web@69: clientETag = req.headers['if-none-match'], rc-web@69: clientMTime = Date.parse(req.headers['if-modified-since']); rc-web@69: rc-web@69: rc-web@69: // Copy default headers rc-web@69: for (var k in this.options.headers) { headers[k] = this.options.headers[k] } rc-web@69: // Copy custom headers rc-web@69: for (var k in _headers) { headers[k] = _headers[k] } rc-web@69: rc-web@69: headers['Etag'] = JSON.stringify([stat.ino, stat.size, mtime].join('-')); rc-web@69: headers['Date'] = new(Date)().toUTCString(); rc-web@69: headers['Last-Modified'] = new(Date)(stat.mtime).toUTCString(); rc-web@69: headers['Content-Type'] = contentType; rc-web@69: headers['Content-Length'] = stat.size; rc-web@69: rc-web@69: for (var k in _headers) { headers[k] = _headers[k] } rc-web@69: rc-web@69: // Conditional GET rc-web@69: // If the "If-Modified-Since" or "If-None-Match" headers rc-web@69: // match the conditions, send a 304 Not Modified. rc-web@69: if ((clientMTime || clientETag) && rc-web@69: (!clientETag || clientETag === headers['Etag']) && rc-web@69: (!clientMTime || clientMTime >= mtime)) { rc-web@69: // 304 response should not contain entity headers rc-web@69: ['Content-Encoding', rc-web@69: 'Content-Language', rc-web@69: 'Content-Length', rc-web@69: 'Content-Location', rc-web@69: 'Content-MD5', rc-web@69: 'Content-Range', rc-web@69: 'Content-Type', rc-web@69: 'Expires', rc-web@69: 'Last-Modified'].forEach(function(entityHeader) { rc-web@69: delete headers[entityHeader]; rc-web@69: }); rc-web@69: finish(304, headers); rc-web@69: } else { rc-web@69: res.writeHead(status, headers); rc-web@69: rc-web@69: this.stream(pathname, files, new(buffer.Buffer)(stat.size), res, function (e, buffer) { rc-web@69: if (e) { return finish(500, {}) } rc-web@69: finish(status, headers); rc-web@69: }); rc-web@69: } rc-web@69: }; rc-web@69: rc-web@69: Server.prototype.respond = function (pathname, status, _headers, files, stat, req, res, finish) { rc-web@69: var contentType = _headers['Content-Type'] || rc-web@69: mime.lookup(files[0]) || rc-web@69: 'application/octet-stream'; rc-web@69: if(this.options.gzip) { rc-web@69: this.respondGzip(pathname, status, contentType, _headers, files, stat, req, res, finish); rc-web@69: } else { rc-web@69: this.respondNoGzip(pathname, status, contentType, _headers, files, stat, req, res, finish); rc-web@69: } rc-web@69: } rc-web@69: rc-web@69: Server.prototype.stream = function (pathname, files, buffer, res, callback) { rc-web@69: (function streamFile(files, offset) { rc-web@69: var file = files.shift(); rc-web@69: rc-web@69: if (file) { rc-web@69: file = file[0] === '/' ? file : path.join(pathname || '.', file); rc-web@69: rc-web@69: // Stream the file to the client rc-web@69: fs.createReadStream(file, { rc-web@69: flags: 'r', rc-web@69: mode: 0666 rc-web@69: }).on('data', function (chunk) { rob@76: // Bounds check the incoming chunk and offset, as copying rob@76: // a buffer from an invalid offset will throw an error and crash rob@76: if (chunk.length && offset < buffer.length && offset >= 0) { rob@76: chunk.copy(buffer, offset); rob@76: offset += chunk.length; rob@76: } rc-web@69: }).on('close', function () { rc-web@69: streamFile(files, offset); rc-web@69: }).on('error', function (err) { rc-web@69: callback(err); rc-web@69: console.error(err); rc-web@69: }).pipe(res, { end: false }); rc-web@69: } else { rc-web@69: res.end(); rc-web@69: callback(null, buffer, offset); rc-web@69: } rc-web@69: })(files.slice(0), 0); rc-web@69: }; rc-web@69: rc-web@69: // Exports rc-web@69: exports.Server = Server; rc-web@69: exports.version = version; rc-web@69: exports.mime = mime; rc-web@69: rc-web@69: rc-web@69: