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
Rodolphe Breard
4 years ago
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
-
424acmed/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));
|
|||
}
|
|||
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)
|
|||
}
|
@ -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