Line data Source code
1 : //
2 : // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3 : //
4 : // Distributed under the Boost Software License, Version 1.0. (See accompanying
5 : // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6 : //
7 : // Official repository: https://github.com/cppalliance/corosio
8 : //
9 :
10 : #ifndef BOOST_COROSIO_RESOLVER_HPP
11 : #define BOOST_COROSIO_RESOLVER_HPP
12 :
13 : #include <boost/corosio/detail/config.hpp>
14 : #include <boost/corosio/detail/except.hpp>
15 : #include <boost/corosio/endpoint.hpp>
16 : #include <boost/corosio/io_object.hpp>
17 : #include <boost/capy/io_result.hpp>
18 : #include <boost/corosio/resolver_results.hpp>
19 : #include <boost/capy/ex/executor_ref.hpp>
20 : #include <boost/capy/ex/execution_context.hpp>
21 : #include <boost/capy/ex/io_env.hpp>
22 : #include <boost/capy/concept/executor.hpp>
23 :
24 : #include <system_error>
25 :
26 : #include <cassert>
27 : #include <concepts>
28 : #include <coroutine>
29 : #include <cstdint>
30 : #include <stop_token>
31 : #include <string>
32 : #include <string_view>
33 : #include <type_traits>
34 :
35 : namespace boost::corosio {
36 :
37 : /** Bitmask flags for resolver queries.
38 :
39 : These flags correspond to the hints parameter of getaddrinfo.
40 : */
41 : enum class resolve_flags : unsigned int
42 : {
43 : /// No flags.
44 : none = 0,
45 :
46 : /// Indicate that returned endpoint is intended for use as a locally
47 : /// bound socket endpoint.
48 : passive = 0x01,
49 :
50 : /// Host name should be treated as a numeric string defining an IPv4
51 : /// or IPv6 address and no name resolution should be attempted.
52 : numeric_host = 0x04,
53 :
54 : /// Service name should be treated as a numeric string defining a port
55 : /// number and no name resolution should be attempted.
56 : numeric_service = 0x08,
57 :
58 : /// Only return IPv4 addresses if a non-loopback IPv4 address is
59 : /// configured for the system. Only return IPv6 addresses if a
60 : /// non-loopback IPv6 address is configured for the system.
61 : address_configured = 0x20,
62 :
63 : /// If the query protocol family is specified as IPv6, return
64 : /// IPv4-mapped IPv6 addresses on finding no IPv6 addresses.
65 : v4_mapped = 0x800,
66 :
67 : /// If used with v4_mapped, return all matching IPv6 and IPv4 addresses.
68 : all_matching = 0x100
69 : };
70 :
71 : /** Combine two resolve_flags. */
72 : inline
73 : resolve_flags
74 10 : operator|(resolve_flags a, resolve_flags b) noexcept
75 : {
76 : return static_cast<resolve_flags>(
77 : static_cast<unsigned int>(a) |
78 10 : static_cast<unsigned int>(b));
79 : }
80 :
81 : /** Combine two resolve_flags. */
82 : inline
83 : resolve_flags&
84 1 : operator|=(resolve_flags& a, resolve_flags b) noexcept
85 : {
86 1 : a = a | b;
87 1 : return a;
88 : }
89 :
90 : /** Intersect two resolve_flags. */
91 : inline
92 : resolve_flags
93 103 : operator&(resolve_flags a, resolve_flags b) noexcept
94 : {
95 : return static_cast<resolve_flags>(
96 : static_cast<unsigned int>(a) &
97 103 : static_cast<unsigned int>(b));
98 : }
99 :
100 : /** Intersect two resolve_flags. */
101 : inline
102 : resolve_flags&
103 1 : operator&=(resolve_flags& a, resolve_flags b) noexcept
104 : {
105 1 : a = a & b;
106 1 : return a;
107 : }
108 :
109 : //------------------------------------------------------------------------------
110 :
111 : /** Bitmask flags for reverse resolver queries.
112 :
113 : These flags correspond to the flags parameter of getnameinfo.
114 : */
115 : enum class reverse_flags : unsigned int
116 : {
117 : /// No flags.
118 : none = 0,
119 :
120 : /// Return the numeric form of the hostname instead of its name.
121 : numeric_host = 0x01,
122 :
123 : /// Return the numeric form of the service name instead of its name.
124 : numeric_service = 0x02,
125 :
126 : /// Return an error if the hostname cannot be resolved.
127 : name_required = 0x04,
128 :
129 : /// Lookup for datagram (UDP) service instead of stream (TCP).
130 : datagram_service = 0x08
131 : };
132 :
133 : /** Combine two reverse_flags. */
134 : inline
135 : reverse_flags
136 6 : operator|(reverse_flags a, reverse_flags b) noexcept
137 : {
138 : return static_cast<reverse_flags>(
139 : static_cast<unsigned int>(a) |
140 6 : static_cast<unsigned int>(b));
141 : }
142 :
143 : /** Combine two reverse_flags. */
144 : inline
145 : reverse_flags&
146 1 : operator|=(reverse_flags& a, reverse_flags b) noexcept
147 : {
148 1 : a = a | b;
149 1 : return a;
150 : }
151 :
152 : /** Intersect two reverse_flags. */
153 : inline
154 : reverse_flags
155 47 : operator&(reverse_flags a, reverse_flags b) noexcept
156 : {
157 : return static_cast<reverse_flags>(
158 : static_cast<unsigned int>(a) &
159 47 : static_cast<unsigned int>(b));
160 : }
161 :
162 : /** Intersect two reverse_flags. */
163 : inline
164 : reverse_flags&
165 1 : operator&=(reverse_flags& a, reverse_flags b) noexcept
166 : {
167 1 : a = a & b;
168 1 : return a;
169 : }
170 :
171 : //------------------------------------------------------------------------------
172 :
173 : /** An asynchronous DNS resolver for coroutine I/O.
174 :
175 : This class provides asynchronous DNS resolution operations that return
176 : awaitable types. Each operation participates in the affine awaitable
177 : protocol, ensuring coroutines resume on the correct executor.
178 :
179 : @par Thread Safety
180 : Distinct objects: Safe.@n
181 : Shared objects: Unsafe. A resolver must not have concurrent resolve
182 : operations.
183 :
184 : @par Semantics
185 : Wraps platform DNS resolution (getaddrinfo/getnameinfo).
186 : Operations dispatch to OS resolver APIs via the io_context
187 : thread pool.
188 :
189 : @par Example
190 : @code
191 : io_context ioc;
192 : resolver r(ioc);
193 :
194 : // Using structured bindings
195 : auto [ec, results] = co_await r.resolve("www.example.com", "https");
196 : if (ec)
197 : co_return;
198 :
199 : for (auto const& entry : results)
200 : std::cout << entry.get_endpoint().port() << std::endl;
201 :
202 : // Or using exceptions
203 : auto results = (co_await r.resolve("www.example.com", "https")).value();
204 : @endcode
205 : */
206 : class BOOST_COROSIO_DECL resolver : public io_object
207 : {
208 : struct resolve_awaitable
209 : {
210 : resolver& r_;
211 : std::string host_;
212 : std::string service_;
213 : resolve_flags flags_;
214 : std::stop_token token_;
215 : mutable std::error_code ec_;
216 : mutable resolver_results results_;
217 :
218 16 : resolve_awaitable(
219 : resolver& r,
220 : std::string_view host,
221 : std::string_view service,
222 : resolve_flags flags) noexcept
223 16 : : r_(r)
224 32 : , host_(host)
225 32 : , service_(service)
226 16 : , flags_(flags)
227 : {
228 16 : }
229 :
230 16 : bool await_ready() const noexcept
231 : {
232 16 : return token_.stop_requested();
233 : }
234 :
235 16 : capy::io_result<resolver_results> await_resume() const noexcept
236 : {
237 16 : if (token_.stop_requested())
238 0 : return {make_error_code(std::errc::operation_canceled), {}};
239 16 : return {ec_, std::move(results_)};
240 16 : }
241 :
242 16 : auto await_suspend(
243 : std::coroutine_handle<> h,
244 : capy::io_env const* env) -> std::coroutine_handle<>
245 : {
246 16 : token_ = env->stop_token;
247 16 : return r_.get().resolve(h, env->executor, host_, service_, flags_, token_, &ec_, &results_);
248 : }
249 : };
250 :
251 : struct reverse_resolve_awaitable
252 : {
253 : resolver& r_;
254 : endpoint ep_;
255 : reverse_flags flags_;
256 : std::stop_token token_;
257 : mutable std::error_code ec_;
258 : mutable reverse_resolver_result result_;
259 :
260 10 : reverse_resolve_awaitable(
261 : resolver& r,
262 : endpoint const& ep,
263 : reverse_flags flags) noexcept
264 10 : : r_(r)
265 10 : , ep_(ep)
266 10 : , flags_(flags)
267 : {
268 10 : }
269 :
270 10 : bool await_ready() const noexcept
271 : {
272 10 : return token_.stop_requested();
273 : }
274 :
275 10 : capy::io_result<reverse_resolver_result> await_resume() const noexcept
276 : {
277 10 : if (token_.stop_requested())
278 0 : return {make_error_code(std::errc::operation_canceled), {}};
279 10 : return {ec_, std::move(result_)};
280 10 : }
281 :
282 10 : auto await_suspend(
283 : std::coroutine_handle<> h,
284 : capy::io_env const* env) -> std::coroutine_handle<>
285 : {
286 10 : token_ = env->stop_token;
287 10 : return r_.get().reverse_resolve(h, env->executor, ep_, flags_, token_, &ec_, &result_);
288 : }
289 : };
290 :
291 : public:
292 : /** Destructor.
293 :
294 : Cancels any pending operations.
295 : */
296 : ~resolver();
297 :
298 : /** Construct a resolver from an execution context.
299 :
300 : @param ctx The execution context that will own this resolver.
301 : */
302 : explicit resolver(capy::execution_context& ctx);
303 :
304 : /** Construct a resolver from an executor.
305 :
306 : The resolver is associated with the executor's context.
307 :
308 : @param ex The executor whose context will own the resolver.
309 : */
310 : template<class Ex>
311 : requires (!std::same_as<std::remove_cvref_t<Ex>, resolver>) &&
312 : capy::Executor<Ex>
313 1 : explicit resolver(Ex const& ex)
314 1 : : resolver(ex.context())
315 : {
316 1 : }
317 :
318 : /** Move constructor.
319 :
320 : Transfers ownership of the resolver resources.
321 :
322 : @param other The resolver to move from.
323 : */
324 1 : resolver(resolver&& other) noexcept
325 1 : : io_object(other.context())
326 : {
327 1 : impl_ = other.impl_;
328 1 : other.impl_ = nullptr;
329 1 : }
330 :
331 : /** Move assignment operator.
332 :
333 : Cancels any existing operations and transfers ownership.
334 : The source and destination must share the same execution context.
335 :
336 : @param other The resolver to move from.
337 :
338 : @return Reference to this resolver.
339 :
340 : @throws std::logic_error if the resolvers have different
341 : execution contexts.
342 : */
343 2 : resolver& operator=(resolver&& other)
344 : {
345 2 : if (this != &other)
346 : {
347 2 : if (ctx_ != other.ctx_)
348 1 : detail::throw_logic_error(
349 : "cannot move resolver across execution contexts");
350 1 : cancel();
351 1 : impl_ = other.impl_;
352 1 : other.impl_ = nullptr;
353 : }
354 1 : return *this;
355 : }
356 :
357 : resolver(resolver const&) = delete;
358 : resolver& operator=(resolver const&) = delete;
359 :
360 : /** Initiate an asynchronous resolve operation.
361 :
362 : Resolves the host and service names into a list of endpoints.
363 :
364 : @param host A string identifying a location. May be a descriptive
365 : name or a numeric address string.
366 :
367 : @param service A string identifying the requested service. This may
368 : be a descriptive name or a numeric string corresponding to a
369 : port number.
370 :
371 : @return An awaitable that completes with `io_result<resolver_results>`.
372 :
373 : @par Example
374 : @code
375 : auto [ec, results] = co_await r.resolve("www.example.com", "https");
376 : @endcode
377 : */
378 5 : auto resolve(
379 : std::string_view host,
380 : std::string_view service)
381 : {
382 5 : return resolve_awaitable(*this, host, service, resolve_flags::none);
383 : }
384 :
385 : /** Initiate an asynchronous resolve operation with flags.
386 :
387 : Resolves the host and service names into a list of endpoints.
388 :
389 : @param host A string identifying a location.
390 :
391 : @param service A string identifying the requested service.
392 :
393 : @param flags Flags controlling resolution behavior.
394 :
395 : @return An awaitable that completes with `io_result<resolver_results>`.
396 : */
397 11 : auto resolve(
398 : std::string_view host,
399 : std::string_view service,
400 : resolve_flags flags)
401 : {
402 11 : return resolve_awaitable(*this, host, service, flags);
403 : }
404 :
405 : /** Initiate an asynchronous reverse resolve operation.
406 :
407 : Resolves an endpoint into a hostname and service name using
408 : reverse DNS lookup (PTR record query).
409 :
410 : @param ep The endpoint to resolve.
411 :
412 : @return An awaitable that completes with
413 : `io_result<reverse_resolver_result>`.
414 :
415 : @par Example
416 : @code
417 : endpoint ep(ipv4_address({127, 0, 0, 1}), 80);
418 : auto [ec, result] = co_await r.resolve(ep);
419 : if (!ec)
420 : std::cout << result.host_name() << ":" << result.service_name();
421 : @endcode
422 : */
423 3 : auto resolve(endpoint const& ep)
424 : {
425 3 : return reverse_resolve_awaitable(*this, ep, reverse_flags::none);
426 : }
427 :
428 : /** Initiate an asynchronous reverse resolve operation with flags.
429 :
430 : Resolves an endpoint into a hostname and service name using
431 : reverse DNS lookup (PTR record query).
432 :
433 : @param ep The endpoint to resolve.
434 :
435 : @param flags Flags controlling resolution behavior. See reverse_flags.
436 :
437 : @return An awaitable that completes with
438 : `io_result<reverse_resolver_result>`.
439 : */
440 7 : auto resolve(endpoint const& ep, reverse_flags flags)
441 : {
442 7 : return reverse_resolve_awaitable(*this, ep, flags);
443 : }
444 :
445 : /** Cancel any pending asynchronous operations.
446 :
447 : All outstanding operations complete with `errc::operation_canceled`.
448 : Check `ec == cond::canceled` for portable comparison.
449 : */
450 : void cancel();
451 :
452 : public:
453 : struct resolver_impl : io_object_impl
454 : {
455 : virtual std::coroutine_handle<> resolve(
456 : std::coroutine_handle<>,
457 : capy::executor_ref,
458 : std::string_view host,
459 : std::string_view service,
460 : resolve_flags flags,
461 : std::stop_token,
462 : std::error_code*,
463 : resolver_results*) = 0;
464 :
465 : virtual std::coroutine_handle<> reverse_resolve(
466 : std::coroutine_handle<>,
467 : capy::executor_ref,
468 : endpoint const& ep,
469 : reverse_flags flags,
470 : std::stop_token,
471 : std::error_code*,
472 : reverse_resolver_result*) = 0;
473 :
474 : virtual void cancel() noexcept = 0;
475 : };
476 :
477 : private:
478 31 : inline resolver_impl& get() const noexcept
479 : {
480 31 : return *static_cast<resolver_impl*>(impl_);
481 : }
482 : };
483 :
484 : } // namespace boost::corosio
485 :
486 : #endif
|