Adding a register, community, and forum rate limit, based on IP.

- Fixes #124
This commit is contained in:
Dessalines 2019-05-01 15:44:39 -07:00
parent 222bee9075
commit ea6b64f9fd
3 changed files with 115 additions and 17 deletions

View file

@ -29,7 +29,8 @@ fn chat_route(req: &HttpRequest<WsChatSessionState>) -> Result<HttpResponse, Err
req, req,
WSSession { WSSession {
id: 0, id: 0,
hb: Instant::now() hb: Instant::now(),
ip: req.connection_info().host().to_string(),
}, },
) )
} }
@ -37,6 +38,7 @@ fn chat_route(req: &HttpRequest<WsChatSessionState>) -> Result<HttpResponse, Err
struct WSSession { struct WSSession {
/// unique session id /// unique session id
id: usize, id: usize,
ip: String,
/// Client must send ping at least once per 10 seconds (CLIENT_TIMEOUT), /// Client must send ping at least once per 10 seconds (CLIENT_TIMEOUT),
/// otherwise we drop connection. /// otherwise we drop connection.
hb: Instant hb: Instant
@ -61,6 +63,7 @@ impl Actor for WSSession {
.addr .addr
.send(Connect { .send(Connect {
addr: addr.recipient(), addr: addr.recipient(),
ip: self.ip.to_owned(),
}) })
.into_actor(self) .into_actor(self)
.then(|res, act, ctx| { .then(|res, act, ctx| {
@ -76,7 +79,10 @@ impl Actor for WSSession {
fn stopping(&mut self, ctx: &mut Self::Context) -> Running { fn stopping(&mut self, ctx: &mut Self::Context) -> Running {
// notify chat server // notify chat server
ctx.state().addr.do_send(Disconnect { id: self.id }); ctx.state().addr.do_send(Disconnect {
id: self.id,
ip: self.ip.to_owned(),
});
Running::Stop Running::Stop
} }
} }
@ -111,7 +117,7 @@ impl StreamHandler<ws::Message, ws::ProtocolError> for WSSession {
.addr .addr
.send(StandardMessage { .send(StandardMessage {
id: self.id, id: self.id,
msg: m msg: m,
}) })
.into_actor(self) .into_actor(self)
.then(|res, _, ctx| { .then(|res, _, ctx| {
@ -215,7 +221,7 @@ impl WSSession {
// notify chat server // notify chat server
ctx.state() ctx.state()
.addr .addr
.do_send(Disconnect { id: act.id }); .do_send(Disconnect { id: act.id, ip: act.ip.to_owned() });
// stop actor // stop actor
ctx.stop(); ctx.stop();

View file

@ -11,6 +11,7 @@ use bcrypt::{verify};
use std::str::FromStr; use std::str::FromStr;
use diesel::PgConnection; use diesel::PgConnection;
use failure::Error; use failure::Error;
use std::time::{SystemTime};
use {Crud, Joinable, Likeable, Followable, Bannable, Saveable, establish_connection, naive_now, naive_from_unix, SortType, SearchType, has_slurs, remove_slurs}; use {Crud, Joinable, Likeable, Followable, Bannable, Saveable, establish_connection, naive_now, naive_from_unix, SortType, SearchType, has_slurs, remove_slurs};
use actions::community::*; use actions::community::*;
@ -25,6 +26,11 @@ use actions::user_view::*;
use actions::moderator_views::*; use actions::moderator_views::*;
use actions::moderator::*; use actions::moderator::*;
const RATE_LIMIT_MESSAGES: i32 = 30;
const RATE_LIMIT_PER_SECOND: i32 = 60;
const RATE_LIMIT_REGISTER_MESSAGES: i32 = 1;
const RATE_LIMIT_REGISTER_PER_SECOND: i32 = 60;
#[derive(EnumString,ToString,Debug)] #[derive(EnumString,ToString,Debug)]
pub enum UserOperation { pub enum UserOperation {
Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetReplies, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser, Search, MarkAllAsRead Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetReplies, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser, Search, MarkAllAsRead
@ -48,12 +54,14 @@ pub struct WSMessage(pub String);
#[rtype(usize)] #[rtype(usize)]
pub struct Connect { pub struct Connect {
pub addr: Recipient<WSMessage>, pub addr: Recipient<WSMessage>,
pub ip: String,
} }
/// Session is disconnected /// Session is disconnected
#[derive(Message)] #[derive(Message)]
pub struct Disconnect { pub struct Disconnect {
pub id: usize, pub id: usize,
pub ip: String,
} }
/// Send message to specific room /// Send message to specific room
@ -487,10 +495,22 @@ pub struct MarkAllAsRead {
auth: String auth: String
} }
#[derive(Debug)]
pub struct RateLimitBucket {
last_checked: SystemTime,
allowance: f64
}
pub struct SessionInfo {
pub addr: Recipient<WSMessage>,
pub ip: String,
}
/// `ChatServer` manages chat rooms and responsible for coordinating chat /// `ChatServer` manages chat rooms and responsible for coordinating chat
/// session. implementation is super primitive /// session. implementation is super primitive
pub struct ChatServer { pub struct ChatServer {
sessions: HashMap<usize, Recipient<WSMessage>>, // A map from generated random ID to session addr sessions: HashMap<usize, SessionInfo>, // A map from generated random ID to session addr
rate_limits: HashMap<String, RateLimitBucket>,
rooms: HashMap<i32, HashSet<usize>>, // A map from room / post name to set of connectionIDs rooms: HashMap<i32, HashSet<usize>>, // A map from room / post name to set of connectionIDs
rng: ThreadRng, rng: ThreadRng,
} }
@ -502,6 +522,7 @@ impl Default for ChatServer {
ChatServer { ChatServer {
sessions: HashMap::new(), sessions: HashMap::new(),
rate_limits: HashMap::new(),
rooms: rooms, rooms: rooms,
rng: rand::thread_rng(), rng: rand::thread_rng(),
} }
@ -514,8 +535,8 @@ impl ChatServer {
if let Some(sessions) = self.rooms.get(&room) { if let Some(sessions) = self.rooms.get(&room) {
for id in sessions { for id in sessions {
if *id != skip_id { if *id != skip_id {
if let Some(addr) = self.sessions.get(id) { if let Some(info) = self.sessions.get(id) {
let _ = addr.do_send(WSMessage(message.to_owned())); let _ = info.addr.do_send(WSMessage(message.to_owned()));
} }
} }
} }
@ -540,8 +561,50 @@ impl ChatServer {
Ok(()) Ok(())
} }
fn check_rate_limit_register(&mut self, addr: usize) -> Result<(), Error> {
self.check_rate_limit_full(addr, RATE_LIMIT_REGISTER_MESSAGES, RATE_LIMIT_REGISTER_PER_SECOND)
}
fn check_rate_limit(&mut self, addr: usize) -> Result<(), Error> {
self.check_rate_limit_full(addr, RATE_LIMIT_MESSAGES, RATE_LIMIT_PER_SECOND)
}
fn check_rate_limit_full(&mut self, addr: usize, rate: i32, per: i32) -> Result<(), Error> {
if let Some(info) = self.sessions.get(&addr) {
if let Some(rate_limit) = self.rate_limits.get_mut(&info.ip) {
if rate_limit.allowance == -2f64 {
rate_limit.allowance = rate as f64;
};
let current = SystemTime::now();
let time_passed = current.duration_since(rate_limit.last_checked)?.as_secs() as f64;
rate_limit.last_checked = current;
rate_limit.allowance += time_passed * (rate as f64 / per as f64);
if rate_limit.allowance > rate as f64 {
rate_limit.allowance = rate as f64;
}
if rate_limit.allowance < 1.0 {
println!("Rate limited IP: {}, time_passed: {}, allowance: {}", &info.ip, time_passed, rate_limit.allowance);
Err(ErrorMessage {
op: "Rate Limit".to_string(),
message: format!("Too many requests. {} per {} seconds", rate, per),
})?
} else {
rate_limit.allowance -= 1.0;
Ok(())
}
} else {
Ok(())
}
} else {
Ok(())
}
}
} }
/// Make actor from `ChatServer` /// Make actor from `ChatServer`
impl Actor for ChatServer { impl Actor for ChatServer {
/// We are going to use simple Context, we just need ability to communicate /// We are going to use simple Context, we just need ability to communicate
@ -555,14 +618,30 @@ impl Actor for ChatServer {
impl Handler<Connect> for ChatServer { impl Handler<Connect> for ChatServer {
type Result = usize; type Result = usize;
fn handle(&mut self, msg: Connect, _: &mut Context<Self>) -> Self::Result { fn handle(&mut self, msg: Connect, _ctx: &mut Context<Self>) -> Self::Result {
// notify all users in same room // notify all users in same room
// self.send_room_message(&"Main".to_owned(), "Someone joined", 0); // self.send_room_message(&"Main".to_owned(), "Someone joined", 0);
// register session with random id // register session with random id
let id = self.rng.gen::<usize>(); let id = self.rng.gen::<usize>();
self.sessions.insert(id, msg.addr); println!("{} Joined", &msg.ip);
self.sessions.insert(id, SessionInfo {
addr: msg.addr,
ip: msg.ip.to_owned(),
});
if self.rate_limits.get(&msg.ip).is_none() {
self.rate_limits.insert(msg.ip, RateLimitBucket {
last_checked: SystemTime::now(),
allowance: -2f64,
});
}
// for (k,v) in &self.rate_limits {
// println!("{}: {:?}", k,v);
// }
// auto join session to Main room // auto join session to Main room
// self.rooms.get_mut(&"Main".to_owned()).unwrap().insert(id); // self.rooms.get_mut(&"Main".to_owned()).unwrap().insert(id);
@ -572,6 +651,7 @@ impl Handler<Connect> for ChatServer {
} }
} }
/// Handler for Disconnect message. /// Handler for Disconnect message.
impl Handler<Disconnect> for ChatServer { impl Handler<Disconnect> for ChatServer {
type Result = (); type Result = ();
@ -794,10 +874,12 @@ impl Perform for Register {
fn op_type(&self) -> UserOperation { fn op_type(&self) -> UserOperation {
UserOperation::Register UserOperation::Register
} }
fn perform(&self, _chat: &mut ChatServer, _addr: usize) -> Result<String, Error> { fn perform(&self, chat: &mut ChatServer, addr: usize) -> Result<String, Error> {
let conn = establish_connection(); let conn = establish_connection();
chat.check_rate_limit_register(addr)?;
// Make sure passwords match // Make sure passwords match
if &self.password != &self.password_verify { if &self.password != &self.password_verify {
return Err(self.error("Passwords do not match."))? return Err(self.error("Passwords do not match."))?
@ -884,10 +966,12 @@ impl Perform for CreateCommunity {
UserOperation::CreateCommunity UserOperation::CreateCommunity
} }
fn perform(&self, _chat: &mut ChatServer, _addr: usize) -> Result<String, Error> { fn perform(&self, chat: &mut ChatServer, addr: usize) -> Result<String, Error> {
let conn = establish_connection(); let conn = establish_connection();
chat.check_rate_limit_register(addr)?;
let claims = match Claims::decode(&self.auth) { let claims = match Claims::decode(&self.auth) {
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => { Err(_e) => {
@ -1030,10 +1114,12 @@ impl Perform for CreatePost {
UserOperation::CreatePost UserOperation::CreatePost
} }
fn perform(&self, _chat: &mut ChatServer, _addr: usize) -> Result<String, Error> { fn perform(&self, chat: &mut ChatServer, addr: usize) -> Result<String, Error> {
let conn = establish_connection(); let conn = establish_connection();
chat.check_rate_limit_register(addr)?;
let claims = match Claims::decode(&self.auth) { let claims = match Claims::decode(&self.auth) {
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => { Err(_e) => {
@ -1242,6 +1328,8 @@ impl Perform for CreateComment {
let conn = establish_connection(); let conn = establish_connection();
chat.check_rate_limit(addr)?;
let claims = match Claims::decode(&self.auth) { let claims = match Claims::decode(&self.auth) {
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => { Err(_e) => {
@ -1390,7 +1478,7 @@ impl Perform for EditComment {
deleted: self.deleted.to_owned(), deleted: self.deleted.to_owned(),
read: self.read.to_owned(), read: self.read.to_owned(),
updated: if self.read.is_some() { orig_comment.updated } else {Some(naive_now())} updated: if self.read.is_some() { orig_comment.updated } else {Some(naive_now())}
}; };
let _updated_comment = match Comment::update(&conn, self.edit_id, &comment_form) { let _updated_comment = match Comment::update(&conn, self.edit_id, &comment_form) {
Ok(comment) => comment, Ok(comment) => comment,
@ -1500,6 +1588,8 @@ impl Perform for CreateCommentLike {
let conn = establish_connection(); let conn = establish_connection();
chat.check_rate_limit(addr)?;
let claims = match Claims::decode(&self.auth) { let claims = match Claims::decode(&self.auth) {
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => { Err(_e) => {
@ -1628,10 +1718,12 @@ impl Perform for CreatePostLike {
UserOperation::CreatePostLike UserOperation::CreatePostLike
} }
fn perform(&self, _chat: &mut ChatServer, _addr: usize) -> Result<String, Error> { fn perform(&self, chat: &mut ChatServer, addr: usize) -> Result<String, Error> {
let conn = establish_connection(); let conn = establish_connection();
chat.check_rate_limit(addr)?;
let claims = match Claims::decode(&self.auth) { let claims = match Claims::decode(&self.auth) {
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => { Err(_e) => {
@ -2695,7 +2787,7 @@ impl Perform for Search {
}, },
SearchType::Comments => { SearchType::Comments => {
comments = CommentView::list(&conn, comments = CommentView::list(&conn,
&sort, &sort,
None, None,
None, None,
Some(self.q.to_owned()), Some(self.q.to_owned()),
@ -2717,7 +2809,7 @@ impl Perform for Search {
self.page, self.page,
self.limit)?; self.limit)?;
comments = CommentView::list(&conn, comments = CommentView::list(&conn,
&sort, &sort,
None, None,
None, None,
Some(self.q.to_owned()), Some(self.q.to_owned()),

View file

@ -128,7 +128,7 @@ export class Main extends Component<any, MainState> {
{this.trendingCommunities()} {this.trendingCommunities()}
{UserService.Instance.user && this.state.subscribedCommunities.length > 0 && {UserService.Instance.user && this.state.subscribedCommunities.length > 0 &&
<div> <div>
<h5>Subscribed communities</h5> <h5>Subscribed <Link class="text-white" to="/communities">communities</Link></h5>
<ul class="list-inline"> <ul class="list-inline">
{this.state.subscribedCommunities.map(community => {this.state.subscribedCommunities.map(community =>
<li class="list-inline-item"><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li> <li class="list-inline-item"><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li>