Merge pull request #636 from calzoneman/deprecate-http

HTTPS enforcement phase 1
This commit is contained in:
Calvin Montgomery 2016-12-15 22:57:06 -08:00 committed by GitHub
commit 041d50cb23
7 changed files with 238 additions and 87 deletions

View file

@ -2,7 +2,7 @@
"author": "Calvin Montgomery", "author": "Calvin Montgomery",
"name": "CyTube", "name": "CyTube",
"description": "Online media synchronizer and chat", "description": "Online media synchronizer and chat",
"version": "3.24.3", "version": "3.25.0",
"repository": { "repository": {
"url": "http://github.com/calzoneman/sync" "url": "http://github.com/calzoneman/sync"
}, },

View file

@ -3,6 +3,10 @@ var Config = require("../config");
var Utilities = require("../utilities"); var Utilities = require("../utilities");
var url = require("url"); var url = require("url");
function realTypeOf(thing) {
return thing === null ? 'null' : typeof thing;
}
function OptionsModule(channel) { function OptionsModule(channel) {
ChannelModule.apply(this, arguments); ChannelModule.apply(this, arguments);
this.opts = { this.opts = {
@ -100,8 +104,11 @@ OptionsModule.prototype.handleSetOptions = function (user, data) {
return; return;
} }
var sendUpdate = false;
if ("allow_voteskip" in data) { if ("allow_voteskip" in data) {
this.opts.allow_voteskip = Boolean(data.allow_voteskip); this.opts.allow_voteskip = Boolean(data.allow_voteskip);
sendUpdate = true;
} }
if ("voteskip_ratio" in data) { if ("voteskip_ratio" in data) {
@ -110,6 +117,7 @@ OptionsModule.prototype.handleSetOptions = function (user, data) {
ratio = 0; ratio = 0;
} }
this.opts.voteskip_ratio = ratio; this.opts.voteskip_ratio = ratio;
sendUpdate = true;
} }
if ("afk_timeout" in data) { if ("afk_timeout" in data) {
@ -125,12 +133,14 @@ OptionsModule.prototype.handleSetOptions = function (user, data) {
u.autoAFK(); u.autoAFK();
}); });
} }
sendUpdate = true;
} }
if ("pagetitle" in data && user.account.effectiveRank >= 3) { if ("pagetitle" in data && user.account.effectiveRank >= 3) {
var title = (""+data.pagetitle).substring(0, 100); var title = (""+data.pagetitle).substring(0, 100);
if (!title.trim().match(Config.get("reserved-names.pagetitles"))) { if (!title.trim().match(Config.get("reserved-names.pagetitles"))) {
this.opts.pagetitle = (""+data.pagetitle).substring(0, 100); this.opts.pagetitle = (""+data.pagetitle).substring(0, 100);
sendUpdate = true;
} else { } else {
user.socket.emit("errorMsg", { user.socket.emit("errorMsg", {
msg: "That pagetitle is reserved", msg: "That pagetitle is reserved",
@ -151,63 +161,90 @@ OptionsModule.prototype.handleSetOptions = function (user, data) {
ml = 0; ml = 0;
} }
this.opts.maxlength = ml; this.opts.maxlength = ml;
sendUpdate = true;
} }
if ("externalcss" in data && user.account.effectiveRank >= 3) { if ("externalcss" in data && user.account.effectiveRank >= 3) {
var link = (""+data.externalcss).substring(0, 255); var prefix = "Invalid URL for external CSS: ";
if (!link) { if (typeof data.externalcss !== "string") {
this.opts.externalcss = ""; user.socket.emit("validationError", {
} else { target: "#cs-externalcss",
try { message: prefix + "URL must be a string, not "
var data = url.parse(link); + realTypeOf(data.externalcss)
if (!data.protocol || !data.protocol.match(/^(https?|ftp):/)) { });
throw "Unacceptable protocol " + data.protocol; }
} else if (!data.host) {
throw "URL is missing host";
} else {
link = data.href;
}
} catch (e) {
user.socket.emit("errorMsg", {
msg: "Invalid URL for external CSS: " + e,
alert: true
});
return;
}
this.opts.externalcss = link; var link = data.externalcss.substring(0, 255).trim();
if (!link) {
sendUpdate = (this.opts.externalcss !== "");
this.opts.externalcss = "";
user.socket.emit("validationPassed", {
target: "#cs-externalcss"
});
} else {
var data = url.parse(link);
if (!data.protocol || data.protocol !== 'https:') {
user.socket.emit("validationError", {
target: "#cs-externalcss",
message: prefix + " URL must begin with 'https://'"
});
} else if (!data.host) {
user.socket.emit("validationError", {
target: "#cs-externalcss",
message: prefix + "missing hostname"
});
} else {
user.socket.emit("validationPassed", {
target: "#cs-externalcss"
});
this.opts.externalcss = data.href;
sendUpdate = true;
}
} }
} }
if ("externaljs" in data && user.account.effectiveRank >= 3) { if ("externaljs" in data && user.account.effectiveRank >= 3) {
var link = (""+data.externaljs).substring(0, 255); var prefix = "Invalid URL for external JS: ";
if (typeof data.externaljs !== "string") {
user.socket.emit("validationError", {
target: "#cs-externaljs",
message: prefix + "URL must be a string, not "
+ realTypeOf(data.externaljs)
});
}
var link = data.externaljs.substring(0, 255).trim();
if (!link) { if (!link) {
sendUpdate = (this.opts.externaljs !== "");
this.opts.externaljs = ""; this.opts.externaljs = "";
user.socket.emit("validationPassed", {
target: "#cs-externaljs"
});
} else { } else {
var data = url.parse(link);
try { if (!data.protocol || data.protocol !== 'https:') {
var data = url.parse(link); user.socket.emit("validationError", {
if (!data.protocol || !data.protocol.match(/^(https?|ftp):/)) { target: "#cs-externaljs",
throw "Unacceptable protocol " + data.protocol; message: prefix + " URL must begin with 'https://'"
} else if (!data.host) {
throw "URL is missing host";
} else {
link = data.href;
}
} catch (e) {
user.socket.emit("errorMsg", {
msg: "Invalid URL for external JS: " + e,
alert: true
}); });
return; } else if (!data.host) {
user.socket.emit("validationError", {
target: "#cs-externaljs",
message: prefix + "missing hostname"
});
} else {
user.socket.emit("validationPassed", {
target: "#cs-externaljs"
});
this.opts.externaljs = data.href;
sendUpdate = true;
} }
this.opts.externaljs = link;
} }
} }
if ("chat_antiflood" in data) { if ("chat_antiflood" in data) {
this.opts.chat_antiflood = Boolean(data.chat_antiflood); this.opts.chat_antiflood = Boolean(data.chat_antiflood);
sendUpdate = true;
} }
if ("chat_antiflood_params" in data) { if ("chat_antiflood_params" in data) {
@ -238,38 +275,46 @@ OptionsModule.prototype.handleSetOptions = function (user, data) {
sustained: s, sustained: s,
cooldown: c cooldown: c
}; };
sendUpdate = true;
} }
if ("show_public" in data && user.account.effectiveRank >= 3) { if ("show_public" in data && user.account.effectiveRank >= 3) {
this.opts.show_public = Boolean(data.show_public); this.opts.show_public = Boolean(data.show_public);
sendUpdate = true;
} }
if ("enable_link_regex" in data) { if ("enable_link_regex" in data) {
this.opts.enable_link_regex = Boolean(data.enable_link_regex); this.opts.enable_link_regex = Boolean(data.enable_link_regex);
sendUpdate = true;
} }
if ("password" in data && user.account.effectiveRank >= 3) { if ("password" in data && user.account.effectiveRank >= 3) {
var pw = data.password + ""; var pw = data.password + "";
pw = pw === "" ? false : pw.substring(0, 100); pw = pw === "" ? false : pw.substring(0, 100);
this.opts.password = pw; this.opts.password = pw;
sendUpdate = true;
} }
if ("allow_dupes" in data) { if ("allow_dupes" in data) {
this.opts.allow_dupes = Boolean(data.allow_dupes); this.opts.allow_dupes = Boolean(data.allow_dupes);
sendUpdate = true;
} }
if ("torbanned" in data && user.account.effectiveRank >= 3) { if ("torbanned" in data && user.account.effectiveRank >= 3) {
this.opts.torbanned = Boolean(data.torbanned); this.opts.torbanned = Boolean(data.torbanned);
sendUpdate = true;
} }
if ("allow_ascii_control" in data && user.account.effectiveRank >= 3) { if ("allow_ascii_control" in data && user.account.effectiveRank >= 3) {
this.opts.allow_ascii_control = Boolean(data.allow_ascii_control); this.opts.allow_ascii_control = Boolean(data.allow_ascii_control);
sendUpdate = true;
} }
if ("playlist_max_per_user" in data && user.account.effectiveRank >= 3) { if ("playlist_max_per_user" in data && user.account.effectiveRank >= 3) {
var max = parseInt(data.playlist_max_per_user); var max = parseInt(data.playlist_max_per_user);
if (!isNaN(max) && max >= 0) { if (!isNaN(max) && max >= 0) {
this.opts.playlist_max_per_user = max; this.opts.playlist_max_per_user = max;
sendUpdate = true;
} }
} }
@ -277,6 +322,7 @@ OptionsModule.prototype.handleSetOptions = function (user, data) {
var delay = data.new_user_chat_delay; var delay = data.new_user_chat_delay;
if (!isNaN(delay) && delay >= 0) { if (!isNaN(delay) && delay >= 0) {
this.opts.new_user_chat_delay = delay; this.opts.new_user_chat_delay = delay;
sendUpdate = true;
} }
} }
@ -284,11 +330,14 @@ OptionsModule.prototype.handleSetOptions = function (user, data) {
var delay = data.new_user_chat_link_delay; var delay = data.new_user_chat_link_delay;
if (!isNaN(delay) && delay >= 0) { if (!isNaN(delay) && delay >= 0) {
this.opts.new_user_chat_link_delay = delay; this.opts.new_user_chat_link_delay = delay;
sendUpdate = true;
} }
} }
this.channel.logger.log("[mod] " + user.getName() + " updated channel options"); this.channel.logger.log("[mod] " + user.getName() + " updated channel options");
this.sendOpts(this.channel.users); if (sendUpdate) {
this.sendOpts(this.channel.users);
}
}; };
module.exports = OptionsModule; module.exports = OptionsModule;

