Mini Kabibi Habibi
/*
* Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
(function () {
"use strict";
/** @define{number} Number of ms to wait for the server to start */
var SETUP_TIMEOUT = 5000; // wait up to 5 seconds for server to start
var url = require("url"),
http = require("http"),
events = require("events"),
util = require("util"),
Q = require("q"),
WebSocket = require("ws"),
ConnectionManager = require("./ConnectionManager"),
DomainManager = require("./DomainManager");
/**
* @constructor
* The Server module is a singleton object that manages both the
* connection to the parent process (over stdin/stdout) and to clients
* over WebSockets.
*
* Server inherits from the EventEmitter class and exports itself
* as the module.
*
* @param {Generator} generator
* @param {Logger} logger
* @param {string=} origin Optional origin string used to verify websocket client connections
*/
var Server = function (generator, logger, origin) {
events.EventEmitter.call(this);
this._generator = generator;
this._logger = logger;
this._origin = origin;
this._connectionManager = new ConnectionManager(logger);
this._domainManager = new DomainManager(this._generator, this._logger, this._connectionManager);
this._httpRequestHandler = this._httpRequestHandler.bind(this);
this._setupHttpAndWebSocketServers = this._setupHttpAndWebSocketServers.bind(this);
};
util.inherits(Server, events.EventEmitter);
/**
* @private
* @type{http.Server} the HTTP server
*/
Server.prototype._httpServer = null;
/**
* @private
* @type{ws.WebSocketServer} the WebSocket server
*/
Server.prototype._wsServer = null;
Server.prototype._httpRequestHandler = function (req, res) {
if (req.method === "GET") {
if (req.url === "/api" || req.url.indexOf("/api/") === 0) {
if (req.headers.origin) {
var allowed;
if (req.headers.origin === "null" || req.headers.origin === "file://") {
allowed = req.headers.origin;
} else {
// Allow requests from localhost on any port
var origin = url.parse(req.headers.origin),
port = origin.port || 80,
host = origin.hostname === "localhost" ? "localhost" : "127.0.0.1";
allowed = origin.protocol + "//" + host + ":" + port;
}
res.setHeader("Access-Control-Allow-Origin", allowed);
}
res.setHeader("Content-Type", "application/json");
res.end(
JSON.stringify(this._domainManager.getDomainDescriptions(),
null,
4)
);
} else {
res.setHeader("Content-Type", "text/plain");
res.end("Generator RPC Server\n");
}
} else { // Not a GET request
res.statusCode = 501;
res.end();
}
};
Server.prototype._setupHttpAndWebSocketServers = function (port, callback, timeout) {
var timeoutTimer = null;
var httpServer = null;
if (timeout) {
timeoutTimer = setTimeout(function () {
callback("ERR_TIMEOUT", null);
}, timeout);
}
httpServer = http.createServer(this._httpRequestHandler);
httpServer.on("error", function () {
if (callback) {
callback("ERR_CREATE_SERVER", null);
}
});
httpServer.listen(port, "127.0.0.1", function () {
var wsServer = null;
var address = httpServer.address();
if (address !== null) {
httpServer.removeAllListeners("error");
httpServer.on("error", function () {
this._logger.error("stopping due to HTTP error",
arguments);
this.stop();
}.bind(this));
var wsOptions = {
server: httpServer
};
// Verify that the request's Origin header matches the specified origin
if (this._origin) {
wsOptions.verifyClient = function (info) {
return info.origin === this._origin;
}.bind(this);
}
wsServer = new WebSocket.Server(wsOptions);
wsServer.on("error", function () {
this._logger.error(
"stopping due to WebSocket error",
arguments
);
this.stop();
}.bind(this));
wsServer.on("connection",
this._connectionManager.createConnection
.bind(this._connectionManager, this._domainManager));
if (timeoutTimer) {
clearTimeout(timeoutTimer);
}
callback(null, {
httpServer : httpServer,
wsServer : wsServer,
port : address.port
});
} else {
// address is null
// this shouldn't happen, because if we didn't get a socket
// we wouldn't have called this callback
if (timeoutTimer) {
clearTimeout(timeoutTimer);
}
if (callback) {
callback("ERR_UNKNOWN", null);
}
}
}.bind(this));
};
/**
* Stops the server and does appropriate cleanup.
* Emits an "end" event when shutdown is complete.
*/
Server.prototype.stop = function () {
if (this._wsServer) {
try {
this._wsServer.close();
} catch (err1) {
this._logger.warn("Error while closing websocket server: " + err1.message, err1.stack);
}
}
if (this._httpServer) {
try {
this._httpServer.close();
} catch (err2) {
this._logger.warn("Error while closing http server: " + err2.message, err2.stack);
}
}
this._connectionManager.closeAllConnections();
this.emit("end");
this.removeAllListeners();
};
/**
* Starts the server.
*
* @param {number=} port Optional port parameter
*/
Server.prototype.start = function (port) {
var deferred = Q.defer();
// Do initialization
this._setupHttpAndWebSocketServers(port, function (err, servers) {
if (err) {
this._logger.error(
"stopping due to error while starting http/ws servers: " + err
);
this.stop();
deferred.reject(err);
return;
}
this._httpServer = servers.httpServer;
this._wsServer = servers.wsServer;
deferred.resolve(servers.port);
}.bind(this), SETUP_TIMEOUT);
this._domainManager.loadDomainModulesFromPaths(["./BaseDomain"]);
return deferred.promise;
};
/**
* Register a domain within this server
*
* @param {Module} domain
* @return {Promise} Resolves when registration is complete
*/
Server.prototype.registerDomain = function (domain) {
this._domainManager.loadDomainModule(domain);
return Q.resolve();
};
module.exports = Server;
}());