yacme_test/
lib.rs

1use std::{
2    io::{self, Read},
3    net::Ipv4Addr,
4    path::{Path, PathBuf},
5    sync::{Arc, Mutex},
6};
7
8use lazy_static::lazy_static;
9use serde::Serialize;
10
11pub fn parse_http_response_example(data: &str) -> http::Response<String> {
12    let mut lines = data.lines();
13
14    let status = {
15        let status_line = lines.next().unwrap().trim();
16        let (version, status) = status_line.split_once(' ').unwrap();
17
18        if version != "HTTP/1.1" {
19            panic!("Expected HTTP/1.1, got {version}");
20        }
21
22        let (code, _reason) = status.split_once(' ').unwrap();
23        http::StatusCode::from_u16(code.parse().expect("status code is u16"))
24            .expect("known status code")
25    };
26
27    let mut headers = http::HeaderMap::new();
28
29    for line in lines.by_ref() {
30        if line.is_empty() {
31            break;
32        } else {
33            let (name, value) = line
34                .trim()
35                .split_once(": ")
36                .expect("Header delimiter is ':'");
37            headers.append(
38                http::header::HeaderName::from_bytes(name.as_bytes()).unwrap(),
39                value.parse().expect("valid header value"),
40            );
41        }
42    }
43
44    let body: String = lines.collect();
45    let mut response = http::Response::new(body);
46    *response.headers_mut() = headers;
47    *response.status_mut() = status;
48    *response.version_mut() = http::Version::HTTP_11;
49    response
50}
51
52pub fn read_bytes<P: AsRef<Path>>(path: P) -> io::Result<Vec<u8>> {
53    let mut rdr = io::BufReader::new(std::fs::File::open(path)?);
54    let mut buf = Vec::new();
55    rdr.read_to_end(&mut buf)?;
56    Ok(buf)
57}
58
59pub fn read_string<P: AsRef<Path>>(path: P) -> io::Result<String> {
60    let mut rdr = io::BufReader::new(std::fs::File::open(path)?);
61    let mut buf = String::new();
62    rdr.read_to_string(&mut buf)?;
63    Ok(buf)
64}
65
66const PEBBLE_DIRECTORY: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../pebble/");
67
68lazy_static! {
69    static ref PEBBLE: Mutex<Arc<Pebble>> = Mutex::new(Arc::new(Pebble::create()));
70}
71
72#[derive(Debug)]
73pub struct Pebble {
74    directory: PathBuf,
75}
76
77impl Pebble {
78    #[allow(clippy::new_without_default)]
79
80    pub fn new() -> Arc<Self> {
81        let pebble = PEBBLE.lock().unwrap();
82        if Arc::strong_count(&pebble) == 1 {
83            pebble.start();
84        }
85        pebble.clone()
86    }
87
88    fn create() -> Self {
89        let pebble_directory = std::fs::canonicalize(PEBBLE_DIRECTORY).expect("valid pebble path");
90        Pebble {
91            directory: pebble_directory,
92        }
93    }
94
95    fn start(&self) {
96        let output = std::process::Command::new("docker")
97            .arg("compose")
98            .args([
99                "up",
100                "--detach",
101                "--remove-orphans",
102                "--renew-anon-volumes",
103                "--wait",
104            ])
105            .current_dir(&self.directory)
106            .output()
107            .expect("able to spawn docker compose command");
108
109        if !output.status.success() {
110            panic!("Failed to start a pebble server");
111        }
112    }
113
114    pub fn certificate(&self) -> reqwest::Certificate {
115        let cert = self.directory.join("pebble.minica.pem");
116
117        reqwest::Certificate::from_pem(&read_bytes(cert).unwrap()).expect("valid pebble root CA")
118    }
119
120    pub async fn dns_a(&self, host: &str, addresses: &[Ipv4Addr]) {
121        #[derive(Debug, Serialize)]
122        struct PebbleDNSRecord {
123            host: String,
124            addresses: Vec<String>,
125        }
126
127        let chall_setup = PebbleDNSRecord {
128            host: host.to_owned(),
129            addresses: addresses.iter().map(|ip| ip.to_string()).collect(),
130        };
131
132        let resp = reqwest::Client::new()
133            .post("http://localhost:8055/add-a")
134            .json(&chall_setup)
135            .send()
136            .await
137            .expect("connect to pebble");
138        match resp.error_for_status_ref() {
139            Ok(_) => {}
140            Err(_) => {
141                eprintln!("Request:");
142                eprintln!("{}", serde_json::to_string(&chall_setup).unwrap());
143                eprintln!("ERROR:");
144                eprintln!("Status: {:?}", resp.status().canonical_reason());
145                eprintln!(
146                    "{}",
147                    resp.text().await.expect("get response body from pebble")
148                );
149                panic!("Failed to update challenge server");
150            }
151        }
152    }
153
154    pub async fn dns01(&self, host: &str, value: &str) {
155        #[derive(Debug, Serialize)]
156        struct Dns01TXT {
157            host: String,
158            value: String,
159        }
160
161        let chall_setup = Dns01TXT {
162            host: host.to_owned(),
163            value: value.to_owned(),
164        };
165
166        tracing::trace!(
167            "Challenge Setup:\n{}",
168            serde_json::to_string(&chall_setup).unwrap()
169        );
170
171        let resp = reqwest::Client::new()
172            .post("http://localhost:8055/set-txt")
173            .json(&chall_setup)
174            .send()
175            .await
176            .expect("connect to pebble");
177        match resp.error_for_status_ref() {
178            Ok(_) => {}
179            Err(_) => {
180                eprintln!("Request:");
181                eprintln!("{}", serde_json::to_string(&chall_setup).unwrap());
182                eprintln!("ERROR:");
183                eprintln!("Status: {:?}", resp.status().canonical_reason());
184                eprintln!(
185                    "{}",
186                    resp.text().await.expect("get response body from pebble")
187                );
188                panic!("Failed to update challenge server");
189            }
190        }
191    }
192
193    pub async fn http01(&self, token: &str, content: &str) {
194        #[derive(Debug, Serialize)]
195        struct Http01ChallengeSetup {
196            token: String,
197            content: String,
198        }
199
200        let chall_setup = Http01ChallengeSetup {
201            token: token.to_owned(),
202            content: content.to_owned(),
203        };
204
205        tracing::trace!(
206            "Challenge Setup:\n{}",
207            serde_json::to_string(&chall_setup).unwrap()
208        );
209
210        let resp = reqwest::Client::new()
211            .post("http://localhost:8055/add-http01")
212            .json(&chall_setup)
213            .send()
214            .await
215            .expect("connect to pebble");
216        match resp.error_for_status_ref() {
217            Ok(_) => {}
218            Err(_) => {
219                eprintln!("Request:");
220                eprintln!("{}", serde_json::to_string(&chall_setup).unwrap());
221                eprintln!("ERROR:");
222                eprintln!("Status: {:?}", resp.status().canonical_reason());
223                eprintln!(
224                    "{}",
225                    resp.text().await.expect("get response body from pebble")
226                );
227                panic!("Failed to update challenge server");
228            }
229        }
230    }
231
232    pub fn down(self: &Arc<Self>) {
233        let pebble = PEBBLE.lock().unwrap();
234        if Arc::strong_count(self) == 2 {
235            self.down_internal();
236        }
237        drop(pebble)
238    }
239
240    fn down_internal(&self) {
241        let output = std::process::Command::new("docker")
242            .arg("compose")
243            .args(["down", "--remove-orphans", "--volumes", "--timeout", "10"])
244            .current_dir(&self.directory)
245            .output()
246            .expect("able to spawn docker compose command");
247
248        if !output.status.success() {
249            panic!("Failed to stop a pebble server");
250        }
251    }
252}