opencellid 0.2.0

Rust client library for the OpenCellID API — sync and async clients with tracing, structured errors, and bounded I/O.
Documentation
//! Helpers for safe tracing of requests (secret redaction in URLs).

use url::Url;

/// Query parameter names that hold authentication secrets and must never be
/// emitted to logs in clear text.
const SECRET_PARAMS: &[&str] = &["key", "token"];

/// Returns a copy of `url` with secret query parameters replaced by `***`.
///
/// Currently redacts both the `key` (used by the `cell/*` and `measure/*` API
/// endpoints) and `token` (used by the `ocid/downloads` and `downloads.php`
/// endpoints) parameters. The original URL is left untouched.
pub(crate) fn redact_api_key(url: &Url) -> String {
    let mut redacted = url.clone();
    let pairs: Vec<(String, String)> = redacted
        .query_pairs()
        .map(|(k, v)| {
            if SECRET_PARAMS.iter().any(|s| *s == k.as_ref()) {
                (k.into_owned(), "***".to_string())
            } else {
                (k.into_owned(), v.into_owned())
            }
        })
        .collect();
    redacted
        .query_pairs_mut()
        .clear()
        .extend_pairs(pairs.iter().map(|(k, v)| (k.as_str(), v.as_str())));
    redacted.to_string()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn redacts_key_query_param() {
        let u = Url::parse("https://opencellid.org/cell/get?key=SECRET&mcc=250&mnc=1").unwrap();
        let s = redact_api_key(&u);
        assert!(!s.contains("SECRET"));
        assert!(s.contains("key=***"));
        assert!(s.contains("mcc=250"));
    }

    #[test]
    fn redacts_token_query_param() {
        let u =
            Url::parse("https://opencellid.org/ocid/downloads?token=SECRET&type=full").unwrap();
        let s = redact_api_key(&u);
        assert!(!s.contains("SECRET"));
        assert!(s.contains("token=***"));
        assert!(s.contains("type=full"));
    }

    #[test]
    fn leaves_url_without_secrets_alone() {
        let u = Url::parse("https://opencellid.org/cell/get?mcc=250").unwrap();
        let s = redact_api_key(&u);
        assert!(s.contains("mcc=250"));
    }
}