Implement POSH and host-meta secure delegation for incoming and outgoing
This commit is contained in:
parent
e792da3312
commit
01714522ec
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -1607,6 +1607,7 @@ name = "xmpp-proxy"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"data-encoding",
|
||||
"die",
|
||||
"env_logger",
|
||||
"futures",
|
||||
@ -1616,11 +1617,13 @@ dependencies = [
|
||||
"quinn",
|
||||
"rand",
|
||||
"reqwest",
|
||||
"ring",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-pemfile",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-tungstenite",
|
||||
|
@ -27,6 +27,8 @@ futures = "0.3"
|
||||
die = "0.2"
|
||||
anyhow = "1.0"
|
||||
tokio = { version = "1.9", features = ["net", "rt", "rt-multi-thread", "macros", "io-util"] }
|
||||
ring = "0.16"
|
||||
data-encoding = "2.3"
|
||||
|
||||
# logging deps
|
||||
log = "0.4"
|
||||
@ -67,3 +69,6 @@ logging = ["rand", "env_logger"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
skip_optional_dependencies = true
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1.0"
|
||||
|
24
contrib/posh.sh
Executable file
24
contrib/posh.sh
Executable file
@ -0,0 +1,24 @@
|
||||
#!/bin/sh
|
||||
|
||||
# these are just examples for how to grab and hash certificates for POSH
|
||||
# adapted from https://curl.se/libcurl/c/CURLOPT_PINNEDPUBLICKEY.html
|
||||
|
||||
# this is for any direct TLS port like xmpps or https
|
||||
openssl s_client -servername posh.badxmpp.eu -connect posh.badxmpp.eu:443 < /dev/null | sed -n "/-----BEGIN/,/-----END/p" > posh.badxmpp.eu.pem
|
||||
openssl asn1parse -noout -inform pem -in posh.badxmpp.eu.pem -out posh.badxmpp.eu.der
|
||||
openssl dgst -sha256 -binary posh.badxmpp.eu.der | openssl base64 | tr -d '\n' > posh.badxmpp.eu.der.sha256
|
||||
openssl dgst -sha512 -binary posh.badxmpp.eu.der | openssl base64 | tr -d '\n' > posh.badxmpp.eu.der.sha512
|
||||
|
||||
openssl base64 < posh.badxmpp.eu.der | tr -d '\n' > posh.badxmpp.eu.der.base64
|
||||
|
||||
# this is for any starttls xmpp port
|
||||
openssl s_client -starttls xmpp -name posh.badxmpp.eu -servername posh.badxmpp.eu -connect snikket2.prosody.im:5222 < /dev/null | sed -n "/-----BEGIN/,/-----END/p" > posh.badxmpp.eu.5222.pem
|
||||
openssl asn1parse -noout -inform pem -in posh.badxmpp.eu.5222.pem -out posh.badxmpp.eu.5222.der
|
||||
openssl dgst -sha256 -binary posh.badxmpp.eu.5222.der | openssl base64 | tr -d '\n' > posh.badxmpp.eu.5222.der.sha256
|
||||
openssl dgst -sha512 -binary posh.badxmpp.eu.5222.der | openssl base64 | tr -d '\n' > posh.badxmpp.eu.5222.der.sha512
|
||||
|
||||
openssl base64 < posh.badxmpp.eu.5222.der | tr -d '\n' > posh.badxmpp.eu.5222.der.base64
|
||||
|
||||
wget https://posh.badxmpp.eu/.well-known/posh/xmpp-server.json https://posh.badxmpp.eu/.well-known/posh/xmpp-client.json
|
||||
|
||||
grep . *.sha*
|
21
src/lib.rs
21
src/lib.rs
@ -8,7 +8,6 @@ use std::net::SocketAddr;
|
||||
|
||||
pub use log::{debug, error, info, log_enabled, trace};
|
||||
use rustls::{Certificate, ServerConnection};
|
||||
use tokio_rustls::webpki::DnsNameRef;
|
||||
|
||||
pub fn to_str(buf: &[u8]) -> std::borrow::Cow<'_, str> {
|
||||
String::from_utf8_lossy(buf)
|
||||
@ -138,24 +137,10 @@ pub enum ServerCerts {
|
||||
}
|
||||
|
||||
impl ServerCerts {
|
||||
pub fn valid(&self, dns_name: DnsNameRef) -> bool {
|
||||
use std::convert::TryFrom;
|
||||
use tokio_rustls::webpki;
|
||||
self.first_peer_cert()
|
||||
.and_then(|c| {
|
||||
if let Ok(cert) = webpki::EndEntityCert::try_from(c.0.as_ref()) {
|
||||
cert.verify_is_valid_for_dns_name(dns_name).map(|_| true).ok()
|
||||
} else {
|
||||
Some(false)
|
||||
}
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn first_peer_cert(&self) -> Option<Certificate> {
|
||||
pub fn peer_certificates(&self) -> Option<Vec<Certificate>> {
|
||||
match self {
|
||||
ServerCerts::Tls(c) => c.peer_certificates().map(|c| c[0].clone()),
|
||||
ServerCerts::Quic(c) => c.peer_identity().and_then(|v| v.downcast::<Vec<Certificate>>().ok()).map(|v| v[0].clone()),
|
||||
ServerCerts::Tls(c) => c.peer_certificates().map(|c| c.to_vec()),
|
||||
ServerCerts::Quic(c) => c.peer_identity().and_then(|v| v.downcast::<Vec<Certificate>>().ok()).map(|v| v.to_vec()),
|
||||
}
|
||||
}
|
||||
|
||||
|
123
src/main.rs
123
src/main.rs
@ -8,6 +8,7 @@ use std::iter::Iterator;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use die::Die;
|
||||
|
||||
@ -27,7 +28,7 @@ use tokio_rustls::{
|
||||
TlsAcceptor, TlsConnector,
|
||||
};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
|
||||
mod slicesubsequence;
|
||||
use slicesubsequence::*;
|
||||
@ -145,44 +146,24 @@ impl Config {
|
||||
|
||||
#[cfg(feature = "outgoing")]
|
||||
fn get_outgoing_cfg(&self) -> OutgoingConfig {
|
||||
let c2s_config = ClientConfig::builder().with_safe_defaults().with_root_certificates(root_cert_store()).with_no_client_auth();
|
||||
let s2s_config = match self.certs_key().and_then(|(tls_certs, tls_key)| {
|
||||
Ok(ClientConfig::builder()
|
||||
.with_safe_defaults()
|
||||
.with_root_certificates(root_cert_store())
|
||||
.with_single_cert(tls_certs, tls_key)?)
|
||||
}) {
|
||||
Ok(s) => s,
|
||||
let certs_key = match self.certs_key() {
|
||||
Ok((tls_certs, tls_key)) => {
|
||||
ClientConfig::builder()
|
||||
.with_safe_defaults()
|
||||
.with_root_certificates(root_cert_store())
|
||||
.with_single_cert(tls_certs.clone(), tls_key.clone())
|
||||
.die("invalid key for certs");
|
||||
Some((tls_certs, tls_key))
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("invalid key/cert for s2s client auth: {}", e);
|
||||
c2s_config.clone()
|
||||
None
|
||||
}
|
||||
};
|
||||
// uncomment to disable cert auth/sasl external
|
||||
//let s2s_config = c2s_config.clone();
|
||||
|
||||
let mut c2s_config_alpn = c2s_config.clone();
|
||||
let mut s2s_config_alpn = s2s_config.clone();
|
||||
c2s_config_alpn.alpn_protocols.push(ALPN_XMPP_CLIENT.to_vec());
|
||||
s2s_config_alpn.alpn_protocols.push(ALPN_XMPP_SERVER.to_vec());
|
||||
|
||||
let c2s_config_alpn = Arc::new(c2s_config_alpn);
|
||||
let s2s_config_alpn = Arc::new(s2s_config_alpn);
|
||||
|
||||
let c2s_connector_alpn: TlsConnector = c2s_config_alpn.clone().into();
|
||||
let s2s_connector_alpn: TlsConnector = s2s_config_alpn.clone().into();
|
||||
|
||||
let c2s_connector: TlsConnector = Arc::new(c2s_config).into();
|
||||
let s2s_connector: TlsConnector = Arc::new(s2s_config).into();
|
||||
|
||||
OutgoingConfig {
|
||||
max_stanza_size_bytes: self.max_stanza_size_bytes,
|
||||
c2s_config_alpn,
|
||||
s2s_config_alpn,
|
||||
c2s_connector_alpn,
|
||||
s2s_connector_alpn,
|
||||
c2s_connector,
|
||||
s2s_connector,
|
||||
certs_key,
|
||||
}
|
||||
}
|
||||
|
||||
@ -209,7 +190,7 @@ impl Config {
|
||||
|
||||
let mut config = ServerConfig::builder()
|
||||
.with_safe_defaults()
|
||||
.with_client_cert_verifier(Arc::new(AllowAnyAnonymousOrAuthenticatedServer))
|
||||
.with_client_cert_verifier(Arc::new(AllowAnonymousOrAnyCert))
|
||||
.with_single_cert(tls_certs, tls_key)?;
|
||||
// todo: will connecting without alpn work then?
|
||||
config.alpn_protocols.push(ALPN_XMPP_CLIENT.to_vec());
|
||||
@ -228,43 +209,53 @@ impl Config {
|
||||
#[cfg(feature = "outgoing")]
|
||||
pub struct OutgoingConfig {
|
||||
max_stanza_size_bytes: usize,
|
||||
|
||||
c2s_config_alpn: Arc<ClientConfig>,
|
||||
s2s_config_alpn: Arc<ClientConfig>,
|
||||
c2s_connector_alpn: TlsConnector,
|
||||
s2s_connector_alpn: TlsConnector,
|
||||
|
||||
c2s_connector: TlsConnector,
|
||||
s2s_connector: TlsConnector,
|
||||
certs_key: Option<(Vec<Certificate>, PrivateKey)>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "outgoing")]
|
||||
impl OutgoingConfig {
|
||||
pub fn client_cfg_alpn(&self, is_c2s: bool) -> Arc<ClientConfig> {
|
||||
if is_c2s {
|
||||
self.c2s_config_alpn.clone()
|
||||
} else {
|
||||
self.s2s_config_alpn.clone()
|
||||
}
|
||||
}
|
||||
pub fn with_custom_certificate_verifier(&self, is_c2s: bool, cert_verifier: XmppServerCertVerifier) -> OutgoingVerifierConfig {
|
||||
let config = match (is_c2s, self.certs_key.as_ref()) {
|
||||
(false, Some((tls_certs, tls_key))) => ClientConfig::builder()
|
||||
.with_safe_defaults()
|
||||
.with_custom_certificate_verifier(Arc::new(cert_verifier))
|
||||
.with_single_cert(tls_certs.to_vec(), tls_key.to_owned())
|
||||
.expect("cannot panic because key was checked for validity in OutgoingConfig constructor"),
|
||||
_ => ClientConfig::builder()
|
||||
.with_safe_defaults()
|
||||
.with_custom_certificate_verifier(Arc::new(cert_verifier))
|
||||
.with_no_client_auth(),
|
||||
};
|
||||
|
||||
pub fn connector_alpn(&self, is_c2s: bool) -> TlsConnector {
|
||||
if is_c2s {
|
||||
self.c2s_connector_alpn.clone()
|
||||
} else {
|
||||
self.s2s_connector_alpn.clone()
|
||||
}
|
||||
}
|
||||
let mut config_alpn = config.clone();
|
||||
config_alpn.alpn_protocols.push(if is_c2s { ALPN_XMPP_CLIENT } else { ALPN_XMPP_SERVER }.to_vec());
|
||||
|
||||
pub fn connector(&self, is_c2s: bool) -> TlsConnector {
|
||||
if is_c2s {
|
||||
self.c2s_connector.clone()
|
||||
} else {
|
||||
self.s2s_connector.clone()
|
||||
let config_alpn = Arc::new(config_alpn);
|
||||
|
||||
let connector_alpn: TlsConnector = config_alpn.clone().into();
|
||||
|
||||
let connector: TlsConnector = Arc::new(config).into();
|
||||
|
||||
OutgoingVerifierConfig {
|
||||
max_stanza_size_bytes: self.max_stanza_size_bytes,
|
||||
config_alpn,
|
||||
connector_alpn,
|
||||
connector,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[cfg(feature = "outgoing")]
|
||||
pub struct OutgoingVerifierConfig {
|
||||
pub max_stanza_size_bytes: usize,
|
||||
|
||||
pub config_alpn: Arc<ClientConfig>,
|
||||
pub connector_alpn: TlsConnector,
|
||||
|
||||
pub connector: TlsConnector,
|
||||
}
|
||||
|
||||
async fn shuffle_rd_wr(in_rd: StanzaRead, in_wr: StanzaWrite, config: CloneableConfig, server_certs: ServerCerts, local_addr: SocketAddr, client_addr: &mut Context<'_>) -> Result<()> {
|
||||
let filter = StanzaFilter::new(config.max_stanza_size_bytes);
|
||||
shuffle_rd_wr_filter(in_rd, in_wr, config, server_certs, local_addr, client_addr, filter).await
|
||||
@ -293,14 +284,14 @@ async fn shuffle_rd_wr_filter(
|
||||
|
||||
if !is_c2s {
|
||||
// for s2s we need this
|
||||
let dns_from = stream_open
|
||||
let domain = stream_open
|
||||
.extract_between(b" from='", b"'")
|
||||
.or_else(|_| stream_open.extract_between(b" from=\"", b"\""))
|
||||
.and_then(|b| Ok(DnsNameRef::try_from_ascii(b)?))?;
|
||||
if !server_certs.valid(dns_from) {
|
||||
// todo: send stream error saying cert is invalid
|
||||
bail!("server certificate invalid for {:?}", dns_from);
|
||||
}
|
||||
.and_then(|b| Ok(std::str::from_utf8(b)?))?;
|
||||
let (_, cert_verifier) = get_xmpp_connections(domain, is_c2s).await?;
|
||||
let certs = server_certs.peer_certificates().ok_or_else(|| anyhow!("no client cert auth for s2s incoming from {}", domain))?;
|
||||
// todo: send stream error saying cert is invalid
|
||||
cert_verifier.verify_cert(&certs[0], &certs[1..], SystemTime::now())?;
|
||||
}
|
||||
drop(server_certs);
|
||||
|
||||
|
@ -6,9 +6,9 @@ use std::{net::SocketAddr, sync::Arc};
|
||||
use anyhow::Result;
|
||||
|
||||
#[cfg(feature = "outgoing")]
|
||||
pub async fn quic_connect(target: SocketAddr, server_name: &str, is_c2s: bool, config: OutgoingConfig) -> Result<(StanzaWrite, StanzaRead)> {
|
||||
pub async fn quic_connect(target: SocketAddr, server_name: &str, config: OutgoingVerifierConfig) -> Result<(StanzaWrite, StanzaRead)> {
|
||||
let bind_addr = "0.0.0.0:0".parse().unwrap();
|
||||
let client_cfg = config.client_cfg_alpn(is_c2s);
|
||||
let client_cfg = config.config_alpn;
|
||||
|
||||
let mut endpoint = quinn::Endpoint::client(bind_addr)?;
|
||||
endpoint.set_default_client_config(quinn::ClientConfig::new(client_cfg));
|
||||
|
289
src/srv.rs
289
src/srv.rs
@ -3,11 +3,15 @@
|
||||
use std::convert::TryFrom;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use data_encoding::BASE64;
|
||||
use ring::digest::{Algorithm, Context as DigestContext, SHA256, SHA512};
|
||||
|
||||
use trust_dns_resolver::error::ResolveError;
|
||||
use trust_dns_resolver::lookup::{SrvLookup, TxtLookup};
|
||||
use trust_dns_resolver::{IntoName, TokioAsyncResolver};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use tokio_rustls::webpki::DnsName;
|
||||
#[cfg(feature = "websocket")]
|
||||
use tokio_tungstenite::tungstenite::http::Uri;
|
||||
|
||||
@ -47,11 +51,10 @@ impl XmppConnection {
|
||||
pub async fn connect(
|
||||
&self,
|
||||
domain: &str,
|
||||
is_c2s: bool,
|
||||
stream_open: &[u8],
|
||||
in_filter: &mut crate::StanzaFilter,
|
||||
client_addr: &mut Context<'_>,
|
||||
config: OutgoingConfig,
|
||||
config: OutgoingVerifierConfig,
|
||||
) -> Result<(StanzaWrite, StanzaRead, SocketAddr, &'static str)> {
|
||||
debug!("{} attempting connection to SRV: {:?}", client_addr.log_from(), self);
|
||||
// todo: need to set options to Ipv4AndIpv6
|
||||
@ -61,23 +64,23 @@ impl XmppConnection {
|
||||
debug!("{} trying ip {}", client_addr.log_from(), to_addr);
|
||||
// todo: for DNSSEC we need to optionally allow target in addition to domain, but what for SNI
|
||||
match self.conn_type {
|
||||
XmppConnectionType::StartTLS => match crate::starttls_connect(to_addr, domain, is_c2s, stream_open, in_filter, config.clone()).await {
|
||||
XmppConnectionType::StartTLS => match crate::starttls_connect(to_addr, domain, stream_open, in_filter, config.clone()).await {
|
||||
Ok((wr, rd)) => return Ok((wr, rd, to_addr, "starttls-out")),
|
||||
Err(e) => error!("starttls connection failed to IP {} from SRV {}, error: {}", to_addr, self.target, e),
|
||||
},
|
||||
XmppConnectionType::DirectTLS => match crate::tls_connect(to_addr, domain, is_c2s, config.clone()).await {
|
||||
XmppConnectionType::DirectTLS => match crate::tls_connect(to_addr, domain, config.clone()).await {
|
||||
Ok((wr, rd)) => return Ok((wr, rd, to_addr, "directtls-out")),
|
||||
Err(e) => error!("direct tls connection failed to IP {} from SRV {}, error: {}", to_addr, self.target, e),
|
||||
},
|
||||
#[cfg(feature = "quic")]
|
||||
XmppConnectionType::QUIC => match crate::quic_connect(to_addr, domain, is_c2s, config.clone()).await {
|
||||
XmppConnectionType::QUIC => match crate::quic_connect(to_addr, domain, config.clone()).await {
|
||||
Ok((wr, rd)) => return Ok((wr, rd, to_addr, "quic-out")),
|
||||
Err(e) => error!("quic connection failed to IP {} from SRV {}, error: {}", to_addr, self.target, e),
|
||||
},
|
||||
#[cfg(feature = "websocket")]
|
||||
// todo: when websocket is found via DNS, we need to validate cert against domain, *not* target, this is a security problem with XEP-0156, we are doing it the secure but likely unexpected way here for now
|
||||
XmppConnectionType::WebSocket(ref url, ref origin, ref secure) => {
|
||||
match crate::websocket_connect(to_addr, if *secure { &self.target } else { domain }, url, origin, is_c2s, config.clone()).await {
|
||||
match crate::websocket_connect(to_addr, if *secure { &self.target } else { domain }, url, origin, config.clone()).await {
|
||||
Ok((wr, rd)) => return Ok((wr, rd, to_addr, "websocket-out")),
|
||||
Err(e) => error!("websocket connection failed to IP {} from TXT {}, error: {}", to_addr, url, e),
|
||||
}
|
||||
@ -163,7 +166,8 @@ fn collect_txts(ret: &mut Vec<XmppConnection>, secure_urls: Vec<String>, txt_rec
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_xmpp_connections(domain: &str, is_c2s: bool) -> Result<Vec<XmppConnection>> {
|
||||
pub async fn get_xmpp_connections(domain: &str, is_c2s: bool) -> Result<(Vec<XmppConnection>, XmppServerCertVerifier)> {
|
||||
let mut valid_tls_cert_server_names: Vec<DnsName> = vec![DnsNameRef::try_from_ascii_str(domain)?.to_owned()];
|
||||
let (starttls, direct_tls, quic, websocket_txt, websocket_rel) = if is_c2s {
|
||||
("_xmpp-client._tcp", "_xmpps-client._tcp", "_xmppq-client._udp", "_xmppconnect", "urn:xmpp:alt-connections:websocket")
|
||||
} else {
|
||||
@ -192,8 +196,8 @@ pub async fn get_xmpp_connections(domain: &str, is_c2s: bool) -> Result<Vec<Xmpp
|
||||
quic,
|
||||
//#[cfg(feature = "websocket")]
|
||||
websocket_txt,
|
||||
websocket_host_meta,
|
||||
websocket_host_meta_json,
|
||||
websocket_host,
|
||||
posh,
|
||||
) = tokio::join!(
|
||||
RESOLVER.srv_lookup(starttls),
|
||||
RESOLVER.srv_lookup(direct_tls),
|
||||
@ -202,7 +206,7 @@ pub async fn get_xmpp_connections(domain: &str, is_c2s: bool) -> Result<Vec<Xmpp
|
||||
//#[cfg(feature = "websocket")]
|
||||
RESOLVER.txt_lookup(websocket_txt),
|
||||
collect_host_meta(domain, websocket_rel),
|
||||
collect_host_meta_json(domain, websocket_rel),
|
||||
collect_posh(domain),
|
||||
);
|
||||
|
||||
let mut ret = Vec::new();
|
||||
@ -212,17 +216,7 @@ pub async fn get_xmpp_connections(domain: &str, is_c2s: bool) -> Result<Vec<Xmpp
|
||||
collect_srvs(&mut ret, quic, XmppConnectionType::QUIC);
|
||||
#[cfg(feature = "websocket")]
|
||||
{
|
||||
let mut urls = Vec::new();
|
||||
match websocket_host_meta {
|
||||
Ok(mut u) => urls.append(&mut u),
|
||||
Err(e) => debug!("websocket_host_meta error for domain {}: {}", domain, e),
|
||||
}
|
||||
match websocket_host_meta_json {
|
||||
Ok(mut u) => urls.append(&mut u),
|
||||
Err(e) => debug!("websocket_host_meta_json error for domain {}: {}", domain, e),
|
||||
}
|
||||
urls.sort();
|
||||
urls.dedup();
|
||||
let urls = websocket_host.unwrap_or_default();
|
||||
for url in &urls {
|
||||
if let Some(url) = wss_to_srv(url, true) {
|
||||
ret.push(url);
|
||||
@ -233,6 +227,25 @@ pub async fn get_xmpp_connections(domain: &str, is_c2s: bool) -> Result<Vec<Xmpp
|
||||
ret.sort_by(|a, b| a.priority.cmp(&b.priority));
|
||||
// todo: do something with weight
|
||||
|
||||
#[allow(clippy::single_match)]
|
||||
for srv in &ret {
|
||||
match srv.conn_type {
|
||||
#[cfg(feature = "websocket")]
|
||||
XmppConnectionType::WebSocket(_, _, ref secure) => {
|
||||
if *secure {
|
||||
if let Ok(target) = DnsNameRef::try_from_ascii_str(srv.target.as_str()) {
|
||||
let target = target.to_owned();
|
||||
if !valid_tls_cert_server_names.contains(&target) {
|
||||
valid_tls_cert_server_names.push(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let cert_verifier = XmppServerCertVerifier::new(valid_tls_cert_server_names, posh.ok());
|
||||
|
||||
if ret.is_empty() {
|
||||
// default starttls ports
|
||||
ret.push(XmppConnection {
|
||||
@ -274,7 +287,7 @@ pub async fn get_xmpp_connections(domain: &str, is_c2s: bool) -> Result<Vec<Xmpp
|
||||
|
||||
debug!("{} records for {}: {:?}", ret.len(), domain, ret);
|
||||
|
||||
Ok(ret)
|
||||
Ok((ret, cert_verifier))
|
||||
}
|
||||
|
||||
pub async fn srv_connect(
|
||||
@ -285,8 +298,10 @@ pub async fn srv_connect(
|
||||
client_addr: &mut Context<'_>,
|
||||
config: OutgoingConfig,
|
||||
) -> Result<(StanzaWrite, StanzaRead, Vec<u8>)> {
|
||||
for srv in get_xmpp_connections(domain, is_c2s).await? {
|
||||
let connect = srv.connect(domain, is_c2s, stream_open, in_filter, client_addr, config.clone()).await;
|
||||
let (srvs, cert_verifier) = get_xmpp_connections(domain, is_c2s).await?;
|
||||
let config = config.with_custom_certificate_verifier(is_c2s, cert_verifier);
|
||||
for srv in srvs {
|
||||
let connect = srv.connect(domain, stream_open, in_filter, client_addr, config.clone()).await;
|
||||
if connect.is_err() {
|
||||
continue;
|
||||
}
|
||||
@ -313,13 +328,20 @@ pub async fn srv_connect(
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "websocket"))]
|
||||
async fn collect_host_meta_json(domain: &str, rel: &str) -> Result<Vec<String>> {
|
||||
async fn collect_host_meta(domain: &str, rel: &str) -> Result<Vec<String>> {
|
||||
bail!("websocket disabled")
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "websocket"))]
|
||||
#[cfg(feature = "websocket")]
|
||||
async fn collect_host_meta(domain: &str, rel: &str) -> Result<Vec<String>> {
|
||||
bail!("websocket disabled")
|
||||
match tokio::join!(collect_host_meta_xml(domain, rel), collect_host_meta_json(domain, rel)) {
|
||||
(Ok(mut xml), Ok(json)) => {
|
||||
combine_uniq(&mut xml, json);
|
||||
Ok(xml)
|
||||
}
|
||||
(_, Ok(json)) => Ok(json),
|
||||
(xml, _) => xml,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "websocket")]
|
||||
@ -335,7 +357,7 @@ async fn collect_host_meta_json(domain: &str, rel: &str) -> Result<Vec<String>>
|
||||
}
|
||||
|
||||
let url = format!("https://{}/.well-known/host-meta.json", domain);
|
||||
let resp = reqwest::get(&url).await?;
|
||||
let resp = https_get(&url).await?;
|
||||
if resp.status().is_success() {
|
||||
let resp = resp.json::<HostMeta>().await?;
|
||||
// we will only support wss:// (TLS) not ws:// (plain text)
|
||||
@ -346,7 +368,7 @@ async fn collect_host_meta_json(domain: &str, rel: &str) -> Result<Vec<String>>
|
||||
}
|
||||
|
||||
#[cfg(feature = "websocket")]
|
||||
async fn parse_host_meta(rel: &str, bytes: &[u8]) -> Result<Vec<String>> {
|
||||
async fn parse_host_meta_xml(rel: &str, bytes: &[u8]) -> Result<Vec<String>> {
|
||||
let mut vec = Vec::new();
|
||||
let mut stanza_reader = StanzaReader(bytes);
|
||||
let mut filter = StanzaFilter::new(8192);
|
||||
@ -374,25 +396,212 @@ async fn parse_host_meta(rel: &str, bytes: &[u8]) -> Result<Vec<String>> {
|
||||
}
|
||||
|
||||
#[cfg(feature = "websocket")]
|
||||
async fn collect_host_meta(domain: &str, rel: &str) -> Result<Vec<String>> {
|
||||
async fn collect_host_meta_xml(domain: &str, rel: &str) -> Result<Vec<String>> {
|
||||
let url = format!("https://{}/.well-known/host-meta", domain);
|
||||
let resp = reqwest::get(&url).await?;
|
||||
let resp = https_get(&url).await?;
|
||||
if resp.status().is_success() {
|
||||
parse_host_meta(rel, resp.bytes().await?.as_ref()).await
|
||||
parse_host_meta_xml(rel, resp.bytes().await?.as_ref()).await
|
||||
} else {
|
||||
bail!("failed with status code {} for url {}", resp.status(), url)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn https_get<T: reqwest::IntoUrl>(url: T) -> reqwest::Result<reqwest::Response> {
|
||||
// todo: resolve URL with our resolver
|
||||
reqwest::Client::builder().https_only(true).build()?.get(url).send().await
|
||||
}
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc7711
|
||||
// https://www.iana.org/assignments/posh-service-names/posh-service-names.xhtml
|
||||
async fn collect_posh(domain: &str) -> Result<Posh> {
|
||||
match tokio::join!(collect_posh_service(domain, "xmpp-client"), collect_posh_service(domain, "xmpp-server")) {
|
||||
(Ok(client), Ok(server)) => Ok(client.append(server)),
|
||||
(_, Ok(server)) => Ok(server),
|
||||
(client, _) => client,
|
||||
}
|
||||
}
|
||||
|
||||
async fn collect_posh_service(domain: &str, service_name: &str) -> Result<Posh> {
|
||||
let url = format!("https://{}/.well-known/posh/{}.json", domain, service_name);
|
||||
let resp = https_get(&url).await?;
|
||||
if resp.status().is_success() {
|
||||
match resp.json::<PoshJson>().await? {
|
||||
PoshJson::PoshFingerprints { fingerprints, expires } => Posh::new(fingerprints, expires),
|
||||
PoshJson::PoshRedirect { url, expires } => {
|
||||
let resp = https_get(&url).await?;
|
||||
match resp.json::<PoshJson>().await? {
|
||||
PoshJson::PoshRedirect { .. } => bail!("posh illegal url redirect to another url"),
|
||||
PoshJson::PoshFingerprints { fingerprints, expires: expires2 } => Posh::new(
|
||||
fingerprints,
|
||||
// expires is supposed to be the least of these two
|
||||
min(expires, expires2),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bail!("failed with status code {} for url {}", resp.status(), url)
|
||||
}
|
||||
}
|
||||
|
||||
fn combine_uniq(target: &mut Vec<String>, mut other: Vec<String>) {
|
||||
target.append(&mut other);
|
||||
target.sort();
|
||||
target.dedup();
|
||||
}
|
||||
|
||||
fn min(a: u64, b: u64) -> u64 {
|
||||
if a < b {
|
||||
a
|
||||
} else {
|
||||
b
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
enum PoshJson {
|
||||
PoshFingerprints { fingerprints: Vec<Fingerprint>, expires: u64 },
|
||||
PoshRedirect { url: String, expires: u64 },
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
struct Fingerprint {
|
||||
// todo: support more algorithms or no?
|
||||
sha_256: Option<String>,
|
||||
sha_512: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Posh {
|
||||
sha_256_fingerprints: Vec<String>,
|
||||
sha_512_fingerprints: Vec<String>,
|
||||
expires: u64,
|
||||
}
|
||||
|
||||
impl Posh {
|
||||
fn new(fingerprints: Vec<Fingerprint>, expires: u64) -> Result<Self> {
|
||||
if expires == 0 {
|
||||
bail!("posh expires is 0, ignoring");
|
||||
}
|
||||
let mut sha_256_fingerprints = Vec::with_capacity(fingerprints.len());
|
||||
let mut sha_512_fingerprints = Vec::with_capacity(fingerprints.len());
|
||||
for f in fingerprints {
|
||||
if let Some(h) = f.sha_256 {
|
||||
sha_256_fingerprints.push(h);
|
||||
}
|
||||
if let Some(h) = f.sha_512 {
|
||||
sha_512_fingerprints.push(h);
|
||||
}
|
||||
}
|
||||
Ok(Posh {
|
||||
sha_256_fingerprints,
|
||||
sha_512_fingerprints,
|
||||
expires,
|
||||
})
|
||||
}
|
||||
|
||||
fn append(mut self, other: Self) -> Self {
|
||||
combine_uniq(&mut self.sha_256_fingerprints, other.sha_256_fingerprints);
|
||||
combine_uniq(&mut self.sha_512_fingerprints, other.sha_512_fingerprints);
|
||||
self.expires = min(self.expires, other.expires);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn valid_cert(&self, cert: &[u8]) -> bool {
|
||||
(!self.sha_256_fingerprints.is_empty() && self.sha_256_fingerprints.contains(&digest(&SHA256, cert)))
|
||||
|| (!self.sha_512_fingerprints.is_empty() && self.sha_512_fingerprints.contains(&digest(&SHA512, cert)))
|
||||
}
|
||||
}
|
||||
|
||||
fn digest(algorithm: &'static Algorithm, buf: &[u8]) -> String {
|
||||
let mut context = DigestContext::new(algorithm);
|
||||
context.update(buf);
|
||||
let digest = context.finish();
|
||||
BASE64.encode(digest.as_ref())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::srv::*;
|
||||
|
||||
fn valid_posh(posh: &[u8], cert: &[u8]) -> bool {
|
||||
let posh: PoshJson = serde_json::from_slice(&posh[..]).unwrap();
|
||||
let cert = BASE64.decode(cert).unwrap();
|
||||
println!("posh: {:?}", posh);
|
||||
if let PoshJson::PoshFingerprints { fingerprints, expires } = posh {
|
||||
let posh = Posh::new(fingerprints, expires).unwrap();
|
||||
println!("posh: {:?}", posh);
|
||||
posh.valid_cert(&cert)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn posh_deserialize() {
|
||||
assert!(valid_posh(
|
||||
br###"{"expires":86400,"fingerprints":[{"sha-256":"6sKZUeE0LBwbCXqeoHJsGCjpFLNrL9QF2W6NhDYnV4I="}]}"###,
|
||||
br###"MIICHDCCAaGgAwIBAgIUQCykdom3fbgtYxbVzk12uY13FqUwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAwwPcG9zaC5iYWR4bXBwLmV1MB4XDTIxMTAxNTE0NDkzMloXDTIyMTAxNTE0NDkzMlowGjEYMBYGA1UEAwwPcG9zaC5iYWR4bXBwLmV1MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEUeyvxJeBihodBTIATT5szGfsgeNE1nNIEjOU+PSDBpfCFEAKw5oIxB35TGyPvOe1MBSBXcaRFXBSKBZ4AkVRPsKsGjEmUa9GpIbEwcsUvw+NTx8OT81tuTEbpjs0QGy0o4GnMIGkMAwGA1UdEwQFMAMBAf8wgZMGA1UdEQSBizCBiKAqBggrBgEFBQcIB6AeFhxfeG1wcC1jbGllbnQucG9zaC5iYWR4bXBwLmV1oCoGCCsGAQUFBwgHoB4WHF94bXBwLXNlcnZlci5wb3NoLmJhZHhtcHAuZXWgHQYIKwYBBQUHCAWgEQwPcG9zaC5iYWR4bXBwLmV1gg9wb3NoLmJhZHhtcHAuZXUwCgYIKoZIzj0EAwIDaQAwZgIxAKLvjCkY9OV9dX7emghbroYgbqqWWBaQuIHLqtOEKpS+R88fOfEJbokViKNinY3ugwIxAPJ/oiK8ekF0gfa4aWmoCscbNv2Ns7HD+iSLm4GcSc/tza9r+uXVsV+0uqJ3UleTFA=="###
|
||||
));
|
||||
assert!(valid_posh(
|
||||
br###"{"expires":86400,"fingerprints":[{"sha-512":"7S7zdev/QvRxHYguWHhD5Thlolj+H4aHo9Qy3Y1R6p7WGKnNBNPxk+tnHRSIs5CJIHIR3M7a6wNkgAC5uLWL/g=="}]}"###,
|
||||
br###"MIICHDCCAaGgAwIBAgIUQCykdom3fbgtYxbVzk12uY13FqUwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAwwPcG9zaC5iYWR4bXBwLmV1MB4XDTIxMTAxNTE0NDkzMloXDTIyMTAxNTE0NDkzMlowGjEYMBYGA1UEAwwPcG9zaC5iYWR4bXBwLmV1MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEUeyvxJeBihodBTIATT5szGfsgeNE1nNIEjOU+PSDBpfCFEAKw5oIxB35TGyPvOe1MBSBXcaRFXBSKBZ4AkVRPsKsGjEmUa9GpIbEwcsUvw+NTx8OT81tuTEbpjs0QGy0o4GnMIGkMAwGA1UdEwQFMAMBAf8wgZMGA1UdEQSBizCBiKAqBggrBgEFBQcIB6AeFhxfeG1wcC1jbGllbnQucG9zaC5iYWR4bXBwLmV1oCoGCCsGAQUFBwgHoB4WHF94bXBwLXNlcnZlci5wb3NoLmJhZHhtcHAuZXWgHQYIKwYBBQUHCAWgEQwPcG9zaC5iYWR4bXBwLmV1gg9wb3NoLmJhZHhtcHAuZXUwCgYIKoZIzj0EAwIDaQAwZgIxAKLvjCkY9OV9dX7emghbroYgbqqWWBaQuIHLqtOEKpS+R88fOfEJbokViKNinY3ugwIxAPJ/oiK8ekF0gfa4aWmoCscbNv2Ns7HD+iSLm4GcSc/tza9r+uXVsV+0uqJ3UleTFA=="###
|
||||
));
|
||||
assert!(!valid_posh(
|
||||
br###"{"expires":86400,"fingerprints":[{"sha-256":"Dp8REwxYw0vFt2tRAGIAT4nNtXD2wwqL0eF5QdN4Zm4="}]}"###,
|
||||
br###"MIICHDCCAaGgAwIBAgIUQCykdom3fbgtYxbVzk12uY13FqUwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAwwPcG9zaC5iYWR4bXBwLmV1MB4XDTIxMTAxNTE0NDkzMloXDTIyMTAxNTE0NDkzMlowGjEYMBYGA1UEAwwPcG9zaC5iYWR4bXBwLmV1MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEUeyvxJeBihodBTIATT5szGfsgeNE1nNIEjOU+PSDBpfCFEAKw5oIxB35TGyPvOe1MBSBXcaRFXBSKBZ4AkVRPsKsGjEmUa9GpIbEwcsUvw+NTx8OT81tuTEbpjs0QGy0o4GnMIGkMAwGA1UdEwQFMAMBAf8wgZMGA1UdEQSBizCBiKAqBggrBgEFBQcIB6AeFhxfeG1wcC1jbGllbnQucG9zaC5iYWR4bXBwLmV1oCoGCCsGAQUFBwgHoB4WHF94bXBwLXNlcnZlci5wb3NoLmJhZHhtcHAuZXWgHQYIKwYBBQUHCAWgEQwPcG9zaC5iYWR4bXBwLmV1gg9wb3NoLmJhZHhtcHAuZXUwCgYIKoZIzj0EAwIDaQAwZgIxAKLvjCkY9OV9dX7emghbroYgbqqWWBaQuIHLqtOEKpS+R88fOfEJbokViKNinY3ugwIxAPJ/oiK8ekF0gfa4aWmoCscbNv2Ns7HD+iSLm4GcSc/tza9r+uXVsV+0uqJ3UleTFA=="###
|
||||
));
|
||||
assert!(!valid_posh(
|
||||
br###"{"expires":86400,"fingerprints":[{"sha-512":"GwfqWa8hIYCGt9V9EgdDHg6npGeGhpAwryUJkU1FuP6CNiF2Auv1s1Tp9gSWSlCTbClSmzz+sorNVOfaDW6m3Q=="}]}"###,
|
||||
br###"MIICHDCCAaGgAwIBAgIUQCykdom3fbgtYxbVzk12uY13FqUwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAwwPcG9zaC5iYWR4bXBwLmV1MB4XDTIxMTAxNTE0NDkzMloXDTIyMTAxNTE0NDkzMlowGjEYMBYGA1UEAwwPcG9zaC5iYWR4bXBwLmV1MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEUeyvxJeBihodBTIATT5szGfsgeNE1nNIEjOU+PSDBpfCFEAKw5oIxB35TGyPvOe1MBSBXcaRFXBSKBZ4AkVRPsKsGjEmUa9GpIbEwcsUvw+NTx8OT81tuTEbpjs0QGy0o4GnMIGkMAwGA1UdEwQFMAMBAf8wgZMGA1UdEQSBizCBiKAqBggrBgEFBQcIB6AeFhxfeG1wcC1jbGllbnQucG9zaC5iYWR4bXBwLmV1oCoGCCsGAQUFBwgHoB4WHF94bXBwLXNlcnZlci5wb3NoLmJhZHhtcHAuZXWgHQYIKwYBBQUHCAWgEQwPcG9zaC5iYWR4bXBwLmV1gg9wb3NoLmJhZHhtcHAuZXUwCgYIKoZIzj0EAwIDaQAwZgIxAKLvjCkY9OV9dX7emghbroYgbqqWWBaQuIHLqtOEKpS+R88fOfEJbokViKNinY3ugwIxAPJ/oiK8ekF0gfa4aWmoCscbNv2Ns7HD+iSLm4GcSc/tza9r+uXVsV+0uqJ3UleTFA=="###
|
||||
));
|
||||
|
||||
let posh = br###"
|
||||
{
|
||||
"fingerprints": [
|
||||
{
|
||||
"sha-256": "4/mggdlVx8A3pvHAWW5sD+qJyMtUHgiRuPjVC48N0XQ=",
|
||||
"sha-512": "25N+1hB2Vo42l9lSGqw+n3BKFhDHsyork8ou+D9B43TXeJ1J81mdQEDqm39oR/EHkPBDDG1y5+AG94Kec0xVqA==",
|
||||
"bla": "woo"
|
||||
}
|
||||
],
|
||||
"expires": 604800
|
||||
}
|
||||
"###;
|
||||
let posh: PoshJson = serde_json::from_slice(&posh[..]).unwrap();
|
||||
println!("posh: {:?}", posh);
|
||||
if let PoshJson::PoshFingerprints { fingerprints, expires } = posh {
|
||||
let posh = Posh::new(fingerprints, expires);
|
||||
println!("posh: {:?}", posh);
|
||||
}
|
||||
|
||||
let posh = br###"
|
||||
{
|
||||
"url":"https://hosting.example.net/.well-known/posh/spice.json",
|
||||
"expires": 604800
|
||||
}
|
||||
"###;
|
||||
let posh: PoshJson = serde_json::from_slice(&posh[..]).unwrap();
|
||||
println!("posh: {:?}", posh);
|
||||
}
|
||||
|
||||
//#[tokio::test]
|
||||
async fn posh() -> Result<()> {
|
||||
let domain = "posh.badxmpp.eu";
|
||||
let posh = collect_posh(domain).await.unwrap();
|
||||
println!("posh for domain {}: {:?}", domain, posh);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
//#[tokio::test]
|
||||
async fn srv() -> Result<()> {
|
||||
let domain = "burtrum.org";
|
||||
let is_c2s = true;
|
||||
for srv in get_xmpp_connections(domain, is_c2s).await? {
|
||||
let (srvs, cert_verifier) = get_xmpp_connections(domain, is_c2s).await?;
|
||||
println!("cert_verifier: {:?}", cert_verifier);
|
||||
for srv in srvs {
|
||||
println!("trying 1 domain {}, SRV: {:?}", domain, srv);
|
||||
let ips = RESOLVER.lookup_ip(srv.target.clone()).await?;
|
||||
for ip in ips.iter() {
|
||||
@ -407,7 +616,7 @@ mod tests {
|
||||
async fn http() -> Result<()> {
|
||||
let hosts = collect_host_meta_json("burtrum.org", "urn:xmpp:alt-connections:websocket").await?;
|
||||
println!("{:?}", hosts);
|
||||
let hosts = collect_host_meta("burtrum.org", "urn:xmpp:alt-connections:websocket").await?;
|
||||
let hosts = collect_host_meta_xml("burtrum.org", "urn:xmpp:alt-connections:websocket").await?;
|
||||
println!("{:?}", hosts);
|
||||
Ok(())
|
||||
}
|
||||
@ -416,22 +625,22 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn test_parse_host_meta() -> Result<()> {
|
||||
let xrd = br#"<XRD xmlns='http://docs.oasis-open.org/ns/xri/xrd-1.0'><Link rel='urn:xmpp:alt-connections:xbosh' href='https://burtrum.org/http-bind'/><Link rel='urn:xmpp:alt-connections:websocket' href='wss://burtrum.org/xmpp-websocket'/></XRD>"#;
|
||||
assert_eq!(parse_host_meta("urn:xmpp:alt-connections:websocket", xrd).await?, vec!["wss://burtrum.org/xmpp-websocket"]);
|
||||
assert_eq!(parse_host_meta_xml("urn:xmpp:alt-connections:websocket", xrd).await?, vec!["wss://burtrum.org/xmpp-websocket"]);
|
||||
|
||||
let xrd = br#"<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="urn:xmpp:alt-connections:xbosh" href="https://burtrum.org/http-bind"/><Link rel="urn:xmpp:alt-connections:websocket" href="wss://burtrum.org/xmpp-websocket"/></XRD>"#;
|
||||
assert_eq!(parse_host_meta("urn:xmpp:alt-connections:websocket", xrd).await?, vec!["wss://burtrum.org/xmpp-websocket"]);
|
||||
assert_eq!(parse_host_meta_xml("urn:xmpp:alt-connections:websocket", xrd).await?, vec!["wss://burtrum.org/xmpp-websocket"]);
|
||||
|
||||
let xrd = br#"<xrd xmlns='http://docs.oasis-open.org/ns/xri/xrd-1.0'><link rel='urn:xmpp:alt-connections:xbosh' href='https://burtrum.org/http-bind'/><link rel='urn:xmpp:alt-connections:websocket' href='wss://burtrum.org/xmpp-websocket'/></xrd>"#;
|
||||
assert_eq!(parse_host_meta("urn:xmpp:alt-connections:websocket", xrd).await?, vec!["wss://burtrum.org/xmpp-websocket"]);
|
||||
assert_eq!(parse_host_meta_xml("urn:xmpp:alt-connections:websocket", xrd).await?, vec!["wss://burtrum.org/xmpp-websocket"]);
|
||||
|
||||
let xrd = br#"<xrd xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><link rel="urn:xmpp:alt-connections:xbosh" href="https://burtrum.org/http-bind"/><link rel="urn:xmpp:alt-connections:websocket" href="wss://burtrum.org/xmpp-websocket"/></xrd>"#;
|
||||
assert_eq!(parse_host_meta("urn:xmpp:alt-connections:websocket", xrd).await?, vec!["wss://burtrum.org/xmpp-websocket"]);
|
||||
assert_eq!(parse_host_meta_xml("urn:xmpp:alt-connections:websocket", xrd).await?, vec!["wss://burtrum.org/xmpp-websocket"]);
|
||||
|
||||
let xrd = br#"<xrd xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><link rel="urn:xmpp:alt-connections:xbosh" href="https://burtrum.org/http-bind"/><link rel="urn:xmpp:alt-connections:websocket" href="wss://burtrum.org/xmpp-websocket"/><link rel="urn:xmpp:alt-connections:s2s-websocket" href="wss://burtrum.org/xmpp-websocket-s2s"/></xrd>"#;
|
||||
assert_eq!(parse_host_meta("urn:xmpp:alt-connections:s2s-websocket", xrd).await?, vec!["wss://burtrum.org/xmpp-websocket-s2s"]);
|
||||
assert_eq!(parse_host_meta_xml("urn:xmpp:alt-connections:s2s-websocket", xrd).await?, vec!["wss://burtrum.org/xmpp-websocket-s2s"]);
|
||||
|
||||
let xrd = br#"<xrd xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><link rel="urn:xmpp:alt-connections:xbosh" href="https://burtrum.org/http-bind"/><link rel="urn:xmpp:alt-connections:websocket" href="wss://burtrum.org/xmpp-websocket"/><link rel="urn:xmpp:alt-connections:s2s-websocket" href="wss://burtrum.org/xmpp-websocket-s2s"/></xrd>"#;
|
||||
assert_eq!(parse_host_meta("urn:xmpp:alt-connections:websocket", xrd).await?, vec!["wss://burtrum.org/xmpp-websocket"]);
|
||||
assert_eq!(parse_host_meta_xml("urn:xmpp:alt-connections:websocket", xrd).await?, vec!["wss://burtrum.org/xmpp-websocket"]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -7,16 +7,16 @@ use tokio::io::{AsyncBufReadExt, BufStream};
|
||||
use tokio_rustls::rustls::ServerName;
|
||||
|
||||
#[cfg(feature = "outgoing")]
|
||||
pub async fn tls_connect(target: SocketAddr, server_name: &str, is_c2s: bool, config: OutgoingConfig) -> Result<(StanzaWrite, StanzaRead)> {
|
||||
pub async fn tls_connect(target: SocketAddr, server_name: &str, config: OutgoingVerifierConfig) -> Result<(StanzaWrite, StanzaRead)> {
|
||||
let dnsname = ServerName::try_from(server_name)?;
|
||||
let stream = tokio::net::TcpStream::connect(target).await?;
|
||||
let stream = config.connector_alpn(is_c2s).connect(dnsname, stream).await?;
|
||||
let stream = config.connector_alpn.connect(dnsname, stream).await?;
|
||||
let (rd, wrt) = tokio::io::split(stream);
|
||||
Ok((StanzaWrite::new(wrt), StanzaRead::new(rd)))
|
||||
}
|
||||
|
||||
#[cfg(feature = "outgoing")]
|
||||
pub async fn starttls_connect(target: SocketAddr, server_name: &str, is_c2s: bool, stream_open: &[u8], in_filter: &mut StanzaFilter, config: OutgoingConfig) -> Result<(StanzaWrite, StanzaRead)> {
|
||||
pub async fn starttls_connect(target: SocketAddr, server_name: &str, stream_open: &[u8], in_filter: &mut StanzaFilter, config: OutgoingVerifierConfig) -> Result<(StanzaWrite, StanzaRead)> {
|
||||
let dnsname = ServerName::try_from(server_name)?;
|
||||
let mut stream = tokio::net::TcpStream::connect(target).await?;
|
||||
let (in_rd, mut in_wr) = stream.split();
|
||||
@ -54,7 +54,7 @@ pub async fn starttls_connect(target: SocketAddr, server_name: &str, is_c2s: boo
|
||||
}
|
||||
|
||||
debug!("starttls starting TLS {}", server_name);
|
||||
let stream = config.connector(is_c2s).connect(dnsname, stream).await?;
|
||||
let stream = config.connector.connect(dnsname, stream).await?;
|
||||
let (rd, wrt) = tokio::io::split(stream);
|
||||
Ok((StanzaWrite::new(wrt), StanzaRead::new(rd)))
|
||||
}
|
||||
|
@ -1,8 +1,12 @@
|
||||
use crate::Posh;
|
||||
use log::debug;
|
||||
use rustls::client::{ServerCertVerified, ServerCertVerifier};
|
||||
use rustls::server::{ClientCertVerified, ClientCertVerifier};
|
||||
use rustls::{Certificate, DistinguishedNames, Error};
|
||||
use rustls::{Certificate, DistinguishedNames, Error, ServerName};
|
||||
use std::convert::TryFrom;
|
||||
use std::time::SystemTime;
|
||||
use tokio_rustls::webpki;
|
||||
use tokio_rustls::webpki::DnsName;
|
||||
|
||||
type SignatureAlgorithms = &'static [&'static webpki::SignatureAlgorithm];
|
||||
|
||||
@ -33,9 +37,9 @@ pub fn pki_error(error: webpki::Error) -> Error {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AllowAnyAnonymousOrAuthenticatedServer;
|
||||
pub struct AllowAnonymousOrAnyCert;
|
||||
|
||||
impl ClientCertVerifier for AllowAnyAnonymousOrAuthenticatedServer {
|
||||
impl ClientCertVerifier for AllowAnonymousOrAnyCert {
|
||||
fn offer_client_auth(&self) -> bool {
|
||||
true
|
||||
}
|
||||
@ -49,11 +53,8 @@ impl ClientCertVerifier for AllowAnyAnonymousOrAuthenticatedServer {
|
||||
}
|
||||
|
||||
fn verify_client_cert(&self, end_entity: &Certificate, intermediates: &[Certificate], now: SystemTime) -> Result<ClientCertVerified, Error> {
|
||||
let (cert, chain) = prepare(end_entity, intermediates)?;
|
||||
let now = webpki::Time::try_from(now).map_err(|_| Error::FailedToGetCurrentTime)?;
|
||||
cert.verify_is_valid_tls_server_cert(SUPPORTED_SIG_ALGS, &crate::TLS_SERVER_ROOTS, &chain, now)
|
||||
.map_err(pki_error)
|
||||
.map(|_| ClientCertVerified::assertion())
|
||||
// this is checked only after the first <stream: stanza so we know the from=
|
||||
Ok(ClientCertVerified::assertion())
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,3 +68,59 @@ fn prepare<'a, 'b>(end_entity: &'a Certificate, intermediates: &'a [Certificate]
|
||||
|
||||
Ok((cert, intermediates))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct XmppServerCertVerifier {
|
||||
names: Vec<DnsName>,
|
||||
posh: Option<Posh>,
|
||||
}
|
||||
|
||||
impl XmppServerCertVerifier {
|
||||
pub fn new(names: Vec<DnsName>, posh: Option<Posh>) -> Self {
|
||||
XmppServerCertVerifier { names, posh }
|
||||
}
|
||||
|
||||
pub fn verify_cert(&self, end_entity: &Certificate, intermediates: &[Certificate], now: SystemTime) -> Result<ServerCertVerified, Error> {
|
||||
if let Some(ref posh) = self.posh {
|
||||
if posh.valid_cert(end_entity.as_ref()) {
|
||||
debug!("posh succeeded for {:?}", self.names.first());
|
||||
return Ok(ServerCertVerified::assertion());
|
||||
} else {
|
||||
// per RFC if POSH fails, continue with other methods
|
||||
debug!("posh failed for {:?}", self.names.first());
|
||||
}
|
||||
}
|
||||
// from WebPkiVerifier, validates CA trusted cert
|
||||
let (cert, chain) = prepare(end_entity, intermediates)?;
|
||||
let webpki_now = webpki::Time::try_from(now).map_err(|_| Error::FailedToGetCurrentTime)?;
|
||||
|
||||
cert.verify_is_valid_tls_server_cert(SUPPORTED_SIG_ALGS, &crate::TLS_SERVER_ROOTS, &chain, webpki_now)
|
||||
.map_err(pki_error)?;
|
||||
|
||||
for name in &self.names {
|
||||
if cert.verify_is_valid_for_dns_name(name.as_ref()).is_ok() {
|
||||
return Ok(ServerCertVerified::assertion());
|
||||
}
|
||||
}
|
||||
|
||||
Err(Error::InvalidCertificateData(format!("invalid peer certificate: all validation attempts failed: {:?}", end_entity)))
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerCertVerifier for XmppServerCertVerifier {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
end_entity: &Certificate,
|
||||
intermediates: &[Certificate],
|
||||
_server_name: &ServerName,
|
||||
_scts: &mut dyn Iterator<Item = &[u8]>,
|
||||
_ocsp_response: &[u8],
|
||||
now: SystemTime,
|
||||
) -> Result<ServerCertVerified, Error> {
|
||||
self.verify_cert(end_entity, intermediates, now)
|
||||
}
|
||||
|
||||
fn request_scts(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
@ -104,14 +104,14 @@ use tokio_tungstenite::tungstenite::http::header::{ORIGIN, SEC_WEBSOCKET_PROTOCO
|
||||
use tokio_tungstenite::tungstenite::http::Uri;
|
||||
|
||||
#[cfg(feature = "outgoing")]
|
||||
pub async fn websocket_connect(target: SocketAddr, server_name: &str, url: &Uri, origin: &str, is_c2s: bool, config: OutgoingConfig) -> Result<(StanzaWrite, StanzaRead)> {
|
||||
pub async fn websocket_connect(target: SocketAddr, server_name: &str, url: &Uri, origin: &str, config: OutgoingVerifierConfig) -> Result<(StanzaWrite, StanzaRead)> {
|
||||
let mut request = url.into_client_request()?;
|
||||
request.headers_mut().append(SEC_WEBSOCKET_PROTOCOL, "xmpp".parse()?);
|
||||
request.headers_mut().append(ORIGIN, origin.parse()?);
|
||||
|
||||
let dnsname = ServerName::try_from(server_name)?;
|
||||
let stream = tokio::net::TcpStream::connect(target).await?;
|
||||
let stream = config.connector(is_c2s).connect(dnsname, stream).await?;
|
||||
let stream = config.connector.connect(dnsname, stream).await?;
|
||||
|
||||
let stream: tokio_rustls::TlsStream<tokio::net::TcpStream> = stream.into();
|
||||
// todo: tokio_tungstenite seems to have a bug, if the write buffer is non-zero, it'll hang forever, even though we always flush, investigate
|
||||
|
Loading…
Reference in New Issue
Block a user