use crate::auth::AuthTransport;
use crate::error::WinrmError;
pub(crate) struct KerberosAuth {
#[cfg_attr(not(feature = "kerberos"), allow(dead_code))]
pub(crate) service_principal: String, }
#[cfg(feature = "kerberos")]
impl AuthTransport for KerberosAuth {
async fn send_authenticated(
&self,
http: &reqwest::Client,
url: &str,
body: String,
) -> Result<String, WinrmError> {
use base64::Engine;
use base64::engine::general_purpose::STANDARD as B64;
use cross_krb5::{ClientCtx, InitiateFlags};
use reqwest::header::{AUTHORIZATION, CONTENT_TYPE};
let (ctx, token) = ClientCtx::new(
InitiateFlags::empty(),
None, &self.service_principal,
None,
)
.map_err(|e| WinrmError::AuthFailed(format!("Kerberos init: {e}")))?;
let auth_header = format!("Negotiate {}", B64.encode(&*token));
let resp = http
.post(url)
.header(CONTENT_TYPE, "application/soap+xml;charset=UTF-8")
.header(AUTHORIZATION, &auth_header)
.body(body)
.send()
.await
.map_err(WinrmError::Http)?;
if let Some(www_auth) = resp
.headers()
.get("WWW-Authenticate")
.and_then(|v| v.to_str().ok())
&& let Some(token_b64) = www_auth.strip_prefix("Negotiate ")
&& let Ok(server_token) = B64.decode(token_b64.trim_ascii())
{
let _ = ctx.step(&server_token);
}
if resp.status().as_u16() == 401 {
return Err(WinrmError::AuthFailed(
"Kerberos authentication rejected".into(),
));
}
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(WinrmError::AuthFailed(format!("HTTP {status}: {body}")));
}
resp.text().await.map_err(WinrmError::Http)
}
}
#[cfg(not(feature = "kerberos"))]
impl AuthTransport for KerberosAuth {
async fn send_authenticated(
&self,
_http: &reqwest::Client,
_url: &str,
_body: String,
) -> Result<String, WinrmError> {
Err(WinrmError::AuthFailed(
"Kerberos auth requires the 'kerberos' feature: \
cargo add winrm-rs --features kerberos"
.into(),
))
}
}
#[cfg(test)]
#[cfg(not(feature = "kerberos"))]
mod tests {
use super::*;
#[tokio::test]
async fn stub_returns_auth_failed_error() {
let auth = KerberosAuth {
service_principal: "HTTP/test".into(),
};
let http = reqwest::Client::new();
let result = auth
.send_authenticated(&http, "http://unused", "body".into())
.await;
assert!(result.is_err(), "stub must return Err, got Ok");
match result.unwrap_err() {
WinrmError::AuthFailed(msg) => {
assert!(
msg.contains("kerberos") && msg.contains("feature"),
"error should mention kerberos feature, got: {msg}"
);
}
other => panic!("expected AuthFailed, got: {other}"),
}
}
}