Skip to main content

pacenotes_installer\http/
url_target.rs

1/// A URL in the pieces `WinHttpConnect` and `WinHttpOpenRequest` take.
2pub struct UrlTarget {
3    pub host: String,
4    pub port: u16,
5    /// Path and query together.
6    pub path: String,
7    pub secure: bool,
8}
9
10impl UrlTarget {
11    /// Split by `WinHttpCrackUrl`, so odd-but-legal URLs divide as the OS stack expects.
12    pub fn parse(url: &str) -> Result<UrlTarget, super::HttpError> {
13        let wide = crate::i18n::to_wide(url);
14        let mut components: windows_sys::Win32::Networking::WinHttp::URL_COMPONENTS =
15            unsafe { core::mem::zeroed() };
16        components.dwStructSize =
17            core::mem::size_of::<windows_sys::Win32::Networking::WinHttp::URL_COMPONENTS>() as u32;
18        // A null pointer with a non-zero length asks for pointers into `wide` instead of copies.
19        components.dwSchemeLength = u32::MAX;
20        components.dwHostNameLength = u32::MAX;
21        components.dwUrlPathLength = u32::MAX;
22        components.dwExtraInfoLength = u32::MAX;
23
24        let length_without_nul = (wide.len() - 1) as u32;
25        let cracked = unsafe {
26            windows_sys::Win32::Networking::WinHttp::WinHttpCrackUrl(
27                wide.as_ptr(),
28                length_without_nul,
29                0,
30                &mut components,
31            )
32        };
33        if cracked == 0 {
34            return Err(super::HttpError::last(&format!("cannot parse URL {url}")));
35        }
36
37        let mut path = wide_slice(components.lpszUrlPath, components.dwUrlPathLength);
38        path.push_str(&wide_slice(
39            components.lpszExtraInfo,
40            components.dwExtraInfoLength,
41        ));
42        if path.is_empty() {
43            path.push('/');
44        }
45        Ok(UrlTarget {
46            host: wide_slice(components.lpszHostName, components.dwHostNameLength),
47            port: components.nPort,
48            path,
49            secure: components.nScheme
50                == windows_sys::Win32::Networking::WinHttp::WINHTTP_INTERNET_SCHEME_HTTPS,
51        })
52    }
53
54    /// Scheme, host and port, which decide whether the token may travel; the
55    /// host is lowercased, since `EXAMPLE.com` and `example.com` are one origin.
56    pub fn origin(&self) -> (bool, String, u16) {
57        (self.secure, self.host.to_ascii_lowercase(), self.port)
58    }
59
60    /// The URL this target stands for, for messages.
61    pub fn url(&self) -> String {
62        format!(
63            "{}://{}:{}{}",
64            self.scheme(),
65            self.host,
66            self.port,
67            self.path
68        )
69    }
70
71    /// Where a `Location` header points: absolute, rooted at `/`, or relative to
72    /// the current directory, all three of which happen.
73    pub fn resolve(&self, location: &str) -> Result<UrlTarget, super::HttpError> {
74        let location = location.trim();
75        if location.is_empty() {
76            return Err(super::HttpError::logical("empty Location header", None));
77        }
78        let lower = location.to_ascii_lowercase();
79        if lower.starts_with("http://") || lower.starts_with("https://") {
80            return UrlTarget::parse(location);
81        }
82        let base = match location.starts_with('/') {
83            true => "",
84            false => self.path.rfind('/').map_or("/", |at| &self.path[..=at]),
85        };
86        let (scheme, host, port) = (self.scheme(), &self.host, self.port);
87        UrlTarget::parse(&format!("{scheme}://{host}:{port}{base}{location}"))
88    }
89
90    fn scheme(&self) -> &'static str {
91        if self.secure {
92            "https"
93        } else {
94            "http"
95        }
96    }
97}
98
99fn wide_slice(start: *const u16, length: u32) -> String {
100    if start.is_null() || length == 0 {
101        return String::new();
102    }
103    unsafe { String::from_utf16_lossy(core::slice::from_raw_parts(start, length as usize)) }
104}