forked from lavina/lavina
1
0
Fork 0

Compare commits

...

2 Commits

Author SHA1 Message Date
Nikita Vilunov ce3c49cff6 move sasl into separate crate 2023-10-13 16:54:08 +02:00
Nikita Vilunov dccb0b0f30 irc: sasl 2023-10-13 16:31:01 +02:00
10 changed files with 177 additions and 99 deletions

View File

@ -12,7 +12,7 @@ jobs:
uses: https://github.com/actions-rs/cargo@v1 uses: https://github.com/actions-rs/cargo@v1
with: with:
command: fmt command: fmt
args: "--check -p mgmt-api -p lavina-core -p projection-irc" args: "--check -p mgmt-api -p lavina-core -p projection-irc -p projection-xmpp -p sasl"
- name: cargo check - name: cargo check
uses: https://github.com/actions-rs/cargo@v1 uses: https://github.com/actions-rs/cargo@v1
with: with:

10
Cargo.lock generated
View File

@ -1286,6 +1286,7 @@ dependencies = [
"proto-xmpp", "proto-xmpp",
"quick-xml", "quick-xml",
"rustls-pemfile", "rustls-pemfile",
"sasl",
"serde", "serde",
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
@ -1325,7 +1326,6 @@ version = "0.0.2-dev"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"assert_matches", "assert_matches",
"base64",
"derive_more", "derive_more",
"lazy_static", "lazy_static",
"quick-xml", "quick-xml",
@ -1557,6 +1557,14 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
name = "sasl"
version = "0.0.2-dev"
dependencies = [
"anyhow",
"base64",
]
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"

View File

@ -6,6 +6,7 @@ members = [
"crates/projection-irc", "crates/projection-irc",
"crates/proto-xmpp", "crates/proto-xmpp",
"crates/mgmt-api", "crates/mgmt-api",
"crates/sasl",
] ]
[workspace.package] [workspace.package]
@ -28,6 +29,7 @@ tracing = "0.1.37" # logging & tracing api
prometheus = { version = "0.13.3", default-features = false } prometheus = { version = "0.13.3", default-features = false }
base64 = "0.21.3" base64 = "0.21.3"
lavina-core = { path = "crates/lavina-core" } lavina-core = { path = "crates/lavina-core" }
sasl = { path = "crates/sasl" }
[package] [package]
name = "lavina" name = "lavina"

View File

@ -127,3 +127,48 @@ async fn scenario_basic() -> Result<()> {
server.server.terminate().await?; server.server.terminate().await?;
Ok(()) Ok(())
} }
#[tokio::test]
async fn scenario_cap_negotiation() -> Result<()> {
let mut server = TestServer::start().await?;
// test scenario
server.storage.create_user("tester").await?;
server.storage.set_password("tester", "password").await?;
let mut stream = TcpStream::connect(server.server.addr).await?;
let mut s = TestScope::new(&mut stream);
s.send("CAP LS 302").await?;
s.send("NICK tester").await?;
s.send("USER UserName 0 * :Real Name").await?;
s.expect(":testserver CAP * LS :sasl=PLAIN").await?;
s.send("CAP REQ :sasl").await?;
s.expect(":testserver CAP tester ACK :sasl=PLAIN").await?;
s.send("AUTHENTICATE PLAIN").await?;
s.expect("AUTHENTICATE +").await?;
s.send("AUTHENTICATE dGVzdGVyAHRlc3RlcgBwYXNzd29yZA==").await?; // base64-encoded 'tester\x00tester\x00password'
s.expect(":testserver 900 tester ??? ??? :You are now logged in as tester").await?;
s.expect(":testserver 903 tester :SASL authentication successful").await?;
s.send("CAP END").await?;
s.expect(":testserver NOTICE * :Welcome to my server!").await?;
s.expect(":testserver 001 tester :Welcome to Kek Server").await?;
s.expect(":testserver 002 tester :Welcome to Kek Server").await?;
s.expect(":testserver 003 tester :Welcome to Kek Server").await?;
s.expect(":testserver 004 tester testserver kek-0.1.alpha.3 r CFILPQbcefgijklmnopqrstvz").await?;
s.expect(":testserver 005 tester CHANTYPES=# :are supported by this server").await?;
s.expect_nothing().await?;
s.send("QUIT :Leaving").await?;
s.expect(":testserver ERROR :Leaving the server").await?;
s.expect_eof().await?;
stream.shutdown().await?;
// wrap up
server.server.terminate().await?;
Ok(())
}

View File

@ -13,6 +13,7 @@ prometheus.workspace = true
futures-util.workspace = true futures-util.workspace = true
quick-xml.workspace = true quick-xml.workspace = true
sasl.workspace = true
proto-xmpp = { path = "../proto-xmpp" } proto-xmpp = { path = "../proto-xmpp" }
uuid = { version = "1.3.0", features = ["v4"] } uuid = { version = "1.3.0", features = ["v4"] }
tokio-rustls = "0.24.1" tokio-rustls = "0.24.1"

