use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use serde_json::{Value, json};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
pub const CHALLENGE_TOKEN: &str = "upstream-token-value";
#[derive(Clone, Default)]
pub struct Script {
pub polls_before_ready: usize,
pub polls_before_valid: usize,
pub bad_nonce_once: bool,
pub bad_csr: bool,
pub already_revoked: bool,
pub order_fails: bool,
pub order_poll_outages: usize,
pub omit_location: bool,
pub chain: String,
pub retry_after: bool,
pub no_renewal_info: bool,
pub renewal_window: Option<(String, String)>,
pub require_eab: bool,
pub reject_eab: bool,
pub pose_challenge: bool,
pub offer_http01: bool,
pub offer_tokenless_challenge: bool,
pub fail_challenge: bool,
pub wildcard_identifier: bool,
pub http01_responder: Option<String>,
}
pub struct Upstream {
pub base: String,
counters: Arc<Counters>,
_task: tokio::task::JoinHandle<()>,
}
#[derive(Default)]
struct Counters {
order_polls: AtomicUsize,
polls_at_finalize: AtomicUsize,
finalized: AtomicUsize,
revoked: AtomicUsize,
nonce_fetches: AtomicUsize,
nonce_serial: AtomicUsize,
bad_nonce_sent: AtomicUsize,
challenge_triggered: AtomicUsize,
tokenless_triggered: AtomicUsize,
http01_body: std::sync::Mutex<Option<String>>,
http01_ok: std::sync::atomic::AtomicBool,
ari_requests: AtomicUsize,
last_cert_id: std::sync::Mutex<Option<String>>,
last_eab_payload: std::sync::Mutex<Option<String>>,
}
impl Upstream {
pub fn order_polls(&self) -> usize {
self.counters.order_polls.load(Ordering::SeqCst)
}
pub fn finalized(&self) -> usize {
self.counters.finalized.load(Ordering::SeqCst)
}
pub fn revoked(&self) -> usize {
self.counters.revoked.load(Ordering::SeqCst)
}
pub fn nonce_fetches(&self) -> usize {
self.counters.nonce_fetches.load(Ordering::SeqCst)
}
pub fn challenge_triggered(&self) -> usize {
self.counters.challenge_triggered.load(Ordering::SeqCst)
}
pub fn tokenless_triggered(&self) -> usize {
self.counters.tokenless_triggered.load(Ordering::SeqCst)
}
pub fn http01_body(&self) -> Option<String> {
self.counters.http01_body.lock().unwrap().clone()
}
pub fn ari_requests(&self) -> usize {
self.counters.ari_requests.load(Ordering::SeqCst)
}
pub fn last_cert_id(&self) -> Option<String> {
self.counters.last_cert_id.lock().unwrap().clone()
}
pub fn last_eab_payload(&self) -> Option<String> {
self.counters.last_eab_payload.lock().unwrap().clone()
}
pub fn directory_url(&self) -> String {
format!("{}/directory", self.base)
}
}
pub async fn start(script: Script) -> Upstream {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
let base = format!("http://127.0.0.1:{port}");
let counters = Arc::new(Counters::default());
let task = {
let base = base.clone();
let counters = counters.clone();
tokio::spawn(async move {
loop {
let Ok((mut stream, _)) = listener.accept().await else {
return;
};
let base = base.clone();
let counters = counters.clone();
let script = script.clone();
tokio::spawn(async move {
let Some((method, path, body)) = read_request(&mut stream).await else {
return;
};
let response = route(&method, &path, &body, &base, &script, &counters).await;
let _ = stream.write_all(response.as_bytes()).await;
let _ = stream.shutdown().await;
});
}
})
};
Upstream {
base,
counters,
_task: task,
}
}
async fn read_request(stream: &mut tokio::net::TcpStream) -> Option<(String, String, String)> {
let mut buffer = Vec::new();
let mut chunk = [0u8; 1024];
let head_end = loop {
let read = stream.read(&mut chunk).await.ok()?;
if read == 0 {
return None;
}
buffer.extend_from_slice(&chunk[..read]);
if let Some(position) = find(&buffer, b"\r\n\r\n") {
break position + 4;
}
};
let head = String::from_utf8_lossy(&buffer[..head_end]).into_owned();
let mut lines = head.lines();
let mut request_line = lines.next()?.split_whitespace();
let method = request_line.next()?.to_string();
let path = request_line.next()?.to_string();
let length: usize = head
.lines()
.find_map(|line| {
let (name, value) = line.split_once(':')?;
name.eq_ignore_ascii_case("content-length")
.then(|| value.trim().parse().ok())?
})
.unwrap_or(0);
let mut body = buffer[head_end..].to_vec();
while body.len() < length {
let read = stream.read(&mut chunk).await.ok()?;
if read == 0 {
break;
}
body.extend_from_slice(&chunk[..read]);
}
Some((method, path, String::from_utf8_lossy(&body).into_owned()))
}
fn decoded_payload(body: &str) -> Option<String> {
use base64::prelude::*;
let value: Value = serde_json::from_str(body).ok()?;
let payload = value.get("payload")?.as_str()?;
let bytes = BASE64_URL_SAFE_NO_PAD.decode(payload).ok()?;
String::from_utf8(bytes).ok()
}
async fn fetch(url: &str) -> Option<(u16, String)> {
let rest = url.strip_prefix("http://")?;
let (authority, path) = match rest.find('/') {
Some(position) => (&rest[..position], &rest[position..]),
None => (rest, "/"),
};
let mut stream = tokio::net::TcpStream::connect(authority).await.ok()?;
stream
.write_all(
format!("GET {path} HTTP/1.1\r\nHost: {authority}\r\nConnection: close\r\n\r\n")
.as_bytes(),
)
.await
.ok()?;
let mut raw = Vec::new();
stream.read_to_end(&mut raw).await.ok()?;
let raw = String::from_utf8_lossy(&raw);
let (head, body) = raw.split_once("\r\n\r\n")?;
let status = head.split_whitespace().nth(1)?.parse().ok()?;
Some((status, body.to_string()))
}
fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack
.windows(needle.len())
.position(|window| window == needle)
}
async fn route(
method: &str,
path: &str,
body: &str,
base: &str,
script: &Script,
counters: &Counters,
) -> String {
let path = path
.strip_prefix(base)
.unwrap_or(path)
.split('?')
.next()
.unwrap_or(path);
if path == "/newNonce" {
counters.nonce_fetches.fetch_add(1, Ordering::SeqCst);
return with_nonce(200, "", None, counters);
}
if path == "/directory" {
let mut directory = json!({
"newNonce": format!("{base}/newNonce"),
"newAccount": format!("{base}/newAccount"),
"newOrder": format!("{base}/newOrder"),
"revokeCert": format!("{base}/revokeCert"),
});
if !script.no_renewal_info {
directory["renewalInfo"] = json!(format!("{base}/renewalInfo"));
}
return json_response(200, &directory, None, counters);
}
if let Some(cert_id) = path.strip_prefix("/renewalInfo/") {
counters.ari_requests.fetch_add(1, Ordering::SeqCst);
*counters.last_cert_id.lock().unwrap() = Some(cert_id.to_string());
let (start, end) = script.renewal_window.clone().unwrap_or_else(|| {
(
"2026-08-01T00:00:00Z".to_string(),
"2026-08-08T00:00:00Z".to_string(),
)
});
return json_response(
200,
&json!({ "suggestedWindow": { "start": start, "end": end } }),
None,
counters,
);
}
if script.bad_nonce_once
&& method == "POST"
&& counters.bad_nonce_sent.fetch_add(1, Ordering::SeqCst) == 0
{
return problem(
400,
"urn:ietf:params:acme:error:badNonce",
"stale",
counters,
);
}
match path {
"/newAccount" => {
if script.reject_eab {
return problem(
403,
"urn:ietf:params:acme:error:externalAccountRequired",
"the offered external account binding was not recognized",
counters,
);
}
if script.require_eab {
let carries_eab = decoded_payload(body)
.map(|payload| payload.contains("externalAccountBinding"))
.unwrap_or(false);
if !carries_eab {
return problem(
403,
"urn:ietf:params:acme:error:externalAccountRequired",
"an external account binding is required",
counters,
);
}
*counters.last_eab_payload.lock().unwrap() = decoded_payload(body);
}
let location = (!script.omit_location).then(|| format!("{base}/acct/1"));
json_response(201, &json!({ "status": "valid" }), location, counters)
}
"/newOrder" => {
let location = (!script.omit_location).then(|| format!("{base}/order/1"));
json_response(
201,
&order_object(base, "pending", false),
location,
counters,
)
}
"/authz/1" => {
let answered = counters.challenge_triggered.load(Ordering::SeqCst) > 0;
let fetch_ok =
script.http01_responder.is_none() || counters.http01_ok.load(Ordering::SeqCst);
let status = if answered && (script.fail_challenge || !fetch_ok) {
"invalid"
} else if answered {
"valid"
} else {
"pending"
};
let typ = if script.offer_http01 {
"http-01"
} else {
"dns-01"
};
let identifier = if script.wildcard_identifier {
"*.example.com"
} else {
"example.com"
};
let mut challenges = Vec::new();
if script.offer_tokenless_challenge {
challenges.push(json!({
"type": "dns-persist-01",
"url": format!("{base}/chall/persist"),
"status": "pending",
"accounturi": format!("{base}/acct/1"),
}));
}
challenges.push(json!({
"type": typ,
"url": format!("{base}/chall/1"),
"token": CHALLENGE_TOKEN,
"status": status,
}));
json_response(
200,
&json!({
"status": status,
"identifier": { "type": "dns", "value": identifier },
"challenges": challenges,
}),
None,
counters,
)
}
"/chall/persist" => {
counters.tokenless_triggered.fetch_add(1, Ordering::SeqCst);
json_response(200, &json!({ "status": "processing" }), None, counters)
}
"/chall/1" => {
counters.challenge_triggered.fetch_add(1, Ordering::SeqCst);
if let Some(responder) = &script.http01_responder {
let url = format!("{responder}/.well-known/acme-challenge/{CHALLENGE_TOKEN}");
if let Some((200, served)) = fetch(&url).await {
let served = served.trim().to_string();
counters.http01_ok.store(
served.starts_with(&format!("{CHALLENGE_TOKEN}.")),
Ordering::SeqCst,
);
*counters.http01_body.lock().unwrap() = Some(served);
}
}
json_response(200, &json!({ "status": "processing" }), None, counters)
}
"/order/1" => {
let polls = counters.order_polls.fetch_add(1, Ordering::SeqCst);
if polls < script.order_poll_outages {
return json_response(
503,
&json!({
"type": "urn:ietf:params:acme:error:serverInternal",
"detail": "the CA is temporarily unavailable",
}),
None,
counters,
);
}
if script.order_fails {
return json_response(200, &order_object(base, "invalid", false), None, counters);
}
if script.pose_challenge && counters.challenge_triggered.load(Ordering::SeqCst) == 0 {
return json_response(200, &order_object(base, "pending", false), None, counters);
}
let status = if counters.finalized.load(Ordering::SeqCst) == 0 {
if polls >= script.polls_before_ready {
"ready"
} else {
"processing"
}
} else {
let since = polls.saturating_sub(counters.polls_at_finalize.load(Ordering::SeqCst));
if since >= script.polls_before_valid {
"valid"
} else {
"processing"
}
};
let response = json_response(200, &order_object(base, status, true), None, counters);
if script.retry_after && status == "processing" {
insert_header(&response, "Retry-After: 0")
} else {
response
}
}
"/order/1/finalize" => {
if script.bad_csr {
return problem(
403,
"urn:ietf:params:acme:error:badCSR",
"CSR rejected upstream",
counters,
);
}
assert!(
body.contains("payload"),
"finalize must carry a signed JWS body"
);
counters.polls_at_finalize.store(
counters.order_polls.load(Ordering::SeqCst),
Ordering::SeqCst,
);
counters.finalized.fetch_add(1, Ordering::SeqCst);
json_response(
200,
&order_object(base, "processing", false),
None,
counters,
)
}
"/cert/1" => text_response(200, &script.chain, counters),
"/revokeCert" => {
counters.revoked.fetch_add(1, Ordering::SeqCst);
if script.already_revoked {
return problem(
400,
"urn:ietf:params:acme:error:alreadyRevoked",
"already revoked",
counters,
);
}
json_response(200, &json!({}), None, counters)
}
_ => problem(
404,
"urn:ietf:params:acme:error:malformed",
"no such resource",
counters,
),
}
}
fn order_object(base: &str, status: &str, with_certificate: bool) -> Value {
let mut object = json!({
"status": status,
"finalize": format!("{base}/order/1/finalize"),
"authorizations": [format!("{base}/authz/1")],
});
if with_certificate && status == "valid" {
object["certificate"] = json!(format!("{base}/cert/1"));
}
if status == "invalid" {
object["error"] = json!({ "detail": "upstream refused the order" });
}
object
}
fn next_nonce(counters: &Counters) -> String {
format!(
"nonce-{}",
counters.nonce_serial.fetch_add(1, Ordering::SeqCst)
)
}
fn with_nonce(status: u16, body: &str, location: Option<String>, counters: &Counters) -> String {
let mut response = format!(
"HTTP/1.1 {status} {}\r\nReplay-Nonce: {}\r\nContent-Type: application/json\r\n\
Content-Length: {}\r\nConnection: close\r\n",
reason(status),
next_nonce(counters),
body.len()
);
if let Some(location) = location {
response.push_str(&format!("Location: {location}\r\n"));
}
response.push_str("\r\n");
response.push_str(body);
response
}
fn json_response(
status: u16,
body: &Value,
location: Option<String>,
counters: &Counters,
) -> String {
with_nonce(status, &body.to_string(), location, counters)
}
fn text_response(status: u16, body: &str, counters: &Counters) -> String {
with_nonce(status, body, None, counters)
}
fn problem(status: u16, typ: &str, detail: &str, counters: &Counters) -> String {
with_nonce(
status,
&json!({ "type": typ, "detail": detail }).to_string(),
None,
counters,
)
}
fn insert_header(response: &str, header: &str) -> String {
let (head, body) = response.split_once("\r\n\r\n").unwrap();
format!("{head}\r\n{header}\r\n\r\n{body}")
}
fn reason(status: u16) -> &'static str {
match status {
200 => "OK",
201 => "Created",
400 => "Bad Request",
403 => "Forbidden",
404 => "Not Found",
_ => "Unknown",
}
}