diff --git a/lib/channel/channel.js b/lib/channel/channel.js index 843b4261..da9b6028 100644 --- a/lib/channel/channel.js +++ b/lib/channel/channel.js @@ -163,10 +163,7 @@ Channel.prototype.loadState = function () { var errorLoad = function (msg) { if (self.modules.customization) { self.modules.customization.load({ - motd: { - motd: msg, - html: msg - } + motd: msg }); } diff --git a/lib/channel/chat.js b/lib/channel/chat.js index de63b83c..24ebc299 100644 --- a/lib/channel/chat.js +++ b/lib/channel/chat.js @@ -482,7 +482,8 @@ ChatModule.prototype.handleCmdMute = function (user, msg, meta) { return; } - if (target.account.effectiveRank >= user.account.effectiveRank) { + if (target.account.effectiveRank >= user.account.effectiveRank + || target.account.globalRank > user.account.globalRank) { user.socket.emit("errorMsg", { msg: "/mute failed - " + target.getName() + " has equal or higher rank " + "than you." @@ -531,7 +532,8 @@ ChatModule.prototype.handleCmdSMute = function (user, msg, meta) { return; } - if (target.account.effectiveRank >= user.account.effectiveRank) { + if (target.account.effectiveRank >= user.account.effectiveRank + || target.account.globalRank > user.account.globalRank) { user.socket.emit("errorMsg", { msg: "/smute failed - " + target.getName() + " has equal or higher rank " + "than you." diff --git a/lib/channel/customization.js b/lib/channel/customization.js index 627c9ae3..5bbfdd97 100644 --- a/lib/channel/customization.js +++ b/lib/channel/customization.js @@ -17,10 +17,7 @@ function CustomizationModule(channel) { ChannelModule.apply(this, arguments); this.css = ""; this.js = ""; - this.motd = { - motd: "", - html: "" - }; + this.motd = ""; } CustomizationModule.prototype = Object.create(ChannelModule.prototype); @@ -35,10 +32,15 @@ CustomizationModule.prototype.load = function (data) { } if ("motd" in data) { - this.motd = { - motd: data.motd.motd || "", - html: data.motd.html || "" - }; + if (typeof data.motd === "object" && data.motd.motd) { + // Old style MOTD, convert to new + this.motd = XSS.sanitizeHTML(data.motd.motd).replace( + /\n/g, "
\n"); + } else if (typeof data.motd === "string") { + // The MOTD is filtered before it is saved, however it is also + // re-filtered on load in case the filtering rules change + this.motd = XSS.sanitizeHTML(data.motd); + } } }; @@ -49,12 +51,7 @@ CustomizationModule.prototype.save = function (data) { }; CustomizationModule.prototype.setMotd = function (motd) { - motd = XSS.sanitizeHTML(motd); - var html = motd.replace(/\n/g, "
"); - this.motd = { - motd: motd, - html: html - }; + this.motd = XSS.sanitizeHTML(motd); this.sendMotd(this.channel.users); }; diff --git a/lib/channel/kickban.js b/lib/channel/kickban.js index 233e02f3..4d83e01e 100644 --- a/lib/channel/kickban.js +++ b/lib/channel/kickban.js @@ -175,7 +175,8 @@ KickBanModule.prototype.handleCmdKick = function (user, msg, meta) { return; } - if (target.account.effectiveRank >= user.account.effectiveRank) { + if (target.account.effectiveRank >= user.account.effectiveRank + || target.account.globalRank > user.account.globalRank) { return user.socket.emit("errorMsg", { msg: "You do not have permission to kick " + target.getName() }); diff --git a/lib/channel/playlist.js b/lib/channel/playlist.js index 4d8ffff0..f414576a 100644 --- a/lib/channel/playlist.js +++ b/lib/channel/playlist.js @@ -627,7 +627,7 @@ PlaylistModule.prototype.handleJumpTo = function (user, data) { var old = this.current; this.current = to; this.startPlayback(); - this.channel.logger.log("[playlist] " + user.getName() + " skipped" + title); + this.channel.logger.log("[playlist] " + user.getName() + " skipped " + title); if (old && old.temp) { this._delete(old.uid); @@ -937,6 +937,9 @@ PlaylistModule.prototype._addItem = function (media, data, user, cb) { self.meta.count++; self.meta.rawTime += media.seconds; self.meta.time = util.formatTime(self.meta.rawTime); + var m = item.media; + self.channel.logger.log("[playlist] " + (data.queueby || "(anonymous)") + + " added " + m.title + " (" + m.type + ":" + m.id + ")"); var perms = self.channel.modules.permissions; self.channel.users.forEach(function (u) { @@ -1046,7 +1049,12 @@ PlaylistModule.prototype.startPlayback = function (time) { self.channel.notifyModules("onMediaChange", [self.current.media]); /* Only start the timer if the media item is not live, i.e. has a duration */ - if (media.seconds > 0) { + /* + * 2015-01-22: Don't start the timer if there is an active leader or if + * the timer is already running. Both are possible since checkModules() + * is asynchronous + */ + if (media.seconds > 0 && !self.leader && !self._leadInterval) { self._lastUpdate = Date.now(); self._leadInterval = setInterval(function() { self._leadLoop(); diff --git a/lib/database.js b/lib/database.js index b77b68f8..ac4e0678 100644 --- a/lib/database.js +++ b/lib/database.js @@ -409,7 +409,8 @@ module.exports.saveUserPlaylist = function (pl, username, plname, callback) { type: pl[i].media.type, meta: { codec: pl[i].media.meta.codec, - bitrate: pl[i].media.meta.bitrate + bitrate: pl[i].media.meta.bitrate, + scuri: pl[i].media.meta.scuri } }; time += pl[i].media.seconds || 0; diff --git a/lib/database/channels.js b/lib/database/channels.js index dd68f083..30e99b31 100644 --- a/lib/database/channels.js +++ b/lib/database/channels.js @@ -426,7 +426,8 @@ module.exports = { var meta = JSON.stringify({ bitrate: media.meta.bitrate, - codec: media.meta.codec + codec: media.meta.codec, + scuri: media.meta.scuri }); db.query("INSERT INTO `channel_libraries` " + diff --git a/lib/get-info.js b/lib/get-info.js index e016fe2c..504427d9 100644 --- a/lib/get-info.js +++ b/lib/get-info.js @@ -34,6 +34,12 @@ const CONTENT_TYPES = { var urlRetrieve = function (transport, options, callback) { var req = transport.request(options, function (res) { + res.on("error", function (err) { + Logger.errlog.log("HTTP response " + options.host + options.path + " failed: "+ + err); + callback(503, ""); + }); + var buffer = ""; res.setEncoding("utf-8"); res.on("data", function (chunk) { @@ -546,7 +552,11 @@ var Getters = { data = JSON.parse(data); var seconds = data.duration / 1000; var title = data.title; - var media = new Media(id, title, seconds, "sc"); + var meta = {}; + if (data.sharing === "private" && data.embeddable_by === "all") { + meta.scuri = data.uri; + } + var media = new Media(id, title, seconds, "sc", meta); callback(false, media); } catch(e) { callback(e, null); @@ -979,17 +989,15 @@ function vimeoWorkaround(id, cb) { } }; - http.get(options, function (res) { - res.setEncoding("utf-8"); - var buffer = ""; + urlRetrieve(http, options, function (status, buffer) { + if (status !== 200) { + setImmediate(function () { + cb({}); + }); + return; + } - res.on("data", function (data) { - buffer += data; - }); - - res.on("end", function () { - parse(buffer); - }); + parse(buffer); }); }; diff --git a/lib/io/ioserver.js b/lib/io/ioserver.js index 8d4308d7..f71217bf 100644 --- a/lib/io/ioserver.js +++ b/lib/io/ioserver.js @@ -217,7 +217,15 @@ module.exports = { if (id in srv.servers) { io.attach(srv.servers[id]); } else { - io.attach(require("http").createServer().listen(bind.port, bind.ip)); + var server = require("http").createServer().listen(bind.port, bind.ip); + server.on("clientError", function (err, socket) { + console.error("clientError on " + id + " - " + err); + try { + socket.destroy(); + } catch (e) { + } + }); + io.attach(server); } bound[id] = null; diff --git a/lib/media.js b/lib/media.js index 442481c1..4faec19e 100644 --- a/lib/media.js +++ b/lib/media.js @@ -36,7 +36,8 @@ Media.prototype = { gpdirect: this.meta.gpdirect, restricted: this.meta.restricted, codec: this.meta.codec, - bitrate: this.meta.bitrate + bitrate: this.meta.bitrate, + scuri: this.meta.scuri } }; }, diff --git a/lib/server.js b/lib/server.js index cca14e9b..c6bf196f 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1,4 +1,4 @@ -const VERSION = "3.6.1"; +const VERSION = "3.6.3"; var singleton = null; var Config = require("./config"); @@ -85,8 +85,22 @@ var Server = function () { if (bind.https && Config.get("https.enabled")) { self.servers[id] = https.createServer(opts, self.express) .listen(bind.port, bind.ip); + self.servers[id].on("clientError", function (err, socket) { + console.error("clientError on " + id + " - " + err); + try { + socket.destroy(); + } catch (e) { + } + }); } else if (bind.http) { self.servers[id] = self.express.listen(bind.port, bind.ip); + self.servers[id].on("clientError", function (err, socket) { + console.error("clientError on " + id + " - " + err); + try { + socket.destroy(); + } catch (e) { + } + }); } }); diff --git a/lib/xss.js b/lib/xss.js index 9b02ce2b..b8a379e9 100644 --- a/lib/xss.js +++ b/lib/xss.js @@ -1,260 +1,64 @@ -/* - WARNING +var sanitizeHTML = require("sanitize-html"); - This file contains an XSS prevention module I wrote myself. It has not - been verified by any external agency, and due to the nature of XSS I cannot - guarantee that it will filter correctly. Feel free to send me bug reports - and I will do my best to fix them, but use at your own risk. +// These tags are allowed in addition to the defaults +// See https://github.com/punkave/sanitize-html +const ALLOWED_TAGS = [ + "button", + "center", + "details", + "font", + "h1", + "h2", + "img", + "marquee", // It pains me to do this, but a lot of people use it... + "s", + "section", + "span", + "summary" +]; -*/ - -/* Prototype for a basic XML tag parser */ -function TagParser(text) { - this.text = text; - this.i = 0; - this.tag = this.parse(); -} - -/* Moves the position marker past any whitespace characters */ -TagParser.prototype.skipWhitespace = function () { - while (this.i < this.text.length && this.text[this.i].match(/\s/)) { - this.i++; - } -}; - -/* Reads a literal value matching the given regexp. Defaults - to /[^\s>]/; i.e. any string not containing whitespace or - the end of tag character '>' -*/ -TagParser.prototype.readLiteral = function (regexp) { - if (regexp === void 0) { - regexp = /[^\s>]/; - } - var str = ""; - while (this.i < this.text.length && this.text[this.i].match(regexp)) { - str += this.text[this.i]; - this.i++; - } - - str = str.replace(/&#([0-9]{2,7});?/g, function (m, p1) { - return String.fromCharCode(parseInt(p1)); - }); - - str = str.replace(/&#x([0-9a-fA-F]{2,7});?/g, function (m, p1) { - return String.fromCharCode(parseInt(p1, 16)); - }); - - str = str.replace(/[\x00-\x1f]/g, ""); - return str; -}; - -/* If the character at the current position is a quote, read - a string. Otherwise, read a literal -*/ -TagParser.prototype.readLiteralOrString = function (regexp) { - if (this.text[this.i].match(/["'`]/)) { - return this.readString(); - } - return this.readLiteral(regexp); -}; - -/* Read a string delimited by the character at the current - position. For XML tags this means strings enclosed in - " or '. Treats \" as a literal '"' symbol and not a - delimiter. -*/ -TagParser.prototype.readString = function () { - var delim = this.text[this.i++]; - - var str = ""; - while (this.i < this.text.length && this.text[this.i] !== delim) { - if (this.text[this.i] === "\\" && this.text[this.i+1] === delim) { - str += this.text[this.i+1]; - this.i++; - } else { - str += this.text[this.i]; - } - this.i++; - } - this.i++; - - str = str.replace(/&#([0-9]{2,7});?/g, function (m, p1) { - return String.fromCharCode(parseInt(p1)); - }); - - str = str.replace(/&#x([0-9a-fA-F]{2,7});?/g, function (m, p1) { - return String.fromCharCode(parseInt(p1, 16)); - }); - - str = str.replace(/[\x00-\x1f]/g, ""); - return str; -}; - -/* Attempts to parse a tagname and attributes from an - XML tag. - NOTE: Does not actually parse a DOM node, only parses - the tag between '<' and '>' because that's all I need - to do XSS filtering, I don't care what's between a tag - and its end tag (if it's another tag I handle that - separately) -*/ -TagParser.prototype.parse = function () { - this.i = this.text.indexOf("<"); - // Not a tag - if (this.i === -1) { - return null; - } - - this.i++; - this.skipWhitespace(); - - // First non-whitespace string after the opening '<' is the tag name - var tname = this.readLiteral(); - - var attrs = {}; - // Continue parsing attributes until the end of string is reached or - // the end of tag is reached - while (this.i < this.text.length && this.text[this.i] !== ">") { - // Read any string not containing equals, possibly delimited by - // " or ' - var key = this.readLiteralOrString(/[^\s=>]/); - this.skipWhitespace(); - // It's possible for tags to have attributes with no value, where - // the equals sign is not necessary - if (this.text[this.i] !== "=") { - if (key.trim().length > 0) { - attrs[key] = ""; - } - continue; - } - - this.i++; - //this.skipWhitespace(); - var value = this.readLiteralOrString(); - if (key.trim().length > 0) { - attrs[key] = value; - } - this.skipWhitespace(); - } - - // If end-of-string was not reached, consume the ending '>' - if (this.i < this.text.length) { - this.i++; - } - - return { - tagName: tname, - attributes: attrs, - text: this.text.substring(0, this.i) // Original text (for replacement) - }; -}; - -/* Some of these may not even be HTML tags, I borrowed them from the - [now deprecated] XSS module of node-validator -*/ -const badTags = new RegExp([ - "alert", - "applet", - "audio", - "basefont", - "base", - "behavior", - "bgsound", - "blink", - "body", - "embed", - "expression", - "form", - "frameset", - "frame", - "head", - "html", - "ilayer", - "iframe", - "input", - "layer", - "link", - "meta", - "object", +const ALLOWED_ATTRIBUTES = [ + "id", + "aria-*", + "border", + "class", + "color", + "data-*", + "height", + "role", "style", - "script", - "textarea", "title", - "video", - "xml", - "xss" -].join("|"), "i"); + "valign", + "width" +]; -/* Nasty attributes. Anything starting with "on" is probably a javascript - callback, and I hope you see why formaction is a bad idea. -*/ -const badAttrs = new RegExp([ - "\\bon\\S*", - "\\bformaction", - "\\baction" -].join("|"), "i"); - -function sanitizeHTML(str) { - var i = str.indexOf("<"); - if (i === -1) { - // No HTML tags in the string - return str; - } - - // Loop across all tag delimiters '<' in string, parse each one, - // and replace the results with sanitized tags - while (i !== -1) { - var t = new TagParser(str.substring(i)).tag; - if (t.tagName.replace("/", "").match(badTags)) { - // Note: Important that I replace the tag with a nonempty value, - // otherwise ipt> would possibly defeat the filter. - str = str.replace(t.text, "[tag removed]"); - i = str.indexOf("<", i+1); - continue; - } - for (var k in t.attributes) { - // Keys should not contain non-word characters. - var k2 = k.replace(/[^\w]/g, ""); - if (k2 !== k) { - t.attributes[k2] = t.attributes[k]; - delete t.attributes[k]; - k = k2; - } - // If it's an evil attribute, just nuke it entirely - if (k.match(badAttrs)) { - delete t.attributes[k]; - } else { - if (t.attributes[k].replace(/\s/g, "").indexOf("javascript:") !== -1) { - t.attributes[k] = "[removed]"; - } - - } - } - // Build the sanitized tag - var fmt = "<" + t.tagName; - for (var k in t.attributes) { - if (k.trim().length > 0) { - fmt += " " + k; - if (t.attributes[k].trim().length > 0) { - var delim = '"'; - if (t.attributes[k].match(/[^\\]"/)) { - delim = "'"; - if (t.attributes[k].match(/[^\\]'/)) { - delim = "`"; - } - } - fmt += "=" + delim + t.attributes[k] + delim; - } - } - } - str = str.replace(t.text, fmt + ">"); - i = str.indexOf("<", i + fmt.length + 1); - } - - return str; +var ATTRIBUTE_MAP = { + a: ["href", "name", "target"], + font: ["size"], + img: ["src"], + marquee: ["behavior", "behaviour", "direction", "scrollamount"], + table: ["cellpadding", "cellspacing"], + th: ["colspan", "rowspan"], + td: ["colspan", "rowspan"] } -/* WIP: Sanitize a string where HTML is prohibited */ +for (var key in ATTRIBUTE_MAP) { + ALLOWED_ATTRIBUTES.forEach(function (attr) { + ATTRIBUTE_MAP[key].push(attr); + }); +} + +sanitizeHTML.defaults.allowedTags.concat(ALLOWED_TAGS).forEach(function (tag) { + if (!(tag in ATTRIBUTE_MAP)) { + ATTRIBUTE_MAP[tag] = ALLOWED_ATTRIBUTES; + } +}); + +const SETTINGS = { + allowedTags: sanitizeHTML.defaults.allowedTags.concat(ALLOWED_TAGS), + allowedAttributes: ATTRIBUTE_MAP +}; + function sanitizeText(str) { str = str.replace(/&/g, "&") .replace(/= 3); setParentVisible("a[href='#cs-banlist']", hasPermission("ban"));