Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ tls-rust = [
encoding = ["dep:encoding", "irc-proto/encoding"]

[dependencies]
base64ct = { version = "1.6.0", features = ["std"] }
chrono = { version = "0.4.24", default-features = false, features = [
"clock",
"std",
Expand Down
1 change: 1 addition & 0 deletions src/client/data/client_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"username": "test",
"realname": "test",
"password": "",
"sasl": "none",
"server": "irc.test.net",
"port": 6667,
"encoding": "UTF-8",
Expand Down
1 change: 1 addition & 0 deletions src/client/data/client_config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ realname = "test"
server = "irc.test.net"
port = 6667
password = ""
sasl = "none"
encoding = "UTF-8"
channels = ["#test", "#test2"]
umodes = "+BR"
Expand Down
1 change: 1 addition & 0 deletions src/client/data/client_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ realname: test
server: irc.test.net
port: 6667
password: ""
sasl: none
encoding: UTF-8
channels:
- "#test"
Expand Down
25 changes: 24 additions & 1 deletion src/client/data/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use toml;

#[cfg(feature = "proxy")]
use crate::client::data::proxy::ProxyType;
use crate::client::data::sasl::SASLMode;

use crate::error::Error::InvalidConfig;
#[cfg(feature = "toml_config")]
Expand Down Expand Up @@ -97,9 +98,16 @@ pub struct Config {
/// The port to connect on.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub port: Option<u16>,
/// The password to connect to the server.
/// The password to connect to the server. Used with PASS or with SASL authentication. Those
/// two modes are exclusive of each other.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub password: Option<String>,
/// The login to connect to the server (used with SASL plain)
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub login: Option<String>,
/// SASL authentication mode. Only SASL PLAIN is is supported
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub sasl: Option<SASLMode>,
/// The proxy type to connect to.
#[cfg(feature = "proxy")]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
Expand Down Expand Up @@ -467,6 +475,18 @@ impl Config {
self.password.as_ref().map_or("", String::as_str)
}

/// Gets the server login specified in the configuration.
/// This defaults to an empty string when not specified.
pub fn login(&self) -> &str {
self.login.as_ref().map_or("", String::as_str)
}

/// Gets the SASL mode specified in the configuration.
/// This defaults to None when not specified.
pub fn sasl(&self) -> SASLMode {
self.sasl.as_ref().cloned().unwrap_or(SASLMode::None)
}

/// Gets the type of the proxy specified in the configuration.
/// This defaults to a None ProxyType when not specified.
#[cfg(feature = "proxy")]
Expand Down Expand Up @@ -646,6 +666,8 @@ impl Config {

#[cfg(test)]
mod test {
use crate::client::data::sasl::SASLMode;

use super::Config;
use std::collections::HashMap;

Expand All @@ -664,6 +686,7 @@ mod test {
username: Some("test".to_string()),
realname: Some("test".to_string()),
password: Some(String::new()),
sasl: Some(SASLMode::None),
umodes: Some("+BR".to_string()),
server: Some("irc.test.net".to_string()),
port: Some(6667),
Expand Down
1 change: 1 addition & 0 deletions src/client/data/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ pub use crate::client::data::user::{AccessLevel, User};
pub mod config;
#[cfg(feature = "proxy")]
pub mod proxy;
pub mod sasl;
pub mod user;
31 changes: 31 additions & 0 deletions src/client/data/sasl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//! SASL authentication support
//!
//! ```
//! use irc::client::prelude::Config;
//! use irc::client::data::sasl::SASLMode;
//!
//! # fn main() {
//! let config = Config {
//! nickname: Some("test".to_owned()),
//! server: Some("irc.example.com".to_owned()),
//! login: Some("server_login".to_owned()),
//! password: Some("server_password".to_owned()),
//! sasl: Some(SASLMode::Plain),
//! ..Config::default()
//! };
//! # }
//! ```
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

/// An enum which defines which type of SASL authentication mode should be used.
#[derive(Clone, PartialEq, Debug)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
pub enum SASLMode {
/// Do not use any SASL auth
None,
/// Authenticate in SASL PLAIN mode
Plain,
}
56 changes: 53 additions & 3 deletions src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
//! # }
//! ```

use base64ct::{Base64, Encoding};
#[cfg(feature = "ctcp")]
use chrono::prelude::*;
use futures_util::{
Expand All @@ -72,6 +73,7 @@ use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
use crate::{
client::{
conn::Connection,
data::sasl::SASLMode,
data::{Config, User},
},
error,
Expand Down Expand Up @@ -1070,11 +1072,37 @@ impl Client {
}

/// Sends a CAP END, NICK and USER to identify.
/// Also does the authentication with PASS or SASL if enabled
pub fn identify(&self) -> error::Result<()> {
// Send a CAP END to signify that we're IRCv3-compliant (and to end negotiations!).
if let SASLMode::Plain = self.config().sasl() {
// Best effort no state-machine (blind) SASL auth
if self.config().login().contains('\0') {
return Err(error::Error::LoginContainsNullByte);
}
if self.config().password().contains('\0') {
return Err(error::Error::PasswordContainsNullByte);
}
// No need to ask server for its capabilities, we won't parse the output
//self.send_cap_ls(NegotiationVersion::V301)?;
self.send_cap_req(&[Capability::Sasl])?;
self.send_sasl_plain()?;
let sasl_pass = Base64::encode_string(
&format!(
"\x00{}\x00{}",
self.config().login(),
self.config().password(),
)
.bytes()
.collect::<Vec<_>>(),
);
self.send_sasl(sasl_pass)?;
}
self.send(CAP(None, END, None, None))?;
if self.config().password() != "" {
self.send(PASS(self.config().password().to_owned()))?;
if let SASLMode::None = self.config().sasl() {
if self.config().password() != "" {
self.send(PASS(self.config().password().to_owned()))?;
}
}
self.send(NICK(self.config().nickname()?.to_owned()))?;
self.send(USER(
Expand All @@ -1097,7 +1125,7 @@ mod test {
#[cfg(feature = "channel-lists")]
use crate::client::data::User;
use crate::{
client::data::Config,
client::data::{sasl::SASLMode, Config},
error::Error,
proto::{
command::Command::{Raw, PRIVMSG},
Expand Down Expand Up @@ -1658,6 +1686,28 @@ mod test {
Ok(())
}

#[tokio::test]
async fn identify_with_sasl() -> Result<()> {
let sasl_config = Config {
login: Some("test_login".to_string()),
password: Some("test_password".to_string()),
sasl: Some(SASLMode::Plain),
..test_config()
};
let mut client = Client::from_config(sasl_config).await?;
client.identify()?;
client.stream()?.collect().await?;
assert_eq!(
&get_client_value(client)[..],
// echo -ne "\x00test_login\x00test_password" | base64
"CAP REQ sasl\r\nAUTHENTICATE PLAIN\r\n\
AUTHENTICATE AHRlc3RfbG9naW4AdGVzdF9wYXNzd29yZA==\r\n\
CAP END\r\nNICK test\r\n\
USER test 0 * test\r\n"
);
Ok(())
}

#[tokio::test]
async fn identify_with_password() -> Result<()> {
let mut client = Client::from_config(Config {
Expand Down
8 changes: 8 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,14 @@ pub enum Error {
/// Stream has already been configured.
#[error("stream has already been configured")]
StreamAlreadyConfigured,

/// Login contains null bytes
#[error("Login contains null '\\0' byte, which makes SASL plain authentication impossible")]
LoginContainsNullByte,

/// Password contains null bytes
#[error("Password contains null '\\0' byte, which makes SASL plain authentication impossible")]
PasswordContainsNullByte,
}

/// Errors that occur with configurations.
Expand Down
Loading