~taiite/ellidri

757ddd327fd7abbf92b77a61b872023b86c5d2f3 — Hubert Hirtz a month ago dbb3e49
Cleanup the mess
M README.md => README.md +2 -6
@@ 26,16 26,13 @@ have their own `README.md`.

## Features

- RFC [1459][r1] and [2812][r2] compliance (almost! see [#1][i1])
- IRCv3 support
- IRC over WebSockets
- IRCv3 server
- Configurable via a file that can be reloaded at runtime
- SASL support with SQLite and PostgreSQL
- kawaii messages

[Supported extensions][ext]: `account-notify`, `away-notify`, `batch`,
`cap-notify`, `echo-message`, `extended-join`, `invite-notify`,
`labeled-response`, `message-ids`, `message-tags`, `multi-prefix`, `sasl`,
`labeled-response`, `message-ids`, `message-tags`, `multi-prefix`,
`server-time`, `setname`, `userhost-in-names`

ellidri doesn't support any server-to-server (S2S) protocol.  As such, it is


@@ 46,7 43,6 @@ casemapping.

[r1]: https://tools.ietf.org/html/rfc1459
[r2]: https://tools.ietf.org/html/rfc2812
[i1]: https://todo.sr.ht/~taiite/ellidri/1
[ext]: https://ircv3.net/irc/



M doc/config_full.yaml => doc/config_full.yaml +0 -65
@@ 11,22 11,6 @@
# For an example configuration file, see `config_example.yaml`.


# Whether ellidri can be started in an unsafe state.
#
# If you are using ellidri for a server on production, you want to set this to
# false and solve any problem that may arise when starting ellidri.  If you
# configure ellidri in an unsafe manner, it will display an error message and
# quit.  If you want to start ellidri anyway, set this to true; then ellidri
# will display a warning and continue.
#
# Unsafe configurations are configurations that fulfill any of the following:
# - has a plain-text or WebSocket binding on a public IP,
# - has a TLS binding with a self-signed certificate on a public IP,
#
# The definition of configuration unsafety may change over time.
unsafe: false


# Domain of the IRC server
#
# It is sent through most replies to clients as a prefix, and should be the same


@@ 106,57 90,8 @@ opers:
password: My password can't be this cute!


# Database URL
#
# Specify the URL to the database ellidri should use for SASL.  If unset,
# ellidri will disable SASL.  While ellidri tries its best at all times to serve
# its users, it cannot reload the database once started.  Please bear with it!
#
# The format of the setting is  <driver>://<url>
#
# Supported drivers:
# - mysql
# - postgres (aliases: psql, postgresql)
# - sqlite
#
# For sqlite, the url is the path to the file.  In-memory databases are not supported.
#
# For postgres, the url must follow the key-value format specified at:
# <https://docs.rs/postgres/0.17.0/postgres/config/struct.Config.html>
#
# Simple SQLite example:
#database:
#    driver: sqlite
#    url: sqlite:///var/lib/ellidri/ellidri.db
#
# More complex example with PostgreSQL:
database:
    # The URL to the PostgreSQL instance.  See here for more details:
    # <https://docs.rs/postgres/0.17.0/postgres/config/struct.Config.html>
    url: postgres://host=/run/postgresql,localhost database=ellidri

    # The values below are optional and their default are shown.

    # Disabled by default, this is the number of *milliseconds* ellidri should
    # wait before closing an inactive connection.  You'll want to set this if
    # you are billed on usage of the database.
    idle_timeout:

    # ellidri will not make more connections to the database than this.
    max_pool_size: 10

    # ellidri will make at least this number of connections to the database.
    min_pool_size: 0

    # If a connection to a database cannot be established within this number of
    # *milliseconds*, then ellidri will stop trying to connect and write an
    # error message.
    connect_timeout: 10000


# Misc settings


# Number of worker threads
#
# Worker threads handle incoming connections and messages.  When set to 0,

M doc/setup-guide.md => doc/setup-guide.md +0 -6
@@ 8,7 8,6 @@ a working installation of ellidri.  Namely,
3. Create a user on your system
4. Generate or localize your certificates
5. Write ellidri's configuration file
6. Enable SASL with SQLite

This guide assume the following locations:



@@ 74,8 73,3 @@ After any change you make to the configuration file, you can apply them with
`systemctl reload ellidri`.

[config]: https://git.sr.ht/~taiite/ellidri/tree/master/doc/config_example.yaml


## 6. Enable SASL with SQLite

TODO

M src/client.rs => src/client.rs +11 -81
@@ 70,9 70,7 @@ impl ConnectionState {
        match self {
            ConnectionState::ConnectionEstablished => match request {
                CapLs { .. } | CapReq { .. } => Ok(ConnectionState::CapGiven),
                Authenticate { .. } | CapEnd | CapList { .. } | Pass { .. } | Ping { .. } => {
                    Ok(self)
                }
                CapEnd | CapList { .. } | Pass { .. } | Ping { .. } => Ok(self),
                Nick { .. } => Ok(ConnectionState::NickGiven),
                User { .. } => Ok(ConnectionState::UserGiven),
                Quit { .. } => Ok(ConnectionState::Quit),


@@ 80,33 78,23 @@ impl ConnectionState {
            },
            ConnectionState::NickGiven => match request {
                CapLs { .. } | CapReq { .. } => Ok(ConnectionState::CapGiven),
                Authenticate { .. }
                | CapEnd
                | CapList { .. }
                | Nick { .. }
                | Pass { .. }
                | Ping { .. } => Ok(self),
                CapEnd | CapList { .. } | Nick { .. } | Pass { .. } | Ping { .. } => Ok(self),
                User { .. } => Ok(ConnectionState::Registered),
                Quit { .. } => Ok(ConnectionState::Quit),
                _ => Err(()),
            },
            ConnectionState::UserGiven => match request {
                CapLs { .. } | CapReq { .. } => Ok(ConnectionState::CapGiven),
                Authenticate { .. } | CapEnd | CapList { .. } | Pass { .. } | Ping { .. } => {
                    Ok(self)
                }
                CapEnd | CapList { .. } | Pass { .. } | Ping { .. } => Ok(self),
                Nick { .. } => Ok(ConnectionState::Registered),
                Quit { .. } => Ok(ConnectionState::Quit),
                _ => Err(()),
            },
            ConnectionState::CapGiven => match request {
                CapEnd => Ok(ConnectionState::ConnectionEstablished),
                Authenticate { .. }
                | CapList { .. }
                | CapLs { .. }
                | CapReq { .. }
                | Pass { .. }
                | Ping { .. } => Ok(self),
                CapList { .. } | CapLs { .. } | CapReq { .. } | Pass { .. } | Ping { .. } => {
                    Ok(self)
                }
                Nick { .. } => Ok(ConnectionState::CapNickGiven),
                User { .. } => Ok(ConnectionState::CapUserGiven),
                Quit { .. } => Ok(ConnectionState::Quit),


@@ 114,8 102,7 @@ impl ConnectionState {
            },
            ConnectionState::CapNickGiven => match request {
                CapEnd => Ok(ConnectionState::NickGiven),
                Authenticate { .. }
                | CapList { .. }
                CapList { .. }
                | CapLs { .. }
                | CapReq { .. }
                | Nick { .. }


@@ 127,20 114,16 @@ impl ConnectionState {
            },
            ConnectionState::CapUserGiven => match request {
                CapEnd => Ok(ConnectionState::UserGiven),
                Authenticate { .. }
                | CapList { .. }
                | CapLs { .. }
                | CapReq { .. }
                | Pass { .. }
                | Ping { .. } => Ok(self),
                CapList { .. } | CapLs { .. } | CapReq { .. } | Pass { .. } | Ping { .. } => {
                    Ok(self)
                }
                Nick { .. } => Ok(ConnectionState::CapNegotiation),
                Quit { .. } => Ok(ConnectionState::Quit),
                _ => Err(()),
            },
            ConnectionState::CapNegotiation => match request {
                CapEnd => Ok(ConnectionState::Registered),
                Authenticate { .. }
                | CapList { .. }
                CapList { .. }
                | CapLs { .. }
                | CapReq { .. }
                | Nick { .. }


@@ 163,9 146,6 @@ impl ConnectionState {
    }
}

pub const AUTHENTICATE_CHUNK_LEN: usize = 400;
pub const AUTHENTICATE_WHOLE_LEN: usize = 1024;

const FULL_NAME_LENGTH: usize = 64;

/// Client data.


@@ 182,10 162,6 @@ pub struct Client {
    pub cap_enabled: data::Capabilities,
    state: ConnectionState,

    auth_buffer: String,
    auth_buffer_complete: bool,
    auth_id: Option<usize>,

    nick: String,
    user: String,
    real: String,


@@ 224,9 200,6 @@ impl Client {
            cap_version: data::cap::Version::V300,
            cap_enabled: data::Capabilities::default(),
            state: ConnectionState::default(),
            auth_buffer: String::new(),
            auth_buffer_complete: false,
            auth_id: None,
            nick: String::from("*"),
            user: String::new(),
            real: String::new(),


@@ 282,45 255,6 @@ impl Client {
        self.state == ConnectionState::Registered
    }

    pub fn auth_id(&self) -> Option<usize> {
        self.auth_id
    }

    pub fn auth_set_id(&mut self, auth_id: usize) {
        self.auth_id = Some(auth_id);
    }

    pub fn auth_buffer_push(&mut self, buf: &str) -> Result<bool, ()> {
        if self.auth_buffer_complete {
            self.auth_buffer_complete = false;
            self.auth_buffer.clear();
        }
        if AUTHENTICATE_CHUNK_LEN < buf.len()
            || AUTHENTICATE_WHOLE_LEN < self.auth_buffer.len() + buf.len()
        {
            return Err(());
        }
        if buf != "+" {
            self.auth_buffer.push_str(buf);
        }
        self.auth_buffer_complete = buf.len() < AUTHENTICATE_CHUNK_LEN;
        Ok(self.auth_buffer_complete)
    }

    pub fn auth_buffer_decode(&self) -> Result<Vec<u8>, base64::DecodeError> {
        if !self.auth_buffer_complete {
            return Err(base64::DecodeError::InvalidLength);
        }
        base64::decode(&self.auth_buffer)
    }

    /// Free authentication-related buffers and reset authentication state.
    pub fn auth_reset(&mut self) {
        self.auth_buffer = String::new();
        self.auth_buffer_complete = false;
        self.auth_id = None;
    }

    pub fn full_name(&self) -> &str {
        &self.full_name
    }


@@ 376,10 310,6 @@ impl Client {
        self.account.as_ref().map(|s| s.as_ref())
    }

    pub fn log_in(&mut self, account: String) {
        self.account = Some(account);
    }

    pub fn signon_time(&self) -> u64 {
        self.signon_time
    }

M src/config.rs => src/config.rs +0 -46
@@ 125,9 125,6 @@ pub struct State {
/// The whole configuration.
#[derive(Deserialize, Serialize)]
pub struct Config {
    #[serde(rename = "unsafe", default)]
    pub is_unsafe: bool,

    #[serde(default = "bindings")]
    pub bindings: Vec<Binding>,



@@ 185,50 182,7 @@ fn login_timeout() -> u64 {
    60_000
}

fn db_max_size() -> u32 {
    10
}
fn db_min_size() -> u32 {
    0
}
fn db_connect_timeout() -> u64 {
    10_000
}

impl State {
    pub fn sample() -> Self {
        Self {
            domain: domain(),
            default_chan_mode: default_chan_mode(),
            motd_file: motd_file(),
            password: String::new(),
            opers: Vec::new(),
            org_name: org(),
            org_location: org(),
            org_mail: org(),
            awaylen: awaylen(),
            channellen: channellen(),
            keylen: keylen(),
            kicklen: kicklen(),
            namelen: namelen(),
            nicklen: nicklen(),
            topiclen: topiclen(),
            userlen: userlen(),
            login_timeout: login_timeout(),
        }
    }
}

impl Config {
    pub fn sample() -> Self {
        Self {
            is_unsafe: false,
            bindings: bindings(),
            workers: 0,
            state: State::sample(),
        }
    }

    /// Reads the configuration file at the given path.
    pub fn from_file(path: impl AsRef<path::Path>) -> Result<Self> {
        let contents = fs::read_to_string(path)?;

D src/data/auth.rs => src/data/auth.rs +0 -43
@@ 1,43 0,0 @@
/// Provider errors, used by the `Provider` trait.
#[derive(Debug)]
pub enum Error {
    /// Challenge response is not valid base64.
    BadBase64,

    /// Challenge response does not follow the mechanism's format.
    BadFormat,

    /// Challenge response is well-formed, but incorrect.
    InvalidCredentials,

    /// The provider cannot perform the authentication.
    ProviderUnavailable,

    /// Chosen mechanism is unsupported by the provider.
    UnsupportedMechanism,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Mechanism {
    Plain,
    External,
}

#[derive(Clone, Copy, Debug)]
pub enum Payload<'a> {
    Abort,
    Mechanism(Mechanism),
    Chunk(&'a str),
}

impl<'a> From<&'a str> for Payload<'a> {
    fn from(val: &'a str) -> Self {
        match val {
            "*" => Self::Abort,
            "+" => Self::Chunk(""),
            "PLAIN" => Self::Mechanism(Mechanism::Plain),
            "EXTERNAL" => Self::Mechanism(Mechanism::External),
            val => Self::Chunk(val),
        }
    }
}

M src/data/cap.rs => src/data/cap.rs +0 -4
@@ 150,10 150,6 @@ caps! {
}

impl Capabilities {
    pub fn has_labeled_response(&self) -> bool {
        self.batch && self.labeled_response
    }

    pub fn has_message_tags(&self) -> bool {
        self.message_tags || self.server_time
    }

M src/data/mod.rs => src/data/mod.rs +0 -1
@@ 1,7 1,6 @@
pub use self::cap::Capabilities;
pub use self::req::Request;
pub use self::strings::{ChannelName, HostName, JoinList, Key, List, Mask, Nickname};
pub mod auth;
pub mod cap;
pub mod modes;
pub mod req;

M src/data/req.rs => src/data/req.rs +1 -6
@@ 135,7 135,6 @@ pub enum Request<'a> {
    TopicSet(TopicSet<'a>),

    // Client session related requests.
    Authenticate(auth::Payload<'a>),
    CapLs(cap::Version),
    CapList,
    CapReq(cap::Diff),


@@ 242,10 241,7 @@ impl<'a> Request<'a> {
                }
            }

            Command::Authenticate => {
                let payload = auth::Payload::from(msg.params[0]);
                Self::Authenticate(payload)
            }
            Command::Authenticate => return Err(Error::UnknownCommand("AUTHENTICATE")),
            Command::Cap => match msg.params[0] {
                "LS" => {
                    let version = cap::Version::from(msg.params[1]);


@@ 427,7 423,6 @@ impl<'a> Request<'a> {
            Self::TopicSet(_) => 7,

            // Client session related requests.
            Self::Authenticate(_) => 16,
            Self::CapLs(_) => 1,
            Self::CapList => 1,
            Self::CapReq(_) => 1,

M src/data/strings.rs => src/data/strings.rs +0 -10
@@ 52,10 52,6 @@ impl Mask<'_> {
        self.0
    }

    pub fn u(&self) -> &UniCase<str> {
        u(self.0)
    }

    pub fn is_channel(&self) -> bool {
        let first = self.0.chars().next().unwrap();
        is_namespace(first)


@@ 152,12 148,6 @@ impl<'a> TryFrom<&'a str> for Key<'a> {
#[derive(Clone, Copy, Debug)]
pub struct HostName<'a>(&'a str);

impl HostName<'_> {
    pub fn get(&self) -> &str {
        self.0
    }
}

impl<'a> TryFrom<&'a str> for HostName<'a> {
    type Error = ();


M src/lines.rs => src/lines.rs +0 -16
@@ 164,22 164,6 @@ macro_rules! lines_welcome {
    };
}

//
// SASL
//

pub const SASL_ABORTED: &str = "ABORT BAKA";

pub const SASL_ALREADY: &str = "I can't authenticate you again senpai!";

pub const SASL_FAILED: &str = "it's not like I wanted to do my best for you, but it didn't worked";

pub const SASL_MECHS: &str = "please use these to authenticate!";

pub const SASL_SUCCESSFUL: &str = "sugoi~~! looks like it worked!";

pub const SASL_TOO_LONG: &str = "senpai, it's too big!";

#[macro_export]
macro_rules! lines_logged_in {
    ( $user:expr ) => {

M src/state/mod.rs => src/state/mod.rs +0 -1
@@ 389,7 389,6 @@ impl StateInner {
            Request::TopicSet(args) => self.cmd_topic_set(ctx, args),

            // Client session related requests.
            Request::Authenticate(args) => self.cmd_authenticate(ctx, args),
            Request::CapLs(args) => self.cmd_cap_ls(ctx, args),
            Request::CapList => self.cmd_cap_list(ctx),
            Request::CapReq(args) => self.cmd_cap_req(ctx, args),

M src/state/v3.rs => src/state/v3.rs +0 -11
@@ 50,17 50,6 @@ impl super::StateInner {
    }
}

/// Handlers for commands related to SASL specifications.
impl super::StateInner {
    pub fn cmd_authenticate(
        &mut self,
        _ctx: CommandContext<'_>,
        _payload: data::auth::Payload<'_>,
    ) -> Result {
        todo!()
    }
}

/// Handlers for commands related to the setname specification.
impl super::StateInner {
    pub fn cmd_setname(&mut self, ctx: CommandContext<'_>, realname: &str) -> Result {