mirror of https://github.com/calzoneman/sync.git
Merge pull request #614 from calzoneman/ip-session-age
Restrict chat messages from newer accounts/IPs
This commit is contained in:
commit
b34a8fce3c
|
@ -2,7 +2,7 @@
|
|||
"author": "Calvin Montgomery",
|
||||
"name": "CyTube",
|
||||
"description": "Online media synchronizer and chat",
|
||||
"version": "3.20.0",
|
||||
"version": "3.21.0",
|
||||
"repository": {
|
||||
"url": "http://github.com/calzoneman/sync"
|
||||
},
|
||||
|
|
|
@ -121,6 +121,27 @@ ChatModule.prototype.shadowMutedUsers = function () {
|
|||
});
|
||||
};
|
||||
|
||||
ChatModule.prototype.restrictNewAccount = function restrictNewAccount(user, data) {
|
||||
if (user.account.effectiveRank < 2 && this.channel.modules.options) {
|
||||
const firstSeen = user.getFirstSeenTime();
|
||||
const opts = this.channel.modules.options;
|
||||
if (firstSeen > Date.now() - opts.get("new_user_chat_delay")*1000) {
|
||||
user.socket.emit("spamFiltered", {
|
||||
reason: "NEW_USER_CHAT"
|
||||
});
|
||||
return true;
|
||||
} else if ((firstSeen > Date.now() - opts.get("new_user_chat_link_delay")*1000)
|
||||
&& data.msg.match(LINK)) {
|
||||
user.socket.emit("spamFiltered", {
|
||||
reason: "NEW_USER_CHAT_LINK"
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
ChatModule.prototype.handleChatMsg = function (user, data) {
|
||||
var self = this;
|
||||
counters.add("chat:incoming");
|
||||
|
@ -131,6 +152,12 @@ ChatModule.prototype.handleChatMsg = function (user, data) {
|
|||
|
||||
// Limit to 240 characters
|
||||
data.msg = data.msg.substring(0, 240);
|
||||
|
||||
// Restrict new accounts/IPs from chatting and posting links
|
||||
if (this.restrictNewAccount(user, data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If channel doesn't permit them, strip ASCII control characters
|
||||
if (!this.channel.modules.options ||
|
||||
!this.channel.modules.options.get("allow_ascii_control")) {
|
||||
|
@ -174,6 +201,11 @@ ChatModule.prototype.handlePm = function (user, data) {
|
|||
});
|
||||
}
|
||||
|
||||
// Restrict new accounts/IPs from chatting and posting links
|
||||
if (this.restrictNewAccount(user, data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.msg.match(Config.get("link-domain-blacklist-regex"))) {
|
||||
this.channel.logger.log(user.displayip + " (" + user.getName() + ") was kicked for " +
|
||||
"blacklisted domain");
|
||||
|
|
|
@ -25,7 +25,9 @@ function OptionsModule(channel) {
|
|||
allow_dupes: false, // Allow duplicate videos on the playlist
|
||||
torbanned: false, // Block connections from Tor exit nodes
|
||||
allow_ascii_control: false,// Allow ASCII control characters (\x00-\x1f)
|
||||
playlist_max_per_user: 0 // Maximum number of playlist items per user
|
||||
playlist_max_per_user: 0, // Maximum number of playlist items per user
|
||||
new_user_chat_delay: 10 * 60, // Minimum account/IP age to chat
|
||||
new_user_chat_link_delay: 60 * 60 // Minimum account/IP age to post links
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -271,6 +273,20 @@ OptionsModule.prototype.handleSetOptions = function (user, data) {
|
|||
}
|
||||
}
|
||||
|
||||
if ("new_user_chat_delay" in data) {
|
||||
var delay = data.new_user_chat_delay;
|
||||
if (!isNaN(delay) && delay >= 0) {
|
||||
this.opts.new_user_chat_delay = delay;
|
||||
}
|
||||
}
|
||||
|
||||
if ("new_user_chat_link_delay" in data) {
|
||||
var delay = data.new_user_chat_link_delay;
|
||||
if (!isNaN(delay) && delay >= 0) {
|
||||
this.opts.new_user_chat_link_delay = delay;
|
||||
}
|
||||
}
|
||||
|
||||
this.channel.logger.log("[mod] " + user.getName() + " updated channel options");
|
||||
this.sendOpts(this.channel.users);
|
||||
};
|
||||
|
|
|
@ -207,7 +207,7 @@ module.exports = {
|
|||
the hashes match.
|
||||
*/
|
||||
|
||||
db.query("SELECT name,password,global_rank FROM `users` WHERE name=?",
|
||||
db.query("SELECT name,password,global_rank,time FROM `users` WHERE name=?",
|
||||
[name],
|
||||
function (err, rows) {
|
||||
if (err) {
|
||||
|
|
|
@ -15,6 +15,7 @@ var crypto = require("crypto");
|
|||
var isTorExit = require("../tor").isTorExit;
|
||||
var session = require("../session");
|
||||
import counters from '../counters';
|
||||
import { verifyIPSessionCookie } from '../web/middleware/ipsessioncookie';
|
||||
|
||||
var CONNECT_RATE = {
|
||||
burst: 5,
|
||||
|
@ -25,33 +26,54 @@ var ipThrottle = {};
|
|||
// Keep track of number of connections per IP
|
||||
var ipCount = {};
|
||||
|
||||
function parseCookies(socket, accept) {
|
||||
var req = socket.request;
|
||||
if (req.headers.cookie) {
|
||||
cookieParser(req, null, () => {
|
||||
accept(null, true);
|
||||
});
|
||||
} else {
|
||||
req.cookies = {};
|
||||
req.signedCookies = {};
|
||||
accept(null, true);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Called before an incoming socket.io connection is accepted.
|
||||
*/
|
||||
function handleAuth(socket, accept) {
|
||||
var data = socket.request;
|
||||
|
||||
socket.user = false;
|
||||
if (data.headers.cookie) {
|
||||
cookieParser(data, null, function () {
|
||||
var auth = data.signedCookies.auth;
|
||||
if (!auth) {
|
||||
return accept(null, true);
|
||||
}
|
||||
|
||||
session.verifySession(auth, function (err, user) {
|
||||
if (!err) {
|
||||
socket.user = {
|
||||
name: user.name,
|
||||
global_rank: user.global_rank
|
||||
};
|
||||
}
|
||||
accept(null, true);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
accept(null, true);
|
||||
var auth = socket.request.signedCookies.auth;
|
||||
if (!auth) {
|
||||
return accept(null, true);
|
||||
}
|
||||
|
||||
session.verifySession(auth, function (err, user) {
|
||||
if (!err) {
|
||||
socket.user = {
|
||||
name: user.name,
|
||||
global_rank: user.global_rank,
|
||||
registrationTime: new Date(user.time)
|
||||
};
|
||||
}
|
||||
accept(null, true);
|
||||
});
|
||||
}
|
||||
|
||||
function handleIPSessionCookie(socket, accept) {
|
||||
var cookie = socket.request.signedCookies['ip-session'];
|
||||
if (!cookie) {
|
||||
socket.ipSessionFirstSeen = new Date();
|
||||
return accept(null, true);
|
||||
}
|
||||
|
||||
var sessionMatch = verifyIPSessionCookie(socket._realip, cookie);
|
||||
if (sessionMatch) {
|
||||
socket.ipSessionFirstSeen = sessionMatch.date;
|
||||
} else {
|
||||
socket.ipSessionFirstSeen = new Date();
|
||||
}
|
||||
accept(null, true);
|
||||
}
|
||||
|
||||
function throttleIP(sock) {
|
||||
|
@ -214,6 +236,7 @@ function handleConnection(sock) {
|
|||
user.setFlag(Flags.U_REGISTERED);
|
||||
user.clearFlag(Flags.U_READY);
|
||||
user.account.name = sock.user.name;
|
||||
user.registrationTime = sock.user.registrationTime;
|
||||
user.refreshAccount(function (err, account) {
|
||||
if (err) {
|
||||
user.clearFlag(Flags.U_REGISTERED);
|
||||
|
@ -247,8 +270,10 @@ module.exports = {
|
|||
};
|
||||
var io = sio.instance = sio();
|
||||
|
||||
io.use(handleAuth);
|
||||
io.use(ipForwardingMiddleware(webConfig));
|
||||
io.use(parseCookies);
|
||||
io.use(handleIPSessionCookie);
|
||||
io.use(handleAuth);
|
||||
io.on("connection", handleConnection);
|
||||
|
||||
Config.get("listen").forEach(function (bind) {
|
||||
|
|
15
src/user.js
15
src/user.js
|
@ -288,6 +288,7 @@ User.prototype.login = function (name, pw) {
|
|||
}
|
||||
|
||||
self.account.name = user.name;
|
||||
self.registrationTime = new Date(user.time);
|
||||
self.setFlag(Flags.U_REGISTERED);
|
||||
self.refreshAccount(function (err, account) {
|
||||
if (err) {
|
||||
|
@ -449,4 +450,18 @@ User.prototype.refreshAccount = function (cb) {
|
|||
});
|
||||
};
|
||||
|
||||
User.prototype.getFirstSeenTime = function getFirstSeenTime() {
|
||||
if (this.registrationTime && this.socket.ipSessionFirstSeen) {
|
||||
return Math.min(this.registrationTime.getTime(), this.socket.ipSessionFirstSeen.getTime());
|
||||
} else if (this.registrationTime) {
|
||||
return this.registrationTime.getTime();
|
||||
} else if (this.socket.ipSessionFirstSeen) {
|
||||
return this.socket.ipSessionFirstSeen.getTime();
|
||||
} else {
|
||||
Logger.errlog.log(`User "${this.getName()}" (IP: ${this.realip}) has neither ` +
|
||||
"an IP sesion first seen time nor a registered account.");
|
||||
return Date.now();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = User;
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import crypto from 'crypto';
|
||||
|
||||
const STATE_FOLDER_PATH = path.resolve(__dirname, '..', '..', '..', 'state');
|
||||
const SALT_PATH = path.resolve(__dirname, '..', '..', '..', 'state', 'ipsessionsalt.json');
|
||||
|
||||
const NO_EXPIRATION = new Date('Fri, 31 Dec 9999 23:59:59 GMT');
|
||||
var SALT;
|
||||
try {
|
||||
SALT = require(SALT_PATH);
|
||||
} catch (error) {
|
||||
SALT = crypto.randomBytes(32).toString('base64');
|
||||
try {
|
||||
fs.mkdirSync(STATE_FOLDER_PATH);
|
||||
} catch (error) {
|
||||
if (error.code !== 'EEXIST') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(SALT_PATH, JSON.stringify(SALT));
|
||||
}
|
||||
|
||||
function sha256(input) {
|
||||
var hash = crypto.createHash("sha256");
|
||||
hash.update(input);
|
||||
return hash.digest("base64");
|
||||
}
|
||||
|
||||
export function createIPSessionCookie(ip, date) {
|
||||
const hashInput = [
|
||||
ip,
|
||||
date.getTime(),
|
||||
SALT
|
||||
].join(':');
|
||||
|
||||
return [
|
||||
date.getTime(),
|
||||
sha256(hashInput)
|
||||
].join(':');
|
||||
}
|
||||
|
||||
export function verifyIPSessionCookie(ip, cookie) {
|
||||
const parts = cookie.split(':');
|
||||
if (parts.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const timestamp = parseInt(parts[0], 10);
|
||||
if (isNaN(timestamp)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const date = new Date(timestamp);
|
||||
const expected = createIPSessionCookie(ip, date);
|
||||
if (expected !== cookie) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
date: date,
|
||||
};
|
||||
}
|
||||
|
||||
export function ipSessionCookieMiddleware(req, res, next) {
|
||||
var firstSeen = new Date();
|
||||
var hasSession = false;
|
||||
if (req.signedCookies && req.signedCookies['ip-session']) {
|
||||
var sessionMatch = verifyIPSessionCookie(req.realIP, req.signedCookies['ip-session']);
|
||||
if (sessionMatch) {
|
||||
hasSession = true;
|
||||
firstSeen = sessionMatch.date;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasSession) {
|
||||
res.cookie('ip-session', createIPSessionCookie(req.realIP, firstSeen), {
|
||||
signed: true,
|
||||
httpOnly: true,
|
||||
expires: NO_EXPIRATION
|
||||
});
|
||||
}
|
||||
|
||||
req.ipSessionFirstSeen = firstSeen;
|
||||
next();
|
||||
}
|
|
@ -146,6 +146,7 @@ module.exports = {
|
|||
}
|
||||
app.use(cookieParser(webConfig.getCookieSecret()));
|
||||
app.use(csrf.init(webConfig.getCookieDomain()));
|
||||
app.use('/r/:channel', require('./middleware/ipsessioncookie').ipSessionCookieMiddleware);
|
||||
initializeLog(app);
|
||||
require('./middleware/authorize')(app, session);
|
||||
|
||||
|
|
|
@ -46,6 +46,15 @@ mixin textbox-auto(id, label, placeholder)
|
|||
else
|
||||
input.form-control.cs-textbox(id=id, type="text")
|
||||
|
||||
mixin textbox-timeinput-auto(id, label, placeholder)
|
||||
.form-group
|
||||
label.control-label.col-sm-4(for=id)= label
|
||||
.col-sm-8
|
||||
if placeholder
|
||||
input.form-control.cs-textbox-timeinput(id=id, type="text", placeholder=placeholder)
|
||||
else
|
||||
input.form-control.cs-textbox-timeinput(id=id, type="text")
|
||||
|
||||
mixin miscoptions
|
||||
#cs-miscoptions.tab-pane.active
|
||||
h4 General Settings
|
||||
|
@ -63,6 +72,11 @@ mixin miscoptions
|
|||
+rcheckbox-auto("cs-chat_antiflood", "Throttle chat")
|
||||
+textbox-auto("cs-chat_antiflood_burst", "# of messages allowed before throttling")
|
||||
+textbox-auto("cs-chat_antiflood_sustained", "# of messages (after burst) allowed per second")
|
||||
+textbox-timeinput-auto("cs-new_user_chat_delay", "Delay before new accounts can chat", "0")
|
||||
.form-group
|
||||
.col-sm-8.col-sm-offset-4
|
||||
span.text-info Restrictions to new accounts can be disabled by setting the delay to 0.
|
||||
+textbox-timeinput-auto("cs-new_user_chat_link_delay", "Delay before new accounts can post links in chat", "0")
|
||||
.form-group
|
||||
.col-sm-8.col-sm-offset-4
|
||||
span.text-info Changes are automatically saved.
|
||||
|
|
|
@ -88,6 +88,22 @@ Callbacks = {
|
|||
scrollChat();
|
||||
},
|
||||
|
||||
spamFiltered: function(data) {
|
||||
var message = "Spam Filtered.";
|
||||
switch (data.reason) {
|
||||
case "NEW_USER_CHAT":
|
||||
message = "Your account is too new to chat in this channel. " +
|
||||
"Please wait a while and try again.";
|
||||
break;
|
||||
case "NEW_USER_CHAT_LINK":
|
||||
message = "Your account is too new to post links in this channel. " +
|
||||
"Please wait a while and try again.";
|
||||
break;
|
||||
}
|
||||
|
||||
errDialog(message);
|
||||
},
|
||||
|
||||
needPassword: function (wrongpw) {
|
||||
var div = $("<div/>");
|
||||
$("<strong/>").text("Channel Password")
|
||||
|
|
30
www/js/ui.js
30
www/js/ui.js
|
@ -636,6 +636,36 @@ $(".cs-textbox").keyup(function () {
|
|||
}, 1000);
|
||||
});
|
||||
|
||||
$(".cs-textbox-timeinput").keyup(function (event) {
|
||||
var box = $(this);
|
||||
var key = box.attr("id").replace("cs-", "");
|
||||
var value = box.val();
|
||||
var lastkey = Date.now();
|
||||
box.data("lastkey", lastkey);
|
||||
|
||||
setTimeout(function () {
|
||||
if (box.data("lastkey") !== lastkey || box.val() !== value) {
|
||||
return;
|
||||
}
|
||||
|
||||
$("#cs-textbox-timeinput-validation-error-" + key).remove();
|
||||
$(event.target).parent().removeClass("has-error");
|
||||
var data = {};
|
||||
try {
|
||||
data[key] = parseTimeout(value);
|
||||
} catch (error) {
|
||||
var msg = "Invalid timespan value '" + value + "'. Please use the format " +
|
||||
"HH:MM:SS or enter a single number for the number of seconds.";
|
||||
var validationError = $("<p/>").addClass("text-danger").text(msg)
|
||||
.attr("id", "cs-textbox-timeinput-validation-error-" + key);
|
||||
validationError.insertAfter(event.target);
|
||||
$(event.target).parent().addClass("has-error");
|
||||
return;
|
||||
}
|
||||
socket.emit("setOptions", data);
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
$("#cs-chanlog-refresh").click(function () {
|
||||
socket.emit("readChanLog");
|
||||
});
|
||||
|
|
|
@ -761,18 +761,23 @@ function applyOpts() {
|
|||
}
|
||||
}
|
||||
|
||||
function showPollMenu() {
|
||||
function parseTimeout(t) {
|
||||
var m;
|
||||
if (m = t.match(/^(\d+):(\d+)$/)) {
|
||||
return parseInt(m[1], 10) * 60 + parseInt(m[2], 10);
|
||||
} else if (m = t.match(/^(\d+)$/)) {
|
||||
return parseInt(m[1], 10);
|
||||
} else {
|
||||
throw new Error("Invalid timeout value '" + t + "'");
|
||||
}
|
||||
function parseTimeout(t) {
|
||||
var m;
|
||||
if (m = t.match(/^(\d+):(\d+):(\d+)$/)) {
|
||||
// HH:MM:SS
|
||||
return parseInt(m[1], 10) * 3600 + parseInt(m[2], 10) * 60 + parseInt(m[3], 10);
|
||||
} else if (m = t.match(/^(\d+):(\d+)$/)) {
|
||||
// MM:SS
|
||||
return parseInt(m[1], 10) * 60 + parseInt(m[2], 10);
|
||||
} else if (m = t.match(/^(\d+)$/)) {
|
||||
// Seconds
|
||||
return parseInt(m[1], 10);
|
||||
} else {
|
||||
throw new Error("Invalid timeout value '" + t + "'");
|
||||
}
|
||||
}
|
||||
|
||||
function showPollMenu() {
|
||||
$("#pollwrap .poll-menu").remove();
|
||||
var menu = $("<div/>").addClass("well poll-menu")
|
||||
.prependTo($("#pollwrap"));
|
||||
|
@ -932,6 +937,8 @@ function handleModPermissions() {
|
|||
$("#cs-torbanned").prop("checked", CHANNEL.opts.torbanned);
|
||||
$("#cs-allow_ascii_control").prop("checked", CHANNEL.opts.allow_ascii_control);
|
||||
$("#cs-playlist_max_per_user").val(CHANNEL.opts.playlist_max_per_user || 0);
|
||||
$("#cs-new_user_chat_delay").val(formatTime(CHANNEL.opts.new_user_chat_delay || 0));
|
||||
$("#cs-new_user_chat_link_delay").val(formatTime(CHANNEL.opts.new_user_chat_link_delay || 0));
|
||||
(function() {
|
||||
if(typeof CHANNEL.opts.maxlength != "number") {
|
||||
$("#cs-maxlength").val("");
|
||||
|
|
Loading…
Reference in New Issue