View file

@ -13,6 +13,7 @@ var Config = require("../config");
var Server = require("../server"); var Server = require("../server");
var session = require("../session"); var session = require("../session");
var csrf = require("./csrf"); var csrf = require("./csrf");
const url = require("url");
/** /**
* Handles a GET request for /account/edit * Handles a GET request for /account/edit
@ -396,6 +397,25 @@ function handleAccountProfilePage(req, res) {
}); });
} }
function validateProfileImage(image, callback) {
var prefix = "Invalid URL for profile image: ";
var link = image.trim();
if (!link) {
process.nextTick(callback, null, link);
} else {
var data = url.parse(link);
if (!data.protocol || data.protocol !== 'https:') {
process.nextTick(callback,
new Error(prefix + " URL must begin with 'https://'"));
} else if (!data.host) {
process.nextTick(callback,
new Error(prefix + "missing hostname"));
} else {
process.nextTick(callback, null, link);
}
}
}
/** /**
* Handles a POST request to edit a profile * Handles a POST request to edit a profile
*/ */
@ -410,23 +430,37 @@ function handleAccountProfile(req, res) {
}); });
} }
var image = req.body.image; var rawImage = String(req.body.image).substring(0, 255);
var text = req.body.text; var text = String(req.body.text).substring(0, 255);
db.users.setProfile(req.user.name, { image: image, text: text }, function (err) { validateProfileImage(rawImage, (error, image) => {
if (err) { if (error) {
sendPug(res, "account-profile", { db.users.getProfile(req.user.name, function (err, profile) {
profileImage: "", var errorMessage = err || error.message;
profileText: "", sendPug(res, "account-profile", {
profileError: err profileImage: profile ? profile.image : "",
profileText: profile ? profile.text : "",
profileError: errorMessage
});
}); });
return; return;
} }
sendPug(res, "account-profile", { db.users.setProfile(req.user.name, { image: image, text: text }, function (err) {
profileImage: image, if (err) {
profileText: text, sendPug(res, "account-profile", {
profileError: false profileImage: "",
profileText: "",
profileError: err
});
return;
}
sendPug(res, "account-profile", {
profileImage: image,
profileText: text,
profileError: false
});
}); });
}); });
} }

