feat(xmpp): implement socket start negotiation up to auth

This commit is contained in:
Nikita Vilunov 2023-03-05 22:04:28 +01:00
parent 435da6663a
commit 42c22d045f
7 changed files with 348 additions and 91 deletions

74
Cargo.lock generated
View File

@ -238,9 +238,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.3.15"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4"
checksum = "5be7b54589b581f624f566bf5d8eb2bab1db736c51528720b6bd36b96b55924d"
dependencies = [
"bytes",
"fnv",
@ -272,9 +272,9 @@ dependencies = [
[[package]]
name = "http"
version = "0.2.8"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
dependencies = [
"bytes",
"fnv",
@ -353,9 +353,9 @@ dependencies = [
[[package]]
name = "hyper"
version = "1.0.0-rc.2"
version = "1.0.0-rc.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "289cfdbf735dea222b0ec6a10224b4d9552c7662bb451d4589cbfda3d407d1a3"
checksum = "7b75264b2003a3913f118d35c586e535293b3e22e41f074930762929d071e092"
dependencies = [
"bytes",
"futures-channel",
@ -406,9 +406,9 @@ checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146"
[[package]]
name = "itoa"
version = "1.0.5"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
[[package]]
name = "js-sys"
@ -428,7 +428,7 @@ dependencies = [
"figment",
"futures-util",
"http-body-util",
"hyper 1.0.0-rc.2",
"hyper 1.0.0-rc.3",
"nom",
"prometheus",
"quick-xml",
@ -502,7 +502,7 @@ dependencies = [
"libc",
"log",
"wasi",
"windows-sys 0.45.0",
"windows-sys",
]
[[package]]
@ -537,9 +537,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.17.0"
version = "1.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
[[package]]
name = "overload"
@ -567,7 +567,7 @@ dependencies = [
"libc",
"redox_syscall",
"smallvec",
"windows-sys 0.45.0",
"windows-sys",
]
[[package]]
@ -656,8 +656,7 @@ dependencies = [
[[package]]
name = "quick-xml"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffc053f057dd768a56f62cd7e434c42c831d296968997e9ac1f76ea7c2d14c41"
source = "git+https://github.com/vilunov/quick-xml.git?branch=async-writer#5adef14e6833e9089c4965b22a249bed43742bb2"
dependencies = [
"memchr",
"tokio",
@ -800,9 +799,9 @@ dependencies = [
[[package]]
name = "ryu"
version = "1.0.12"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde"
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
[[package]]
name = "scopeguard"
@ -842,9 +841,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.93"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76"
checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea"
dependencies = [
"itoa",
"ryu",
@ -894,9 +893,9 @@ dependencies = [
[[package]]
name = "slab"
version = "0.4.7"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef"
checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d"
dependencies = [
"autocfg",
]
@ -909,9 +908,9 @@ checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]]
name = "socket2"
version = "0.4.7"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd"
checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
dependencies = [
"libc",
"winapi",
@ -925,9 +924,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "syn"
version = "1.0.107"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
@ -981,9 +980,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.25.0"
version = "1.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af"
checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64"
dependencies = [
"autocfg",
"bytes",
@ -996,7 +995,7 @@ dependencies = [
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.42.0",
"windows-sys",
]
[[package]]
@ -1168,9 +1167,9 @@ checksum = "d54675592c1dbefd78cbd98db9bacd89886e1ca50692a0692baefffdeb92dd58"
[[package]]
name = "unicode-ident"
version = "1.0.6"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
checksum = "775c11906edafc97bc378816b94585fbd9a054eabaf86fdd0ced94af449efab7"
[[package]]
name = "unicode-normalization"
@ -1340,21 +1339,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows-sys"
version = "0.45.0"

View File

@ -26,3 +26,6 @@ quick-xml = { version = "0.27.1", features = ["async-tokio"] }
assert_matches = "1.5.0"
regex = "1.7.1"
reqwest = { version = "0.11", default-features = false }
[patch.crates-io]
quick-xml = { git = "https://github.com/vilunov/quick-xml.git", branch = "async-writer" }

View File

@ -13,6 +13,6 @@ pub type Result<T> = std::result::Result<T, anyhow::Error>;
pub type ByteVec = Vec<u8>;
pub fn fail(msg: &'static str) -> anyhow::Error {
anyhow::Error::msg(msg)
pub fn fail(msg: &str) -> anyhow::Error {
anyhow::Error::msg(msg.to_owned())
}

View File

@ -7,9 +7,11 @@ use std::sync::Arc;
use futures_util::future::join_all;
use prometheus::Registry as MetricsRegistry;
use quick_xml::events::{BytesDecl, Event};
use quick_xml::{NsReader, Writer};
use rustls_pemfile::{certs, rsa_private_keys};
use serde::Deserialize;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::io::{AsyncWriteExt, BufReader, BufWriter};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::mpsc::channel;
use tokio_rustls::rustls::{Certificate, PrivateKey};
@ -18,6 +20,7 @@ use tokio_rustls::TlsAcceptor;
use crate::core::player::PlayerRegistry;
use crate::core::room::RoomRegistry;
use crate::prelude::*;
use crate::protos::xmpp;
use crate::util::Terminator;
#[derive(Deserialize, Debug, Clone)]
@ -126,26 +129,47 @@ async fn handle_socket(
rooms: RoomRegistry,
termination: Deferred<()>, // TODO use it to stop the connection gracefully
) -> Result<()> {
use xmpp::stream::*;
log::debug!("Received an XMPP connection from {socket_addr}");
// writer.write_all(b"Hi!\n").await?;
let mut buf = [0; 1024];
stream.write_all(br###"<?xml version='1.0'?>
<stream:stream id='11698431101746707873' version='1.0' xml:lang='en' xmlns:stream='http://etherx.jabber.org/streams' from='localhost' xmlns='jabber:client'>
<stream:features>
<starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls">
<required/>
</starttls>
</stream:features>
"###).await?;
let mut reader_buf = vec![];
let (reader, writer) = stream.split();
let mut buf_reader = BufReader::new(reader);
let mut buf_writer = BufWriter::new(writer);
{
let i = stream.read(&mut buf).await?;
match std::str::from_utf8(&buf[0..i]) {
Ok(e) => println!("{} END", e),
Err(_) => println!("{:?} END", &buf[0..i]),
let mut xml_reader = NsReader::from_reader(&mut buf_reader);
let mut xml_writer = Writer::new(&mut buf_writer);
let aaa = xml_reader.read_event_into_async(&mut reader_buf).await?;
if let Event::Decl(_) = aaa {
// this is <?xml ...> header
} else {
return Err(fail("expected XML header"));
}
stream
.write_all(br###"<proceed xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>"###)
let _ = ClientStreamStart::parse(&mut xml_reader, &mut reader_buf).await?;
xml_writer
.write_event_async(Event::Decl(BytesDecl::new("1.0", None, None)))
.await?;
xmpp::stream::ServerStreamStart {
from: "localhost".into(),
lang: "en".into(),
version: "1.0".into(),
}
.write_xml(&mut xml_writer)
.await?;
xmpp::stream::Features {
start_tls: true,
mechanisms: false,
bind: false,
}
.write_xml(&mut xml_writer)
.await?;
xml_writer.get_mut().flush().await?;
let StartTLS = StartTLS::parse(&mut xml_reader, &mut reader_buf).await?;
// TODO read <starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>
xmpp::stream::Proceed.write_xml(&mut xml_writer).await?;
xml_writer.get_mut().flush().await?;
}
let config = tokio_rustls::rustls::ServerConfig::builder()
@ -153,27 +177,83 @@ async fn handle_socket(
.with_no_client_auth()
.with_single_cert(vec![config.cert.clone()], config.key.clone())?;
let i = stream.read(&mut buf).await?;
match std::str::from_utf8(&buf[0..i]) {
Ok(e) => println!("{} END", e),
Err(_) => println!("{:?} END", &buf[0..i]),
}
let acceptor = TlsAcceptor::from(Arc::new(config));
let mut new_stream = acceptor.accept(stream).await?;
let new_stream = acceptor.accept(stream).await?;
log::debug!("TLS connection established");
let (a, b) = tokio::io::split(new_stream);
let buf_reader = BufReader::new(a);
let mut xml_reader = NsReader::from_reader(buf_reader);
let mut xml_writer = Writer::new(b);
{
if let Event::Decl(_) = xml_reader.read_event_into_async(&mut reader_buf).await? {
// this is <?xml ...> header
} else {
return Err(fail("expected XML header"));
}
let _ = ClientStreamStart::parse(&mut xml_reader, &mut reader_buf).await?;
xml_writer
.write_event_async(Event::Decl(BytesDecl::new("1.0", None, None)))
.await?;
xmpp::stream::ServerStreamStart {
from: "localhost".into(),
lang: "en".into(),
version: "1.0".into(),
}
.write_xml(&mut xml_writer)
.await?;
xmpp::stream::Features {
start_tls: false,
mechanisms: true,
bind: false,
}
.write_xml(&mut xml_writer)
.await?;
xml_writer.get_mut().flush().await?;
let _ = xmpp::sasl::Auth::parse(&mut xml_reader, &mut reader_buf).await?;
xmpp::sasl::Success.write_xml(&mut xml_writer).await?;
}
{
if let Event::Decl(_) = xml_reader.read_event_into_async(&mut reader_buf).await? {
// this is <?xml ...> header
} else {
return Err(fail("expected XML header"));
}
let _ = ClientStreamStart::parse(&mut xml_reader, &mut reader_buf).await?;
xml_writer
.write_event_async(Event::Decl(BytesDecl::new("1.0", None, None)))
.await?;
xmpp::stream::ServerStreamStart {
from: "localhost".into(),
lang: "en".into(),
version: "1.0".into(),
}
.write_xml(&mut xml_writer)
.await?;
xmpp::stream::Features {
start_tls: false,
mechanisms: false,
bind: true,
}
.write_xml(&mut xml_writer)
.await?;
xml_writer.get_mut().flush().await?;
}
loop {
let i = new_stream.read(&mut buf).await?;
if i == 0 {
let event = xml_reader.read_event_into_async(&mut reader_buf).await?;
println!("EVENT: {event:?}");
if event == Event::Eof {
break;
}
match std::str::from_utf8(&buf[0..i]) {
Ok(e) => println!("{} END", e),
Err(_) => println!("{:?} END", &buf[0..i]),
}
}
new_stream.shutdown().await?;
let a = xml_reader.into_inner().into_inner();
let b = xml_writer.into_inner();
a.unsplit(b).shutdown().await?;
Ok(())
}

View File

@ -1,2 +1,21 @@
pub mod client;
pub mod sasl;
pub mod stream;
// Implemented as a macro instead of a fn due to borrowck limitations
macro_rules! skip_text {
($reader: ident, $buf: ident) => {
loop {
use quick_xml::events::Event;
$buf.clear();
let res = $reader.read_event_into_async($buf).await?;
if let Event::Text(_) = res {
continue;
} else {
break res;
}
}
};
}
pub(super) use skip_text;

78
src/protos/xmpp/sasl.rs Normal file
View File

@ -0,0 +1,78 @@
use std::borrow::Borrow;
use quick_xml::{
events::{BytesStart, Event},
NsReader, Writer,
};
use tokio::io::{AsyncBufRead, AsyncWrite};
use super::skip_text;
use crate::prelude::*;
pub enum Mechanism {
Plain,
}
impl Mechanism {
pub fn to_str(&self) -> &'static str {
match self {
Mechanism::Plain => "PLAIN",
}
}
pub fn from_str(input: &[u8]) -> Result<Mechanism> {
match input {
b"PLAIN" => Ok(Mechanism::Plain),
_ => Err(fail(format!("unknown auth mechanism: {input:?}").as_str())),
}
}
}
pub struct Auth {
pub mechanism: Mechanism,
pub body: Vec<u8>,
}
impl Auth {
pub async fn parse(
reader: &mut NsReader<impl AsyncBufRead + Unpin>,
buf: &mut Vec<u8>,
) -> Result<Auth> {
let event = skip_text!(reader, buf);
let mechanism = if let Event::Start(bytes) = event {
let mut mechanism = None;
for attr in bytes.attributes() {
let attr = attr?;
if attr.key.0 == b"mechanism" {
mechanism = Some(attr.value)
}
}
if let Some(mechanism) = mechanism {
Mechanism::from_str(mechanism.borrow())?
} else {
return Err(fail("expected mechanism attribute in <auth>"));
}
} else {
return Err(fail("expected start of <auth>"));
};
let body = if let Event::Text(text) = reader.read_event_into_async(buf).await? {
text.into_inner().into_owned()
} else {
return Err(fail("expected text body in <auth>"));
};
if let Event::End(_) = reader.read_event_into_async(buf).await? {
//TODO
} else {
return Err(fail("expected end of <auth>"));
};
Ok(Auth { mechanism, body })
}
}
pub struct Success;
impl Success {
pub async fn write_xml(&self, writer: &mut Writer<impl AsyncWrite + Unpin>) -> Result<()> {
let event = BytesStart::new(r#"success xmlns="urn:ietf:params:xml:ns:xmpp-sasl""#);
writer.write_event_async(Event::Empty(event)).await?;
Ok(())
}
}

View File

@ -1,15 +1,16 @@
use std::io::Write;
use quick_xml::events::attributes::Attribute;
use quick_xml::events::{BytesStart, Event};
use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
use quick_xml::name::{Namespace, QName, ResolveResult};
use quick_xml::writer::Writer;
use quick_xml::{NsReader, Result};
use tokio::io::AsyncBufRead;
use quick_xml::{NsReader, Writer};
use tokio::io::{AsyncBufRead, AsyncWrite};
use super::skip_text;
use crate::prelude::*;
pub static XMLNS: &'static str = "http://etherx.jabber.org/streams";
pub static PREFIX: &'static str = "stream";
pub static XMLNS_XML: &'static str = "http://www.w3.org/XML/1998/namespace";
pub static XMLNS_TLS: &'static str = "urn:ietf:params:xml:ns:xmpp-tls";
#[derive(Debug, PartialEq, Eq)]
pub struct ClientStreamStart {
@ -22,7 +23,8 @@ impl ClientStreamStart {
reader: &mut NsReader<impl AsyncBufRead + Unpin>,
buf: &mut Vec<u8>,
) -> Result<ClientStreamStart> {
if let Event::Start(e) = reader.read_event_into_async(buf).await? {
let incoming = skip_text!(reader, buf);
if let Event::Start(e) = incoming {
let (ns, local) = reader.resolve_element(e.name());
if ns != ResolveResult::Bound(Namespace(XMLNS.as_bytes())) {
return Err(panic!());
@ -61,6 +63,7 @@ impl ClientStreamStart {
version: version.unwrap(),
})
} else {
log::error!("WAT: {incoming:?}");
Err(panic!())
}
}
@ -72,7 +75,7 @@ pub struct ServerStreamStart {
pub version: String,
}
impl ServerStreamStart {
pub fn write(&self, writer: &mut Writer<impl Write>) -> Result<()> {
pub async fn write_xml(&self, writer: &mut Writer<impl AsyncWrite + Unpin>) -> Result<()> {
let mut event = BytesStart::new("stream:stream");
let attributes = [
Attribute {
@ -97,7 +100,97 @@ impl ServerStreamStart {
},
];
event.extend_attributes(attributes.into_iter());
writer.write_event(Event::Start(event))?;
writer.write_event_async(Event::Start(event)).await?;
Ok(())
}
}
pub struct Features {
pub start_tls: bool,
pub mechanisms: bool,
pub bind: bool,
}
impl Features {
pub async fn write_xml(&self, writer: &mut Writer<impl AsyncWrite + Unpin>) -> Result<()> {
writer
.write_event_async(Event::Start(BytesStart::new("stream:features")))
.await?;
if self.start_tls {
writer
.write_event_async(Event::Start(BytesStart::new(
r#"starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls""#,
)))
.await?;
writer
.write_event_async(Event::Empty(BytesStart::new("required")))
.await?;
writer
.write_event_async(Event::End(BytesEnd::new("starttls")))
.await?;
}
if self.mechanisms {
writer
.write_event_async(Event::Start(BytesStart::new(
r#"mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl""#,
)))
.await?;
writer
.write_event_async(Event::Start(BytesStart::new(r#"mechanism"#)))
.await?;
writer
.write_event_async(Event::Text(BytesText::new("PLAIN")))
.await?;
writer
.write_event_async(Event::End(BytesEnd::new("mechanism")))
.await?;
writer
.write_event_async(Event::End(BytesEnd::new("mechanisms")))
.await?;
}
if self.bind {
writer
.write_event_async(Event::Empty(BytesStart::new(
r#"bind xmlns="urn:ietf:params:xml:ns:xmpp-bind""#,
)))
.await?;
}
writer
.write_event_async(Event::End(BytesEnd::new("stream:features")))
.await?;
Ok(())
}
}
pub struct StartTLS;
impl StartTLS {
pub async fn parse(
reader: &mut NsReader<impl AsyncBufRead + Unpin>,
buf: &mut Vec<u8>,
) -> Result<StartTLS> {
let incoming = skip_text!(reader, buf);
if let Event::Empty(e) = incoming {
if e.name().0 == b"starttls" {
Ok(StartTLS)
} else {
Err(fail("starttls expected"))
}
} else {
log::error!("WAT: {incoming:?}");
Err(panic!())
}
}
}
pub struct Proceed;
impl Proceed {
pub async fn write_xml(&self, writer: &mut Writer<impl AsyncWrite + Unpin>) -> Result<()> {
let mut event = BytesStart::new("proceed");
let attributes = [Attribute {
key: QName(b"xmlns"),
value: XMLNS_TLS.as_bytes().into(),
}];
event.extend_attributes(attributes.into_iter());
writer.write_event_async(Event::Empty(event)).await?;
Ok(())
}
}
@ -124,8 +217,8 @@ mod test {
)
}
#[test]
fn server_stream_start_write() {
#[tokio::test]
async fn server_stream_start_write() {
let input = ServerStreamStart {
from: "vlnv.dev".to_owned(),
lang: "en".to_owned(),
@ -134,7 +227,7 @@ mod test {
let expected = r###"<stream:stream from="vlnv.dev" version="1.0" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams" xml:lang="en">"###;
let mut output: Vec<u8> = vec![];
let mut writer = Writer::new(&mut output);
input.write(&mut writer).unwrap();
input.write_xml(&mut writer).await.unwrap();
assert_eq!(std::str::from_utf8(&output).unwrap(), expected);
}
}