Wire Sysio Wire Sysion 1.0.0
Loading...
Searching...
No Matches
http_client.cpp
Go to the documentation of this file.
2#include <fc/io/json.hpp>
3#include <fc/scoped_exit.hpp>
5
6#include <boost/beast/core.hpp>
7#include <boost/beast/http.hpp>
8#include <boost/beast/version.hpp>
9#include <boost/asio/connect.hpp>
10#include <boost/asio/ip/tcp.hpp>
11#include <boost/asio/ssl/error.hpp>
12#include <boost/asio/ssl/stream.hpp>
13#include <boost/asio/local/stream_protocol.hpp>
14#include <boost/asio/ssl/rfc2818_verification.hpp>
15#include <boost/filesystem.hpp>
16
17using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp>
18namespace http = boost::beast::http; // from <boost/beast/http.hpp>
19namespace ssl = boost::asio::ssl; // from <boost/asio/ssl.hpp>
20namespace local = boost::asio::local;
21
22namespace fc {
23
27static const std::map<string,uint16_t> default_proto_ports = {
28 {"http", 80},
29 {"https", 443}
30};
31
33public:
34 using host_key = std::tuple<std::string, std::string, uint16_t>;
35 using raw_socket_ptr = std::unique_ptr<tcp::socket>;
36 using ssl_socket_ptr = std::unique_ptr<ssl::stream<tcp::socket>>;
37 using unix_socket_ptr = std::unique_ptr<local::stream_protocol::socket>;
38 using connection = std::variant<raw_socket_ptr, ssl_socket_ptr, unix_socket_ptr>;
39 using connection_map = std::map<host_key, connection>;
40 using unix_url_split_map = std::map<string, fc::url>;
41 using error_code = boost::system::error_code;
42 using deadline_type = boost::posix_time::ptime;
43
45 :_ioc()
46 ,_sslc(ssl::context::sslv23_client)
47 {
48 set_verify_peers(true);
49 }
50
51 void add_cert(const std::string& cert_pem_string) {
52 error_code ec;
53 _sslc.add_certificate_authority(boost::asio::buffer(cert_pem_string.data(), cert_pem_string.size()), ec);
54 FC_ASSERT(!ec, "Failed to add cert: ${msg}", ("msg", ec.message()));
55 }
56
57 void set_verify_peers(bool enabled) {
58 if (enabled) {
59 _sslc.set_verify_mode(ssl::verify_peer);
60 } else {
61 _sslc.set_verify_mode(ssl::verify_none);
62 }
63 }
64
65 template<typename SyncReadStream, typename Fn, typename CancelFn>
66 error_code sync_do_with_deadline( SyncReadStream& s, deadline_type deadline, Fn f, CancelFn cf ) {
67 bool timer_expired = false;
68 boost::asio::deadline_timer timer(_ioc);
69
70 timer.expires_at(deadline);
71 bool timer_cancelled = false;
72 timer.async_wait([&timer_expired, &timer_cancelled] (const error_code&) {
73 // the only non-success error_code this is called with is operation_aborted but since
74 // we could have queued "success" when we cancelled the timer, we set a flag at the
75 // safer scope and only respect that.
76 if (!timer_cancelled) {
77 timer_expired = true;
78 }
79 });
80
81 std::optional<error_code> f_result;
82 f(f_result);
83
84 _ioc.restart();
85 while (_ioc.run_one())
86 {
87 if (f_result) {
88 timer_cancelled = true;
89 timer.cancel();
90 } else if (timer_expired) {
91 cf();
92 }
93 }
94
95 if (!timer_expired) {
96 return *f_result;
97 } else {
98 return error_code(boost::system::errc::timed_out, boost::system::system_category());
99 }
100 }
101
102 template<typename SyncReadStream, typename Fn>
103 error_code sync_do_with_deadline( SyncReadStream& s, deadline_type deadline, Fn f) {
104 return sync_do_with_deadline(s, deadline, f, [&s](){
105 s.lowest_layer().cancel();
106 });
107 };
108
109 template<typename SyncReadStream>
110 error_code sync_connect_with_timeout( SyncReadStream& s, const std::string& host, const std::string& port, const deadline_type& deadline ) {
111 tcp::resolver local_resolver(_ioc);
112 bool cancelled = false;
113
114 auto res = sync_do_with_deadline(s, deadline, [&local_resolver, &cancelled, &s, &host, &port](std::optional<error_code>& final_ec){
115 local_resolver.async_resolve(host, port, [&cancelled, &s, &final_ec](const error_code& ec, tcp::resolver::results_type resolved ){
116 if (ec) {
117 final_ec.emplace(ec);
118 return;
119 }
120
121 if (!cancelled) {
122 boost::asio::async_connect(s, resolved.begin(), resolved.end(), [&final_ec](const error_code& ec, tcp::resolver::iterator ){
123 final_ec.emplace(ec);
124 });
125 }
126 });
127 },[&local_resolver, &cancelled](){
128 cancelled = true;
129 local_resolver.cancel();
130 });
131
132 return res;
133 };
134
135 template<typename SyncReadStream>
136 error_code sync_write_with_timeout(SyncReadStream& s, http::request<http::string_body>& req, const deadline_type& deadline ) {
137 return sync_do_with_deadline(s, deadline, [&s, &req](std::optional<error_code>& final_ec){
138 http::async_write(s, req, [&final_ec]( const error_code& ec, std::size_t ) {
139 final_ec.emplace(ec);
140 });
141 });
142 }
143
144 template<typename SyncReadStream>
145 error_code sync_read_with_timeout(SyncReadStream& s, boost::beast::flat_buffer& buffer, http::response<http::string_body>& res, const deadline_type& deadline ) {
146 return sync_do_with_deadline(s, deadline, [&s, &buffer, &res](std::optional<error_code>& final_ec){
147 http::async_read(s, buffer, res, [&final_ec]( const error_code& ec, std::size_t ) {
148 final_ec.emplace(ec);
149 });
150 });
151 }
152
153 host_key url_to_host_key( const url& dest ) {
154 FC_ASSERT(dest.host(), "Provided URL has no host");
155 uint16_t port = 80;
156 if (dest.port()) {
157 port = *dest.port();
158 }
159
160 return std::make_tuple(dest.proto(), *dest.host(), port);
161 }
162
163 connection_map::iterator create_unix_connection( const url& dest, const deadline_type& deadline) {
164 auto key = url_to_host_key(dest);
165 auto socket = std::make_unique<local::stream_protocol::socket>(_ioc);
166
167 error_code ec;
168 socket->connect(local::stream_protocol::endpoint(*dest.host()), ec);
169 FC_ASSERT(!ec, "Failed to connect: ${message}", ("message",ec.message()));
170
171 auto res = _connections.emplace(std::piecewise_construct,
172 std::forward_as_tuple(key),
173 std::forward_as_tuple(std::move(socket)));
174
175 return res.first;
176 }
177
178 connection_map::iterator create_raw_connection( const url& dest, const deadline_type& deadline ) {
179 auto key = url_to_host_key(dest);
180 auto socket = std::make_unique<tcp::socket>(_ioc);
181
182 error_code ec = sync_connect_with_timeout(*socket, *dest.host(), dest.port() ? std::to_string(*dest.port()) : "80", deadline);
183 FC_ASSERT(!ec, "Failed to connect: ${message}", ("message",ec.message()));
184
185 auto res = _connections.emplace(std::piecewise_construct,
186 std::forward_as_tuple(key),
187 std::forward_as_tuple(std::move(socket)));
188
189 return res.first;
190 }
191
192 connection_map::iterator create_ssl_connection( const url& dest, const deadline_type& deadline ) {
193 auto key = url_to_host_key(dest);
194 auto ssl_socket = std::make_unique<ssl::stream<tcp::socket>>(_ioc, _sslc);
195
196 // Set SNI Hostname (many hosts need this to handshake successfully)
197 if(!SSL_set_tlsext_host_name(ssl_socket->native_handle(), dest.host()->c_str()))
198 {
199 error_code ec{static_cast<int>(::ERR_get_error()), boost::asio::error::get_ssl_category()};
200 FC_THROW("Unable to set SNI Host Name: ${msg}", ("msg", ec.message()));
201 }
202
203 ssl_socket->set_verify_callback(boost::asio::ssl::rfc2818_verification(*dest.host()));
204
205 error_code ec = sync_connect_with_timeout(ssl_socket->next_layer(), *dest.host(), dest.port() ? std::to_string(*dest.port()) : "443", deadline);
206 if (!ec) {
207 ec = sync_do_with_deadline(ssl_socket->next_layer(), deadline, [&ssl_socket](std::optional<error_code>& final_ec) {
208 ssl_socket->async_handshake(ssl::stream_base::client, [&final_ec](const error_code& ec) {
209 final_ec.emplace(ec);
210 });
211 });
212 }
213 FC_ASSERT(!ec, "Failed to connect: ${message}", ("message",ec.message()));
214
215 auto res = _connections.emplace(std::piecewise_construct,
216 std::forward_as_tuple(key),
217 std::forward_as_tuple(std::move(ssl_socket)));
218
219 return res.first;
220 }
221
222 connection_map::iterator create_connection( const url& dest, const deadline_type& deadline ) {
223 if (dest.proto() == "http") {
224 return create_raw_connection(dest, deadline);
225 } else if (dest.proto() == "https") {
226 return create_ssl_connection(dest, deadline);
227 } else if (dest.proto() == "unix") {
228 return create_unix_connection(dest, deadline);
229 } else {
230 FC_THROW("Unknown protocol ${proto}", ("proto", dest.proto()));
231 }
232 }
233
234 struct check_closed_visitor : public visitor<bool> {
235 bool operator() ( const raw_socket_ptr& ptr ) const {
236 return !ptr->is_open();
237 }
238
239 bool operator() ( const ssl_socket_ptr& ptr ) const {
240 return !ptr->lowest_layer().is_open();
241 }
242
243 bool operator() ( const unix_socket_ptr& ptr) const {
244 return !ptr->is_open();
245 }
246 };
247
248 bool check_closed( const connection_map::iterator& conn_itr ) {
249 if (std::visit(check_closed_visitor(), conn_itr->second)) {
250 _connections.erase(conn_itr);
251 return true;
252 } else {
253 return false;
254 }
255 }
256
257 connection_map::iterator get_connection( const url& dest, const deadline_type& deadline ) {
258 auto key = url_to_host_key(dest);
259 auto conn_itr = _connections.find(key);
260 if (conn_itr == _connections.end() || check_closed(conn_itr)) {
261 return create_connection(dest, deadline);
262 } else {
263 return conn_itr;
264 }
265 }
266
267 struct write_request_visitor : visitor<error_code> {
268 write_request_visitor(http_client_impl* that, http::request<http::string_body>& req, const deadline_type& deadline)
269 :that(that)
270 ,req(req)
271 ,deadline(deadline)
272 {}
273
274 template<typename S>
275 error_code operator() ( S& stream ) const {
276 return that->sync_write_with_timeout(*stream, req, deadline);
277 }
278
280 http::request<http::string_body>& req;
282 };
283
284 struct read_response_visitor : visitor<error_code> {
285 read_response_visitor(http_client_impl* that, boost::beast::flat_buffer& buffer, http::response<http::string_body>& res, const deadline_type& deadline)
286 :that(that)
287 ,buffer(buffer)
288 ,res(res)
289 ,deadline(deadline)
290 {}
291
292 template<typename S>
293 error_code operator() ( S& stream ) const {
294 return that->sync_read_with_timeout(*stream, buffer, res, deadline);
295 }
296
298 boost::beast::flat_buffer& buffer;
299 http::response<http::string_body>& res;
301 };
302
303 variant post_sync(const url& dest, const variant& payload, const fc::time_point& _deadline) {
304 static const deadline_type epoch(boost::gregorian::date(1970, 1, 1));
305 auto deadline = epoch + boost::posix_time::microseconds(_deadline.time_since_epoch().count());
306 FC_ASSERT(dest.host(), "No host set on URL");
307
308 string path = dest.path() ? dest.path()->generic_string() : "/";
309 if (dest.query()) {
310 path = path + "?" + *dest.query();
311 }
312
313 string host_str = *dest.host();
314 if (dest.port()) {
315 auto port = *dest.port();
316 auto proto_iter = default_proto_ports.find(dest.proto());
317 if (proto_iter != default_proto_ports.end() && proto_iter->second != port) {
318 host_str = host_str + ":" + std::to_string(port);
319 }
320 }
321
322 http::request<http::string_body> req{http::verb::post, path, 11};
323 req.set(http::field::host, host_str);
324 req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
325 req.set(http::field::content_type, "application/json");
326 req.keep_alive(true);
327 req.body() = json::to_string(payload, _deadline);
328 req.prepare_payload();
329
330 auto conn_iter = get_connection(dest, deadline);
331 auto eraser = make_scoped_exit([this, &conn_iter](){
332 _connections.erase(conn_iter);
333 });
334
335 // Send the HTTP request to the remote host
336 error_code ec = std::visit(write_request_visitor(this, req, deadline), conn_iter->second);
337 FC_ASSERT(!ec, "Failed to send request: ${message}", ("message",ec.message()));
338
339 // This buffer is used for reading and must be persisted
340 boost::beast::flat_buffer buffer;
341
342 // Declare a container to hold the response
343 http::response<http::string_body> res;
344
345 // Receive the HTTP response
346 ec = std::visit(read_response_visitor(this, buffer, res, deadline), conn_iter->second);
347 FC_ASSERT(!ec, "Failed to read response: ${message}", ("message",ec.message()));
348
349 // if the connection can be kept open, keep it open
350 if (res.keep_alive()) {
351 eraser.cancel();
352 }
353
354 fc::variant result;
355 if( !res.body().empty() ) {
356 try {
357 result = json::from_string( res.body() );
358 } catch( ... ) {}
359 }
360 if (res.result() == http::status::internal_server_error) {
362 try {
363 auto err_var = result.get_object()["error"].get_object();
364 excp = std::make_shared<fc::exception>(err_var["code"].as_int64(), err_var["name"].as_string(), err_var["what"].as_string());
365
366 if (err_var.contains("details")) {
367 for (const auto& dvar : err_var["details"].get_array()) {
368 excp->append_log(FC_LOG_MESSAGE(error, dvar.get_object()["message"].as_string()));
369 }
370 }
371 } catch( ... ) {
372
373 }
374
375 if (excp) {
376 throw *excp;
377 } else {
378 FC_THROW("Request failed with 500 response, but response was not parseable");
379 }
380 } else if (res.result() == http::status::not_found) {
381 FC_THROW("URL not found: ${url}", ("url", (std::string)dest));
382 }
383
384 return result;
385 }
386
387 /*
388 Unix URLs work a little special here. They'll originally be in the format of
389 unix:///home/username/sysio-wallet/kiod.sock/v1/wallet/sign_digest
390 for example. When the fc::url is given to http_client in post_sync(), this will
391 have proto=unix and host=/home/username/sysio-wallet/kiod.sock/v1/wallet/sign_digest
392
393 At this point we still don't know what part of the above string is the unix socket path
394 and which part is the path to access on the server. This function discovers that
395 host=/home/username/sysio-wallet/kiod.sock and path=/v1/wallet/sign_digest
396 and creates another fc::url that will be used downstream of the http_client::post_sync()
397 call.
398 */
399 const fc::url& get_unix_url(const std::string& full_url) {
400 unix_url_split_map::const_iterator found = _unix_url_paths.find(full_url);
401 if(found != _unix_url_paths.end())
402 return found->second;
403
404 boost::filesystem::path socket_file(full_url);
405 if(socket_file.is_relative())
406 FC_THROW_EXCEPTION( parse_error_exception, "socket url cannot be relative (${url})", ("url", socket_file.string()));
407 if(socket_file.empty())
408 FC_THROW_EXCEPTION( parse_error_exception, "missing socket url");
409 boost::filesystem::path url_path;
410 do {
411 if(boost::filesystem::status(socket_file).type() == boost::filesystem::socket_file)
412 break;
413 url_path = socket_file.filename() / url_path;
414 socket_file = socket_file.remove_filename();
415 } while(!socket_file.empty());
416 if(socket_file.empty())
417 FC_THROW_EXCEPTION( parse_error_exception, "couldn't discover socket path");
418 url_path = "/" / url_path;
419 return _unix_url_paths.emplace(full_url, fc::url("unix", socket_file.string(), ostring(), ostring(), url_path.string(), ostring(), ovariant_object(), std::optional<uint16_t>())).first->second;
420 }
421
422 boost::asio::io_context _ioc;
423 ssl::context _sslc;
426};
427
428
429http_client::http_client()
430:_my(new http_client_impl())
431{
432
433}
434
435variant http_client::post_sync(const url& dest, const variant& payload, const fc::time_point& deadline) {
436 if(dest.proto() == "unix")
437 return _my->post_sync(_my->get_unix_url(*dest.host()), payload, deadline);
438 else
439 return _my->post_sync(dest, payload, deadline);
440}
441
442void http_client::add_cert(const std::string& cert_pem_string) {
443 _my->add_cert(cert_pem_string);
444}
445
447 _my->set_verify_peers(enabled);
448}
449
453
454}
boost::system::error_code error_code
error_code sync_do_with_deadline(SyncReadStream &s, deadline_type deadline, Fn f)
void set_verify_peers(bool enabled)
std::unique_ptr< local::stream_protocol::socket > unix_socket_ptr
boost::asio::io_context _ioc
void add_cert(const std::string &cert_pem_string)
error_code sync_do_with_deadline(SyncReadStream &s, deadline_type deadline, Fn f, CancelFn cf)
variant post_sync(const url &dest, const variant &payload, const fc::time_point &_deadline)
const fc::url & get_unix_url(const std::string &full_url)
connection_map _connections
unix_url_split_map _unix_url_paths
std::map< host_key, connection > connection_map
connection_map::iterator get_connection(const url &dest, const deadline_type &deadline)
std::variant< raw_socket_ptr, ssl_socket_ptr, unix_socket_ptr > connection
std::tuple< std::string, std::string, uint16_t > host_key
std::map< string, fc::url > unix_url_split_map
connection_map::iterator create_raw_connection(const url &dest, const deadline_type &deadline)
std::unique_ptr< ssl::stream< tcp::socket > > ssl_socket_ptr
error_code sync_read_with_timeout(SyncReadStream &s, boost::beast::flat_buffer &buffer, http::response< http::string_body > &res, const deadline_type &deadline)
boost::posix_time::ptime deadline_type
bool check_closed(const connection_map::iterator &conn_itr)
connection_map::iterator create_ssl_connection(const url &dest, const deadline_type &deadline)
connection_map::iterator create_unix_connection(const url &dest, const deadline_type &deadline)
error_code sync_write_with_timeout(SyncReadStream &s, http::request< http::string_body > &req, const deadline_type &deadline)
connection_map::iterator create_connection(const url &dest, const deadline_type &deadline)
std::unique_ptr< tcp::socket > raw_socket_ptr
error_code sync_connect_with_timeout(SyncReadStream &s, const std::string &host, const std::string &port, const deadline_type &deadline)
host_key url_to_host_key(const url &dest)
variant post_sync(const url &dest, const variant &payload, const time_point &deadline=time_point::maximum())
void set_verify_peers(bool enabled)
void add_cert(const std::string &cert_pem_string)
static string to_string(const variant &v, const yield_function_t &yield, const output_formatting format=output_formatting::stringify_large_ints_and_doubles)
Definition json.cpp:674
static variant from_string(const string &utf8_str, const parse_type ptype=parse_type::legacy_parser, uint32_t max_depth=DEFAULT_MAX_RECURSION_DEPTH)
Definition json.cpp:442
constexpr int64_t count() const
Definition time.hpp:26
wraps boost::filesystem::path to provide platform independent path manipulation.
constexpr const microseconds & time_since_epoch() const
Definition time.hpp:52
opath path() const
Definition url.cpp:182
ostring host() const
Definition url.cpp:170
string proto() const
Definition url.cpp:166
ostring query() const
Definition url.cpp:186
std::optional< uint16_t > port() const
Definition url.cpp:194
stores null, int64, uint64, double, bool, string, std::vector<variant>, and variant_object's.
Definition variant.hpp:191
#define FC_THROW( ...)
#define FC_THROW_EXCEPTION(EXCEPTION, FORMAT,...)
#define FC_ASSERT(TEST,...)
Checks a condition and throws an assert_exception if the test is FALSE.
boost::asio::ip::tcp tcp
#define FC_LOG_MESSAGE(LOG_LEVEL, FORMAT,...)
A helper method for generating log messages.
namespace sysio::chain
Definition authority.cpp:3
std::shared_ptr< exception > exception_ptr
std::optional< fc::string > ostring
Definition url.hpp:10
scoped_exit< Callback > make_scoped_exit(Callback &&c)
std::optional< fc::variant_object > ovariant_object
Definition url.hpp:12
unsigned short uint16_t
Definition stdint.h:125
http::response< http::string_body > & res
read_response_visitor(http_client_impl *that, boost::beast::flat_buffer &buffer, http::response< http::string_body > &res, const deadline_type &deadline)
write_request_visitor(http_client_impl *that, http::request< http::string_body > &req, const deadline_type &deadline)
http::request< http::string_body > & req
yh_object_type type
Definition yubihsm.h:672
char * s