1use std::sync::Arc;
10use std::time::{Duration, SystemTime, UNIX_EPOCH};
11
12use reqwest::Client as ReqwestClient;
13use reqwest::header::{ACCEPT_ENCODING, AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderValue};
14use tokio::sync::Semaphore;
15use tracing::{debug, info, warn};
16use url::Url;
17
18use crate::core::auth::GoogleAuth;
19use crate::core::error::{GrrError, Result};
20
21pub(crate) const MAX_INFLIGHT_REQUESTS: usize = 64;
26const REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
28const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
30const POOL_IDLE_PER_HOST: usize = 32;
32const POOL_IDLE_TIMEOUT: Duration = Duration::from_secs(90);
33const TCP_KEEPALIVE: Duration = Duration::from_secs(60);
34const H2_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(30);
35const RETRY_ATTEMPTS: u32 = 3;
37const RETRY_BACKOFF_MS: u64 = 100;
41const MAX_RATE_LIMIT_WAIT: Duration = Duration::from_secs(30);
43
44const MONTHS: [&str; 12] = [
47 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
48];
49
50fn retry_backoff(attempt: u32) -> Duration {
52 Duration::from_millis(RETRY_BACKOFF_MS * 2_u64.pow(attempt))
53}
54
55fn parse_retry_after(value: Option<&str>, now: SystemTime) -> Option<Duration> {
60 let raw = value?.trim();
61 if let Ok(seconds) = raw.parse::<u64>() {
62 return Some(Duration::from_secs(seconds));
63 }
64 let at = http_date_unix_secs(raw)?;
65 let now = now.duration_since(UNIX_EPOCH).ok()?.as_secs() as i64;
66 Some(Duration::from_secs(at.saturating_sub(now).max(0) as u64))
67}
68
69fn http_date_unix_secs(raw: &str) -> Option<i64> {
74 let (_weekday, rest) = raw.split_once(", ")?;
75 let mut fields = rest.split(' ');
76 let day: i64 = fields.next()?.parse().ok()?;
77 let month_name = fields.next()?;
78 let month = MONTHS.iter().position(|m| *m == month_name)? as i64 + 1;
79 let year: i64 = fields.next()?.parse().ok()?;
80 let mut clock = fields.next()?.split(':');
81 let hour: i64 = clock.next()?.parse().ok()?;
82 let minute: i64 = clock.next()?.parse().ok()?;
83 let second: i64 = clock.next()?.parse().ok()?;
84 if fields.next()? != "GMT" {
85 return None;
86 }
87 if !(1..=31).contains(&day) || hour > 23 || minute > 59 || second > 60 {
89 return None;
90 }
91 Some(days_from_civil(year, month, day) * 86_400 + hour * 3600 + minute * 60 + second)
92}
93
94fn days_from_civil(year: i64, month: i64, day: i64) -> i64 {
97 let year = if month <= 2 { year - 1 } else { year };
98 let era = if year >= 0 { year } else { year - 399 } / 400;
99 let year_of_era = year - era * 400;
100 let day_of_year = (153 * ((month + 9) % 12) + 2) / 5 + day - 1;
101 let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
102 era * 146_097 + day_of_era - 719_468
103}
104
105pub(crate) fn parse_url(raw: &str, kind: &str) -> Result<Url> {
106 Url::parse(raw).map_err(|error| GrrError::Config(format!("Invalid {kind} URL: {error}")))
107}
108
109#[derive(Debug, Clone)]
110pub struct TransportInfo {
111 pub negotiated_version: String,
112 pub http3_requested: bool,
113 pub http3_effective: bool,
114 pub fell_back: bool,
115}
116
117impl Default for TransportInfo {
118 fn default() -> Self {
119 Self {
120 negotiated_version: "not-probed".into(),
121 http3_requested: true,
122 http3_effective: false,
123 fell_back: false,
124 }
125 }
126}
127
128#[derive(Clone)]
132pub struct HttpCore {
133 auth: Arc<GoogleAuth>,
134 http_client: ReqwestClient,
135 semaphore: Arc<Semaphore>,
136 transport_info: TransportInfo,
137}
138
139impl HttpCore {
140 pub async fn connect(auth: GoogleAuth, base_url: &Url, probe_path: &str) -> Result<Self> {
145 let mut info = TransportInfo {
146 http3_requested: true,
147 http3_effective: true,
148 ..TransportInfo::default()
149 };
150 let mut http_client = build_http_client()?;
151
152 match probe(
153 &http_client,
154 base_url,
155 &auth,
156 reqwest::Version::HTTP_3,
157 probe_path,
158 )
159 .await
160 {
161 Ok(version) => info.negotiated_version = version,
162 Err(GrrError::Auth(error)) => return Err(GrrError::Auth(error)),
163 Err(error) => {
164 warn!("HTTP/3 probe failed ({error}); rebuilding with HTTP/2");
165 info.http3_effective = false;
166 info.fell_back = true;
167 http_client = build_http_client_with_mode(false)?;
168 match probe(
169 &http_client,
170 base_url,
171 &auth,
172 reqwest::Version::HTTP_2,
173 probe_path,
174 )
175 .await
176 {
177 Ok(version) => info.negotiated_version = version,
178 Err(error) => {
179 warn!("HTTP/2 probe failed ({error}); continuing unprobed");
180 info.negotiated_version = "not-probed".into();
181 }
182 }
183 }
184 }
185
186 info!(
187 "HttpCore connected (probe {}: {}): http3_effective={}, max_inflight_requests={}",
188 probe_path, info.negotiated_version, info.http3_effective, MAX_INFLIGHT_REQUESTS
189 );
190
191 Ok(Self {
192 auth: Arc::new(auth),
193 http_client,
194 semaphore: Arc::new(Semaphore::new(MAX_INFLIGHT_REQUESTS)),
195 transport_info: info,
196 })
197 }
198
199 pub fn http_client(&self) -> &ReqwestClient {
201 &self.http_client
202 }
203
204 pub fn transport_info(&self) -> &TransportInfo {
206 &self.transport_info
207 }
208
209 pub async fn execute(&self, mut request: reqwest::RequestBuilder) -> Result<reqwest::Response> {
224 let _permit = self
225 .semaphore
226 .acquire()
227 .await
228 .map_err(|_| GrrError::Internal("Semaphore closed".into()))?;
229
230 let token = self.auth.get_access_token().await?;
231 request = request.header(AUTHORIZATION, format!("Bearer {}", token));
232 request = apply_transport_version(request, self.transport_info.http3_effective);
233
234 let mut pending = Some(request);
235 let mut last_error = None;
236
237 for attempt in 0..=RETRY_ATTEMPTS {
238 let to_send = match pending.take() {
241 None => break,
242 Some(builder) => match builder.try_clone() {
243 Some(replayable) => {
244 pending = Some(builder);
245 replayable
246 }
247 None => builder,
248 },
249 };
250
251 match to_send.send().await {
252 Ok(resp) => {
253 let status = resp.status();
254
255 match status.as_u16() {
256 401 => {
257 return Err(GrrError::Auth(
263 "request rejected as unauthorized (401); \
264 your access token is expired or invalid — \
265 rerun `grr auth login`"
266 .into(),
267 ));
268 }
269 429 => {
270 let server_wait = parse_retry_after(
277 resp.headers()
278 .get("retry-after")
279 .and_then(|h| h.to_str().ok()),
280 SystemTime::now(),
281 );
282 let wait = server_wait.unwrap_or_else(|| retry_backoff(attempt));
283 if attempt < RETRY_ATTEMPTS && wait <= MAX_RATE_LIMIT_WAIT {
284 warn!(
285 "rate limited; honoring a wait of {wait:?} \
286 (attempt {}/{})",
287 attempt + 1,
288 RETRY_ATTEMPTS
289 );
290 tokio::time::sleep(wait).await;
291 continue;
292 }
293 return Err(GrrError::RateLimited {
296 retry_after_secs: wait.as_secs(),
297 });
298 }
299 500..=599 if attempt < RETRY_ATTEMPTS => {}
302 _ => return Ok(resp),
303 }
304 }
305 Err(e) => {
306 let retryable = e.is_timeout() || e.is_connect() || e.is_request();
310 if !retryable || attempt == RETRY_ATTEMPTS {
311 return Err(GrrError::Http(e));
312 }
313 last_error = Some(GrrError::Http(e));
314 }
315 }
316
317 let backoff = retry_backoff(attempt);
318 debug!(
319 "Request failed, retrying in {:?} (attempt {}/{})",
320 backoff,
321 attempt + 1,
322 RETRY_ATTEMPTS
323 );
324 tokio::time::sleep(backoff).await;
325 }
326
327 Err(last_error.unwrap_or_else(|| GrrError::Internal("Max retries exceeded".into())))
328 }
329}
330
331pub fn apply_transport_version(
338 builder: reqwest::RequestBuilder,
339 http3_effective: bool,
340) -> reqwest::RequestBuilder {
341 if http3_effective {
342 builder.version(reqwest::Version::HTTP_3)
343 } else {
344 builder
345 }
346}
347
348async fn probe(
354 client: &ReqwestClient,
355 base: &Url,
356 auth: &GoogleAuth,
357 version: reqwest::Version,
358 probe_path: &str,
359) -> std::result::Result<String, GrrError> {
360 let token = auth.get_access_token().await?;
361 let url = base.join(probe_path)?;
362 let resp = client
363 .get(url)
364 .bearer_auth(&token)
365 .version(version)
366 .send()
367 .await?;
368 Ok(format!("{:?}", resp.version()))
369}
370
371pub fn build_http_client() -> Result<ReqwestClient> {
376 build_http_client_with_mode(true)
377}
378
379fn build_http_client_with_mode(use_http3: bool) -> Result<ReqwestClient> {
380 let mut builder = ReqwestClient::builder()
381 .timeout(REQUEST_TIMEOUT)
382 .connect_timeout(CONNECT_TIMEOUT)
383 .pool_max_idle_per_host(POOL_IDLE_PER_HOST)
384 .pool_idle_timeout(POOL_IDLE_TIMEOUT)
385 .tcp_keepalive(TCP_KEEPALIVE)
386 .http2_keep_alive_interval(H2_KEEPALIVE_INTERVAL)
387 .http2_adaptive_window(true)
388 .brotli(true)
389 .zstd(true)
390 .gzip(true);
391
392 builder = if use_http3 {
393 builder.http3_prior_knowledge()
394 } else {
395 builder.http2_prior_knowledge()
396 };
397
398 let mut headers = HeaderMap::new();
399 headers.insert(
400 ACCEPT_ENCODING,
401 HeaderValue::from_static("zstd, br, gzip, deflate"),
402 );
403 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
404
405 builder
406 .default_headers(headers)
407 .build()
408 .map_err(|error| GrrError::Config(format!("Failed to build HTTP client: {error}")))
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414
415 #[test]
416 fn retry_after_delta_seconds_is_read_directly() {
417 let now = UNIX_EPOCH;
418 assert_eq!(
419 parse_retry_after(Some("5"), now),
420 Some(Duration::from_secs(5))
421 );
422 assert_eq!(parse_retry_after(None, now), None);
423 assert_eq!(parse_retry_after(Some("soon"), now), None);
424 }
425
426 #[test]
427 fn retry_after_http_date_is_read_as_an_instant() {
428 let header = Some("Sun, 06 Nov 1994 08:49:37 GMT");
430 assert_eq!(
431 parse_retry_after(header, UNIX_EPOCH),
432 Some(Duration::from_secs(784_111_777))
433 );
434 let now = UNIX_EPOCH + Duration::from_secs(784_111_775);
436 assert_eq!(parse_retry_after(header, now), Some(Duration::from_secs(2)));
437 }
438
439 #[test]
440 fn an_expired_retry_after_date_means_retry_now() {
441 let now = UNIX_EPOCH + Duration::from_secs(1_700_000_000);
442 assert_eq!(
443 parse_retry_after(Some("Thu, 01 Jan 1970 00:00:00 GMT"), now),
444 Some(Duration::ZERO)
445 );
446 }
447
448 #[test]
449 fn retry_after_dates_handle_a_leap_day() {
450 assert_eq!(
451 parse_retry_after(Some("Tue, 29 Feb 2000 00:00:00 GMT"), UNIX_EPOCH),
452 Some(Duration::from_secs(951_782_400))
453 );
454 }
455
456 #[test]
457 fn a_malformed_retry_after_date_is_unusable_rather_than_wrong() {
458 let now = UNIX_EPOCH;
459 assert_eq!(
460 parse_retry_after(Some("Sun, 06 Nov 1994 08:49:37 UTC"), now),
461 None
462 );
463 assert_eq!(
464 parse_retry_after(Some("Sun, 06 Foo 1994 08:49:37 GMT"), now),
465 None
466 );
467 assert_eq!(parse_retry_after(Some("Sun, 06 Nov 1994"), now), None);
468 }
469
470 #[test]
471 fn the_headerless_rate_limit_fallback_is_always_retryable() {
472 for attempt in 0..=RETRY_ATTEMPTS {
475 assert!(retry_backoff(attempt) <= MAX_RATE_LIMIT_WAIT);
476 }
477 assert_eq!(retry_backoff(0), Duration::from_millis(100));
478 assert_eq!(retry_backoff(3), Duration::from_millis(800));
479 }
480}