UNPKG

thrill

Version:

Exciting testing

296 lines (244 loc) 8.91 kB
var path = require('path'), http = require('http'), its = require('its'), fs = require('fs'), express = require('express'), EventEmitter = require('events').EventEmitter, generateId = require('node-uuid').v4, utils = require('../utils.js'), adapters = require('../../').adapters; function getExternalIpAddress(){ var interfaces = require('os').networkInterfaces(); var addresses = []; utils.each(interfaces, function(iface, name){ addresses = addresses.concat( utils.filter(iface, function(node){ return node.family === "IPv4" && node.internal === false; }) ); }); if(addresses.length > 0){ return addresses[0].address; } } var create = module.exports = function(options){ options = options || {}; var callback = options.callback || utils.noop, host = options.host || "", hostArr = host.split(":"), hostname = hostArr[0], port = hostArr[1] || 9300, expressInstance = options.expressInstance || express(), httpServer = options.httpServer || require('http').createServer(), server; httpServer.listen(port, hostname); httpServer.on('request', expressInstance); // If no hostname was found, we still need a path to refer to ourself if(!hostname) host = getExternalIpAddress() + ":" + port; else host = hostname + ":" + port; httpServer.on('error', function(e){ server.log('[HTTP Server] Unable to start: ' + e + "\n"); server.emitter.emit('error', e); callback(e); }); httpServer.on('listening', function(){ server.debug('[HTTP Server] Listening for connections on ' + host + "\n"); server.emitter.emit('listening'); callback(server); }); server = new HttpServer( httpServer, expressInstance, host ); if(options.log) server.log = options.log; if(options.debug) server.debug = options.debug; if(options.autoAdapt !== void 0) server.autoAdapt = options.autoAdapt; if(options.autoAdaptExtensions) server.autoAdaptExtensions = options.autoAdaptExtensions; return server; }; var HttpServer = exports.HttpServer = function(httpServer, expressInstance, host){ its.object(httpServer, 'An http server must be defined'); its.object(expressInstance, 'An express instance must be defined'); its.string(host, 'A host string must be defined'); this.emitter = new EventEmitter(); this.expressInstance = expressInstance; this.httpServer = httpServer; this.host = host; var connections = this.connections = []; httpServer.on('connection', function(socket) { connections.push(socket); socket.on('close', function(){ connections.splice(connections.indexOf(socket), 1); }); }); this.resourceMap = {}; this.urlToResourceId = {}; this.expressInstance.all('/g/:groupId/*', this.getResource.bind(this)); }; HttpServer.prototype.log = utils.noop; HttpServer.prototype.debug = utils.noop; HttpServer.prototype.autoAdapt = true; HttpServer.prototype.autoAdaptExtensions = ['.html', '.htm']; HttpServer.prototype.kill = function(callback){ this.connections.forEach(function(connection){ connection.destroy(connection); }); delete this.expressInstance; this.httpServer.close(function(){ callback(); }); }; HttpServer.prototype.getResource = function(request, response){ var self = this, groupId = request.params.groupId, group = this.resourceMap[groupId], resourcePath, filePath; // HEAD requests must not return a body, OPTIONS is not handled by this server if(request.route.method === "options" || request.route.method === "head"){ return response.send(200); } if(group === void 0){ this.log("[HTTP Server] Couldn't find requested group: " + groupId + "\n"); return response.send(404); } // Remove the junk for group routing, decode it as it may contain encoded characters resourcePath = decodeURI(request.path.replace('/g/' + groupId + '/', '')); filePath = group[resourcePath]; if(filePath === void 0){ // Check to see if the file is contained in one of the directories // served for this group utils.each(group, function(value, key){ if(~resourcePath.indexOf(key)){ filePath = path.join(value, resourcePath.replace(key, "")); } }); // We did our best and still can't find the file. if(filePath === void 0){ this.log("[HTTP Server] Couldn't find requested file path: " + resourcePath + "\n"); return response.send(404); } } fs.exists(filePath, function(exists){ if(exists){ if(!self.autoAdapt) return response.sendfile(filePath); // We're going to see if this is an html-like file. If it is, we're going to // see if it has a testing library that we're familiar with. If it does, and // it doesn't have a thrill adapter, we're going to add it in there. This is // a relatively heavy operation, but it makes thrill super easy to use and //can be disabled with a switch (autoAdapt). if(~self.autoAdaptExtensions.indexOf(path.extname(filePath))){ serveAdaptedHtmlFile(request, response, filePath); } else { response.sendfile(filePath); } } else { // The file doesn't exist response.send(404); } }); }; // Precompute the regexes used to find library and adapters in html-like files var adaptableLibraryRegexMap = utils.map(adapters, function(path, name){ return new RegExp('<script.*src=[\'"]?.*' + name + '.*\\.js[\'"].*?>.*</.*script.*>', 'im'); }); var adapterRegexMap = utils.map(adapters, function(path, name){ return new RegExp('<script.*src=[\'"]?.*thrill-' + name + '-adapter.*\\.js[\'"].*?>.*</.*script.*>', 'im'); }); // Internal helper which adapts a given file path to thrill function serveAdaptedHtmlFile(request, response, filePath){ var self = this; fs.readFile(filePath, function(err, data){ if(err) return response.send(500); var waitingOn = 0; // Keeps track of how many async ops we're waiting on. data = data.toString(); // Try to find files for each adaptable library utils.each(adaptableLibraryRegexMap, function(regex, name){ var match = data.match(regex); if(!match) return; // If an adapter already exists on this page, don't add another. var adapterRegex = adapterRegexMap[name]; if(data.match(adapterRegex)) return; waitingOn++; fs.readFile(adapters[name], function(err, adapterScript){ waitingOn--; if(err){ self.log("[HTTP Server] Error in reading adapter script: " + err); } else { var injectPosition = match.index + match[0].length; data = data.substr(0, injectPosition) + "<script>" + adapterScript + "</script>" + data.substr(injectPosition); } // Once all adapter scripts had a chance to be injected // send the response if(waitingOn === 0){ response.send(data); } }); }); // If there were no matched, waiting on will be 0 // in which case we can serve the original file if(waitingOn === 0){ response.send(data); } }); } HttpServer.prototype.serve = function(pathMap, run){ var self = this, runUrls, resourceMap = this.resourceMap, runIsString = typeof run === "string", groupId = generateId(), uid = 0, groupUrl = "http://" + self.host + "/g/" + groupId + "/", groupResources = resourceMap[groupId] = {}; // This function serves a file from the root group "directory" function serveFile(filePath){ var baseName, resourcePath; // Skip if this if the file is already a capable of being served by a browser if(~filePath.indexOf('://')) return filePath; filePath = path.resolve(filePath); resourcePath = "s"+ (uid++) + "_" + path.basename(filePath); groupResources[resourcePath] = filePath; self.debug('[HTTP Server] Serving ' + filePath + " as " + resourcePath + "\n"); return groupUrl + resourcePath; } var runFileIsServed = false; utils.each(pathMap, function(value, key){ var relativePath, filePath = value; groupResources[key] = filePath; self.debug('[HTTP Server] Serving ' + filePath + " as " + (key ? key : "root") + "\n"); // If the run file is a string, and shares it's path with a // directory we're serving, then we don't need to serve it separately if(runIsString && run.indexOf(value) === 0){ var file = fs.statSync(filePath); if(file.isDirectory()){ run = run.replace(value, '').replace(/\\/g,'/'); if(run[0] === "/") run = run.substr(1); runFileIsServed = true; } } }); if(runIsString){ if(runFileIsServed){ // One of our served directories contains the run file runUrls = groupUrl + run; } else { runUrls = serveFile(run); // Since this is assumed to be an html file, we need to make the "root" // directory that is served the base directory of this file. if(!('' in groupResources)) groupResources[''] = path.dirname(run); } } else {// if it's a list of files, serve them up runUrls = run.map(serveFile); } return { groupId: groupId, runUrls: runUrls }; }; HttpServer.prototype.stopServing = function(groupId){ if(groupId in this.resourceMap){ delete this.resourceMap[groupId]; } };