1mod http_error;
5mod request_handle;
6mod url_target;
7
8pub use http_error::HttpError;
9
10pub type ProgressFn<'a> = &'a mut dyn FnMut(u64, Option<u64>);
12
13const TIMEOUT_RESOLVE_MS: i32 = 10_000;
16const TIMEOUT_CONNECT_MS: i32 = 10_000;
17const TIMEOUT_SEND_MS: i32 = 30_000;
18const TIMEOUT_RECEIVE_MS: i32 = 30_000;
19
20const CHUNK_SIZE: usize = 64 * 1024;
22
23const USER_AGENT: &str = concat!("Pacenotes-Installer/", env!("PACENOTES_VERSION"));
25
26const MAX_BUFFERED_BYTES: usize = 8 * 1024 * 1024;
31
32pub fn get(
35 url: &str,
36 bearer: Option<&str>,
37 progress: ProgressFn,
38 cancel: &std::sync::atomic::AtomicBool,
39) -> Result<Vec<u8>, HttpError> {
40 let mut body = Vec::new();
41 let mut too_large = false;
42 let fetched = get_streaming(
43 url,
44 bearer,
45 &mut |chunk| {
46 too_large = body.len() + chunk.len() > MAX_BUFFERED_BYTES;
47 if !too_large {
48 body.extend_from_slice(chunk);
49 }
50 !too_large
51 },
52 progress,
53 cancel,
54 );
55 if too_large {
56 return Err(HttpError::logical(
57 format!(
58 "the response is larger than the {} accepted from {url}",
59 pacenotes_installer::size::human(MAX_BUFFERED_BYTES as u64)
60 ),
61 None,
62 ));
63 }
64 fetched.map(|()| body)
65}
66
67pub fn get_streaming(
70 url: &str,
71 bearer: Option<&str>,
72 sink: &mut dyn FnMut(&[u8]) -> bool,
73 progress: ProgressFn,
74 cancel: &std::sync::atomic::AtomicBool,
75) -> Result<(), HttpError> {
76 send("GET", url, bearer, None, sink, progress, cancel)
77}
78
79pub fn post_json(
82 url: &str,
83 body: &[u8],
84 cancel: &std::sync::atomic::AtomicBool,
85) -> Result<(), HttpError> {
86 let mut ignore_progress = |_: u64, _: Option<u64>| {};
87 send(
88 "POST",
89 url,
90 None,
91 Some(body),
92 &mut |_| true,
93 &mut ignore_progress,
94 cancel,
95 )
96}
97
98const MAX_REDIRECTS: usize = 3;
101
102fn send(
109 method: &str,
110 url: &str,
111 bearer: Option<&str>,
112 body: Option<&[u8]>,
113 sink: &mut dyn FnMut(&[u8]) -> bool,
114 progress: ProgressFn,
115 cancel: &std::sync::atomic::AtomicBool,
116) -> Result<(), HttpError> {
117 let mut target = url_target::UrlTarget::parse(url)?;
118 let origin = target.origin();
119 for _ in 0..=MAX_REDIRECTS {
120 let token = bearer.filter(|_| target.origin() == origin);
121 let open = request_handle::OpenRequest::send(&target, method, token, body)?;
122 let status = open
123 .query_number(windows_sys::Win32::Networking::WinHttp::WINHTTP_QUERY_STATUS_CODE)?
124 as u16;
125 let Some(location) = redirect_location(&open, status, body.is_some())? else {
126 return read_response(&open, status, &target, sink, progress, cancel);
127 };
128 let next = target.resolve(&location)?;
129 if target.secure && !next.secure {
130 return Err(HttpError::logical(
131 format!("refusing a redirect from https to http ({location})"),
132 None,
133 ));
134 }
135 target = next;
136 }
137 Err(HttpError::logical(
138 format!("gave up after {MAX_REDIRECTS} redirects from {url}"),
139 None,
140 ))
141}
142
143fn redirect_location(
146 open: &request_handle::OpenRequest,
147 status: u16,
148 has_body: bool,
149) -> Result<Option<String>, HttpError> {
150 if !(300..400).contains(&status) {
151 return Ok(None);
152 }
153 if has_body {
154 return Err(HttpError::logical(
155 format!("HTTP {status}: refusing to redirect a request with a body"),
156 Some(status),
157 ));
158 }
159 open.query_text(windows_sys::Win32::Networking::WinHttp::WINHTTP_QUERY_LOCATION)
160 .map(Some)
161 .ok_or_else(|| {
162 HttpError::logical(
163 format!("HTTP {status} without a Location header"),
164 Some(status),
165 )
166 })
167}
168
169fn read_response(
170 open: &request_handle::OpenRequest,
171 status: u16,
172 target: &url_target::UrlTarget,
173 sink: &mut dyn FnMut(&[u8]) -> bool,
174 progress: ProgressFn,
175 cancel: &std::sync::atomic::AtomicBool,
176) -> Result<(), HttpError> {
177 if !(200..300).contains(&status) {
178 return Err(HttpError::logical(
179 format!("HTTP {status} for {}", target.url()),
180 Some(status),
181 ));
182 }
183 let content_length = open
184 .query_number(windows_sys::Win32::Networking::WinHttp::WINHTTP_QUERY_CONTENT_LENGTH)
185 .ok()
186 .map(u64::from);
187
188 let bytes_read = read_body(open, sink, progress, content_length, cancel)?;
189 if let Some(expected) = content_length {
190 if bytes_read != expected {
191 return Err(HttpError::logical(
192 format!("truncated response: expected {expected} bytes, read {bytes_read}"),
193 Some(status),
194 ));
195 }
196 }
197 Ok(())
198}
199
200fn read_body(
201 open: &request_handle::OpenRequest,
202 sink: &mut dyn FnMut(&[u8]) -> bool,
203 progress: ProgressFn,
204 content_length: Option<u64>,
205 cancel: &std::sync::atomic::AtomicBool,
206) -> Result<u64, HttpError> {
207 let mut bytes_read: u64 = 0;
208 let mut chunk = [0u8; CHUNK_SIZE];
209 loop {
210 if cancel.load(std::sync::atomic::Ordering::Relaxed) {
211 return Err(HttpError::cancelled());
212 }
213 let read = open.read(&mut chunk)?;
214 if read == 0 {
215 return Ok(bytes_read);
216 }
217 if !sink(&chunk[..read]) {
218 return Err(HttpError::logical("aborted by caller", None));
219 }
220 bytes_read += read as u64;
221 progress(bytes_read, content_length);
222 }
223}