1mod auth;
20mod jwt;
21
22pub use auth::{Auth, CcgConfig, OAuthConfig, RefreshTokenStore};
23pub use jwt::JwtConfig;
24
25use std::collections::HashMap;
26use std::time::Duration;
27
28const MAX_RETRY_DELAY: Duration = Duration::from_secs(300);
32
33#[derive(Debug)]
36pub struct Error(String);
37
38impl Error {
39 pub(crate) fn new(message: impl Into<String>) -> Error {
40 Error(message.into())
41 }
42}
43
44impl std::fmt::Display for Error {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 write!(f, "{}", self.0)
47 }
48}
49
50impl std::error::Error for Error {}
51
52impl From<serde_json::Error> for Error {
53 fn from(err: serde_json::Error) -> Self {
54 Error(err.to_string())
55 }
56}
57
58impl From<reqwest::Error> for Error {
59 fn from(err: reqwest::Error) -> Self {
60 Error(err.to_string())
61 }
62}
63
64pub struct Stream(Vec<u8>);
68
69impl Stream {
70 pub fn empty() -> Stream {
72 Stream(Vec::new())
73 }
74
75 pub fn from_bytes(bytes: Vec<u8>) -> Stream {
77 Stream(bytes)
78 }
79
80 pub fn into_bytes(self) -> Vec<u8> {
82 self.0
83 }
84}
85
86enum Body {
88 Bytes { content_type: String, data: Vec<u8> },
90 Multipart {
92 attributes: Vec<u8>,
93 file_name: String,
94 file: Vec<u8>,
95 },
96}
97
98pub struct Request {
101 method: String,
102 url: String,
103 headers: Vec<(String, String)>,
104 query: Vec<(String, String)>,
105 body: Option<Body>,
106}
107
108pub struct Response {
111 status: i64,
112 headers: Vec<(String, String)>,
113 body: Vec<u8>,
114}
115
116pub struct Client {
119 auth: Auth,
120 http: reqwest::Client,
121 base_urls: HashMap<String, String>,
122 max_retries: u32,
123}
124
125impl Client {
126 pub fn new(auth: Auth) -> Client {
129 let http = reqwest::Client::builder()
130 .timeout(Duration::from_secs(60))
131 .build()
132 .unwrap_or_default();
133 Client {
134 auth,
135 http,
136 max_retries: 5,
137 base_urls: default_base_urls(),
138 }
139 }
140
141 pub fn with_max_retries(mut self, n: u32) -> Client {
143 self.max_retries = n;
144 self
145 }
146
147 pub fn with_base_url(mut self, name: &str, base: &str) -> Client {
149 self.base_urls
150 .insert(name.to_string(), base.trim_end_matches('/').to_string());
151 self
152 }
153
154 pub fn base_url(&self, name: &str) -> String {
156 self.base_urls.get(name).cloned().unwrap_or_default()
157 }
158
159 pub async fn access_token(&self) -> Result<String, Error> {
161 self.auth.access_token().await
162 }
163
164 pub fn new_request(&self, method: &str, url: &str) -> Request {
166 Request {
167 method: method.to_string(),
168 url: url.to_string(),
169 headers: Vec::new(),
170 query: Vec::new(),
171 body: None,
172 }
173 }
174
175 pub async fn fetch(&self, request: Request) -> Result<Response, Error> {
183 let method = reqwest::Method::from_bytes(request.method.as_bytes()).map_err(|_| {
184 Error::new(format!(
185 "gantryruntime: invalid method {:?}",
186 request.method
187 ))
188 })?;
189 let idempotent = method.is_idempotent();
190 let mut token = self.access_token().await?;
191 let mut refreshed = false;
192
193 for attempt in 0..=self.max_retries {
194 let mut builder = self
195 .http
196 .request(method.clone(), &request.url)
197 .query(&request.query)
198 .header("Authorization", format!("Bearer {token}"));
199 for (name, value) in &request.headers {
200 builder = builder.header(name, value);
201 }
202 builder = apply_body(builder, request.body.as_ref());
203
204 let response = match builder.send().await {
205 Ok(response) => response,
206 Err(err) => {
207 if attempt == self.max_retries || !idempotent {
210 return Err(err.into());
211 }
212 sleep(backoff(attempt)).await;
213 continue;
214 }
215 };
216 let response = read_response(response).await?;
217
218 if response.status == 401 && !refreshed {
221 refreshed = true;
222 token = self.auth.force_refresh(&token).await?;
223 continue;
224 }
225 if should_retry(response.status, idempotent) && attempt < self.max_retries {
227 sleep(retry_delay(&response, attempt)).await;
228 continue;
229 }
230 return Ok(response);
231 }
232 Err(Error::new("gantryruntime: retries exhausted"))
233 }
234}
235
236fn default_base_urls() -> HashMap<String, String> {
239 [
240 ("api", "https://api.box.com/2.0"),
241 ("api_root", "https://api.box.com"),
242 ("upload", "https://upload.box.com/api/2.0"),
243 ("upload_session", "https://upload.box.com/api/2.0"),
244 ("oauth_authorize", "https://account.box.com/api/oauth2"),
245 ("download", "https://api.box.com/2.0"),
246 ]
247 .iter()
248 .map(|(k, v)| (k.to_string(), v.to_string()))
249 .collect()
250}
251
252fn apply_body(builder: reqwest::RequestBuilder, body: Option<&Body>) -> reqwest::RequestBuilder {
254 match body {
255 None => builder,
256 Some(Body::Bytes { content_type, data }) => builder
257 .header("Content-Type", content_type)
258 .body(data.clone()),
259 Some(Body::Multipart {
260 attributes,
261 file_name,
262 file,
263 }) => {
264 let boundary = multipart_boundary(attributes, file);
267 let payload = multipart_body(&boundary, attributes, file_name, file);
268 builder
269 .header(
270 "Content-Type",
271 format!("multipart/form-data; boundary={boundary}"),
272 )
273 .body(payload)
274 }
275 }
276}
277
278fn multipart_body(boundary: &str, attributes: &[u8], file_name: &str, file: &[u8]) -> Vec<u8> {
281 let mut out = Vec::new();
282 out.extend_from_slice(format!("--{boundary}\r\n").as_bytes());
283 out.extend_from_slice(b"Content-Disposition: form-data; name=\"attributes\"\r\n");
284 out.extend_from_slice(b"Content-Type: application/json\r\n\r\n");
285 out.extend_from_slice(attributes);
286 out.extend_from_slice(b"\r\n");
287 out.extend_from_slice(format!("--{boundary}\r\n").as_bytes());
288 out.extend_from_slice(
289 format!(
290 "Content-Disposition: form-data; name=\"file\"; filename=\"{}\"\r\n",
291 escape_filename(file_name)
292 )
293 .as_bytes(),
294 );
295 out.extend_from_slice(b"Content-Type: application/octet-stream\r\n\r\n");
296 out.extend_from_slice(file);
297 out.extend_from_slice(b"\r\n");
298 out.extend_from_slice(format!("--{boundary}--\r\n").as_bytes());
299 out
300}
301
302fn multipart_boundary(attributes: &[u8], file: &[u8]) -> String {
307 let mut seed = std::time::SystemTime::now()
308 .duration_since(std::time::UNIX_EPOCH)
309 .map(|d| d.as_nanos() as u64)
310 .unwrap_or(0x9e37_79b9_7f4a_7c15);
311 loop {
312 let candidate = format!("gantryruntimeXboundaryX{seed:016x}");
313 let needle = candidate.as_bytes();
314 if !contains_subslice(attributes, needle) && !contains_subslice(file, needle) {
315 return candidate;
316 }
317 seed = seed
318 .wrapping_mul(6364136223846793005)
319 .wrapping_add(1442695040888963407);
320 }
321}
322
323fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool {
325 !needle.is_empty()
326 && haystack.len() >= needle.len()
327 && haystack
328 .windows(needle.len())
329 .any(|window| window == needle)
330}
331
332fn escape_filename(name: &str) -> String {
336 let mut out = String::with_capacity(name.len());
337 for ch in name.chars() {
338 match ch {
339 '\r' | '\n' => {}
340 '\\' | '"' => {
341 out.push('\\');
342 out.push(ch);
343 }
344 _ => out.push(ch),
345 }
346 }
347 out
348}
349
350async fn read_response(response: reqwest::Response) -> Result<Response, Error> {
352 let status = response.status().as_u16() as i64;
353 let headers = response
354 .headers()
355 .iter()
356 .map(|(name, value)| {
357 (
358 name.as_str().to_string(),
359 value.to_str().unwrap_or_default().to_string(),
360 )
361 })
362 .collect();
363 let body = response.bytes().await?.to_vec();
364 Ok(Response {
365 status,
366 headers,
367 body,
368 })
369}
370
371fn should_retry(status: i64, idempotent: bool) -> bool {
375 status == 429 || (status >= 500 && idempotent)
376}
377
378fn backoff(attempt: u32) -> Duration {
380 let base = (500u64.saturating_mul(1u64 << attempt.min(20))).min(30_000);
381 Duration::from_millis(jitter(base))
382}
383
384fn retry_delay(response: &Response, attempt: u32) -> Duration {
388 let mut delay = backoff(attempt);
389 if let Some(value) = header_value(&response.headers, "retry-after") {
390 if let Ok(secs) = value.trim().parse::<u64>() {
391 delay = delay.max(Duration::from_secs(secs));
392 }
393 }
394 delay.min(MAX_RETRY_DELAY)
395}
396
397fn jitter(max: u64) -> u64 {
401 if max == 0 {
402 return 0;
403 }
404 let mut x = std::time::SystemTime::now()
405 .duration_since(std::time::UNIX_EPOCH)
406 .map(|d| d.as_nanos() as u64)
407 .unwrap_or(0x9e37_79b9_7f4a_7c15)
408 | 1;
409 x ^= x << 13;
410 x ^= x >> 7;
411 x ^= x << 17;
412 x % (max + 1)
413}
414
415async fn sleep(duration: Duration) {
416 tokio::time::sleep(duration).await;
417}
418
419fn header_value<'a>(headers: &'a [(String, String)], name: &str) -> Option<&'a str> {
421 headers
422 .iter()
423 .find(|(key, _)| key.eq_ignore_ascii_case(name))
424 .map(|(_, value)| value.as_str())
425}
426
427pub fn with_header(mut request: Request, name: &str, value: &str) -> Request {
429 request
430 .headers
431 .retain(|(key, _)| !key.eq_ignore_ascii_case(name));
432 request.headers.push((name.to_string(), value.to_string()));
433 request
434}
435
436pub fn with_query(mut request: Request, name: &str, value: &str) -> Request {
438 request.query.push((name.to_string(), value.to_string()));
439 request
440}
441
442pub fn with_json_body(mut request: Request, body: &[u8]) -> Request {
444 request.body = Some(Body::Bytes {
445 content_type: "application/json".to_string(),
446 data: body.to_vec(),
447 });
448 request
449}
450
451pub fn with_form_body(mut request: Request, form: &[u8]) -> Request {
454 request.body = Some(Body::Bytes {
455 content_type: "application/x-www-form-urlencoded".to_string(),
456 data: form.to_vec(),
457 });
458 request
459}
460
461pub fn with_stream_body(mut request: Request, body: Stream, content_type: &str) -> Request {
463 request.body = Some(Body::Bytes {
464 content_type: content_type.to_string(),
465 data: body.into_bytes(),
466 });
467 request
468}
469
470pub fn with_multipart_body(
473 mut request: Request,
474 attributes: &[u8],
475 file_name: &str,
476 file: Stream,
477) -> Request {
478 request.body = Some(Body::Multipart {
479 attributes: attributes.to_vec(),
480 file_name: file_name.to_string(),
481 file: file.into_bytes(),
482 });
483 request
484}
485
486pub fn response_bytes(response: &Response) -> Result<Vec<u8>, Error> {
488 Ok(response.body.clone())
489}
490
491pub fn response_stream(response: &Response) -> Stream {
493 Stream::from_bytes(response.body.clone())
494}
495
496pub fn response_header(response: &Response, name: &str) -> String {
499 header_value(&response.headers, name)
500 .unwrap_or_default()
501 .to_string()
502}
503
504pub fn status_code(response: &Response) -> i64 {
506 response.status
507}