vector/sinks/util/
uri.rs

1use std::{fmt, str::FromStr};
2
3use http::uri::{Authority, PathAndQuery, Scheme, Uri};
4use percent_encoding::percent_decode_str;
5use vector_lib::configurable::configurable_component;
6
7use crate::http::Auth;
8
9/// A wrapper for `http::Uri` that implements `Deserialize` and `Serialize`.
10///
11/// Authorization credentials, if they exist, will be removed from the URI and stored separately in `auth`.
12#[configurable_component]
13#[configurable(title = "The URI component of a request.", description = "")]
14#[derive(Default, Debug, Clone)]
15#[serde(try_from = "String", into = "String")]
16pub struct UriSerde {
17    pub uri: Uri,
18    pub auth: Option<Auth>,
19}
20
21impl UriSerde {
22    /// `Uri` supports incomplete URIs such as "/test", "example.com", etc.
23    /// This function fills in empty scheme with HTTP,
24    /// and empty authority with "127.0.0.1".
25    pub fn with_default_parts(&self) -> Self {
26        let mut parts = self.uri.clone().into_parts();
27        if parts.scheme.is_none() {
28            parts.scheme = Some(Scheme::HTTP);
29        }
30        if parts.authority.is_none() {
31            parts.authority = Some(Authority::from_static("127.0.0.1"));
32        }
33        if parts.path_and_query.is_none() {
34            // just an empty `path_and_query`,
35            // but `from_parts` will fail without this.
36            parts.path_and_query = Some(PathAndQuery::from_static(""));
37        }
38        let uri = Uri::from_parts(parts).expect("invalid parts");
39        Self {
40            uri,
41            auth: self.auth.clone(),
42        }
43    }
44
45    /// Creates a new instance of `UriSerde` by appending a path to the existing one.
46    pub fn append_path(&self, path: &str) -> crate::Result<Self> {
47        let uri = self.uri.to_string();
48        let self_path = uri.trim_end_matches('/');
49        let other_path = path.trim_start_matches('/');
50        let path = format!("{self_path}/{other_path}");
51        let uri = path.parse::<Uri>()?;
52        Ok(Self {
53            uri,
54            auth: self.auth.clone(),
55        })
56    }
57
58    #[allow(clippy::missing_const_for_fn)] // constant functions cannot evaluate destructors
59    pub fn with_auth(mut self, auth: Option<Auth>) -> Self {
60        self.auth = auth;
61        self
62    }
63}
64
65impl TryFrom<String> for UriSerde {
66    type Error = <Uri as FromStr>::Err;
67
68    fn try_from(value: String) -> Result<Self, Self::Error> {
69        value.as_str().parse()
70    }
71}
72
73impl From<UriSerde> for String {
74    fn from(uri: UriSerde) -> Self {
75        uri.to_string()
76    }
77}
78
79impl fmt::Display for UriSerde {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        match (self.uri.authority(), &self.auth) {
82            (Some(authority), Some(Auth::Basic { user, password })) => {
83                let authority = format!("{user}:{password}@{authority}");
84                let authority =
85                    Authority::from_maybe_shared(authority).map_err(|_| std::fmt::Error)?;
86                let mut parts = self.uri.clone().into_parts();
87                parts.authority = Some(authority);
88                Uri::from_parts(parts).unwrap().fmt(f)
89            }
90            _ => self.uri.fmt(f),
91        }
92    }
93}
94
95impl FromStr for UriSerde {
96    type Err = <Uri as FromStr>::Err;
97
98    fn from_str(s: &str) -> Result<Self, Self::Err> {
99        s.parse::<Uri>().map(Into::into)
100    }
101}
102
103impl From<Uri> for UriSerde {
104    fn from(uri: Uri) -> Self {
105        match uri.authority() {
106            None => Self { uri, auth: None },
107            Some(authority) => {
108                let (authority, auth) = get_basic_auth(authority);
109
110                let mut parts = uri.into_parts();
111                parts.authority = Some(authority);
112                let uri = Uri::from_parts(parts).unwrap();
113
114                Self { uri, auth }
115            }
116        }
117    }
118}
119
120fn get_basic_auth(authority: &Authority) -> (Authority, Option<Auth>) {
121    // We get a valid `Authority` as input, therefore cannot fail here.
122    let mut url = url::Url::parse(&format!("http://{authority}")).expect("invalid authority");
123
124    let user = url.username();
125    if !user.is_empty() {
126        let user = percent_decode_str(user).decode_utf8_lossy().into_owned();
127
128        let password = url.password().unwrap_or("");
129        let password = percent_decode_str(password)
130            .decode_utf8_lossy()
131            .into_owned();
132
133        // These methods have the same failure condition as `username`,
134        // because we have a non-empty username, they cannot fail here.
135        url.set_username("").expect("unexpected empty authority");
136        url.set_password(None).expect("unexpected empty authority");
137
138        // We get a valid `Authority` as input, therefore cannot fail here.
139        let authority = Uri::from_maybe_shared(String::from(url))
140            .expect("invalid url")
141            .authority()
142            .expect("unexpected empty authority")
143            .clone();
144
145        (
146            authority,
147            Some(Auth::Basic {
148                user,
149                password: password.into(),
150            }),
151        )
152    } else {
153        (authority.clone(), None)
154    }
155}
156
157/// Simplify the URI into a protocol and endpoint by removing the
158/// "query" portion of the `path_and_query`.
159pub fn protocol_endpoint(uri: Uri) -> (String, String) {
160    let mut parts = uri.into_parts();
161
162    // Drop any username and password
163    parts.authority = parts.authority.map(|auth| {
164        let host = auth.host();
165        match auth.port() {
166            None => host.to_string(),
167            Some(port) => format!("{host}:{port}"),
168        }
169        .parse()
170        .unwrap_or_else(|_| unreachable!())
171    });
172
173    // Drop the query and fragment
174    parts.path_and_query = parts.path_and_query.map(|pq| {
175        pq.path()
176            .parse::<PathAndQuery>()
177            .unwrap_or_else(|_| unreachable!())
178    });
179
180    (
181        parts.scheme.clone().unwrap_or(Scheme::HTTP).as_str().into(),
182        Uri::from_parts(parts)
183            .unwrap_or_else(|_| unreachable!())
184            .to_string(),
185    )
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    fn test_parse(input: &str, expected_uri: &'static str, expected_auth: Option<(&str, &str)>) {
193        let UriSerde { uri, auth } = input.parse().unwrap();
194        assert_eq!(uri, Uri::from_static(expected_uri));
195        assert_eq!(
196            auth,
197            expected_auth.map(|(user, password)| {
198                Auth::Basic {
199                    user: user.to_owned(),
200                    password: password.to_owned().into(),
201                }
202            })
203        );
204    }
205
206    #[test]
207    fn parse_endpoint() {
208        test_parse(
209            "http://user:pass@example.com/test",
210            "http://example.com/test",
211            Some(("user", "pass")),
212        );
213
214        test_parse("localhost:8080", "localhost:8080", None);
215
216        test_parse("/api/test", "/api/test", None);
217
218        test_parse(
219            "http://user:pass;@example.com",
220            "http://example.com",
221            Some(("user", "pass;")),
222        );
223
224        test_parse(
225            "user:pass@example.com",
226            "example.com",
227            Some(("user", "pass")),
228        );
229
230        test_parse("user@example.com", "example.com", Some(("user", "")));
231    }
232
233    #[test]
234    fn protocol_endpoint_parses_urls() {
235        let parse = |uri: &str| protocol_endpoint(uri.parse().unwrap());
236
237        assert_eq!(
238            parse("http://example.com/"),
239            ("http".into(), "http://example.com/".into())
240        );
241        assert_eq!(
242            parse("https://user:pass@example.org:123/path?query"),
243            ("https".into(), "https://example.org:123/path".into())
244        );
245        assert_eq!(
246            parse("gopher://example.net:123/path?query#frag,emt"),
247            ("gopher".into(), "gopher://example.net:123/path".into())
248        );
249    }
250}