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>
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
26using namespace isc;
27using namespace isc::asiolink;
28using namespace http;
29
30namespace {
31
35constexpr size_t MAX_LOGGED_MESSAGE_SIZE = 1024;
36
38typedef boost::function<void(boost::system::error_code ec, size_t length)>
39SocketCallbackFunction;
40
46class SocketCallback {
47public:
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
71private:
72
74 SocketCallbackFunction callback_;
75
76};
77
78class ConnectionPool;
79
81typedef boost::shared_ptr<ConnectionPool> ConnectionPoolPtr;
82
98class Connection : public boost::enable_shared_from_this<Connection> {
99public:
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
139private:
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
241typedef boost::shared_ptr<Connection> ConnectionPtr;
242
250class ConnectionPool : public boost::enable_shared_from_this<ConnectionPool> {
251public:
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,
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
387private:
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_;
427 HttpClient::ConnectHandler connect_callback_;
428 };
429
431 std::map<Url, std::queue<RequestDescriptor> > queue_;
432};
433
434Connection::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
442Connection::~Connection() {
443 close();
444}
445
446void
447Connection::resetState() {
448 current_request_.reset();
449 current_response_.reset();
450 parser_.reset();
451 current_callback_ = HttpClient::RequestHandler();
452}
453
454void
455Connection::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.
507 }
508}
509
510void
511Connection::close() {
512 timer_.cancel();
513 socket_.close();
514 resetState();
515}
516
517bool
518Connection::isTransactionOngoing() const {
519 return (static_cast<bool>(current_request_));
520}
521
522void
523Connection::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;
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
591void
592Connection::scheduleTimer(const long request_timeout) {
593 if (request_timeout > 0) {
594 timer_.setup(boost::bind(&Connection::timerCallback, this), request_timeout,
596 }
597}
598
599void
600Connection::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
611void
612Connection::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
625void
626Connection::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
652void
653Connection::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
687void
688Connection::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
736void
737Connection::timerCallback() {
738 // Request timeout occured.
739 terminate(boost::asio::error::timed_out);
740}
741
742}
743
744namespace isc {
745namespace http {
746
749public:
750
755 : conn_pool_(new ConnectionPool(io_service)) {
756 }
757
759 ConnectionPoolPtr conn_pool_;
760
761};
762
764 : impl_(new HttpClientImpl(io_service)) {
765}
766
767void
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
793void
795 impl_->conn_pool_->closeAll();
796}
797
798} // end of namespace isc::http
799} // end of namespace isc
virtual const char * what() const
Returns a C-style character string of the cause of the exception.
A generic error raised by the HttpClient class.
Definition: client.h:23
HttpClient implementation.
Definition: client.cc:748
HttpClientImpl(IOService &io_service)
Constructor.
Definition: client.cc:754
ConnectionPoolPtr conn_pool_
Holds a pointer to the connection pool.
Definition: client.cc:759
std::function< bool(const boost::system::error_code &)> ConnectHandler
Optional handler invoked when client connects to the server.
Definition: client.h:91
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
void stop()
Closes all connections.
Definition: client.cc:794
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
HttpClient(asiolink::IOService &io_service)
Constructor.
Definition: client.cc:763
static std::string logFormatHttpMessage(const std::string &message, const size_t limit=0)
Formats provided HTTP message for logging.
A generic parser for HTTP responses.
Represents an URL.
Definition: url.h:20
bool isValid() const
Checks if the URL is valid.
Definition: url.h:47
#define isc_throw(type, stream)
A shortcut macro to insert known values into exception arguments.
#define LOG_DEBUG(LOGGER, LEVEL, MESSAGE)
Macro to conveniently test debug output and log it.
Definition: macros.h:14
isc::log::Logger http_logger("http")
Defines the logger used within libkea-http library.
Definition: http_log.h:18
boost::shared_ptr< HttpResponseParser > HttpResponseParserPtr
Pointer to the HttpResponseParser.
boost::shared_ptr< HttpResponse > HttpResponsePtr
Pointer to the HttpResponse object.
Definition: response.h:81
boost::shared_ptr< HttpRequest > HttpRequestPtr
Pointer to the HttpRequest object.
Definition: request.h:28
const int DBGLVL_TRACE_BASIC
Trace basic operations.
Definition: log_dbglevels.h:65
const int DBGLVL_TRACE_DETAIL_DATA
Trace data associated with detailed operations.
Definition: log_dbglevels.h:74
const int DBGLVL_TRACE_BASIC_DATA
Trace data associated with the basic operations.
Definition: log_dbglevels.h:68
const int DBGLVL_TRACE_DETAIL
Trace detailed operations.
Definition: log_dbglevels.h:71
Defines the logger used by the top-level component of kea-dhcp-ddns.
HTTP request/response timeout value.
Definition: client.h:70
long value_
Timeout value specified.
Definition: client.h:77