include/boost/corosio/resolver.hpp

97.3% Lines (71/73) 100.0% Functions (24/24) 70.0% Branches (7/10)
include/boost/corosio/resolver.hpp
Line Branch Hits 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
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
16 if (token_.stop_requested())
238 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
1/1
✓ Branch 5 taken 16 times.
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
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
10 if (token_.stop_requested())
278 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
1/1
✓ Branch 3 taken 10 times.
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(std::move(other))
326 {
327 1 }
328
329 /** Move assignment operator.
330
331 Cancels any existing operations and transfers ownership.
332 The source and destination must share the same execution context.
333
334 @param other The resolver to move from.
335
336 @return Reference to this resolver.
337
338 @throws std::logic_error if the resolvers have different
339 execution contexts.
340 */
341 2 resolver& operator=(resolver&& other)
342 {
343
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (this != &other)
344 {
345
2/2
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 1 time.
2 if (&context() != &other.context())
346 1 detail::throw_logic_error(
347 "cannot move resolver across execution contexts");
348 1 h_ = std::move(other.h_);
349 }
350 1 return *this;
351 }
352
353 resolver(resolver const&) = delete;
354 resolver& operator=(resolver const&) = delete;
355
356 /** Initiate an asynchronous resolve operation.
357
358 Resolves the host and service names into a list of endpoints.
359
360 @param host A string identifying a location. May be a descriptive
361 name or a numeric address string.
362
363 @param service A string identifying the requested service. This may
364 be a descriptive name or a numeric string corresponding to a
365 port number.
366
367 @return An awaitable that completes with `io_result<resolver_results>`.
368
369 @par Example
370 @code
371 auto [ec, results] = co_await r.resolve("www.example.com", "https");
372 @endcode
373 */
374 5 auto resolve(
375 std::string_view host,
376 std::string_view service)
377 {
378 5 return resolve_awaitable(*this, host, service, resolve_flags::none);
379 }
380
381 /** Initiate an asynchronous resolve operation with flags.
382
383 Resolves the host and service names into a list of endpoints.
384
385 @param host A string identifying a location.
386
387 @param service A string identifying the requested service.
388
389 @param flags Flags controlling resolution behavior.
390
391 @return An awaitable that completes with `io_result<resolver_results>`.
392 */
393 11 auto resolve(
394 std::string_view host,
395 std::string_view service,
396 resolve_flags flags)
397 {
398 11 return resolve_awaitable(*this, host, service, flags);
399 }
400
401 /** Initiate an asynchronous reverse resolve operation.
402
403 Resolves an endpoint into a hostname and service name using
404 reverse DNS lookup (PTR record query).
405
406 @param ep The endpoint to resolve.
407
408 @return An awaitable that completes with
409 `io_result<reverse_resolver_result>`.
410
411 @par Example
412 @code
413 endpoint ep(ipv4_address({127, 0, 0, 1}), 80);
414 auto [ec, result] = co_await r.resolve(ep);
415 if (!ec)
416 std::cout << result.host_name() << ":" << result.service_name();
417 @endcode
418 */
419 3 auto resolve(endpoint const& ep)
420 {
421 3 return reverse_resolve_awaitable(*this, ep, reverse_flags::none);
422 }
423
424 /** Initiate an asynchronous reverse resolve operation with flags.
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 @param flags Flags controlling resolution behavior. See reverse_flags.
432
433 @return An awaitable that completes with
434 `io_result<reverse_resolver_result>`.
435 */
436 7 auto resolve(endpoint const& ep, reverse_flags flags)
437 {
438 7 return reverse_resolve_awaitable(*this, ep, flags);
439 }
440
441 /** Cancel any pending asynchronous operations.
442
443 All outstanding operations complete with `errc::operation_canceled`.
444 Check `ec == cond::canceled` for portable comparison.
445 */
446 void cancel();
447
448 public:
449 struct implementation : io_object::implementation
450 {
451 virtual std::coroutine_handle<> resolve(
452 std::coroutine_handle<>,
453 capy::executor_ref,
454 std::string_view host,
455 std::string_view service,
456 resolve_flags flags,
457 std::stop_token,
458 std::error_code*,
459 resolver_results*) = 0;
460
461 virtual std::coroutine_handle<> reverse_resolve(
462 std::coroutine_handle<>,
463 capy::executor_ref,
464 endpoint const& ep,
465 reverse_flags flags,
466 std::stop_token,
467 std::error_code*,
468 reverse_resolver_result*) = 0;
469
470 virtual void cancel() noexcept = 0;
471 };
472
473 private:
474 30 inline implementation& get() const noexcept
475 {
476 30 return *static_cast<implementation*>(h_.get());
477 }
478 };
479
480 } // namespace boost::corosio
481
482 #endif
483