Wire Sysio Wire Sysion 1.0.0
Loading...
Searching...
No Matches
httpc.cpp
Go to the documentation of this file.
1//
2// sync_client.cpp
3// ~~~~~~~~~~~~~~~
4//
5// Copyright (c) 2003-2012 Christopher M. Kohlhoff (chris at kohlhoff dot com)
6//
7// Distributed under the Boost Software License, Version 1.0. (See accompanying
8// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
9//
10
11#include <iostream>
12#include <istream>
13#include <ostream>
14#include <string>
15#include <regex>
16#include <boost/algorithm/string.hpp>
17#include <boost/asio.hpp>
18#include <boost/asio/ssl.hpp>
19#include <fc/variant.hpp>
20#include <fc/io/json.hpp>
25#include <boost/asio/ssl/rfc2818_verification.hpp>
26#include "httpc.hpp"
27
28using boost::asio::ip::tcp;
29using namespace sysio::chain;
30namespace sysio { namespace client { namespace http {
31
32 namespace detail {
34 public:
35 boost::asio::io_service ios;
36 };
37
38 void http_context_deleter::operator()(http_context_impl* p) const {
39 delete p;
40 }
41 }
42
46
47 void do_connect(tcp::socket& sock, const resolved_url& url) {
48 // Get a list of endpoints corresponding to the server name.
49 vector<tcp::endpoint> endpoints;
50 endpoints.reserve(url.resolved_addresses.size());
51 for (const auto& addr: url.resolved_addresses) {
52 endpoints.emplace_back(boost::asio::ip::make_address(addr), url.resolved_port);
53 }
54 boost::asio::connect(sock, endpoints);
55 }
56
57 template<class T>
58 std::string do_txrx(T& socket, boost::asio::streambuf& request_buff, unsigned int& status_code) {
59 // Send the request.
60 boost::asio::write(socket, request_buff);
61
62 // Read the response status line. The response streambuf will automatically
63 // grow to accommodate the entire line. The growth may be limited by passing
64 // a maximum size to the streambuf constructor.
65 boost::asio::streambuf response;
66 boost::asio::read_until(socket, response, "\r\n");
67
68 // Check that response is OK.
69 std::istream response_stream(&response);
70 std::string http_version;
71 response_stream >> http_version;
72 response_stream >> status_code;
73
74 std::string status_message;
75 std::getline(response_stream, status_message);
76 SYS_ASSERT( !(!response_stream || http_version.substr(0, 5) != "HTTP/"), invalid_http_response, "Invalid Response" );
77
78 // Read the response headers, which are terminated by a blank line.
79 boost::asio::read_until(socket, response, "\r\n\r\n");
80
81 // Process the response headers.
82 std::string header;
83 int response_content_length = -1;
84 std::regex clregex(R"xx(^content-length:\s+(\d+))xx", std::regex_constants::icase);
85 while (std::getline(response_stream, header) && header != "\r") {
86 std::smatch match;
87 if(std::regex_search(header, match, clregex))
88 response_content_length = std::stoi(match[1]);
89 }
90
91 // Attempt to read the response body using the length indicated by the
92 // Content-length header. If the header was not present just read all available bytes.
93 if( response_content_length != -1 ) {
94 response_content_length -= response.size();
95 if( response_content_length > 0 )
96 boost::asio::read(socket, response, boost::asio::transfer_exactly(response_content_length));
97 } else {
98 boost::system::error_code ec;
99 boost::asio::read(socket, response, boost::asio::transfer_all(), ec);
100 SYS_ASSERT(!ec || ec == boost::asio::ssl::error::stream_truncated, http_exception, "Unable to read http response: ${err}", ("err",ec.message()));
101 }
102
103 std::stringstream re;
104 re << &response;
105 return re.str();
106 }
107
108 parsed_url parse_url( const string& server_url ) {
109 parsed_url res;
110
111 //unix socket doesn't quite follow classical "URL" rules so deal with it manually
112 if(boost::algorithm::starts_with(server_url, "unix://")) {
113 res.scheme = "unix";
114 res.server = server_url.substr(strlen("unix://"));
115 return res;
116 }
117
118 //via rfc3986 and modified a bit to suck out the port number
119 //Sadly this doesn't work for ipv6 addresses
120 std::regex rgx(R"xx(^(([^:/?#]+):)?(//([^:/?#]*)(:(\d+))?)?([^?#]*)(\?([^#]*))?(#(.*))?)xx");
121 std::smatch match;
122 if(std::regex_search(server_url.begin(), server_url.end(), match, rgx)) {
123 res.scheme = match[2];
124 res.server = match[4];
125 res.port = match[6];
126 res.path = match[7];
127 }
128 if(res.scheme != "http" && res.scheme != "https")
129 SYS_THROW(fail_to_resolve_host, "Unrecognized URL scheme (${s}) in URL \"${u}\"", ("s", res.scheme)("u", server_url));
130 if(res.server.empty())
131 SYS_THROW(fail_to_resolve_host, "No server parsed from URL \"${u}\"", ("u", server_url));
132 if(res.port.empty())
133 res.port = res.scheme == "http" ? "80" : "443";
134 boost::trim_right_if(res.path, boost::is_any_of("/"));
135 return res;
136 }
137
139 if(url.scheme == "unix")
140 return resolved_url(url);
141
142 tcp::resolver resolver(context->ios);
143 boost::system::error_code ec;
144 auto result = resolver.resolve(tcp::v4(), url.server, url.port, ec);
145 if (ec) {
146 SYS_THROW(fail_to_resolve_host, "Error resolving \"${server}:${port}\" : ${m}", ("server", url.server)("port",url.port)("m",ec.message()));
147 }
148
149 // non error results are guaranteed to return a non-empty range
150 vector<string> resolved_addresses;
151 resolved_addresses.reserve(result.size());
152 std::optional<uint16_t> resolved_port;
153 bool is_loopback = true;
154
155 for(const auto& r : result) {
156 const auto& addr = r.endpoint().address();
157 if (addr.is_v6()) continue;
158 uint16_t port = r.endpoint().port();
159 resolved_addresses.emplace_back(addr.to_string());
160 is_loopback = is_loopback && addr.is_loopback();
161
162 if (resolved_port) {
163 SYS_ASSERT(*resolved_port == port, resolved_to_multiple_ports, "Service name \"${port}\" resolved to multiple ports and this is not supported!", ("port",url.port));
164 } else {
165 resolved_port = port;
166 }
167 }
168
169 return resolved_url(url, std::move(resolved_addresses), *resolved_port, is_loopback);
170 }
171
173 // common practice is to only make the port explicit when it is the non-default port
174 if (
175 (url.scheme == "https" && url.resolved_port == 443) ||
176 (url.scheme == "http" && url.resolved_port == 80)
177 ) {
178 return url.server;
179 } else {
180 return url.server + ":" + url.port;
181 }
182 }
183
185 const fc::variant& postdata,
186 bool print_request,
187 bool print_response ) {
188 std::string postjson;
189 if( !postdata.is_null() ) {
191 }
192
193 const auto& url = cp.url;
194
195 boost::asio::streambuf request;
196 std::ostream request_stream(&request);
197 auto host_header_value = format_host_header(url);
198 request_stream << "POST " << url.path << " HTTP/1.0\r\n";
199 request_stream << "Host: " << host_header_value << "\r\n";
200 request_stream << "content-length: " << postjson.size() << "\r\n";
201 request_stream << "Accept: */*\r\n";
202 request_stream << "Connection: close\r\n";
203 // append more customized headers
204 std::vector<string>::iterator itr;
205 for (itr = cp.headers.begin(); itr != cp.headers.end(); itr++) {
206 request_stream << *itr << "\r\n";
207 }
208 request_stream << "\r\n";
209 request_stream << postjson;
210
211 if ( print_request ) {
212 string s(request.size(), '\0');
213 buffer_copy(boost::asio::buffer(s), request.data());
214 std::cerr << "REQUEST:" << std::endl
215 << "---------------------" << std::endl
216 << s << std::endl
217 << "---------------------" << std::endl;
218 }
219
220 unsigned int status_code;
221 std::string re;
222
223 try {
224 if(url.scheme == "unix") {
225 boost::asio::local::stream_protocol::socket unix_socket(cp.context->ios);
226 unix_socket.connect(boost::asio::local::stream_protocol::endpoint(url.server));
227 re = do_txrx(unix_socket, request, status_code);
228 }
229 else if(url.scheme == "http") {
230 tcp::socket socket(cp.context->ios);
231 do_connect(socket, url);
232 re = do_txrx(socket, request, status_code);
233 }
234 else { //https
235 boost::asio::ssl::context ssl_context(boost::asio::ssl::context::sslv23_client);
237
238 boost::asio::ssl::stream<boost::asio::ip::tcp::socket> socket(cp.context->ios, ssl_context);
239 SSL_set_tlsext_host_name(socket.native_handle(), url.server.c_str());
240 if(cp.verify_cert) {
241 socket.set_verify_mode(boost::asio::ssl::verify_peer);
242 socket.set_verify_callback(boost::asio::ssl::rfc2818_verification(url.server));
243 }
244 do_connect(socket.next_layer(), url);
245 socket.handshake(boost::asio::ssl::stream_base::client);
246 re = do_txrx(socket, request, status_code);
247 //try and do a clean shutdown; but swallow if this fails (other side could have already gave TCP the ax)
248 try {socket.shutdown();} catch(...) {}
249 }
250 } catch ( invalid_http_request& e ) {
251 e.append_log( FC_LOG_MESSAGE( info, "Please verify this url is valid: ${url}", ("url", url.scheme + "://" + url.server + ":" + url.port + url.path) ) );
252 e.append_log( FC_LOG_MESSAGE( info, "If the condition persists, please contact the RPC server administrator for ${server}!", ("server", url.server) ) );
253 throw;
254 }
255
256 const auto response_result = fc::json::from_string(re);
257 if( print_response ) {
258 std::cerr << "RESPONSE:" << std::endl
259 << "---------------------" << std::endl
260 << fc::json::to_pretty_string( response_result ) << std::endl
261 << "---------------------" << std::endl;
262 }
263 if( status_code == 200 || status_code == 201 || status_code == 202 ) {
264 return response_result;
265 } else if( status_code == 404 ) {
266 // Unknown endpoint
267 if (url.path.compare(0, chain_func_base.size(), chain_func_base) == 0) {
268 throw chain::missing_chain_api_plugin_exception(FC_LOG_MESSAGE(error, "Chain API plugin is not enabled"));
269 } else if (url.path.compare(0, wallet_func_base.size(), wallet_func_base) == 0) {
270 throw chain::missing_wallet_api_plugin_exception(FC_LOG_MESSAGE(error, "Wallet is not available"));
271 } else if (url.path.compare(0, history_func_base.size(), history_func_base) == 0) {
272 throw chain::missing_history_api_plugin_exception(FC_LOG_MESSAGE(error, "History API plugin is not enabled"));
273 } else if (url.path.compare(0, net_func_base.size(), net_func_base) == 0) {
274 throw chain::missing_net_api_plugin_exception(FC_LOG_MESSAGE(error, "Net API plugin is not enabled"));
275 } else if (url.path.compare(0, trace_api_func_base.size(), trace_api_func_base) == 0) {
276 if ( re.find("Trace API:") == string::npos ) {
277 throw chain::missing_trace_api_plugin_exception(FC_LOG_MESSAGE(error, "Trace API plugin is not enabled"));
278 }
279 }
280 } else {
281 auto &&error_info = response_result.as<sysio::error_results>().error;
282 // Construct fc exception from error
283 const auto &error_details = error_info.details;
284
285 fc::log_messages logs;
286 for (auto itr = error_details.begin(); itr != error_details.end(); itr++) {
287 const auto& context = fc::log_context(fc::log_level::error, itr->file.data(), itr->line_number, itr->method.data());
288 logs.emplace_back(fc::log_message(context, itr->message));
289 }
290
291 throw fc::exception(logs, error_info.code, error_info.name, error_info.what);
292 }
293
294 SYS_ASSERT( status_code == 200, http_request_fail, "Error code ${c}\n: ${msg}\n", ("c", status_code)("msg", re) );
295 return response_result;
296 }
297}}}
const mie::Vuint & p
Definition bn.cpp:27
const mie::Vuint & r
Definition bn.cpp:28
#define SYS_THROW(exc_type, FORMAT,...)
#define SYS_ASSERT(expr, exc_type, FORMAT,...)
Definition exceptions.hpp:7
Used to generate a useful error report when an exception is thrown.
Definition exception.hpp:58
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 string to_pretty_string(const variant &v, const yield_function_t &yield, const output_formatting format=output_formatting::stringify_large_ints_and_doubles)
Definition json.cpp:775
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
provides information about where and when a log message was generated.
aggregates a message along with the context and associated meta-information.
static constexpr time_point maximum()
Definition time.hpp:46
opath path() const
Definition url.cpp:182
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
bool is_null() const
Definition variant.cpp:309
#define FC_LOG_MESSAGE(LOG_LEVEL, FORMAT,...)
A helper method for generating log messages.
std::vector< log_message > log_messages
void add_platform_root_cas_to_context(boost::asio::ssl::context &ctx)
std::string do_txrx(T &socket, boost::asio::streambuf &request_buff, unsigned int &status_code)
Definition httpc.cpp:58
fc::variant do_http_call(const connection_param &cp, const fc::variant &postdata, bool print_request, bool print_response)
Definition httpc.cpp:184
const string trace_api_func_base
Definition httpc.hpp:108
std::unique_ptr< detail::http_context_impl, detail::http_context_deleter > http_context
Definition httpc.hpp:15
void do_connect(tcp::socket &sock, const resolved_url &url)
Definition httpc.cpp:47
const string chain_func_base
Definition httpc.hpp:81
http_context create_http_context()
Definition httpc.cpp:43
parsed_url parse_url(const string &server_url)
Definition httpc.cpp:108
const string wallet_func_base
Definition httpc.hpp:123
string format_host_header(const resolved_url &url)
Definition httpc.cpp:172
const string net_func_base
Definition httpc.hpp:116
const string history_func_base
Definition httpc.hpp:107
resolved_url resolve_url(const http_context &context, const parsed_url &url)
Definition httpc.cpp:138
#define T(meth, val, expected)
bool print_request
Definition main.cpp:187
bool print_response
Definition main.cpp:188
unsigned short uint16_t
Definition stdint.h:125
std::vector< string > & headers
Definition httpc.hpp:58
const http_context & context
Definition httpc.hpp:55
Structure used to create JSON error responses.
char * s