View File

@ -24,17 +24,17 @@ use tokio_rustls::TlsAcceptor;
use lavina_core::player::{PlayerConnection, PlayerId, PlayerRegistry}; use lavina_core::player::{PlayerConnection, PlayerId, PlayerRegistry};
use lavina_core::prelude::*; use lavina_core::prelude::*;
use lavina_core::repo::Storage;
use lavina_core::room::{RoomId, RoomRegistry}; use lavina_core::room::{RoomId, RoomRegistry};
use lavina_core::terminator::Terminator; use lavina_core::terminator::Terminator;
use lavina_core::repo::Storage;
use proto_xmpp::bind::{BindResponse, Jid, Name, Resource, Server}; use proto_xmpp::bind::{BindResponse, Jid, Name, Resource, Server};
use proto_xmpp::client::{Iq, Message, MessageType, Presence}; use proto_xmpp::client::{Iq, Message, MessageType, Presence};
use proto_xmpp::disco::*; use proto_xmpp::disco::*;
use proto_xmpp::roster::RosterQuery; use proto_xmpp::roster::RosterQuery;
use proto_xmpp::sasl::AuthBody;
use proto_xmpp::session::Session; use proto_xmpp::session::Session;
use proto_xmpp::stream::*; use proto_xmpp::stream::*;
use proto_xmpp::xml::{Continuation, FromXml, Parser, ToXml}; use proto_xmpp::xml::{Continuation, FromXml, Parser, ToXml};
use sasl::AuthBody;
use self::proto::{ClientPacket, IqClientBody}; use self::proto::{ClientPacket, IqClientBody};
@ -286,8 +286,8 @@ async fn socket_auth(
xmpp_resource: Resource(name.to_string().into()), xmpp_resource: Resource(name.to_string().into()),
xmpp_muc_name: Resource(name.to_string().into()), xmpp_muc_name: Resource(name.to_string().into()),
}) })
}, }
Err(e) => return Err(e) Err(e) => return Err(e),
} }
} }

View File

@ -10,7 +10,6 @@ regex.workspace = true
anyhow.workspace = true anyhow.workspace = true
tokio.workspace = true tokio.workspace = true
derive_more.workspace = true derive_more.workspace = true
base64.workspace = true
[dev-dependencies] [dev-dependencies]
assert_matches.workspace = true assert_matches.workspace = true

View File