View file

@ -35,13 +35,42 @@ html(lang="en")
input(type="hidden", name="_csrf", value=csrfToken) input(type="hidden", name="_csrf", value=csrfToken)
.form-group .form-group
label.control-label(for="profileimage") Image label.control-label(for="profileimage") Image
input#profileimage.form-control(type="text", name="image") input#profileimage.form-control(type="text", name="image", maxlength="255")
.form-group .form-group
label.control-label(for="profiletext") Text label.control-label(for="profiletext") Text
textarea#profiletext.form-control(cols="10", name="text")= profileText textarea#profiletext.form-control(cols="10", name="text", maxlength="255")= profileText
button.btn.btn-primary.btn-block(type="submit") Save button.btn.btn-primary.btn-block(type="submit") Save
include footer include footer
+footer() +footer()
script(type="text/javascript"). script(type="text/javascript").
$("#profileimage").val("#{profileImage}"); var $profileImage = $("#profileimage");
$profileImage.val("#{profileImage}");
var hasError = false;
function validateImage() {
var value = $profileImage.val().trim();
$profileImage.val(value);
if (!/^$|^https:/.test(value)) {
hasError = true;
$profileImage.parent().addClass("has-error");
var $error = $("#profileimage-error");
if ($error.length === 0) {
$error = $("<p/>")
.attr({ id: "profileimage-error" })
.addClass("text-danger")
.html("Profile image must be a URL beginning with <code>https://</code>")
.insertAfter($profileImage);
}
} else {
hasError = false;
$profileImage.parent().removeClass("has-error");
$("#profileimage-error").remove();
}
}
$("form").submit(function (event) {
validateImage();
if (hasError) {
event.preventDefault();
}
});

