httpc.cpp 11.9 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14
//
// sync_client.cpp
// ~~~~~~~~~~~~~~~
//
// Copyright (c) 2003-2012 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//

#include <iostream>
#include <istream>
#include <ostream>
#include <string>
15
#include <regex>
16
#include <boost/algorithm/string.hpp>
17
#include <boost/asio.hpp>
18
#include <boost/asio/ssl.hpp>
19 20
#include <fc/variant.hpp>
#include <fc/io/json.hpp>
21
#include <eosio/chain/exceptions.hpp>
22 23
#include <eosio/http_plugin/http_plugin.hpp>
#include <eosio/chain_plugin/chain_plugin.hpp>
24
#include <boost/asio/ssl/rfc2818_verification.hpp>
25
#include "httpc.hpp"
26 27

using boost::asio::ip::tcp;
28
using namespace eosio::chain;
29
namespace eosio { namespace client { namespace http {
B
Bart Wyatt 已提交
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46

   namespace detail {
      class http_context_impl {
         public:
            boost::asio::io_service ios;
      };

      void http_context_deleter::operator()(http_context_impl* p) const {
         delete p;
      }
   }

   http_context create_http_context() {
      return http_context(new detail::http_context_impl, detail::http_context_deleter());
   }

   void do_connect(tcp::socket& sock, const resolved_url& url) {
47
      // Get a list of endpoints corresponding to the server name.
B
Bart Wyatt 已提交
48 49 50 51 52 53
      vector<tcp::endpoint> endpoints;
      endpoints.reserve(url.resolved_addresses.size());
      for (const auto& addr: url.resolved_addresses) {
         endpoints.emplace_back(boost::asio::ip::make_address(addr), url.resolved_port);
      }
      boost::asio::connect(sock, endpoints);
54
   }
55

56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
   template<class T>
   std::string do_txrx(T& socket, boost::asio::streambuf& request_buff, unsigned int& status_code) {
      // Send the request.
      boost::asio::write(socket, request_buff);

      // Read the response status line. The response streambuf will automatically
      // grow to accommodate the entire line. The growth may be limited by passing
      // a maximum size to the streambuf constructor.
      boost::asio::streambuf response;
      boost::asio::read_until(socket, response, "\r\n");

      // Check that response is OK.
      std::istream response_stream(&response);
      std::string http_version;
      response_stream >> http_version;
      response_stream >> status_code;
72 73 74

      EOS_ASSERT( status_code != 400, invalid_http_request, "The server has rejected the request as invalid!");

75 76
      std::string status_message;
      std::getline(response_stream, status_message);
77
      EOS_ASSERT( !(!response_stream || http_version.substr(0, 5) != "HTTP/"), invalid_http_response, "Invalid Response" );
78 79 80 81 82 83 84 85 86 87 88 89 90

      // Read the response headers, which are terminated by a blank line.
      boost::asio::read_until(socket, response, "\r\n\r\n");

      // Process the response headers.
      std::string header;
      int response_content_length = -1;
      std::regex clregex(R"xx(^content-length:\s+(\d+))xx", std::regex_constants::icase);
      while (std::getline(response_stream, header) && header != "\r") {
         std::smatch match;
         if(std::regex_search(header, match, clregex))
            response_content_length = std::stoi(match[1]);
      }
91
      EOS_ASSERT(response_content_length >= 0, invalid_http_response, "Invalid content-length response");
92

93 94 95 96 97 98 99 100 101 102 103 104
      std::stringstream re;
      // Write whatever content we already have to output.
      response_content_length -= response.size();
      if (response.size() > 0)
         re << &response;

      boost::asio::read(socket, response, boost::asio::transfer_exactly(response_content_length));
      re << &response;

      return re.str();
   }

105 106 107
   parsed_url parse_url( const string& server_url ) {
      parsed_url res;

108 109 110 111 112 113 114
      //unix socket doesn't quite follow classical "URL" rules so deal with it manually
      if(boost::algorithm::starts_with(server_url, "unix://")) {
         res.scheme = "unix";
         res.server = server_url.substr(strlen("unix://"));
         return res;
      }

115 116 117 118 119 120 121 122
      //via rfc3986 and modified a bit to suck out the port number
      //Sadly this doesn't work for ipv6 addresses
      std::regex rgx(R"xx(^(([^:/?#]+):)?(//([^:/?#]*)(:(\d+))?)?([^?#]*)(\?([^#]*))?(#(.*))?)xx");
      std::smatch match;
      if(std::regex_search(server_url.begin(), server_url.end(), match, rgx)) {
         res.scheme = match[2];
         res.server = match[4];
         res.port = match[6];
B
Bart Wyatt 已提交
123
         res.path = match[7];
124 125
      }
      if(res.scheme != "http" && res.scheme != "https")
126
         EOS_THROW(fail_to_resolve_host, "Unrecognized URL scheme (${s}) in URL \"${u}\"", ("s", res.scheme)("u", server_url));
127
      if(res.server.empty())
128
         EOS_THROW(fail_to_resolve_host, "No server parsed from URL \"${u}\"", ("u", server_url));
129
      if(res.port.empty())
130
         res.port = res.scheme == "http" ? "80" : "443";
B
Bart Wyatt 已提交
131
      boost::trim_right_if(res.path, boost::is_any_of("/"));
132 133 134
      return res;
   }

B
Bart Wyatt 已提交
135
   resolved_url resolve_url( const http_context& context, const parsed_url& url ) {
136 137 138
      if(url.scheme == "unix")
         return resolved_url(url);

B
Bart Wyatt 已提交
139 140
      tcp::resolver resolver(context->ios);
      boost::system::error_code ec;
141
      auto result = resolver.resolve(tcp::v4(), url.server, url.port, ec);
B
Bart Wyatt 已提交
142
      if (ec) {
143
         EOS_THROW(fail_to_resolve_host, "Error resolving \"${server}:${port}\" : ${m}", ("server", url.server)("port",url.port)("m",ec.message()));
B
Bart Wyatt 已提交
144 145 146 147 148 149 150 151 152 153
      }

      // non error results are guaranteed to return a non-empty range
      vector<string> resolved_addresses;
      resolved_addresses.reserve(result.size());
      optional<uint16_t> resolved_port;
      bool is_loopback = true;

      for(const auto& r : result) {
         const auto& addr = r.endpoint().address();
T
t10471 已提交
154
         if (addr.is_v6()) continue;
B
Bart Wyatt 已提交
155 156 157 158 159
         uint16_t port = r.endpoint().port();
         resolved_addresses.emplace_back(addr.to_string());
         is_loopback = is_loopback && addr.is_loopback();

         if (resolved_port) {
160
            EOS_ASSERT(*resolved_port == port, resolved_to_multiple_ports, "Service name \"${port}\" resolved to multiple ports and this is not supported!", ("port",url.port));
B
Bart Wyatt 已提交
161 162 163 164 165 166 167 168
         } else {
            resolved_port = port;
         }
      }

      return resolved_url(url, std::move(resolved_addresses), *resolved_port, is_loopback);
   }

169 170 171 172 173 174 175 176 177 178 179 180
   string format_host_header(const resolved_url& url) {
      // common practice is to only make the port explicit when it is the non-default port
      if (
         (url.scheme == "https" && url.resolved_port == 443) ||
         (url.scheme == "http" && url.resolved_port == 80)
      ) {
         return url.server;
      } else {
         return url.server + ":" + url.port;
      }
   }

181
   fc::variant do_http_call( const connection_param& cp,
A
Anton Perkov 已提交
182
                             const fc::variant& postdata,
183 184
                             bool print_request,
                             bool print_response ) {
185
   std::string postjson;
186 187 188
   if( !postdata.is_null() ) {
      postjson = print_request ? fc::json::to_pretty_string( postdata ) : fc::json::to_string( postdata );
   }
189

B
Bart Wyatt 已提交
190
   const auto& url = cp.url;
191 192 193

   boost::asio::streambuf request;
   std::ostream request_stream(&request);
194
   auto host_header_value = format_host_header(url);
B
Bart Wyatt 已提交
195
   request_stream << "POST " << url.path << " HTTP/1.0\r\n";
196
   request_stream << "Host: " << host_header_value << "\r\n";
197 198
   request_stream << "content-length: " << postjson.size() << "\r\n";
   request_stream << "Accept: */*\r\n";
199
   request_stream << "Connection: close\r\n";
200
   request_stream << "\r\n";
201 202 203 204 205
   // append more customized headers
   std::vector<string>::iterator itr;
   for (itr = cp.headers.begin(); itr != cp.headers.end(); itr++) {
      request_stream << *itr << "\r\n";
   }
206
   request_stream << postjson;
207

208
   if ( print_request ) {
A
Anton Perkov 已提交
209 210 211 212
      string s(request.size(), '\0');
      buffer_copy(boost::asio::buffer(s), request.data());
      std::cerr << "REQUEST:" << std::endl
                << "---------------------" << std::endl
213
                << s << std::endl
A
Anton Perkov 已提交
214 215 216
                << "---------------------" << std::endl;
   }

217 218 219
   unsigned int status_code;
   std::string re;

220
   try {
221 222 223 224 225 226
      if(url.scheme == "unix") {
         boost::asio::local::stream_protocol::socket unix_socket(cp.context->ios);
         unix_socket.connect(boost::asio::local::stream_protocol::endpoint(url.server));
         re = do_txrx(unix_socket, request, status_code);
      }
      else if(url.scheme == "http") {
227 228 229 230 231 232
         tcp::socket socket(cp.context->ios);
         do_connect(socket, url);
         re = do_txrx(socket, request, status_code);
      }
      else { //https
         boost::asio::ssl::context ssl_context(boost::asio::ssl::context::sslv23_client);
233
#if defined( __APPLE__ )
234 235
         //TODO: this is undocumented/not supported; fix with keychain based approach
         ssl_context.load_verify_file("/private/etc/ssl/cert.pem");
236
#elif defined( _WIN32 )
237
         EOS_THROW(http_exception, "HTTPS on Windows not supported");
238
#else
239
         ssl_context.set_default_verify_paths();
240 241
#endif

242 243
         boost::asio::ssl::stream<boost::asio::ip::tcp::socket> socket(cp.context->ios, ssl_context);
         SSL_set_tlsext_host_name(socket.native_handle(), url.server.c_str());
244
         if(cp.verify_cert) {
245
            socket.set_verify_mode(boost::asio::ssl::verify_peer);
246 247
            socket.set_verify_callback(boost::asio::ssl::rfc2818_verification(url.server));
         }
248 249 250 251 252 253 254 255 256 257
         do_connect(socket.next_layer(), url);
         socket.handshake(boost::asio::ssl::stream_base::client);
         re = do_txrx(socket, request, status_code);
         //try and do a clean shutdown; but swallow if this fails (other side could have already gave TCP the ax)
         try {socket.shutdown();} catch(...) {}
      }
   } catch ( invalid_http_request& e ) {
      e.append_log( FC_LOG_MESSAGE( info, "Please verify this url is valid: ${url}", ("url", url.scheme + "://" + url.server + ":" + url.port + url.path) ) );
      e.append_log( FC_LOG_MESSAGE( info, "If the condition persists, please contact the RPC server administrator for ${server}!", ("server", url.server) ) );
      throw;
258
   }
A
arhag 已提交
259

260
   const auto response_result = fc::json::from_string(re);
261 262 263 264 265 266
   if( print_response ) {
      std::cerr << "RESPONSE:" << std::endl
                << "---------------------" << std::endl
                << fc::json::to_pretty_string( response_result ) << std::endl
                << "---------------------" << std::endl;
   }
267 268 269 270
   if( status_code == 200 || status_code == 201 || status_code == 202 ) {
      return response_result;
   } else if( status_code == 404 ) {
      // Unknown endpoint
B
Bart Wyatt 已提交
271
      if (url.path.compare(0, chain_func_base.size(), chain_func_base) == 0) {
272
         throw chain::missing_chain_api_plugin_exception(FC_LOG_MESSAGE(error, "Chain API plugin is not enabled"));
B
Bart Wyatt 已提交
273
      } else if (url.path.compare(0, wallet_func_base.size(), wallet_func_base) == 0) {
274
         throw chain::missing_wallet_api_plugin_exception(FC_LOG_MESSAGE(error, "Wallet is not available"));
275
      } else if (url.path.compare(0, history_func_base.size(), history_func_base) == 0) {
276
         throw chain::missing_history_api_plugin_exception(FC_LOG_MESSAGE(error, "History API plugin is not enabled"));
B
Bart Wyatt 已提交
277
      } else if (url.path.compare(0, net_func_base.size(), net_func_base) == 0) {
278 279 280 281 282 283 284 285 286 287 288
         throw chain::missing_net_api_plugin_exception(FC_LOG_MESSAGE(error, "Net API plugin is not enabled"));
      }
   } else {
      auto &&error_info = response_result.as<eosio::error_results>().error;
      // Construct fc exception from error
      const auto &error_details = error_info.details;

      fc::log_messages logs;
      for (auto itr = error_details.begin(); itr != error_details.end(); itr++) {
         const auto& context = fc::log_context(fc::log_level::error, itr->file.data(), itr->line_number, itr->method.data());
         logs.emplace_back(fc::log_message(context, itr->message));
289 290
      }

291 292 293
      throw fc::exception(logs, error_info.code, error_info.name, error_info.what);
   }

294
   EOS_ASSERT( status_code == 200, http_request_fail, "Error code ${c}\n: ${msg}\n", ("c", status_code)("msg", re) );
295
   return response_result;
296
   }
297
}}}