use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
type Response =
std::result::Result<http::Response<bytes::Bytes>, Box<dyn std::error::Error + Send + Sync>>;
#[derive(Default)]
struct State {
served: HashMap<String, Vec<u8>>,
failing: HashSet<String>,
asked: Vec<String>,
}
#[derive(Clone, Default)]
pub struct RevocationHttp {
state: Arc<Mutex<State>>,
fallback: Option<Arc<dyn asx::HttpClient>>,
}
impl std::fmt::Debug for RevocationHttp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let state = self.state.lock().expect("state");
f.debug_struct("RevocationHttp")
.field("served", &state.served.len())
.field("asked", &state.asked.len())
.finish_non_exhaustive()
}
}
impl RevocationHttp {
pub fn new() -> RevocationHttp {
RevocationHttp::default()
}
pub fn with_fallback(mut self, client: impl asx::HttpClient) -> RevocationHttp {
self.fallback = Some(Arc::new(client));
self
}
pub fn serve(&self, url: &str, body: Vec<u8>) {
self.state
.lock()
.expect("state")
.served
.insert(url.to_string(), body);
}
pub fn fail(&self, url: &str) {
self.state
.lock()
.expect("state")
.failing
.insert(url.to_string());
}
pub fn heal(&self, url: &str) {
self.state.lock().expect("state").failing.remove(url);
}
pub fn requests(&self) -> usize {
self.state.lock().expect("state").asked.len()
}
pub fn requests_to(&self, url: &str) -> usize {
self.state
.lock()
.expect("state")
.asked
.iter()
.filter(|u| *u == url)
.count()
}
pub fn urls(&self) -> Vec<String> {
self.state.lock().expect("state").asked.clone()
}
async fn answer(&self, request: http::Request<bytes::Bytes>) -> Response {
let url = request.uri().to_string();
let local = {
let state = self.state.lock().expect("state");
if state.failing.contains(&url) {
Some(Err(()))
} else {
state.served.get(&url).cloned().map(Ok)
}
};
let local = match local {
Some(answer) => Some(answer),
None => super::pki::published_crl(&url).map(Ok),
};
match local {
Some(answer) => {
self.state.lock().expect("state").asked.push(url.clone());
match answer {
Ok(body) => Ok(http::Response::builder()
.status(200)
.body(bytes::Bytes::from(body))
.expect("response")),
Err(()) => Err(Box::new(Unreachable(url))),
}
}
None => match &self.fallback {
Some(fallback) => fallback.send(request).await,
None => {
self.state.lock().expect("state").asked.push(url);
Ok(http::Response::builder()
.status(404)
.body(bytes::Bytes::new())
.expect("response"))
}
},
}
}
}
#[derive(Debug)]
pub struct Unreachable(pub String);
impl std::fmt::Display for Unreachable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "revocation test client: {} is unreachable", self.0)
}
}
impl std::error::Error for Unreachable {}
impl asx::HttpClient for RevocationHttp {
fn send(&self, request: http::Request<bytes::Bytes>) -> asx::BoxFuture<'_, Response> {
Box::pin(self.answer(request))
}
}