import echoircd — from-scratch irc daemon in native rust
This commit is contained in:
commit
9b12791774
38 changed files with 9757 additions and 0 deletions
398
src/coremods/core_user.rs
Normal file
398
src/coremods/core_user.rs
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
//! core_user — the client registration & session commands: CAP, NICK, USER,
|
||||
//! PING, PONG, QUIT.
|
||||
|
||||
use crate::command::{CmdResult, Command};
|
||||
use crate::numeric::*;
|
||||
use crate::server::Server;
|
||||
use crate::users::{ident_of, valid_nick, Caps};
|
||||
use crate::Uid;
|
||||
|
||||
pub fn commands() -> Vec<Box<dyn Command>> {
|
||||
vec![
|
||||
Box::new(Cap),
|
||||
Box::new(Authenticate),
|
||||
Box::new(Nick),
|
||||
Box::new(UserCmd),
|
||||
Box::new(Ping),
|
||||
Box::new(Pong),
|
||||
Box::new(Quit),
|
||||
Box::new(Away),
|
||||
Box::new(SetName),
|
||||
]
|
||||
}
|
||||
|
||||
struct Away;
|
||||
impl Command for Away {
|
||||
fn name(&self) -> &'static str {
|
||||
"AWAY"
|
||||
}
|
||||
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
|
||||
let msg = params.first().cloned().filter(|m| !m.is_empty());
|
||||
let now_away = msg.is_some();
|
||||
let prefix = match s.users.get_mut(&uid) {
|
||||
Some(u) => {
|
||||
u.flags.away = msg.clone();
|
||||
u.prefix()
|
||||
}
|
||||
None => return CmdResult::Fail,
|
||||
};
|
||||
// away-notify: tell capable peers we went away / came back
|
||||
let line = match &msg {
|
||||
Some(m) => format!(":{prefix} AWAY :{m}"),
|
||||
None => format!(":{prefix} AWAY"),
|
||||
};
|
||||
s.notify_peers(uid, &line, |c| c.away_notify);
|
||||
if now_away {
|
||||
s.numeric(uid, RPL_NOWAWAY, ":You have been marked as being away");
|
||||
} else {
|
||||
s.numeric(uid, RPL_UNAWAY, ":You are no longer marked as being away");
|
||||
}
|
||||
CmdResult::Ok
|
||||
}
|
||||
}
|
||||
|
||||
struct Cap;
|
||||
impl Command for Cap {
|
||||
fn name(&self) -> &'static str {
|
||||
"CAP"
|
||||
}
|
||||
fn min_params(&self) -> usize {
|
||||
1
|
||||
}
|
||||
fn before_reg(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
|
||||
let who = cap_target(s, uid);
|
||||
match params[0].to_ascii_uppercase().as_str() {
|
||||
"LS" => {
|
||||
let cap302 = params.get(1).map(|v| v == "302").unwrap_or(false);
|
||||
if let Some(u) = s.users.get_mut(&uid) {
|
||||
u.cap = true; // hold registration until CAP END
|
||||
u.cap_302 |= cap302;
|
||||
}
|
||||
s.send(
|
||||
uid,
|
||||
format!(":{} CAP {who} LS :{}", s.name, Caps::ls_line(cap302)),
|
||||
);
|
||||
}
|
||||
"REQ" => {
|
||||
if let Some(u) = s.users.get_mut(&uid) {
|
||||
u.cap = true;
|
||||
}
|
||||
let req = params.get(1).cloned().unwrap_or_default();
|
||||
let wanted: Vec<(&str, bool)> = req
|
||||
.split_whitespace()
|
||||
.map(|t| match t.strip_prefix('-') {
|
||||
Some(rest) => (rest, false),
|
||||
None => (t, true),
|
||||
})
|
||||
.collect();
|
||||
// CAP REQ is atomic: ACK the whole set or NAK the whole set
|
||||
if !wanted.is_empty() && wanted.iter().all(|(n, _)| Caps::is_known(n)) {
|
||||
for (name, on) in &wanted {
|
||||
if let Some(u) = s.users.get_mut(&uid) {
|
||||
u.caps.set(name, *on);
|
||||
}
|
||||
}
|
||||
s.send(uid, format!(":{} CAP {who} ACK :{req}", s.name));
|
||||
} else {
|
||||
s.send(uid, format!(":{} CAP {who} NAK :{req}", s.name));
|
||||
}
|
||||
}
|
||||
"LIST" => {
|
||||
let list = s
|
||||
.users
|
||||
.get(&uid)
|
||||
.map(|u| u.caps.enabled())
|
||||
.unwrap_or_default();
|
||||
s.send(uid, format!(":{} CAP {who} LIST :{list}", s.name));
|
||||
}
|
||||
"END" => {
|
||||
if let Some(u) = s.users.get_mut(&uid) {
|
||||
u.cap = false;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
CmdResult::Ok
|
||||
}
|
||||
}
|
||||
|
||||
/// CAP reply target: the nick, or `*` before one is set.
|
||||
fn cap_target(s: &Server, uid: Uid) -> String {
|
||||
s.users
|
||||
.get(&uid)
|
||||
.map(|u| {
|
||||
if u.nick.is_empty() {
|
||||
"*".to_string()
|
||||
} else {
|
||||
u.nick.clone()
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| "*".to_string())
|
||||
}
|
||||
|
||||
/// AUTHENTICATE — the SASL handshake. echoIRCd verifies nothing itself (it has no
|
||||
/// accounts); once a services server is linked over S2S the payload is relayed to
|
||||
/// it and `set_login` applied on success. Until then — exactly like InspIRCd with
|
||||
/// no services — SASL fails cleanly.
|
||||
struct Authenticate;
|
||||
impl Command for Authenticate {
|
||||
fn name(&self) -> &'static str {
|
||||
"AUTHENTICATE"
|
||||
}
|
||||
fn min_params(&self) -> usize {
|
||||
1
|
||||
}
|
||||
fn before_reg(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
|
||||
if !s.users.get(&uid).map(|u| u.caps.sasl).unwrap_or(false) {
|
||||
s.numeric(
|
||||
uid,
|
||||
ERR_SASLFAIL,
|
||||
":You must request the sasl capability first",
|
||||
);
|
||||
return CmdResult::Fail;
|
||||
}
|
||||
let arg = ¶ms[0];
|
||||
let mech = s.users.get(&uid).and_then(|u| u.sasl_mech.clone());
|
||||
match mech {
|
||||
// step 1 — the client picks a mechanism
|
||||
None => {
|
||||
if arg.eq_ignore_ascii_case("PLAIN") {
|
||||
if let Some(u) = s.users.get_mut(&uid) {
|
||||
u.sasl_mech = Some("PLAIN".to_string());
|
||||
}
|
||||
s.send(uid, "AUTHENTICATE +".to_string());
|
||||
CmdResult::Ok
|
||||
} else if arg == "*" {
|
||||
s.numeric(uid, ERR_SASLABORTED, ":SASL authentication aborted");
|
||||
CmdResult::Ok
|
||||
} else {
|
||||
s.numeric(uid, RPL_SASLMECHS, "PLAIN :are available SASL mechanisms");
|
||||
s.numeric(uid, ERR_SASLFAIL, ":Unsupported SASL mechanism");
|
||||
CmdResult::Fail
|
||||
}
|
||||
}
|
||||
// step 2 — the client sends the base64 payload (or aborts with `*`)
|
||||
Some(_) => {
|
||||
if let Some(u) = s.users.get_mut(&uid) {
|
||||
u.sasl_mech = None;
|
||||
}
|
||||
if arg == "*" {
|
||||
s.numeric(uid, ERR_SASLABORTED, ":SASL authentication aborted");
|
||||
return CmdResult::Ok;
|
||||
}
|
||||
if arg.len() > 400 {
|
||||
s.numeric(uid, ERR_SASLTOOLONG, ":SASL message too long");
|
||||
return CmdResult::Fail;
|
||||
}
|
||||
// base64(authzid \0 authcid \0 passwd) — would be relayed to services
|
||||
let _creds = openssl::base64::decode_block(arg).unwrap_or_default();
|
||||
s.numeric(
|
||||
uid,
|
||||
ERR_SASLFAIL,
|
||||
":SASL authentication failed (services are not available)",
|
||||
);
|
||||
CmdResult::Fail
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// SETNAME — change your realname (IRCv3). Broadcast to `setname`-capable peers.
|
||||
struct SetName;
|
||||
impl Command for SetName {
|
||||
fn name(&self) -> &'static str {
|
||||
"SETNAME"
|
||||
}
|
||||
fn min_params(&self) -> usize {
|
||||
1
|
||||
}
|
||||
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
|
||||
let realname = params[0].clone();
|
||||
let prefix = match s.users.get_mut(&uid) {
|
||||
Some(u) => {
|
||||
u.realname = realname.clone();
|
||||
u.prefix()
|
||||
}
|
||||
None => return CmdResult::Fail,
|
||||
};
|
||||
let line = format!(":{prefix} SETNAME :{realname}");
|
||||
if s.users.get(&uid).map(|u| u.caps.setname).unwrap_or(false) {
|
||||
s.send(uid, line.clone());
|
||||
}
|
||||
s.notify_peers(uid, &line, |c| c.setname);
|
||||
CmdResult::Ok
|
||||
}
|
||||
}
|
||||
|
||||
struct Nick;
|
||||
impl Command for Nick {
|
||||
fn name(&self) -> &'static str {
|
||||
"NICK"
|
||||
}
|
||||
fn before_reg(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
|
||||
let Some(newnick) = params.first() else {
|
||||
s.numeric(uid, ERR_NONICKNAMEGIVEN, ":No nickname given");
|
||||
return CmdResult::Fail;
|
||||
};
|
||||
if !valid_nick(newnick) {
|
||||
s.numeric(
|
||||
uid,
|
||||
ERR_ERRONEUSNICKNAME,
|
||||
&format!("{newnick} :Erroneous nickname"),
|
||||
);
|
||||
return CmdResult::Fail;
|
||||
}
|
||||
if let Some(other) = s.find_nick(newnick) {
|
||||
if other != uid {
|
||||
s.numeric(
|
||||
uid,
|
||||
ERR_NICKNAMEINUSE,
|
||||
&format!("{newnick} :Nickname is already in use"),
|
||||
);
|
||||
return CmdResult::Fail;
|
||||
}
|
||||
return CmdResult::Ok; // same nick, no-op
|
||||
}
|
||||
// a nick already held by a user on a linked server is taken too
|
||||
if s.remote_nick.contains_key(&newnick.to_ascii_lowercase()) {
|
||||
s.numeric(
|
||||
uid,
|
||||
ERR_NICKNAMEINUSE,
|
||||
&format!("{newnick} :Nickname is already in use"),
|
||||
);
|
||||
return CmdResult::Fail;
|
||||
}
|
||||
// +N — can't change nick while on a no-nick-change channel (opers bypass)
|
||||
if !s.is_oper(uid) {
|
||||
let blocked = s
|
||||
.users
|
||||
.get(&uid)
|
||||
.map(|u| u.channels.clone())
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.find_map(|k| {
|
||||
s.channels
|
||||
.get(k)
|
||||
.filter(|c| c.modes.no_nick)
|
||||
.map(|c| c.name.clone())
|
||||
});
|
||||
if let Some(cn) = blocked {
|
||||
s.numeric(
|
||||
uid,
|
||||
ERR_CANTCHANGENICK,
|
||||
&format!("{cn} :Cannot change nick while on this channel (+N is set)"),
|
||||
);
|
||||
return CmdResult::Fail;
|
||||
}
|
||||
// extban `n:` — a matched user can't change nick on that channel
|
||||
let chans = s
|
||||
.users
|
||||
.get(&uid)
|
||||
.map(|u| u.channels.clone())
|
||||
.unwrap_or_default();
|
||||
if let Some(k) = chans.into_iter().find(|k| s.extban_active(uid, k, 'n')) {
|
||||
let cn = s.channels.get(&k).map(|c| c.name.clone()).unwrap_or(k);
|
||||
s.numeric(
|
||||
uid,
|
||||
ERR_CANTCHANGENICK,
|
||||
&format!("{cn} :Cannot change nick here (+b n:)"),
|
||||
);
|
||||
return CmdResult::Fail;
|
||||
}
|
||||
// +F nick-change flood — locks nick changes on the channel for 60s
|
||||
if let Some(cn) = s.nickflood_blocked(uid) {
|
||||
s.numeric(
|
||||
uid,
|
||||
ERR_CANTCHANGENICK,
|
||||
&format!("{cn} :Too many nick changes, try later (+F is set)"),
|
||||
);
|
||||
return CmdResult::Fail;
|
||||
}
|
||||
}
|
||||
s.set_nick(uid, newnick);
|
||||
CmdResult::Ok
|
||||
}
|
||||
}
|
||||
|
||||
struct UserCmd;
|
||||
impl Command for UserCmd {
|
||||
fn name(&self) -> &'static str {
|
||||
"USER"
|
||||
}
|
||||
fn min_params(&self) -> usize {
|
||||
4
|
||||
}
|
||||
fn before_reg(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
|
||||
if s.users.get(&uid).map(|u| u.registered).unwrap_or(false) {
|
||||
s.numeric(uid, ERR_ALREADYREGISTERED, ":You may not reregister");
|
||||
return CmdResult::Fail;
|
||||
}
|
||||
let ident = ident_of(¶ms[0]);
|
||||
let realname = params[3].clone();
|
||||
if let Some(u) = s.users.get_mut(&uid) {
|
||||
u.ident = format!("~{ident}");
|
||||
u.realname = realname;
|
||||
}
|
||||
CmdResult::Ok
|
||||
}
|
||||
}
|
||||
|
||||
struct Ping;
|
||||
impl Command for Ping {
|
||||
fn name(&self) -> &'static str {
|
||||
"PING"
|
||||
}
|
||||
fn min_params(&self) -> usize {
|
||||
1
|
||||
}
|
||||
fn before_reg(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
|
||||
s.send(uid, format!(":{} PONG {} :{}", s.name, s.name, params[0]));
|
||||
CmdResult::Ok
|
||||
}
|
||||
}
|
||||
|
||||
struct Pong;
|
||||
impl Command for Pong {
|
||||
fn name(&self) -> &'static str {
|
||||
"PONG"
|
||||
}
|
||||
fn before_reg(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn handle(&self, _s: &mut Server, _uid: Uid, _params: &[String]) -> CmdResult {
|
||||
CmdResult::Ok // keepalive; nothing to do yet
|
||||
}
|
||||
}
|
||||
|
||||
struct Quit;
|
||||
impl Command for Quit {
|
||||
fn name(&self) -> &'static str {
|
||||
"QUIT"
|
||||
}
|
||||
fn before_reg(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
|
||||
let reason = params
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "Client quit".to_string());
|
||||
s.mark_quit(uid, format!("Quit: {reason}"));
|
||||
CmdResult::Ok
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue