use std::{
cell::RefCell,
collections::{HashMap},
sync::Mutex,
};
use log::{debug, error};
use reqwest::{
self, Client, StatusCode,
header::{HeaderMap},
};
use serde::{
self, Serialize, Deserialize,
de::DeserializeOwned,
};
use serde_json::{
map::Map,
to_string, to_value, Value,
};
use crate::{
account::{AcmeAccount, AcmeAccountRequest},
authorization::{AcmeAuthorization, AcmeChallenge},
error::{Error, Result},
helper::b64,
key::KeyAlg,
order::{AcmeOrder, AcmeOrderRequest},
};
pub const LETSENCRYPT_DIRECTORY_URL: &str = "https://acme-v02.api.letsencrypt.org/directory";
pub const LETSENCRYPT_STAGING_DIRECTORY_URL: &str = "https://acme-staging-v02.api.letsencrypt.org/directory";
pub const LETSENCRYPT_AGREEMENT_URL: &str = "https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf";
#[derive(Debug, Serialize, Deserialize)]
pub struct AcmeDirectoryMetadata {
#[serde(rename="termsOfService")]
pub terms_os_service: Option<String>,
pub website: Option<String>,
#[serde(rename="caaIdentities")]
pub caa_identities: Option<Vec<String>>,
#[serde(rename="externalAccountRequired")]
pub external_account_required: Option<bool>,
#[serde(flatten)]
pub additional_fields: HashMap<String, Value>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AcmeDirectoryInfo {
#[serde(rename="keyChange")]
pub key_change: String,
#[serde(rename="newAccount")]
pub new_account: String,
#[serde(rename="newAuthz")]
pub new_authz: Option<String>,
#[serde(rename="newNonce")]
pub new_nonce: String,
#[serde(rename="newOrder")]
pub new_order: String,
#[serde(rename="revokeCert")]
pub revoke_cert: String,
#[serde(rename="meta")]
pub meta: Option<AcmeDirectoryMetadata>,
}
pub struct AcmeDirectory {
info: AcmeDirectoryInfo,
next_nonce: Mutex<RefCell<Option<String>>>,
key: KeyAlg,
}
#[derive(Debug, Serialize, Deserialize)]
struct Jws {
protected: String,
payload: String,
signature: String,
}
impl AcmeDirectory {
pub async fn from_url(url: &str, key: KeyAlg) -> Result<AcmeDirectory> {
let client = Client::new();
let response = client.get(url).send().await?;
let response = response.error_for_status()?;
let nonce = get_nonce_from_response(&response);
let info = response.json::<AcmeDirectoryInfo>().await?;
let next_nonce = Mutex::new(RefCell::new(nonce.ok()));
let dir = Self{info: info, next_nonce: next_nonce, key: key};
Ok(dir)
}
pub async fn login(self, request: &AcmeAccountRequest) -> Result<(AcmeBoundDirectory, AcmeAccount)> {
let url = self.info.new_account.clone();
let (headers, _, account) = self.request::<&AcmeAccountRequest, AcmeAccount>(&url, request, None).await?;
let location_header = headers.get("location");
match location_header {
None => Err(Error::invalid_acme_server_response("Location header not found")),
Some(value_unchecked) => {
match value_unchecked.to_str() {
Ok(account_key_id) => Ok((AcmeBoundDirectory::new(self, account_key_id), account)),
Err(_) => Err(Error::invalid_acme_server_response("Location header contains invalid characters"))
}
}
}
}
async fn request<T, V>(&self, url: &str, payload: T, key_id: Option<&str>) -> Result<(HeaderMap, StatusCode, V)>
where
T: Serialize,
V: DeserializeOwned,
{
let jws = self.create_jws_request_body(url, payload, key_id).await?;
let jws_string = to_string(&jws)?;
debug!(target: "acmev02", "jws: {:?}", jws_string);
let client = Client::new();
let request = client
.post(url)
.body(jws_string)
.header("content-type", "application/jose+json")
.build()?;
debug!(target: "acmev02", "request: {:?}", request);
let response = client.execute(request).await?;
match get_nonce_from_response(&response) {
Ok(nonce) => {
let rc = self.next_nonce.lock().unwrap();
(*rc).replace(Some(nonce));
}
Err(_) => {
}
}
let status = response.status();
if status.as_u16() >= 300 {
let err = response.error_for_status_ref().unwrap_err();
let text = response.text().await.unwrap_or("<unknown>".to_string());
error!(target: "acmev02", "Unexpected response from server: {} - {}", status.as_u16(), text);
return Err(Error::ReqwestError(err));
}
let headers = response.headers().clone();
let value = response.json::<V>().await?;
Ok((headers, status, value))
}
async fn request_get<V>(&self, url: &str, key_id: Option<&str>) -> Result<(HeaderMap, StatusCode, V)>
where
V: DeserializeOwned,
{
let jws = self.create_post_as_get_jws_request_body(url, key_id).await?;
let jws_string = to_string(&jws)?;
debug!(target: "acmev02", "jws: {:?}", jws_string);
let client = Client::new();
let request = client
.post(url)
.body(jws_string)
.header("content-type", "application/jose+json")
.build()?;
debug!(target: "acmev02", "request: {:?}", request);
let response = client.execute(request).await?;
match get_nonce_from_response(&response) {
Ok(nonce) => {
let rc = self.next_nonce.lock().unwrap();
(*rc).replace(Some(nonce));
}
Err(_) => {
}
}
let status = response.status();
if status.as_u16() >= 300 {
let err = response.error_for_status_ref().unwrap_err();
let text = response.text().await.unwrap_or("<unknown>".to_string());
error!(target: "acmev02", "Unexpected response from server: {} - {}", status.as_u16(), text);
return Err(Error::ReqwestError(err));
}
let headers = response.headers().clone();
let value = response.json::<V>().await?;
Ok((headers, status, value))
}
async fn create_jws_request_body<T>(&self, url: &str, payload: T, key_id: Option<&str>) -> Result<Jws>
where T: Serialize
{
let nonce = self.get_nonce().await?;
let header = self.key.get_header(url, &nonce, key_id)?;
let header64 = b64(to_string(&header)?.as_bytes());
let payload_json = to_value(&payload)?;
let payload64 = b64(to_string(&payload_json)?.as_bytes());
let signature = self.key.get_signature(&header64, &payload64)?;
Ok(Jws{protected: header64, payload: payload64, signature: signature})
}
async fn create_post_as_get_jws_request_body(&self, url: &str, key_id: Option<&str>) -> Result<Jws> {
let nonce = self.get_nonce().await?;
let header = self.key.get_header(url, &nonce, key_id)?;
let header64 = b64(to_string(&header)?.as_bytes());
let payload64 = "";
let signature = self.key.get_signature(&header64, &payload64)?;
Ok(Jws{protected: header64, payload: payload64.to_string(), signature: signature})
}
pub async fn get_nonce(&self) -> Result<String> {
let rc = self.next_nonce.lock().unwrap();
let next_nonce = (*rc).replace(None);
match next_nonce {
Some(nonce) => Ok(nonce),
None => self.get_nonce_remote().await
}
}
async fn get_nonce_remote(&self) -> Result<String> {
let client = Client::new();
let response = client.get(&self.info.new_nonce).send().await?;
let response = response.error_for_status()?;
get_nonce_from_response(&response)
}
}
pub struct AcmeBoundDirectory {
dir: AcmeDirectory,
key_id: String,
}
impl AcmeBoundDirectory {
pub(crate) fn new<S: Into<String>>(dir: AcmeDirectory, key_id: S) -> Self {
Self { dir: dir, key_id: key_id.into() }
}
pub async fn new_order(&self, request: &AcmeOrderRequest) -> Result<AcmeOrder> {
let url = self.dir.info.new_order.clone();
let (_, _, result) = self.request(&url, request).await?;
Ok(result)
}
pub async fn get_authorization(&self, authorization_url: &str) -> Result<AcmeAuthorization> {
let (_, _, result) = self.request_get(authorization_url).await?;
Ok(result)
}
pub async fn respond_challenge(&self, url: &str) -> Result<AcmeChallenge> {
let payload = Value::Object(Map::<String, Value>::with_capacity(0));
let (_, _, result) = self.request(url, payload).await?;
Ok(result)
}
async fn request<T, V>(&self, url: &str, payload: T) -> Result<(HeaderMap, StatusCode, V)>
where
T: Serialize,
V: DeserializeOwned
{
self.dir.request(url, payload, Some(&self.key_id)).await
}
async fn request_get<V>(&self, url: &str) -> Result<(HeaderMap, StatusCode, V)>
where
V: DeserializeOwned
{
self.dir.request_get(url, Some(&self.key_id)).await
}
}
fn get_nonce_from_response(response: &reqwest::Response) -> Result<String> {
let replay_nonce_header = response.headers().get("replay-nonce");
match replay_nonce_header {
None => Err(Error::invalid_acme_server_response("Replay-Nonce header not found")),
Some(value_bytes) => {
let value_str = value_bytes.to_str();
match value_str {
Ok(nonce) => Ok(nonce.to_string()),
Err(_) => Err(Error::invalid_acme_server_response("Replay-Nonce header contains invalid characeters"))
}
}
}
}