Add IP cloaking; make tor bans channel specific

This commit is contained in:
calzoneman 2014-08-14 21:42:13 -05:00
parent ecca806a58
commit 8fddbc3e6e
14 changed files with 193 additions and 142 deletions

View file

@ -115,8 +115,6 @@ max-channels-per-user: 5
max-accounts-per-ip: 5 max-accounts-per-ip: 5
# Minimum number of seconds between guest logins from the same IP # Minimum number of seconds between guest logins from the same IP
guest-login-delay: 60 guest-login-delay: 60
# Block known Tor IP addresses
enable-tor-blocker: true
# Configure statistics tracking # Configure statistics tracking
stats: stats:

View file

@ -17,7 +17,7 @@ var Config = require("./config");
var Server = require("./server"); var Server = require("./server");
function eventUsername(user) { function eventUsername(user) {
return user.getName() + "@" + user.ip; return user.getName() + "@" + user.realip;
} }
function handleAnnounce(user, data) { function handleAnnounce(user, data) {

View file

@ -330,10 +330,24 @@ Channel.prototype.acceptUser = function (user) {
user.autoAFK(); user.autoAFK();
user.socket.on("readChanLog", this.handleReadLog.bind(this, user)); user.socket.on("readChanLog", this.handleReadLog.bind(this, user));
Logger.syslog.log(user.ip + " joined " + this.name); Logger.syslog.log(user.realip + " joined " + this.name);
this.logger.log("[login] Accepted connection from " + user.longip); if (user.socket._isUsingTor) {
if (this.modules.options && this.modules.options.get("torbanned")) {
user.kick("This channel has banned connections from Tor.");
user.socket.disconnect(true);
this.logger.log("[login] Blocked connection from Tor exit at " +
user.displayip);
return;
}
this.logger.log("[login] Accepted connection from Tor exit at " +
user.displayip);
} else {
this.logger.log("[login] Accepted connection from " + user.displayip);
}
if (user.is(Flags.U_LOGGED_IN)) { if (user.is(Flags.U_LOGGED_IN)) {
this.logger.log("[login] " + user.longip + " authenticated as " + user.getName()); this.logger.log("[login] " + user.displayip + " authenticated as " + user.getName());
} }
var self = this; var self = this;
@ -367,7 +381,7 @@ Channel.prototype.partUser = function (user) {
return; return;
} }
this.logger.log("[login] " + user.longip + " (" + user.getName() + ") " + this.logger.log("[login] " + user.displayip + " (" + user.getName() + ") " +
"disconnected."); "disconnected.");
user.channel = null; user.channel = null;
/* Should be unnecessary because partUser only occurs if the socket dies */ /* Should be unnecessary because partUser only occurs if the socket dies */
@ -412,7 +426,7 @@ Channel.prototype.packUserData = function (user) {
muted: user.is(Flags.U_MUTED), muted: user.is(Flags.U_MUTED),
smuted: user.is(Flags.U_SMUTED), smuted: user.is(Flags.U_SMUTED),
aliases: user.account.aliases, aliases: user.account.aliases,
ip: util.maskIP(user.longip) ip: user.displayip
} }
}; };
@ -425,7 +439,7 @@ Channel.prototype.packUserData = function (user) {
muted: user.is(Flags.U_MUTED), muted: user.is(Flags.U_MUTED),
smuted: user.is(Flags.U_SMUTED), smuted: user.is(Flags.U_SMUTED),
aliases: user.account.aliases, aliases: user.account.aliases,
ip: user.ip ip: user.realip
} }
}; };
@ -534,7 +548,7 @@ Channel.prototype.sendUserJoin = function (users, user) {
user.account.aliases.join(",") + ")", 2); user.account.aliases.join(",") + ")", 2);
}; };
Channel.prototype.readLog = function (shouldMaskIP, cb) { Channel.prototype.readLog = function (cb) {
var maxLen = 102400; var maxLen = 102400;
var file = this.logger.filename; var file = this.logger.filename;
this.activeLock.lock(); this.activeLock.lock();
@ -558,16 +572,6 @@ Channel.prototype.readLog = function (shouldMaskIP, cb) {
buffer += data; buffer += data;
}); });
read.on("end", function () { read.on("end", function () {
if (shouldMaskIP) {
buffer = buffer.replace(
/(?:^|\s)(\d+\.\d+\.\d+)\.\d+/g,
"$1.x"
).replace(
/(?:^|\s)((?:[0-9a-f]+:){3}[0-9a-f]+):(?:[0-9a-f]+:){3}[0-9a-f]+/g,
"$1:x:x:x:x"
);
}
cb(null, buffer); cb(null, buffer);
self.activeLock.release(); self.activeLock.release();
}); });
@ -589,7 +593,7 @@ Channel.prototype.handleReadLog = function (user) {
} }
var shouldMaskIP = user.account.globalRank < 255; var shouldMaskIP = user.account.globalRank < 255;
this.readLog(shouldMaskIP, function (err, data) { this.readLog(function (err, data) {
if (err) { if (err) {
user.socket.emit("readChanLog", { user.socket.emit("readChanLog", {
success: false, success: false,

View file

@ -24,36 +24,48 @@ function KickBanModule(channel) {
KickBanModule.prototype = Object.create(ChannelModule.prototype); KickBanModule.prototype = Object.create(ChannelModule.prototype);
function checkIPBan(cname, ip, cb) {
db.channels.isIPBanned(cname, ip, function (err, banned) {
if (err) {
cb(false);
} else {
cb(banned);
}
});
}
function checkNameBan(cname, name, cb) {
db.channels.isNameBanned(cname, name, function (err, banned) {
if (err) {
cb(false);
} else {
cb(banned);
}
});
}
KickBanModule.prototype.onUserPreJoin = function (user, data, cb) { KickBanModule.prototype.onUserPreJoin = function (user, data, cb) {
if (!this.channel.is(Flags.C_REGISTERED)) { if (!this.channel.is(Flags.C_REGISTERED)) {
return cb(null, ChannelModule.PASSTHROUGH); return cb(null, ChannelModule.PASSTHROUGH);
} }
var cname = this.channel.name; var cname = this.channel.name;
db.channels.isIPBanned(cname, user.longip, function (err, banned) { checkIPBan(cname, user.realip, function (banned) {
if (err) { if (banned) {
cb(null, ChannelModule.PASSTHROUGH);
} else if (!banned) {
if (user.is(Flags.U_LOGGED_IN)) {
checkNameBan();
} else {
cb(null, ChannelModule.PASSTHROUGH);
}
} else {
cb(null, ChannelModule.DENY); cb(null, ChannelModule.DENY);
user.kick("Your IP address is banned from this channel."); user.kick("Your IP address is banned from this channel.");
} else {
checkNameBan(cname, user.getName(), function (banned) {
if (banned) {
cb(null, ChannelModule.DENY);
user.kick("Your username is banned from this channel.");
} else {
cb(null, ChannelModule.PASSTHROUGH);
}
});
} }
}); });
function checkNameBan() {
db.channels.isNameBanned(cname, user.getName(), function (err, banned) {
if (err) {
cb(null, ChannelModule.PASSTHROUGH);
} else {
cb(null, banned ? ChannelModule.DENY : ChannelModule.PASSTHROUGH);
}
});
}
}; };
KickBanModule.prototype.onUserPostJoin = function (user) { KickBanModule.prototype.onUserPostJoin = function (user) {
@ -98,7 +110,7 @@ KickBanModule.prototype.sendBanlist = function (users) {
for (var i = 0; i < banlist.length; i++) { for (var i = 0; i < banlist.length; i++) {
bans.push({ bans.push({
id: banlist[i].id, id: banlist[i].id,
ip: banlist[i].ip === "*" ? "*" : util.maskIP(banlist[i].ip), ip: banlist[i].ip === "*" ? "*" : util.cloakIP(banlist[i].ip),
name: banlist[i].name, name: banlist[i].name,
reason: banlist[i].reason, reason: banlist[i].reason,
bannedby: banlist[i].bannedby bannedby: banlist[i].bannedby
@ -381,7 +393,7 @@ KickBanModule.prototype.kickBanTarget = function (name, ip) {
name = name.toLowerCase(); name = name.toLowerCase();
for (var i = 0; i < this.channel.users.length; i++) { for (var i = 0; i < this.channel.users.length; i++) {
if (this.channel.users[i].getLowerName() === name || if (this.channel.users[i].getLowerName() === name ||
this.channel.users[i].longip === ip) { this.channel.users[i].realip === ip) {
this.channel.users[i].kick("You're banned!"); this.channel.users[i].kick("You're banned!");
} }
} }

View file

@ -22,7 +22,8 @@ function OptionsModule(channel) {
show_public: false, // List the channel on the index page show_public: false, // List the channel on the index page
enable_link_regex: true, // Use the built-in link filter enable_link_regex: true, // Use the built-in link filter
password: false, // Channel password (false -> no password required for entry) password: false, // Channel password (false -> no password required for entry)
allow_dupes: false // Allow duplicate videos on the playlist allow_dupes: false, // Allow duplicate videos on the playlist
torbanned: false // Block connections from Tor exit nodes
}; };
} }
@ -245,6 +246,10 @@ OptionsModule.prototype.handleSetOptions = function (user, data) {
this.opts.allow_dupes = Boolean(data.allow_dupes); this.opts.allow_dupes = Boolean(data.allow_dupes);
} }
if ("torbanned" in data && user.account.effectiveRank >= 3) {
this.opts.torbanned = Boolean(data.torbanned);
}
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); this.sendOpts(this.channel.users);
}; };

View file

@ -67,7 +67,7 @@ PollModule.prototype.onUserPostJoin = function (user) {
PollModule.prototype.onUserPart = function(user) { PollModule.prototype.onUserPart = function(user) {
if (this.poll) { if (this.poll) {
this.poll.unvote(user.ip); this.poll.unvote(user.realip);
this.sendPollUpdate(this.channel.users); this.sendPollUpdate(this.channel.users);
} }
}; };
@ -142,7 +142,7 @@ PollModule.prototype.handleVote = function (user, data) {
} }
if (this.poll) { if (this.poll) {
this.poll.vote(user.ip, data.option); this.poll.vote(user.realip, data.option);
this.sendPollUpdate(this.channel.users); this.sendPollUpdate(this.channel.users);
} }
}; };

View file

@ -19,7 +19,7 @@ VoteskipModule.prototype.onUserPart = function(user) {
return; return;
} }
this.unvote(user.ip); this.unvote(user.realip);
this.update(); this.update();
}; };
@ -40,7 +40,7 @@ VoteskipModule.prototype.handleVoteskip = function (user) {
this.poll = new Poll("[server]", "voteskip", ["skip"], false); this.poll = new Poll("[server]", "voteskip", ["skip"], false);
} }
this.poll.vote(user.ip, 0); this.poll.vote(user.realip, 0);
var title = ""; var title = "";
if (this.channel.modules.playlist.current) { if (this.channel.modules.playlist.current) {

View file

@ -69,7 +69,6 @@ var defaults = {
"max-channels-per-user": 5, "max-channels-per-user": 5,
"max-accounts-per-ip": 5, "max-accounts-per-ip": 5,
"guest-login-delay": 60, "guest-login-delay": 60,
"enable-tor-blocker": true,
stats: { stats: {
interval: 3600000, interval: 3600000,
"max-age": 86400000 "max-age": 86400000

View file

@ -11,6 +11,8 @@ var Account = require("../account");
var typecheck = require("json-typecheck"); var typecheck = require("json-typecheck");
var net = require("net"); var net = require("net");
var util = require("../utilities"); var util = require("../utilities");
var crypto = require("crypto");
var isTorExit = require("../tor").isTorExit;
var CONNECT_RATE = { var CONNECT_RATE = {
burst: 5, burst: 5,
@ -43,27 +45,8 @@ function handleAuth(data, accept) {
} }
} }
/** function throttleIP(sock) {
* Called after a connection is accepted var ip = sock._realip;
*/
function handleConnection(sock) {
var ip = sock.handshake.address.address;
var longip = ip;
sock._ip = ip;
if (net.isIPv6(ip)) {
longip = util.expandIPv6(ip);
}
sock._longip = longip;
var srv = Server.getServer();
if (srv.torblocker && srv.torblocker.shouldBlockIP(ip)) {
sock.emit("kick", {
reason: "This server does not allow connections from Tor. "+
"Please log in with your regular internet connection."
});
Logger.syslog.log("Blocked Tor IP: " + ip);
sock.disconnect(true);
return;
}
if (!(ip in ipThrottle)) { if (!(ip in ipThrottle)) {
ipThrottle[ip] = $util.newRateLimiter(); ipThrottle[ip] = $util.newRateLimiter();
@ -75,16 +58,14 @@ function handleConnection(sock) {
reason: "Your IP address is connecting too quickly. Please "+ reason: "Your IP address is connecting too quickly. Please "+
"wait 10 seconds before joining again." "wait 10 seconds before joining again."
}); });
return; return true;
} }
// Check for global ban on the IP return false;
if (db.isGlobalIPBanned(ip)) { }
Logger.syslog.log("Rejecting " + ip + " - global banned");
sock.emit("kick", { reason: "Your IP is globally banned." }); function ipLimitReached(sock) {
sock.disconnect(true); var ip = sock._realip;
return;
}
sock.on("disconnect", function () { sock.on("disconnect", function () {
ipCount[ip]--; ipCount[ip]--;
@ -106,9 +87,9 @@ function handleConnection(sock) {
sock.disconnect(true); sock.disconnect(true);
return; return;
} }
}
Logger.syslog.log("Accepted socket from " + ip); function addTypecheckedFunctions(sock) {
sock.typecheckedOn = function (msg, template, cb) { sock.typecheckedOn = function (msg, template, cb) {
sock.on(msg, function (data) { sock.on(msg, function (data) {
typecheck(data, template, function (err, data) { typecheck(data, template, function (err, data) {
@ -136,6 +117,44 @@ function handleConnection(sock) {
}); });
}); });
}; };
}
/**
* Called after a connection is accepted
*/
function handleConnection(sock) {
var ip = sock.handshake.address.address;
if (net.isIPv6(ip)) {
ip = util.expandIPv6(ip);
}
sock._realip = ip;
sock._displayip = $util.cloakIP(ip);
if (isTorExit(ip)) {
sock._isUsingTor = true;
}
var srv = Server.getServer();
if (throttleIP(ip)) {
return;
}
// Check for global ban on the IP
if (db.isGlobalIPBanned(ip)) {
Logger.syslog.log("Rejecting " + ip + " - global banned");
sock.emit("kick", { reason: "Your IP is globally banned." });
sock.disconnect(true);
return;
}
if (ipLimitReached(sock)) {
return;
}
Logger.syslog.log("Accepted socket from " + ip);
addTypecheckedFunctions(sock);
var user = new User(sock); var user = new User(sock);
if (sock.handshake.user) { if (sock.handshake.user) {
@ -148,6 +167,7 @@ function handleConnection(sock) {
user.setFlag(Flags.U_READY); user.setFlag(Flags.U_READY);
return; return;
} }
user.socket.emit("login", { user.socket.emit("login", {
success: true, success: true,
name: user.getName(), name: user.getName(),

View file

@ -24,8 +24,8 @@ function retrieveIPs(cb) {
var d = domain.create(); var d = domain.create();
d.on("error", function (err) { d.on("error", function (err) {
if (err.trace) if (err.stack)
Logger.errlog.log(err.trace()); Logger.errlog.log(err.stack);
else else
Logger.errlog.log(err); Logger.errlog.log(err);
}); });
@ -63,26 +63,17 @@ function getTorIPs(cb) {
}); });
} }
module.exports = function () { var _ipList = [];
var x = { getTorIPs(function (err, ips) {
ipList: [], if (err) {
shouldBlockIP: function (ip) { Logger.errlog.log(err);
return this.ipList.indexOf(ip) >= 0; return;
} }
};
var init = function () { Logger.syslog.log("Loaded Tor IP list");
getTorIPs(function (err, ips) { _ipList = ips;
if (err) { });
Logger.errlog.log(err);
return;
}
Logger.syslog.log("Loaded Tor IP list"); exports.isTorExit = function (ip) {
x.ipList = ips; return this._ipList.indexOf(ip) >= 0;
});
};
init();
return x;
}; };

View file

@ -14,9 +14,10 @@ function User(socket) {
MakeEmitter(self); MakeEmitter(self);
self.flags = 0; self.flags = 0;
self.socket = socket; self.socket = socket;
self.ip = socket._ip; self.realip = socket._realip;
self.longip = socket._longip; self.displayip = socket._displayip;
self.account = Account.default(self.longip); self.hostmask = socket._hostmask;
self.account = Account.default(self.realip);
self.channel = null; self.channel = null;
self.queueLimiter = util.newRateLimiter(); self.queueLimiter = util.newRateLimiter();
self.chatLimiter = util.newRateLimiter(); self.chatLimiter = util.newRateLimiter();
@ -65,7 +66,7 @@ function User(socket) {
self.kick("Attempted initACP from non privileged user. This incident " + self.kick("Attempted initACP from non privileged user. This incident " +
"will be reported."); "will be reported.");
Logger.eventlog.log("[acp] Attempted initACP from socket client " + Logger.eventlog.log("[acp] Attempted initACP from socket client " +
self.getName() + "@" + self.ip); self.getName() + "@" + self.realip);
} }
}); });
}); });
@ -176,7 +177,7 @@ User.prototype.setAFK = function (afk) {
if (afk) { if (afk) {
this.setFlag(Flags.U_AFK); this.setFlag(Flags.U_AFK);
if (this.channel.modules.voteskip) { if (this.channel.modules.voteskip) {
this.channel.modules.voteskip.unvote(this.ip); this.channel.modules.voteskip.unvote(this.realip);
} }
} else { } else {
this.clearFlag(Flags.U_AFK); this.clearFlag(Flags.U_AFK);
@ -255,7 +256,7 @@ User.prototype.login = function (name, pw) {
if (err) { if (err) {
if (err === "Invalid username/password combination") { if (err === "Invalid username/password combination") {
Logger.eventlog.log("[loginfail] Login failed (bad password): " + name Logger.eventlog.log("[loginfail] Login failed (bad password): " + name
+ "@" + self.ip); + "@" + self.realip);
} }
self.socket.emit("login", { self.socket.emit("login", {
@ -283,11 +284,11 @@ User.prototype.login = function (name, pw) {
success: true, success: true,
name: user.name name: user.name
}); });
db.recordVisit(self.longip, self.getName()); db.recordVisit(self.realip, self.getName());
self.socket.emit("rank", self.account.effectiveRank); self.socket.emit("rank", self.account.effectiveRank);
Logger.syslog.log(self.ip + " logged in as " + user.name); Logger.syslog.log(self.realip + " logged in as " + user.name);
if (self.inChannel()) { if (self.inChannel()) {
self.channel.logger.log(self.longip + " logged in as " + user.name); self.channel.logger.log(self.displayip + " logged in as " + user.name);
} }
self.setFlag(Flags.U_LOGGED_IN); self.setFlag(Flags.U_LOGGED_IN);
self.clearFlag(Flags.U_LOGGING_IN); self.clearFlag(Flags.U_LOGGING_IN);
@ -300,8 +301,8 @@ var lastguestlogin = {};
User.prototype.guestLogin = function (name) { User.prototype.guestLogin = function (name) {
var self = this; var self = this;
if (self.ip in lastguestlogin) { if (self.realip in lastguestlogin) {
var diff = (Date.now() - lastguestlogin[self.ip]) / 1000; var diff = (Date.now() - lastguestlogin[self.realip]) / 1000;
if (diff < Config.get("guest-login-delay")) { if (diff < Config.get("guest-login-delay")) {
self.socket.emit("login", { self.socket.emit("login", {
success: false, success: false,
@ -355,7 +356,7 @@ User.prototype.guestLogin = function (name) {
} }
// Login succeeded // Login succeeded
lastguestlogin[self.ip] = Date.now(); lastguestlogin[self.realip] = Date.now();
var opts = { name: name }; var opts = { name: name };
if (self.inChannel()) { if (self.inChannel()) {
@ -373,11 +374,11 @@ User.prototype.guestLogin = function (name) {
name: name, name: name,
guest: true guest: true
}); });
db.recordVisit(self.longip, self.getName()); db.recordVisit(self.realip, self.getName());
self.socket.emit("rank", 0); self.socket.emit("rank", 0);
Logger.syslog.log(self.ip + " signed in as " + name); Logger.syslog.log(self.realip + " signed in as " + name);
if (self.inChannel()) { if (self.inChannel()) {
self.channel.logger.log(self.longip + " signed in as " + name); self.channel.logger.log(self.displayip + " signed in as " + name);
} }
self.setFlag(Flags.U_LOGGED_IN); self.setFlag(Flags.U_LOGGED_IN);
self.emit("login", self.account); self.emit("login", self.account);
@ -422,7 +423,7 @@ User.prototype.refreshAccount = function (opts, cb) {
opts.registered = this.is(Flags.U_REGISTERED); opts.registered = this.is(Flags.U_REGISTERED);
var self = this; var self = this;
var old = this.account; var old = this.account;
Account.getAccount(name, this.longip, opts, function (err, account) { Account.getAccount(name, this.realip, opts, function (err, account) {
if (!err) { if (!err) {
/* Update account if anything changed in the meantime */ /* Update account if anything changed in the meantime */
for (var key in old) { for (var key in old) {

View file

@ -78,31 +78,6 @@
return salt.join(''); return salt.join('');
}, },
root.maskIP = function (ip) {
if (net.isIPv4(ip)) {
/* Full /32 IPv4 address */
return ip.replace(/^(\d+\.\d+\.\d+)\.\d+/, "$1.x");
} else if (net.isIPv4(ip + ".0")) {
/* /24 IPv4 range */
return ip + ".0/24";
} else if (net.isIPv4(ip + ".0.0")) {
/* /16 IPv4 widerange */
return ip + ".0.0/16";
} else if (net.isIPv6(ip)) {
/* /128 IPv6 address */
return ip.replace(/^((?:[0-9a-f]+:){3}[0-9a-f]+):(?:[0-9a-f]+:){3}[0-9a-f]+$/,
"$1:x:x:x:x");
} else if (net.isIPv6(ip + ":0:0:0:0")) {
/* /64 IPv6 range */
return ip + "::0/64";
} else if (net.isIPv6(ip + ":0:0:0:0:0")) {
/* /48 IPv6 widerange */
return ip + "::0/48";
} else {
return ip;
}
},
root.getIPRange = function (ip) { root.getIPRange = function (ip) {
if (net.isIPv6(ip)) { if (net.isIPv6(ip)) {
return root.expandIPv6(ip) return root.expandIPv6(ip)
@ -291,4 +266,48 @@
shasum.update(data); shasum.update(data);
return shasum.digest("hex"); return shasum.digest("hex");
} }
root.cloakIP = function (ip) {
if (ip.match(/\d+\.\d+(\.\d+)?(\.\d+)?/)) {
return cloakIPv4(ip);
} else if (ip.match(/([0-9a-f]{1,4}\:){1,7}[0-9a-f]{1,4}/)) {
return cloakIPv6(ip);
} else {
return ip;
}
function iphash(ip, segment, len) {
var md5 = crypto.createHash("md5");
md5.update(ip);
md5.update(segment);
return md5.digest("base64").substring(0, len);
}
function cloakIPv4(ip) {
var parts = ip.split(".");
parts = parts.map(function (segment, i) {
if (i < 2) return segment;
return iphash(ip, segment + i, 3);
});
while (parts.length < 4) parts.push("*");
return parts.join(".");
}
function cloakIPv6(ip) {
var parts = ip.split(":");
parts.splice(4, 4);
parts = parts.map(function (segment, i) {
if (i < 2) return segment;
return iphash(ip, segment + i, 4);
});
while (parts.length < 4) parts.push("*");
return parts.join(":");
}
}
})(); })();

View file

@ -75,6 +75,7 @@ mixin adminoptions
mixin textbox-auto("cs-externalcss", "External CSS", "Stylesheet URL") mixin textbox-auto("cs-externalcss", "External CSS", "Stylesheet URL")
mixin textbox-auto("cs-externaljs", "External Javascript", "Script URL") mixin textbox-auto("cs-externaljs", "External Javascript", "Script URL")
mixin rcheckbox-auto("cs-show_public", "List channel publicly") mixin rcheckbox-auto("cs-show_public", "List channel publicly")
mixin rcheckbox-auto("cs-torbanned", "Block connections from Tor")
.form-group .form-group
.col-sm-8.col-sm-offset-4 .col-sm-8.col-sm-offset-4
span.text-info Changes are automatically saved. span.text-info Changes are automatically saved.

View file

@ -864,6 +864,7 @@ function handleModPermissions() {
$("#cs-allow_voteskip").prop("checked", CHANNEL.opts.allow_voteskip); $("#cs-allow_voteskip").prop("checked", CHANNEL.opts.allow_voteskip);
$("#cs-voteskip_ratio").val(CHANNEL.opts.voteskip_ratio); $("#cs-voteskip_ratio").val(CHANNEL.opts.voteskip_ratio);
$("#cs-allow_dupes").val(CHANNEL.opts.allow_dupes); $("#cs-allow_dupes").val(CHANNEL.opts.allow_dupes);
$("#cs-torbanned").val(CHANNEL.opts.torbanned);
(function() { (function() {
if(typeof CHANNEL.opts.maxlength != "number") { if(typeof CHANNEL.opts.maxlength != "number") {
$("#cs-maxlength").val(""); $("#cs-maxlength").val("");