Skip to content

Commit a957b37

Browse files
committed
ACME: HTTP-01 challenge solver implementation.
1 parent 2ac27d9 commit a957b37

File tree

4 files changed

+186
-1
lines changed

4 files changed

+186
-1
lines changed

src/acme/solvers.rs

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

12+
pub mod http;
13+
1214
#[derive(Debug, Error)]
1315
#[error("challenge registration failed: {0}")]
1416
pub enum SolverError {

src/acme/solvers/http.rs

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

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)