├── .gitignore ├── demo ├── plugin │ ├── www │ │ └── index.html │ ├── package.json │ └── plugin.js ├── configs │ └── default.js └── server.js ├── .npmignore ├── .travis.yml ├── connect.session.memory ├── package.json └── session-ext.js ├── connect.session.file ├── package.json └── session-ext.js ├── connect.session ├── package.json └── session-ext.js ├── connect.static ├── package.json └── static-plugin.js ├── README.md ├── connect ├── package.json ├── connect-plugin.js └── middleware │ └── static.js ├── test └── all.js ├── package.json └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /demo/plugin/www/index.html: -------------------------------------------------------------------------------- 1 | HelloWorld -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /demo/ 3 | /tests/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.6 4 | - 0.8 -------------------------------------------------------------------------------- /connect.session.memory/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "connect.session.memory", 3 | "version": "0.0.1", 4 | "main": "session-ext.js", 5 | "private": true, 6 | 7 | "plugin": { 8 | "provides": ["session-store"] 9 | } 10 | } -------------------------------------------------------------------------------- /demo/plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo.plugin", 3 | "version": "0.0.1", 4 | "main": "plugin.js", 5 | "private": true, 6 | 7 | "plugin": { 8 | "provides": [], 9 | "consumes": [ 10 | "connect" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /connect.session.file/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "connect.session.file", 3 | "version": "0.0.1", 4 | "main": "session-ext.js", 5 | "private": true, 6 | 7 | "plugin": { 8 | "provides": ["session-store"], 9 | "consumes": ["connect"] 10 | } 11 | } -------------------------------------------------------------------------------- /connect.session/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "connect.session", 3 | "version": "0.0.1", 4 | "main": "session-ext.js", 5 | "private": true, 6 | 7 | "plugin": { 8 | "provides": ["session"], 9 | "consumes": [ 10 | "connect", 11 | "session-store" 12 | ] 13 | } 14 | } -------------------------------------------------------------------------------- /connect.static/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "connect-architect.static", 3 | "version": "0.0.1", 4 | "main": "./static-plugin.js", 5 | "private": true, 6 | 7 | "plugin": { 8 | "provides": [ 9 | "static" 10 | ], 11 | "consumes": [ 12 | "connect" 13 | ] 14 | } 15 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | connect-architect 2 | ================= 3 | 4 | [Connect](https://github.com/senchalabs/connect) for [architect](https://github.com/c9/connect-architect). 5 | 6 | 7 | Demo 8 | ==== 9 | 10 | npm install 11 | cd ./demo 12 | node server 13 | open http://localhost:8080 14 | 15 | Test 16 | ==== 17 | 18 | npm install 19 | npm test 20 | -------------------------------------------------------------------------------- /connect/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "connect-architect.connect", 3 | "version": "0.0.1", 4 | "main": "./connect-plugin.js", 5 | "private": true, 6 | 7 | "plugin": { 8 | "provides": [ 9 | "connect", 10 | "http" 11 | ], 12 | "consumes": [ 13 | "log" 14 | ] 15 | } 16 | } -------------------------------------------------------------------------------- /demo/plugin/plugin.js: -------------------------------------------------------------------------------- 1 | 2 | const PATH = require("path"); 3 | const STATIC = require("../../connect/middleware/static"); 4 | 5 | module.exports = function startup(options, imports, register) { 6 | 7 | var connect = imports.connect; 8 | var sessionStore = imports["session-store"]; 9 | 10 | connect.useMain(STATIC(PATH.join(__dirname, "www"))); 11 | 12 | register(null, {}); 13 | }; 14 | -------------------------------------------------------------------------------- /demo/configs/default.js: -------------------------------------------------------------------------------- 1 | 2 | var port = parseInt(process.env.PORT || 8080, 10); 3 | 4 | module.exports = [{ 5 | packagePath: "../../connect", 6 | port: port 7 | }, { 8 | packagePath: "../../connect.session", 9 | key: "connect.architect." + port, 10 | secret: "1234" 11 | }, { 12 | packagePath: "../../connect.session.memory" 13 | }, { 14 | packagePath: "architect/plugins/architect.log" 15 | }, { 16 | packagePath: "../plugin" 17 | }]; 18 | -------------------------------------------------------------------------------- /connect.session.memory/session-ext.js: -------------------------------------------------------------------------------- 1 | var MemoryStore = require("connect").session.MemoryStore; 2 | 3 | module.exports = function startup(options, imports, register) { 4 | 5 | var sessionStore = new MemoryStore({ reapInterval: -1 }); 6 | 7 | register(null, { 8 | "session-store": { 9 | on: sessionStore.on.bind(sessionStore), 10 | get: sessionStore.get.bind(sessionStore), 11 | set: sessionStore.set.bind(sessionStore), 12 | destroy: sessionStore.destroy.bind(sessionStore), 13 | createSession: sessionStore.createSession.bind(sessionStore) 14 | } 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /connect.session/session-ext.js: -------------------------------------------------------------------------------- 1 | var Session = require("connect").session; 2 | var assert = require("assert"); 3 | 4 | module.exports = function startup(options, imports, register) { 5 | 6 | assert(options.key, "option 'key' is required"); 7 | assert(options.secret, "option 'secret' is required"); 8 | 9 | var connect = imports.connect; 10 | var sessionStore = imports["session-store"]; 11 | 12 | connect.useSession(Session({ 13 | store: sessionStore, 14 | key: options.key, 15 | secret: options.secret 16 | })); 17 | 18 | register(null, { 19 | session: { 20 | getKey: function() { 21 | return options.key; 22 | }, 23 | get: sessionStore.get 24 | } 25 | }); 26 | }; -------------------------------------------------------------------------------- /demo/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const PATH = require("path"); 4 | const ARCHITECT = require("architect"); 5 | 6 | exports.main = function(configName, callback) { 7 | 8 | var configPath = PATH.join(__dirname, "configs", configName); 9 | var config = ARCHITECT.loadConfig(configPath); 10 | 11 | ARCHITECT.createApp(config, function (err, app) { 12 | if (err) { 13 | console.error("While starting the '%s':", configPath); 14 | return callback(err); 15 | } 16 | console.log("Started '%s'!", configPath); 17 | callback(null, app); 18 | }); 19 | } 20 | 21 | 22 | if (require.main === module) { 23 | 24 | var configName = process.argv[2] || "default"; 25 | 26 | exports.main(configName, function(err, app) { 27 | if (err) { 28 | console.error(err.stack); 29 | return process.exit(1); 30 | } 31 | console.log(app); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /test/all.js: -------------------------------------------------------------------------------- 1 | 2 | const ASSERT = require("assert"); 3 | const DEMO_SERVER = require("../demo/server"); 4 | const REQUEST = require("request"); 5 | 6 | 7 | function main(callback) { 8 | 9 | var port = parseInt(process.env.PORT || 8080, 10); 10 | 11 | console.log("Port: " + port); 12 | 13 | DEMO_SERVER.main("default", function(err, app) { 14 | if (err) return callback(err); 15 | 16 | REQUEST("http://localhost:" + port, function (error, response, body) { 17 | if (error || response.statusCode !== 200) { 18 | ASSERT.fail("Did not get status 200!"); 19 | return; 20 | } 21 | 22 | ASSERT.equal(body, "HelloWorld"); 23 | 24 | callback(null); 25 | }); 26 | }); 27 | } 28 | 29 | 30 | if (require.main === module) { 31 | main(function(err) { 32 | if (err) { 33 | console.error(err.stack); 34 | process.exit(1); 35 | } 36 | console.log("OK"); 37 | process.exit(0); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "connect-architect", 3 | "description": "build a connect server using architect plugins", 4 | "version": "0.0.6", 5 | "author": "ajax.org B.V. ", 6 | "contributors": [ 7 | { 8 | "name": "Fabian Jakobs", 9 | "email": "fabian@c9.io" 10 | }, 11 | { 12 | "name": "Christoph Dorn", 13 | "email": "christoph@christophdorn.com" 14 | } 15 | ], 16 | "pm": "npm", 17 | "dependencies": { 18 | "architect": "~0.1.4", 19 | "connect": "1.8.7", 20 | "netutil": "~0.0.1", 21 | "mime": ">= 0.0.1" 22 | }, 23 | "devDependencies": { 24 | "request": "2.11.1" 25 | }, 26 | "scripts": { 27 | "test": "node test/all.js" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "http://github.com/c9/connect-architect.git" 32 | }, 33 | "licenses": [ 34 | { 35 | "type": "MIT", 36 | "url": "http://github.com/c9/connect-architect/raw/master/LICENSE" 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2012 ajax.org B.V 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /connect/connect-plugin.js: -------------------------------------------------------------------------------- 1 | var utils = require("connect/lib/utils"); 2 | var netutil = require("netutil"); 3 | var connect = require("connect"); 4 | 5 | module.exports = function startup(options, imports, register) { 6 | imports.log.info("connect plugin start"); 7 | 8 | var server = connect(); 9 | 10 | var hookNames = [ 11 | "Start", 12 | "Setup", 13 | "Main", 14 | "Session", 15 | "Auth", 16 | "Error" 17 | ]; 18 | var api = { 19 | getModule: function() { 20 | return connect; 21 | }, 22 | getUtils: function() { 23 | return utils; 24 | } 25 | }; 26 | hookNames.forEach(function(name) { 27 | var hookServer = connect(); 28 | server.use(hookServer); 29 | api["use" + name] = hookServer.use.bind(hookServer); 30 | }); 31 | 32 | api.useSetup(connect.cookieParser()); 33 | api.useSetup(connect.bodyParser()); 34 | 35 | api.addRoute = server.addRoute; 36 | api.use = api.useStart; 37 | 38 | api.on = server.on; 39 | api.emit = server.emit; 40 | 41 | function startListening (port, host) { 42 | api.getPort = function () { 43 | return port; 44 | }; 45 | api.getHost = function () { 46 | return host; 47 | }; 48 | 49 | server.listen(port, host, function(err) { 50 | if (err) 51 | return register(err); 52 | 53 | imports.log.info("Connect server listening at http://" + host + ":" + port); 54 | 55 | register(null, { 56 | "onDestruct": function(callback) { 57 | server.close(); 58 | server.on("close", callback); 59 | }, 60 | "connect": api, 61 | "http": { 62 | getServer: function() { 63 | return server; 64 | } 65 | } 66 | }); 67 | }); 68 | } 69 | 70 | if (options.port instanceof Array) { 71 | netutil.findFreePort(options.port[0], options.port[1], options.host, function(err, port) { 72 | if (err) 73 | return register(err); 74 | 75 | startListening(port, options.host || "localhost"); 76 | }); 77 | } else { 78 | startListening(options.port, options.host || "localhost"); 79 | } 80 | }; -------------------------------------------------------------------------------- /connect.static/static-plugin.js: -------------------------------------------------------------------------------- 1 | 2 | // TODO: This version may be needed for c9local but ideally we can find other solution so 3 | // we don't have to keep it up to date. 4 | //var connect_static = require("connect-architect/connect/middleware/static"); 5 | 6 | module.exports = function startup(options, imports, register) { 7 | 8 | var rjs = { 9 | "paths": {}, 10 | "packages": [] 11 | }; 12 | var prefix = options.prefix || "/static"; 13 | var workerPrefix = options.workerPrefix || "/static"; 14 | 15 | var connect = imports.connect.getModule(); 16 | var staticServer = connect.createServer(); 17 | imports.connect.useMain(options.bindPrefix || prefix, staticServer); 18 | 19 | register(null, { 20 | "static": { 21 | addStatics: function(statics) { 22 | 23 | statics.forEach(function(s) { 24 | 25 | // console.log("MOUNT", prefix, s.mount, s.path); 26 | 27 | if (s.router) { 28 | 29 | var server = connect.static(s.path); 30 | staticServer.use(s.mount, function(req, res, next) { 31 | s.router(req, res); 32 | server(req, res, next); 33 | }); 34 | 35 | } else { 36 | 37 | staticServer.use(s.mount, connect.static(s.path)); 38 | 39 | } 40 | 41 | var libs = s.rjs || {}; 42 | for (var name in libs) { 43 | if (typeof libs[name] === "string") { 44 | rjs.paths[name] = join(prefix, libs[name]); 45 | } else { 46 | // TODO: Ensure package is not already registered! 47 | rjs.packages.push(libs[name]); 48 | } 49 | } 50 | }); 51 | }, 52 | 53 | getRequireJsPaths: function() { 54 | return rjs.paths; 55 | }, 56 | 57 | getRequireJsPackages: function() { 58 | return rjs.packages; 59 | }, 60 | 61 | getStaticPrefix: function() { 62 | return prefix; 63 | }, 64 | 65 | getWorkerPrefix: function() { 66 | return workerPrefix; 67 | } 68 | } 69 | }); 70 | 71 | function join(prefix, path) { 72 | return prefix.replace(/\/*$/, "") + "/" + path.replace(/^\/*/, ""); 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /connect.session.file/session-ext.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"); 2 | var path = require("path"); 3 | var fs = require("fs"); 4 | var Store = require("connect/lib/middleware/session/store"); 5 | 6 | module.exports = function startup(options, imports, register) { 7 | 8 | assert(options.sessionsPath, "option 'sessionsPath' is required"); 9 | 10 | if (!path.existsSync(path.dirname(options.sessionsPath))) { 11 | fs.mkdir(path.dirname(options.sessionsPath), 0755); 12 | } 13 | if (!path.existsSync(options.sessionsPath)) { 14 | fs.mkdir(options.sessionsPath, 0755); 15 | } 16 | 17 | var sessionStore = new FileStore({ 18 | basePath: options.sessionsPath, 19 | reapInterval: 60 * 60 * 1000 // 1 hour 20 | }); 21 | 22 | register(null, { 23 | "session-store": { 24 | on: sessionStore.on.bind(sessionStore), 25 | get: sessionStore.get.bind(sessionStore), 26 | set: sessionStore.set.bind(sessionStore), 27 | destroy: sessionStore.destroy.bind(sessionStore), 28 | createSession: sessionStore.createSession.bind(sessionStore) 29 | } 30 | }); 31 | 32 | }; 33 | 34 | 35 | var FileStore = function(options) { 36 | var self = this; 37 | self.basePath = options.basePath; 38 | self.reapInterval = options.reapInterval || -1; 39 | if (self.reapInterval > 0) { 40 | setInterval(function() { 41 | fs.readdir(self.basePath, function(err, files) { 42 | if (err) { 43 | console.error(err); 44 | return; 45 | } 46 | files.forEach(function(file) { 47 | fs.readFile(self.basePath + "/" + file, function(err, data) { 48 | if (err) { 49 | console.error(err); 50 | return; 51 | } 52 | var sess = JSON.parse(data); 53 | var expires = (typeof sess.cookie.expires === 'string') 54 | ? new Date(sess.cookie.expires) 55 | : sess.cookie.expires; 56 | if (!expires || new Date < expires) { 57 | // session ok 58 | } else { 59 | self.destroy(file); 60 | } 61 | }); 62 | }); 63 | }); 64 | }, self.reapInterval); 65 | } 66 | }; 67 | 68 | FileStore.prototype.__proto__ = Store.prototype; 69 | 70 | FileStore.prototype.get = function(sid, fn){ 71 | var self = this; 72 | path.exists(self.basePath + "/" + sid, function(exists) { 73 | if (exists) { 74 | fs.readFile(self.basePath + "/" + sid, function(err, data) { 75 | if (err) { 76 | fn && fn(err); 77 | } 78 | else { 79 | var sess; 80 | try { 81 | sess = JSON.parse(data); 82 | } catch(e) { 83 | console.warn("Error '" + e + "' reading session: " + sid, data); 84 | self.destroy(sid, fn); 85 | return; 86 | } 87 | var expires = (typeof sess.cookie.expires === 'string') 88 | ? new Date(sess.cookie.expires) 89 | : sess.cookie.expires; 90 | if (!expires || new Date < expires) { 91 | fn(null, sess); 92 | } else { 93 | self.destroy(sid, fn); 94 | } 95 | } 96 | }); 97 | } 98 | else { 99 | fn(); 100 | } 101 | }); 102 | }; 103 | 104 | FileStore.prototype.set = function(sid, sess, fn){ 105 | var self = this; 106 | var path = self.basePath + "/" + sid; 107 | var tmpPath = path + "~" + new Date().getTime(); 108 | fs.writeFile(path, JSON.stringify(sess), function(err) { 109 | if (err) 110 | return fn && fn(err); 111 | 112 | fs.rename(tmpPath, path, function(err) { 113 | fn && fn(err); 114 | }); 115 | }); 116 | }; 117 | 118 | FileStore.prototype.destroy = function(sid, fn){ 119 | var self = this; 120 | path.exists(self.basePath + "/" + sid, function(exists) { 121 | if (exists) { 122 | fs.unlink(self.basePath + "/" + sid, function(err) { 123 | if (err) { 124 | fn && fn(err); 125 | } 126 | else { 127 | fn && fn(); 128 | } 129 | }); 130 | } else { 131 | fn && fn(); 132 | } 133 | }); 134 | }; 135 | 136 | FileStore.prototype.all = function(fn){ 137 | var self = this; 138 | fs.readdir(self.basePath, function(err, files) { 139 | if (err) { 140 | fn && fn(err); 141 | return; 142 | } 143 | var arr = []; 144 | files.forEach(function(file) { 145 | // TODO: Make this async. 146 | arr.push(JSON.parse(fs.readFileSync(self.basePath + "/" + file))); 147 | }); 148 | fn && fn(arr); 149 | }); 150 | }; 151 | 152 | FileStore.prototype.clear = function(fn){ 153 | throw new Error("NYI"); 154 | /* 155 | this.sessions = {}; 156 | fn && fn(); 157 | */ 158 | }; 159 | 160 | FileStore.prototype.length = function(fn){ 161 | throw new Error("NYI"); 162 | /* 163 | fn(null, Object.keys(this.sessions).length); 164 | */ 165 | }; 166 | -------------------------------------------------------------------------------- /connect/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('connect/lib/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 | * - `redirect` Redirect to trailing "/" when the pathname is a dir 43 | * 44 | * @param {String} root 45 | * @param {Object} options 46 | * @return {Function} 47 | * @api public 48 | */ 49 | 50 | exports = module.exports = function static(root, options){ 51 | options = options || {}; 52 | 53 | // root required 54 | if (!root) throw new Error('static() root path required'); 55 | options.root = root; 56 | 57 | return function static(req, res, next) { 58 | options.path = req.url; 59 | options.getOnly = true; 60 | send(req, res, next, options); 61 | }; 62 | }; 63 | 64 | /** 65 | * Expose mime module. 66 | */ 67 | 68 | exports.mime = mime; 69 | 70 | /** 71 | * Respond with 416 "Requested Range Not Satisfiable" 72 | * 73 | * @param {ServerResponse} res 74 | * @api private 75 | */ 76 | 77 | function invalidRange(res) { 78 | var body = 'Requested Range Not Satisfiable'; 79 | res.setHeader('Content-Type', 'text/plain'); 80 | res.setHeader('Content-Length', body.length); 81 | res.statusCode = 416; 82 | res.end(body); 83 | } 84 | 85 | /** 86 | * Attempt to tranfer the requseted file to `res`. 87 | * 88 | * @param {ServerRequest} 89 | * @param {ServerResponse} 90 | * @param {Function} next 91 | * @param {Object} options 92 | * @api private 93 | */ 94 | 95 | var send = exports.send = function(req, res, next, options){ 96 | options = options || {}; 97 | if (!options.path) throw new Error('path required'); 98 | 99 | // setup 100 | var maxAge = options.maxAge || 0 101 | , ranges = req.headers.range 102 | , head = 'HEAD' == req.method 103 | , get = 'GET' == req.method 104 | , root = options.root ? normalize(options.root) : null 105 | , redirect = false === options.redirect ? false : true 106 | , getOnly = options.getOnly 107 | , fn = options.callback 108 | , hidden = options.hidden 109 | , done; 110 | 111 | // replace next() with callback when available 112 | if (fn) next = fn; 113 | 114 | // ignore non-GET requests 115 | if (getOnly && !get && !head) return next(); 116 | 117 | // parse url 118 | var url = parse(options.path) 119 | , path = decodeURIComponent(url.pathname) 120 | , type; 121 | 122 | // null byte(s) 123 | if (~path.indexOf('\0')) return utils.badRequest(res); 124 | 125 | // when root is not given, consider .. malicious 126 | if (!root && ~path.indexOf('..')) return utils.forbidden(res); 127 | 128 | // join / normalize from optional root dir 129 | path = normalize(join(root, path)); 130 | 131 | // malicious path 132 | if (root && 0 != path.indexOf(root)) return fn 133 | ? fn(new Error('Forbidden')) 134 | : utils.forbidden(res); 135 | 136 | // index.html support 137 | if (normalize('/') == path[path.length - 1]) path += 'index.html'; 138 | 139 | // "hidden" file 140 | if (!hidden && '.' == basename(path)[0]) return next(); 141 | 142 | fs.stat(path, function(err, stat){ 143 | 144 | // check for gzip content encoding 145 | if (/\.[^\.]*\.gz$/.test(path)) { 146 | // mime type 147 | type = mime.lookup(path.substring(0, path.length - 3)); 148 | if (!res.getHeader('content-encoding')) { 149 | res.setHeader('Content-Encoding', 'gzip'); 150 | } 151 | } else { 152 | // mime type 153 | type = mime.lookup(path); 154 | } 155 | 156 | // ignore ENOENT 157 | if (err) { 158 | if (fn) return fn(err); 159 | return 'ENOENT' == err.code 160 | ? next() 161 | : next(err); 162 | // redirect directory in case index.html is present 163 | } else if (stat.isDirectory()) { 164 | if (!redirect) return next(); 165 | res.statusCode = 301; 166 | res.setHeader('Location', url.pathname + '/'); 167 | res.end('Redirecting to ' + url.pathname + '/'); 168 | return; 169 | } 170 | 171 | // header fields 172 | if (!res.getHeader('Date')) res.setHeader('Date', new Date().toUTCString()); 173 | if (!res.getHeader('Cache-Control')) res.setHeader('Cache-Control', 'public, max-age=' + (maxAge / 1000)); 174 | if (!res.getHeader('Last-Modified')) res.setHeader('Last-Modified', stat.mtime.toUTCString()); 175 | if (!res.getHeader('ETag')) res.setHeader('ETag', utils.etag(stat)); 176 | if (!res.getHeader('content-type')) { 177 | var charset = mime.charsets.lookup(type); 178 | res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : '')); 179 | } 180 | res.setHeader('Accept-Ranges', 'bytes'); 181 | 182 | // conditional GET support 183 | if (utils.conditionalGET(req)) { 184 | if (!utils.modified(req, res)) { 185 | req.emit('static'); 186 | return utils.notModified(res); 187 | } 188 | } 189 | 190 | var opts = {}; 191 | var chunkSize = stat.size; 192 | 193 | // we have a Range request 194 | if (ranges) { 195 | ranges = utils.parseRange(stat.size, ranges); 196 | // valid 197 | if (ranges) { 198 | // TODO: stream options 199 | // TODO: multiple support 200 | opts.start = ranges[0].start; 201 | opts.end = ranges[0].end; 202 | chunkSize = opts.end - opts.start + 1; 203 | res.statusCode = 206; 204 | res.setHeader('Content-Range', 'bytes ' 205 | + opts.start 206 | + '-' 207 | + opts.end 208 | + '/' 209 | + stat.size); 210 | // invalid 211 | } else { 212 | return fn 213 | ? fn(new Error('Requested Range Not Satisfiable')) 214 | : invalidRange(res); 215 | } 216 | } 217 | 218 | res.setHeader('Content-Length', chunkSize); 219 | 220 | // transfer 221 | if (head) return res.end(); 222 | 223 | // stream 224 | var stream = fs.createReadStream(path, opts); 225 | req.emit('static', stream); 226 | stream.pipe(res); 227 | 228 | // callback 229 | if (fn) { 230 | function callback(err) { done || fn(err); done = true } 231 | req.on('close', callback); 232 | stream.on('end', callback); 233 | } 234 | }); 235 | }; --------------------------------------------------------------------------------