mirror of https://github.com/breard-r/acmed.git
				
				
			
			
			
				Browse Source
			
			
			Refactor the HTTP back-end
			
				
		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
				 13 changed files with 429 additions and 414 deletions
			
			
		- 
					6CHANGELOG.md
 - 
					2acme_common/Cargo.toml
 - 
					16acme_common/src/error.rs
 - 
					3acmed/Cargo.toml
 - 
					2acmed/build.rs
 - 
					124acmed/src/acme_proto.rs
 - 
					25acmed/src/acme_proto/account.rs
 - 
					428acmed/src/acme_proto/http.rs
 - 
					8acmed/src/config.rs
 - 
					24acmed/src/endpoint.rs
 - 
					200acmed/src/http.rs
 - 
					1acmed/src/main.rs
 - 
					4acmed/src/main_event_loop.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));
 | 
				
			|||
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<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() {
 | 
				
			|||
        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::<Directory>()?;
 | 
				
			|||
    Ok(())
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
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)
 | 
				
			|||
}
 | 
				
			|||
@ -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,
 | 
				
			|||
        }
 | 
				
			|||
    }
 | 
				
			|||
}
 | 
				
			|||
@ -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));
 | 
				
			|||
        }
 | 
				
			|||
    }
 | 
				
			|||
}
 | 
				
			|||
						Write
						Preview
					
					
					Loading…
					
					Cancel
						Save
					
		Reference in new issue