wip initial state w/ not working certs
This commit is contained in:
commit
5b9dcd6a60
|
@ -0,0 +1,4 @@
|
|||
/target
|
||||
salut.toml
|
||||
.vscode
|
||||
input.txt
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,9 @@
|
|||
[workspace]
|
||||
|
||||
members = ["salut", "desec"]
|
||||
|
||||
[profile.release]
|
||||
lto = "fat"
|
||||
opt-level = 3
|
||||
strip = "symbols"
|
||||
panic = "abort"
|
|
@ -0,0 +1,20 @@
|
|||
[package]
|
||||
name = "desec"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
# minreq = { version = "2.8.1", features = [
|
||||
# "punycode",
|
||||
# "https",
|
||||
# "urlencoding",
|
||||
# "json-using-serde",
|
||||
# ] }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
anyhow = "1"
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
enum-display = "0.1.3"
|
|
@ -0,0 +1,71 @@
|
|||
use enum_display::EnumDisplay;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, Copy, EnumDisplay)]
|
||||
pub enum Record {
|
||||
TXT,
|
||||
A,
|
||||
AAAA,
|
||||
MX,
|
||||
ANY,
|
||||
CAA,
|
||||
CNAME,
|
||||
DNSKEY,
|
||||
DS,
|
||||
NS,
|
||||
PTR,
|
||||
SOA,
|
||||
SRV,
|
||||
TLSA,
|
||||
TSIG,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct RRSet {
|
||||
#[serde(rename = "type")]
|
||||
pub record: Record,
|
||||
pub domain: String,
|
||||
#[serde(deserialize_with = "empty_string_as_none")]
|
||||
pub subname: Option<String>,
|
||||
pub name: String,
|
||||
pub ttl: i32,
|
||||
pub records: Vec<String>,
|
||||
pub created: String,
|
||||
pub touched: String,
|
||||
}
|
||||
|
||||
use serde::de::IntoDeserializer;
|
||||
|
||||
fn empty_string_as_none<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
T: serde::Deserialize<'de>,
|
||||
{
|
||||
let opt = Option::<String>::deserialize(de)?;
|
||||
let opt = opt.as_ref().map(String::as_str);
|
||||
match opt {
|
||||
None | Some("") => Ok(None),
|
||||
Some(s) => T::deserialize(s.into_deserializer()).map(Some),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
|
||||
pub struct RRSetPatch {
|
||||
#[serde(alias = "type")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub record: Option<Record>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub domain: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub subname: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ttl: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub records: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub created: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub touched: Option<String>,
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Domain {
|
||||
pub keys: Option<Vec<Key>>,
|
||||
pub created: String,
|
||||
pub published: String,
|
||||
pub touched: String,
|
||||
pub name: String,
|
||||
pub minimum_ttl: i32,
|
||||
pub zonefile: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Key {
|
||||
pub dnskey: String,
|
||||
pub ds: Vec<String>,
|
||||
pub managed: bool,
|
||||
}
|
|
@ -0,0 +1,205 @@
|
|||
pub mod dns;
|
||||
pub mod domains;
|
||||
|
||||
use dns::{RRSet, RRSetPatch, Record};
|
||||
use domains::Domain;
|
||||
use reqwest::{Client, Method, RequestBuilder};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
|
||||
const BASE_URL: &str = "https://desec.io/api/v1/";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Session {
|
||||
client: Client,
|
||||
login: Login,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
struct Login {
|
||||
token: String,
|
||||
}
|
||||
#[derive(Serialize)]
|
||||
struct Req<'a> {
|
||||
email: &'a str,
|
||||
password: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DeError {
|
||||
RequestError(reqwest::Error),
|
||||
InvalidStatus(u16, Option<String>),
|
||||
InvalidCredentials,
|
||||
InvalidToken,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DeError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
DeError::RequestError(err) => write!(f, "request error: {err}"),
|
||||
DeError::InvalidStatus(code, content) => {
|
||||
write!(f, "Unexpected status code [{code}], body: {:?}", content)
|
||||
}
|
||||
DeError::InvalidCredentials => write!(f, "invalid credentials"),
|
||||
DeError::InvalidToken => write!(f, "invalid or expired token"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for DeError {}
|
||||
|
||||
impl From<reqwest::Error> for DeError {
|
||||
fn from(value: reqwest::Error) -> Self {
|
||||
Self::RequestError(value)
|
||||
}
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, DeError>;
|
||||
|
||||
impl Session {
|
||||
pub async fn login(email: &str, password: &str) -> Result<Self> {
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.post(format!("{BASE_URL}/auth/login/"))
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&Req { email, password })
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
match response.status().as_u16() {
|
||||
200 => Ok(Self {
|
||||
client,
|
||||
login: response.json().await?,
|
||||
}),
|
||||
403 => Err(DeError::InvalidCredentials),
|
||||
code => Err(DeError::InvalidStatus(code, response.text().await.ok())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_token(token: String) -> Self {
|
||||
Self {
|
||||
client: reqwest::Client::new(),
|
||||
login: Login { token },
|
||||
}
|
||||
}
|
||||
|
||||
async fn authorized_query<D>(&self, method: Method, path: &str) -> Result<D>
|
||||
where
|
||||
D: DeserializeOwned,
|
||||
{
|
||||
let response = self
|
||||
.client
|
||||
.request(method, format!("{BASE_URL}/{path}/"))
|
||||
.header("Authorization", format!("Token {}", &self.login.token))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
match response.status().as_u16() {
|
||||
200 => Ok(response.json().await?),
|
||||
403 => Err(DeError::InvalidToken),
|
||||
code => Err(DeError::InvalidStatus(code, response.text().await.ok())),
|
||||
}
|
||||
}
|
||||
|
||||
async fn authorized_exchange<R, D>(&self, method: Method, path: &str, item: &R) -> Result<D>
|
||||
where
|
||||
R: Serialize,
|
||||
D: DeserializeOwned,
|
||||
{
|
||||
let response = self
|
||||
.client
|
||||
.request(method, format!("{BASE_URL}/{path}/"))
|
||||
.header("Authorization", format!("Token {}", &self.login.token))
|
||||
.json(item)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
match response.status().as_u16() {
|
||||
200 | 201 => Ok(response.json().await?),
|
||||
403 => Err(DeError::InvalidToken),
|
||||
code => Err(DeError::InvalidStatus(code, response.text().await.ok())),
|
||||
}
|
||||
}
|
||||
|
||||
async fn authorized_get_query<D, Q>(&self, path: &str, query: Option<Q>) -> Result<D>
|
||||
where
|
||||
D: DeserializeOwned,
|
||||
Q: Serialize,
|
||||
{
|
||||
let mut request: RequestBuilder = self
|
||||
.client
|
||||
.get(format!("{BASE_URL}/{path}/"))
|
||||
.header("Authorization", format!("Token {}", &self.login.token));
|
||||
if let Some(query) = query {
|
||||
request = request.query(&query);
|
||||
}
|
||||
let response = request.send().await?;
|
||||
|
||||
match response.status().as_u16() {
|
||||
200 => Ok(response.json().await?),
|
||||
403 => Err(DeError::InvalidToken),
|
||||
code => Err(DeError::InvalidStatus(code, response.text().await.ok())),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_domains(&self) -> Result<Vec<Domain>> {
|
||||
self.authorized_query(Method::GET, "domains").await
|
||||
}
|
||||
|
||||
pub async fn get_domain(&self, domain: &str) -> Result<Domain> {
|
||||
self.authorized_query(Method::GET, &format!("domains/{domain}"))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_rrsets(
|
||||
&self,
|
||||
domain: &str,
|
||||
type_filter: Option<Vec<Record>>,
|
||||
) -> Result<Vec<RRSet>> {
|
||||
self.authorized_get_query::<Vec<RRSet>, Vec<(&str, Record)>>(
|
||||
&format!("domains/{domain}/rrsets"),
|
||||
type_filter.map(|f| f.into_iter().map(|rec| ("type", rec)).collect()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn modify_rrset(&self, rrset: RRSet, patch: RRSetPatch) -> Result<RRSet> {
|
||||
self.authorized_exchange(Method::PATCH, &rrset_url(&rrset), &patch)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_rrset(&self, rrset: RRSet) -> Result<RRSet> {
|
||||
let mut rrset = rrset;
|
||||
rrset.subname = Some(rrset.subname.unwrap_or(String::new()));
|
||||
|
||||
self.authorized_exchange(
|
||||
Method::POST,
|
||||
&format!("domains/{}/rrsets", rrset.domain),
|
||||
&rrset,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_rrset(&self, rrset: RRSet) -> Result<()> {
|
||||
let response = self
|
||||
.client
|
||||
.delete(format!("{BASE_URL}/{}/", rrset_url(&rrset)))
|
||||
.header("Authorization", format!("Token {}", &self.login.token))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
match response.status().as_u16() {
|
||||
204 => Ok(()),
|
||||
403 => Err(DeError::InvalidToken),
|
||||
code => Err(DeError::InvalidStatus(code, response.text().await.ok())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rrset_url(rrset: &RRSet) -> String {
|
||||
format!(
|
||||
"domains/{}/rrsets/{}/{}",
|
||||
rrset.domain,
|
||||
rrset.subname.clone().unwrap_or("...".into()),
|
||||
rrset.record
|
||||
)
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,22 @@
|
|||
[package]
|
||||
name = "salut"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
quick-xml = { version = "0.29", features = ["async-tokio"] }
|
||||
enum-display = "0.1.3"
|
||||
anyhow = "1"
|
||||
log = { version = "0.4" }
|
||||
config_struct = { version = "0.5.0", features = ["toml-parsing"] }
|
||||
toml = "0.7.5"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
pretty_env_logger = "0.5.0"
|
||||
async-trait = "0.1.68"
|
||||
tokio-rustls = { version = "0.24.1" }
|
||||
instant-acme = "0.3.2"
|
||||
desec = { path = "../desec" }
|
||||
rcgen = "0.11.1"
|
|
@ -0,0 +1,298 @@
|
|||
use std::{
|
||||
fs::File,
|
||||
io::{prelude::Write, Read},
|
||||
time::Duration,
|
||||
vec,
|
||||
};
|
||||
|
||||
use desec::dns::{RRSet, RRSetPatch, Record};
|
||||
use instant_acme::{
|
||||
Account, AuthorizationStatus, ChallengeType, Identifier, LetsEncrypt, NewAccount, NewOrder,
|
||||
OrderStatus,
|
||||
};
|
||||
use log::{debug, error, info, warn};
|
||||
use rcgen::{Certificate, CertificateParams, DistinguishedName};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum CertStore {
|
||||
Provision,
|
||||
Existing(CertificatePEM),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub domain: String,
|
||||
pub subdomain: Option<String>,
|
||||
pub port: u16,
|
||||
pub cert_store: CertStore,
|
||||
pub desec_cfg: DesecConfig,
|
||||
#[serde(skip)]
|
||||
original_path: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DesecConfig {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CertificatePEM {
|
||||
pub cert_chain_pem: String,
|
||||
pub private_key_pem: String,
|
||||
}
|
||||
|
||||
const CONFIG_PATHS: [&str; 3] = [DEFAULT_PATH, "/etc/salut.toml", "/usr/local/etc/salut.toml"];
|
||||
pub const DEFAULT_PATH: &str = "salut.toml";
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
domain: String::new(),
|
||||
subdomain: Some(String::new()),
|
||||
port: 5222,
|
||||
cert_store: CertStore::Provision,
|
||||
desec_cfg: DesecConfig {
|
||||
username: String::new(),
|
||||
password: String::new(),
|
||||
},
|
||||
original_path: DEFAULT_PATH,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load() -> Result<Self, anyhow::Error> {
|
||||
for path in CONFIG_PATHS {
|
||||
if let Ok(mut file) = File::open(path) {
|
||||
let mut cfg = String::new();
|
||||
file.read_to_string(&mut cfg)?;
|
||||
let mut cfg: Self = toml::from_str(&cfg)?;
|
||||
cfg.original_path = path;
|
||||
return Ok(cfg);
|
||||
};
|
||||
}
|
||||
Err(anyhow::anyhow!(
|
||||
"could not find salut.toml in {CONFIG_PATHS:?}"
|
||||
))
|
||||
}
|
||||
|
||||
pub fn save(&self, path: &str) -> Result<(), anyhow::Error> {
|
||||
Ok(write!(
|
||||
File::create(path)?,
|
||||
"{}",
|
||||
toml::to_string_pretty(self)?
|
||||
)?)
|
||||
}
|
||||
|
||||
pub fn hostname(&self) -> String {
|
||||
match &self.subdomain {
|
||||
Some(sub) => format!("{sub}.{}", &self.domain),
|
||||
None => self.domain.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ACME_PREFIX: &str = "_acme-challenge";
|
||||
|
||||
impl Config {
|
||||
// Returns existing certificate or provisions a new one via DNS challenge using DeSEC
|
||||
pub async fn certificate(&self) -> Result<CertificatePEM, anyhow::Error> {
|
||||
let desec_cfg = match self.cert_store.clone() {
|
||||
CertStore::Provision => self.desec_cfg.clone(),
|
||||
CertStore::Existing(existing) => return Ok(existing),
|
||||
};
|
||||
|
||||
let account = Account::create(
|
||||
&NewAccount {
|
||||
contact: &["mailto:emilis@puff.place"],
|
||||
terms_of_service_agreed: true,
|
||||
only_return_existing: false,
|
||||
},
|
||||
LetsEncrypt::Staging.url(),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let identifier = Identifier::Dns(self.hostname());
|
||||
let mut order = account
|
||||
.new_order(&NewOrder {
|
||||
identifiers: &[identifier],
|
||||
})
|
||||
.await?;
|
||||
|
||||
debug!("order url: {}", order.url());
|
||||
let state = order.state();
|
||||
info!("cert order state: {:#?}", state);
|
||||
assert!(matches!(state.status, OrderStatus::Pending));
|
||||
|
||||
debug!("logging into desec as <{}>", &desec_cfg.username);
|
||||
let dns = desec::Session::login(&desec_cfg.username, &desec_cfg.password).await?;
|
||||
debug!("querying existing TXT records");
|
||||
let existing_records: Vec<RRSet> = dns
|
||||
.get_rrsets(&self.domain, Some(vec![Record::TXT]))
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|rec| {
|
||||
if let Some(sub) = &rec.subname {
|
||||
sub.starts_with(ACME_PREFIX)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
debug!(
|
||||
"got {} existing DNS TXT records that match {ACME_PREFIX}",
|
||||
existing_records.len()
|
||||
);
|
||||
|
||||
let authorizations = order.authorizations().await.unwrap();
|
||||
debug!("got {} authorizations for this order", authorizations.len());
|
||||
let mut challenges = Vec::with_capacity(authorizations.len());
|
||||
let mut cleanup_records: Vec<RRSet> = vec![];
|
||||
|
||||
for authz in &authorizations {
|
||||
if let AuthorizationStatus::Valid = authz.status {
|
||||
debug!("Valid authorization, skipping: {authz:?}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// We'll use the DNS challenges for this example, but you could
|
||||
// pick something else to use here.
|
||||
let challenge = authz
|
||||
.challenges
|
||||
.iter()
|
||||
.find(|c| c.r#type == ChallengeType::Dns01)
|
||||
.ok_or_else(|| anyhow::anyhow!("no dns01 challenge found"))?;
|
||||
|
||||
let Identifier::Dns(identifier) = &authz.identifier;
|
||||
let dns_challenge = order.key_authorization(challenge).dns_value();
|
||||
let subname = format!(
|
||||
"{ACME_PREFIX}{}",
|
||||
match &self.subdomain {
|
||||
Some(sub) => ".".to_owned() + sub,
|
||||
None => String::new(),
|
||||
}
|
||||
);
|
||||
debug!("challenge for {identifier} ready: {dns_challenge}");
|
||||
|
||||
let record = if let Some(id) = (&existing_records)
|
||||
.into_iter()
|
||||
.find(|r| *r.subname.as_ref().unwrap() == subname)
|
||||
{
|
||||
debug!("modifying existing record: {}", id.name);
|
||||
dns.modify_rrset(
|
||||
id.clone(),
|
||||
RRSetPatch {
|
||||
name: Some(self.hostname()),
|
||||
subname: Some(subname),
|
||||
records: Some(vec![format!("\"{dns_challenge}\"")]),
|
||||
..RRSetPatch::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
id.clone()
|
||||
} else {
|
||||
let record = RRSet {
|
||||
record: Record::TXT,
|
||||
domain: self.domain.clone(),
|
||||
name: self.hostname(),
|
||||
subname: Some(subname),
|
||||
ttl: 3600,
|
||||
records: vec![format!("\"{dns_challenge}\"")],
|
||||
created: String::new(),
|
||||
touched: String::new(),
|
||||
};
|
||||
debug!("creating new record: {record:?}");
|
||||
dns.create_rrset(record).await?
|
||||
};
|
||||
|
||||
challenges.push((identifier, &challenge.url));
|
||||
cleanup_records.push(record);
|
||||
}
|
||||
|
||||
// Let the server know we're ready to accept the challenges.
|
||||
debug!("done setting challenges, notifying CA");
|
||||
for (_, url) in &challenges {
|
||||
order.set_challenge_ready(url).await.unwrap();
|
||||
}
|
||||
|
||||
let mut tries = 1u8;
|
||||
let mut delay = Duration::from_millis(250);
|
||||
loop {
|
||||
tokio::time::sleep(delay).await;
|
||||
let state = order.refresh().await?;
|
||||
if let OrderStatus::Ready | OrderStatus::Invalid = state.status {
|
||||
info!("order state: {:#?}", state);
|
||||
std::io::stdin().read_line(&mut String::new()).unwrap();
|
||||
break;
|
||||
} else {
|
||||
info!("waiting on order... state: {:?}", state.status);
|
||||
}
|
||||
|
||||
delay *= 2;
|
||||
tries += 1;
|
||||
match tries < 5 {
|
||||
true => info!("[{state:?}({tries})] order is not ready, waiting {delay:?}"),
|
||||
false => {
|
||||
info!("[{state:?}({tries})] order is not ready");
|
||||
return Err(anyhow::anyhow!("order is not ready"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let state = order.state();
|
||||
if state.status != OrderStatus::Ready {
|
||||
for rec in cleanup_records {
|
||||
warn!("cleaning up record: {}", rec.name);
|
||||
if let Err(err) = dns.delete_rrset(rec).await {
|
||||
error!("failed cleaning up record: {err}")
|
||||
}
|
||||
}
|
||||
return Err(anyhow::anyhow!(
|
||||
"unexpected order status: {:?}\nwith state:{state:#?}",
|
||||
state.status
|
||||
));
|
||||
}
|
||||
|
||||
for rec in cleanup_records {
|
||||
if let Err(err) = dns.delete_rrset(rec).await {
|
||||
error!("failed cleaning up record: {err}")
|
||||
}
|
||||
}
|
||||
|
||||
let mut names = Vec::with_capacity(challenges.len());
|
||||
for (identifier, _) in challenges {
|
||||
names.push(identifier.to_owned());
|
||||
}
|
||||
|
||||
// If the order is ready, we can provision the certificate.
|
||||
// Use the rcgen library to create a Certificate Signing Request.
|
||||
|
||||
let mut params = CertificateParams::new(names.clone());
|
||||
params.distinguished_name = DistinguishedName::new();
|
||||
let cert = Certificate::from_params(params).unwrap();
|
||||
let csr = cert.serialize_request_der()?;
|
||||
|
||||
// Finalize the order and update config
|
||||
order.finalize(&csr).await.unwrap();
|
||||
let cert_chain_pem = loop {
|
||||
match order.certificate().await.unwrap() {
|
||||
Some(cert_chain_pem) => break cert_chain_pem,
|
||||
None => tokio::time::sleep(Duration::from_secs(1)).await,
|
||||
}
|
||||
};
|
||||
|
||||
let cert = CertificatePEM {
|
||||
cert_chain_pem,
|
||||
private_key_pem: cert.serialize_private_key_pem(),
|
||||
};
|
||||
|
||||
let mut new_cfg = self.clone();
|
||||
new_cfg.cert_store = CertStore::Existing(cert.clone());
|
||||
new_cfg.save(self.original_path)?;
|
||||
|
||||
Ok(cert)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
use enum_display::EnumDisplay;
|
||||
use std::string::FromUtf8Error;
|
||||
|
||||
use log::error;
|
||||
use quick_xml::events::attributes::AttrError;
|
||||
|
||||
#[derive(Debug, EnumDisplay, Clone, Copy)]
|
||||
#[enum_display(case = "Kebab")]
|
||||
#[allow(unused)]
|
||||
pub enum StreamError {
|
||||
/// The entity has sent XML that cannot be processed.
|
||||
BadFormat,
|
||||
/// The entity has sent a namespace prefix that is unsupported, or has
|
||||
/// sent no namespace prefix on an element that needs such a prefix
|
||||
BadNamespacePrefix,
|
||||
/// The server either (1) is closing the existing stream for this entity
|
||||
/// because a new stream has been initiated that conflicts with the
|
||||
/// existing stream, or (2) is refusing a new stream for this entity
|
||||
/// because allowing the new stream would conflict with an existing stream
|
||||
Conflict,
|
||||
/// One party is closing the stream because it has reason to believe that
|
||||
/// the other party has permanently lost the ability to communicate over
|
||||
/// the stream
|
||||
ConnectionTimeout,
|
||||
/// The value of the 'to' attribute provided in the initial stream header
|
||||
/// corresponds to an FQDN that is no longer serviced by the receiving entity
|
||||
HostGone,
|
||||
/// The value of the 'to' attribute provided in the initial stream header
|
||||
/// does not correspond to an FQDN that is serviced by the receiving entity
|
||||
HostUnknown,
|
||||
/// A stanza sent between two servers lacks a 'to' or 'from' attribute,
|
||||
/// the 'from' or 'to' attribute has no value, or the value violates the
|
||||
/// rules for XMPP addresses
|
||||
ImproperAddressing,
|
||||
/// The server has experienced a misconfiguration or other internal error
|
||||
/// that prevents it from servicing the stream
|
||||
InternalServerError,
|
||||
/// The data provided in a 'from' attribute does not match an authorized
|
||||
/// JID or validated domain as negotiated (1) between two servers using
|
||||
/// SASL or Server Dialback, or (2) between a client and a server via
|
||||
/// SASL authentication and resource binding
|
||||
InvalidFrom,
|
||||
/// The stream namespace name is something other than
|
||||
/// "http://etherx.jabber.org/streams" or the content
|
||||
/// namespace declared as the default namespace is not supported
|
||||
/// (e.g., something other than "jabber:client" or "jabber:server").
|
||||
InvalidNamespace,
|
||||
/// The entity has sent invalid XML over the stream to a server that
|
||||
/// performs validation
|
||||
InvalidXml,
|
||||
/// The entity has attempted to send XML stanzas or other outbound data
|
||||
/// before the stream has been authenticated, or otherwise is not
|
||||
/// authorized to perform an action related to stream negotiation; the
|
||||
/// receiving entity MUST NOT process the offending data before sending
|
||||
/// the stream error.
|
||||
NotAuthorized,
|
||||
/// The initiating entity has sent XML that violates the well-formedness
|
||||
/// rules of [XML](http://www.w3.org/TR/2008/REC-xml-20081126) or [XML-NAMES](http://www.w3.org/TR/2008/REC-xml-20081126).
|
||||
NotWellFormed,
|
||||
/// The entity has violated some local service policy (e.g., a stanza
|
||||
/// exceeds a configured size limit); the server MAY choose to specify
|
||||
/// the policy in the <text/> element or in an application-specific
|
||||
/// condition element
|
||||
PolicyViolation,
|
||||
/// The server is unable to properly connect to a remote entity that is
|
||||
/// needed for authentication or authorization.
|
||||
/// This condition is not to be used when the cause of the error is within the
|
||||
/// administrative domain of the XMPP service provider, in which case the
|
||||
/// [`StreamError::InternalServerError`] condition is more appropriate.
|
||||
RemoteConnectionFailed,
|
||||
/// The server is closing the stream because it has new (typically security-critical)
|
||||
/// features to offer, because the keys or
|
||||
/// certificates used to establish a secure context for the stream have
|
||||
/// expired or have been revoked during the life of the stream
|
||||
/// because the TLS sequence number has wrapped, etc.
|
||||
/// The reset applies to the stream and to any
|
||||
/// security context established for that stream (e.g., via TLS and SASL),
|
||||
/// which means that encryption and authentication need to be
|
||||
/// negotiated again for the new stream (e.g., TLS session resumption cannot be used)
|
||||
Reset,
|
||||
/// The server lacks the system resources necessary to service the stream
|
||||
ResourceConstraint,
|
||||
/// The entity has attempted to send restricted XML features such as a
|
||||
/// comment, processing instruction, DTD subset, or XML entity reference
|
||||
RestrictedXml,
|
||||
/// The server will not provide service to the initiating entity but is
|
||||
/// redirecting traffic to another host under the administrative control
|
||||
/// of the same service provider. The XML character data of the <see-
|
||||
/// other-host/> element returned by the server MUST specify the
|
||||
/// alternate FQDN or IP address at which to connect, which MUST be a
|
||||
/// valid domainpart or a domainpart plus port number (separated by the
|
||||
/// ':' character in the form "domainpart:port"). If the domainpart is
|
||||
/// the same as the source domain, derived domain, or resolved IPv4 or
|
||||
/// IPv6 address to which the initiating entity originally connected
|
||||
/// (differing only by the port number), then the initiating entity
|
||||
/// SHOULD simply attempt to reconnect at that address.
|
||||
/// (The format of an IPv6 address MUST follow [IPv6-ADDR](https://www.rfc-editor.org/rfc/rfc5952),
|
||||
/// which includes the enclosing the IPv6 address in square brackets '[' and ']'
|
||||
/// as originally defined by [URI](https://www.rfc-editor.org/rfc/rfc3986).)
|
||||
/// Otherwise, the initiating entity MUST resolve the FQDN
|
||||
/// specified in the <see-other-host/> element
|
||||
SeeOtherHost,
|
||||
/// The server is being shut down and all active streams are being closed
|
||||
SystemShutdown,
|
||||
/// The error condition is not one of those defined by the other
|
||||
/// conditions in this list; this error condition SHOULD NOT be used
|
||||
/// except in conjunction with an application-specific condition.
|
||||
UndefinedCondition,
|
||||
/// The initiating entity has encoded the stream in an encoding that is
|
||||
/// not supported by the server (see Section 11.6) or has otherwise
|
||||
/// improperly encoded the stream (e.g., by violating the rules of the
|
||||
/// [UTF-8](https://www.rfc-editor.org/rfc/rfc3629) encoding).
|
||||
UnsupportedEncoding,
|
||||
/// The receiving entity has advertised a mandatory-to-negotiate stream
|
||||
/// feature that the initiating entity does not support, and has offered
|
||||
/// no other mandatory-to-negotiate feature alongside the unsupported feature.
|
||||
UnsupportedFeature,
|
||||
/// The initiating entity has sent a first-level child of the stream that
|
||||
/// is not supported by the server, either because the receiving entity
|
||||
/// does not understand the namespace or because the receiving entity
|
||||
/// does not understand the element name for the applicable namespace
|
||||
/// (which might be the content namespace declared as the default namespace).
|
||||
UnsupportedStanzaType,
|
||||
/// The 'version' attribute provided by the initiating entity in the
|
||||
/// stream header specifies a version of XMPP that is not supported by
|
||||
/// the server.
|
||||
UnsupportedVersion,
|
||||
}
|
||||
|
||||
impl From<FromUtf8Error> for StreamError {
|
||||
fn from(_: FromUtf8Error) -> Self {
|
||||
Self::UnsupportedEncoding
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AttrError> for StreamError {
|
||||
fn from(_: AttrError) -> Self {
|
||||
Self::NotWellFormed
|
||||
}
|
||||
}
|
||||
|
||||
impl From<quick_xml::Error> for StreamError {
|
||||
fn from(value: quick_xml::Error) -> Self {
|
||||
match value {
|
||||
quick_xml::Error::Io(err) => {
|
||||
error!("io error: {err}");
|
||||
Self::InternalServerError
|
||||
}
|
||||
quick_xml::Error::NonDecodable(_) => Self::UnsupportedEncoding,
|
||||
_ => Self::BadFormat,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for StreamError {}
|
|
@ -0,0 +1,55 @@
|
|||
use std::vec;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use quick_xml::{
|
||||
events::{BytesEnd, BytesStart, Event},
|
||||
Writer,
|
||||
};
|
||||
use tokio::io::AsyncWrite;
|
||||
|
||||
use crate::{error::StreamError, tag};
|
||||
|
||||
pub struct Feature {
|
||||
name: &'static str,
|
||||
required: bool,
|
||||
namespace: Option<&'static str>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl tag::Tag for Feature {
|
||||
async fn write_tag<W>(&self, writer: W) -> Result<(), StreamError>
|
||||
where
|
||||
W: AsyncWrite + Unpin + Send,
|
||||
{
|
||||
let mut writer = Writer::new(writer);
|
||||
writer
|
||||
.write_event_async(Event::Start(BytesStart::new(self.name).with_attributes(
|
||||
if let Some(namespace) = self.namespace {
|
||||
vec![("xmlns", namespace)]
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
)))
|
||||
.await?;
|
||||
if self.required {
|
||||
writer
|
||||
.write_event_async(Event::Empty(BytesStart::new("required")))
|
||||
.await?;
|
||||
}
|
||||
writer
|
||||
.write_event_async(Event::End(BytesEnd::new(self.name)))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Feature {
|
||||
pub const fn start_tls(required: bool) -> Feature {
|
||||
Feature {
|
||||
required,
|
||||
name: "starttls",
|
||||
namespace: Some(tag::TLS_NAMESPACE),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
use std::process;
|
||||
|
||||
use log::{error, info};
|
||||
|
||||
mod config;
|
||||
mod error;
|
||||
mod feature;
|
||||
mod negotiator;
|
||||
mod server;
|
||||
mod streamstart;
|
||||
mod tag;
|
||||
mod tls;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), anyhow::Error> {
|
||||
if !std::env::var("LOG").is_ok() {
|
||||
#[cfg(debug_assertions)]
|
||||
std::env::set_var("LOG", "debug");
|
||||
#[cfg(not(debug_assertions))]
|
||||
std::env::set_var("LOG", "info");
|
||||
}
|
||||
pretty_env_logger::init_custom_env("LOG");
|
||||
|
||||
let cfg = match config::Config::load() {
|
||||
Ok(cfg) => cfg,
|
||||
Err(err) => {
|
||||
error!(
|
||||
"getting config: {err}. writing default to {}",
|
||||
config::DEFAULT_PATH
|
||||
);
|
||||
config::Config::default().save(config::DEFAULT_PATH)?;
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
info!("checking for certificates");
|
||||
let certs = cfg.certificate().await.expect("getting certificates");
|
||||
|
||||
let host = cfg.hostname();
|
||||
info!("listening on {host}:{}!", cfg.port);
|
||||
server::listen(host, cfg.port).await.unwrap();
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
use async_trait::async_trait;
|
||||
use quick_xml::{
|
||||
events::{BytesStart, Event},
|
||||
Writer,
|
||||
};
|
||||
use tokio::io::{AsyncBufRead, AsyncWrite};
|
||||
use tokio_rustls::rustls;
|
||||
|
||||
use crate::{
|
||||
error::StreamError,
|
||||
tag::{self, Tag},
|
||||
};
|
||||
|
||||
pub enum Step {
|
||||
Proceed,
|
||||
Failure,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tag for Step {
|
||||
async fn write_tag<W>(&self, writer: W) -> Result<(), crate::error::StreamError>
|
||||
where
|
||||
W: AsyncWrite + Unpin + Send,
|
||||
{
|
||||
let mut writer = Writer::new(writer);
|
||||
writer
|
||||
.write_event_async(Event::Empty(
|
||||
BytesStart::new(match self {
|
||||
Step::Proceed => "proceed",
|
||||
Step::Failure => "failure",
|
||||
})
|
||||
.with_attributes(vec![("xmlns", tag::TLS_NAMESPACE)]),
|
||||
))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start_tls<R, W>(
|
||||
reader: R,
|
||||
writer: W,
|
||||
start_tls_event: BytesStart<'_>,
|
||||
) -> Result<Step, StreamError>
|
||||
where
|
||||
R: AsyncBufRead + Unpin,
|
||||
W: AsyncWrite + Unpin + Send,
|
||||
{
|
||||
match start_tls_event.try_get_attribute("xmlns") {
|
||||
Ok(namespace) => {
|
||||
if &namespace
|
||||
.map(|a| String::from_utf8(a.value.as_ref().to_vec()).unwrap_or_default())
|
||||
.unwrap_or_default()
|
||||
!= tag::TLS_NAMESPACE
|
||||
{
|
||||
return Ok(Step::Failure);
|
||||
}
|
||||
}
|
||||
Err(_) => return Ok(Step::Failure),
|
||||
}
|
||||
|
||||
// let config = rustls::ServerConfig::builder()
|
||||
// .with_safe_defaults()
|
||||
// .with_no_client_auth()
|
||||
// .with_single_cert(certs, keys.remove(0))
|
||||
// .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
|
||||
|
||||
Step::Proceed.write_tag(writer).await?;
|
||||
|
||||
// match TlsConnector::builder(). {
|
||||
// Ok(conn) => conn.,
|
||||
// Err(err) => {
|
||||
// error!("getting a tls connector: {err}");
|
||||
// return Ok(Step::Failure);
|
||||
// }
|
||||
// }
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_secs(3));
|
||||
todo!()
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
use log::info;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
use crate::streamstart;
|
||||
|
||||
pub async fn listen(hostname: String, port: u16) -> Result<(), anyhow::Error> {
|
||||
let listener = TcpListener::bind(("0.0.0.0", port)).await?;
|
||||
loop {
|
||||
match listener.accept().await {
|
||||
Ok(conn) => {
|
||||
info!("opening connection from {}", conn.1);
|
||||
streamstart::spawn(hostname.clone(), conn);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("listening: {e}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,234 @@
|
|||
use std::net::SocketAddr;
|
||||
|
||||
use log::{error, info};
|
||||
use quick_xml::{
|
||||
events::{attributes::Attributes, BytesDecl, BytesEnd, BytesStart, Event},
|
||||
Reader, Writer,
|
||||
};
|
||||
use tokio::{
|
||||
io::{AsyncWrite, AsyncWriteExt, BufReader},
|
||||
net::{
|
||||
tcp::{ReadHalf, WriteHalf},
|
||||
TcpStream,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::StreamError,
|
||||
feature::Feature,
|
||||
negotiator::{self, Step},
|
||||
tag::{self, Tag},
|
||||
};
|
||||
|
||||
type Result<T> = std::result::Result<T, StreamError>;
|
||||
|
||||
const FEATURES: &'static [Feature] = &[Feature::start_tls(true)];
|
||||
|
||||
struct StreamStart<'a> {
|
||||
reader: Reader<BufReader<ReadHalf<'a>>>,
|
||||
writer: Writer<WriteHalf<'a>>,
|
||||
buffer: Vec<u8>,
|
||||
hostname: String,
|
||||
}
|
||||
|
||||
impl<'a> StreamStart<'a> {
|
||||
fn new(stream: &'a mut TcpStream, hostname: String) -> Self {
|
||||
let (read, write) = stream.split();
|
||||
let (reader, writer) = (
|
||||
Reader::from_reader(BufReader::new(read)),
|
||||
Writer::new(write),
|
||||
);
|
||||
|
||||
Self {
|
||||
reader,
|
||||
writer,
|
||||
hostname,
|
||||
buffer: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_stream(mut self) {
|
||||
match self.negotiate_stream().await {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
if let Err(err2) = error(self.writer.get_mut(), err).await {
|
||||
error!("error writing error: {err2}");
|
||||
return;
|
||||
} else {
|
||||
info!("wrote error {err}")
|
||||
}
|
||||
|
||||
if let Err(e) = self.writer.get_mut().write_all(b"</stream:stream>").await {
|
||||
error!("writing end to stream: {e}")
|
||||
}
|
||||
if let Err(e) = self.writer.get_mut().shutdown().await {
|
||||
error!("shutting down stream: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn negotiate_stream(&mut self) -> Result<()> {
|
||||
let attrs = loop {
|
||||
match self.reader.read_event_into_async(&mut self.buffer).await? {
|
||||
Event::Start(start) => {
|
||||
if start.name().as_ref() == tag::STREAM_ELEMENT_NAME {
|
||||
let attrs: StreamAttrs = start.attributes().try_into()?;
|
||||
if attrs.namespace != XMLNamespace::JabberClient {
|
||||
return Err(StreamError::InvalidNamespace);
|
||||
}
|
||||
break attrs;
|
||||
} else {
|
||||
info!("element: {:?}", start);
|
||||
}
|
||||
}
|
||||
Event::End(_) => return Err(StreamError::BadFormat),
|
||||
Event::Eof => return Err(StreamError::BadFormat),
|
||||
_ => continue,
|
||||
}
|
||||
};
|
||||
info!("starting negotiation with: {attrs:?}");
|
||||
self.write_stream_header(StreamAttrs {
|
||||
from: attrs.to.clone(),
|
||||
to: attrs.from,
|
||||
namespace: XMLNamespace::JabberClient,
|
||||
})
|
||||
.await?;
|
||||
if attrs.to != self.hostname {
|
||||
return Err(StreamError::HostUnknown);
|
||||
}
|
||||
self.send_features().await?;
|
||||
loop {
|
||||
match self.reader.read_event_into_async(&mut self.buffer).await? {
|
||||
Event::Empty(empty) => match empty.name().as_ref() {
|
||||
tag::STARTTLS => {
|
||||
info!("starttls negotiation");
|
||||
if let Step::Failure = negotiator::start_tls(
|
||||
self.reader.get_mut(),
|
||||
self.writer.get_mut(),
|
||||
empty,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Step::Failure.write_tag(self.writer.get_mut()).await;
|
||||
};
|
||||
}
|
||||
_ => return Err(StreamError::UnsupportedFeature),
|
||||
},
|
||||
Event::End(_) => return Err(StreamError::BadFormat),
|
||||
Event::Eof => return Err(StreamError::BadFormat),
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
|
||||
Err(StreamError::InternalServerError)
|
||||
}
|
||||
async fn write_stream_header(&mut self, req: StreamAttrs) -> Result<()> {
|
||||
self.writer
|
||||
.write_event_async(Event::Decl(BytesDecl::new("1.0", Some("utf-8"), None)))
|
||||
.await?;
|
||||
self.writer
|
||||
.write_event_async(Event::Start(
|
||||
BytesStart::new("stream:stream").with_attributes(vec![
|
||||
("from", req.from.as_str()),
|
||||
("to", req.to.as_str()),
|
||||
("xmlns:stream", "http://etherx.jabber.org/streams"),
|
||||
("xml:lang", "en"),
|
||||
("version", "1.0"),
|
||||
]),
|
||||
))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_features(&mut self) -> Result<()> {
|
||||
self.writer
|
||||
.write_event_async(Event::Start(BytesStart::new(tag::FEATURE)))
|
||||
.await?;
|
||||
for feature in FEATURES.into_iter() {
|
||||
feature.write_tag(self.writer.get_mut()).await?;
|
||||
}
|
||||
self.writer
|
||||
.write_event_async(Event::End(BytesEnd::new(tag::FEATURE)))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn(hostname: String, (mut stream, _): (TcpStream, SocketAddr)) {
|
||||
tokio::spawn(async move {
|
||||
StreamStart::new(&mut stream, hostname).start_stream().await;
|
||||
});
|
||||
}
|
||||
|
||||
async fn error<W: AsyncWrite + Unpin>(writer: W, err: StreamError) -> Result<()> {
|
||||
let mut writer = Writer::new(writer);
|
||||
let err = err.to_string();
|
||||
writer
|
||||
.write_event_async(Event::Start(BytesStart::new(tag::ERROR_ELEMENT)))
|
||||
.await?;
|
||||
writer
|
||||
.write_event_async(Event::Start(
|
||||
BytesStart::new(&err)
|
||||
.with_attributes(vec![("xmlns", "urn:ietf:params:xml:ns:xmpp-streams")]),
|
||||
))
|
||||
.await?;
|
||||
writer
|
||||
.write_event_async(Event::End(BytesEnd::new(&err)))
|
||||
.await?;
|
||||
writer
|
||||
.write_event_async(Event::End(BytesEnd::new(tag::ERROR_ELEMENT)))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct StreamAttrs {
|
||||
from: String,
|
||||
to: String,
|
||||
namespace: XMLNamespace,
|
||||
}
|
||||
|
||||
impl TryFrom<Attributes<'_>> for StreamAttrs {
|
||||
type Error = StreamError;
|
||||
|
||||
fn try_from(value: Attributes<'_>) -> std::result::Result<Self, Self::Error> {
|
||||
let mut from: Option<String> = None;
|
||||
let mut to: Option<String> = None;
|
||||
let mut ns: Option<XMLNamespace> = None;
|
||||
for v in value {
|
||||
let v = v?;
|
||||
match v.key.local_name().into_inner() {
|
||||
b"from" => {
|
||||
from = Some(String::from_utf8(v.value.to_vec())?);
|
||||
}
|
||||
b"to" => {
|
||||
to = Some(String::from_utf8(v.value.to_vec())?);
|
||||
}
|
||||
b"xmlns" => match v.value.to_vec().as_slice() {
|
||||
b"jabber:client" => {
|
||||
ns = Some(XMLNamespace::JabberClient);
|
||||
}
|
||||
_ => return Err(StreamError::InvalidNamespace),
|
||||
},
|
||||
other => {
|
||||
info!(
|
||||
"ignoring key {}",
|
||||
String::from_utf8(other.to_vec()).unwrap_or_default()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(StreamAttrs {
|
||||
from: from.ok_or(StreamError::InvalidFrom)?,
|
||||
to: to.ok_or(StreamError::HostUnknown)?,
|
||||
namespace: ns.ok_or(StreamError::BadNamespacePrefix)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum XMLNamespace {
|
||||
JabberClient,
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
use async_trait::async_trait;
|
||||
use quick_xml::{
|
||||
events::{BytesEnd, BytesStart, Event},
|
||||
Writer,
|
||||
};
|
||||
use tokio::io::AsyncWrite;
|
||||
|
||||
use crate::error::StreamError;
|
||||
|
||||
pub const STREAM_ELEMENT_NAME: &[u8] = b"stream:stream";
|
||||
pub const ERROR_ELEMENT: &str = "stream:error";
|
||||
pub const TLS_NAMESPACE: &str = "urn:ietf:params:xml:ns:xmpp-tls";
|
||||
pub const STARTTLS: &[u8] = b"starttls";
|
||||
pub const FEATURE: &str = "stream:features";
|
||||
|
||||
pub struct HollowTag<'a> {
|
||||
name: &'a str,
|
||||
namespace: &'static str,
|
||||
}
|
||||
|
||||
impl<'a> From<(&'a str, &'static str)> for HollowTag<'a> {
|
||||
fn from((name, namespace): (&'a str, &'static str)) -> Self {
|
||||
Self { name, namespace }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Tag {
|
||||
async fn write_tag<W>(&self, writer: W) -> Result<(), StreamError>
|
||||
where
|
||||
W: AsyncWrite + Unpin + Send;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'a> Tag for HollowTag<'a> {
|
||||
async fn write_tag<W>(&self, writer: W) -> Result<(), StreamError>
|
||||
where
|
||||
W: AsyncWrite + Unpin + Send,
|
||||
{
|
||||
let mut writer = Writer::new(writer);
|
||||
writer
|
||||
.write_event_async(Event::Start(
|
||||
BytesStart::new(self.name).with_attributes(vec![("xmlns", self.namespace)]),
|
||||
))
|
||||
.await?;
|
||||
writer
|
||||
.write_event_async(Event::End(BytesEnd::new(self.name)))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
|
Loading…
Reference in New Issue