use std::{fmt, str::FromStr};
use http::uri::{Authority, PathAndQuery, Scheme, Uri};
use percent_encoding::percent_decode_str;
use vector_lib::configurable::configurable_component;
use crate::http::Auth;
#[configurable_component]
#[configurable(title = "The URI component of a request.", description = "")]
#[derive(Default, Debug, Clone)]
#[serde(try_from = "String", into = "String")]
pub struct UriSerde {
pub uri: Uri,
pub auth: Option<Auth>,
}
impl UriSerde {
pub fn with_default_parts(&self) -> Self {
let mut parts = self.uri.clone().into_parts();
if parts.scheme.is_none() {
parts.scheme = Some(Scheme::HTTP);
}
if parts.authority.is_none() {
parts.authority = Some(Authority::from_static("127.0.0.1"));
}
if parts.path_and_query.is_none() {
parts.path_and_query = Some(PathAndQuery::from_static(""));
}
let uri = Uri::from_parts(parts).expect("invalid parts");
Self {
uri,
auth: self.auth.clone(),
}
}
pub fn append_path(&self, path: &str) -> crate::Result<Self> {
let uri = self.uri.to_string();
let self_path = uri.trim_end_matches('/');
let other_path = path.trim_start_matches('/');
let path = format!("{}/{}", self_path, other_path);
let uri = path.parse::<Uri>()?;
Ok(Self {
uri,
auth: self.auth.clone(),
})
}
#[allow(clippy::missing_const_for_fn)] pub fn with_auth(mut self, auth: Option<Auth>) -> Self {
self.auth = auth;
self
}
}
impl TryFrom<String> for UriSerde {
type Error = <Uri as FromStr>::Err;
fn try_from(value: String) -> Result<Self, Self::Error> {
value.as_str().parse()
}
}
impl From<UriSerde> for String {
fn from(uri: UriSerde) -> Self {
uri.to_string()
}
}
impl fmt::Display for UriSerde {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match (self.uri.authority(), &self.auth) {
(Some(authority), Some(Auth::Basic { user, password })) => {
let authority = format!("{}:{}@{}", user, password, authority);
let authority =
Authority::from_maybe_shared(authority).map_err(|_| std::fmt::Error)?;
let mut parts = self.uri.clone().into_parts();
parts.authority = Some(authority);
Uri::from_parts(parts).unwrap().fmt(f)
}
_ => self.uri.fmt(f),
}
}
}
impl FromStr for UriSerde {
type Err = <Uri as FromStr>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.parse::<Uri>().map(Into::into)
}
}
impl From<Uri> for UriSerde {
fn from(uri: Uri) -> Self {
match uri.authority() {
None => Self { uri, auth: None },
Some(authority) => {
let (authority, auth) = get_basic_auth(authority);
let mut parts = uri.into_parts();
parts.authority = Some(authority);
let uri = Uri::from_parts(parts).unwrap();
Self { uri, auth }
}
}
}
}
fn get_basic_auth(authority: &Authority) -> (Authority, Option<Auth>) {
let mut url = url::Url::parse(&format!("http://{}", authority)).expect("invalid authority");
let user = url.username();
if !user.is_empty() {
let user = percent_decode_str(user).decode_utf8_lossy().into_owned();
let password = url.password().unwrap_or("");
let password = percent_decode_str(password)
.decode_utf8_lossy()
.into_owned();
url.set_username("").expect("unexpected empty authority");
url.set_password(None).expect("unexpected empty authority");
let authority = Uri::from_maybe_shared(String::from(url))
.expect("invalid url")
.authority()
.expect("unexpected empty authority")
.clone();
(
authority,
Some(Auth::Basic {
user,
password: password.into(),
}),
)
} else {
(authority.clone(), None)
}
}
pub fn protocol_endpoint(uri: Uri) -> (String, String) {
let mut parts = uri.into_parts();
parts.authority = parts.authority.map(|auth| {
let host = auth.host();
match auth.port() {
None => host.to_string(),
Some(port) => format!("{}:{}", host, port),
}
.parse()
.unwrap_or_else(|_| unreachable!())
});
parts.path_and_query = parts.path_and_query.map(|pq| {
pq.path()
.parse::<PathAndQuery>()
.unwrap_or_else(|_| unreachable!())
});
(
parts.scheme.clone().unwrap_or(Scheme::HTTP).as_str().into(),
Uri::from_parts(parts)
.unwrap_or_else(|_| unreachable!())
.to_string(),
)
}
#[cfg(test)]
mod tests {
use super::*;
fn test_parse(input: &str, expected_uri: &'static str, expected_auth: Option<(&str, &str)>) {
let UriSerde { uri, auth } = input.parse().unwrap();
assert_eq!(uri, Uri::from_static(expected_uri));
assert_eq!(
auth,
expected_auth.map(|(user, password)| {
Auth::Basic {
user: user.to_owned(),
password: password.to_owned().into(),
}
})
);
}
#[test]
fn parse_endpoint() {
test_parse(
"http://user:pass@example.com/test",
"http://example.com/test",
Some(("user", "pass")),
);
test_parse("localhost:8080", "localhost:8080", None);
test_parse("/api/test", "/api/test", None);
test_parse(
"http://user:pass;@example.com",
"http://example.com",
Some(("user", "pass;")),
);
test_parse(
"user:pass@example.com",
"example.com",
Some(("user", "pass")),
);
test_parse("user@example.com", "example.com", Some(("user", "")));
}
#[test]
fn protocol_endpoint_parses_urls() {
let parse = |uri: &str| protocol_endpoint(uri.parse().unwrap());
assert_eq!(
parse("http://example.com/"),
("http".into(), "http://example.com/".into())
);
assert_eq!(
parse("https://user:pass@example.org:123/path?query"),
("https".into(), "https://example.org:123/path".into())
);
assert_eq!(
parse("gopher://example.net:123/path?query#frag,emt"),
("gopher".into(), "gopher://example.net:123/path".into())
);
}
}