use crate::Error;
use crate::auth::AuthError;
use crate::client_trait::*;
use crate::oauth2::{Authorization, TokenCache};
use std::borrow::Cow;
use std::fmt::Write;
use std::sync::Arc;
const USER_AGENT: &str = concat!("Dropbox-APIv2-Rust/", env!("CARGO_PKG_VERSION"));
macro_rules! forward_noauth_request {
($self:ident, $inner:expr, $path_root:expr) => {
fn request(
&$self,
endpoint: Endpoint,
style: Style,
function: &str,
params: String,
params_type: ParamsType,
body: Option<&[u8]>,
range_start: Option<u64>,
range_end: Option<u64>,
) -> crate::Result<HttpRequestResultRaw> {
$inner.request(endpoint, style, function, ¶ms, params_type, body, range_start,
range_end, None, $path_root, None)
}
}
}
macro_rules! forward_authed_request {
($self:ident, $tokens:expr, $inner:expr, $path_root:expr, $team_select:expr) => {
fn request(
&$self,
endpoint: Endpoint,
style: Style,
function: &str,
params: String,
params_type: ParamsType,
body: Option<&[u8]>,
range_start: Option<u64>,
range_end: Option<u64>,
) -> crate::Result<HttpRequestResultRaw> {
let mut token = $tokens.get_token(TokenUpdateClient { inner: &$inner })?;
let mut retried = false;
loop {
let result = $inner.request(endpoint, style, function, ¶ms, params_type, body,
range_start, range_end, Some(&token), $path_root, $team_select);
if retried {
break result;
}
if let Err(crate::Error::Authentication(AuthError::ExpiredAccessToken)) = &result {
info!("refreshing auth token");
let old_token = token;
token = $tokens.update_token(
TokenUpdateClient { inner: &$inner },
old_token,
)?;
retried = true;
continue;
}
break result;
}
}
}
}
macro_rules! impl_set_path_root {
($self:ident) => {
#[cfg(feature = "dbx_common")]
pub fn set_path_root(&mut $self, path_root: &crate::common::PathRoot) {
$self.path_root = Some(serde_json::to_string(path_root).expect("invalid path root"));
}
}
}
pub struct UserAuthDefaultClient {
inner: UreqClient,
tokens: Arc<TokenCache>,
path_root: Option<String>, }
impl UserAuthDefaultClient {
pub fn new(auth: Authorization) -> Self {
Self::from_token_cache(Arc::new(TokenCache::new(auth)))
}
pub fn from_token_cache(tokens: Arc<TokenCache>) -> Self {
Self {
inner: UreqClient::default(),
tokens,
path_root: None,
}
}
impl_set_path_root!(self);
}
impl HttpClient for UserAuthDefaultClient {
forward_authed_request! { self, self.tokens, self.inner, self.path_root.as_deref(), None }
}
impl UserAuthClient for UserAuthDefaultClient {}
pub struct TeamAuthDefaultClient {
inner: UreqClient,
tokens: Arc<TokenCache>,
path_root: Option<String>, team_select: Option<TeamSelect>,
}
impl TeamAuthDefaultClient {
pub fn new(tokens: impl Into<Arc<TokenCache>>) -> Self {
Self {
inner: UreqClient::default(),
tokens: tokens.into(),
path_root: None,
team_select: None,
}
}
pub fn select(&mut self, team_select: Option<TeamSelect>) {
self.team_select = team_select;
}
impl_set_path_root!(self);
}
impl HttpClient for TeamAuthDefaultClient {
forward_authed_request! { self, self.tokens, self.inner, self.path_root.as_deref(), self.team_select.as_ref() }
}
impl TeamAuthClient for TeamAuthDefaultClient {}
#[derive(Debug, Default)]
pub struct NoauthDefaultClient {
inner: UreqClient,
path_root: Option<String>,
}
impl NoauthDefaultClient {
impl_set_path_root!(self);
}
impl HttpClient for NoauthDefaultClient {
forward_noauth_request! { self, self.inner, self.path_root.as_deref() }
}
impl NoauthClient for NoauthDefaultClient {}
struct TokenUpdateClient<'a> {
inner: &'a UreqClient,
}
impl<'a> HttpClient for TokenUpdateClient<'a> {
forward_noauth_request! { self, self.inner, None }
}
impl<'a> NoauthClient for TokenUpdateClient<'a> {}
#[derive(Debug, Default)]
struct UreqClient {}
impl UreqClient {
#[allow(clippy::too_many_arguments)]
fn request(
&self,
endpoint: Endpoint,
style: Style,
function: &str,
params: &str,
params_type: ParamsType,
body: Option<&[u8]>,
range_start: Option<u64>,
range_end: Option<u64>,
token: Option<&str>,
path_root: Option<&str>,
team_select: Option<&TeamSelect>,
) -> crate::Result<HttpRequestResultRaw> {
let url = endpoint.url().to_owned() + function;
debug!("request for {:?}", url);
let mut req = ureq::post(&url)
.set("User-Agent", USER_AGENT);
if let Some(token) = token {
req = req.set("Authorization", &format!("Bearer {}", token));
}
if let Some(path_root) = path_root {
req = req.set("Dropbox-API-Path-Root", path_root);
}
if let Some(team_select) = team_select {
req = match team_select {
TeamSelect::User(id) => req.set("Dropbox-API-Select-User", id),
TeamSelect::Admin(id) => req.set("Dropbox-API-Select-Admin", id),
};
}
req = match (range_start, range_end) {
(Some(start), Some(end)) => req.set("Range", &format!("bytes={}-{}", start, end)),
(Some(start), None) => req.set("Range", &format!("bytes={}-", start)),
(None, Some(end)) => req.set("Range", &format!("bytes=-{}", end)),
(None, None) => req,
};
let result = if params.is_empty() {
req.call()
} else {
match style {
Style::Rpc => {
req = req.set("Content-Type", params_type.content_type());
req.send_string(params)
}
Style::Upload | Style::Download => {
req = req.set(
"Dropbox-API-Arg",
json_escape_header(params).as_ref());
if style == Style::Upload {
req = req.set("Content-Type", "application/octet-stream");
if let Some(body) = body {
req.send_bytes(body)
} else {
req.send_bytes(&[])
}
} else {
assert!(body.is_none(), "body can only be set for Style::Upload request");
req.call()
}
}
}
};
let resp = match result {
Ok(resp) => resp,
Err(e @ ureq::Error::Transport(_)) => {
error!("request failed: {}", e);
return Err(RequestError { inner: e }.into());
}
Err(ureq::Error::Status(code, resp)) => {
let status = resp.status_text().to_owned();
let json = resp.into_string()?;
return Err(Error::UnexpectedHttpError {
code,
status,
json,
});
}
};
match style {
Style::Rpc | Style::Upload => {
let result_json = resp.into_string()?;
Ok(HttpRequestResultRaw {
result_json,
content_length: None,
body: None,
})
}
Style::Download => {
let result_json = resp.header("Dropbox-API-Result")
.ok_or(Error::UnexpectedResponse("missing Dropbox-API-Result header"))?
.to_owned();
let content_length = match resp.header("Content-Length") {
Some(s) => Some(s.parse()
.map_err(|_| Error::UnexpectedResponse("invalid Content-Length header"))?),
None => None,
};
Ok(HttpRequestResultRaw {
result_json,
content_length,
body: Some(Box::new(resp.into_reader())),
})
}
}
}
}
#[derive(thiserror::Error, Debug)]
#[allow(clippy::large_enum_variant)] pub enum DefaultClientError {
#[error("invalid UTF-8 string")]
Utf8(#[from] std::string::FromUtf8Error),
#[error("I/O error: {0}")]
#[allow(clippy::upper_case_acronyms)]
IO(#[from] std::io::Error),
#[error(transparent)]
Request(#[from] RequestError),
}
macro_rules! wrap_error {
($e:ty) => {
impl From<$e> for crate::Error {
fn from(e: $e) -> Self {
Self::HttpClient(Box::new(DefaultClientError::from(e)))
}
}
}
}
wrap_error!(std::io::Error);
wrap_error!(std::string::FromUtf8Error);
wrap_error!(RequestError);
pub struct RequestError {
inner: ureq::Error,
}
impl std::fmt::Display for RequestError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
<ureq::Error as std::fmt::Display>::fmt(&self.inner, f)
}
}
impl std::fmt::Debug for RequestError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
<ureq::Error as std::fmt::Debug>::fmt(&self.inner, f)
}
}
impl std::error::Error for RequestError {
fn cause(&self) -> Option<&dyn std::error::Error> {
Some(&self.inner)
}
}
fn json_escape_header(s: &str) -> Cow<'_, str> {
let mut out = Cow::Borrowed(s);
for (i, c) in s.char_indices() {
if !c.is_ascii() || c == '\x7f' {
let mstr = match out {
Cow::Borrowed(_) => {
out = Cow::Owned(s[0..i].to_owned());
out.to_mut()
}
Cow::Owned(ref mut m) => m,
};
write!(mstr, "\\u{:04x}", c as u32).unwrap();
} else if let Cow::Owned(ref mut o) = out {
o.push(c);
}
}
out
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_json_escape() {
assert_eq!(Cow::Borrowed("foobar"), json_escape_header("foobar"));
assert_eq!(
Cow::<'_, str>::Owned("tro\\u0161kovi".to_owned()),
json_escape_header("troškovi"));
assert_eq!(
Cow::<'_, str>::Owned(
r#"{"field": "some_\u00fc\u00f1\u00eec\u00f8d\u00e9_and_\u007f"}"#.to_owned()),
json_escape_header("{\"field\": \"some_üñîcødé_and_\x7f\"}"));
assert_eq!(
Cow::<'_, str>::Owned("almost,\\u007f but not quite".to_owned()),
json_escape_header("almost,\x7f but not quite"));
}
}