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 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};
|
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())
|
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(())
|
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],
|
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
|
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],
|
root_certs: &[String],
|
||||
url: &str,
|
|
||||
data_builder: &G,
|
|
||||
nonce: &str,
|
|
||||
) -> Result<(T, String, String), Error>
|
|
||||
|
data_builder: &F,
|
||||
|
) -> Result<(Order, String), Error>
|
||||
where
|
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],
|
root_certs: &[String],
|
||||
|
data_builder: &F,
|
||||
url: &str,
|
url: &str,
|
||||
data_builder: &G,
|
|
||||
nonce: &str,
|
|
||||
) -> Result<(T, String, String), Error>
|
|
||||
|
) -> Result<Authorization, Error>
|
||||
where
|
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],
|
root_certs: &[String],
|
||||
|
data_builder: &F,
|
||||
url: &str,
|
url: &str,
|
||||
data_builder: &G,
|
|
||||
nonce: &str,
|
|
||||
) -> Result<(T, String), Error>
|
|
||||
|
) -> Result<(), Error>
|
||||
where
|
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],
|
root_certs: &[String],
|
||||
url: &str,
|
|
||||
data_builder: &G,
|
|
||||
|
data_builder: &F,
|
||||
break_fn: &S,
|
break_fn: &S,
|
||||
nonce: &str,
|
|
||||
) -> Result<(T, String), Error>
|
|
||||
|
url: &str,
|
||||
|
) -> Result<Authorization, Error>
|
||||
where
|
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],
|
root_certs: &[String],
|
||||
|
data_builder: &F,
|
||||
|
break_fn: &S,
|
||||
url: &str,
|
url: &str,
|
||||
data_builder: &G,
|
|
||||
nonce: &str,
|
|
||||
) -> Result<String, Error>
|
|
||||
|
) -> Result<Order, Error>
|
||||
where
|
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],
|
root_certs: &[String],
|
||||
|
data_builder: &F,
|
||||
url: &str,
|
url: &str,
|
||||
data_builder: &G,
|
|
||||
nonce: &str,
|
|
||||
) -> Result<(String, String), Error>
|
|
||||
|
) -> Result<Order, Error>
|
||||
where
|
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],
|
root_certs: &[String],
|
||||
|
data_builder: &F,
|
||||
url: &str,
|
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 struct Endpoint {
|
||||
pub name: String,
|
pub name: String,
|
||||
pub url: String,
|
pub url: String,
|
||||
pub tos_agreed: bool,
|
pub tos_agreed: bool,
|
||||
|
pub dir: Directory,
|
||||
|
pub nonce: Option<String>,
|
||||
// TODO: rate limits
|
// 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