@ -1,14 +1,11 @@
use std::borrow::Borrow; use std::borrow::Borrow;
use quick_xml::{ use anyhow::{anyhow, Result};
events::{BytesStart, Event}, use quick_xml::events::{BytesStart, Event};
NsReader, Writer, use quick_xml::{NsReader, Writer};
};
use tokio::io::{AsyncBufRead, AsyncWrite}; use tokio::io::{AsyncBufRead, AsyncWrite};
use base64::{Engine as _, engine::general_purpose};
use super::skip_text; use super::skip_text;
use anyhow::{anyhow, Result};
pub enum Mechanism { pub enum Mechanism {
Plain, Plain,
@ -29,98 +26,13 @@ impl Mechanism {
} }
} }
#[derive(PartialEq, Debug)]
pub struct AuthBody {
pub login: String,
pub password: String,
}
impl AuthBody {
pub fn from_str(input: &[u8]) -> Result<AuthBody> {
match general_purpose::STANDARD.decode(input){
Ok(decoded_body) => {
match String::from_utf8(decoded_body) {
Ok(parsed_to_string) => {
let separated_words: Vec<&str> = parsed_to_string.split("\x00").collect::<Vec<_>>().clone();
if separated_words.len() == 3 {
// first segment ignored (might be needed in the future)
Ok(AuthBody { login: separated_words[1].to_string(), password: separated_words[2].to_string() })
} else { return Err(anyhow!("Incorrect auth format")) }
},
Err(e) => return Err(anyhow!(e))
}
},
Err(e) => return Err(anyhow!(e))
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_returning_auth_body() {
let orig = b"\x00login\x00pass";
let encoded = general_purpose::STANDARD.encode(orig);
let expected = AuthBody {login: "login".to_string(), password: "pass".to_string()};
let result = AuthBody::from_str(encoded.as_bytes()).unwrap();
assert_eq!(expected, result);
}
#[test]
fn test_ignoring_first_segment() {
let orig = b"ignored\x00login\x00pass";
let encoded = general_purpose::STANDARD.encode(orig);
let expected = AuthBody {login: "login".to_string(), password: "pass".to_string()};
let result = AuthBody::from_str(encoded.as_bytes()).unwrap();
assert_eq!(expected, result);
}
#[test]
fn test_returning_auth_body_with_empty_strings() {
let orig = b"\x00\x00";
let encoded = general_purpose::STANDARD.encode(orig);
let expected = AuthBody {login: "".to_string(), password: "".to_string()};
let result = AuthBody::from_str(encoded.as_bytes()).unwrap();
assert_eq!(expected, result);
}
#[test]
fn test_fail_if_size_less_then_3() {
let orig = b"login\x00pass";
let encoded = general_purpose::STANDARD.encode(orig);
let expected = AuthBody {login: "login".to_string(), password: "pass".to_string()};
let result = AuthBody::from_str(encoded.as_bytes());
assert!(result.is_err());
}
#[test]
fn test_fail_if_size_greater_then_3() {
let orig = b"first\x00login\x00pass\x00other";
let encoded = general_purpose::STANDARD.encode(orig);
let expected = AuthBody {login: "login".to_string(), password: "pass".to_string()};
let result = AuthBody::from_str(encoded.as_bytes());
assert!(result.is_err());
}
}
pub struct Auth { pub struct Auth {
pub mechanism: Mechanism, pub mechanism: Mechanism,
pub body: Vec<u8>, pub body: Vec<u8>,
} }
impl Auth { impl Auth {
pub async fn parse( pub async fn parse(reader: &mut NsReader<impl AsyncBufRead + Unpin>, buf: &mut Vec<u8>) -> Result<Auth> {
reader: &mut NsReader<impl AsyncBufRead + Unpin>,
buf: &mut Vec<u8>,
) -> Result<Auth> {
let event = skip_text!(reader, buf); let event = skip_text!(reader, buf);
let mechanism = if let Event::Start(bytes) = event { let mechanism = if let Event::Start(bytes) = event {
let mut mechanism = None; let mut mechanism = None;

8
crates/sasl/Cargo.toml Normal file
View File

@ -0,0 +1,8 @@
[package]
name = "sasl"
edition = "2021"
version.workspace = true
[dependencies]
anyhow.workspace = true
base64.workspace = true

103
crates/sasl/src/lib.rs Normal file
View File

@ -0,0 +1,103 @@
use anyhow::{anyhow, Result};
use base64::engine::general_purpose;
use base64::Engine;
#[derive(PartialEq, Debug)]
pub struct AuthBody {
pub login: String,
pub password: String,
}
impl AuthBody {
pub fn from_str(input: &[u8]) -> Result<AuthBody> {
match general_purpose::STANDARD.decode(input) {
Ok(decoded_body) => {
match String::from_utf8(decoded_body) {
Ok(parsed_to_string) => {
let separated_words: Vec<&str> = parsed_to_string.split("\x00").collect::<Vec<_>>().clone();
if separated_words.len() == 3 {
// first segment ignored (might be needed in the future)
Ok(AuthBody {
login: separated_words[1].to_string(),
password: separated_words[2].to_string(),
})
} else {
return Err(anyhow!("Incorrect auth format"));
}
}
Err(e) => return Err(e.into()),
}
}
Err(e) => return Err(e.into()),
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_returning_auth_body() {
let orig = b"\x00login\x00pass";
let encoded = general_purpose::STANDARD.encode(orig);
let expected = AuthBody {
login: "login".to_string(),
password: "pass".to_string(),
};
let result = AuthBody::from_str(encoded.as_bytes()).unwrap();
assert_eq!(expected, result);
}
#[test]
fn test_ignoring_first_segment() {
let orig = b"ignored\x00login\x00pass";
let encoded = general_purpose::STANDARD.encode(orig);
let expected = AuthBody {
login: "login".to_string(),
password: "pass".to_string(),
};
let result = AuthBody::from_str(encoded.as_bytes()).unwrap();
assert_eq!(expected, result);
}
#[test]
fn test_returning_auth_body_with_empty_strings() {
let orig = b"\x00\x00";
let encoded = general_purpose::STANDARD.encode(orig);
let expected = AuthBody {
login: "".to_string(),
password: "".to_string(),
};
let result = AuthBody::from_str(encoded.as_bytes()).unwrap();
assert_eq!(expected, result);
}
#[test]
fn test_fail_if_size_less_then_3() {
let orig = b"login\x00pass";
let encoded = general_purpose::STANDARD.encode(orig);
let expected = AuthBody {
login: "login".to_string(),
password: "pass".to_string(),
};
let result = AuthBody::from_str(encoded.as_bytes());
assert!(result.is_err());
}
#[test]
fn test_fail_if_size_greater_then_3() {
let orig = b"first\x00login\x00pass\x00other";
let encoded = general_purpose::STANDARD.encode(orig);
let expected = AuthBody {
login: "login".to_string(),
password: "pass".to_string(),
};
let result = AuthBody::from_str(encoded.as_bytes());
assert!(result.is_err());
}
}