From 26ce6fdf409478d04a1c527e134dea22ba91e66e Mon Sep 17 00:00:00 2001 From: Rodolphe Breard Date: Thu, 11 Jun 2020 19:05:34 +0200 Subject: [PATCH] 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. --- CHANGELOG.md | 6 + acme_common/Cargo.toml | 2 +- acme_common/src/error.rs | 16 +- acmed/Cargo.toml | 3 +- acmed/build.rs | 2 +- acmed/src/acme_proto.rs | 124 ++++----- acmed/src/acme_proto/account.rs | 25 +- acmed/src/acme_proto/http.rs | 428 +++++++++----------------------- acmed/src/config.rs | 8 +- acmed/src/endpoint.rs | 24 ++ acmed/src/http.rs | 200 +++++++++++++++ acmed/src/main.rs | 1 + acmed/src/main_event_loop.rs | 4 +- 13 files changed, 429 insertions(+), 414 deletions(-) create mode 100644 acmed/src/http.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 39ffbbe..5f79b56 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/acme_common/Cargo.toml b/acme_common/Cargo.toml index 77a8781..84da482 100644 --- a/acme_common/Cargo.toml +++ b/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" diff --git a/acme_common/src/error.rs b/acme_common/src/error.rs index 9e9ffd6..d81caff 100644 --- a/acme_common/src/error.rs +++ b/acme_common/src/error.rs @@ -87,12 +87,24 @@ impl From for Error { } } -impl From for Error { - fn from(error: http_req::error::Error) -> Self { +impl From for Error { + fn from(error: reqwest::Error) -> Self { format!("HTTP error: {}", error).into() } } +impl From for Error { + fn from(error: reqwest::header::InvalidHeaderName) -> Self { + format!("Invalid HTTP header name: {}", error).into() + } +} + +impl From for Error { + fn from(error: reqwest::header::InvalidHeaderValue) -> Self { + format!("Invalid HTTP header value: {}", error).into() + } +} + #[cfg(unix)] impl From for Error { fn from(error: nix::Error) -> Self { diff --git a/acmed/Cargo.toml b/acmed/Cargo.toml index 092268f..8bfd991 100644 --- a/acmed/Cargo.toml +++ b/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" diff --git a/acmed/build.rs b/acmed/build.rs index bd0b419..bd3a2f1 100644 --- a/acmed/build.rs +++ b/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); diff --git a/acmed/src/acme_proto.rs b/acmed/src/acme_proto.rs index 992ac66..3eb2600 100644 --- a/acmed/src/acme_proto.rs +++ b/acmed/src/acme_proto.rs @@ -57,17 +57,21 @@ impl PartialEq 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::>(); 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 = 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"); diff --git a/acmed/src/acme_proto/account.rs b/acmed/src/acme_proto/account.rs index 0c9abbf..96fbada 100644 --- a/acmed/src/acme_proto/account.rs +++ b/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 { // 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) } } diff --git a/acmed/src/acme_proto/http.rs b/acmed/src/acme_proto/http.rs index 923d12b..87720ae 100644 --- a/acmed/src/acme_proto/http.rs +++ b/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 { - 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)); +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(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 get_header(res: &Response, name: &str) -> Result { - 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 { - 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::()?; - 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::() - }; - let rpos = res_body.rfind('}').unwrap_or(0); - let res_body = if rpos == 0 { - res_body - } else { - res_body.chars().take(rpos + 1).collect::() - }; - 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() { - Ok(()) - } else { - Err(HttpApiError::from_str(body)?.get_acme_type()) - } +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::()?; + Ok(()) } -fn fetch_obj_type( - cert: &Certificate, +pub fn new_account( + 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, - G: Fn(&str) -> Result, + F: Fn(&str, &str) -> Result, { - 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( - 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::()?; + Ok((acc_resp, acc_uri)) +} + +pub fn new_order( + 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, - G: Fn(&str) -> Result, + F: Fn(&str, &str) -> Result, { - fetch_obj_type( - cert, - root_certs, - url, - data_builder, - nonce, - CONTENT_TYPE_JSON, - ) -} - -pub fn get_obj_loc( - 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::()?; + Ok((order_resp, order_uri)) +} + +pub fn get_authorization( + endpoint: &mut Endpoint, root_certs: &[String], + data_builder: &F, url: &str, - data_builder: &G, - nonce: &str, -) -> Result<(T, String, String), Error> +) -> Result where - T: std::str::FromStr, - G: Fn(&str) -> Result, + F: Fn(&str, &str) -> Result, { - 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::()?; + Ok(auth) } -pub fn get_obj( - cert: &Certificate, +pub fn post_challenge_response( + 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, - G: Fn(&str) -> Result, + F: Fn(&str, &str) -> Result, { - 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( - cert: &Certificate, +pub fn pool_authorization( + 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 where - T: std::str::FromStr + ApiError, - G: Fn(&str) -> Result, - S: Fn(&T) -> bool, + F: Fn(&str, &str) -> Result, + 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( - cert: &Certificate, +pub fn pool_order( + endpoint: &mut Endpoint, root_certs: &[String], + data_builder: &F, + break_fn: &S, url: &str, - data_builder: &G, - nonce: &str, -) -> Result +) -> Result where - G: Fn(&str) -> Result, + F: Fn(&str, &str) -> Result, + 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( - cert: &Certificate, +pub fn finalize_order( + endpoint: &mut Endpoint, root_certs: &[String], + data_builder: &F, url: &str, - data_builder: &G, - nonce: &str, -) -> Result<(String, String), Error> +) -> Result where - G: Fn(&str) -> Result, + F: Fn(&str, &str) -> Result, { - 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::()?; + Ok(order) } -pub fn get_directory( - cert: &Certificate, +pub fn get_certificate( + endpoint: &mut Endpoint, root_certs: &[String], + data_builder: &F, url: &str, -) -> Result { - let uri = url.parse::()?; - 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 { - let uri = url.parse::()?; - 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 +where + F: Fn(&str, &str) -> Result, +{ + 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) } diff --git a/acmed/src/config.rs b/acmed/src/config.rs index f436c53..53715e1 100644 --- a/acmed/src/config.rs +++ b/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 { // 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) } } diff --git a/acmed/src/endpoint.rs b/acmed/src/endpoint.rs index 9d8ef4a..3d1fd2e 100644 --- a/acmed/src/endpoint.rs +++ b/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, // 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, + } + } +} diff --git a/acmed/src/http.rs b/acmed/src/http.rs new file mode 100644 index 0000000..1986320 --- /dev/null +++ b/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(""); + 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 { + let s = header_value + .to_str() + .map_err(|_| Error::from("Invalid nonce format."))?; + Ok(s.to_string()) +} + +fn get_client(root_certs: &[String]) -> Result { + 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 { + 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( + endpoint: &mut Endpoint, + root_certs: &[String], + url: &str, + data_builder: &F, + content_type: &str, + accept: &str, +) -> Result +where + F: Fn(&str, &str) -> Result, +{ + 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::()?; + 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( + endpoint: &mut Endpoint, + root_certs: &[String], + url: &str, + data_builder: &F, +) -> Result +where + F: Fn(&str, &str) -> Result, +{ + 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)); + } + } +} diff --git a/acmed/src/main.rs b/acmed/src/main.rs index 9f7e4d1..026beb2 100644 --- a/acmed/src/main.rs +++ b/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; diff --git a/acmed/src/main_event_loop.rs b/acmed/src/main_event_loop.rs index b8f1bc4..ded30df 100644 --- a/acmed/src/main_event_loop.rs +++ b/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)); }