Malloy
Loading...
Searching...
No Matches
controller.hpp
1#pragma once
2
3#include "type_traits.hpp"
4#include "http/connection_plain.hpp"
5#include "websocket/connection.hpp"
6#include "../core/controller.hpp"
7#include "../core/error.hpp"
8#include "../core/detail/controller_run_result.hpp"
9#include "../core/http/request.hpp"
10#include "../core/http/response.hpp"
11#include "../core/http/type_traits.hpp"
12#include "../core/http/utils.hpp"
13#include "../core/tcp/stream.hpp"
14#if MALLOY_FEATURE_TLS
15 #include "http/connection_tls.hpp"
16
17 #include <boost/beast/ssl.hpp>
18#endif
19
20#include <boost/asio/strand.hpp>
21#include <spdlog/logger.h>
22
23#include <filesystem>
24
25namespace boost::asio::ssl
26{
27 class context;
28}
29
30namespace malloy::client
31{
32 namespace websocket
33 {
34 class connection_plain;
35 }
36
37 namespace detail
38 {
39
47 {
49 using header_type = boost::beast::http::response_header<>;
50 using value_type = std::string;
51
52 [[nodiscard]]
53 std::variant<boost::beast::http::string_body>
54 body_for(const header_type&) const
55 {
56 return {};
57 }
58
59 void setup_body(const header_type&, std::string&) const { }
60 };
61
62 static_assert(malloy::client::concepts::response_filter<default_resp_filter>, "default_resp_filter must satisfy response_filter");
63 } // namespace detail
64
69 {
70 public:
75
79 struct config :
81 {
86 std::string user_agent{"malloy"};
87
91 std::uint64_t body_limit = 100'000'000;
92 };
93
99 explicit
100 controller(config cfg);
101
105 ~controller() = default;
106
107#if MALLOY_FEATURE_TLS
113 [[nodiscard("init might fail")]]
114 bool
115 init_tls();
116#endif
117
131 template<malloy::http::concepts::body ReqBody, typename Callback, concepts::response_filter Filter = detail::default_resp_filter>
133 [[nodiscard]]
134 auto
135 http_request(malloy::http::request<ReqBody> req, Callback&& done, Filter filter = {}) -> std::future<malloy::error_code>
136 {
137 return make_http_connection<false>(std::move(req), std::forward<Callback>(done), std::move(filter));
138 }
139
140#if MALLOY_FEATURE_TLS
146 template<malloy::http::concepts::body ReqBody, typename Callback, concepts::response_filter Filter = detail::default_resp_filter>
147 requires concepts::http_callback<Callback, Filter>
148 [[nodiscard]]
149 auto
150 https_request(malloy::http::request<ReqBody> req, Callback&& done, Filter filter = {}) -> std::future<malloy::error_code>
151 {
152 return make_http_connection<true>(std::move(req), std::forward<Callback>(done), std::move(filter));
153 }
154
161 void
163 const std::string& host,
164 std::uint16_t port,
165 const std::string& resource,
166 std::invocable<malloy::error_code, std::shared_ptr<websocket::connection>> auto&& handler)
167 {
168 check_tls();
169
170 // Create connection
171 make_ws_connection<true>(host, port, resource, std::forward<decltype(handler)>(handler));
172 }
173
181 void
182 add_ca_file(const std::filesystem::path& file);
183
191 void
192 add_ca(const std::string& contents);
193#endif
194
211 void
213 const std::string& host,
214 std::uint16_t port,
215 const std::string& resource,
216 std::invocable<malloy::error_code, std::shared_ptr<websocket::connection>> auto&& handler
217 )
218 {
219 // Create connection
220 make_ws_connection<false>(host, port, resource, std::forward<decltype(handler)>(handler));
221 }
222
223 private:
224 std::shared_ptr<boost::asio::ssl::context> m_tls_ctx{nullptr};
225 std::unique_ptr<boost::asio::io_context> m_ioc_sm{std::make_unique<boost::asio::io_context>()};
226 boost::asio::io_context* m_ioc{m_ioc_sm.get()};
227 config m_cfg;
228
229 friend
230 auto
231 start(controller& ctrl) -> session
232 {
233 return session{ctrl.m_cfg, ctrl.m_tls_ctx, std::move(ctrl.m_ioc_sm)};
234 }
235
240 void
241 check_tls() const
242 {
243 // Check whether TLS context was initialized
244 if (!m_tls_ctx)
245 throw std::logic_error("TLS context not initialized.");
246 }
247
248 template<bool isHttps, malloy::http::concepts::body Body, typename Callback, typename Filter>
249 auto
250 make_http_connection(malloy::http::request<Body>&& req, Callback&& cb, Filter&& filter) -> std::future<malloy::error_code>
251 {
252 std::promise<malloy::error_code> prom;
253 auto err_channel = prom.get_future();
254 [req, this](auto&& cb) { // ToDo: Don't capture req by value
255#if MALLOY_FEATURE_TLS
256 if constexpr (isHttps) {
257 // Create connection
258 auto conn = std::make_shared<http::connection_tls<Body, Filter, std::decay_t<Callback>>>(
259 m_cfg.logger->clone(m_cfg.logger->name() + " | HTTPS connection"),
260 *m_ioc,
261 *m_tls_ctx,
262 m_cfg.body_limit
263 );
264
265 // Set SNI hostname (many hosts need this to handshake successfully)
266 // Note: We copy the returned std::string_view into an std::string as the underlying OpenSSL API expects C-strings.
267 const std::string hostname{ req.base()[malloy::http::field::host] };
268 if (!SSL_set_tlsext_host_name(conn->stream().native_handle(), hostname.c_str())) {
269 // ToDo: Improve error handling
270 m_cfg.logger->error("could not set SNI hostname.");
271 boost::system::error_code ec{static_cast<int>(::ERR_get_error()), boost::asio::error::get_ssl_category()};
272 throw boost::system::system_error{ec};
273 }
274
275 // Run
276 cb(std::move(conn));
277
278 return;
279 }
280#endif
281 cb(std::make_shared<http::connection_plain<Body, Filter, std::decay_t<Callback>>>(
282 m_cfg.logger->clone(m_cfg.logger->name() + " | HTTP connection"),
283 *m_ioc,
284 m_cfg.body_limit)
285 );
286 }([this, prom = std::move(prom), req = std::move(req), filter = std::forward<Filter>(filter), cb = std::forward<Callback>(cb)](auto&& conn) mutable {
287 if (!malloy::http::has_field(req, malloy::http::field::user_agent))
288 req.set(malloy::http::field::user_agent, m_cfg.user_agent);
289
290 // Run
291 conn->run(
292 std::to_string(req.port()).c_str(),
293 req,
294 std::move(prom),
295 std::forward<Callback>(cb),
296 std::forward<Filter>(filter));
297 });
298
299 return err_channel;
300
301 }
302
303 template<bool isSecure>
304 void
305 make_ws_connection(
306 const std::string& host,
307 std::uint16_t port,
308 const std::string& resource,
309 std::invocable<malloy::error_code, std::shared_ptr<websocket::connection>> auto&& handler
310 )
311 {
312 // Create connection
313 auto resolver = std::make_shared<boost::asio::ip::tcp::resolver>(boost::asio::make_strand(*m_ioc));
314 resolver->async_resolve(
315 host,
316 std::to_string(port),
317 [this, resolver, done = std::forward<decltype(handler)>(handler), resource](auto ec, auto results) mutable {
318 if (ec) {
319 std::invoke(std::forward<decltype(done)>(done), ec, std::shared_ptr<websocket::connection>{nullptr});
320 } else {
321 auto conn = websocket::connection::make(m_cfg.logger->clone("connection"), [this]() -> malloy::websocket::stream {
322#if MALLOY_FEATURE_TLS
323 if constexpr (isSecure) {
324 return malloy::websocket::stream{boost::beast::ssl_stream<malloy::tcp::stream<>>{
325 malloy::tcp::stream<>{boost::asio::make_strand(*m_ioc)}, *m_tls_ctx}};
326 } else
327#endif
328 return malloy::websocket::stream{malloy::tcp::stream<>{boost::asio::make_strand(*m_ioc)}};
329 }(), m_cfg.user_agent);
330
331 conn->connect(results, resource, [conn, done = std::forward<decltype(done)>(done)](auto ec) mutable {
332 if (ec) {
333 std::invoke(std::forward<decltype(handler)>(done), ec, std::shared_ptr<websocket::connection>{nullptr});
334 } else {
335 std::invoke(std::forward<decltype(handler)>(done), ec, conn);
336 }
337 });
338 }
339 }
340 );
341 }
342 };
343
344 auto start(controller&) -> controller::session;
345
346} // namespace malloy::client
Definition: controller.hpp:69
auto https_request(malloy::http::request< ReqBody > req, Callback &&done, Filter filter={}) -> std::future< malloy::error_code >
Definition: controller.hpp:150
bool init_tls()
Definition: controller.cpp:19
void ws_connect(const std::string &host, std::uint16_t port, const std::string &resource, std::invocable< malloy::error_code, std::shared_ptr< websocket::connection > > auto &&handler)
Definition: controller.hpp:212
void wss_connect(const std::string &host, std::uint16_t port, const std::string &resource, std::invocable< malloy::error_code, std::shared_ptr< websocket::connection > > auto &&handler)
Same as ws_connect() but uses TLS.
Definition: controller.hpp:162
auto http_request(malloy::http::request< ReqBody > req, Callback &&done, Filter filter={}) -> std::future< malloy::error_code >
Definition: controller.hpp:135
void add_ca(const std::string &contents)
Like add_ca_file(std::filesystem::path) but loads from an in-memory string.
Definition: controller.cpp:40
malloy::detail::controller_run_result< std::shared_ptr< boost::asio::ssl::context > > session
Definition: controller.hpp:74
void add_ca_file(const std::filesystem::path &file)
Load a certificate authority for use with TLS validation.
Definition: controller.cpp:30
Definition: controller_run_result.hpp:42
Definition: request.hpp:19
std::uint16_t port() const noexcept
Definition: request.hpp:117
Definition: response.hpp:22
static std::shared_ptr< connection > make(const std::shared_ptr< spdlog::logger > logger, stream &&ws, const std::string &agent_string)
Construct a new connection object.
Definition: connection.hpp:106
Websocket stream. May use TLS.
Definition: stream.hpp:50
Definition: type_traits.hpp:68
Definition: type_traits.hpp:56
boost::beast::error_code error_code
Error code used to signify errors without throwing. Truthy means it holds an error.
Definition: error.hpp:9
Definition: controller.hpp:81
std::string user_agent
Agent string used for connections.
Definition: controller.hpp:86
std::uint64_t body_limit
Definition: controller.hpp:91
Default filter provided to ease use of interface.
Definition: controller.hpp:47
Definition: controller_run_result.hpp:19