1 #ifndef CLIENT_HTTP_HPP 2 #define CLIENT_HTTP_HPP 3 4 #include <boost/asio.hpp> 5 #include <boost/utility/string_ref.hpp> 6 #include <boost/algorithm/string/predicate.hpp> 7 #include <boost/functional/hash.hpp> 8 9 #include <unordered_map> 10 #include <map> 11 #include <random> 12 #include <mutex> 13 #include <type_traits> 14 15 #ifndef CASE_INSENSITIVE_EQUALS_AND_HASH 16 #define CASE_INSENSITIVE_EQUALS_AND_HASH 17 //Based on http://www.boost.org/doc/libs/1_60_0/doc/html/unordered/hash_equality.html 18 class case_insensitive_equals { 19 public: operator ()(const std::string & key1,const std::string & key2) const20 bool operator()(const std::string &key1, const std::string &key2) const { 21 return boost::algorithm::iequals(key1, key2); 22 } 23 }; 24 class case_insensitive_hash { 25 public: operator ()(const std::string & key) const26 size_t operator()(const std::string &key) const { 27 std::size_t seed=0; 28 for(auto &c: key) 29 boost::hash_combine(seed, std::tolower(c)); 30 return seed; 31 } 32 }; 33 #endif 34 35 namespace SimpleWeb { 36 template <class socket_type> 37 class Client; 38 39 template <class socket_type> 40 class ClientBase { 41 public: ~ClientBase()42 virtual ~ClientBase() {} 43 44 class Response { 45 friend class ClientBase<socket_type>; 46 friend class Client<socket_type>; 47 public: 48 std::string http_version, status_code; 49 50 std::istream content; 51 52 std::unordered_multimap<std::string, std::string, case_insensitive_hash, case_insensitive_equals> header; 53 54 private: 55 boost::asio::streambuf content_buffer; 56 Response()57 Response(): content(&content_buffer) {} 58 }; 59 60 class Config { 61 friend class ClientBase<socket_type>; 62 private: Config()63 Config() {} 64 public: 65 /// Set timeout on requests in seconds. Default value: 0 (no timeout). 66 size_t timeout=0; 67 /// Set connect timeout in seconds. Default value: 0 (Config::timeout is then used instead). 68 size_t timeout_connect=0; 69 /// Set proxy server (server:port) 70 std::string proxy_server; 71 }; 72 73 /// Set before calling request 74 Config config; 75 76 std::shared_ptr<Response> request(const std::string& request_type, const std::string& path="/", boost::string_ref content="", 77 const std::map<std::string, std::string>& header=std::map<std::string, std::string>()) { 78 auto corrected_path=path; 79 if(corrected_path=="") 80 corrected_path="/"; 81 if(!config.proxy_server.empty() && std::is_same<socket_type, boost::asio::ip::tcp::socket>::value) 82 corrected_path="http://"+host+':'+std::to_string(port)+corrected_path; 83 84 boost::asio::streambuf write_buffer; 85 std::ostream write_stream(&write_buffer); 86 write_stream << request_type << " " << corrected_path << " HTTP/1.1\r\n"; 87 write_stream << "Host: " << host << "\r\n"; 88 for(auto& h: header) { 89 write_stream << h.first << ": " << h.second << "\r\n"; 90 } 91 if(content.size()>0) 92 write_stream << "Content-Length: " << content.size() << "\r\n"; 93 write_stream << "\r\n"; 94 95 connect(); 96 97 auto timer=get_timeout_timer(); 98 boost::asio::async_write(*socket, write_buffer, __anon78824fe30102(const boost::system::error_code &ec, size_t ) 99 [this, &content, timer](const boost::system::error_code &ec, size_t /*bytes_transferred*/) { 100 if(timer) 101 timer->cancel(); 102 if(!ec) { 103 if(!content.empty()) { 104 auto timer=get_timeout_timer(); 105 boost::asio::async_write(*socket, boost::asio::buffer(content.data(), content.size()), 106 [this, timer](const boost::system::error_code &ec, size_t /*bytes_transferred*/) { 107 if(timer) 108 timer->cancel(); 109 if(ec) { 110 std::lock_guard<std::mutex> lock(socket_mutex); 111 this->socket=nullptr; 112 throw boost::system::system_error(ec); 113 } 114 }); 115 } 116 } 117 else { 118 std::lock_guard<std::mutex> lock(socket_mutex); 119 socket=nullptr; 120 throw boost::system::system_error(ec); 121 } 122 }); 123 io_service.reset(); 124 io_service.run(); 125 126 return request_read(); 127 } 128 129 std::shared_ptr<Response> request(const std::string& request_type, const std::string& path, std::iostream& content, 130 const std::map<std::string, std::string>& header=std::map<std::string, std::string>()) { 131 auto corrected_path=path; 132 if(corrected_path=="") 133 corrected_path="/"; 134 if(!config.proxy_server.empty() && std::is_same<socket_type, boost::asio::ip::tcp::socket>::value) 135 corrected_path="http://"+host+':'+std::to_string(port)+corrected_path; 136 137 content.seekp(0, std::ios::end); 138 auto content_length=content.tellp(); 139 content.seekp(0, std::ios::beg); 140 141 boost::asio::streambuf write_buffer; 142 std::ostream write_stream(&write_buffer); 143 write_stream << request_type << " " << corrected_path << " HTTP/1.1\r\n"; 144 write_stream << "Host: " << host << "\r\n"; 145 for(auto& h: header) { 146 write_stream << h.first << ": " << h.second << "\r\n"; 147 } 148 if(content_length>0) 149 write_stream << "Content-Length: " << content_length << "\r\n"; 150 write_stream << "\r\n"; 151 if(content_length>0) 152 write_stream << content.rdbuf(); 153 154 connect(); 155 156 auto timer=get_timeout_timer(); 157 boost::asio::async_write(*socket, write_buffer, __anon78824fe30302(const boost::system::error_code &ec, size_t ) 158 [this, timer](const boost::system::error_code &ec, size_t /*bytes_transferred*/) { 159 if(timer) 160 timer->cancel(); 161 if(ec) { 162 std::lock_guard<std::mutex> lock(socket_mutex); 163 socket=nullptr; 164 throw boost::system::system_error(ec); 165 } 166 }); 167 io_service.reset(); 168 io_service.run(); 169 170 return request_read(); 171 } 172 close()173 void close() { 174 std::lock_guard<std::mutex> lock(socket_mutex); 175 if(socket) { 176 boost::system::error_code ec; 177 socket->lowest_layer().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec); 178 socket->lowest_layer().close(); 179 } 180 } 181 182 protected: 183 boost::asio::io_service io_service; 184 boost::asio::ip::tcp::resolver resolver; 185 186 std::unique_ptr<socket_type> socket; 187 std::mutex socket_mutex; 188 189 std::string host; 190 unsigned short port; 191 ClientBase(const std::string & host_port,unsigned short default_port)192 ClientBase(const std::string& host_port, unsigned short default_port) : resolver(io_service) { 193 auto parsed_host_port=parse_host_port(host_port, default_port); 194 host=parsed_host_port.first; 195 port=parsed_host_port.second; 196 } 197 parse_host_port(const std::string & host_port,unsigned short default_port)198 std::pair<std::string, unsigned short> parse_host_port(const std::string &host_port, unsigned short default_port) { 199 std::pair<std::string, unsigned short> parsed_host_port; 200 size_t host_end=host_port.find(':'); 201 if(host_end==std::string::npos) { 202 parsed_host_port.first=host_port; 203 parsed_host_port.second=default_port; 204 } 205 else { 206 parsed_host_port.first=host_port.substr(0, host_end); 207 parsed_host_port.second=static_cast<unsigned short>(stoul(host_port.substr(host_end+1))); 208 } 209 return parsed_host_port; 210 } 211 212 virtual void connect()=0; 213 get_timeout_timer(size_t timeout=0)214 std::shared_ptr<boost::asio::deadline_timer> get_timeout_timer(size_t timeout=0) { 215 if(timeout==0) 216 timeout=config.timeout; 217 if(timeout==0) 218 return nullptr; 219 220 auto timer=std::make_shared<boost::asio::deadline_timer>(io_service); 221 timer->expires_from_now(boost::posix_time::seconds(timeout)); 222 timer->async_wait([this](const boost::system::error_code& ec) { 223 if(!ec) { 224 close(); 225 } 226 }); 227 return timer; 228 } 229 parse_response_header(const std::shared_ptr<Response> & response) const230 void parse_response_header(const std::shared_ptr<Response> &response) const { 231 std::string line; 232 getline(response->content, line); 233 size_t version_end=line.find(' '); 234 if(version_end!=std::string::npos) { 235 if(5<line.size()) 236 response->http_version=line.substr(5, version_end-5); 237 if((version_end+1)<line.size()) 238 response->status_code=line.substr(version_end+1, line.size()-(version_end+1)-1); 239 240 getline(response->content, line); 241 size_t param_end; 242 while((param_end=line.find(':'))!=std::string::npos) { 243 size_t value_start=param_end+1; 244 if((value_start)<line.size()) { 245 if(line[value_start]==' ') 246 value_start++; 247 if(value_start<line.size()) 248 response->header.insert(std::make_pair(line.substr(0, param_end), line.substr(value_start, line.size()-value_start-1))); 249 } 250 251 getline(response->content, line); 252 } 253 } 254 } 255 request_read()256 std::shared_ptr<Response> request_read() { 257 std::shared_ptr<Response> response(new Response()); 258 259 boost::asio::streambuf chunked_streambuf; 260 261 auto timer=get_timeout_timer(); 262 boost::asio::async_read_until(*socket, response->content_buffer, "\r\n\r\n", 263 [this, &response, &chunked_streambuf, timer](const boost::system::error_code& ec, size_t bytes_transferred) { 264 if(timer) 265 timer->cancel(); 266 if(!ec) { 267 size_t num_additional_bytes=response->content_buffer.size()-bytes_transferred; 268 269 parse_response_header(response); 270 271 auto header_it=response->header.find("Content-Length"); 272 if(header_it!=response->header.end()) { 273 auto content_length=stoull(header_it->second); 274 if(content_length>num_additional_bytes) { 275 auto timer=get_timeout_timer(); 276 boost::asio::async_read(*socket, response->content_buffer, 277 boost::asio::transfer_exactly(content_length-num_additional_bytes), 278 [this, timer](const boost::system::error_code& ec, size_t /*bytes_transferred*/) { 279 if(timer) 280 timer->cancel(); 281 if(ec) { 282 std::lock_guard<std::mutex> lock(socket_mutex); 283 this->socket=nullptr; 284 throw boost::system::system_error(ec); 285 } 286 }); 287 } 288 } 289 else if((header_it=response->header.find("Transfer-Encoding"))!=response->header.end() && header_it->second=="chunked") { 290 request_read_chunked(response, chunked_streambuf); 291 } 292 else if(response->http_version<"1.1" || ((header_it=response->header.find("Connection"))!=response->header.end() && header_it->second=="close")) { 293 auto timer=get_timeout_timer(); 294 boost::asio::async_read(*socket, response->content_buffer, 295 [this, timer](const boost::system::error_code& ec, size_t /*bytes_transferred*/) { 296 if(timer) 297 timer->cancel(); 298 if(ec) { 299 std::lock_guard<std::mutex> lock(socket_mutex); 300 this->socket=nullptr; 301 if(ec!=boost::asio::error::eof) 302 throw boost::system::system_error(ec); 303 } 304 }); 305 } 306 } 307 else { 308 std::lock_guard<std::mutex> lock(socket_mutex); 309 socket=nullptr; 310 throw boost::system::system_error(ec); 311 } 312 }); 313 io_service.reset(); 314 io_service.run(); 315 316 return response; 317 } 318 request_read_chunked(const std::shared_ptr<Response> & response,boost::asio::streambuf & streambuf)319 void request_read_chunked(const std::shared_ptr<Response> &response, boost::asio::streambuf &streambuf) { 320 auto timer=get_timeout_timer(); 321 boost::asio::async_read_until(*socket, response->content_buffer, "\r\n", 322 [this, &response, &streambuf, timer](const boost::system::error_code& ec, size_t bytes_transferred) { 323 if(timer) 324 timer->cancel(); 325 if(!ec) { 326 std::string line; 327 getline(response->content, line); 328 bytes_transferred-=line.size()+1; 329 line.pop_back(); 330 std::streamsize length=stol(line, 0, 16); 331 332 auto num_additional_bytes=static_cast<std::streamsize>(response->content_buffer.size()-bytes_transferred); 333 334 auto post_process=[this, &response, &streambuf, length] { 335 std::ostream stream(&streambuf); 336 if(length>0) { 337 std::vector<char> buffer(static_cast<size_t>(length)); 338 response->content.read(&buffer[0], length); 339 stream.write(&buffer[0], length); 340 } 341 342 //Remove "\r\n" 343 response->content.get(); 344 response->content.get(); 345 346 if(length>0) 347 request_read_chunked(response, streambuf); 348 else { 349 std::ostream response_stream(&response->content_buffer); 350 response_stream << stream.rdbuf(); 351 } 352 }; 353 354 if((2+length)>num_additional_bytes) { 355 auto timer=get_timeout_timer(); 356 boost::asio::async_read(*socket, response->content_buffer, 357 boost::asio::transfer_exactly(2+length-num_additional_bytes), 358 [this, post_process, timer](const boost::system::error_code& ec, size_t /*bytes_transferred*/) { 359 if(timer) 360 timer->cancel(); 361 if(!ec) { 362 post_process(); 363 } 364 else { 365 std::lock_guard<std::mutex> lock(socket_mutex); 366 this->socket=nullptr; 367 throw boost::system::system_error(ec); 368 } 369 }); 370 } 371 else 372 post_process(); 373 } 374 else { 375 std::lock_guard<std::mutex> lock(socket_mutex); 376 socket=nullptr; 377 throw boost::system::system_error(ec); 378 } 379 }); 380 } 381 }; 382 383 template<class socket_type> 384 class Client : public ClientBase<socket_type> {}; 385 386 typedef boost::asio::ip::tcp::socket HTTP; 387 388 template<> 389 class Client<HTTP> : public ClientBase<HTTP> { 390 public: Client(const std::string & server_port_path)391 Client(const std::string& server_port_path) : ClientBase<HTTP>::ClientBase(server_port_path, 80) {} 392 393 protected: connect()394 void connect() { 395 if(!socket || !socket->is_open()) { 396 std::unique_ptr<boost::asio::ip::tcp::resolver::query> query; 397 if(config.proxy_server.empty()) 398 query=std::unique_ptr<boost::asio::ip::tcp::resolver::query>(new boost::asio::ip::tcp::resolver::query(host, std::to_string(port))); 399 else { 400 auto proxy_host_port=parse_host_port(config.proxy_server, 8080); 401 query=std::unique_ptr<boost::asio::ip::tcp::resolver::query>(new boost::asio::ip::tcp::resolver::query(proxy_host_port.first, std::to_string(proxy_host_port.second))); 402 } 403 resolver.async_resolve(*query, [this](const boost::system::error_code &ec, 404 boost::asio::ip::tcp::resolver::iterator it){ 405 if(!ec) { 406 { 407 std::lock_guard<std::mutex> lock(socket_mutex); 408 socket=std::unique_ptr<HTTP>(new HTTP(io_service)); 409 } 410 411 auto timer=get_timeout_timer(config.timeout_connect); 412 boost::asio::async_connect(*socket, it, [this, timer] 413 (const boost::system::error_code &ec, boost::asio::ip::tcp::resolver::iterator /*it*/){ 414 if(timer) 415 timer->cancel(); 416 if(!ec) { 417 boost::asio::ip::tcp::no_delay option(true); 418 this->socket->set_option(option); 419 } 420 else { 421 std::lock_guard<std::mutex> lock(socket_mutex); 422 this->socket=nullptr; 423 throw boost::system::system_error(ec); 424 } 425 }); 426 } 427 else { 428 std::lock_guard<std::mutex> lock(socket_mutex); 429 socket=nullptr; 430 throw boost::system::system_error(ec); 431 } 432 }); 433 io_service.reset(); 434 io_service.run(); 435 } 436 } 437 }; 438 } 439 440 #endif /* CLIENT_HTTP_HPP */ 441