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>
17using tcp = boost::asio::ip::tcp;
18namespace http = boost::beast::http;
19namespace ssl = boost::asio::ssl;
20namespace local = boost::asio::local;
27static const std::map<string,uint16_t> default_proto_ports = {
34 using host_key = std::tuple<std::string, std::string, uint16_t>;
38 using connection = std::variant<raw_socket_ptr, ssl_socket_ptr, unix_socket_ptr>;
51 void add_cert(
const std::string& cert_pem_string) {
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()));
59 _sslc.set_verify_mode(ssl::verify_peer);
61 _sslc.set_verify_mode(ssl::verify_none);
65 template<
typename SyncReadStream,
typename Fn,
typename CancelFn>
67 bool timer_expired =
false;
68 boost::asio::deadline_timer timer(
_ioc);
70 timer.expires_at(deadline);
71 bool timer_cancelled =
false;
72 timer.async_wait([&timer_expired, &timer_cancelled] (
const error_code&) {
76 if (!timer_cancelled) {
81 std::optional<error_code> f_result;
85 while (
_ioc.run_one())
88 timer_cancelled =
true;
90 }
else if (timer_expired) {
98 return error_code(boost::system::errc::timed_out, boost::system::system_category());
102 template<
typename SyncReadStream,
typename Fn>
105 s.lowest_layer().cancel();
109 template<
typename SyncReadStream>
111 tcp::resolver local_resolver(
_ioc);
112 bool cancelled =
false;
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 ){
117 final_ec.emplace(ec);
122 boost::asio::async_connect(
s, resolved.begin(), resolved.end(), [&final_ec](
const error_code& ec, tcp::resolver::iterator ){
123 final_ec.emplace(ec);
127 },[&local_resolver, &cancelled](){
129 local_resolver.cancel();
135 template<
typename SyncReadStream>
138 http::async_write(
s, req, [&final_ec](
const error_code& ec, std::size_t ) {
139 final_ec.emplace(ec);
144 template<
typename SyncReadStream>
147 http::async_read(
s, buffer, res, [&final_ec](
const error_code& ec, std::size_t ) {
148 final_ec.emplace(ec);
160 return std::make_tuple(dest.
proto(), *dest.
host(), port);
165 auto socket = std::make_unique<local::stream_protocol::socket>(
_ioc);
168 socket->connect(local::stream_protocol::endpoint(*dest.
host()), ec);
169 FC_ASSERT(!ec,
"Failed to connect: ${message}", (
"message",ec.message()));
171 auto res =
_connections.emplace(std::piecewise_construct,
172 std::forward_as_tuple(key),
173 std::forward_as_tuple(std::move(socket)));
180 auto socket = std::make_unique<tcp::socket>(
_ioc);
183 FC_ASSERT(!ec,
"Failed to connect: ${message}", (
"message",ec.message()));
185 auto res =
_connections.emplace(std::piecewise_construct,
186 std::forward_as_tuple(key),
187 std::forward_as_tuple(std::move(socket)));
194 auto ssl_socket = std::make_unique<ssl::stream<tcp::socket>>(
_ioc,
_sslc);
197 if(!SSL_set_tlsext_host_name(ssl_socket->native_handle(), dest.
host()->c_str()))
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()));
203 ssl_socket->set_verify_callback(boost::asio::ssl::rfc2818_verification(*dest.
host()));
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);
213 FC_ASSERT(!ec,
"Failed to connect: ${message}", (
"message",ec.message()));
215 auto res =
_connections.emplace(std::piecewise_construct,
216 std::forward_as_tuple(key),
217 std::forward_as_tuple(std::move(ssl_socket)));
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);
230 FC_THROW(
"Unknown protocol ${proto}", (
"proto", dest.
proto()));
236 return !ptr->is_open();
240 return !ptr->lowest_layer().is_open();
244 return !ptr->is_open();
250 _connections.erase(conn_itr);
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);
276 return that->sync_write_with_timeout(*stream, req, deadline);
280 http::request<http::string_body>&
req;
294 return that->sync_read_with_timeout(*stream, buffer, res, deadline);
299 http::response<http::string_body>&
res;
304 static const deadline_type epoch(boost::gregorian::date(1970, 1, 1));
308 string path = dest.
path() ? dest.
path()->generic_string() :
"/";
313 string host_str = *dest.
host();
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);
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);
328 req.prepare_payload();
330 auto conn_iter = get_connection(dest, deadline);
332 _connections.erase(conn_iter);
337 FC_ASSERT(!ec,
"Failed to send request: ${message}", (
"message",ec.message()));
340 boost::beast::flat_buffer buffer;
343 http::response<http::string_body> res;
347 FC_ASSERT(!ec,
"Failed to read response: ${message}", (
"message",ec.message()));
350 if (res.keep_alive()) {
355 if( !res.body().empty() ) {
360 if (res.result() == http::status::internal_server_error) {
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());
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()));
378 FC_THROW(
"Request failed with 500 response, but response was not parseable");
380 }
else if (res.result() == http::status::not_found) {
381 FC_THROW(
"URL not found: ${url}", (
"url", (std::string)dest));
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;
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())
409 boost::filesystem::path url_path;
411 if(boost::filesystem::status(socket_file).
type() == boost::filesystem::socket_file)
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())
418 url_path =
"/" / url_path;
429http_client::http_client()
436 if(dest.
proto() ==
"unix")
437 return _my->post_sync(_my->get_unix_url(*dest.
host()), payload, deadline);
439 return _my->post_sync(dest, payload, deadline);
443 _my->add_cert(cert_pem_string);
447 _my->set_verify_peers(enabled);
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)
static variant from_string(const string &utf8_str, const parse_type ptype=parse_type::legacy_parser, uint32_t max_depth=DEFAULT_MAX_RECURSION_DEPTH)
constexpr int64_t count() const
wraps boost::filesystem::path to provide platform independent path manipulation.
constexpr const microseconds & time_since_epoch() const
std::optional< uint16_t > port() const
stores null, int64, uint64, double, bool, string, std::vector<variant>, and variant_object's.
#define FC_THROW_EXCEPTION(EXCEPTION, FORMAT,...)
#define FC_ASSERT(TEST,...)
Checks a condition and throws an assert_exception if the test is FALSE.
#define FC_LOG_MESSAGE(LOG_LEVEL, FORMAT,...)
A helper method for generating log messages.
std::shared_ptr< exception > exception_ptr
std::optional< fc::string > ostring
scoped_exit< Callback > make_scoped_exit(Callback &&c)
std::optional< fc::variant_object > ovariant_object
const deadline_type & deadline
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)
boost::beast::flat_buffer & buffer
write_request_visitor(http_client_impl *that, http::request< http::string_body > &req, const deadline_type &deadline)
const deadline_type & deadline
http::request< http::string_body > & req