use std::{
fmt::{self, Display, Formatter},
ops::Deref,
sync::{Arc, Mutex},
time::Duration,
};
use bytes::{Bytes, BytesMut};
use reqwest::{
header::{AsHeaderName, HeaderMap, HeaderValue},
Body, Client as HttpClient, IntoUrl, Response as HttpResponse, StatusCode,
};
use serde::{Deserialize, Serialize};
use crate::{
acme::{
jws::{Message, MessageBuilder, ES256},
Directory,
},
Error,
};
const MAX_RESPONSE_BODY_SIZE: usize = 10_000_000;
#[derive(Clone)]
pub struct Client {
inner: HttpClient,
identity: Arc<ES256>,
}
impl Client {
pub async fn new() -> Result<Self, Error> {
let identity = tokio::task::spawn_blocking(ES256::new)
.await
.map_err(|_| Error::from_static_msg("terminating"))??
.into();
let mut default_headers = HeaderMap::new();
let user_agent =
HeaderValue::try_from(format!("GoodCamDeviceProxy/{}", env!("CARGO_PKG_VERSION")));
let language = HeaderValue::try_from("en");
default_headers.append("User-Agent", user_agent.unwrap());
default_headers.append("Accept-Language", language.unwrap());
let client = HttpClient::builder()
.timeout(Duration::from_secs(20))
.default_headers(default_headers)
.build()?;
let res = Self {
inner: client,
identity,
};
Ok(res)
}
pub fn identity(&self) -> &ES256 {
&self.identity
}
pub async fn open_directory<U>(&self, url: U) -> Result<Directory, Error>
where
U: IntoUrl,
{
let response = self.get(url).send().await?;
let response = Response::new(response).await?;
response.error_for_status()?;
let directory = response.parse_json::<DirectoryResponse>()?;
let client = DirectoryClient {
client: self.clone(),
new_nonce_url: directory.new_nonce,
next_nonce: Arc::new(Mutex::new(None)),
};
let res = Directory {
client,
new_account_url: directory.new_account,
new_order_url: directory.new_order,
};
Ok(res)
}
}
impl Deref for Client {
type Target = HttpClient;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
#[derive(Clone)]
pub struct DirectoryClient {
client: Client,
new_nonce_url: String,
next_nonce: Arc<Mutex<Option<String>>>,
}
impl DirectoryClient {
pub fn to_account_client<T>(&self, kid: T) -> AccountClient
where
T: ToString,
{
AccountClient {
client: self.clone(),
kid: kid.to_string(),
}
}
pub async fn post<P>(&self, url: &str, payload: &P) -> Result<Response, Error>
where
P: Serialize,
{
self.post_jws_message(url, |identity, nonce, url| {
MessageBuilder::new(identity)
.with_payload(payload)
.build_with_jwk_header(url, nonce)
})
.await
}
async fn post_jws_message<F>(&self, url: &str, mut f: F) -> Result<Response, Error>
where
F: FnMut(&ES256, &str, &str) -> Result<Message, Error>,
{
let mut nonce = self.acquire_nonce().await?;
loop {
let message = f(&self.identity, &nonce, url)?;
let body = serde_json::to_string(&message).map_err(|err| {
Error::from_static_msg_and_cause("unable to serialize a JWS message", err)
})?;
let response = self
.client
.post(url)
.header("Content-Type", "application/jose+json")
.body(Body::from(body))
.send()
.await?;
let response = Response::new(response).await?;
let status = response.status();
let headers = response.headers();
if status.as_u16() == 400 {
if let Ok(err) = response.parse_json::<ErrorResponse>() {
if err.kind == "urn:ietf:params:acme:error:badNonce" {
nonce = headers
.replay_nonce()?
.ok_or_else(|| Error::from_static_msg("replay-nonce not provided"))?
.to_string();
continue;
}
}
}
if let Ok(Some(nonce)) = headers.replay_nonce() {
*self.next_nonce.lock().unwrap() = Some(nonce.to_string());
}
return Ok(response);
}
}
async fn acquire_nonce(&self) -> Result<String, Error> {
let next_nonce = self.next_nonce.lock().unwrap().take();
if let Some(nonce) = next_nonce {
Ok(nonce)
} else {
self.new_nonce().await
}
}
async fn new_nonce(&self) -> Result<String, Error> {
self.client
.head(&self.new_nonce_url)
.send()
.await?
.headers()
.replay_nonce()?
.ok_or_else(|| Error::from_static_msg("missing replay-nonce header"))
.map(String::from)
}
}
impl Deref for DirectoryClient {
type Target = Client;
fn deref(&self) -> &Self::Target {
&self.client
}
}
#[derive(Clone)]
pub struct AccountClient {
client: DirectoryClient,
kid: String,
}
impl AccountClient {
pub async fn get(&self, url: &str) -> Result<Response, Error> {
self.client
.post_jws_message(url, |identity, nonce, url| {
MessageBuilder::new(identity).build_with_kid_header(&self.kid, url, nonce)
})
.await
}
pub async fn post<P>(&self, url: &str, payload: &P) -> Result<Response, Error>
where
P: Serialize,
{
self.client
.post_jws_message(url, |identity, nonce, url| {
MessageBuilder::new(identity)
.with_payload(payload)
.build_with_kid_header(&self.kid, url, nonce)
})
.await
}
}
impl Deref for AccountClient {
type Target = DirectoryClient;
fn deref(&self) -> &Self::Target {
&self.client
}
}
pub struct Response {
inner: HttpResponse,
body: Bytes,
}
impl Response {
async fn new(mut response: HttpResponse) -> Result<Self, Error> {
let mut body = BytesMut::new();
while let Some(chunk) = response.chunk().await? {
if (body.len() + chunk.len()) > MAX_RESPONSE_BODY_SIZE {
return Err(Error::from_static_msg("response body size exceeded"));
}
body.extend_from_slice(&chunk);
}
let res = Self {
inner: response,
body: body.freeze(),
};
Ok(res)
}
pub fn error_for_status(&self) -> Result<(), Error> {
let status = self.inner.status();
if status.is_success() {
Ok(())
} else {
Err(Error::from_cause(UnexpectedStatus::from(status)))
}
}
pub fn body(&self) -> &Bytes {
&self.body
}
pub fn parse_json<'a, T>(&'a self) -> Result<T, Error>
where
T: Deserialize<'a>,
{
serde_json::from_slice(&self.body)
.map_err(|err| Error::from_static_msg_and_cause("unable to deserialize JSON body", err))
}
}
impl Deref for Response {
type Target = HttpResponse;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
pub trait HeaderMapExt {
fn get_str<K>(&self, key: K) -> Result<Option<&str>, Error>
where
K: AsHeaderName;
fn replay_nonce(&self) -> Result<Option<&str>, Error>;
fn location(&self) -> Result<Option<&str>, Error>;
}
impl HeaderMapExt for HeaderMap {
fn get_str<K>(&self, key: K) -> Result<Option<&str>, Error>
where
K: AsHeaderName,
{
self.get(key)
.map(|value| value.to_str())
.transpose()
.map_err(|_| Error::from_static_msg("invalid response header"))
}
fn replay_nonce(&self) -> Result<Option<&str>, Error> {
self.get_str("replay-nonce")
}
fn location(&self) -> Result<Option<&str>, Error> {
self.get_str("location")
}
}
#[derive(Debug, Copy, Clone)]
pub struct UnexpectedStatus {
status: StatusCode,
}
impl Display for UnexpectedStatus {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"server responded with unexpected status: {}",
self.status
)
}
}
impl std::error::Error for UnexpectedStatus {}
impl From<StatusCode> for UnexpectedStatus {
fn from(status: StatusCode) -> Self {
Self { status }
}
}
impl From<UnexpectedStatus> for Error {
fn from(err: UnexpectedStatus) -> Self {
Error::from_cause(err)
}
}
#[derive(Deserialize)]
struct ErrorResponse<'a> {
#[serde(rename = "type")]
kind: &'a str,
}
#[derive(Deserialize)]
struct DirectoryResponse {
#[serde(rename = "newNonce")]
new_nonce: String,
#[serde(rename = "newAccount")]
new_account: String,
#[serde(rename = "newOrder")]
new_order: String,
}