Clio  develop
The XRP Ledger API server.
Loading...
Searching...
No Matches
HttpConnection.hpp
1//------------------------------------------------------------------------------
2/*
3 This file is part of clio: https://github.com/XRPLF/clio
4 Copyright (c) 2024, the clio developers.
5
6 Permission to use, copy, modify, and distribute this software for any
7 purpose with or without fee is hereby granted, provided that the above
8 copyright notice and this permission notice appear in all copies.
9
10 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17*/
18//==============================================================================
19
20#pragma once
21
22#include "util/Assert.hpp"
23#include "util/Taggable.hpp"
24#include "web/ng/Connection.hpp"
25#include "web/ng/Error.hpp"
26#include "web/ng/Request.hpp"
27#include "web/ng/Response.hpp"
28#include "web/ng/impl/Concepts.hpp"
29#include "web/ng/impl/SendingQueue.hpp"
30#include "web/ng/impl/WsConnection.hpp"
31
32#include <boost/asio/buffer.hpp>
33#include <boost/asio/ip/tcp.hpp>
34#include <boost/asio/spawn.hpp>
35#include <boost/asio/ssl/context.hpp>
36#include <boost/asio/ssl/stream.hpp>
37#include <boost/asio/ssl/stream_base.hpp>
38#include <boost/beast/core/basic_stream.hpp>
39#include <boost/beast/core/error.hpp>
40#include <boost/beast/core/flat_buffer.hpp>
41#include <boost/beast/core/tcp_stream.hpp>
42#include <boost/beast/http.hpp>
43#include <boost/beast/http/message.hpp>
44#include <boost/beast/http/string_body.hpp>
45#include <boost/beast/websocket.hpp>
46
47#include <chrono>
48#include <memory>
49#include <optional>
50#include <string>
51#include <utility>
52
53namespace web::ng::impl {
54
56public:
58
59 virtual std::expected<bool, Error>
60 isUpgradeRequested(boost::asio::yield_context yield) = 0;
61
62 virtual std::expected<ConnectionPtr, Error>
63 upgrade(
64 util::TagDecoratorFactory const& tagDecoratorFactory,
65 boost::asio::yield_context yield
66 ) = 0;
67
68 virtual std::expected<void, Error>
69 sendRaw(
70 boost::beast::http::response<boost::beast::http::string_body> response,
71 boost::asio::yield_context yield
72 ) = 0;
73};
74
75using UpgradableConnectionPtr = std::unique_ptr<UpgradableConnection>;
76
77template <typename StreamType>
78class HttpConnection : public UpgradableConnection {
79 StreamType stream_;
80 std::optional<boost::beast::http::request<boost::beast::http::string_body>> request_;
81 std::chrono::steady_clock::duration timeout_{kDEFAULT_TIMEOUT};
82
83 using MessageType = boost::beast::http::response<boost::beast::http::string_body>;
84 SendingQueue<MessageType> sendingQueue_;
85
86 bool closed_{false};
87
88public:
89 HttpConnection(
90 boost::asio::ip::tcp::socket socket,
91 std::string ip,
92 boost::beast::flat_buffer buffer,
93 util::TagDecoratorFactory const& tagDecoratorFactory
94 )
96 : UpgradableConnection(std::move(ip), std::move(buffer), tagDecoratorFactory)
97 , stream_{std::move(socket)}
98 , sendingQueue_([this](MessageType const& message, auto&& yield) {
99 boost::beast::get_lowest_layer(stream_).expires_after(timeout_);
100 boost::beast::http::async_write(stream_, message, yield);
101 })
102 {
103 }
104
105 HttpConnection(
106 boost::asio::ip::tcp::socket socket,
107 std::string ip,
108 boost::beast::flat_buffer buffer,
109 boost::asio::ssl::context& sslCtx,
110 util::TagDecoratorFactory const& tagDecoratorFactory
111 )
113 : UpgradableConnection(std::move(ip), std::move(buffer), tagDecoratorFactory)
114 , stream_{std::move(socket), sslCtx}
115 , sendingQueue_([this](MessageType const& message, auto&& yield) {
116 boost::beast::get_lowest_layer(stream_).expires_after(timeout_);
117 boost::beast::http::async_write(stream_, message, yield);
118 })
119 {
120 }
121
122 HttpConnection(HttpConnection&& other) = delete;
123 HttpConnection&
124 operator=(HttpConnection&& other) = delete;
125 HttpConnection(HttpConnection const& other) = delete;
126 HttpConnection&
127 operator=(HttpConnection const& other) = delete;
128
129 std::expected<void, Error>
130 sslHandshake(boost::asio::yield_context yield)
132 {
133 boost::system::error_code error;
134 boost::beast::get_lowest_layer(stream_).expires_after(timeout_);
135 auto const bytesUsed = stream_.async_handshake(
136 boost::asio::ssl::stream_base::server, buffer_.cdata(), yield[error]
137 );
138 if (error)
139 return std::unexpected{error};
140
141 buffer_.consume(bytesUsed);
142
143 return {};
144 }
145
146 bool
147 wasUpgraded() const override
148 {
149 return false;
150 }
151
152 std::expected<void, Error>
153 sendRaw(
154 boost::beast::http::response<boost::beast::http::string_body> response,
155 boost::asio::yield_context yield
156 ) override
157 {
158 return sendingQueue_.send(std::move(response), yield);
159 }
160
161 void
162 setTimeout(std::chrono::steady_clock::duration newTimeout) override
163 {
164 timeout_ = newTimeout;
165 }
166
167 std::expected<void, Error>
168 send(Response response, boost::asio::yield_context yield) override
169 {
170 auto httpResponse = std::move(response).intoHttpResponse();
171 return sendRaw(std::move(httpResponse), yield);
172 }
173
174 std::expected<Request, Error>
175 receive(boost::asio::yield_context yield) override
176 {
177 if (request_.has_value()) {
178 Request result{std::move(request_).value()};
179 request_.reset();
180 return result;
181 }
182 auto expectedRequest = fetch(yield);
183 if (expectedRequest.has_value())
184 return Request{std::move(expectedRequest).value()};
185
186 return std::unexpected{std::move(expectedRequest).error()};
187 }
188
189 void
190 close(boost::asio::yield_context yield) override
191 {
192 // This is needed because calling async_shutdown() multiple times may lead to hanging
193 // coroutines. See WsConnection for more details.
194 if (closed_)
195 return;
196
197 closed_ = true;
198
199 [[maybe_unused]] boost::system::error_code error;
200 if constexpr (IsSslTcpStream<StreamType>) {
201 boost::beast::get_lowest_layer(stream_).expires_after(timeout_);
202 stream_.async_shutdown(yield[error]); // Close the SSL connection gracefully
203 }
204 boost::beast::get_lowest_layer(stream_).socket().shutdown(
205 boost::asio::ip::tcp::socket::shutdown_type::shutdown_both, error
206 );
207 }
208
209 std::expected<bool, Error>
210 isUpgradeRequested(boost::asio::yield_context yield) override
211 {
212 auto expectedRequest = fetch(yield);
213 if (not expectedRequest.has_value())
214 return std::unexpected{std::move(expectedRequest).error()};
215
216 request_ = std::move(expectedRequest).value();
217
218 return boost::beast::websocket::is_upgrade(request_.value());
219 }
220
221 std::expected<ConnectionPtr, Error>
222 upgrade(
223 util::TagDecoratorFactory const& tagDecoratorFactory,
224 boost::asio::yield_context yield
225 ) override
226 {
227 ASSERT(request_.has_value(), "Request must be present to upgrade the connection");
228
229 return makeWsConnection(
230 std::move(stream_),
231 std::move(ip_),
232 std::move(buffer_),
233 std::move(request_).value(),
234 tagDecoratorFactory,
235 yield
236 );
237 }
238
239private:
240 std::expected<boost::beast::http::request<boost::beast::http::string_body>, Error>
241 fetch(boost::asio::yield_context yield)
242 {
243 boost::beast::http::request<boost::beast::http::string_body> request{};
244 boost::system::error_code error;
245 boost::beast::get_lowest_layer(stream_).expires_after(timeout_);
246 boost::beast::http::async_read(stream_, buffer_, request, yield[error]);
247 if (error)
248 return std::unexpected{error};
249 return request;
250 }
251};
252
253using PlainHttpConnection = HttpConnection<boost::beast::tcp_stream>;
254
256
257} // namespace web::ng::impl
A factory for TagDecorator instantiation.
Definition Taggable.hpp:184
std::string const & ip() const
Get the ip of the client.
Definition Connection.cpp:40
Connection(std::string ip, boost::beast::flat_buffer buffer, util::TagDecoratorFactory const &tagDecoratorFactory)
Construct a new Connection object.
Definition Connection.cpp:51
static constexpr std::chrono::steady_clock::duration kDEFAULT_TIMEOUT
The default timeout for send, receive, and close operations.
Definition Connection.hpp:122
Represents an HTTP or WebSocket request.
Definition Request.hpp:37
Represents an HTTP or Websocket response.
Definition Response.hpp:40
Definition HttpConnection.hpp:78
bool wasUpgraded() const override
Whether the connection was upgraded. Upgraded connections are websocket connections.
Definition HttpConnection.hpp:147
std::expected< void, Error > send(Response response, boost::asio::yield_context yield) override
Send a response to the client.
Definition HttpConnection.hpp:168
void setTimeout(std::chrono::steady_clock::duration newTimeout) override
Get the timeout for send, receive, and close operations. For WebSocket connections,...
Definition HttpConnection.hpp:162
void close(boost::asio::yield_context yield) override
Gracefully close the connection.
Definition HttpConnection.hpp:190
std::expected< Request, Error > receive(boost::asio::yield_context yield) override
Receive a request from the client.
Definition HttpConnection.hpp:175
Definition SendingQueue.hpp:35
Definition HttpConnection.hpp:55
Connection(std::string ip, boost::beast::flat_buffer buffer, util::TagDecoratorFactory const &tagDecoratorFactory)
Construct a new Connection object.
Definition Connection.cpp:51
Definition Concepts.hpp:33
Definition Concepts.hpp:30