Skip to content

Commit 783f68f

Browse files
pchickeyensh63bavshin-f5
committed
feat: add async wrapper for resolver
Added behind the async feature. Additionally, add an `as_ptr` method to ngx::core::Pool. This was originally an upstreaming of nginx-acme::net::resolver, but was rewritten during this upstreaming process see: https://github.com/nginx/nginx-acme/blob/main/src/net/resolver.rs There are no tests for this functionality in the ngx-rust repo at this time. This PR has been tested by manually using a test suite in a private nginx repo. Tests will be added in this repo at some point in the future. Co-authored-by: Evgeny Shirykalov <[email protected]> Co-authored-by: Aleksei Bavshin <[email protected]>
1 parent 54b7316 commit 783f68f

File tree

3 files changed

+344
-0
lines changed

3 files changed

+344
-0
lines changed

src/async_/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,7 @@
22
pub use self::sleep::{sleep, Sleep};
33
pub use self::spawn::{spawn, Task};
44

5+
pub mod resolver;
6+
57
mod sleep;
68
mod spawn;

src/async_/resolver.rs

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
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+
//! Wrapper for the nginx resolver.
7+
//!
8+
//! See <https://nginx.org/en/docs/http/ngx_http_core_module.html#resolver>.
9+
10+
use alloc::string::{String, ToString};
11+
use core::ffi::c_void;
12+
use core::fmt;
13+
use core::num::NonZero;
14+
use core::pin::Pin;
15+
use core::ptr::NonNull;
16+
use core::task::{Context, Poll, Waker};
17+
18+
use crate::{
19+
allocator::Box,
20+
collections::Vec,
21+
core::Pool,
22+
ffi::{
23+
ngx_addr_t, ngx_msec_t, ngx_resolve_name, ngx_resolve_start, ngx_resolver_ctx_t,
24+
ngx_resolver_t, ngx_str_t,
25+
},
26+
};
27+
use nginx_sys::{
28+
NGX_RESOLVE_FORMERR, NGX_RESOLVE_NOTIMP, NGX_RESOLVE_NXDOMAIN, NGX_RESOLVE_REFUSED,
29+
NGX_RESOLVE_SERVFAIL, NGX_RESOLVE_TIMEDOUT,
30+
};
31+
32+
/// Error type for all uses of `Resolver`.
33+
#[derive(Debug)]
34+
pub enum Error {
35+
/// No resolver configured
36+
NoResolver,
37+
/// Resolver error, with context of name being resolved
38+
Resolver(ResolverError, String),
39+
/// Allocation failed
40+
AllocationFailed,
41+
}
42+
43+
impl fmt::Display for Error {
44+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
45+
match self {
46+
Error::NoResolver => write!(f, "No resolver configured"),
47+
Error::Resolver(err, context) => write!(f, "{err}: resolving `{context}`"),
48+
Error::AllocationFailed => write!(f, "Allocation failed"),
49+
}
50+
}
51+
}
52+
impl core::error::Error for Error {}
53+
54+
/// These cases directly reflect the NGX_RESOLVE_ error codes,
55+
/// plus a timeout, and a case for an unknown error where a known
56+
/// NGX_RESOLVE_ should be.
57+
#[derive(Debug)]
58+
pub enum ResolverError {
59+
/// Format error (NGX_RESOLVE_FORMERR)
60+
FormErr,
61+
/// Server failure (NGX_RESOLVE_SERVFAIL)
62+
ServFail,
63+
/// Host not found (NGX_RESOLVE_NXDOMAIN)
64+
NXDomain,
65+
/// Unimplemented (NGX_RESOLVE_NOTIMP)
66+
NotImp,
67+
/// Operation refused (NGX_RESOLVE_REFUSED)
68+
Refused,
69+
/// Timed out (NGX_RESOLVE_TIMEDOUT)
70+
TimedOut,
71+
/// Unknown NGX_RESOLVE error
72+
Unknown(isize),
73+
}
74+
impl fmt::Display for ResolverError {
75+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
76+
match self {
77+
ResolverError::FormErr => write!(f, "Format error"),
78+
ResolverError::ServFail => write!(f, "Server Failure"),
79+
ResolverError::NXDomain => write!(f, "Host not found"),
80+
ResolverError::NotImp => write!(f, "Unimplemented"),
81+
ResolverError::Refused => write!(f, "Refused"),
82+
ResolverError::TimedOut => write!(f, "Timed out"),
83+
ResolverError::Unknown(code) => write!(f, "Unknown NGX_RESOLVE error {code}"),
84+
}
85+
}
86+
}
87+
impl core::error::Error for ResolverError {}
88+
89+
/// Convert from the NGX_RESOLVE_ error codes.
90+
impl From<NonZero<isize>> for ResolverError {
91+
fn from(code: NonZero<isize>) -> ResolverError {
92+
match code.get() as u32 {
93+
NGX_RESOLVE_FORMERR => ResolverError::FormErr,
94+
NGX_RESOLVE_SERVFAIL => ResolverError::ServFail,
95+
NGX_RESOLVE_NXDOMAIN => ResolverError::NXDomain,
96+
NGX_RESOLVE_NOTIMP => ResolverError::NotImp,
97+
NGX_RESOLVE_REFUSED => ResolverError::Refused,
98+
NGX_RESOLVE_TIMEDOUT => ResolverError::TimedOut,
99+
_ => ResolverError::Unknown(code.get()),
100+
}
101+
}
102+
}
103+
104+
type Res = Result<Vec<ngx_addr_t, Pool>, Error>;
105+
106+
/// A wrapper for an ngx_resolver_t which provides an async Rust API
107+
pub struct Resolver {
108+
resolver: NonNull<ngx_resolver_t>,
109+
timeout: ngx_msec_t,
110+
}
111+
112+
impl Resolver {
113+
/// Create a new `Resolver` from existing pointer to `ngx_resolver_t` and
114+
/// timeout.
115+
pub fn from_resolver(resolver: NonNull<ngx_resolver_t>, timeout: ngx_msec_t) -> Self {
116+
Self { resolver, timeout }
117+
}
118+
119+
/// Resolve a name into a set of addresses.
120+
pub async fn resolve_name(&self, name: &ngx_str_t, pool: &Pool) -> Res {
121+
let mut resolver = Resolution::new(name, &ngx_str_t::empty(), self, pool)?;
122+
resolver.as_mut().await
123+
}
124+
125+
/// Resolve a service into a set of addresses.
126+
pub async fn resolve_service(&self, name: &ngx_str_t, service: &ngx_str_t, pool: &Pool) -> Res {
127+
let mut resolver = Resolution::new(name, service, self, pool)?;
128+
resolver.as_mut().await
129+
}
130+
}
131+
132+
struct Resolution<'a> {
133+
// Storage for the result of the resolution `Res`. Populated by the
134+
// callback handler, and taken by the Future::poll impl.
135+
complete: Option<Res>,
136+
// Storage for a pending Waker. Populated by the Future::poll impl,
137+
// and taken by the callback handler.
138+
waker: Option<Waker>,
139+
// Pool used for allocating `Vec<ngx_addr_t>` contents in `Res`. Read by
140+
// the callback handler.
141+
pool: &'a Pool,
142+
// Owned pointer to the ngx_resolver_ctx_t.
143+
ctx: Option<ResolverCtx>,
144+
}
145+
146+
impl<'a> Resolution<'a> {
147+
pub fn new(
148+
name: &ngx_str_t,
149+
service: &ngx_str_t,
150+
resolver: &Resolver,
151+
pool: &'a Pool,
152+
) -> Result<Pin<Box<Self, Pool>>, Error> {
153+
// Create a pinned Resolution on the Pool, so that we can make
154+
// a stable pointer to the Resolution struct.
155+
let mut this = Box::pin_in(
156+
Resolution {
157+
complete: None,
158+
waker: None,
159+
pool,
160+
ctx: None,
161+
},
162+
pool.clone(),
163+
);
164+
165+
// Set up the ctx with everything the resolver needs to resolve a
166+
// name, and the handler callback which is called on completion.
167+
let mut ctx = ResolverCtx::new(resolver.resolver)?;
168+
ctx.name = *name;
169+
ctx.service = *service;
170+
ctx.timeout = resolver.timeout;
171+
ctx.set_cancelable(1);
172+
ctx.handler = Some(Self::handler);
173+
174+
{
175+
// Safety: Self::handler, Future::poll, and Drop::drop will have
176+
// access to &mut Resolution. Nginx is single-threaded and we are
177+
// assured only one of those is on the stack at a time, except if
178+
// Self::handler wakes a task which polls or drops the Future,
179+
// which it only does after use of &mut Resolution is complete.
180+
let ptr: &mut Resolution = unsafe { Pin::into_inner_unchecked(this.as_mut()) };
181+
ctx.data = ptr as *mut Resolution as *mut c_void;
182+
}
183+
184+
// Neither ownership nor borrows are tracked for this pointer,
185+
// and ctxp will not be used after the ctx destruction.
186+
let ctxp = ctx.0;
187+
this.ctx = Some(ctx);
188+
189+
// Start name resolution using the ctx. If the name is in the dns
190+
// cache, the handler may get called from this stack. Otherwise, it
191+
// will be called later by nginx when it gets a dns response or a
192+
// timeout.
193+
let ret = unsafe { ngx_resolve_name(ctxp.as_ptr()) };
194+
if let Some(e) = NonZero::new(ret) {
195+
return Err(Error::Resolver(ResolverError::from(e), name.to_string()));
196+
}
197+
198+
Ok(this)
199+
}
200+
201+
// Nginx will call this handler when name resolution completes. If the
202+
// result is in the cache, this could be called from inside ngx_resolve_name.
203+
// Otherwise, it will be called later on the event loop.
204+
unsafe extern "C" fn handler(ctx: *mut ngx_resolver_ctx_t) {
205+
let mut data = unsafe { NonNull::new_unchecked((*ctx).data as *mut Resolution) };
206+
let this: &mut Resolution = unsafe { data.as_mut() };
207+
208+
if let Some(ctx) = this.ctx.take() {
209+
this.complete = Some(ctx.into_result(this.pool));
210+
}
211+
212+
// Wake last, after all use of &mut Resolution, because wake may
213+
// poll Resolution future on current stack.
214+
if let Some(waker) = this.waker.take() {
215+
waker.wake();
216+
}
217+
}
218+
}
219+
220+
impl<'a> core::future::Future for Resolution<'a> {
221+
type Output = Result<Vec<ngx_addr_t, Pool>, Error>;
222+
223+
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
224+
// Resolution is Unpin, so we can use it as just a &mut Resolution
225+
let this: &mut Resolution = self.get_mut();
226+
227+
// The handler populates this.complete, and we consume it here:
228+
match this.complete.take() {
229+
Some(res) => Poll::Ready(res),
230+
None => {
231+
// If the handler has not yet fired, populate the waker field,
232+
// which the handler will consume:
233+
match &mut this.waker {
234+
None => {
235+
this.waker = Some(cx.waker().clone());
236+
}
237+
Some(w) => w.clone_from(cx.waker()),
238+
}
239+
Poll::Pending
240+
}
241+
}
242+
}
243+
}
244+
245+
/// An owned ngx_resolver_ctx_t.
246+
struct ResolverCtx(NonNull<ngx_resolver_ctx_t>);
247+
248+
impl core::ops::Deref for ResolverCtx {
249+
type Target = ngx_resolver_ctx_t;
250+
251+
fn deref(&self) -> &Self::Target {
252+
// SAFETY: this wrapper is always constructed with a valid non-empty resolve context
253+
unsafe { self.0.as_ref() }
254+
}
255+
}
256+
257+
impl core::ops::DerefMut for ResolverCtx {
258+
fn deref_mut(&mut self) -> &mut Self::Target {
259+
// SAFETY: this wrapper is always constructed with a valid non-empty resolve context
260+
unsafe { self.0.as_mut() }
261+
}
262+
}
263+
264+
impl Drop for ResolverCtx {
265+
fn drop(&mut self) {
266+
unsafe {
267+
nginx_sys::ngx_resolve_name_done(self.0.as_mut());
268+
}
269+
}
270+
}
271+
272+
impl ResolverCtx {
273+
/// Creates a new resolver context.
274+
///
275+
/// This implementation currently passes a null for the second argument `temp`. A non-null
276+
/// `temp` provides a fast, non-callback-based path for immediately returning an addr if
277+
/// `temp` contains a name which is textual form of an addr.
278+
pub fn new(resolver: NonNull<ngx_resolver_t>) -> Result<Self, Error> {
279+
let ctx = unsafe { ngx_resolve_start(resolver.as_ptr(), core::ptr::null_mut()) };
280+
NonNull::new(ctx).map(Self).ok_or(Error::AllocationFailed)
281+
}
282+
283+
/// Take the results in a ctx and make an owned copy as a
284+
/// Result<Vec<ngx_addr_t, Pool>, Error>, where the Vec and the internals
285+
/// of the ngx_addr_t are allocated on the given Pool
286+
pub fn into_result(self, pool: &Pool) -> Result<Vec<ngx_addr_t, Pool>, Error> {
287+
if let Some(e) = NonZero::new(self.state) {
288+
return Err(Error::Resolver(
289+
ResolverError::from(e),
290+
self.name.to_string(),
291+
));
292+
}
293+
if self.addrs.is_null() {
294+
Err(Error::AllocationFailed)?;
295+
}
296+
297+
let mut out = Vec::new_in(pool.clone());
298+
299+
if self.naddrs > 0 {
300+
out.try_reserve_exact(self.naddrs)
301+
.map_err(|_| Error::AllocationFailed)?;
302+
303+
for addr in unsafe { core::slice::from_raw_parts(self.addrs, self.naddrs) } {
304+
out.push(copy_resolved_addr(addr, pool)?);
305+
}
306+
}
307+
308+
Ok(out)
309+
}
310+
}
311+
312+
/// Take the contents of an ngx_resolver_addr_t and make an owned copy as
313+
/// an ngx_addr_t, using the Pool for allocation of the internals.
314+
fn copy_resolved_addr(
315+
addr: &nginx_sys::ngx_resolver_addr_t,
316+
pool: &Pool,
317+
) -> Result<ngx_addr_t, Error> {
318+
let sockaddr = pool.alloc(addr.socklen as usize) as *mut nginx_sys::sockaddr;
319+
if sockaddr.is_null() {
320+
Err(Error::AllocationFailed)?;
321+
}
322+
unsafe {
323+
addr.sockaddr
324+
.cast::<u8>()
325+
.copy_to_nonoverlapping(sockaddr.cast(), addr.socklen as usize)
326+
};
327+
328+
let name = unsafe { ngx_str_t::from_bytes(pool.as_ptr(), addr.name.as_bytes()) }
329+
.ok_or(Error::AllocationFailed)?;
330+
331+
Ok(ngx_addr_t {
332+
sockaddr,
333+
socklen: addr.socklen,
334+
name,
335+
})
336+
}

src/core/pool.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,12 @@ impl Pool {
145145
Pool(NonNull::new_unchecked(pool))
146146
}
147147

148+
/// Expose the underlying `ngx_pool_t` pointer, for use with `ngx::ffi`
149+
/// functions.
150+
pub fn as_ptr(&self) -> *mut ngx_pool_t {
151+
self.0.as_ptr()
152+
}
153+
148154
/// Creates a buffer of the specified size in the memory pool.
149155
///
150156
/// Returns `Some(TemporaryBuffer)` if the buffer is successfully created, or `None` if

0 commit comments

Comments
 (0)