dropbox_sdk/
default_client.rs

1// Copyright (c) 2020-2025 Dropbox, Inc.
2
3//! The default HTTP client.
4//!
5//! Use this client if you're not particularly picky about implementation details, as the specific
6//! implementation is not exposed, and may be changed in the future.
7//!
8//! If you have a need for a specific HTTP client implementation, or your program is already using
9//! some HTTP client crate, you probably want to have this Dropbox SDK crate use it as well. To do
10//! that, you should implement the traits in `crate::client_trait` for it and use it instead.
11//!
12//! This code (and its dependencies) are only built if you use the `default_client` Cargo feature.
13
14use crate::Error;
15use crate::oauth2::{Authorization, TokenCache};
16use std::borrow::Cow;
17use std::fmt::Write;
18use std::str::FromStr;
19use std::sync::Arc;
20use futures::FutureExt;
21use crate::client_trait::{AppAuthClient, HttpClient, HttpRequest, HttpRequestResultRaw,
22    NoauthClient, TeamAuthClient, TeamSelect, UserAuthClient};
23use crate::default_client_common::impl_set_path_root;
24
25macro_rules! impl_update_token {
26    ($self:ident) => {
27        fn update_token(&$self, old_token: Arc<String>) -> Result<bool, Error> {
28            info!("refreshing auth token");
29            match $self.tokens.update_token(
30                TokenUpdateClient { inner: &$self.inner },
31                old_token,
32            ).now_or_never().unwrap() {
33                Ok(_) => Ok(true),
34                Err(e) => {
35                    error!("failed to update auth token: {e}");
36                    Err(e.into())
37                }
38            }
39        }
40    };
41}
42
43/// Default HTTP client using User authorization.
44pub struct UserAuthDefaultClient {
45    inner: UreqClient,
46    tokens: Arc<TokenCache>,
47    path_root: Option<String>, // a serialized PathRoot enum
48}
49
50impl UserAuthDefaultClient {
51    /// Create a new client using the given OAuth2 authorization.
52    pub fn new(auth: Authorization) -> Self {
53        Self::from_token_cache(Arc::new(TokenCache::new(auth)))
54    }
55
56    /// Create a new client from a [`TokenCache`], which lets you share the same tokens between
57    /// multiple clients.
58    pub fn from_token_cache(tokens: Arc<TokenCache>) -> Self {
59        Self {
60            inner: UreqClient::default(),
61            tokens,
62            path_root: None,
63        }
64    }
65
66    impl_set_path_root!(self);
67}
68
69impl HttpClient for UserAuthDefaultClient {
70    type Request = UreqRequest;
71
72    fn execute(&self, request: Self::Request, body: &[u8]) -> Result<HttpRequestResultRaw, Error> {
73        self.inner.execute(request, body)
74    }
75
76    fn new_request(&self, url: &str) -> Self::Request {
77        self.inner.new_request(url)
78    }
79
80    impl_update_token!(self);
81
82    fn token(&self) -> Option<Arc<String>> {
83        self.tokens.get_token()
84    }
85
86    fn path_root(&self) -> Option<&str> {
87        self.path_root.as_deref()
88    }
89}
90
91impl UserAuthClient for UserAuthDefaultClient {}
92
93/// Default HTTP client using Team authorization.
94pub struct TeamAuthDefaultClient {
95    inner: UreqClient,
96    tokens: Arc<TokenCache>,
97    path_root: Option<String>, // a serialized PathRoot enum
98    team_select: Option<TeamSelect>,
99}
100
101impl TeamAuthDefaultClient {
102    /// Create a new client using the given OAuth2 token, with no user/admin context selected.
103    pub fn new(tokens: impl Into<Arc<TokenCache>>) -> Self {
104        Self {
105            inner: UreqClient::default(),
106            tokens: tokens.into(),
107            path_root: None,
108            team_select: None,
109        }
110    }
111
112    /// Select a user or team context to operate in.
113    pub fn select(&mut self, team_select: Option<TeamSelect>) {
114        self.team_select = team_select;
115    }
116
117    impl_set_path_root!(self);
118}
119
120impl HttpClient for TeamAuthDefaultClient {
121    type Request = UreqRequest;
122
123    fn execute(&self, request: Self::Request, body: &[u8]) -> Result<HttpRequestResultRaw, Error> {
124        self.inner.execute(request, body)
125    }
126
127    fn new_request(&self, url: &str) -> Self::Request {
128        self.inner.new_request(url)
129    }
130
131    fn token(&self) -> Option<Arc<String>> {
132        self.tokens.get_token()
133    }
134
135    impl_update_token!(self);
136
137    fn path_root(&self) -> Option<&str> {
138        self.path_root.as_deref()
139    }
140
141    fn team_select(&self) -> Option<&TeamSelect> {
142        self.team_select.as_ref()
143    }
144}
145
146impl TeamAuthClient for TeamAuthDefaultClient {}
147
148/// Default HTTP client using App authorization.
149#[derive(Debug)]
150pub struct AppAuthDefaultClient {
151    inner: UreqClient,
152    path_root: Option<String>,
153    auth: String,
154}
155
156impl AppAuthDefaultClient {
157    /// Create a new App auth client using the given app key and secret, which can be found in the Dropbox app console.
158    pub fn new(app_key: &str, app_secret: &str) -> Self {
159        use base64::prelude::*;
160        let encoded = BASE64_STANDARD.encode(format!("{app_key}:{app_secret}"));
161        Self {
162            inner: UreqClient::default(),
163            path_root: None,
164            auth: format!("Basic {encoded}"),
165        }
166    }
167
168    impl_set_path_root!(self);
169}
170
171impl HttpClient for AppAuthDefaultClient {
172    type Request = UreqRequest;
173
174    fn execute(&self, request: Self::Request, body: &[u8]) -> Result<HttpRequestResultRaw, Error> {
175        self.inner.execute(request, body)
176    }
177
178    fn new_request(&self, url: &str) -> Self::Request {
179        self.inner.new_request(url)
180            .set_header("Authorization", &self.auth)
181    }
182}
183
184impl AppAuthClient for AppAuthDefaultClient {}
185
186/// Default HTTP client for unauthenticated API calls.
187#[derive(Debug, Default)]
188pub struct NoauthDefaultClient {
189    inner: UreqClient,
190    path_root: Option<String>,
191}
192
193impl NoauthDefaultClient {
194    impl_set_path_root!(self);
195}
196
197impl HttpClient for NoauthDefaultClient {
198    type Request = UreqRequest;
199
200    fn execute(&self, request: Self::Request, body: &[u8]) -> Result<HttpRequestResultRaw, Error> {
201        self.inner.execute(request, body)
202    }
203
204    fn new_request(&self, url: &str) -> Self::Request {
205        self.inner.new_request(url)
206    }
207
208    fn path_root(&self) -> Option<&str> {
209        self.path_root.as_deref()
210    }
211}
212
213impl NoauthClient for NoauthDefaultClient {}
214
215/// Same as NoauthDefaultClient but with inner by reference and no path_root.
216/// Only used for updating authorization tokens.
217struct TokenUpdateClient<'a> {
218    inner: &'a UreqClient,
219}
220
221impl HttpClient for TokenUpdateClient<'_> {
222    type Request = UreqRequest;
223
224    fn execute(&self, request: Self::Request, body: &[u8]) -> Result<HttpRequestResultRaw, Error> {
225        self.inner.execute(request, body)
226    }
227
228    fn new_request(&self, url: &str) -> Self::Request {
229        self.inner.new_request(url)
230    }
231}
232
233impl crate::async_client_trait::NoauthClient for TokenUpdateClient<'_> {}
234
235#[derive(Debug)]
236struct UreqClient {
237    agent: ureq::Agent,
238}
239
240impl Default for UreqClient {
241    fn default() -> Self {
242        Self {
243            agent: ureq::Agent::new(),
244        }
245    }
246}
247
248impl HttpClient for UreqClient {
249    type Request = UreqRequest;
250
251    fn execute(&self, request: Self::Request, body: &[u8]) -> Result<HttpRequestResultRaw, Error> {
252        let resp = if body.is_empty() {
253            request.req.call()
254        } else {
255            request.req.send_bytes(body)
256        };
257
258        let (status, resp) = match resp {
259            Ok(resp) => {
260                (resp.status(), resp)
261            }
262            Err(ureq::Error::Status(status, resp)) => {
263                (status, resp)
264            }
265            Err(e @ ureq::Error::Transport(_)) => {
266                return Err(RequestError { inner: e }.into());
267            }
268        };
269
270        let result_header = resp.header("Dropbox-API-Result").map(String::from);
271
272        let content_length = resp.header("Content-Length")
273            .map(|s| {
274                u64::from_str(s)
275                    .map_err(|e| Error::UnexpectedResponse(
276                        format!("invalid Content-Length {s:?}: {e}")))
277            })
278            .transpose()?;
279
280        Ok(HttpRequestResultRaw {
281            status,
282            result_header,
283            content_length,
284            body: resp.into_reader(),
285        })
286    }
287
288    fn new_request(&self, url: &str) -> Self::Request {
289        UreqRequest {
290            req: self.agent.post(url),
291        }
292    }
293}
294
295/// This is an implementation detail of the HTTP client.
296pub struct UreqRequest {
297    req: ureq::Request,
298}
299
300impl HttpRequest for UreqRequest {
301    fn set_header(mut self, name: &str, value: &str) -> Self {
302        if name.eq_ignore_ascii_case("dropbox-api-arg") {
303            // Non-ASCII and 0x7F in a header need to be escaped per the HTTP spec, and ureq doesn't
304            // do this for us. This is only an issue for this particular header.
305            self.req = self.req.set(name, json_escape_header(value).as_ref());
306        } else {
307            self.req = self.req.set(name, value);
308        }
309        self
310    }
311}
312
313/// Errors from the HTTP client encountered in the course of making a request.
314#[derive(thiserror::Error, Debug)]
315#[allow(clippy::large_enum_variant)] // it's always boxed
316pub enum DefaultClientError {
317    /// The HTTP client encountered invalid UTF-8 data.
318    #[error("invalid UTF-8 string")]
319    Utf8(#[from] std::string::FromUtf8Error),
320
321    /// The HTTP client encountered some I/O error.
322    #[error("I/O error: {0}")]
323    #[allow(clippy::upper_case_acronyms)]
324    IO(#[from] std::io::Error),
325
326    /// Some other error from the HTTP client implementation.
327    #[error(transparent)]
328    Request(#[from] RequestError),
329}
330
331macro_rules! wrap_error {
332    ($e:ty) => {
333        impl From<$e> for crate::Error {
334            fn from(e: $e) -> Self {
335                Self::HttpClient(Box::new(DefaultClientError::from(e)))
336            }
337        }
338    }
339}
340
341wrap_error!(std::io::Error);
342wrap_error!(std::string::FromUtf8Error);
343wrap_error!(RequestError);
344
345/// Something went wrong making the request, or the server returned a response we didn't expect.
346/// Use the `Display` or `Debug` impls to see more details.
347/// Note that this type is intentionally vague about the details beyond these string
348/// representations, to allow implementation changes in the future.
349pub struct RequestError {
350    inner: ureq::Error,
351}
352
353impl std::fmt::Display for RequestError {
354    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
355        <ureq::Error as std::fmt::Display>::fmt(&self.inner, f)
356    }
357}
358
359impl std::fmt::Debug for RequestError {
360    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
361        <ureq::Error as std::fmt::Debug>::fmt(&self.inner, f)
362    }
363}
364
365impl std::error::Error for RequestError {
366    fn cause(&self) -> Option<&dyn std::error::Error> {
367        Some(&self.inner)
368    }
369}
370
371/// Replaces any non-ASCII characters (and 0x7f) with JSON-style '\uXXXX' sequence. Otherwise,
372/// returns it unmodified without any additional allocation or copying.
373fn json_escape_header(s: &str) -> Cow<'_, str> {
374    // Unfortunately, the HTTP spec requires escaping ASCII DEL (0x7F), so we can't use the quicker
375    // bit pattern check done in str::is_ascii() to skip this for the common case of all ASCII. :(
376
377    let mut out = Cow::Borrowed(s);
378    for (i, c) in s.char_indices() {
379        if !c.is_ascii() || c == '\x7f' {
380            let mstr = match out {
381                Cow::Borrowed(_) => {
382                    // If we're still borrowed, we must have had ascii up until this point.
383                    // Clone the string up until here, and from now on we'll be pushing chars to it.
384                    out = Cow::Owned(s[0..i].to_owned());
385                    out.to_mut()
386                }
387                Cow::Owned(ref mut m) => m,
388            };
389            write!(mstr, "\\u{:04x}", c as u32).unwrap();
390        } else if let Cow::Owned(ref mut o) = out {
391            o.push(c);
392        }
393    }
394    out
395}
396
397#[cfg(test)]
398mod test {
399    use super::*;
400
401    #[test]
402    fn test_json_escape() {
403        assert_eq!(Cow::Borrowed("foobar"), json_escape_header("foobar"));
404        assert_eq!(
405            Cow::<'_, str>::Owned("tro\\u0161kovi".to_owned()),
406            json_escape_header("troškovi"));
407        assert_eq!(
408            Cow::<'_, str>::Owned(
409                r#"{"field": "some_\u00fc\u00f1\u00eec\u00f8d\u00e9_and_\u007f"}"#.to_owned()),
410            json_escape_header("{\"field\": \"some_üñîcødé_and_\x7f\"}"));
411        assert_eq!(
412            Cow::<'_, str>::Owned("almost,\\u007f but not quite".to_owned()),
413            json_escape_header("almost,\x7f but not quite"));
414    }
415}