use hmac::{Hmac, Mac, NewMac};
use reqwest::{
blocking,
header::{HeaderMap, HeaderValue, USER_AGENT},
};
use serde::de::DeserializeOwned;
use sha2::{Digest, Sha256, Sha512};
use std::{
convert::TryInto,
fmt,
time::{SystemTime, UNIX_EPOCH},
};
use crate::{api::Body, Api, Credentials, Error, Response, Result};
#[derive(Clone)]
pub struct Client {
client: blocking::Client,
credentials: Option<Credentials>,
user_agent: HeaderValue,
}
impl fmt::Display for Client {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} <{}>",
self.user_agent.to_str().unwrap_or("User-Agent N/A"),
user_agent()
)
}
}
impl Client {
pub fn new(user_agent: impl fmt::Display) -> Result<Self> {
ClientBuilder::with_user_agent(user_agent).build()
}
pub fn with_credentials(
user_agent: impl fmt::Display,
credentials: impl Into<Credentials>,
) -> Result<Self> {
ClientBuilder::with_user_agent(user_agent)
.with_credentials(credentials)
.build()
}
pub fn send<Req: Into<Api>, Resp: DeserializeOwned>(
&self,
api: Req,
) -> Result<Response<Resp>> {
let mut api = api.into();
log::trace!("Sending request {}", api);
let user_agent = self.user_agent.to_owned();
api.inner.headers.append(USER_AGENT, user_agent);
let resp = if api.is_public() {
self.get(api)?
} else {
self.post(api)?
};
let status = resp.status();
let mut resp: Response<Resp> = resp.json()?;
resp.status_code = status.as_u16();
Ok(resp)
}
fn get(&self, api: Api) -> Result<blocking::Response> {
let resp = self
.client
.get(&api.url())
.headers(api.inner.headers)
.send()?;
Ok(resp)
}
fn post(&self, api: Api) -> Result<blocking::Response> {
let nonce = self.nonce()?;
let url = api.url();
let uri_path = api.inner.uri_path();
debug_assert!(!api.is_public());
let body = Body::with_params(nonce, api.inner.params);
let body = body.urlencode();
let mut headers: HeaderMap = api.inner.headers;
if let Some(credentials) = &self.credentials {
let api_sign = self.api_sign(uri_path, nonce, &body)?;
headers.insert("API-Key", credentials.api_key().to_owned());
headers.insert("API-Sign", api_sign);
}
let resp = self.client.post(&url).headers(headers).body(body).send()?;
Ok(resp)
}
fn nonce(&self) -> Result<u64> {
let elapsed = SystemTime::now().duration_since(UNIX_EPOCH)?;
Ok(elapsed.as_millis() as u64)
}
fn api_sign(
&self,
uri_path: String,
nonce: u64,
body: &str,
) -> Result<HeaderValue> {
if let Some(credentials) = &self.credentials {
type HmacSha512 = Hmac<Sha512>;
let sha_body = format!("{}{}", nonce, body);
let sha = Sha256::digest(sha_body.as_bytes());
let private_key = credentials.private_key()?;
let mut mac = HmacSha512::new_varkey(&private_key)?;
let mut hmac_data = uri_path.into_bytes();
hmac_data.append(&mut sha.to_vec());
mac.update(&hmac_data);
let b64 = base64::encode(mac.finalize().into_bytes());
HeaderValue::from_str(&b64).map_err(Error::internal)
} else {
Err(Error::Unauthorized)
}
}
}
struct ClientBuilder {
user_agent: String,
credentials: Option<Credentials>,
}
impl ClientBuilder {
fn with_user_agent(user_agent: impl fmt::Display) -> Self {
Self {
user_agent: user_agent.to_string(),
credentials: None,
}
}
fn with_credentials(mut self, credentials: impl Into<Credentials>) -> Self {
self.credentials = Some(credentials.into());
self
}
fn build(self) -> Result<Client> {
self.try_into()
}
}
impl TryInto<Client> for ClientBuilder {
type Error = Error;
fn try_into(self) -> Result<Client> {
Ok(Client {
client: blocking::Client::default(),
credentials: self.credentials,
user_agent: self
.user_agent
.try_into()
.map_err(Error::invalid_agent)?,
})
}
}
const fn user_agent() -> &'static str {
concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"))
}
#[cfg(test)]
impl Default for Client {
fn default() -> Self {
Self {
client: blocking::Client::default(),
credentials: None,
user_agent: HeaderValue::from_static(user_agent()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::auth::tests::DummyCredentials;
use anyhow::Result;
const USER_AGENT: &str = user_agent();
#[test]
fn client_builder() -> Result<()> {
let client = Client::new(USER_AGENT)?;
assert_eq!(client.user_agent.to_str()?, USER_AGENT);
assert!(client.credentials.is_none());
Ok(())
}
#[test]
fn client_builder_with_credentials() -> Result<()> {
let dummy = DummyCredentials::new()?;
let credentials = Credentials::read(&dummy.path)?;
let client: Client = ClientBuilder::with_user_agent(USER_AGENT)
.with_credentials(credentials)
.build()?;
assert_eq!(client.user_agent.to_str()?, USER_AGENT);
assert!(client.credentials.is_some());
Ok(())
}
}