libs/corosio/include/boost/corosio/resolver.hpp

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