use reqwest::header::HeaderValue;
use serde::Deserialize;
use crate::http::{AUTH_HEADER_NAME, AUTH_QUERY_PARAM};
use crate::types::AuthMethod;
#[derive(Deserialize)]
struct ValidLoginResponse {
#[serde(default)]
result: ValidLoginResult,
}
#[derive(Deserialize, Default)]
#[serde(try_from = "serde_json::Value")]
struct ValidLoginResult(bool);
impl ValidLoginResult {
fn is_valid(&self) -> bool {
self.0
}
}
impl TryFrom<serde_json::Value> for ValidLoginResult {
type Error = String;
fn try_from(v: serde_json::Value) -> std::result::Result<Self, Self::Error> {
match v {
serde_json::Value::Bool(b) => Ok(Self(b)),
serde_json::Value::Number(n) => Ok(Self(n.as_u64() == Some(1))),
other => Err(format!("expected bool or integer, got {other}")),
}
}
}
pub(super) enum ValidLoginOutcome {
Authenticated(AuthMethod),
AuthRejected,
NetworkError,
}
pub(super) async fn detect_valid_login_auth(
http: &reqwest::Client,
base: &str,
api_key: &str,
key_header: &HeaderValue,
login: &str,
) -> ValidLoginOutcome {
let url = format!("{base}/rest/valid_login");
let probes: [(_, _, _); 2] = [
(vec![("login", login)], Some(key_header), AuthMethod::Header),
(
vec![("login", login), (AUTH_QUERY_PARAM, api_key)],
None,
AuthMethod::QueryParam,
),
];
for (query, header, method) in &probes {
match probe_valid_login(http, &url, query, *header, *method).await {
ValidLoginOutcome::AuthRejected => {} outcome => return outcome,
}
}
tracing::debug!("valid_login probes both failed");
ValidLoginOutcome::AuthRejected
}
async fn probe_valid_login(
http: &reqwest::Client,
url: &str,
query: &[(&str, &str)],
key_header: Option<&HeaderValue>,
method: AuthMethod,
) -> ValidLoginOutcome {
let mut req = http.get(url).query(query);
if let Some(hdr) = key_header {
req = req.header(AUTH_HEADER_NAME, hdr.clone());
}
let resp = match req.send().await {
Ok(r) => r,
Err(e) => {
tracing::warn!(error = %e, "valid_login probe network error");
return ValidLoginOutcome::NetworkError;
}
};
let status = resp.status();
if !status.is_success() {
tracing::debug!(%status, %method, "valid_login probe failed");
return ValidLoginOutcome::AuthRejected;
}
let body_text = match resp.text().await {
Ok(t) => t,
Err(e) => {
tracing::warn!(error = %e, "valid_login response read error");
return ValidLoginOutcome::NetworkError;
}
};
tracing::trace!(probe = "valid_login", %method, body = body_text, "auth probe response");
let parsed: ValidLoginResponse = match serde_json::from_str(&body_text) {
Ok(p) => p,
Err(_) => return ValidLoginOutcome::AuthRejected,
};
if parsed.result.is_valid() {
ValidLoginOutcome::Authenticated(method)
} else {
tracing::debug!(%method, "valid_login returned false");
ValidLoginOutcome::AuthRejected
}
}
pub(super) async fn verify_header_auth_via_rest(
http: &reqwest::Client,
base: &str,
key_header: &HeaderValue,
) -> bool {
let url = format!("{base}/rest/bug");
let resp = http
.get(&url)
.query(&[("limit", "1")])
.header(AUTH_HEADER_NAME, key_header.clone())
.send()
.await;
match resp {
Ok(r) if r.status().is_success() => {
tracing::debug!("header auth probe on rest/bug succeeded");
true
}
Ok(r) => {
tracing::debug!(
status = %r.status(),
"header auth probe on rest/bug failed"
);
false
}
Err(e) => {
tracing::debug!("header auth probe request failed: {e}");
false
}
}
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn valid_login_result_from_bool_true() {
let v: ValidLoginResult = serde_json::Value::Bool(true).try_into().unwrap();
assert!(v.is_valid());
}
#[test]
fn valid_login_result_from_bool_false() {
let v: ValidLoginResult = serde_json::Value::Bool(false).try_into().unwrap();
assert!(!v.is_valid());
}
#[test]
fn valid_login_result_from_integer_1() {
let v: ValidLoginResult = serde_json::json!(1).try_into().unwrap();
assert!(v.is_valid());
}
#[test]
fn valid_login_result_from_integer_0() {
let v: ValidLoginResult = serde_json::json!(0).try_into().unwrap();
assert!(!v.is_valid());
}
#[test]
fn valid_login_result_from_string_errors() {
let result: Result<ValidLoginResult, _> = serde_json::json!("yes").try_into();
assert!(result.is_err());
}
#[test]
fn valid_login_response_deserializes() {
let json = r#"{"result": true}"#;
let resp: ValidLoginResponse = serde_json::from_str(json).unwrap();
assert!(resp.result.is_valid());
}
#[test]
fn valid_login_response_integer_result() {
let json = r#"{"result": 1}"#;
let resp: ValidLoginResponse = serde_json::from_str(json).unwrap();
assert!(resp.result.is_valid());
}
#[test]
fn valid_login_response_missing_result_defaults_false() {
let json = r"{}";
let resp: ValidLoginResponse = serde_json::from_str(json).unwrap();
assert!(!resp.result.is_valid());
}
}