Skip to content

Commit b34480c

Browse files
committed
ACME: HTTP-01 challenge solver implementation.
1 parent 084f831 commit b34480c

File tree

4 files changed

+182
-1
lines changed

4 files changed

+182
-1
lines changed

src/acme/solvers.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use super::types::{Challenge, ChallengeKind};
44
use super::AuthorizationContext;
55
use crate::conf::identifier::Identifier;
66

7+
pub mod http;
8+
79
#[derive(Debug, Error)]
810
#[error("challenge registration failed: {0}")]
911
pub enum SolverError {

src/acme/solvers/http.rs

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
use core::ptr;
2+
3+
use nginx_sys::{
4+
ngx_array_push, ngx_buf_t, ngx_chain_t, ngx_conf_t, ngx_http_discard_request_body,
5+
ngx_http_finalize_request, ngx_http_handler_pt, ngx_http_output_filter,
6+
ngx_http_phases_NGX_HTTP_POST_READ_PHASE, ngx_http_request_t,
7+
};
8+
use ngx::allocator::TryCloneIn;
9+
use ngx::collections::RbTreeMap;
10+
use ngx::core::{NgxStr, NgxString, SlabPool, Status};
11+
use ngx::http::HttpModuleMainConf;
12+
use ngx::sync::RwLock;
13+
use ngx::{http_request_handler, ngx_log_debug_http};
14+
15+
use crate::acme;
16+
use crate::conf::identifier::Identifier;
17+
use crate::conf::AcmeMainConfig;
18+
19+
use super::{ChallengeSolver, SolverError};
20+
21+
/// Registers http-01 challenge handler.
22+
pub fn postconfiguration(cf: &mut ngx_conf_t, _amcf: &mut AcmeMainConfig) -> Result<(), Status> {
23+
let cmcf = ngx::http::NgxHttpCoreModule::main_conf_mut(cf).expect("http core main conf");
24+
25+
// The handler needs to be set as early as possible, to ensure that it is not affected by the
26+
// server configuration.
27+
let h: *mut ngx_http_handler_pt = unsafe {
28+
ngx_array_push(&mut cmcf.phases[ngx_http_phases_NGX_HTTP_POST_READ_PHASE as usize].handlers)
29+
}
30+
.cast();
31+
32+
if h.is_null() {
33+
return Err(Status::NGX_ERROR);
34+
}
35+
36+
unsafe { *h = Some(handler) };
37+
38+
Ok(())
39+
}
40+
41+
pub type Http01SolverState<A> = RbTreeMap<NgxString<A>, NgxString<A>, A>;
42+
43+
#[derive(Debug)]
44+
pub struct Http01Solver<'a>(&'a RwLock<Http01SolverState<SlabPool>>);
45+
46+
impl<'a> Http01Solver<'a> {
47+
pub fn new(inner: &'a RwLock<Http01SolverState<SlabPool>>) -> Self {
48+
Self(inner)
49+
}
50+
}
51+
52+
impl ChallengeSolver for Http01Solver<'_> {
53+
fn supports(&self, c: &acme::types::ChallengeKind) -> bool {
54+
matches!(c, crate::acme::types::ChallengeKind::Http01)
55+
}
56+
57+
fn register(
58+
&self,
59+
ctx: &acme::AuthorizationContext,
60+
_identifier: &Identifier<&str>,
61+
challenge: &acme::types::Challenge,
62+
) -> Result<(), SolverError> {
63+
let alloc = self.0.read().allocator().clone();
64+
65+
let mut key_authorization = NgxString::new_in(alloc.clone());
66+
key_authorization.try_reserve_exact(challenge.token.len() + ctx.thumbprint.len() + 1)?;
67+
// write to a preallocated buffer of a sufficient size should succeed
68+
let _ = key_authorization.append_within_capacity(challenge.token.as_bytes());
69+
let _ = key_authorization.append_within_capacity(b".");
70+
let _ = key_authorization.append_within_capacity(ctx.thumbprint);
71+
let token = NgxString::try_from_bytes_in(&challenge.token, alloc)?;
72+
self.0.write().try_insert(token, key_authorization)?;
73+
Ok(())
74+
}
75+
76+
fn unregister(
77+
&self,
78+
_identifier: &Identifier<&str>,
79+
challenge: &acme::types::Challenge,
80+
) -> Result<(), SolverError> {
81+
self.0.write().remove(challenge.token.as_bytes());
82+
Ok(())
83+
}
84+
}
85+
86+
http_request_handler!(handler, |r: &mut ngx::http::Request| {
87+
if r.method() != ngx::http::Method::GET {
88+
return Status::NGX_DECLINED;
89+
}
90+
91+
let amcf = crate::HttpAcmeModule::main_conf(r).expect("acme config");
92+
let Some(amsh) = amcf.data else {
93+
return Status::NGX_DECLINED;
94+
};
95+
96+
let Some(token) = r
97+
.path()
98+
.as_bytes()
99+
.strip_prefix(b"/.well-known/acme-challenge/")
100+
else {
101+
return Status::NGX_DECLINED;
102+
};
103+
104+
let token = NgxStr::from_bytes(token);
105+
106+
let key_auth = if let Some(resp) = amsh.http_01_state.read().get(token) {
107+
resp.try_clone_in(r.pool())
108+
} else {
109+
ngx_log_debug_http!(r, "acme/http-01: no challenge registered for {token}");
110+
return Status::NGX_DECLINED;
111+
};
112+
113+
let Ok(key_auth) = key_auth else {
114+
return Status::NGX_ERROR;
115+
};
116+
117+
ngx_log_debug_http!(r, "acme/http-01: challenge for {token}");
118+
119+
let rc = Status(unsafe { ngx_http_discard_request_body(r.as_mut()) });
120+
if rc != Status::NGX_OK {
121+
return rc;
122+
}
123+
124+
r.set_status(ngx::http::HTTPStatus::OK);
125+
126+
r.set_content_length_n(key_auth.len());
127+
if r.add_header_out("connection", "close").is_none()
128+
|| r.add_header_out("content-type", "text/plain").is_none()
129+
{
130+
return Status::NGX_ERROR;
131+
}
132+
133+
let rc = r.send_header();
134+
if rc == Status::NGX_ERROR || rc > Status::NGX_OK {
135+
return rc;
136+
}
137+
138+
let buf: *mut ngx_buf_t = r.pool().calloc_type();
139+
if buf.is_null() {
140+
return Status::NGX_ERROR;
141+
}
142+
143+
let (p, len, _, _) = key_auth.into_raw_parts();
144+
145+
unsafe {
146+
(*buf).set_memory(1);
147+
(*buf).set_last_buf(if r.is_main() { 1 } else { 0 });
148+
(*buf).set_last_in_chain(1);
149+
(*buf).start = p;
150+
(*buf).end = p.add(len);
151+
(*buf).pos = (*buf).start;
152+
(*buf).last = (*buf).end;
153+
}
154+
155+
let mut chain = ngx_chain_t {
156+
buf,
157+
next: ptr::null_mut(),
158+
};
159+
160+
let r: *mut ngx_http_request_t = r.into();
161+
unsafe { ngx_http_finalize_request(r, ngx_http_output_filter(r, &mut chain)) }
162+
163+
Status::NGX_DONE
164+
});

