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(util::TagDecoratorFactory const& tagDecoratorFactory, boost::asio::yield_context yield) = 0;
64
65 virtual std::optional<Error>
66 sendRaw(
67 boost::beast::http::response<boost::beast::http::string_body> response,
68 boost::asio::yield_context yield
69 ) = 0;
70};
71
72using UpgradableConnectionPtr = std::unique_ptr<UpgradableConnection>;
73
74template <typename StreamType>
76 StreamType stream_;
77 std::optional<boost::beast::http::request<boost::beast::http::string_body>> request_;
78 std::chrono::steady_clock::duration timeout_{kDEFAULT_TIMEOUT};
79
80 using MessageType = boost::beast::http::response<boost::beast::http::string_body>;
81 SendingQueue<MessageType> sendingQueue_;
82
83 bool closed_{false};
84
85public:
87 boost::asio::ip::tcp::socket socket,
88 std::string ip,
89 boost::beast::flat_buffer buffer,
90 util::TagDecoratorFactory const& tagDecoratorFactory
91 )
93 : UpgradableConnection(std::move(ip), std::move(buffer), tagDecoratorFactory)
94 , stream_{std::move(socket)}
95 , sendingQueue_([this](MessageType const& message, auto&& yield) {
96 boost::beast::get_lowest_layer(stream_).expires_after(timeout_);
97 boost::beast::http::async_write(stream_, message, yield);
98 })
99 {
100 }
101
103 boost::asio::ip::tcp::socket socket,
104 std::string ip,
105 boost::beast::flat_buffer buffer,
106 boost::asio::ssl::context& sslCtx,
107 util::TagDecoratorFactory const& tagDecoratorFactory
108 )
110 : UpgradableConnection(std::move(ip), std::move(buffer), tagDecoratorFactory)
111 , stream_{std::move(socket), sslCtx}
112 , sendingQueue_([this](MessageType const& message, auto&& yield) {
113 boost::beast::get_lowest_layer(stream_).expires_after(timeout_);
114 boost::beast::http::async_write(stream_, message, yield);
115 })
116 {
117 }
118
119 HttpConnection(HttpConnection&& other) = delete;
121 operator=(HttpConnection&& other) = delete;
122 HttpConnection(HttpConnection const& other) = delete;
124 operator=(HttpConnection const& other) = delete;
125
126 std::optional<Error>
127 sslHandshake(boost::asio::yield_context yield)
129 {
130 boost::system::error_code error;
131 boost::beast::get_lowest_layer(stream_).expires_after(timeout_);
132 auto const bytesUsed =
133 stream_.async_handshake(boost::asio::ssl::stream_base::server, buffer_.cdata(), yield[error]);
134 if (error)
135 return error;
136
137 buffer_.consume(bytesUsed);
138
139 return std::nullopt;
140 }
141
142 bool
143 wasUpgraded() const override
144 {
145 return false;
146 }
147
148 std::optional<Error>
149 sendRaw(
150 boost::beast::http::response<boost::beast::http::string_body> response,
151 boost::asio::yield_context yield
152 ) override
153 {
154 return sendingQueue_.send(std::move(response), yield);
155 }
156
157 void
158 setTimeout(std::chrono::steady_clock::duration newTimeout) override
159 {
160 timeout_ = newTimeout;
161 }
162
163 std::optional<Error>
164 send(Response response, boost::asio::yield_context yield) override
165 {
166 auto httpResponse = std::move(response).intoHttpResponse();
167 return sendRaw(std::move(httpResponse), yield);
168 }
169
170 std::expected<Request, Error>
171 receive(boost::asio::yield_context yield) override
172 {
173 if (request_.has_value()) {
174 Request result{std::move(request_).value()};
175 request_.reset();
176 return result;
177 }
178 auto expectedRequest = fetch(yield);
179 if (expectedRequest.has_value())
180 return Request{std::move(expectedRequest).value()};
181
182 return std::unexpected{std::move(expectedRequest).error()};
183 }
184
185 void
186 close(boost::asio::yield_context yield) override
187 {
188 // This is needed because calling async_shutdown() multiple times may lead to hanging coroutines.
189 // See WsConnection for more details.
190 if (closed_)
191 return;
192
193 closed_ = true;
194
195 [[maybe_unused]] boost::system::error_code error;
196 if constexpr (IsSslTcpStream<StreamType>) {
197 boost::beast::get_lowest_layer(stream_).expires_after(timeout_);
198 stream_.async_shutdown(yield[error]); // Close the SSL connection gracefully
199 }
200 boost::beast::get_lowest_layer(stream_).socket().shutdown(
201 boost::asio::ip::tcp::socket::shutdown_type::shutdown_both, error
202 );
203 }
204
205 std::expected<bool, Error>
206 isUpgradeRequested(boost::asio::yield_context yield) override
207 {
208 auto expectedRequest = fetch(yield);
209 if (not expectedRequest.has_value())
210 return std::unexpected{std::move(expectedRequest).error()};
211
212 request_ = std::move(expectedRequest).value();
213
214 return boost::beast::websocket::is_upgrade(request_.value());
215 }
216
217 std::expected<ConnectionPtr, Error>
218 upgrade(util::TagDecoratorFactory const& tagDecoratorFactory, boost::asio::yield_context yield) override
219 {
220 ASSERT(request_.has_value(), "Request must be present to upgrade the connection");
221
222 return makeWsConnection(
223 std::move(stream_),
224 std::move(ip_),
225 std::move(buffer_),
226 std::move(request_).value(),
227 tagDecoratorFactory,
228 yield
229 );
230 }
231
232private:
233 std::expected<boost::beast::http::request<boost::beast::http::string_body>, Error>
234 fetch(boost::asio::yield_context yield)
235 {
236 boost::beast::http::request<boost::beast::http::string_body> request{};
237 boost::system::error_code error;
238 boost::beast::get_lowest_layer(stream_).expires_after(timeout_);
239 boost::beast::http::async_read(stream_, buffer_, request, yield[error]);
240 if (error)
241 return std::unexpected{error};
242 return request;
243 }
244};
245
246using PlainHttpConnection = HttpConnection<boost::beast::tcp_stream>;
247
248using SslHttpConnection = HttpConnection<boost::asio::ssl::stream<boost::beast::tcp_stream>>;
249
250} // namespace web::ng::impl
A factory for TagDecorator instantiation.
Definition Taggable.hpp:182
std::string const & ip() const
Get the ip of the client.
Definition Connection.cpp:37
A class representing a connection to a client.
Definition Connection.hpp:100
Connection(std::string ip, boost::beast::flat_buffer buffer, util::TagDecoratorFactory const &tagDecoratorFactory)
Construct a new Connection object.
Definition Connection.cpp:48
static constexpr std::chrono::steady_clock::duration kDEFAULT_TIMEOUT
The default timeout for send, receive, and close operations.
Definition Connection.hpp:109
Represents an HTTP or WebSocket request.
Definition Request.hpp:37
Represents an HTTP or Websocket response.
Definition Response.hpp:40
Definition HttpConnection.hpp:75
bool wasUpgraded() const override
Whether the connection was upgraded. Upgraded connections are websocket connections.
Definition HttpConnection.hpp:143
std::optional< Error > send(Response response, boost::asio::yield_context yield) override
Send a response to the client.
Definition HttpConnection.hpp:164
void setTimeout(std::chrono::steady_clock::duration newTimeout) override
Get the timeout for send, receive, and close operations. For WebSocket connections,...
Definition HttpConnection.hpp:158
void close(boost::asio::yield_context yield) override
Gracefully close the connection.
Definition HttpConnection.hpp:186
std::expected< Request, Error > receive(boost::asio::yield_context yield) override
Receive a request from the client.
Definition HttpConnection.hpp:171
Definition SendingQueue.hpp:35
Definition HttpConnection.hpp:55
Definition Concepts.hpp:33
Definition Concepts.hpp:30