Browse Source

Refactor the HTTP back-end

The previous HTTP back-end was tightly coupled with the threads, which
was very inconvenient. It is now completely decoupled so a new threading
model may be implemented.
pull/31/head
Rodolphe Breard 4 years ago
parent
commit
26ce6fdf40
  1. 6
      CHANGELOG.md
  2. 2
      acme_common/Cargo.toml
  3. 16
      acme_common/src/error.rs
  4. 3
      acmed/Cargo.toml
  5. 2
      acmed/build.rs
  6. 124
      acmed/src/acme_proto.rs
  7. 25
      acmed/src/acme_proto/account.rs
  8. 424
      acmed/src/acme_proto/http.rs
  9. 8
      acmed/src/config.rs
  10. 24
      acmed/src/endpoint.rs
  11. 200
      acmed/src/http.rs
  12. 1
      acmed/src/main.rs
  13. 4
      acmed/src/main_event_loop.rs

6
CHANGELOG.md

@ -13,6 +13,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Changed
- The HTTP(S) part is now handled by `reqwest` instead of `http_req`.
## [0.7.0] - 2020-03-12
### Added

2
acme_common/Cargo.toml

@ -17,10 +17,10 @@ base64 = "0.12"
daemonize = "0.4"
env_logger = "0.7"
handlebars = "3.0"
http_req = "0.5"
log = "0.4"
openssl = "0.10"
punycode = "0.4"
reqwest = { version = "0.10", features = ["blocking", "default-tls"] }
serde_json = "1.0"
syslog = "5.0"
toml = "0.5"

16
acme_common/src/error.rs