View file

@ -49,7 +49,6 @@ Callbacks = {
}, },
costanza: function (data) { costanza: function (data) {
hidePlayer();
$("#costanza-modal").modal("hide"); $("#costanza-modal").modal("hide");
var modal = makeModal(); var modal = makeModal();
modal.attr("id", "costanza-modal") modal.attr("id", "costanza-modal")
@ -61,7 +60,6 @@ Callbacks = {
.appendTo(body); .appendTo(body);
$("<strong/>").text(data.msg).appendTo(body); $("<strong/>").text(data.msg).appendTo(body);
hidePlayer();
modal.modal(); modal.modal();
}, },
@ -1052,6 +1050,38 @@ Callbacks = {
HAS_CONNECTED_BEFORE = false; HAS_CONNECTED_BEFORE = false;
ioServerConnect(socketConfig); ioServerConnect(socketConfig);
setupCallbacks(); setupCallbacks();
},
validationError: function (error) {
var target = $(error.target);
target.parent().find(".text-danger").remove();
var formGroup = target.parent();
while (!formGroup.hasClass("form-group") && formGroup.length > 0) {
formGroup = formGroup.parent();
}
if (formGroup.length > 0) {
formGroup.addClass("has-error");
}
$("<p/>").addClass("text-danger")
.text(error.message)
.insertAfter(target);
},
validationPassed: function (data) {
var target = $(data.target);
target.parent().find(".text-danger").remove();
var formGroup = target.parent();
while (!formGroup.hasClass("form-group") && formGroup.length > 0) {
formGroup = formGroup.parent();
}
if (formGroup.length > 0) {
formGroup.removeClass("has-error");
}
} }
} }

View file

