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, PartialEq, Eq, Clone, Copy)]
110pub enum TransportMode {
111 Http3PriorKnowledge,
112 Http2PriorKnowledge,
113}
114
115pub fn resolve_transport_mode(use_http3: bool) -> TransportMode {
120 if use_http3 {
121 TransportMode::Http3PriorKnowledge
122 } else {
123 TransportMode::Http2PriorKnowledge
124 }
125}
126
127#[derive(Debug, Clone)]
128pub struct TransportInfo {
129 pub negotiated_version: String,
130 pub http3_requested: bool,
131 pub http3_effective: bool,
132 pub fell_back: bool,
133}
134
135impl Default for TransportInfo {
136 fn default() -> Self {
137 Self {
138 negotiated_version: "not-probed".into(),
139 http3_requested: true,
140 http3_effective: false,
141 fell_back: false,
142 }
143 }
144}
145
146#[derive(Clone)]
150pub struct HttpCore {
151 auth: Arc<GoogleAuth>,
152 http_client: ReqwestClient,
153 semaphore: Arc<Semaphore>,
154 transport_info: TransportInfo,
155}
156
157impl HttpCore {
158 pub async fn connect(auth: GoogleAuth, base_url: &Url, probe_path: &str) -> Result<Self> {
163 let mut info = TransportInfo {
164 http3_requested: true,
165 http3_effective: true,
166 ..TransportInfo::default()
167 };
168 let mut http_client = build_http_client()?;
169
170 match probe(
171 &http_client,
172 base_url,
173 &auth,
174 reqwest::Version::HTTP_3,
175 probe_path,
176 )
177 .await
178 {
179 Ok(version) => info.negotiated_version = version,
180 Err(GrrError::Auth(error)) => return Err(GrrError::Auth(error)),
181 Err(error) => {
182 warn!("HTTP/3 probe failed ({error}); rebuilding with HTTP/2");
183 info.http3_effective = false;
184 info.fell_back = true;
185 http_client = build_http_client_with_mode(resolve_transport_mode(false))?;
186 match probe(
187 &http_client,
188 base_url,
189 &auth,
190 reqwest::Version::HTTP_2,
191 probe_path,
192 )
193 .await
194 {
195 Ok(version) => info.negotiated_version = version,
196 Err(error) => {
197 warn!("HTTP/2 probe failed ({error}); continuing unprobed");
198 info.negotiated_version = "not-probed".into();
199 }
200 }
201 }
202 }
203
204 info!(
205 "HttpCore connected (probe {}: {}): http3_effective={}, max_inflight_requests={}",
206 probe_path, info.negotiated_version, info.http3_effective, MAX_INFLIGHT_REQUESTS
207 );
208
209 Ok(Self {
210 auth: Arc::new(auth),
211 http_client,
212 semaphore: Arc::new(Semaphore::new(MAX_INFLIGHT_REQUESTS)),
213 transport_info: info,
214 })
215 }
216
217 pub fn get(&self, url: Url) -> reqwest::RequestBuilder {
219 self.http_client.get(url)
220 }
221
222 pub fn post(&self, url: Url) -> reqwest::RequestBuilder {
224 self.http_client.post(url)
225 }
226
227 pub fn put(&self, url: Url) -> reqwest::RequestBuilder {
229 self.http_client.put(url)
230 }
231
232 pub fn patch(&self, url: Url) -> reqwest::RequestBuilder {
234 self.http_client.patch(url)
235 }
236
237 pub fn delete(&self, url: Url) -> reqwest::RequestBuilder {
239 self.http_client.delete(url)
240 }
241
242 pub fn http_client(&self) -> &ReqwestClient {
244 &self.http_client
245 }
246
247 pub fn auth(&self) -> &GoogleAuth {
249 &self.auth
250 }
251
252 pub fn transport_info(&self) -> &TransportInfo {
254 &self.transport_info
255 }
256
257 pub async fn execute(&self, mut request: reqwest::RequestBuilder) -> Result<reqwest::Response> {
272 let _permit = self
273 .semaphore
274 .acquire()
275 .await
276 .map_err(|_| GrrError::Internal("Semaphore closed".into()))?;
277
278 let token = self.auth.get_access_token().await?;
279 request = request.header(AUTHORIZATION, format!("Bearer {}", token));
280 request = apply_transport_version(request, self.transport_info.http3_effective);
281
282 let mut pending = Some(request);
283 let mut last_error = None;
284
285 for attempt in 0..=RETRY_ATTEMPTS {
286 let to_send = match pending.take() {
289 None => break,
290 Some(builder) => match builder.try_clone() {
291 Some(replayable) => {
292 pending = Some(builder);
293 replayable
294 }
295 None => builder,
296 },
297 };
298
299 match to_send.send().await {
300 Ok(resp) => {
301 let status = resp.status();
302
303 match status.as_u16() {
304 401 => {
305 return Err(GrrError::Auth(
311 "request rejected as unauthorized (401); \
312 your access token is expired or invalid — \
313 rerun `grr auth login`"
314 .into(),
315 ));
316 }
317 429 => {
318 let server_wait = parse_retry_after(
325 resp.headers()
326 .get("retry-after")
327 .and_then(|h| h.to_str().ok()),
328 SystemTime::now(),
329 );
330 let wait = server_wait.unwrap_or_else(|| retry_backoff(attempt));
331 if attempt < RETRY_ATTEMPTS && wait <= MAX_RATE_LIMIT_WAIT {
332 warn!(
333 "rate limited; honoring a wait of {wait:?} \
334 (attempt {}/{})",
335 attempt + 1,
336 RETRY_ATTEMPTS
337 );
338 tokio::time::sleep(wait).await;
339 continue;
340 }
341 return Err(GrrError::RateLimited {
344 retry_after_secs: wait.as_secs(),
345 });
346 }
347 500..=599 if attempt < RETRY_ATTEMPTS => {}
350 _ => return Ok(resp),
351 }
352 }
353 Err(e) => {
354 let retryable = e.is_timeout() || e.is_connect() || e.is_request();
358 if !retryable || attempt == RETRY_ATTEMPTS {
359 return Err(GrrError::Http(e));
360 }
361 last_error = Some(GrrError::Http(e));
362 }
363 }
364
365 let backoff = retry_backoff(attempt);
366 debug!(
367 "Request failed, retrying in {:?} (attempt {}/{})",
368 backoff,
369 attempt + 1,
370 RETRY_ATTEMPTS
371 );
372 tokio::time::sleep(backoff).await;
373 }
374
375 Err(last_error.unwrap_or_else(|| GrrError::Internal("Max retries exceeded".into())))
376 }
377}
378
379pub fn apply_transport_version(
386 builder: reqwest::RequestBuilder,
387 http3_effective: bool,
388) -> reqwest::RequestBuilder {
389 if http3_effective {
390 builder.version(reqwest::Version::HTTP_3)
391 } else {
392 builder
393 }
394}
395
396async fn probe(
402 client: &ReqwestClient,
403 base: &Url,
404 auth: &GoogleAuth,
405 version: reqwest::Version,
406 probe_path: &str,
407) -> std::result::Result<String, GrrError> {
408 let token = auth.get_access_token().await?;
409 let url = base.join(probe_path)?;
410 let resp = client
411 .get(url)
412 .bearer_auth(&token)
413 .version(version)
414 .send()
415 .await?;
416 Ok(format!("{:?}", resp.version()))
417}
418
419pub fn build_http_client() -> Result<ReqwestClient> {
424 build_http_client_with_mode(resolve_transport_mode(true))
425}
426
427fn build_http_client_with_mode(mode: TransportMode) -> Result<ReqwestClient> {
428 let mut builder = ReqwestClient::builder()
429 .timeout(REQUEST_TIMEOUT)
430 .connect_timeout(CONNECT_TIMEOUT)
431 .pool_max_idle_per_host(POOL_IDLE_PER_HOST)
432 .pool_idle_timeout(POOL_IDLE_TIMEOUT)
433 .tcp_keepalive(TCP_KEEPALIVE)
434 .http2_keep_alive_interval(H2_KEEPALIVE_INTERVAL)
435 .http2_adaptive_window(true)
436 .brotli(true)
437 .zstd(true)
438 .gzip(true);
439
440 builder = match mode {
441 TransportMode::Http3PriorKnowledge => builder.http3_prior_knowledge(),
442 TransportMode::Http2PriorKnowledge => builder.http2_prior_knowledge(),
443 };
444
445 let mut headers = HeaderMap::new();
446 headers.insert(
447 ACCEPT_ENCODING,
448 HeaderValue::from_static("zstd, br, gzip, deflate"),
449 );
450 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
451
452 builder
453 .default_headers(headers)
454 .build()
455 .map_err(|error| GrrError::Config(format!("Failed to build HTTP client: {error}")))
456}
457
458#[cfg(test)]
459mod tests {
460 use super::*;
461
462 #[test]
463 fn retry_after_delta_seconds_is_read_directly() {
464 let now = UNIX_EPOCH;
465 assert_eq!(
466 parse_retry_after(Some("5"), now),
467 Some(Duration::from_secs(5))
468 );
469 assert_eq!(parse_retry_after(None, now), None);
470 assert_eq!(parse_retry_after(Some("soon"), now), None);
471 }
472
473 #[test]
474 fn retry_after_http_date_is_read_as_an_instant() {
475 let header = Some("Sun, 06 Nov 1994 08:49:37 GMT");
477 assert_eq!(
478 parse_retry_after(header, UNIX_EPOCH),
479 Some(Duration::from_secs(784_111_777))
480 );
481 let now = UNIX_EPOCH + Duration::from_secs(784_111_775);
483 assert_eq!(parse_retry_after(header, now), Some(Duration::from_secs(2)));
484 }
485
486 #[test]
487 fn an_expired_retry_after_date_means_retry_now() {
488 let now = UNIX_EPOCH + Duration::from_secs(1_700_000_000);
489 assert_eq!(
490 parse_retry_after(Some("Thu, 01 Jan 1970 00:00:00 GMT"), now),
491 Some(Duration::ZERO)
492 );
493 }
494
495 #[test]
496 fn retry_after_dates_handle_a_leap_day() {
497 assert_eq!(
498 parse_retry_after(Some("Tue, 29 Feb 2000 00:00:00 GMT"), UNIX_EPOCH),
499 Some(Duration::from_secs(951_782_400))
500 );
501 }
502
503 #[test]
504 fn a_malformed_retry_after_date_is_unusable_rather_than_wrong() {
505 let now = UNIX_EPOCH;
506 assert_eq!(
507 parse_retry_after(Some("Sun, 06 Nov 1994 08:49:37 UTC"), now),
508 None
509 );
510 assert_eq!(
511 parse_retry_after(Some("Sun, 06 Foo 1994 08:49:37 GMT"), now),
512 None
513 );
514 assert_eq!(parse_retry_after(Some("Sun, 06 Nov 1994"), now), None);
515 }
516
517 #[test]
518 fn the_headerless_rate_limit_fallback_is_always_retryable() {
519 for attempt in 0..=RETRY_ATTEMPTS {
522 assert!(retry_backoff(attempt) <= MAX_RATE_LIMIT_WAIT);
523 }
524 assert_eq!(retry_backoff(0), Duration::from_millis(100));
525 assert_eq!(retry_backoff(3), Duration::from_millis(800));
526 }
527}