@ -87,12 +87,24 @@ impl From<openssl::error::ErrorStack> for Error {
}
}
impl From<http_req::error::Error> for Error {
fn from(error: http_req::error::Error) -> Self {
impl From<reqwest::Error> for Error {
fn from(error: reqwest::Error) -> Self {
format!("HTTP error: {}", error).into()
}
}
impl From<reqwest::header::InvalidHeaderName> for Error {
fn from(error: reqwest::header::InvalidHeaderName) -> Self {
format!("Invalid HTTP header name: {}", error).into()
}
}
impl From<reqwest::header::InvalidHeaderValue> for Error {
fn from(error: reqwest::header::InvalidHeaderValue) -> Self {
format!("Invalid HTTP header value: {}", error).into()
}
}
#[cfg(unix)]
impl From<nix::Error> for Error {
fn from(error: nix::Error) -> Self {

3
acmed/Cargo.toml

@ -16,11 +16,10 @@ publish = false
acme_common = { path = "../acme_common" }
clap = "2.32"
handlebars = "3.0"
http_req = "0.5"
log = "0.4"
nom = "5.0"
openssl-sys = "0.9"
reqwest = { version = "0.10", features = ["blocking"] }
reqwest = { version = "0.10", features = ["blocking", "default-tls", "json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "0.5"

2
acmed/build.rs

@ -58,7 +58,7 @@ fn set_lock() {
}
};
for p in lock.package.iter() {
if p.name == "http_req" {
if p.name == "reqwest" {
let agent = format!("{}/{}", p.name, p.version);
set_rustc_env_var!("ACMED_HTTP_LIB_AGENT", agent);
set_rustc_env_var!("ACMED_HTTP_LIB_NAME", p.name);

124
acmed/src/acme_proto.rs

@ -57,17 +57,21 @@ impl PartialEq<structs::Challenge> for Challenge {
}
macro_rules! set_data_builder {
($account: ident, $data: expr, $url: expr) => {
|n: &str| encode_kid(&$account.key_pair, &$account.account_url, $data, &$url, n)
($account: ident, $data: expr) => {
|n: &str, url: &str| encode_kid(&$account.key_pair, &$account.account_url, $data, url, n)
};
}
macro_rules! set_empty_data_builder {
($account: ident, $url: expr) => {
set_data_builder!($account, b"", $url)
($account: ident) => {
set_data_builder!($account, b"")
};
}
pub fn request_certificate(cert: &Certificate, root_certs: &[String], endpoint: &mut Endpoint) -> Result<(), Error> {
pub fn request_certificate(
cert: &Certificate,
root_certs: &[String],
endpoint: &mut Endpoint,
) -> Result<(), Error> {
let domains = cert
.domains
.iter()
@ -75,37 +79,26 @@ pub fn request_certificate(cert: &Certificate, root_certs: &[String], endpoint:
.collect::<Vec<String>>();
let mut hook_datas = vec![];
// 1. Get the directory
let directory = http::get_directory(cert, root_certs, &endpoint.url)?;
// Refresh the directory
http::refresh_directory(endpoint, root_certs)?;
// 2. Get a first nonce
let nonce = http::get_nonce(cert, root_certs, &directory.new_nonce)?;
// Get or create the account
let account = AccountManager::new(endpoint, root_certs, cert)?;
// 3. Get or create the account
let (account, nonce) = AccountManager::new(cert, &directory, endpoint, &nonce, root_certs)?;
// 4. Create a new order
// Create a new order
let new_order = NewOrder::new(&domains);
let new_order = serde_json::to_string(&new_order)?;
let data_builder = set_data_builder!(account, new_order.as_bytes(), directory.new_order);
let (order, order_url, mut nonce): (Order, String, String) = http::get_obj_loc(
cert,
root_certs,
&directory.new_order,
&data_builder,
&nonce,
)?;
let data_builder = set_data_builder!(account, new_order.as_bytes());
let (order, order_url) = http::new_order(endpoint, root_certs, &data_builder)?;
if let Some(e) = order.get_error() {
cert.warn(&e.prefix("Error").message);
}
// 5. Get all the required authorizations
// Begin iter over authorizations
for auth_url in order.authorizations.iter() {
let data_builder = set_empty_data_builder!(account, auth_url);
let (auth, new_nonce): (Authorization, String) =
http::get_obj(cert, root_certs, &auth_url, &data_builder, &nonce)?;
nonce = new_nonce;
// Fetch the authorization
let data_builder = set_empty_data_builder!(account);
let auth = http::get_authorization(endpoint, root_certs, &data_builder, &auth_url)?;
if let Some(e) = auth.get_error() {
cert.warn(&e.prefix("Error").message);
}
@ -120,7 +113,7 @@ pub fn request_certificate(cert: &Certificate, root_certs: &[String], endpoint:
return Err(msg.into());
}
// 6. For each authorization, fetch the associated challenges
// Fetch the associated challenges
let current_challenge = cert.get_domain_challenge(&auth.identifier.value)?;
for challenge in auth.challenges.iter() {
if current_challenge == *challenge {
@ -128,87 +121,60 @@ pub fn request_certificate(cert: &Certificate, root_certs: &[String], endpoint:
let file_name = challenge.get_file_name();
let domain = auth.identifier.value.to_owned();
// 7. Call the challenge hook in order to complete it
// Call the challenge hook in order to complete it
let mut data = cert.call_challenge_hooks(&file_name, &proof, &domain)?;
data.0.is_clean_hook = true;
hook_datas.push(data);
// 8. Tell the server the challenge has been completed
// Tell the server the challenge has been completed
let chall_url = challenge.get_url();
let data_builder = set_data_builder!(account, b"{}", chall_url);
let new_nonce = http::post_challenge_response(
cert,
root_certs,
&chall_url,
&data_builder,
&nonce,
)?;
nonce = new_nonce;
let data_builder = set_data_builder!(account, b"{}");
let _ =
http::post_challenge_response(endpoint, root_certs, &data_builder, &chall_url)?;
}
}
// 9. Pool the authorization in order to see whether or not it is valid
let data_builder = set_empty_data_builder!(account, auth_url);
// Pool the authorization in order to see whether or not it is valid
let data_builder = set_empty_data_builder!(account);
let break_fn = |a: &Authorization| a.status == AuthorizationStatus::Valid;
let (_, new_nonce): (Authorization, String) = http::pool_obj(
cert,
root_certs,
&auth_url,
&data_builder,
&break_fn,
&nonce,
)?;
nonce = new_nonce;
let _ =
http::pool_authorization(endpoint, root_certs, &data_builder, &break_fn, &auth_url)?;
for (data, hook_type) in hook_datas.iter() {
cert.call_challenge_hooks_clean(&data, (*hook_type).to_owned())?;
}
hook_datas.clear();
}
// End iter over authorizations
// 10. Pool the order in order to see whether or not it is ready
let data_builder = set_empty_data_builder!(account, order_url);
// Pool the order in order to see whether or not it is ready
let data_builder = set_empty_data_builder!(account);
let break_fn = |o: &Order| o.status == OrderStatus::Ready;
let (order, nonce): (Order, String) = http::pool_obj(
cert,
root_certs,
&order_url,
&data_builder,
&break_fn,
&nonce,
)?;
// 11. Finalize the order by sending the CSR
let order = http::pool_order(endpoint, root_certs, &data_builder, &break_fn, &order_url)?;
// Finalize the order by sending the CSR
let key_pair = certificate::get_key_pair(cert)?;
let domains: Vec<String> = cert.domains.iter().map(|e| e.dns.to_owned()).collect();
let csr = json!({
"csr": Csr::new(&key_pair, domains.as_slice())?.to_der_base64()?,
});
let csr = csr.to_string();
let data_builder = set_data_builder!(account, csr.as_bytes(), order.finalize);
let (order, nonce): (Order, String) =
http::get_obj(cert, root_certs, &order.finalize, &data_builder, &nonce)?;
let data_builder = set_data_builder!(account, csr.as_bytes());
let order = http::finalize_order(endpoint, root_certs, &data_builder, &order.finalize)?;
if let Some(e) = order.get_error() {
cert.warn(&e.prefix("Error").message);
}
// 12. Pool the order in order to see whether or not it is valid
let data_builder = set_empty_data_builder!(account, order_url);
// Pool the order in order to see whether or not it is valid
let data_builder = set_empty_data_builder!(account);
let break_fn = |o: &Order| o.status == OrderStatus::Valid;
let (order, nonce): (Order, String) = http::pool_obj(
cert,
root_certs,
&order_url,
&data_builder,
&break_fn,
&nonce,
)?;
// 13. Download the certificate
let order = http::pool_order(endpoint, root_certs, &data_builder, &break_fn, &order_url)?;
// Download the certificate
let crt_url = order
.certificate
.ok_or_else(|| Error::from("No certificate available for download."))?;
let data_builder = set_empty_data_builder!(account, crt_url);
let (crt, _) = http::get_certificate(cert, root_certs, &crt_url, &data_builder, &nonce)?;
let data_builder = set_empty_data_builder!(account);
let crt = http::get_certificate(endpoint, root_certs, &data_builder, &crt_url)?;
storage::write_certificate(cert, &crt.as_bytes())?;
cert.info("Certificate renewed");

25
acmed/src/acme_proto/account.rs

@ -1,5 +1,5 @@
use crate::acme_proto::http;
use crate::acme_proto::structs::{Account, AccountResponse, Directory};
use crate::acme_proto::structs::Account;
use crate::certificate::Certificate;
use crate::endpoint::Endpoint;
use crate::jws::algorithms::SignatureAlgorithm;
@ -17,32 +17,25 @@ pub struct AccountManager {
impl AccountManager {
pub fn new(
cert: &Certificate,
directory: &Directory,
endpoint: &Endpoint,
nonce: &str,
endpoint: &mut Endpoint,
root_certs: &[String],
) -> Result<(Self, String), Error> {
cert: &Certificate,
) -> Result<Self, Error> {
// TODO: store the key id (account url)
let key_pair = storage::get_account_keypair(cert)?;
let kp_ref = &key_pair;
let account = Account::new(cert, endpoint);
let account = serde_json::to_string(&account)?;
let data_builder =
|n: &str| encode_jwk(&key_pair, account.as_bytes(), &directory.new_account, n);
let (acc_rep, account_url, nonce): (AccountResponse, String, String) = http::get_obj_loc(
cert,
root_certs,
&directory.new_account,
&data_builder,
&nonce,
)?;
let acc_ref = &account;
let data_builder = |n: &str, url: &str| encode_jwk(kp_ref, acc_ref.as_bytes(), url, n);
let (acc_rep, account_url) = http::new_account(endpoint, root_certs, &data_builder)?;
let ac = AccountManager {
key_pair,
account_url,
orders_url: acc_rep.orders.unwrap_or_default(),
};
// TODO: check account data and, if different from config, update them
Ok((ac, nonce))
Ok(ac)
}
}

424
acmed/src/acme_proto/http.rs

@ -1,353 +1,171 @@
use crate::acme_proto::structs::{AcmeError, ApiError, Directory, HttpApiError};
use crate::certificate::Certificate;
use crate::acme_proto::structs::{AccountResponse, Authorization, Directory, Order};
use crate::endpoint::Endpoint;
use crate::http;
use acme_common::error::Error;
use http_req::request::{self, Method};
use http_req::response::Response;
use http_req::uri::Uri;
use std::path::Path;
use std::str::FromStr;
use std::{thread, time};
const CONTENT_TYPE_JOSE: &str = "application/jose+json";
const CONTENT_TYPE_JSON: &str = "application/json";
struct Request<'a> {
r: request::Request<'a>,
uri: &'a Uri,
method: Method,
}
struct DummyString {
pub content: String,
}
impl FromStr for DummyString {
type Err = Error;
fn from_str(data: &str) -> Result<Self, Self::Err> {
Ok(DummyString {
content: data.to_string(),
})
}
}
fn new_request<'a>(root_certs: &'a [String], uri: &'a Uri, method: Method) -> Request<'a> {
let useragent = format!(
"{}/{} ({}) {}",
crate::APP_NAME,
crate::APP_VERSION,
env!("ACMED_TARGET"),
env!("ACMED_HTTP_LIB_AGENT")
);
let mut rb = request::Request::new(uri);
for file_name in root_certs.iter() {
rb.root_cert_file_pem(&Path::new(file_name));
}
rb.method(method.to_owned());
rb.header("User-Agent", &useragent);
// TODO: allow to configure the language
rb.header("Accept-Language", "en-US,en;q=0.5");
Request { r: rb, method, uri }
}
fn send_request(cert: &Certificate, request: &Request) -> Result<(Response, String), Error> {
let mut buffer = Vec::new();
cert.debug(&format!("{}: {}", request.method, request.uri));
let res = request.r.send(&mut buffer)?;
let res_str = String::from_utf8(buffer)?;
Ok((res, res_str))
}
fn send_request_retry(cert: &Certificate, request: &Request) -> Result<(Response, String), Error> {
for _ in 0..crate::DEFAULT_HTTP_FAIL_NB_RETRY {
let (res, res_body) = send_request(cert, request)?;
match check_response(&res, &res_body) {
Ok(()) => {
return Ok((res, res_body));
}
Err(e) => {
if !e.is_recoverable() {
let msg = format!("HTTP error: {}: {}", res.status_code(), res.reason());
return Err(msg.into());
}
cert.warn(&format!("{}", e));
}
};
thread::sleep(time::Duration::from_secs(crate::DEFAULT_HTTP_FAIL_WAIT_SEC));
macro_rules! pool_object {
($obj_type: ty, $obj_name: expr, $endpoint: expr, $root_certs: expr, $url: expr, $data_builder: expr, $break: expr) => {{
for _ in 0..crate::DEFAULT_POOL_NB_TRIES {
thread::sleep(time::Duration::from_secs(crate::DEFAULT_POOL_WAIT_SEC));
let response = http::post_jose($endpoint, $root_certs, $url, $data_builder)?;
let obj = response.json::<$obj_type>()?;
if $break(&obj) {
return Ok(obj);
}
Err("Too much errors, will not retry".into())
}
fn get_header(res: &Response, name: &str) -> Result<String, Error> {
match res.headers().get(name) {
Some(v) => Ok(v.to_string()),
None => Err(format!("{}: header not found.", name).into()),
}
}
fn is_nonce(data: &str) -> bool {
!data.is_empty()
&& data
.bytes()
.all(|c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_')
}
fn nonce_from_response(cert: &Certificate, res: &Response) -> Result<String, Error> {
let nonce = get_header(res, "Replay-Nonce")?;
if is_nonce(&nonce) {
cert.trace(&format!("New nonce: {}", nonce));
Ok(nonce)
} else {
let msg = format!("{}: invalid nonce.", nonce);
let msg = format!("{} pooling failed on {}", $obj_name, $url);
Err(msg.into())
}
}};
}
fn post_jose_type(
cert: &Certificate,
root_certs: &[String],
url: &str,
data: &[u8],
accept_type: &str,
) -> Result<(Response, String), Error> {
let uri = url.parse::<Uri>()?;
let mut request = new_request(root_certs, &uri, Method::POST);
request.r.header("Content-Type", CONTENT_TYPE_JOSE);
request.r.header("Content-Length", &data.len().to_string());
request.r.header("Accept", accept_type);
request.r.body(data);
let rstr = String::from_utf8_lossy(data);
cert.trace(&format!("request body: {}", rstr));
let (res, res_body) = send_request(cert, &request)?;
let lpos = res_body.find('{').unwrap_or(0);
let res_body = if lpos == 0 {
res_body
} else {
res_body.chars().skip(lpos).collect::<String>()
};
let rpos = res_body.rfind('}').unwrap_or(0);
let res_body = if rpos == 0 {
res_body
} else {
res_body.chars().take(rpos + 1).collect::<String>()
};
cert.trace(&format!("response body: {}", res_body));
Ok((res, res_body))
}
fn check_response(res: &Response, body: &str) -> Result<(), AcmeError> {
if res.status_code().is_success() {
pub fn refresh_directory(endpoint: &mut Endpoint, root_certs: &[String]) -> Result<(), Error> {
let url = endpoint.url.clone();
let response = http::get(endpoint, root_certs, &url)?;
endpoint.dir = response.json::<Directory>()?;
Ok(())
} else {
Err(HttpApiError::from_str(body)?.get_acme_type())
}
}
fn fetch_obj_type<T, G>(
cert: &Certificate,
pub fn new_account<F>(
endpoint: &mut Endpoint,
root_certs: &[String],
url: &str,
data_builder: &G,
nonce: &str,
accept_type: &str,
) -> Result<(T, String, String), Error>
data_builder: &F,
) -> Result<(AccountResponse, String), Error>
where
T: std::str::FromStr<Err = Error>,
G: Fn(&str) -> Result<String, Error>,
F: Fn(&str, &str) -> Result<String, Error>,
{
let mut nonce = nonce.to_string();
for _ in 0..crate::DEFAULT_HTTP_FAIL_NB_RETRY {
let data = data_builder(&nonce)?;
let (res, res_body) = post_jose_type(cert, root_certs, url, data.as_bytes(), accept_type)?;
nonce = nonce_from_response(cert, &res)?;
match check_response(&res, &res_body) {
Ok(()) => {
let obj = T::from_str(&res_body)?;
let location = get_header(&res, "Location").unwrap_or_else(|_| String::new());
return Ok((obj, location, nonce));
}
Err(e) => {
if !e.is_recoverable() {
let msg = format!("HTTP error: {}: {}", res.status_code(), res.reason());
return Err(msg.into());
}
cert.warn(&format!("{}", e));
}
};
thread::sleep(time::Duration::from_secs(crate::DEFAULT_HTTP_FAIL_WAIT_SEC));
}
Err("Too much errors, will not retry".into())
}
fn fetch_obj<T, G>(
cert: &Certificate,
let url = endpoint.dir.new_account.clone();
let response = http::post_jose(endpoint, root_certs, &url, data_builder)?;
let acc_uri = response
.headers()
.get(http::HEADER_LOCATION)
.ok_or_else(|| Error::from("No account location found."))?;
let acc_uri = http::header_to_string(&acc_uri)?;
let acc_resp = response.json::<AccountResponse>()?;
Ok((acc_resp, acc_uri))
}
pub fn new_order<F>(
endpoint: &mut Endpoint,
root_certs: &[String],
url: &str,
data_builder: &G,
nonce: &str,
) -> Result<(T, String, String), Error>
data_builder: &F,
) -> Result<(Order, String), Error>
where
T: std::str::FromStr<Err = Error>,
G: Fn(&str) -> Result<String, Error>,
F: Fn(&str, &str) -> Result<String, Error>,
{
fetch_obj_type(
cert,
root_certs,
url,
data_builder,
nonce,
CONTENT_TYPE_JSON,
)
}
pub fn get_obj_loc<T, G>(
cert: &Certificate,
let url = endpoint.dir.new_order.clone();
let response = http::post_jose(endpoint, root_certs, &url, data_builder)?;
let order_uri = response
.headers()
.get(http::HEADER_LOCATION)
.ok_or_else(|| Error::from("No order location found."))?;
let order_uri = http::header_to_string(&order_uri)?;
let order_resp = response.json::<Order>()?;
Ok((order_resp, order_uri))
}
pub fn get_authorization<F>(
endpoint: &mut Endpoint,
root_certs: &[String],
data_builder: &F,
url: &str,
data_builder: &G,
nonce: &str,
) -> Result<(T, String, String), Error>
) -> Result<Authorization, Error>
where
T: std::str::FromStr<Err = Error>,
G: Fn(&str) -> Result<String, Error>,
F: Fn(&str, &str) -> Result<String, Error>,
{
let (obj, location, nonce) = fetch_obj(cert, root_certs, url, data_builder, nonce)?;
if location.is_empty() {
Err("Location header not found.".into())
} else {
Ok((obj, location, nonce))
}
let response = http::post_jose(endpoint, root_certs, &url, data_builder)?;
let auth = response.json::<Authorization>()?;
Ok(auth)
}
pub fn get_obj<T, G>(
cert: &Certificate,
pub fn post_challenge_response<F>(
endpoint: &mut Endpoint,
root_certs: &[String],
data_builder: &F,
url: &str,
data_builder: &G,
nonce: &str,
) -> Result<(T, String), Error>
) -> Result<(), Error>
where
T: std::str::FromStr<Err = Error>,
G: Fn(&str) -> Result<String, Error>,
F: Fn(&str, &str) -> Result<String, Error>,
{
let (obj, _, nonce) = fetch_obj(cert, root_certs, url, data_builder, nonce)?;
Ok((obj, nonce))
let _ = http::post_jose(endpoint, root_certs, &url, data_builder)?;
Ok(())
}
pub fn pool_obj<T, G, S>(
cert: &Certificate,
pub fn pool_authorization<F, S>(
endpoint: &mut Endpoint,
root_certs: &[String],
url: &str,
data_builder: &G,
data_builder: &F,
break_fn: &S,
nonce: &str,
) -> Result<(T, String), Error>
url: &str,
) -> Result<Authorization, Error>
where
T: std::str::FromStr<Err = Error> + ApiError,
G: Fn(&str) -> Result<String, Error>,
S: Fn(&T) -> bool,
F: Fn(&str, &str) -> Result<String, Error>,
S: Fn(&Authorization) -> bool,
{
let mut nonce: String = nonce.to_string();
for _ in 0..crate::DEFAULT_POOL_NB_TRIES {
thread::sleep(time::Duration::from_secs(crate::DEFAULT_POOL_WAIT_SEC));
let (obj, _, new_nonce) = fetch_obj(cert, root_certs, url, data_builder, &nonce)?;
if break_fn(&obj) {
return Ok((obj, new_nonce));
}
if let Some(e) = obj.get_error() {
cert.warn(&e.prefix("Error").message);
}
nonce = new_nonce;
}
let msg = format!("Pooling failed for {}", url);
Err(msg.into())
pool_object!(
Authorization,
"Authorization",
endpoint,
root_certs,
url,
data_builder,
break_fn
)
}
pub fn post_challenge_response<G>(
cert: &Certificate,
pub fn pool_order<F, S>(
endpoint: &mut Endpoint,
root_certs: &[String],
data_builder: &F,
break_fn: &S,
url: &str,
data_builder: &G,
nonce: &str,
) -> Result<String, Error>
) -> Result<Order, Error>
where
G: Fn(&str) -> Result<String, Error>,
F: Fn(&str, &str) -> Result<String, Error>,
S: Fn(&Order) -> bool,
{
let (_, _, nonce): (DummyString, String, String) =
fetch_obj(cert, root_certs, url, data_builder, nonce)?;
Ok(nonce)
pool_object!(
Order,
"Order",
endpoint,
root_certs,
url,
data_builder,
break_fn
)
}
pub fn get_certificate<G>(
cert: &Certificate,
pub fn finalize_order<F>(
endpoint: &mut Endpoint,
root_certs: &[String],
data_builder: &F,
url: &str,
data_builder: &G,
nonce: &str,
) -> Result<(String, String), Error>
) -> Result<Order, Error>
where
G: Fn(&str) -> Result<String, Error>,
F: Fn(&str, &str) -> Result<String, Error>,
{
let (res_body, _, nonce): (DummyString, String, String) =
fetch_obj(cert, root_certs, url, data_builder, nonce)?;
Ok((res_body.content, nonce))
let response = http::post_jose(endpoint, root_certs, &url, data_builder)?;
let order = response.json::<Order>()?;
Ok(order)
}
pub fn get_directory(
cert: &Certificate,
pub fn get_certificate<F>(
endpoint: &mut Endpoint,
root_certs: &[String],
data_builder: &F,
url: &str,
) -> Result<Directory, Error> {
let uri = url.parse::<Uri>()?;
let mut request = new_request(root_certs, &uri, Method::GET);
request.r.header("Accept", CONTENT_TYPE_JSON);
let (r, s) = send_request_retry(cert, &request)?;
check_response(&r, &s)?;
Directory::from_str(&s)
}
pub fn get_nonce(cert: &Certificate, root_certs: &[String], url: &str) -> Result<String, Error> {
let uri = url.parse::<Uri>()?;
let request = new_request(root_certs, &uri, Method::HEAD);
let (res, res_body) = send_request_retry(cert, &request)?;
check_response(&res, &res_body)?;
nonce_from_response(cert, &res)
}
#[cfg(test)]
mod tests {
use super::is_nonce;
#[test]
fn test_nonce_valid() {
let lst = [
"XFHw3qcgFNZAdw",
"XFHw3qcg-NZAdw",
"XFHw3qcg_NZAdw",
"XFHw3qcg-_ZAdw",
"a",
"1",
"-",
"_",
];
for n in lst.iter() {
assert!(is_nonce(n));
}
}
#[test]
fn test_nonce_invalid() {
let lst = [
"",
"rdo9x8gS4K/mZg==",
"rdo9x8gS4K/mZg",
"rdo9x8gS4K+mZg",
"৬",
"京",
];
for n in lst.iter() {
assert!(!is_nonce(n));
}
}
) -> Result<String, Error>
where
F: Fn(&str, &str) -> Result<String, Error>,
{
let response = http::post(
endpoint,
root_certs,
&url,
data_builder,
http::CONTENT_TYPE_JOSE,
http::CONTENT_TYPE_PEM,
)?;
let crt_pem = response.text()?;
Ok(crt_pem)
}

8
acmed/src/config.rs

@ -5,10 +5,10 @@ use acme_common::to_idna;
use log::info;
use serde::Deserialize;
use std::collections::HashMap;
use std::fmt;
use std::fs::{self, File};
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use std::fmt;
macro_rules! set_cfg_attr {
($to: expr, $from: expr) => {
@ -186,11 +186,7 @@ pub struct Endpoint {
impl Endpoint {
fn to_generic(&self, _cnf: &Config) -> Result<crate::endpoint::Endpoint, Error> {
// TODO: include rate limits using `cnf.get_rate_limit()`
let ep = crate::endpoint::Endpoint {
name: self.name.to_owned(),
url: self.url.to_owned(),
tos_agreed: self.tos_agreed,
};
let ep = crate::endpoint::Endpoint::new(&self.name, &self.url, self.tos_agreed);
Ok(ep)
}
}

24
acmed/src/endpoint.rs

@ -1,6 +1,30 @@
use crate::acme_proto::structs::Directory;
pub struct Endpoint {
pub name: String,
pub url: String,
pub tos_agreed: bool,
pub dir: Directory,
pub nonce: Option<String>,
// TODO: rate limits
}
impl Endpoint {
pub fn new(name: &str, url: &str, tos_agreed: bool) -> Self {
Self {
name: name.to_string(),
url: url.to_string(),
tos_agreed,
dir: Directory {
meta: None,
new_nonce: String::new(),
new_account: String::new(),
new_order: String::new(),
new_authz: None,
revoke_cert: String::new(),
key_change: String::new(),
},
nonce: None,
}
}
}

200
acmed/src/http.rs

@ -0,0 +1,200 @@
use crate::acme_proto::structs::HttpApiError;
use crate::endpoint::Endpoint;
use acme_common::error::Error;
use reqwest::blocking::{Client, Response};
use reqwest::header::{self, HeaderMap, HeaderValue};
use std::fs::File;
use std::io::prelude::*;
use std::{thread, time};
pub const CONTENT_TYPE_JOSE: &str = "application/jose+json";
pub const CONTENT_TYPE_JSON: &str = "application/json";
pub const CONTENT_TYPE_PEM: &str = "application/pem-certificate-chain";
pub const HEADER_NONCE: &str = "Replay-Nonce";
pub const HEADER_LOCATION: &str = "Location";
fn is_nonce(data: &str) -> bool {
!data.is_empty()
&& data
.bytes()
.all(|c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_')
}
fn new_nonce(endpoint: &mut Endpoint, root_certs: &[String]) -> Result<(), Error> {
rate_limit(endpoint);
let url = endpoint.dir.new_nonce.clone();
let _ = get(endpoint, root_certs, &url)?;
Ok(())
}
fn update_nonce(endpoint: &mut Endpoint, response: &Response) -> Result<(), Error> {
if let Some(nonce) = response.headers().get(HEADER_NONCE) {
let nonce = header_to_string(&nonce)?;
if !is_nonce(&nonce) {
let msg = format!("{}: invalid nonce.", &nonce);
return Err(msg.into());
}
endpoint.nonce = Some(nonce.to_string());
}
Ok(())
}
fn check_status(response: &Response) -> Result<(), Error> {
let status = response.status();
if !status.is_success() {
let msg = status
.canonical_reason()
.unwrap_or("<no description provided>");
let msg = format!("HTTP error: {}: {}", status.as_u16(), msg);
return Err(msg.into());
}
Ok(())
}
fn rate_limit(endpoint: &Endpoint) {
// TODO: Implement
}
pub fn header_to_string(header_value: &HeaderValue) -> Result<String, Error> {
let s = header_value
.to_str()
.map_err(|_| Error::from("Invalid nonce format."))?;
Ok(s.to_string())
}
fn get_client(root_certs: &[String]) -> Result<Client, Error> {
let useragent = format!(
"{}/{} ({}) {}",
crate::APP_NAME,
crate::APP_VERSION,
env!("ACMED_TARGET"),
env!("ACMED_HTTP_LIB_AGENT")
);
let mut headers = HeaderMap::with_capacity(2);
// TODO: allow to change the language
headers.insert(
header::ACCEPT_LANGUAGE,
HeaderValue::from_static("en-US,en;q=0.5"),
);
headers.insert(header::USER_AGENT, HeaderValue::from_str(&useragent)?);
let mut client_builder = Client::builder().default_headers(headers).referer(false);
for crt_file in root_certs.iter() {
let mut buff = Vec::new();
File::open(crt_file)?.read_to_end(&mut buff)?;
let crt = reqwest::Certificate::from_pem(&buff)?;
client_builder = client_builder.add_root_certificate(crt);
}
let client = client_builder.build()?;
Ok(client)
}
pub fn get(endpoint: &mut Endpoint, root_certs: &[String], url: &str) -> Result<Response, Error> {
let client = get_client(root_certs)?;
rate_limit(endpoint);
let response = client
.get(url)
.header(header::ACCEPT, HeaderValue::from_static(CONTENT_TYPE_JSON))
.send()?;
update_nonce(endpoint, &response)?;
check_status(&response)?;
Ok(response)
}
pub fn post<F>(
endpoint: &mut Endpoint,
root_certs: &[String],
url: &str,
data_builder: &F,
content_type: &str,
accept: &str,
) -> Result<Response, Error>
where
F: Fn(&str, &str) -> Result<String, Error>,
{
let client = get_client(root_certs)?;
if endpoint.nonce.is_none() {
let _ = new_nonce(endpoint, root_certs);
}
for _ in 0..crate::DEFAULT_HTTP_FAIL_NB_RETRY {
let nonce = &endpoint.nonce.clone().unwrap();
let body = data_builder(&nonce, url)?.into_bytes();
rate_limit(endpoint);
let response = client
.post(url)
.body(body)
.header(header::ACCEPT, HeaderValue::from_str(accept)?)
.header(header::CONTENT_TYPE, HeaderValue::from_str(content_type)?)
.send()?;
update_nonce(endpoint, &response)?;
match check_status(&response) {
Ok(_) => {
return Ok(response);
}
Err(e) => {
let api_err = response.json::<HttpApiError>()?;
let acme_err = api_err.get_acme_type();
if !acme_err.is_recoverable() {
return Err(e);
}
}
}
thread::sleep(time::Duration::from_secs(crate::DEFAULT_HTTP_FAIL_WAIT_SEC));
}
Err("Too much errors, will not retry".into())
}
pub fn post_jose<F>(
endpoint: &mut Endpoint,
root_certs: &[String],
url: &str,
data_builder: &F,
) -> Result<Response, Error>
where
F: Fn(&str, &str) -> Result<String, Error>,
{
post(
endpoint,
root_certs,
url,
data_builder,
CONTENT_TYPE_JOSE,
CONTENT_TYPE_JSON,
)
}
#[cfg(test)]
mod tests {
use super::is_nonce;
#[test]
fn test_nonce_valid() {
let lst = [
"XFHw3qcgFNZAdw",
"XFHw3qcg-NZAdw",
"XFHw3qcg_NZAdw",
"XFHw3qcg-_ZAdw",
"a",
"1",
"-",
"_",
];
for n in lst.iter() {
assert!(is_nonce(n));
}
}
#[test]
fn test_nonce_invalid() {
let lst = [
"",
"rdo9x8gS4K/mZg==",
"rdo9x8gS4K/mZg",
"rdo9x8gS4K+mZg",
"৬",
"京",
];
for n in lst.iter() {
assert!(!is_nonce(n));
}
}
}

1
acmed/src/main.rs

@ -8,6 +8,7 @@ mod certificate;
mod config;
mod endpoint;
mod hooks;
mod http;
mod jws;
mod main_event_loop;
mod storage;

4
acmed/src/main_event_loop.rs

@ -60,7 +60,7 @@ impl MainEventLoop {
env: crt.env.to_owned(),
id: i + 1,
};
if ! endpoints.contains_key(&endpoint.name) {
if !endpoints.contains_key(&endpoint.name) {
endpoints.insert(endpoint.name.clone(), endpoint);
}
init_account(&cert)?;
@ -89,7 +89,7 @@ impl MainEventLoop {
match self.endpoints.get_mut(&crt.endpoint_name) {
Some(mut endpoint) => {
renew_certificate(&crt, &root_certs, &mut endpoint);
},
}
None => {
crt.warn(&format!("{}: Endpoint not found", &crt.endpoint_name));
}

Loading…
Cancel
Save