TLA Line data Source code
1 : //
2 : // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3 : // Copyright (c) 2026 Steve Gerbino
4 : // Copyright (c) 2026 Michael Vandeberg
5 : //
6 : // Distributed under the Boost Software License, Version 1.0. (See accompanying
7 : // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
8 : //
9 : // Official repository: https://github.com/cppalliance/corosio
10 : //
11 :
12 : #ifndef BOOST_COROSIO_RESOLVER_HPP
13 : #define BOOST_COROSIO_RESOLVER_HPP
14 :
15 : #include <boost/corosio/detail/config.hpp>
16 : #include <boost/corosio/detail/op_base.hpp>
17 : #include <boost/corosio/endpoint.hpp>
18 : #include <boost/corosio/io/io_object.hpp>
19 : #include <boost/capy/io_result.hpp>
20 : #include <boost/capy/ex/executor_ref.hpp>
21 : #include <boost/capy/ex/execution_context.hpp>
22 : #include <boost/capy/ex/io_env.hpp>
23 : #include <boost/capy/concept/executor.hpp>
24 :
25 : #include <system_error>
26 :
27 : #include <cassert>
28 : #include <concepts>
29 : #include <coroutine>
30 : #include <stop_token>
31 : #include <string>
32 : #include <string_view>
33 : #include <vector>
34 : #include <type_traits>
35 :
36 : namespace boost::corosio {
37 :
38 : /** Bitmask flags for resolver queries.
39 :
40 : These flags correspond to the hints parameter of `getaddrinfo`.
41 : */
42 : enum class resolve_flags : unsigned int
43 : {
44 : /// No flags.
45 : none = 0,
46 :
47 : /// Indicate that returned endpoint is intended for use as a locally
48 : /// bound socket endpoint.
49 : passive = 0x01,
50 :
51 : /// Host name should be treated as a numeric string defining an IPv4
52 : /// or IPv6 address and no name resolution should be attempted.
53 : numeric_host = 0x04,
54 :
55 : /// Service name should be treated as a numeric string defining a port
56 : /// number and no name resolution should be attempted.
57 : numeric_service = 0x08,
58 :
59 : /// Only return IPv4 addresses if a non-loopback IPv4 address is
60 : /// configured for the system. Only return IPv6 addresses if a
61 : /// non-loopback IPv6 address is configured for the system.
62 : address_configured = 0x20,
63 :
64 : /// If the query protocol family is specified as IPv6, return
65 : /// IPv4-mapped IPv6 addresses on finding no IPv6 addresses.
66 : v4_mapped = 0x800,
67 :
68 : /// If used with v4_mapped, return all matching IPv6 and IPv4 addresses.
69 : all_matching = 0x100
70 : };
71 :
72 : /** Combine two `resolve_flags`. */
73 : inline resolve_flags
74 HIT 17 : operator|(resolve_flags a, resolve_flags b) noexcept
75 : {
76 : return static_cast<resolve_flags>(
77 17 : static_cast<unsigned int>(a) | static_cast<unsigned int>(b));
78 : }
79 :
80 : /** Combine two `resolve_flags`. */
81 : inline resolve_flags&
82 1 : operator|=(resolve_flags& a, resolve_flags b) noexcept
83 : {
84 1 : a = a | b;
85 1 : return a;
86 : }
87 :
88 : /** Intersect two `resolve_flags`. */
89 : inline resolve_flags
90 205 : operator&(resolve_flags a, resolve_flags b) noexcept
91 : {
92 : return static_cast<resolve_flags>(
93 205 : static_cast<unsigned int>(a) & static_cast<unsigned int>(b));
94 : }
95 :
96 : /** Intersect two `resolve_flags`. */
97 : inline resolve_flags&
98 1 : operator&=(resolve_flags& a, resolve_flags b) noexcept
99 : {
100 1 : a = a & b;
101 1 : return a;
102 : }
103 :
104 : /** Bitmask flags for reverse resolver queries.
105 :
106 : These flags correspond to the flags parameter of `getnameinfo`.
107 : */
108 : enum class reverse_flags : unsigned int
109 : {
110 : /// No flags.
111 : none = 0,
112 :
113 : /// Return the numeric form of the hostname instead of its name.
114 : numeric_host = 0x01,
115 :
116 : /// Return the numeric form of the service name instead of its name.
117 : numeric_service = 0x02,
118 :
119 : /// Return an error if the hostname cannot be resolved.
120 : name_required = 0x04,
121 :
122 : /// Lookup for datagram (UDP) service instead of stream (TCP).
123 : datagram_service = 0x08
124 : };
125 :
126 : /** Combine two `reverse_flags`. */
127 : inline reverse_flags
128 9 : operator|(reverse_flags a, reverse_flags b) noexcept
129 : {
130 : return static_cast<reverse_flags>(
131 9 : static_cast<unsigned int>(a) | static_cast<unsigned int>(b));
132 : }
133 :
134 : /** Combine two `reverse_flags`. */
135 : inline reverse_flags&
136 1 : operator|=(reverse_flags& a, reverse_flags b) noexcept
137 : {
138 1 : a = a | b;
139 1 : return a;
140 : }
141 :
142 : /** Intersect two `reverse_flags`. */
143 : inline reverse_flags
144 75 : operator&(reverse_flags a, reverse_flags b) noexcept
145 : {
146 : return static_cast<reverse_flags>(
147 75 : static_cast<unsigned int>(a) & static_cast<unsigned int>(b));
148 : }
149 :
150 : /** Intersect two `reverse_flags`. */
151 : inline reverse_flags&
152 1 : operator&=(reverse_flags& a, reverse_flags b) noexcept
153 : {
154 1 : a = a & b;
155 1 : return a;
156 : }
157 :
158 : /** The name of an endpoint.
159 :
160 : Reverse resolution translates an endpoint into its symbolic
161 : spelling: the host name and the service name. Both fields carry
162 : resolved data; the endpoint they name is the one the caller
163 : passed to `resolve`.
164 : */
165 : struct endpoint_name
166 : {
167 : /// The resolved host name.
168 : std::string host_name;
169 :
170 : /// The resolved service name.
171 : std::string service_name;
172 : };
173 :
174 : /** Resolves host names and services to endpoints, from a coroutine.
175 :
176 : This class provides asynchronous DNS resolution operations that return
177 : awaitable types. Each operation participates in the affine awaitable
178 : protocol, ensuring coroutines resume on the correct executor.
179 :
180 : @par Thread Safety
181 : Distinct objects: Safe.@n
182 : Shared objects: Unsafe. A resolver must not have concurrent resolve
183 : operations.
184 :
185 : @par Semantics
186 : Wraps platform DNS resolution (`getaddrinfo`/`getnameinfo`).
187 : Operations dispatch to OS resolver APIs via the `io_context`
188 : thread pool.
189 :
190 : @par Example
191 : @par !example resolver
192 : */
193 : class BOOST_COROSIO_DECL resolver : public io_object
194 : {
195 : struct resolve_awaitable
196 : : detail::value_op_base<resolve_awaitable, std::vector<endpoint>>
197 : {
198 : private:
199 : friend resolver;
200 :
201 29 : resolve_awaitable(
202 : resolver& r,
203 : std::string_view host,
204 : std::string_view service,
205 : resolve_flags flags) noexcept
206 58 : : r_(r)
207 58 : , host_(host)
208 58 : , service_(service)
209 29 : , flags_(flags)
210 : {
211 29 : }
212 :
213 : friend detail::value_op_base<resolve_awaitable, std::vector<endpoint>>;
214 : resolver& r_;
215 : std::string host_;
216 : std::string service_;
217 : resolve_flags flags_;
218 :
219 : std::coroutine_handle<>
220 28 : dispatch(std::coroutine_handle<> h, capy::executor_ref ex) const
221 : {
222 84 : return r_.get().resolve(
223 84 : h, ex, host_, service_, flags_, token_, &ec_, &value_);
224 : }
225 : };
226 :
227 : struct resolve_host_awaitable
228 : : detail::value_op_base<resolve_host_awaitable, std::vector<endpoint>>
229 : {
230 : private:
231 : friend resolver;
232 :
233 6 : resolve_host_awaitable(
234 : resolver& r, std::string_view host, resolve_flags flags) noexcept
235 12 : : r_(r)
236 12 : , host_(host)
237 6 : , flags_(flags)
238 : {
239 6 : }
240 :
241 : friend detail::
242 : value_op_base<resolve_host_awaitable, std::vector<endpoint>>;
243 : resolver& r_;
244 : std::string host_;
245 : resolve_flags flags_;
246 :
247 : std::coroutine_handle<>
248 5 : dispatch(std::coroutine_handle<> h, capy::executor_ref ex) const
249 : {
250 : // An empty service reaches the system resolver as null,
251 : // which is the host-only query
252 15 : return r_.get().resolve(
253 15 : h, ex, host_, {}, flags_, token_, &ec_, &value_);
254 : }
255 :
256 : public:
257 : // Shadows the base: the endpoint result is reshaped into
258 : // the honest address list
259 : [[nodiscard]] capy::io_result<std::vector<ip_address>>
260 6 : await_resume() const
261 : {
262 6 : std::vector<ip_address> addrs;
263 6 : addrs.reserve(value_.size());
264 9 : for (auto const& entry : value_)
265 : {
266 3 : auto a = entry.address();
267 3 : bool duplicate = false;
268 3 : for (auto const& seen : addrs)
269 : {
270 MIS 0 : if (seen == a)
271 : {
272 0 : duplicate = true;
273 0 : break;
274 : }
275 : }
276 : // The same address can come back more than once
277 : // (mixed name sources, repeated records); each
278 : // address is reported once
279 HIT 3 : if (!duplicate)
280 3 : addrs.push_back(a);
281 : }
282 12 : return {ec_, std::move(addrs)};
283 6 : }
284 : };
285 :
286 : struct reverse_resolve_awaitable
287 : : detail::value_op_base<reverse_resolve_awaitable, endpoint_name>
288 : {
289 : private:
290 : friend resolver;
291 :
292 20 : reverse_resolve_awaitable(
293 : resolver& r, endpoint const& ep, reverse_flags flags) noexcept
294 40 : : r_(r)
295 20 : , ep_(ep)
296 20 : , flags_(flags)
297 : {
298 20 : }
299 :
300 : friend detail::value_op_base<reverse_resolve_awaitable, endpoint_name>;
301 :
302 : resolver& r_;
303 : endpoint ep_;
304 : reverse_flags flags_;
305 :
306 : std::coroutine_handle<>
307 19 : dispatch(std::coroutine_handle<> h, capy::executor_ref ex) const
308 : {
309 38 : return r_.get().reverse_resolve(
310 38 : h, ex, ep_, flags_, token_, &ec_, &value_);
311 : }
312 : };
313 :
314 : public:
315 : /** Destructor.
316 :
317 : Cancels any pending operations.
318 : */
319 : ~resolver() override;
320 :
321 : /** Construct a resolver from an execution context.
322 :
323 : @param ctx The execution context that owns this resolver.
324 : */
325 : explicit resolver(capy::execution_context& ctx);
326 :
327 : /** Construct a resolver from an executor.
328 :
329 : The resolver is associated with the executor's context.
330 :
331 : @param ex The executor whose context owns the resolver.
332 : */
333 : template<class Ex>
334 : requires(!std::same_as<std::remove_cvref_t<Ex>, resolver>) &&
335 : capy::Executor<Ex>
336 1 : explicit resolver(Ex const& ex) : resolver(ex.context())
337 : {
338 1 : }
339 :
340 : /** Move constructor.
341 :
342 : Transfers ownership of the resolver resources. After the move,
343 : @p other is in a moved-from state and may only be destroyed or
344 : assigned to.
345 :
346 : @param other The resolver to move from.
347 :
348 : @pre No awaitables returned by @p other's `resolve` methods
349 : exist.
350 : @pre The execution context associated with @p other must
351 : outlive this resolver.
352 : */
353 2 : resolver(resolver&& other) noexcept : io_object(std::move(other)) {}
354 :
355 : /** Move assignment operator.
356 :
357 : Destroys the current implementation and transfers ownership
358 : from @p other. After the move, @p other is in a moved-from
359 : state and may only be destroyed or assigned to.
360 :
361 : @param other The resolver to move from.
362 :
363 : @pre No awaitables returned by either `*this` or @p other's
364 : `resolve` methods exist.
365 : @pre The execution context associated with @p other must
366 : outlive this resolver.
367 :
368 : @return Reference to this resolver.
369 : */
370 2 : resolver& operator=(resolver&& other) noexcept
371 : {
372 2 : if (this != &other)
373 2 : h_ = std::move(other.h_);
374 2 : return *this;
375 : }
376 :
377 : /// Copy construction is disabled; the handle is uniquely owned.
378 : resolver(resolver const&) = delete;
379 : /// Copy assignment is disabled; the handle is uniquely owned.
380 : resolver& operator=(resolver const&) = delete;
381 :
382 : /** Initiate an asynchronous resolve operation.
383 :
384 : Resolves the host and service names into a list of endpoints.
385 :
386 : This resolver must outlive the returned awaitable.
387 :
388 : @param host A string identifying a location. May be a descriptive
389 : name or a numeric address string.
390 :
391 : @param service A string identifying the requested service. This may
392 : be a descriptive name or a numeric string corresponding to a
393 : port number.
394 :
395 : @return An awaitable that completes with
396 : `io_result<std::vector<endpoint>>`.
397 :
398 : @par Example
399 : @par !example forward_resolve
400 : */
401 13 : [[nodiscard]] auto resolve(std::string_view host, std::string_view service)
402 : {
403 13 : return resolve_awaitable(*this, host, service, resolve_flags::none);
404 : }
405 :
406 : /** Initiate an asynchronous host-only resolve operation.
407 :
408 : Resolves a host name into its addresses, with no service or
409 : port involved — the query `getaddrinfo` performs with a null
410 : service. Use this when the host and port travel separately,
411 : as they do in most configuration.
412 :
413 : Each address appears once in the result even when the query
414 : reports it more than once, and link-local results keep
415 : their zone.
416 :
417 : @param host The host name or numeric address string.
418 :
419 : @return An awaitable that completes with
420 : `io_result<std::vector<ip_address>>`.
421 :
422 : @par Example
423 : @par !example host_only_resolve
424 : */
425 3 : [[nodiscard]] auto resolve(std::string_view host)
426 : {
427 3 : return resolve_host_awaitable(*this, host, resolve_flags::none);
428 : }
429 :
430 : /** Initiate an asynchronous host-only resolve operation with flags.
431 :
432 : @param host The host name or numeric address string.
433 : @param flags Resolution behavior flags.
434 :
435 : @return An awaitable that completes with
436 : `io_result<std::vector<ip_address>>`.
437 : */
438 3 : [[nodiscard]] auto resolve(std::string_view host, resolve_flags flags)
439 : {
440 3 : return resolve_host_awaitable(*this, host, flags);
441 : }
442 :
443 : /** Initiate an asynchronous resolve operation with flags.
444 :
445 : Resolves the host and service names into a list of endpoints.
446 :
447 : This resolver must outlive the returned awaitable.
448 :
449 : @param host A string identifying a location.
450 :
451 : @param service A string identifying the requested service.
452 :
453 : @param flags Flags controlling resolution behavior.
454 :
455 : @return An awaitable that completes with
456 : `io_result<std::vector<endpoint>>`.
457 : */
458 16 : [[nodiscard]] auto resolve(
459 : std::string_view host, std::string_view service, resolve_flags flags)
460 : {
461 16 : return resolve_awaitable(*this, host, service, flags);
462 : }
463 :
464 : /** Initiate an asynchronous reverse resolve operation.
465 :
466 : Resolves an endpoint into a hostname and service name using
467 : reverse DNS lookup (PTR record query).
468 :
469 : This resolver must outlive the returned awaitable.
470 :
471 : @param ep The endpoint to resolve.
472 :
473 : @return An awaitable that completes with
474 : `io_result<endpoint_name>`.
475 :
476 : @par Example
477 : @par !example reverse_resolve
478 : */
479 11 : [[nodiscard]] auto resolve(endpoint const& ep)
480 : {
481 11 : return reverse_resolve_awaitable(*this, ep, reverse_flags::none);
482 : }
483 :
484 : /** Initiate an asynchronous reverse resolve operation with flags.
485 :
486 : Resolves an endpoint into a hostname and service name using
487 : reverse DNS lookup (PTR record query).
488 :
489 : This resolver must outlive the returned awaitable.
490 :
491 : @param ep The endpoint to resolve.
492 :
493 : @param flags Flags controlling resolution behavior. See reverse_flags.
494 :
495 : @return An awaitable that completes with
496 : `io_result<endpoint_name>`.
497 : */
498 9 : [[nodiscard]] auto resolve(endpoint const& ep, reverse_flags flags)
499 : {
500 9 : return reverse_resolve_awaitable(*this, ep, flags);
501 : }
502 :
503 : /** Cancel any pending asynchronous operations.
504 :
505 : A resolve transfers no bytes, so a cancellation always wins. An
506 : operation reports `errc::operation_canceled` even when the lookup
507 : had already completed when the cancellation landed. Check
508 : `ec == cond::canceled` for a portable comparison.
509 : */
510 : void cancel() noexcept;
511 :
512 : public:
513 : /** Define backend hooks for DNS resolution operations.
514 :
515 : Platform backends derive from this to implement forward and
516 : reverse DNS resolution via `getaddrinfo`/`getnameinfo`.
517 : */
518 : struct implementation : io_object::implementation
519 : {
520 : /** Initiate an asynchronous forward DNS resolution.
521 :
522 : @param h Coroutine handle to resume on completion.
523 : @param ex Executor for dispatching the completion.
524 : @param host The host name or address literal to resolve.
525 : @param service The service name or port number.
526 : @param flags Flags controlling the lookup.
527 : @param token Stop token for cancellation.
528 : @param ec Output error code.
529 : @param results Output resolver results.
530 :
531 : @return Coroutine handle to resume immediately.
532 : */
533 : virtual std::coroutine_handle<> resolve(
534 : std::coroutine_handle<> h,
535 : capy::executor_ref ex,
536 : std::string_view host,
537 : std::string_view service,
538 : resolve_flags flags,
539 : std::stop_token token,
540 : std::error_code* ec,
541 : std::vector<endpoint>* results) = 0;
542 :
543 : /** Initiate an asynchronous reverse DNS resolution.
544 :
545 : @param h Coroutine handle to resume on completion.
546 : @param ex Executor for dispatching the completion.
547 : @param ep The endpoint to resolve.
548 : @param flags Flags controlling the lookup.
549 : @param token Stop token for cancellation.
550 : @param ec Output error code.
551 : @param result Output reverse-resolution result.
552 :
553 : @return Coroutine handle to resume immediately.
554 : */
555 : virtual std::coroutine_handle<> reverse_resolve(
556 : std::coroutine_handle<> h,
557 : capy::executor_ref ex,
558 : endpoint const& ep,
559 : reverse_flags flags,
560 : std::stop_token token,
561 : std::error_code* ec,
562 : endpoint_name* result) = 0;
563 :
564 : /// Cancel pending resolve operations.
565 : virtual void cancel() noexcept = 0;
566 : };
567 :
568 : protected:
569 : /** Adopt an existing handle.
570 :
571 : @param h The handle the resolver takes ownership of.
572 : */
573 : explicit resolver(handle h) noexcept : io_object(std::move(h)) {}
574 :
575 : private:
576 59 : inline implementation& get() const noexcept
577 : {
578 59 : return *static_cast<implementation*>(h_.get());
579 : }
580 : };
581 :
582 : } // namespace boost::corosio
583 :
584 : #endif
|