roar
Loading...
Searching...
No Matches
client.hpp
Go to the documentation of this file.
1#pragma once
2
5#include <roar/error.hpp>
6#include <roar/request.hpp>
7#include <roar/response.hpp>
9
10#include <boost/asio/any_io_executor.hpp>
11#include <boost/asio/ssl/context.hpp>
12#include <boost/beast/http/field.hpp>
13#include <boost/beast/http/write.hpp>
14#include <boost/beast/http/read.hpp>
15#include <boost/beast/ssl/ssl_stream.hpp>
16#include <boost/beast/core/tcp_stream.hpp>
17#include <boost/asio/ip/tcp.hpp>
18#include <promise-cpp/promise.hpp>
19
20#include <memory>
21#include <optional>
22#include <variant>
23#include <chrono>
24#include <unordered_map>
25#include <string_view>
26#include <functional>
27#include <type_traits>
28#include <future>
29#include <any>
30
31namespace Roar
32{
33 class Client : public std::enable_shared_from_this<Client>
34 {
35 public:
36 constexpr static std::chrono::seconds defaultTimeout{10};
37
39 {
41 boost::asio::ssl::context sslContext;
42
44 boost::asio::ssl::verify_mode sslVerifyMode = boost::asio::ssl::verify_peer;
45
49 std::function<bool(bool, boost::asio::ssl::verify_context&)> sslVerifyCallback;
50 };
51
53 {
55 boost::asio::any_io_executor executor;
56 std::optional<SslOptions> sslOptions = std::nullopt;
57 };
58
60
62
71 send(std::string message, std::chrono::seconds timeout = defaultTimeout);
72
81 read(std::chrono::seconds timeout = defaultTimeout);
82
89 template <typename T>
90 void attachState(std::string const& tag, T&& state)
91 {
92 attachedState_[tag] = std::forward<T>(state);
93 }
94
102 template <typename T, typename... ConstructionArgs>
103 void emplaceState(std::string const& tag, ConstructionArgs&&... args)
104 {
105 attachedState_[tag] = std::make_any<T>(std::forward<ConstructionArgs>(args)...);
106 }
107
115 template <typename T>
116 T& state(std::string const& tag)
117 {
118 return std::any_cast<T&>(attachedState_.at(tag));
119 }
120
126 void removeState(std::string const& tag)
127 {
128 attachedState_.erase(tag);
129 }
130
137 template <typename BodyT>
139 request(Request<BodyT>&& request, std::chrono::seconds timeout = defaultTimeout)
140 {
141 return promise::newPromise([&, this](promise::Defer d) mutable {
142 const auto host = request.host();
143 const auto port = request.port();
144 doResolve(
145 host,
146 port,
147 [weak = weak_from_this(), timeout, request = std::move(request), d = std::move(d)](
148 boost::beast::error_code ec, boost::asio::ip::tcp::resolver::results_type results) mutable {
149 auto self = weak.lock();
150 if (!self)
151 return d.reject(Error{.error = ec, .additionalInfo = "Client is no longer alive."});
152
153 if (ec)
154 return d.reject(Error{.error = ec, .additionalInfo = "DNS resolve failed."});
155
156 self->withLowerLayerDo([&](auto& socket) {
157 socket.expires_after(timeout);
158 socket.async_connect(
159 results,
160 [weak = self->weak_from_this(),
161 d = std::move(d),
162 request = std::move(request),
163 timeout](
164 boost::beast::error_code ec,
165 boost::asio::ip::tcp::resolver::results_type::endpoint_type endpoint) mutable {
166 auto self = weak.lock();
167 if (!self)
168 return d.reject(
169 Error{.error = ec, .additionalInfo = "Client is no longer alive."});
170
171 if (ec)
172 return d.reject(Error{.error = ec, .additionalInfo = "TCP connect failed."});
173
174 self->endpoint_ = endpoint;
175
176 self->onConnect(std::move(request), std::move(d), timeout);
177 });
178 });
179 });
180 });
181 }
182
194 template <typename ResponseBodyT>
196 boost::beast::http::response_parser<ResponseBodyT>& parser,
197 std::chrono::seconds timeout = defaultTimeout)
198 {
199 return promise::newPromise([&, this](promise::Defer d) mutable {
200 withLowerLayerDo([timeout](auto& socket) {
201 socket.expires_after(timeout);
202 });
203 withStreamDo([this, timeout, d = std::move(d), &parser](auto& socket) mutable {
204 boost::beast::http::async_read_header(
205 socket,
206 *buffer_,
207 parser,
208 [weak = weak_from_this(), buffer = this->buffer_, d = std::move(d), timeout, &parser](
209 boost::beast::error_code ec, std::size_t) mutable {
210 auto self = weak.lock();
211 if (!self)
212 return d.reject(Error{.error = ec, .additionalInfo = "Client is no longer alive."});
213
214 if (ec)
215 return d.reject(Error{.error = ec, .additionalInfo = "HTTP read failed."});
216
217 d.resolve();
218 });
219 });
220 });
221 }
222
232 template <typename ResponseBodyT>
236 readResponse(std::chrono::seconds timeout = defaultTimeout)
237 {
238 return promise::newPromise([this, timeout](promise::Defer d) mutable {
239 withLowerLayerDo([timeout](auto& socket) {
240 socket.expires_after(timeout);
241 });
242 withStreamDo([this, d = std::move(d)](auto& socket) mutable {
243 struct Context
244 {
245 Response<ResponseBodyT> response{};
246 };
247 auto context = std::make_shared<Context>();
248 boost::beast::http::async_read(
249 socket,
250 *buffer_,
251 context->response.response(),
252 [weak = weak_from_this(), buffer = this->buffer_, d = std::move(d), context](
253 boost::beast::error_code ec, std::size_t) mutable {
254 auto self = weak.lock();
255 if (!self)
256 return d.reject(Error{.error = ec, .additionalInfo = "Client is no longer alive."});
257
258 if (ec)
259 return d.reject(Error{.error = ec, .additionalInfo = "HTTP read failed."});
260
261 d.resolve(Detail::ref(context->response));
262 });
263 });
264 });
265 }
266
275 template <typename ResponseBodyT>
277 boost::beast::http::response_parser<ResponseBodyT>& parser,
278 std::chrono::seconds timeout = defaultTimeout)
279 {
280 return promise::newPromise([&, this](promise::Defer d) mutable {
281 withLowerLayerDo([timeout](auto& socket) {
282 socket.expires_after(timeout);
283 });
284 withStreamDo([this, timeout, d = std::move(d), &parser](auto& socket) mutable {
285 boost::beast::http::async_read(
286 socket,
287 *buffer_,
288 parser,
289 [weak = weak_from_this(), buffer = this->buffer_, d = std::move(d), timeout, &parser](
290 boost::beast::error_code ec, std::size_t) mutable {
291 auto self = weak.lock();
292 if (!self)
293 return d.reject(Error{.error = ec, .additionalInfo = "Client is no longer alive."});
294
295 if (ec)
296 return d.reject(Error{.error = ec, .additionalInfo = "HTTP read failed."});
297
298 d.resolve();
299 });
300 });
301 });
302 }
303
304 template <typename ResponseBodyT, typename RequestBodyT>
308 requestAndReadResponse(Request<RequestBodyT>&& request, std::chrono::seconds timeout = defaultTimeout)
309 {
310 return promise::newPromise([r = std::move(request), timeout, this](promise::Defer d) mutable {
311 this->request(std::move(r), timeout)
312 .then([weak = weak_from_this(), timeout, d]() {
313 auto client = weak.lock();
314 if (!client)
315 return d.reject(Error{.additionalInfo = "Client is no longer alive."});
316
317 client->readResponse<ResponseBodyT>(timeout)
318 .then([d](auto& response) {
319 d.resolve(Detail::ref(response));
320 })
321 .fail([d](auto error) {
322 d.reject(std::move(error));
323 });
324 })
325 .fail([d](auto error) {
326 d.reject(std::move(error));
327 });
328 });
329 }
330
332 {
333 return promise::newPromise([&, this](promise::Defer d) mutable {
334 resolver_.cancel();
335 withLowerLayerDo([&](auto& socket) {
336 if (socket.socket().is_open())
337 {
338 socket.cancel();
339 socket.close();
340 }
341 });
342 return d.resolve();
343 });
344 }
345
347 shutdownSsl(std::chrono::seconds timeout = defaultTimeout)
348 {
349 return promise::newPromise([&, this](promise::Defer d) mutable {
350 if (std::holds_alternative<boost::beast::tcp_stream>(socket_))
351 return d.resolve();
352
353 withLowerLayerDo([timeout](auto& socket) {
354 socket.expires_after(timeout);
355 });
356 auto& socket = std::get<boost::beast::ssl_stream<boost::beast::tcp_stream>>(socket_);
357 socket.async_shutdown([d = std::move(d)](boost::beast::error_code ec) mutable {
358 if (ec == boost::asio::error::eof)
359 ec = {};
360
361 if (ec)
362 return d.reject(Error{.error = ec, .additionalInfo = "Stream shutdown failed."});
363
364 d.resolve();
365 });
366 });
367 }
368
369 public:
370 template <typename FunctionT>
371 void withStreamDo(FunctionT&& func)
372 {
373 std::visit(std::forward<FunctionT>(func), socket_);
374 }
375
376 template <typename FunctionT>
377 void withStreamDo(FunctionT&& func) const
378 {
379 std::visit(std::forward<FunctionT>(func), socket_);
380 }
381
382 template <typename FunctionT>
383 void withLowerLayerDo(FunctionT&& func)
384 {
386 socket_,
387 [&func](boost::beast::ssl_stream<boost::beast::tcp_stream>& stream) {
388 return func(stream.next_layer());
389 },
390 [&func](boost::beast::tcp_stream& stream) {
391 return func(stream);
392 });
393 }
394
395 template <typename FunctionT>
396 void withLowerLayerDo(FunctionT&& func) const
397 {
399 socket_,
400 [&func](boost::beast::ssl_stream<boost::beast::tcp_stream> const& stream) {
401 return func(stream.next_layer());
402 },
403 [&func](boost::beast::tcp_stream const& stream) {
404 return func(stream);
405 });
406 }
407
408 private:
409 void doResolve(
410 std::string const& host,
411 std::string const& port,
412 std::function<void(boost::beast::error_code ec, boost::asio::ip::tcp::resolver::results_type results)>
413 onResolve);
414
415 template <typename BodyT>
416 void onConnect(Request<BodyT>&& request, promise::Defer&& d, std::chrono::seconds timeout)
417 {
418 if (std::holds_alternative<boost::beast::ssl_stream<boost::beast::tcp_stream>>(socket_))
419 {
420 auto maybeError = setupSsl(request.host());
421 if (maybeError)
422 return d.reject(*maybeError);
423
424 auto& sslSocket = std::get<boost::beast::ssl_stream<boost::beast::tcp_stream>>(socket_);
425 withLowerLayerDo([&](auto& socket) {
426 socket.expires_after(timeout);
427 });
428 sslSocket.async_handshake(
429 boost::asio::ssl::stream_base::client,
430 [weak = weak_from_this(), d = std::move(d), request = std::move(request), timeout](
431 boost::beast::error_code ec) mutable {
432 auto self = weak.lock();
433 if (!self)
434 return d.reject(Error{.error = ec, .additionalInfo = "Client is no longer alive."});
435
436 if (ec)
437 return d.reject(Error{.error = ec, .additionalInfo = "SSL handshake failed."});
438
439 self->performRequest(std::move(request), std::move(d), timeout);
440 });
441 }
442 else
443 performRequest(std::move(request), std::move(d), timeout);
444 }
445
446 template <typename BodyT>
447 void performRequest(Request<BodyT>&& request, promise::Defer&& d, std::chrono::seconds timeout)
448 {
449 withLowerLayerDo([timeout](auto& socket) {
450 socket.expires_after(timeout);
451 });
452 withStreamDo([this, request, &d](auto& socket) mutable {
453 std::shared_ptr<Request<BodyT>> requestPtr = std::make_shared<Request<BodyT>>(std::move(request));
454 boost::beast::http::async_write(
455 socket,
456 *requestPtr,
457 [weak = weak_from_this(), d = std::move(d), requestPtr](
458 boost::beast::error_code ec, std::size_t) mutable {
459 auto self = weak.lock();
460 if (!self)
461 return d.reject(Error{.error = ec, .additionalInfo = "Client is no longer alive."});
462
463 if (ec)
464 return d.reject(Error{.error = ec, .additionalInfo = "HTTP write failed."});
465
466 d.resolve();
467 });
468 });
469 }
470
471 std::optional<Error> setupSsl(std::string const& host);
472
473 private:
474 std::optional<SslOptions> sslOptions_;
475 boost::asio::ip::tcp::resolver resolver_;
476 std::shared_ptr<boost::beast::flat_buffer> buffer_;
477 std::variant<boost::beast::ssl_stream<boost::beast::tcp_stream>, boost::beast::tcp_stream> socket_;
478 boost::asio::ip::tcp::resolver::results_type::endpoint_type endpoint_;
479 std::unordered_map<std::string, std::any> attachedState_;
480 };
481}
static constexpr int port
Definition main.cpp:16
Definition client.hpp:34
void emplaceState(std::string const &tag, ConstructionArgs &&... args)
Create state in place.
Definition client.hpp:103
void withStreamDo(FunctionT &&func)
Definition client.hpp:371
void withLowerLayerDo(FunctionT &&func)
Definition client.hpp:383
void onConnect(Request< BodyT > &&request, promise::Defer &&d, std::chrono::seconds timeout)
Definition client.hpp:416
void doResolve(std::string const &host, std::string const &port, std::function< void(boost::beast::error_code ec, boost::asio::ip::tcp::resolver::results_type results)> onResolve)
Definition client.cpp:134
std::shared_ptr< boost::beast::flat_buffer > buffer_
Definition client.hpp:476
Detail::PromiseTypeBind< Detail::PromiseTypeBindThen<>, Detail::PromiseTypeBindFail< Error > > readHeader(boost::beast::http::response_parser< ResponseBodyT > &parser, std::chrono::seconds timeout=defaultTimeout)
Reads only the header, will need be followed up by a readResponse.
Definition client.hpp:195
Detail::PromiseTypeBind< Detail::PromiseTypeBindThen<>, Detail::PromiseTypeBindFail< Error > > readResponse(boost::beast::http::response_parser< ResponseBodyT > &parser, std::chrono::seconds timeout=defaultTimeout)
Read a response using a beast response parser. You are responsible for keeping the parser alive!
Definition client.hpp:276
std::optional< SslOptions > sslOptions_
Definition client.hpp:474
T & state(std::string const &tag)
Retrieve attached state by tag.
Definition client.hpp:116
void performRequest(Request< BodyT > &&request, promise::Defer &&d, std::chrono::seconds timeout)
Definition client.hpp:447
void withStreamDo(FunctionT &&func) const
Definition client.hpp:377
boost::asio::ip::tcp::resolver::results_type::endpoint_type endpoint_
Definition client.hpp:478
ROAR_PIMPL_SPECIAL_FUNCTIONS_NO_MOVE(Client)
Detail::PromiseTypeBind< Detail::PromiseTypeBindThen< Detail::PromiseReferenceWrap< Response< ResponseBodyT > > >, Detail::PromiseTypeBindFail< Error > > requestAndReadResponse(Request< RequestBodyT > &&request, std::chrono::seconds timeout=defaultTimeout)
Definition client.hpp:308
void withLowerLayerDo(FunctionT &&func) const
Definition client.hpp:396
Detail::PromiseTypeBind< Detail::PromiseTypeBindThen<>, Detail::PromiseTypeBindFail< Error > > shutdownSsl(std::chrono::seconds timeout=defaultTimeout)
Definition client.hpp:347
boost::asio::ip::tcp::resolver resolver_
Definition client.hpp:475
static constexpr std::chrono::seconds defaultTimeout
Definition client.hpp:36
Detail::PromiseTypeBind< Detail::PromiseTypeBindThen< std::string_view, std::size_t >, Detail::PromiseTypeBindFail< Error > > read(std::chrono::seconds timeout=defaultTimeout)
Reads something from the server.
Definition client.cpp:109
void attachState(std::string const &tag, T &&state)
Attach some state to the client lifetime.
Definition client.hpp:90
void removeState(std::string const &tag)
Remove attached state.
Definition client.hpp:126
Detail::PromiseTypeBind< Detail::PromiseTypeBindThen<>, Detail::PromiseTypeBindFail< Error > > request(Request< BodyT > &&request, std::chrono::seconds timeout=defaultTimeout)
Connects the client to a server and performs a request.
Definition client.hpp:139
std::unordered_map< std::string, std::any > attachedState_
Definition client.hpp:479
Detail::PromiseTypeBind< Detail::PromiseTypeBindThen< std::size_t >, Detail::PromiseTypeBindFail< Error > > send(std::string message, std::chrono::seconds timeout=defaultTimeout)
Sends a string to the server.
Definition client.cpp:83
std::variant< boost::beast::ssl_stream< boost::beast::tcp_stream >, boost::beast::tcp_stream > socket_
Definition client.hpp:477
Detail::PromiseTypeBind< Detail::PromiseTypeBindThen<>, Detail::PromiseTypeBindFail< Error > > close()
Definition client.hpp:331
Detail::PromiseTypeBind< Detail::PromiseTypeBindThen< Detail::PromiseReferenceWrap< Response< ResponseBodyT > > >, Detail::PromiseTypeBindFail< Error > > readResponse(std::chrono::seconds timeout=defaultTimeout)
Read a response from the server.
Definition client.hpp:236
This class extends the boost::beast::http::request<BodyT> with additional convenience.
Definition request.hpp:52
Definition response.hpp:29
auto ref(T &thing)
Definition promise_compat.hpp:20
Definition authorization.hpp:10
auto visitOverloaded(std::variant< VariantTypes... > const &variant, VisitFunctionTypes &&... visitFunctions)
Definition visit_overloaded.hpp:10
Definition client.hpp:53
boost::asio::any_io_executor executor
Required io executor for boost::asio.
Definition client.hpp:55
std::optional< SslOptions > sslOptions
Definition client.hpp:56
Definition client.hpp:39
std::function< bool(bool, boost::asio::ssl::verify_context &)> sslVerifyCallback
sslVerifyCallback, you can use boost::asio::ssl::rfc2818_verification(host) most of the time.
Definition client.hpp:49
boost::asio::ssl::verify_mode sslVerifyMode
SSL verify mode:
Definition client.hpp:44
boost::asio::ssl::context sslContext
Supply for SSL support.
Definition client.hpp:41
Definition promise_compat.hpp:67
Definition promise_compat.hpp:63
Definition promise_compat.hpp:70
Holds errors that are produced asynchronously anywhere.
Definition error.hpp:20
std::variant< boost::system::error_code, std::string > error
Definition error.hpp:21