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

97.4% Lines (75/77) 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 return r_.get().resolve(h, ex, host_, service_, flags_, token_, &ec_, &results_);
247 }
248
249 template<typename Ex>
250 16 auto await_suspend(
251 std::coroutine_handle<> h,
252 Ex const& ex,
253 std::stop_token token) -> std::coroutine_handle<>
254 {
255 16 token_ = std::move(token);
256
1/1
✓ Branch 5 taken 16 times.
16 return r_.get().resolve(h, ex, host_, service_, flags_, token_, &ec_, &results_);
257 }
258 };
259
260 struct reverse_resolve_awaitable
261 {
262 resolver& r_;
263 endpoint ep_;
264 reverse_flags flags_;
265 std::stop_token token_;
266 mutable std::error_code ec_;
267 mutable reverse_resolver_result result_;
268
269 10 reverse_resolve_awaitable(
270 resolver& r,
271 endpoint const& ep,
272 reverse_flags flags) noexcept
273 10 : r_(r)
274 10 , ep_(ep)
275 10 , flags_(flags)
276 {
277 10 }
278
279 10 bool await_ready() const noexcept
280 {
281 10 return token_.stop_requested();
282 }
283
284 10 capy::io_result<reverse_resolver_result> await_resume() const noexcept
285 {
286
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
10 if (token_.stop_requested())
287 return {make_error_code(std::errc::operation_canceled), {}};
288 10 return {ec_, std::move(result_)};
289 10 }
290
291 template<typename Ex>
292 auto await_suspend(
293 std::coroutine_handle<> h,
294 Ex const& ex) -> std::coroutine_handle<>
295 {
296 return r_.get().reverse_resolve(h, ex, ep_, flags_, token_, &ec_, &result_);
297 }
298
299 template<typename Ex>
300 10 auto await_suspend(
301 std::coroutine_handle<> h,
302 Ex const& ex,
303 std::stop_token token) -> std::coroutine_handle<>
304 {
305 10 token_ = std::move(token);
306
1/1
✓ Branch 3 taken 10 times.
10 return r_.get().reverse_resolve(h, ex, ep_, flags_, token_, &ec_, &result_);
307 }
308 };
309
310 public:
311 /** Destructor.
312
313 Cancels any pending operations.
314 */
315 ~resolver();
316
317 /** Construct a resolver from an execution context.
318
319 @param ctx The execution context that will own this resolver.
320 */
321 explicit resolver(capy::execution_context& ctx);
322
323 /** Construct a resolver from an executor.
324
325 The resolver is associated with the executor's context.
326
327 @param ex The executor whose context will own the resolver.
328 */
329 template<class Ex>
330 requires (!std::same_as<std::remove_cvref_t<Ex>, resolver>) &&
331 capy::Executor<Ex>
332 1 explicit resolver(Ex const& ex)
333 1 : resolver(ex.context())
334 {
335 1 }
336
337 /** Move constructor.
338
339 Transfers ownership of the resolver resources.
340
341 @param other The resolver to move from.
342 */
343 1 resolver(resolver&& other) noexcept
344 1 : io_object(other.context())
345 {
346 1 impl_ = other.impl_;
347 1 other.impl_ = nullptr;
348 1 }
349
350 /** Move assignment operator.
351
352 Cancels any existing operations and transfers ownership.
353 The source and destination must share the same execution context.
354
355 @param other The resolver to move from.
356
357 @return Reference to this resolver.
358
359 @throws std::logic_error if the resolvers have different
360 execution contexts.
361 */
362 2 resolver& operator=(resolver&& other)
363 {
364
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (this != &other)
365 {
366
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
2 if (ctx_ != other.ctx_)
367 1 detail::throw_logic_error(
368 "cannot move resolver across execution contexts");
369 1 cancel();
370 1 impl_ = other.impl_;
371 1 other.impl_ = nullptr;
372 }
373 1 return *this;
374 }
375
376 resolver(resolver const&) = delete;
377 resolver& operator=(resolver const&) = delete;
378
379 /** Initiate an asynchronous resolve operation.
380
381 Resolves the host and service names into a list of endpoints.
382
383 @param host A string identifying a location. May be a descriptive
384 name or a numeric address string.
385
386 @param service A string identifying the requested service. This may
387 be a descriptive name or a numeric string corresponding to a
388 port number.
389
390 @return An awaitable that completes with `io_result<resolver_results>`.
391
392 @par Example
393 @code
394 auto [ec, results] = co_await r.resolve("www.example.com", "https");
395 @endcode
396 */
397 5 auto resolve(
398 std::string_view host,
399 std::string_view service)
400 {
401 5 return resolve_awaitable(*this, host, service, resolve_flags::none);
402 }
403
404 /** Initiate an asynchronous resolve operation with flags.
405
406 Resolves the host and service names into a list of endpoints.
407
408 @param host A string identifying a location.
409
410 @param service A string identifying the requested service.
411
412 @param flags Flags controlling resolution behavior.
413
414 @return An awaitable that completes with `io_result<resolver_results>`.
415 */
416 11 auto resolve(
417 std::string_view host,
418 std::string_view service,
419 resolve_flags flags)
420 {
421 11 return resolve_awaitable(*this, host, service, flags);
422 }
423
424 /** Initiate an asynchronous reverse resolve operation.
425
426 Resolves an endpoint into a hostname and service name using
427 reverse DNS lookup (PTR record query).
428
429 @param ep The endpoint to resolve.
430
431 @return An awaitable that completes with
432 `io_result<reverse_resolver_result>`.
433
434 @par Example
435 @code
436 endpoint ep(ipv4_address({127, 0, 0, 1}), 80);
437 auto [ec, result] = co_await r.resolve(ep);
438 if (!ec)
439 std::cout << result.host_name() << ":" << result.service_name();
440 @endcode
441 */
442 3 auto resolve(endpoint const& ep)
443 {
444 3 return reverse_resolve_awaitable(*this, ep, reverse_flags::none);
445 }
446
447 /** Initiate an asynchronous reverse resolve operation with flags.
448
449 Resolves an endpoint into a hostname and service name using
450 reverse DNS lookup (PTR record query).
451
452 @param ep The endpoint to resolve.
453
454 @param flags Flags controlling resolution behavior. See reverse_flags.
455
456 @return An awaitable that completes with
457 `io_result<reverse_resolver_result>`.
458 */
459 7 auto resolve(endpoint const& ep, reverse_flags flags)
460 {
461 7 return reverse_resolve_awaitable(*this, ep, flags);
462 }
463
464 /** Cancel any pending asynchronous operations.
465
466 All outstanding operations complete with `errc::operation_canceled`.
467 Check `ec == cond::canceled` for portable comparison.
468 */
469 void cancel();
470
471 public:
472 struct resolver_impl : io_object_impl
473 {
474 virtual std::coroutine_handle<> resolve(
475 std::coroutine_handle<>,
476 capy::executor_ref,
477 std::string_view host,
478 std::string_view service,
479 resolve_flags flags,
480 std::stop_token,
481 std::error_code*,
482 resolver_results*) = 0;
483
484 virtual std::coroutine_handle<> reverse_resolve(
485 std::coroutine_handle<>,
486 capy::executor_ref,
487 endpoint const& ep,
488 reverse_flags flags,
489 std::stop_token,
490 std::error_code*,
491 reverse_resolver_result*) = 0;
492
493 virtual void cancel() noexcept = 0;
494 };
495
496 private:
497 31 inline resolver_impl& get() const noexcept
498 {
499 31 return *static_cast<resolver_impl*>(impl_);
500 }
501 };
502
503 } // namespace boost::corosio
504
505 #endif
506