src/lib.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ impl HttpModule for HttpAcmeModule {
104104
return e.into();
105105
}
106106

107+
/* http-01 challenge handler */
108+
109+
if let Err(err) = acme::solvers::http::postconfiguration(cf, amcf) {
110+
return err.into();
111+
};
112+
107113
Status::NGX_OK.into()
108114
}
109115
}
@@ -203,7 +209,7 @@ async fn ngx_http_acme_update_certificates(amcf: &AcmeMainConfig) -> Time {
203209
}
204210

205211
async fn ngx_http_acme_update_certificates_for_issuer(
206-
_amcf: &AcmeMainConfig,
212+
amcf: &AcmeMainConfig,
207213
issuer: &conf::issuer::Issuer,
208214
) -> anyhow::Result<Time> {
209215
let log = ngx_cycle_log();
@@ -216,6 +222,11 @@ async fn ngx_http_acme_update_certificates_for_issuer(
216222
);
217223
let mut client = AcmeClient::new(http, issuer, log)?;
218224

225+
let amsh = amcf.data.expect("acme shared data");
226+
227+
let http_solver = acme::solvers::http::Http01Solver::new(&amsh.http_01_state);
228+
client.add_solver(http_solver);
229+
219230
let mut next = Time::MAX;
220231

221232
for (order, cert) in issuer.orders.iter() {

src/state.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use ngx::log::ngx_cycle_log;
1010
use ngx::sync::RwLock;
1111
use ngx::{ngx_log_debug, ngx_log_error};
1212

13+
use crate::acme;
1314
use crate::conf::shared_zone::SharedZone;
1415
use crate::conf::AcmeMainConfig;
1516

@@ -25,15 +26,18 @@ where
2526
A: Allocator + Clone,
2627
{
2728
pub issuers: Queue<RwLock<IssuerContext>, A>,
29+
pub http_01_state: RwLock<acme::solvers::http::Http01SolverState<A>>,
2830
}
2931

3032
impl<A> AcmeSharedData<A>
3133
where
3234
A: Allocator + Clone,
3335
{
3436
pub fn try_new_in(alloc: A) -> Result<Self, AllocError> {
37+
let http_01_state = acme::solvers::http::Http01SolverState::try_new_in(alloc.clone())?;
3538
Ok(Self {
3639
issuers: Queue::try_new_in(alloc)?,
40+
http_01_state: RwLock::new(http_01_state),
3741
})
3842
}
3943
}

0 commit comments

Comments
 (0)