#[derive(Debug, thiserror::Error)]
pub enum UgosError {
#[error("incorrect account or password")]
AuthFailed,
#[error("parameter error: {0}")]
ParameterError(String),
#[error("login expired")]
LoginExpired,
#[error("operation failed: {0}")]
OperationFailed(String),
#[error("app not found: {0}")]
AppNotFound(String),
#[error("app service error: {0}")]
AppServiceError(String),
#[error("API error {code}: {msg}")]
Api {
code: i32,
msg: String,
},
#[error("encryption error: {0}")]
Encryption(String),
#[error("request to {url} failed")]
Http {
url: String,
source: reqwest::Error,
},
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error("{context}")]
Io {
context: String,
source: std::io::Error,
},
#[error("{kind} not found: {name}")]
NotFound {
kind: &'static str,
name: String,
},
}
impl UgosError {
pub(crate) fn io(context: impl Into<String>, source: std::io::Error) -> Self {
Self::Io {
context: context.into(),
source,
}
}
}
pub type Result<T> = std::result::Result<T, UgosError>;
const SENSITIVE_PARAMS: [&str; 3] = ["token", "password", "passwd"];
#[must_use]
pub fn redact_url(url: &reqwest::Url) -> String {
if url.query().is_none() {
return url.to_string();
}
let pairs: Vec<(String, String)> = url
.query_pairs()
.map(|(key, value)| {
let value = if SENSITIVE_PARAMS.contains(&key.as_ref()) {
"REDACTED".to_owned()
} else {
value.into_owned()
};
(key.into_owned(), value)
})
.collect();
let mut clean = url.clone();
let _serializer = clean.query_pairs_mut().clear().extend_pairs(pairs);
clean.to_string()
}
impl From<reqwest::Error> for UgosError {
fn from(err: reqwest::Error) -> Self {
let url = err
.url()
.map_or_else(|| "<unknown URL>".to_owned(), redact_url);
Self::Http {
url,
source: err.without_url(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn url(input: &str) -> reqwest::Url {
reqwest::Url::parse(input).unwrap_or_else(|_| {
reqwest::Url::parse("https://invalid.example/").unwrap_or_else(|_| unreachable!())
})
}
#[test]
fn removes_the_session_token() {
let shown = redact_url(&url("https://nas:9443/ugreen/v1/vm?token=SECRET123"));
assert!(!shown.contains("SECRET123"));
assert!(shown.contains("token=REDACTED"));
}
#[test]
fn keeps_the_path_for_debugging() {
let shown = redact_url(&url(
"https://nas:9443/ugreen/v1/sysinfo/machine/common?token=X",
));
assert!(shown.contains("/ugreen/v1/sysinfo/machine/common"));
assert!(shown.contains("nas:9443"));
}
#[test]
fn keeps_harmless_parameters() {
let shown = redact_url(&url("https://nas/v1/kvm/PowerOn?name=debian&token=X"));
assert!(shown.contains("name=debian"));
assert!(!shown.contains("token=X"));
}
#[test]
fn redacts_a_token_in_any_position() {
let shown = redact_url(&url(
"https://nas/v1/x?token=SECRET&name=a&password=HUNTER2",
));
assert!(!shown.contains("SECRET"));
assert!(!shown.contains("HUNTER2"));
}
#[test]
fn leaves_a_url_without_a_query_alone() {
let plain = "https://nas:9443/ugreen/v1/verify/login";
assert_eq!(redact_url(&url(plain)), plain);
}
#[test]
fn keeps_the_encrypted_query_visible() {
let shown = redact_url(&url("https://nas/v2/filemgr/list?encrypt_query=AbCd"));
assert!(shown.contains("encrypt_query=AbCd"));
}
}