#[must_use]
pub(crate) fn percent_encode_query_value(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for b in s.bytes() {
match b {
b'&' | b'=' | b'#' | b'+' | b' ' | b'%' | 0x00..=0x1F | 0x7F..=0xFF => {
out.push('%');
out.push(char::from(b"0123456789ABCDEF"[(b >> 4) as usize]));
out.push(char::from(b"0123456789ABCDEF"[(b & 0x0f) as usize]));
}
_ => out.push(char::from(b)),
}
}
out
}
pub(crate) fn validate_http_endpoint(endpoint: &str) -> Result<(), &'static str> {
if !endpoint.starts_with("http://") && !endpoint.starts_with("https://") {
return Err("endpoint must start with http:// or https://");
}
let after_scheme = endpoint
.strip_prefix("https://")
.or_else(|| endpoint.strip_prefix("http://"))
.unwrap_or("");
let authority_end = after_scheme.find(['/', '?']).unwrap_or(after_scheme.len());
if after_scheme[..authority_end].contains('@') {
return Err("endpoint must not contain credentials (user:pass@host)");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn percent_encode_escapes_reserved_bytes() {
assert_eq!(percent_encode_query_value("a&b=c"), "a%26b%3Dc");
assert_eq!(percent_encode_query_value("hello world"), "hello%20world");
assert_eq!(percent_encode_query_value("plain"), "plain");
}
#[test]
fn validate_http_endpoint_accepts_plain_http() {
assert!(validate_http_endpoint("http://tempo:3200").is_ok());
assert!(validate_http_endpoint("https://jaeger.prod/api").is_ok());
}
#[test]
fn validate_http_endpoint_rejects_non_http_scheme() {
assert!(validate_http_endpoint("ftp://x").is_err());
assert!(validate_http_endpoint("x").is_err());
}
#[test]
fn validate_http_endpoint_rejects_credentials() {
assert!(validate_http_endpoint("http://user:pass@host").is_err());
assert!(validate_http_endpoint("https://u@jaeger").is_err());
}
#[test]
fn validate_http_endpoint_accepts_at_in_query_string() {
assert!(validate_http_endpoint("http://host/api?owner=foo%40example.com").is_ok());
}
}