forked from lavina/lavina
Compare commits
2 Commits
4929da3f05
...
ce3c49cff6
Author | SHA1 | Date |
---|---|---|
Nikita Vilunov | ce3c49cff6 | |
Nikita Vilunov | dccb0b0f30 |
|
@ -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:
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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(())
|
||||||
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
[package]
|
||||||
|
name = "sasl"
|
||||||
|
edition = "2021"
|
||||||
|
version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
base64.workspace = true
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue