diff --git a/changelog b/changelog index 7a58a285..1f9ce5e8 100644 --- a/changelog +++ b/changelog @@ -1,3 +1,13 @@ +Wed Sep 18 18:26 2013 CDT + * lib/user.js: Change channel checks to include checking for whether + the channel is dead + +Wed Sep 18 18:14 2013 CDT + * lib/channel.js: Add a bunch of checks to prevent callbacks from doing + things with a dead channel + * tests/channelDeadRace.js: Add a few client tests that cause exceptions + in the pre-patched channel code + Tue Sep 17 22:24 2013 CDT * lib/user.js: Fix what I assume was a race condition that caused an error message when a user's login callback fired after the channel unloaded. diff --git a/lib/channel.js b/lib/channel.js index e894a024..352d14d5 100644 --- a/lib/channel.js +++ b/lib/channel.js @@ -122,7 +122,7 @@ var Channel = function(name, Server) { Server.db.loadChannelData(self, function (err) { if (err && err === "channel_dead") - return; + return; else if (!err || err === "channel_unregistered") self.dbloaded = true; @@ -214,11 +214,12 @@ Channel.prototype.loadDump = function() { } // Current else if(data.playlist) { - var chan = self; self.playlist.load(data.playlist, function() { - chan.sendAll("playlist", chan.playlist.items.toArray()); - chan.broadcastPlaylistMeta(); - chan.playlist.startPlayback(data.playlist.time); + if (self.dead) + return; + self.sendAll("playlist", self.playlist.items.toArray()); + self.broadcastPlaylistMeta(); + self.playlist.startPlayback(data.playlist.time); }); } for(var key in data.opts) { @@ -270,7 +271,7 @@ Channel.prototype.loadDump = function() { self.js = data.js || ""; self.sendAll("channelCSSJS", {css: self.css, js: self.js}); self.initialized = true; - setTimeout(function() { incrementalDump(self); }.bind(self), 300000); + setTimeout(function() { incrementalDump(self); }, 300000); } catch(e) { Logger.errlog.log("Channel dump load failed: "); @@ -281,6 +282,8 @@ Channel.prototype.loadDump = function() { } Channel.prototype.saveDump = function() { + if (this.dead) + return; if(!this.initialized || this.name === "") return; var filts = new Array(this.filters.length); @@ -306,7 +309,7 @@ Channel.prototype.saveDump = function() { // Save channel dumps every 5 minutes, in case of crash function incrementalDump(chan) { - if(chan && chan.users && chan.users.length > 0) { + if(!chan.dead && chan.users && chan.users.length > 0) { chan.saveDump(); setTimeout(function() { incrementalDump(chan); }, 300000); } @@ -415,6 +418,8 @@ Channel.prototype.tryRegister = function (user) { self.server.actionlog.record(user.ip, user.name, "channel-register-success", self.name); + if (self.dead) + return; self.registered = true; self.initialized = true; self.saveDump(); @@ -454,14 +459,18 @@ Channel.prototype.unregister = function (user) { return; } - self.registered = false; user.socket.emit("unregisterChannel", { success: true }); + if (!self.dead) + self.registered = false; }); } Channel.prototype.getRank = function (name, callback) { var self = this; self.server.db.getGlobalRank(name, function (err, global) { + if (self.dead) + return; + if(err) { callback(err, null); return; @@ -495,8 +504,13 @@ Channel.prototype.saveRank = function (user, callback) { Channel.prototype.getIPRank = function (ip, callback) { var self = this; self.server.db.listAliases(ip, function (err, names) { + if (self.dead) + return; self.server.db.listChannelUserRanks(self.name, names, function (err, res) { + if (self.dead) + return; + if(err) { callback(err, null); return; @@ -550,6 +564,8 @@ Channel.prototype.tryNameBan = function(actor, name) { } self.getRank(name, function (err, rank) { + if (self.dead) + return; if(err) { actor.socket.emit("errorMsg", { msg: "Internal error " + err @@ -604,6 +620,8 @@ Channel.prototype.unbanName = function(actor, name) { self.logger.log("*** " + actor.name + " un-namebanned " + name); self.server.db.clearChannelNameBan(self.name, name, function (err, res) { + if (self.dead) + return; self.users.forEach(function(u) { self.sendBanlist(u); @@ -627,6 +645,9 @@ Channel.prototype.tryIPBan = function(actor, name, range) { return; } self.server.db.listIPsForName(name, function (err, ips) { + if (self.dead) + return; + if(err) { actor.socket.emit("errorMsg", { msg: "Internal error: " + err @@ -637,6 +658,9 @@ Channel.prototype.tryIPBan = function(actor, name, range) { if(range) ip = ip.replace(/(\d+)\.(\d+)\.(\d+)\.(\d+)/, "$1.$2.$3"); self.getIPRank(ip, function (err, rank) { + if (self.dead) + return; + if(err) { actor.socket.emit("errorMsg", { msg: "Internal error: " + err @@ -669,6 +693,9 @@ Channel.prototype.tryIPBan = function(actor, name, range) { self.server.db.addChannelBan(self.name, ip, name, actor.name, function (err, res) { + if (self.dead) + return; + var notice = { username: "[server]", msg: actor.name + " banned " + $util.maskIP(ip) + @@ -782,6 +809,8 @@ Channel.prototype.userJoin = function(user) { this.broadcastVoteskipUpdate(); if(user.name != "") { self.getRank(user.name, function (err, rank) { + if (self.dead) + return; if(err) { user.rank = user.global_rank; user.saverank = false; @@ -1343,6 +1372,9 @@ Channel.prototype.addMedia = function(data, user) { : this.opts.maxlength; var postAdd = function (item, cached) { + if (self.dead) + return; + if(item.media.type === "cu" && data.title) { var t = data.title; if(t.length > 100) @@ -1362,6 +1394,9 @@ Channel.prototype.addMedia = function(data, user) { // No need to check library for livestreams - they aren't cached if(isLive(data.type)) { self.playlist.addMedia(data, function (err, data) { + if (self.dead) + return; + if(err) { if(err === true) err = false; @@ -1379,6 +1414,9 @@ Channel.prototype.addMedia = function(data, user) { // Don't search library if the channel isn't registered if(!self.registered) { self.playlist.addMedia(data, function(err, item) { + if (self.dead) + return; + if(err) { if(err === true) err = false; @@ -1393,6 +1431,9 @@ Channel.prototype.addMedia = function(data, user) { } self.server.db.getLibraryItem(self.name, data.id, function (err, item) { + if (self.dead) + return; + if(err) { user.socket.emit("queueFail", "Internal error: " + err); return; @@ -1407,6 +1448,9 @@ Channel.prototype.addMedia = function(data, user) { data.media = m; self.playlist.addCachedMedia(data, function (err, item) { + if (self.dead) + return; + if(err) { if(err === true) err = false; @@ -1419,6 +1463,9 @@ Channel.prototype.addMedia = function(data, user) { }); } else { self.playlist.addMedia(data, function(err, item) { + if (self.dead) + return; + if(err) { if(err === true) err = false; @@ -1438,6 +1485,9 @@ Channel.prototype.addMedia = function(data, user) { Channel.prototype.addMediaList = function(data, user) { var chan = this; this.playlist.addMediaList(data, function(err, item) { + if (chan.dead) + return; + if(err) { if(err === true) err = false; @@ -1477,6 +1527,9 @@ Channel.prototype.tryQueuePlaylist = function(user, data) { self.server.db.getUserPlaylist(user.name, data.name, function (err, pl) { + if (self.dead) + return; + if(err) { user.socket.emit("errorMsg", { msg: "Playlist load failed: " + err @@ -1552,6 +1605,9 @@ Channel.prototype.tryUncache = function(user, data) { } self.server.db.removeFromLibrary(self.name, data.id, function (err, res) { + if (self.dead) + return; + if(err) return; @@ -1662,6 +1718,9 @@ Channel.prototype.tryUpdate = function(user, data) { Channel.prototype.move = function(data, user) { var chan = this; function afterMove() { + if (chan.dead) + return; + var moveby = user && user.name ? user.name : null; if(typeof data.moveby !== "undefined") moveby = data.moveby; @@ -2117,6 +2176,9 @@ Channel.prototype.trySetRank = function(user, data) { receiver.rank = data.rank; if(receiver.loggedIn) { self.saveRank(receiver, function (err, res) { + if (self.dead) + return; + self.logger.log("*** " + user.name + " set " + data.user + "'s rank to " + data.rank); self.sendAllWithPermission("acl", "setChannelRank", data); @@ -2125,6 +2187,9 @@ Channel.prototype.trySetRank = function(user, data) { self.broadcastUserUpdate(receiver); } else if(self.registered) { self.getRank(data.user, function (err, rrank) { + if (self.dead) + return; + if(err) return; if(rrank >= user.rank) @@ -2132,6 +2197,9 @@ Channel.prototype.trySetRank = function(user, data) { self.server.db.setChannelRank(self.name, data.user, data.rank, function (err, res) { + if (self.dead) + return; + self.logger.log("*** " + user.name + " set " + data.user + "'s rank to " + data.rank); self.sendAllWithPermission("acl", "setChannelRank", data); diff --git a/lib/user.js b/lib/user.js index 65cb3b7c..0acfdb6a 100644 --- a/lib/user.js +++ b/lib/user.js @@ -47,6 +47,10 @@ var User = function(socket, Server) { } }; +User.prototype.inChannel = function () { + return this.channel !== null && !this.channel.dead; +}; + // Throttling/cooldown User.prototype.noflood = function(name, hz) { var time = new Date().getTime(); @@ -82,7 +86,7 @@ User.prototype.noflood = function(name, hz) { } User.prototype.setAFK = function (afk) { - if(this.channel === null) + if(!this.inChannel()) return; if(this.meta.afk === afk) return; @@ -105,7 +109,7 @@ User.prototype.autoAFK = function () { if(this.awaytimer) clearTimeout(this.awaytimer); - if(this.channel === null || this.channel.opts.afk_timeout == 0) + if(!this.inChannel() || this.channel.opts.afk_timeout == 0) return; this.awaytimer = setTimeout(function () { @@ -117,12 +121,12 @@ User.prototype.initCallbacks = function() { var self = this; self.socket.on("disconnect", function() { self.awaytimer && clearTimeout(self.awaytimer); - if(self.channel != null) + if(self.inChannel()) self.channel.userLeave(self); }); self.socket.on("joinChannel", function(data) { - if(self.channel != null) + if(self.inChannel()) return; if(typeof data.name != "string") return; @@ -180,49 +184,49 @@ User.prototype.initCallbacks = function() { }); self.socket.on("assignLeader", function(data) { - if(self.channel != null) { + if(self.inChannel()) { self.channel.tryChangeLeader(self, data); } }); self.socket.on("promote", function(data) { - if(self.channel != null) { + if(self.inChannel()) { self.channel.tryPromoteUser(self, data); } }); self.socket.on("demote", function(data) { - if(self.channel != null) { + if(self.inChannel()) { self.channel.tryDemoteUser(self, data); } }); self.socket.on("setChannelRank", function(data) { - if(self.channel != null) { + if(self.inChannel()) { self.channel.trySetRank(self, data); } }); self.socket.on("banName", function(data) { - if(self.channel != null) { + if(self.inChannel()) { self.channel.banName(self, data.name || ""); } }); self.socket.on("banIP", function(data) { - if(self.channel != null) { + if(self.inChannel()) { self.channel.tryIPBan(self, data); } }); self.socket.on("unban", function(data) { - if(self.channel != null) { + if(self.inChannel()) { self.channel.tryUnban(self, data); } }); self.socket.on("chatMsg", function(data) { - if(self.channel != null) { + if(self.inChannel()) { if(data.msg.indexOf("/afk") != 0) { self.setAFK(false); self.autoAFK(); @@ -232,91 +236,91 @@ User.prototype.initCallbacks = function() { }); self.socket.on("newPoll", function(data) { - if(self.channel != null) { + if(self.inChannel()) { self.channel.tryOpenPoll(self, data); } }); self.socket.on("playerReady", function() { - if(self.channel != null) { + if(self.inChannel()) { self.channel.sendMediaUpdate(self); } }); self.socket.on("requestPlaylist", function() { - if(self.channel != null) { + if(self.inChannel()) { self.channel.sendPlaylist(self); } }); self.socket.on("queue", function(data) { - if(self.channel != null) { + if(self.inChannel()) { self.channel.tryQueue(self, data); } }); self.socket.on("setTemp", function(data) { - if(self.channel != null) { + if(self.inChannel()) { self.channel.trySetTemp(self, data); } }); self.socket.on("delete", function(data) { - if(self.channel != null) { + if(self.inChannel()) { self.channel.tryDequeue(self, data); } }); self.socket.on("uncache", function(data) { - if(self.channel != null) { + if(self.inChannel()) { self.channel.tryUncache(self, data); } }); self.socket.on("moveMedia", function(data) { - if(self.channel != null) { + if(self.inChannel()) { self.channel.tryMove(self, data); } }); self.socket.on("jumpTo", function(data) { - if(self.channel != null) { + if(self.inChannel()) { self.channel.tryJumpTo(self, data); } }); self.socket.on("playNext", function() { - if(self.channel != null) { + if(self.inChannel()) { self.channel.tryPlayNext(self); } }); self.socket.on("clearPlaylist", function() { - if(self.channel != null) { + if(self.inChannel()) { self.channel.tryClearqueue(self); } }); self.socket.on("shufflePlaylist", function() { - if(self.channel != null) { + if(self.inChannel()) { self.channel.tryShufflequeue(self); } }); self.socket.on("togglePlaylistLock", function() { - if(self.channel != null) { + if(self.inChannel()) { self.channel.tryToggleLock(self); } }); self.socket.on("mediaUpdate", function(data) { - if(self.channel != null) { + if(self.inChannel()) { self.channel.tryUpdate(self, data); } }); self.socket.on("searchMedia", function(data) { - if(self.channel != null) { + if(self.inChannel()) { if(data.source == "yt") { var searchfn = self.server.infogetter.Getters["ytSearch"]; searchfn(data.query.split(" "), function (e, vids) { @@ -337,19 +341,19 @@ User.prototype.initCallbacks = function() { }); self.socket.on("closePoll", function() { - if(self.channel != null) { + if(self.inChannel()) { self.channel.tryClosePoll(self); } }); self.socket.on("vote", function(data) { - if(self.channel != null) { + if(self.inChannel()) { self.channel.tryVote(self, data); } }); self.socket.on("registerChannel", function(data) { - if(self.channel == null) { + if(!self.inChannel()) { self.socket.emit("channelRegistration", { success: false, error: "You're not in any channel!" @@ -361,80 +365,80 @@ User.prototype.initCallbacks = function() { }); self.socket.on("unregisterChannel", function() { - if(self.channel == null) { + if(!self.inChannel()) { return; } self.channel.unregister(self); }); self.socket.on("setOptions", function(data) { - if(self.channel != null) { + if(self.inChannel()) { self.channel.tryUpdateOptions(self, data); } }); self.socket.on("setPermissions", function(data) { - if(self.channel != null) { + if(self.inChannel()) { self.channel.tryUpdatePermissions(self, data); } }); self.socket.on("setChannelCSS", function(data) { - if(self.channel != null) { + if(self.inChannel()) { self.channel.trySetCSS(self, data); } }); self.socket.on("setChannelJS", function(data) { - if(self.channel != null) { + if(self.inChannel()) { self.channel.trySetJS(self, data); } }); self.socket.on("updateFilter", function(data) { - if(self.channel != null) { + if(self.inChannel()) { self.channel.tryUpdateFilter(self, data); } }); self.socket.on("removeFilter", function(data) { - if(self.channel != null) { + if(self.inChannel()) { self.channel.tryRemoveFilter(self, data); } }); self.socket.on("moveFilter", function(data) { - if(self.channel != null) { + if(self.inChannel()) { self.channel.tryMoveFilter(self, data); } }); self.socket.on("setMotd", function(data) { - if(self.channel != null) { + if(self.inChannel()) { self.channel.tryUpdateMotd(self, data); } }); self.socket.on("requestLoginHistory", function() { - if(self.channel != null) { + if(self.inChannel()) { self.channel.sendLoginHistory(self); } }); self.socket.on("requestBanlist", function() { - if(self.channel != null) { + if(self.inChannel()) { self.channel.sendBanlist(self); } }); self.socket.on("requestChatFilters", function() { - if(self.channel != null) { + if(self.inChannel()) { self.channel.sendChatFilters(self); } }); self.socket.on("requestChannelRanks", function() { - if(self.channel != null) { + if(self.inChannel()) { if(self.noflood("requestChannelRanks", 0.25)) return; self.channel.sendChannelRanks(self); @@ -442,7 +446,7 @@ User.prototype.initCallbacks = function() { }); self.socket.on("voteskip", function(data) { - if(self.channel != null) { + if(self.inChannel()) { self.channel.tryVoteskip(self); } }); @@ -477,7 +481,7 @@ User.prototype.initCallbacks = function() { return; } - if(self.channel == null) { + if(!self.inChannel()) { self.socket.emit("savePlaylist", { success: false, error: "Not in a channel" @@ -519,7 +523,7 @@ User.prototype.initCallbacks = function() { }); self.socket.on("queuePlaylist", function(data) { - if(self.channel != null) { + if(self.inChannel()) { self.channel.tryQueuePlaylist(self, data); } }); @@ -546,7 +550,7 @@ User.prototype.initCallbacks = function() { }); self.socket.on("readChanLog", function () { - if(self.channel !== null) { + if(self.inChannel()) { self.channel.tryReadLog(self); } }); @@ -564,7 +568,7 @@ User.prototype.initCallbacks = function() { self.rank = rank; self.socket.emit("rank", rank); - if(self.channel != null) + if(self.inChannel()) self.channel.broadcastUserUpdate(self); }); @@ -615,7 +619,7 @@ User.prototype.login = function(name, pw, session) { return; } - if(self.channel != null) { + if(self.inChannel()) { for(var i = 0; i < self.channel.users.length; i++) { if(self.channel.users[i].name == name) { self.socket.emit("login", { @@ -637,7 +641,7 @@ User.prototype.login = function(name, pw, session) { name: name }); self.socket.emit("rank", self.rank); - if(self.channel != null) { + if(self.inChannel()) { self.channel.logger.log(self.ip + " signed in as " + name); self.channel.broadcastNewUser(self); } @@ -654,7 +658,7 @@ User.prototype.login = function(name, pw, session) { }); return; } - if(self.channel !== null && !self.channel.dead) { + if(self.inChannel()) { for(var i = 0; i < self.channel.users.length; i++) { if(self.channel.users[i].name.toLowerCase() == name.toLowerCase()) { if (self.channel.users[i] == self) { @@ -685,13 +689,13 @@ User.prototype.login = function(name, pw, session) { var afterRankLookup = function () { self.socket.emit("rank", self.rank); self.name = name; - if(self.channel != null) { + if(self.inChannel()) { self.channel.logger.log(self.ip + " logged in as " + name); self.channel.broadcastNewUser(self); } }; - if(self.channel !== null) { + if(self.inChannel()) { self.channel.getRank(name, function (err, rank) { if(!err) { self.saverank = true; diff --git a/tests/channelDeadRace.js b/tests/channelDeadRace.js new file mode 100644 index 00000000..139adf74 --- /dev/null +++ b/tests/channelDeadRace.js @@ -0,0 +1,34 @@ +var io = require('socket.io-client'); + +function testLogin() { + var socket = io.connect('http://localhost:1337'); + socket.on('connect', function () { + socket.emit('login', { name: 'test', pw: 'test' }); + socket.emit('joinChannel', { name: 'test' }); + socket.disconnect(); + }); +} + +function testBan() { + var socket = io.connect('http://localhost:1337'); + socket.on('connect', function () { + socket.emit('login', { name: 'test', pw: 'test' }); + socket.emit('joinChannel', { name: 'test' }); + socket.emit('chatMsg', { msg: '/ban asdf' }); + socket.disconnect(); + }); +} + +function testRankChange() { + var socket = io.connect('http://localhost:1337'); + socket.on('connect', function () { + socket.emit('login', { name: 'test', pw: 'test' }); + socket.emit('joinChannel', { name: 'test' }); + socket.emit('setChannelRank', { user: 'test2', rank: 2 }); + socket.disconnect(); + }); +} + +testLogin(); +testBan(); +testRankChange();