Kea  1.5.0
client.cc
Go to the documentation of this file.
1 // Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
2 //
3 // This Source Code Form is subject to the terms of the Mozilla Public
4 // License, v. 2.0. If a copy of the MPL was not distributed with this
5 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 
7 #include <config.h>
8 
11 #include <asiolink/tcp_socket.h>
12 #include <http/client.h>
13 #include <http/http_log.h>
14 #include <http/http_messages.h>
15 #include <http/response_json.h>
16 #include <http/response_parser.h>
17 #include <boost/bind.hpp>
18 #include <boost/enable_shared_from_this.hpp>
19 #include <boost/weak_ptr.hpp>
20 #include <array>
21 #include <map>
22 #include <queue>
23 
24 #include <iostream>
25 
26 using namespace isc;
27 using namespace isc::asiolink;
28 using namespace http;
29 
30 namespace {
31 
35 constexpr size_t MAX_LOGGED_MESSAGE_SIZE = 1024;
36 
38 typedef boost::function<void(boost::system::error_code ec, size_t length)>
39 SocketCallbackFunction;
40 
46 class SocketCallback {
47 public:
48 
54  SocketCallback(SocketCallbackFunction socket_callback)
55  : callback_(socket_callback) {
56  }
57 
64  void operator()(boost::system::error_code ec, size_t length = 0) {
65  if (ec.value() == boost::asio::error::operation_aborted) {
66  return;
67  }
68  callback_(ec, length);
69  }
70 
71 private:
72 
74  SocketCallbackFunction callback_;
75 
76 };
77 
78 class ConnectionPool;
79 
81 typedef boost::shared_ptr<ConnectionPool> ConnectionPoolPtr;
82 
98 class Connection : public boost::enable_shared_from_this<Connection> {
99 public:
100 
107  explicit Connection(IOService& io_service, const ConnectionPoolPtr& conn_pool,
108  const Url& url);
109 
111  ~Connection();
112 
127  void doTransaction(const HttpRequestPtr& request, const HttpResponsePtr& response,
128  const long request_timeout, const HttpClient::RequestHandler& callback,
129  const HttpClient::ConnectHandler& connect_callback);
130 
132  void close();
133 
137  bool isTransactionOngoing() const;
138 
139 private:
140 
145  void resetState();
146 
156  void terminate(const boost::system::error_code& ec,
157  const std::string& parsing_error = "");
158 
162  void scheduleTimer(const long request_timeout);
163 
167  void doSend();
168 
172  void doReceive();
173 
182  void connectCallback(HttpClient::ConnectHandler connect_callback,
183  const boost::system::error_code& ec);
184 
194  void sendCallback(const boost::system::error_code& ec, size_t length);
195 
201  void receiveCallback(const boost::system::error_code& ec, size_t length);
202 
204  void timerCallback();
205 
210  boost::weak_ptr<ConnectionPool> conn_pool_;
211 
213  Url url_;
214 
217 
219  IntervalTimer timer_;
220 
222  HttpRequestPtr current_request_;
223 
225  HttpResponsePtr current_response_;
226 
228  HttpResponseParserPtr parser_;
229 
231  HttpClient::RequestHandler current_callback_;
232 
234  std::string buf_;
235 
237  std::array<char, 32768> input_buf_;
238 };
239 
241 typedef boost::shared_ptr<Connection> ConnectionPtr;
242 
250 class ConnectionPool : public boost::enable_shared_from_this<ConnectionPool> {
251 public:
252 
257  explicit ConnectionPool(IOService& io_service)
258  : io_service_(io_service), conns_(), queue_() {
259  }
260 
264  ~ConnectionPool() {
265  closeAll();
266  }
267 
281  bool getNextRequest(const Url& url,
282  HttpRequestPtr& request,
283  HttpResponsePtr& response,
284  long& request_timeout,
285  HttpClient::RequestHandler& callback,
286  HttpClient::ConnectHandler& connect_callback) {
287  // Check if there is a queue for this URL. If there is no queue, there
288  // is no request queued either.
289  auto it = queue_.find(url);
290  if (it != queue_.end()) {
291  // If the queue is non empty, we take the oldest request.
292  if (!it->second.empty()) {
293  RequestDescriptor desc = it->second.front();
294  it->second.pop();
295  request = desc.request_;
296  response = desc.response_;
297  request_timeout = desc.request_timeout_,
298  callback = desc.callback_;
299  connect_callback = desc.connect_callback_;
300  return (true);
301  }
302  }
303 
304  return (false);
305  }
306 
322  void queueRequest(const Url& url,
323  const HttpRequestPtr& request,
324  const HttpResponsePtr& response,
325  const long request_timeout,
326  const HttpClient::RequestHandler& request_callback,
327  const HttpClient::ConnectHandler& connect_callback) {
328  auto it = conns_.find(url);
329  if (it != conns_.end()) {
330  ConnectionPtr conn = it->second;
331  // There is a connection for this URL already. Check if it is idle.
332  if (conn->isTransactionOngoing()) {
333  // Connection is busy, so let's queue the request.
334  queue_[url].push(RequestDescriptor(request, response,
335  request_timeout,
336  request_callback,
337  connect_callback));
338 
339  } else {
340  // Connection is idle, so we can start the transaction.
341  conn->doTransaction(request, response, request_timeout,
342  request_callback, connect_callback);
343  }
344 
345  } else {
346  // There is no connection with this destination yet. Let's create
347  // it and start the transaction.
348  ConnectionPtr conn(new Connection(io_service_, shared_from_this(),
349  url));
350  conn->doTransaction(request, response, request_timeout, request_callback,
351  connect_callback);
352  conns_[url] = conn;
353  }
354  }
355 
360  void closeConnection(const Url& url) {
361  // Close connection for the specified URL.
362  auto conns_it = conns_.find(url);
363  if (conns_it != conns_.end()) {
364  conns_it->second->close();
365  conns_.erase(conns_it);
366  }
367 
368  // Remove requests from the queue.
369  auto queue_it = queue_.find(url);
370  if (queue_it != queue_.end()) {
371  queue_.erase(queue_it);
372  }
373  }
374 
377  void closeAll() {
378  for (auto conns_it = conns_.begin(); conns_it != conns_.end();
379  ++conns_it) {
380  conns_it->second->close();
381  }
382 
383  conns_.clear();
384  queue_.clear();
385  }
386 
387 private:
388 
390  IOService& io_service_;
391 
393  std::map<Url, ConnectionPtr> conns_;
394 
397  struct RequestDescriptor {
407  RequestDescriptor(const HttpRequestPtr& request,
408  const HttpResponsePtr& response,
409  const long request_timeout,
410  const HttpClient::RequestHandler& callback,
411  const HttpClient::ConnectHandler& connect_callback)
412  : request_(request), response_(response),
413  request_timeout_(request_timeout),
414  callback_(callback),
415  connect_callback_(connect_callback) {
416  }
417 
419  HttpRequestPtr request_;
421  HttpResponsePtr response_;
423  long request_timeout_;
425  HttpClient::RequestHandler callback_;
427  HttpClient::ConnectHandler connect_callback_;
428  };
429 
431  std::map<Url, std::queue<RequestDescriptor> > queue_;
432 };
433 
434 Connection::Connection(IOService& io_service,
435  const ConnectionPoolPtr& conn_pool,
436  const Url& url)
437  : conn_pool_(conn_pool), url_(url), socket_(io_service), timer_(io_service),
438  current_request_(), current_response_(), parser_(), current_callback_(),
439  buf_(), input_buf_() {
440 }
441 
442 Connection::~Connection() {
443  close();
444 }
445 
446 void
447 Connection::resetState() {
448  current_request_.reset();
449  current_response_.reset();
450  parser_.reset();
451  current_callback_ = HttpClient::RequestHandler();
452 }
453 
454 void
455 Connection::doTransaction(const HttpRequestPtr& request,
456  const HttpResponsePtr& response,
457  const long request_timeout,
458  const HttpClient::RequestHandler& callback,
459  const HttpClient::ConnectHandler& connect_callback) {
460  try {
461  current_request_ = request;
462  current_response_ = response;
463  parser_.reset(new HttpResponseParser(*current_response_));
464  parser_->initModel();
465  current_callback_ = callback;
466 
467  buf_ = request->toString();
468 
469  // If the socket is open we check if it is possible to transmit the data
470  // over this socket by reading from it with message peeking. If the socket
471  // is not usable, we close it and then re-open it. There is a narrow window of
472  // time between checking the socket usability and actually transmitting the
473  // data over this socket, when the peer may close the connection. In this
474  // case we'll need to re-transmit but we don't handle it here.
475  if (socket_.getASIOSocket().is_open() && !socket_.isUsable()) {
476  socket_.close();
477  }
478 
480  HTTP_CLIENT_REQUEST_SEND)
481  .arg(request->toBriefString())
482  .arg(url_.toText());
483 
485  HTTP_CLIENT_REQUEST_SEND_DETAILS)
486  .arg(url_.toText())
487  .arg(HttpMessageParserBase::logFormatHttpMessage(request->toString(),
488  MAX_LOGGED_MESSAGE_SIZE));
489 
490  // Setup request timer.
491  scheduleTimer(request_timeout);
492 
496  TCPEndpoint endpoint(url_.getStrippedHostname(),
497  static_cast<unsigned short>(url_.getPort()));
498  SocketCallback socket_cb(boost::bind(&Connection::connectCallback, shared_from_this(),
499  connect_callback, _1));
500 
501  // Establish new connection or use existing connection.
502  socket_.open(&endpoint, socket_cb);
503 
504  } catch (const std::exception& ex) {
505  // Re-throw with the expected exception type.
506  isc_throw(HttpClientError, ex.what());
507  }
508 }
509 
510 void
511 Connection::close() {
512  timer_.cancel();
513  socket_.close();
514  resetState();
515 }
516 
517 bool
518 Connection::isTransactionOngoing() const {
519  return (static_cast<bool>(current_request_));
520 }
521 
522 void
523 Connection::terminate(const boost::system::error_code& ec,
524  const std::string& parsing_error) {
525 
526  timer_.cancel();
527  socket_.cancel();
528 
529  HttpResponsePtr response;
530 
531  if (!ec && current_response_->isFinalized()) {
532  response = current_response_;
533 
535  HTTP_SERVER_RESPONSE_RECEIVED)
536  .arg(url_.toText());
537 
539  HTTP_SERVER_RESPONSE_RECEIVED_DETAILS)
540  .arg(url_.toText())
541  .arg(parser_->getBufferAsString(MAX_LOGGED_MESSAGE_SIZE));
542 
543  } else {
544  std::string err = parsing_error.empty() ? ec.message() : parsing_error;
545 
547  HTTP_BAD_SERVER_RESPONSE_RECEIVED)
548  .arg(url_.toText())
549  .arg(err);
550 
551  // Only log the details if we have received anything and tried
552  // to parse it.
553  if (!parsing_error.empty()) {
555  HTTP_BAD_SERVER_RESPONSE_RECEIVED_DETAILS)
556  .arg(url_.toText())
557  .arg(parser_->getBufferAsString());
558  }
559 
560  }
561 
562  try {
563  // The callback should take care of its own exceptions but one
564  // never knows.
565  current_callback_(ec, response, parsing_error);
566 
567  } catch (...) {
568  }
569 
570  // If we're not requesting connection persistence, we should close the socket.
571  // We're going to reconnect for the next transaction.
572  if (!current_request_->isPersistent()) {
573  close();
574  }
575 
576  resetState();
577 
578  // Check if there are any requests queued for this connection and start
579  // another transaction if there is at least one.
580  HttpRequestPtr request;
581  long request_timeout;
582  HttpClient::RequestHandler callback;
583  HttpClient::ConnectHandler connect_callback;
584  ConnectionPoolPtr conn_pool = conn_pool_.lock();
585  if (conn_pool && conn_pool->getNextRequest(url_, request, response, request_timeout,
586  callback, connect_callback)) {
587  doTransaction(request, response, request_timeout, callback, connect_callback);
588  }
589 }
590 
591 void
592 Connection::scheduleTimer(const long request_timeout) {
593  if (request_timeout > 0) {
594  timer_.setup(boost::bind(&Connection::timerCallback, this), request_timeout,
596  }
597 }
598 
599 void
600 Connection::doSend() {
601  SocketCallback socket_cb(boost::bind(&Connection::sendCallback, shared_from_this(),
602  _1, _2));
603  try {
604  socket_.asyncSend(&buf_[0], buf_.size(), socket_cb);
605 
606  } catch (...) {
607  terminate(boost::asio::error::not_connected);
608  }
609 }
610 
611 void
612 Connection::doReceive() {
613  TCPEndpoint endpoint;
614  SocketCallback socket_cb(boost::bind(&Connection::receiveCallback, shared_from_this(),
615  _1, _2));
616 
617  try {
618  socket_.asyncReceive(static_cast<void*>(input_buf_.data()), input_buf_.size(), 0,
619  &endpoint, socket_cb);
620  } catch (...) {
621  terminate(boost::asio::error::not_connected);
622  }
623 }
624 
625 void
626 Connection::connectCallback(HttpClient::ConnectHandler connect_callback,
627  const boost::system::error_code& ec) {
628  // Run user defined connect callback if specified.
629  if (connect_callback) {
630  // If the user defined callback indicates that the connection
631  // should not be continued.
632  if (!connect_callback(ec)) {
633  return;
634  }
635  }
636 
637  // In some cases the "in progress" status code may be returned. It doesn't
638  // indicate an error. Sending the request over the socket is expected to
639  // be successful. Getting such status appears to be highly dependent on
640  // the operating system.
641  if (ec &&
642  (ec.value() != boost::asio::error::in_progress) &&
643  (ec.value() != boost::asio::error::already_connected)) {
644  terminate(ec);
645 
646  } else {
647  // Start sending the request asynchronously.
648  doSend();
649  }
650 }
651 
652 void
653 Connection::sendCallback(const boost::system::error_code& ec, size_t length) {
654  if (ec) {
655  // EAGAIN and EWOULDBLOCK don't really indicate an error. The length
656  // should be 0 in this case but let's be sure.
657  if ((ec.value() == boost::asio::error::would_block) ||
658  (ec.value() == boost::asio::error::try_again)) {
659  length = 0;
660 
661  } else {
662  // Any other error should cause the transaction to terminate.
663  terminate(ec);
664  return;
665  }
666  }
667 
668  // Sending is in progress, so push back the timeout.
669  scheduleTimer(timer_.getInterval());
670 
671  // If any data have been sent, remove it from the buffer and only leave the
672  // portion that still has to be sent.
673  if (length > 0) {
674  buf_.erase(0, length);
675  }
676 
677  // If there is no more data to be sent, start receiving a response. Otherwise,
678  // continue sending.
679  if (buf_.empty()) {
680  doReceive();
681 
682  } else {
683  doSend();
684  }
685 }
686 
687 void
688 Connection::receiveCallback(const boost::system::error_code& ec, size_t length) {
689  if (ec) {
690  // EAGAIN and EWOULDBLOCK don't indicate an error in this case. All
691  // other errors should terminate the transaction.
692  if ((ec.value() != boost::asio::error::try_again) &&
693  (ec.value() != boost::asio::error::would_block)) {
694  terminate(ec);
695  return;
696 
697  } else {
698  // For EAGAIN and EWOULDBLOCK the length should be 0 anyway, but let's
699  // make sure.
700  length = 0;
701  }
702  }
703 
704  // Receiving is in progress, so push back the timeout.
705  scheduleTimer(timer_.getInterval());
706 
707  // If we have received any data, let's feed the parser with it.
708  if (length != 0) {
709  parser_->postBuffer(static_cast<void*>(input_buf_.data()), length);
710  parser_->poll();
711  }
712 
713  // If the parser still needs data, let's schedule another receive.
714  if (parser_->needData()) {
715  doReceive();
716 
717  } else if (parser_->httpParseOk()) {
718  // No more data needed and parsing has been successful so far. Let's
719  // try to finalize the response parsing.
720  try {
721  current_response_->finalize();
722  terminate(ec);
723 
724  } catch (const std::exception& ex) {
725  // If there is an error here, we need to return the error message.
726  terminate(ec, ex.what());
727  }
728 
729  } else {
730  // Parsing was unsuccessul. Let's pass the error message held in the
731  // parser.
732  terminate(ec, parser_->getErrorMessage());
733  }
734 }
735 
736 void
737 Connection::timerCallback() {
738  // Request timeout occured.
739  terminate(boost::asio::error::timed_out);
740 }
741 
742 }
743 
744 namespace isc {
745 namespace http {
746 
749 public:
750 
755  : conn_pool_(new ConnectionPool(io_service)) {
756  }
757 
759  ConnectionPoolPtr conn_pool_;
760 
761 };
762 
763 HttpClient::HttpClient(IOService& io_service)
764  : impl_(new HttpClientImpl(io_service)) {
765 }
766 
767 void
769  const HttpResponsePtr& response,
770  const HttpClient::RequestHandler& request_callback,
771  const HttpClient::RequestTimeout& request_timeout,
772  const HttpClient::ConnectHandler& connect_callback) {
773  if (!url.isValid()) {
774  isc_throw(HttpClientError, "invalid URL specified for the HTTP client");
775  }
776 
777  if (!request) {
778  isc_throw(HttpClientError, "HTTP request must not be null");
779  }
780 
781  if (!response) {
782  isc_throw(HttpClientError, "HTTP response must not be null");
783  }
784 
785  if (!request_callback) {
786  isc_throw(HttpClientError, "callback for HTTP transaction must not be null");
787  }
788 
789  impl_->conn_pool_->queueRequest(url, request, response, request_timeout.value_,
790  request_callback, connect_callback);
791 }
792 
793 void
795  impl_->conn_pool_->closeAll();
796 }
797 
798 } // end of namespace isc::http
799 } // end of namespace isc
isc::http::HttpClient::RequestTimeout::value_
long value_
Timeout value specified.
Definition: client.h:77
response_parser.h
isc::log::DBGLVL_TRACE_DETAIL
const int DBGLVL_TRACE_DETAIL
Trace detailed operations.
Definition: log_dbglevels.h:71
isc::http::Url::isValid
bool isValid() const
Checks if the URL is valid.
Definition: url.h:47
isc::http::Url
Represents an URL.
Definition: url.h:20
isc::http::HttpClientError
A generic error raised by the HttpClient class.
Definition: client.h:23
isc::http::HttpRequestPtr
boost::shared_ptr< HttpRequest > HttpRequestPtr
Pointer to the HttpRequest object.
Definition: request.h:25
isc::http::HttpResponsePtr
boost::shared_ptr< HttpResponse > HttpResponsePtr
Pointer to the HttpResponse object.
Definition: response.h:78
isc::http::HttpClientImpl
HttpClient implementation.
Definition: client.cc:748
isc::log::DBGLVL_TRACE_BASIC_DATA
const int DBGLVL_TRACE_BASIC_DATA
Trace data associated with the basic operations.
Definition: log_dbglevels.h:68
isc::http::HttpClient::RequestTimeout
HTTP request/response timeout value.
Definition: client.h:70
isc
Defines the logger used by the top-level component of kea-dhcp-ddns.
Definition: agent_parser.cc:144
isc_throw
#define isc_throw(type, stream)
A shortcut macro to insert known values into exception arguments.
Definition: exceptions/exceptions.h:192
LOG_DEBUG
#define LOG_DEBUG(LOGGER, LEVEL, MESSAGE)
Macro to conveniently test debug output and log it.
Definition: macros.h:14
http_log.h
isc::http::HttpClient::ConnectHandler
std::function< bool(const boost::system::error_code &)> ConnectHandler
Optional handler invoked when client connects to the server.
Definition: client.h:91
isc::http::HttpClient::stop
void stop()
Closes all connections.
Definition: client.cc:794
isc::http::http_logger
isc::log::Logger http_logger("http")
Defines the logger used within libkea-http library.
Definition: http_log.h:18
isc::http::HttpResponseParserPtr
boost::shared_ptr< HttpResponseParser > HttpResponseParserPtr
Pointer to the HttpResponseParser.
Definition: response_parser.h:17
isc::http::HttpClient::asyncSendRequest
void asyncSendRequest(const Url &url, const HttpRequestPtr &request, const HttpResponsePtr &response, const RequestHandler &request_callback, const RequestTimeout &request_timeout=RequestTimeout(10000), const ConnectHandler &connect_callback=ConnectHandler())
Queues new asynchronous HTTP request.
Definition: client.cc:768
isc::log::DBGLVL_TRACE_BASIC
const int DBGLVL_TRACE_BASIC
Trace basic operations.
Definition: log_dbglevels.h:65
response_json.h
isc::log::DBGLVL_TRACE_DETAIL_DATA
const int DBGLVL_TRACE_DETAIL_DATA
Trace data associated with detailed operations.
Definition: log_dbglevels.h:74
isc::http::HttpClientImpl::HttpClientImpl
HttpClientImpl(IOService &io_service)
Constructor.
Definition: client.cc:754
isc::http::HttpClient::RequestHandler
std::function< void(const boost::system::error_code &, const HttpResponsePtr &, const std::string &)> RequestHandler
Callback type used in call to HttpClient::asyncSendRequest.
Definition: client.h:83
interval_timer.h
asio_wrapper.h
client.h
isc::http::HttpClientImpl::conn_pool_
ConnectionPoolPtr conn_pool_
Holds a pointer to the connection pool.
Definition: client.cc:759
tcp_socket.h