Reserve user nicks per user and prevent nick changes to other user's reserved nicks
Diff
migrations/2023010814480_initial-schema.sql | 6 ++++++
src/client.rs | 56 ++++++++++++++++++++++++++++++++++++++++++++++++--------
src/connection.rs | 23 +++++++++++++++++++++++
src/main.rs | 2 +-
src/persistence.rs | 26 ++++++++++++++++++++++++++
src/database/mod.rs | 21 +++++++++++++++++++++
src/persistence/events.rs | 7 +++++++
7 files changed, 116 insertions(+), 25 deletions(-)
@@ -6,6 +6,12 @@
CREATE UNIQUE INDEX users_username ON users(username);
CREATE TABLE user_nicks (
nick VARCHAR(255) NOT NULL PRIMARY KEY,
user INTEGER NOT NULL,
FOREIGN KEY(user) REFERENCES users(id)
);
CREATE TABLE channels (
id INTEGER PRIMARY KEY,
name VARCHAR(255) NOT NULL
@@ -20,7 +20,7 @@
UserNickChange, UserNickChangeInternal,
},
persistence::{
events::{FetchUnseenMessages, FetchUserChannels},
events::{FetchUnseenMessages, FetchUserChannels, ReserveNick},
Persistence,
},
server::Server,
@@ -271,30 +271,44 @@
}
impl Handler<UserNickChangeInternal> for Client {
type Result = ();
type Result = ResponseActFuture<Self, ()>;
#[instrument(parent = &msg.span, skip_all)]
fn handle(&mut self, msg: UserNickChangeInternal, ctx: &mut Self::Context) -> Self::Result {
self.server.do_send(UserNickChange {
client: ctx.address(),
connection: self.connection.clone(),
new_nick: msg.new_nick.clone(),
span: Span::current(),
});
fn handle(&mut self, msg: UserNickChangeInternal, _ctx: &mut Self::Context) -> Self::Result {
self.persistence
.send(ReserveNick {
user_id: self.connection.user_id,
nick: msg.new_nick.clone(),
})
.into_actor(self)
.map(|res, this, ctx| {
if !res.unwrap() {
return;
}
for channel in self.channels.values() {
channel.do_send(UserNickChange {
client: ctx.address(),
connection: self.connection.clone(),
new_nick: msg.new_nick.clone(),
span: Span::current(),
});
}
this.server.do_send(UserNickChange {
client: ctx.address(),
connection: this.connection.clone(),
new_nick: msg.new_nick.clone(),
span: Span::current(),
});
for channel in this.channels.values() {
channel.do_send(UserNickChange {
client: ctx.address(),
connection: this.connection.clone(),
new_nick: msg.new_nick.clone(),
span: Span::current(),
});
}
self.connection.nick = msg.new_nick;
this.connection.nick = msg.new_nick;
})
.boxed_local()
}
}
@@ -1,10 +1,10 @@
use std::{
io::{Error, ErrorKind},
net::SocketAddr,
str::FromStr,
};
use actix::io::FramedWrite;
use actix::{io::FramedWrite, Addr};
use argon2::PasswordHash;
use base64::{prelude::BASE64_STANDARD, Engine};
use const_format::concatcp;
@@ -19,7 +19,10 @@
use tokio_util::codec::FramedRead;
use tracing::{instrument, warn};
use crate::database::verify_password;
use crate::{
database::verify_password,
persistence::{events::ReserveNick, Persistence},
};
pub type MessageStream = FramedRead<ReadHalf<TcpStream>, irc_proto::IrcCodec>;
pub type MessageSink = FramedWrite<Message, WriteHalf<TcpStream>, irc_proto::IrcCodec>;
@@ -91,6 +94,7 @@
s: &mut MessageStream,
write: &mut tokio_util::codec::FramedWrite<WriteHalf<TcpStream>, IrcCodec>,
host: SocketAddr,
persistence: &Addr<Persistence>,
database: sqlx::Pool<sqlx::Any>,
) -> Result<Option<InitiatedConnection>, ProtocolError> {
let mut request = ConnectionRequest {
@@ -189,6 +193,21 @@
if let Some(user_id) = user_id {
initiated.user_id.0 = user_id;
let reserved_nick = persistence
.send(ReserveNick {
user_id: initiated.user_id,
nick: initiated.nick.clone(),
})
.await
.map_err(|e| ProtocolError::Io(Error::new(ErrorKind::InvalidData, e)))?;
if !reserved_nick {
return Err(ProtocolError::Io(Error::new(
ErrorKind::InvalidData,
"nick is already in use by another user",
)));
}
Ok(Some(initiated))
} else {
@@ -147,7 +147,7 @@
let Some(connection) = connection::negotiate_client_connection(&mut read, &mut write, addr, database).await.unwrap() else {
let Some(connection) = connection::negotiate_client_connection(&mut read, &mut write, addr, &persistence, database).await.unwrap() else {
error!("Failed to fully handshake with client, dropping connection");
return;
};
@@ -11,7 +11,7 @@
channel::permissions::Permission,
persistence::events::{
ChannelCreated, ChannelJoined, ChannelMessage, ChannelParted, FetchUnseenMessages,
FetchUserChannelPermissions, FetchUserChannels, SetUserChannelPermissions,
FetchUserChannelPermissions, FetchUserChannels, ReserveNick, SetUserChannelPermissions,
},
};
@@ -277,6 +277,30 @@
.unwrap();
res
})
}
}
impl Handler<ReserveNick> for Persistence {
type Result = ResponseFuture<bool>;
fn handle(&mut self, msg: ReserveNick, _ctx: &mut Self::Context) -> Self::Result {
let database = self.database.clone();
Box::pin(async move {
let (owning_user,): (i64,) = sqlx::query_as(
"INSERT INTO user_nicks (nick, user)
VALUES (?, ?)
ON CONFLICT(nick) DO UPDATE SET nick = nick
RETURNING user",
)
.bind(msg.nick)
.bind(msg.user_id.0)
.fetch_one(&database)
.await
.unwrap();
owning_user == msg.user_id.0
})
}
}
@@ -1,6 +1,8 @@
use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use rand::rngs::OsRng;
use crate::connection::UserId;
@@ -24,6 +26,25 @@
.bind(password_hash)
.fetch_one(conn)
.await
}
pub async fn reserve_nick(
conn: &sqlx::Pool<sqlx::Any>,
nick: &str,
user_id: UserId,
) -> Result<bool, sqlx::Error> {
let (owning_user,): (i64,) = sqlx::query_as(
"INSERT INTO user_nicks (nick, user)
VALUES (?, ?)
ON CONFLICT(nick) DO UPDATE SET nick = nick
RETURNING user",
)
.bind(nick)
.bind(user_id.0)
.fetch_one(conn)
.await?;
Ok(owning_user == user_id.0)
}
@@ -66,3 +66,10 @@
pub user_id: UserId,
pub span: Span,
}
#[derive(Message)]
#[rtype(result = "bool")]
pub struct ReserveNick {
pub user_id: UserId,
pub nick: String,
}