@ -488,7 +488,6 @@ $("#voteskip").click(function() {
$("#getplaylist").click(function() { $("#getplaylist").click(function() {
var callback = function(data) { var callback = function(data) {
hidePlayer();
var idx = socket.listeners("errorMsg").indexOf(errCallback); var idx = socket.listeners("errorMsg").indexOf(errCallback);
if (idx >= 0) { if (idx >= 0) {
socket.listeners("errorMsg").splice(idx); socket.listeners("errorMsg").splice(idx);
@ -523,7 +522,6 @@ $("#getplaylist").click(function() {
$("<div/>").addClass("modal-footer").appendTo(modal); $("<div/>").addClass("modal-footer").appendTo(modal);
outer.on("hidden.bs.modal", function() { outer.on("hidden.bs.modal", function() {
outer.remove(); outer.remove();
unhidePlayer();
}); });
outer.modal(); outer.modal();
}; };
@ -862,7 +860,6 @@ applyOpts();
})(); })();
var EMOTELISTMODAL = $("#emotelist"); var EMOTELISTMODAL = $("#emotelist");
EMOTELISTMODAL.on("hidden.bs.modal", unhidePlayer);
$("#emotelistbtn").click(function () { $("#emotelistbtn").click(function () {
EMOTELISTMODAL.modal(); EMOTELISTMODAL.modal();
}); });

View file

@ -616,11 +616,6 @@ function rebuildPlaylist() {
/* user settings menu */ /* user settings menu */
function showUserOptions() { function showUserOptions() {
hidePlayer();
$("#useroptions").on("hidden.bs.modal", function () {
unhidePlayer();
});
if (CLIENT.rank < 2) { if (CLIENT.rank < 2) {
$("a[href='#us-mod']").parent().hide(); $("a[href='#us-mod']").parent().hide();
} else { } else {
@ -2046,21 +2041,6 @@ function waitUntilDefined(obj, key, fn) {
fn(); fn();
} }
function hidePlayer() {
/* 2015-09-16
* Originally used to hide the player while a modal was open because of
* certain flash videos that always rendered on top. Seems to no longer
* be an issue. Uncomment this if it is.
if (!PLAYER) return;
$("#ytapiplayer").hide();
*/
}
function unhidePlayer() {
//$("#ytapiplayer").show();
}
function chatDialog(div) { function chatDialog(div) {
var parent = $("<div/>").addClass("profile-box") var parent = $("<div/>").addClass("profile-box")
.css({ .css({
@ -2103,6 +2083,44 @@ function errDialog(err) {
return div; return div;
} }
/**
* 2016-12-08
* I *promise* that one day I will actually split this file into submodules
* -cal
*/
function modalAlert(options) {
if (typeof options !== "object" || options === null) {
throw new Error("modalAlert() called without required parameter");
}
var modal = makeModal();
modal.addClass("cytube-modal-alert");
modal.removeClass("fade");
modal.find(".modal-dialog").addClass("modal-dialog-nonfluid");
if (options.title) {
$("<h3/>").text(options.title).appendTo(modal.find(".modal-header"));
}
var contentDiv = $("<div/>").addClass("modal-body");
if (options.htmlContent) {
contentDiv.html(options.htmlContent);
} else if (options.textContent) {
contentDiv.text(options.textContent);
}
contentDiv.appendTo(modal.find(".modal-content"));
var footer = $("<div/>").addClass("modal-footer");
var okButton = $("<button/>").addClass("btn btn-primary")
.attr({ "data-dismiss": "modal"})
.text("OK")
.appendTo(footer);
footer.appendTo(modal.find(".modal-content"));
modal.appendTo(document.body);
modal.modal();
}
function queueMessage(data, type) { function queueMessage(data, type) {
if (!data) if (!data)
data = { link: null }; data = { link: null };
@ -2237,7 +2255,6 @@ function makeModal() {
.appendTo(head); .appendTo(head);
wrap.on("hidden.bs.modal", function () { wrap.on("hidden.bs.modal", function () {
unhidePlayer();
wrap.remove(); wrap.remove();
}); });
return wrap; return wrap;
@ -3160,11 +3177,6 @@ window.CSEMOTELIST = new CSEmoteList("#cs-emotes");
window.CSEMOTELIST.sortAlphabetical = USEROPTS.emotelist_sort; window.CSEMOTELIST.sortAlphabetical = USEROPTS.emotelist_sort;
function showChannelSettings() { function showChannelSettings() {
hidePlayer();
$("#channeloptions").on("hidden.bs.modal", function () {
unhidePlayer();
});
$("#channeloptions").modal(); $("#channeloptions").modal();
} }