diff --git a/lib/acp.js b/lib/acp.js index fe702ab2..26756fa8 100644 --- a/lib/acp.js +++ b/lib/acp.js @@ -131,7 +131,7 @@ module.exports = function (Server) { }); user.socket.on("acp-channel-unload", function(data) { - if(Server.channelLoaded(data.name)) { + if(Server.isChannelLoaded(data.name)) { var c = Server.getChannel(data.name); if(!c) return; @@ -146,7 +146,7 @@ module.exports = function (Server) { // At this point c should be unloaded // if it's still loaded, kill it - if(Server.channelLoaded(data.name)) + if(Server.isChannelLoaded(data.name)) Server.unloadChannel(Server.getChannel(data.name)); } }); diff --git a/lib/api.js b/lib/api.js index 13c4252a..f1661c9f 100644 --- a/lib/api.js +++ b/lib/api.js @@ -51,7 +51,7 @@ module.exports = function (Server) { return data; } - var app = Server.app; + var app = Server.express; var db = Server.db; /* */ diff --git a/lib/channel.js b/lib/channel.js index 30854cf6..dc3033f3 100644 --- a/lib/channel.js +++ b/lib/channel.js @@ -1028,7 +1028,7 @@ Channel.prototype.sendAll = function(message, data) { return; this.server.io.sockets.in(this.name).emit(message, data); if (this.server.cfg["enable-ssl"]) - this.server.sslio.sockets.in(this.name).emit(message, data); + this.server.ioSecure.sockets.in(this.name).emit(message, data); } Channel.prototype.sendAllWithPermission = function(perm, msg, data) { diff --git a/lib/database.js b/lib/database.js index 0cb13919..e49a48ca 100644 --- a/lib/database.js +++ b/lib/database.js @@ -5,8 +5,9 @@ var $util = require("./utilities"); var Logger = require("./logger"); var Database = function (cfg) { - this.cfg = cfg; - this.pool = mysql.createPool({ + var self = this; + self.cfg = cfg; + self.pool = mysql.createPool({ host: cfg["mysql-server"], user: cfg["mysql-user"], password: cfg["mysql-pw"], @@ -15,16 +16,16 @@ var Database = function (cfg) { }); // Test the connection - this.pool.getConnection(function (err, conn) { + self.pool.getConnection(function (err, conn) { if(err) { Logger.errlog.log("! DB connection failed"); return; + } else { + self.init(); } - - }); - this.global_ipbans = {}; + self.global_ipbans = {}; }; Database.prototype.query = function (query, sub, callback) { diff --git a/lib/server.js b/lib/server.js index e010215e..fe27063f 100644 --- a/lib/server.js +++ b/lib/server.js @@ -7,23 +7,152 @@ var Config = require("./config"); var Logger = require("./logger"); var Channel = require("./channel"); var User = require("./user"); +var $util = require("./utilities"); const VERSION = "2.4.3"; -function getIP(req) { + +var Server = function (cfg) { + var self = this; + + self.cfg = cfg; + self.channels = [], + self.express = null; + self.http = null; + self.https = null; + self.io = null; + self.ioWeb = null; + self.ioSecure = null; + self.ipCount = {}; + self.db = null; + self.acp = null; + self.api = null; + self.announcement = null; + self.httplog = null; + self.actionlog = null; + self.infogetter = null; + + // database init ------------------------------------------------------ + var Database = require("./database"); + self.db = new Database(self.cfg); + // TODO self.db.init(); + self.actionlog = require("./actionlog")(self); + self.httplog = new Logger.Logger(path.join(__dirname, + "../httpaccess.log")); + // webserver init ----------------------------------------------------- + self.express = express(); + self.express.use(express.bodyParser()); + + // channel route + self.express.get("/r/:channel(*)", function (req, res, next) { + var c = req.params.channel; + if (!$util.isValidChannelName(c)) { + res.redirect("/" + c); + return; + } + + self.logHTTP(req); + res.sendfile("channel.html", { + root: path.join(__dirname, "../www") + }); + }); + + // api route + self.api = require("./api")(self); + + // default route + self.express.get("/:thing(*)", function (req, res, next) { + var opts = { + root: path.join(__dirname, "../www"), + maxAge: self.cfg["asset-cache-ttl"] + }; + + res.sendfile(req.params.thing, opts, function (e) { + if (e) { + self.logHTTP(req, e.status); + if (req.params.thing.match(/\.\.|(%25)?%2e(%25)?%2e/)) { + res.send("Don't try that again."); + Logger.syslog.log("WARNING: Attempted path traversal "+ + "from IP " + self.getHTTPIP(req)); + Logger.syslog.log("Path was: " + req.url); + self.actionlog.record(self.getHTTPIP(req), "", + "path-traversal", + req.url); + } else if (e.status >= 500) { + Logger.errlog.log(err); + } + res.send(e.status); + } else { + self.logHTTP(req); + } + }); + }); + + // fallback route + self.express.use(function (err, req, res, next) { + self.logHTTP(req, err.status); + if (err.status === 404) { + res.send(404); + } else { + next(err); + } + }); + + // http/https/sio server init ----------------------------------------- + if (self.cfg["enable-ssl"]) { + var key = fs.readFileSync(path.resolve(__dirname, "..", + self.cfg["ssl-keyfile"])); + var cert = fs.readFileSync(path.resolve(__dirname, "..", + self.cfg["ssl-certfile"])); + var opts = { + key: key, + cert: cert, + passphrase: self.cfg["ssl-passphrase"] + }; + + self.https = https.createServer(opts, self.express) + .listen(self.cfg["ssl-port"]); + self.ioSecure = require("socket.io").listen(self.https); + self.ioSecure.set("log level", 1); + self.ioSecure.on("connection", function (sock) { + self.handleSocketConnection(sock); + }); + } + + self.http = self.express.listen(self.cfg["web-port"], + self.cfg["express-host"]); + self.ioWeb = express().listen(self.cfg["io-port"], self.cfg["io-host"]); + self.io = require("socket.io").listen(self.ioWeb); + self.io.set("log level", 1); + self.io.sockets.on("connection", function (sock) { + self.handleSocketConnection(sock); + }); + + // acp init ----------------------------------------------------------- + self.acp = require("./acp")(self); + + // background tasks init ---------------------------------------------- + require("./bgtask")(self); + + // media metadata retriever init -------------------------------------- + // TODO make the constructor just accept cfg? + self.infogetter = require("./get-info")(self); +}; + +Server.prototype.getHTTPIP = function (req) { var raw = req.connection.remoteAddress; var forward = req.header("x-forwarded-for"); - if((Server.cfg["trust-x-forward"] || raw === "127.0.0.1") && forward) { + if((this.cfg["trust-x-forward"] || raw === "127.0.0.1") && forward) { var ip = forward.split(",")[0]; Logger.syslog.log("REVPROXY " + raw + " => " + ip); return ip; } return raw; -} +}; -function getSocketIP(socket) { +Server.prototype.getSocketIP = function (socket) { var raw = socket.handshake.address.address; - if(Server.cfg["trust-x-forward"] || raw === "127.0.0.1") { + if(this.cfg["trust-x-forward"] || raw === "127.0.0.1") { if(typeof socket.handshake.headers["x-forwarded-for"] == "string") { var ip = socket.handshake.headers["x-forwarded-for"] .split(",")[0]; @@ -32,237 +161,131 @@ function getSocketIP(socket) { } } return raw; -} +}; -var Server = { - channels: [], - channelLoaded: function (name) { - for(var i in this.channels) { - if(this.channels[i].canonical_name == name.toLowerCase()) - return true; - } - return false; - }, - getChannel: function (name) { - for(var i in this.channels) { - if(this.channels[i].canonical_name == name.toLowerCase()) - return this.channels[i]; +Server.prototype.isChannelLoaded = function (name) { + name = name.toLowerCase(); + for (var i = 0; i < this.channels.length; i++) { + if (this.channels[i].canonical_name == name) + return true; + } + return false; +}; + +Server.prototype.getChannel = function (name) { + var cname = name.toLowerCase(); + for (var i = 0; i < this.channels.length; i++) { + if (this.channels[i].canonical_name == name) + return this.channels[i]; + } + + var c = new Channel(name, this); + this.channels.push(c); + return c; +}; + +Server.prototype.unloadChannel = function (chan) { + if (chan.registered) + chan.saveDump(); + + chan.playlist.die(); + chan.logger.close(); + for (var i = 0; i < this.channels.length; i++) { + if (this.channels[i].canonical_name === chan.canonical_name) { + this.channels.splice(i, 1); + i--; } + } - var c = new Channel(name, this); - this.channels.push(c); - return c; - }, - unloadChannel: function(chan) { - if(chan.registered) - chan.saveDump(); - chan.playlist.die(); - chan.logger.close(); - for(var i in this.channels) { - if(this.channels[i].canonical_name == chan.canonical_name) { - this.channels.splice(i, 1); - break; - } - } - var keys = Object.keys(chan); - for (var i in keys) { - delete chan[keys[i]]; - } - chan.dead = true; - }, - app: null, - io: null, - httpserv: null, - sslserv: null, - sslio: null, - ioserv: null, - db: null, - ips: {}, - acp: null, - announcement: null, - httpaccess: null, - actionlog: null, - logHTTP: function (req, status) { - if(status === undefined) - status = 200; - var ip = req.connection.remoteAddress; - var ip2 = false; - if(this.cfg["trust-x-forward"]) - ip2 = req.header("x-forwarded-for") || req.header("cf-connecting-ip"); - var ipstr = !ip2 ? ip : ip + " (X-Forwarded-For " + ip2 + ")"; - var url = req.url; - // Remove query - if(url.indexOf("?") != -1) - url = url.substring(0, url.lastIndexOf("?")); - this.httpaccess.log([ipstr, req.method, url, status, req.headers["user-agent"]].join(" ")); - }, - handleIOConnection: function (socket) { - var self = this; - var ip = getSocketIP(socket); - socket._ip = ip; - self.db.isGlobalIPBanned(ip, function (err, bant) { - if(bant) { - Logger.syslog.log("Disconnecting " + ip + " - gbanned"); - socket.emit("kick", { - reason: "You're globally banned." - }); - socket.disconnect(true); - } - }); + // Empty all outward references from the channel + var keys = Object.keys(chan); + for (var i in keys) { + delete chan[keys[i]]; + } + chan.dead = true; +}; - socket.on("disconnect", function () { - self.ips[ip]--; - }.bind(self)); +Server.prototype.logHTTP = function (req, status) { + if (status === undefined) + status = 200; - if(!(ip in self.ips)) - self.ips[ip] = 0; - self.ips[ip]++; + var ip = req.connection.remoteAddress; + var ip2 = req.header("x-forwarded-for") || + req.header("cf-connecting-ip"); + var ipstr = !ip2 ? ip : ip + " (X-Forwarded-For " + ip2 + ")"; + var url = req.url; + // Remove query + if(url.indexOf("?") != -1) + url = url.substring(0, url.lastIndexOf("?")); + this.httplog.log([ + ipstr, + req.method, + url, + status, + req.header("user-agent") + ].join(" ")); +}; - if(self.ips[ip] > Server.cfg["ip-connection-limit"]) { - socket.emit("kick", { - reason: "Too many connections from your IP address" - }); +Server.prototype.handleSocketConnection = function (socket) { + var self = this; + var ip = self.getSocketIP(socket); + socket._ip = ip; + + // Check for global ban on the IP + self.db.isGlobalIPBanned(ip, function (err, banned) { + if (banned) { + Logger.syslog.log("Disconnecting " + ip + " - global banned"); + socket.emit("kick", { reason: "Your IP is globally banned." }); socket.disconnect(true); - return; } + }); - // finally a valid user - Logger.syslog.log("Accepted socket from /" + socket._ip); - new User(socket, self); + socket.on("disconnect", function () { + self.ipCount[ip]--; + }); + + if (!(ip in self.ipCount)) + self.ipCount[ip] = 0; + + self.ipCount[ip]++; + if (self.ipCount[ip] > self.cfg["ip-connection-limit"]) { + socket.emit("kick", { + reason: "Too many connections from your IP address" + }); + socket.disconnect(true); + return; + } + + Logger.syslog.log("Accepted socket from " + ip); + new User(socket, self); +}; + +Server.prototype.shutdown = function () { + Logger.syslog.log("Unloading channels"); + for (var i = 0; i < this.channels.length; i++) { + if (this.channels[i].registered) { + Logger.syslog.log("Saving /r/" + this.channels[i].name); + this.channels[i].saveDump(); + } + } + Logger.syslog.log("Goodbye"); + process.exit(0); +}; + +var singleton = null; + +module.exports = { + init: function (cfg) { + singleton = new Server(cfg); + return singleton; }, - init: function () { - var self = this; - // init database - var Database = require("./database"); - this.db = new Database(self.cfg); - this.db.init(); - this.actionlog = require("./actionlog")(self); - this.httpaccess = new Logger.Logger(path.join(__dirname, - "../httpaccess.log")); - this.app = express(); - this.app.use(express.bodyParser()); - // channel path - self.app.get("/r/:channel(*)", function (req, res, next) { - var c = req.params.channel; - if(!c.match(/^[\w-_]+$/)) { - res.redirect("/" + c); - } - else { - self.logHTTP(req); - res.sendfile("channel.html", { - root: path.join(__dirname, "../www") - }); - } - }); - // api path - self.api = require("./api")(self); - - self.app.get("/", function (req, res, next) { - self.logHTTP(req); - res.sendfile("index.html", { - root: path.join(__dirname, "../www") - }); - }); - - // default path - self.app.get("/:thing(*)", function (req, res, next) { - var opts = { - root: path.join(__dirname, "../www"), - maxAge: self.cfg["asset-cache-ttl"] - } - res.sendfile(req.params.thing, opts, function (err) { - if(err) { - self.logHTTP(req, err.status); - // Damn path traversal attacks - if(req.params.thing.indexOf("%2e") != -1) { - res.send("Don't try that again, I'll ban you"); - Logger.syslog.log("WARNING: Attempted path "+ - "traversal from /" + getIP(req)); - Logger.syslog.log("URL: " + req.url); - } - // Something actually went wrong - else { - // Status codes over 500 are server errors - if(err.status >= 500) - Logger.errlog.log(err); - res.send(err.status); - } - } - else { - self.logHTTP(req); - } - }); - }); - - // fallback - self.app.use(function (err, req, res, next) { - self.logHTTP(req, err.status); - if(err.status == 404) { - res.send(404); - } else { - next(err); - } - }); - - // bind servers - if (self.cfg["enable-ssl"]) { - var key = fs.readFileSync(path.resolve(__dirname, "..", - self.cfg["ssl-keyfile"])); - var cert = fs.readFileSync(path.resolve(__dirname, "..", - self.cfg["ssl-certfile"])); - - var options = { - key: key, - passphrase: self.cfg["ssl-passphrase"], - cert: cert - }; - - self.sslserv = https.createServer(options, self.app) - .listen(self.cfg["ssl-port"]); - self.sslio = require("socket.io").listen(self.sslserv); - self.sslio.set("log level", 1); - self.sslio.sockets.on("connection", function (socket) { - self.handleIOConnection(socket); - }); - } - self.httpserv = self.app.listen(Server.cfg["web-port"], - Server.cfg["express-host"]); - self.ioserv = express().listen(Server.cfg["io-port"], - Server.cfg["io-host"]); - - // init socket.io - self.io = require("socket.io").listen(self.ioserv); - self.io.set("log level", 1); - self.io.sockets.on("connection", function (socket) { - self.handleIOConnection(socket); - }); - - - // init ACP - self.acp = require("./acp")(self); - - // init background tasks - require("./bgtask")(self); - - // init media retriever - self.infogetter = require("./get-info")(self); - }, - shutdown: function () { - Logger.syslog.log("Unloading channels"); - for(var i in this.channels) { - if(this.channels[i].registered) { - Logger.syslog.log("Saving /r/" + this.channels[i].name); - this.channels[i].saveDump(); - } - } - Logger.syslog.log("Goodbye"); - process.exit(0); + getServer: function () { + return singleton; } }; Logger.syslog.log("Starting CyTube v" + VERSION); - var chanlogpath = path.join(__dirname, "../chanlogs"); fs.exists(chanlogpath, function (exists) { exists || fs.mkdir(chanlogpath); @@ -273,16 +296,17 @@ fs.exists(chandumppath, function (exists) { exists || fs.mkdir(chandumppath); }); -Config.load(Server, path.join(__dirname, "../cfg.json"), function () { - Server.init(); - if(!Server.cfg["debug"]) { +var x = {}; +Config.load(x, path.join(__dirname, "../cfg.json"), function () { + module.exports.init(x.cfg); + if(!singleton.cfg["debug"]) { process.on("uncaughtException", function (err) { Logger.errlog.log("[SEVERE] Uncaught Exception: " + err); Logger.errlog.log(err.stack); }); process.on("SIGINT", function () { - Server.shutdown(); + singleton.shutdown(); }); } });