summaryrefslogtreecommitdiffstats
path: root/src/lib
diff options
context:
space:
mode:
authorMarcin Siodelski <marcin@isc.org>2018-01-05 21:43:49 +0100
committerMarcin Siodelski <marcin@isc.org>2018-01-05 21:43:49 +0100
commit95923b6c408da923e4556a18b32660ac51539711 (patch)
tree6b0662978d0c21f8b367f17382c01b5c105451da /src/lib
parent[5451] Implemented HTTP response parser. (diff)
downloadkea-95923b6c408da923e4556a18b32660ac51539711.tar.xz
kea-95923b6c408da923e4556a18b32660ac51539711.zip
[5451] Implemented HTTP client.
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/asiolink/tcp_socket.h49
-rw-r--r--src/lib/http/Makefile.am4
-rw-r--r--src/lib/http/client.cc708
-rw-r--r--src/lib/http/client.h142
-rw-r--r--src/lib/http/post_request.cc11
-rw-r--r--src/lib/http/post_request.h10
-rw-r--r--src/lib/http/post_request_json.cc10
-rw-r--r--src/lib/http/post_request_json.h13
-rw-r--r--src/lib/http/request.cc6
-rw-r--r--src/lib/http/request.h5
-rw-r--r--src/lib/http/response_parser.cc5
-rw-r--r--src/lib/http/tests/Makefile.am1
-rw-r--r--src/lib/http/tests/listener_unittests.cc480
-rw-r--r--src/lib/http/tests/response_test.h28
-rw-r--r--src/lib/http/tests/url_unittests.cc115
-rw-r--r--src/lib/http/url.cc208
-rw-r--r--src/lib/http/url.h110
17 files changed, 1864 insertions, 41 deletions
diff --git a/src/lib/asiolink/tcp_socket.h b/src/lib/asiolink/tcp_socket.h
index adf74d1f0f..8465c0705e 100644
--- a/src/lib/asiolink/tcp_socket.h
+++ b/src/lib/asiolink/tcp_socket.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2011-2017 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2018 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -90,6 +90,47 @@ public:
return (false);
}
+ /// \brief Checks if the connection is usable.
+ ///
+ /// The connection is usable if the peer has closed it.
+ ///
+ /// \return true if the connection is usable.
+ bool isUsable() const {
+ // If the socket is open it doesn't mean that it is still usable. The connection
+ // could have been closed on the other end. We have to check if we can still
+ // use this socket.
+ if (socket_.is_open()) {
+ // Remember the current non blocking setting.
+ const bool non_blocking_orig = socket_.non_blocking();
+ // Set the socket to non blocking mode. We're going to test if the socket
+ // returns would_block status on the attempt to read from it.
+ socket_.non_blocking(true);
+
+ boost::system::error_code ec;
+ char data[2];
+
+ // Use receive with message peek flag to avoid removing the data awaiting
+ // to be read.
+ socket_.receive(boost::asio::buffer(data, sizeof(data)),
+ boost::asio::socket_base::message_peek,
+ ec);
+
+ // Revert the original non_blocking flag on the socket.
+ socket_.non_blocking(non_blocking_orig);
+
+ // If the connection is alive we'd typically get would_block status code.
+ // If there are any data that haven't been read we may also get success
+ // status. We're guessing that try_again may also be returned by some
+ // implementations in some situations. Any other error code indicates a
+ // problem with the connection so we assume that the connection has been
+ // closed.
+ return (!ec || (ec.value() == boost::asio::error::try_again) ||
+ (ec.value() == boost::asio::error::would_block));
+ }
+
+ return (false);
+ }
+
/// \brief Open Socket
///
/// Opens the TCP socket. This is an asynchronous operation, completion of
@@ -227,7 +268,11 @@ TCPSocket<C>::~TCPSocket()
template <typename C> void
TCPSocket<C>::open(const IOEndpoint* endpoint, C& callback) {
-
+ // If socket is open on this end but has been closed by the peer,
+ // we need to reconnect.
+ if (socket_.is_open() && !isUsable()) {
+ close();
+ }
// Ignore opens on already-open socket. Don't throw a failure because
// of uncertainties as to what precedes whan when using asynchronous I/O.
// At also allows us a treat a passed-in socket as a self-managed socket.
diff --git a/src/lib/http/Makefile.am b/src/lib/http/Makefile.am
index ebdaa25f13..f5d9ded7a2 100644
--- a/src/lib/http/Makefile.am
+++ b/src/lib/http/Makefile.am
@@ -22,7 +22,8 @@ EXTRA_DIST = http_messages.mes
CLEANFILES = *.gcno *.gcda http_messages.h http_messages.cc s-messages
lib_LTLIBRARIES = libkea-http.la
-libkea_http_la_SOURCES = connection.cc connection.h
+libkea_http_la_SOURCES = client.cc client.h
+libkea_http_la_SOURCES += connection.cc connection.h
libkea_http_la_SOURCES += connection_pool.cc connection_pool.h
libkea_http_la_SOURCES += date_time.cc date_time.h
libkea_http_la_SOURCES += http_log.cc http_log.h
@@ -44,6 +45,7 @@ libkea_http_la_SOURCES += response_context.h
libkea_http_la_SOURCES += response_creator.cc response_creator.h
libkea_http_la_SOURCES += response_creator_factory.h
libkea_http_la_SOURCES += response_json.cc response_json.h
+libkea_http_la_SOURCES += url.cc url.h
nodist_libkea_http_la_SOURCES = http_messages.cc http_messages.h
diff --git a/src/lib/http/client.cc b/src/lib/http/client.cc
new file mode 100644
index 0000000000..b1a2b9ed23
--- /dev/null
+++ b/src/lib/http/client.cc
@@ -0,0 +1,708 @@
+// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <asiolink/asio_wrapper.h>
+#include <asiolink/interval_timer.h>
+#include <asiolink/tcp_socket.h>
+#include <http/client.h>
+#include <http/response_json.h>
+#include <http/response_parser.h>
+#include <boost/bind.hpp>
+#include <boost/enable_shared_from_this.hpp>
+#include <boost/weak_ptr.hpp>
+#include <array>
+#include <map>
+#include <queue>
+
+#include <iostream>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace http;
+
+namespace {
+
+/// @brief Default request timeout of 10s.
+const long REQUEST_TIMEOUT = 10000;
+
+/// @brief TCP socket callback function type.
+typedef boost::function<void(boost::system::error_code ec, size_t length)>
+SocketCallbackFunction;
+
+/// @brief Socket callback class required by the TCPSocket API.
+///
+/// Its function call operator ignores callbacks invoked with "operation aborted"
+/// error codes. Such status codes are generated when the posted IO operations
+/// are canceled.
+class SocketCallback {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// Stores pointer to a callback function provided by a caller.
+ ///
+ //// @param socket_callback Pointer to a callback function.
+ SocketCallback(SocketCallbackFunction socket_callback)
+ : callback_(socket_callback) {
+ }
+
+ /// @brief Function call operator.
+ ///
+ /// Invokes the callback for all error codes except "operation aborted".
+ ///
+ /// @param ec Error code.
+ /// @param length Length of the data transmitted over the socket.
+ void operator()(boost::system::error_code ec, size_t length = 0) {
+ if (ec.value() == boost::asio::error::operation_aborted) {
+ return;
+ }
+ callback_(ec, length);
+ }
+
+private:
+
+ /// @brief Holds pointer to a supplied callback.
+ SocketCallbackFunction callback_;
+
+};
+
+class ConnectionPool;
+
+/// @brief Shared pointer to a connection pool.
+typedef boost::shared_ptr<ConnectionPool> ConnectionPoolPtr;
+
+/// @brief Client side HTTP connection to the server.
+///
+/// Each connection is established with a unique destination identified by the
+/// specified URL. Multiple requests to the same destination can be sent over
+/// the same connection, if the connection is persistent. If the server closes
+/// the TCP connection (e.g. after sending a response), the connection can
+/// be re-established (using the same @c Connection object).
+///
+/// If new request is created while the previous request is still in progress,
+/// the new request is stored in the FIFO queue. The queued requests to the
+/// particular URL are sent to the server when the current transaction ends.
+///
+/// The communication over the TCP socket is asynchronous. The caller is notified
+/// about the completion of the transaction via a callback that the caller supplies
+/// when initiating the transaction.
+class Connection : public boost::enable_shared_from_this<Connection> {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// @param io_service IO service to be used for the connection.
+ /// @param conn_pool Back pointer to the connection pool to which this connection
+ /// belongs.
+ /// @param url URL associated with this connection.
+ explicit Connection(IOService& io_service, const ConnectionPoolPtr& conn_pool,
+ const Url& url);
+
+ /// @brief Destructor.
+ ~Connection();
+
+ /// @brief Starts new asynchronous transaction (HTTP request and response).
+ ///
+ /// This method expects that all pointers provided as argument are non-null.
+ ///
+ /// @param request Pointer to the request to be sent to the server.
+ /// @param response Pointer to the object into which the response is stored. The
+ /// caller should create a response object of the type which matches the content
+ /// type expected by the caller, e.g. HttpResponseJson when JSON content type
+ /// is expected to be received.
+ /// @param request_timeout Request timeout in milliseconds.
+ /// @param callback Pointer to the callback function to be invoked when the
+ /// transaction completes.
+ void doTransaction(const HttpRequestPtr& request, const HttpResponsePtr& response,
+ const long request_timeout, const HttpClient::RequestHandler& callback);
+
+ /// @brief Closes connection and removes it from the connection pool.
+ void stop();
+
+ /// @brief Closes the socket and cancels the request timer.
+ void close();
+
+ /// @brief Checks if a transaction has been initiated over this connection.
+ ///
+ /// @return true if transaction has been initiated, false otherwise.
+ bool isTransactionOngoing() const;
+
+private:
+
+ /// @brief Resets the state of the object.
+ ///
+ /// In particular, it removes instances of objects provided for the previous
+ /// transaction by a caller. It doesn't close the socket, though.
+ void resetState();
+
+ /// @brief Performs tasks required after receiving a response or after an
+ /// error.
+ ///
+ /// This method triggers user's callback, resets the state of the connection
+ /// and initiates next transaction if there is any transaction queued for the
+ /// URL associated with this connection.
+ ///
+ /// @param ec Error code received as a result of the IO operation.
+ /// @param parsing_error Message parsing error.
+ void terminate(const boost::system::error_code& ec,
+ const std::string& parsing_error = "");
+
+ /// @brief Asynchronously sends data over the socket.
+ ///
+ /// The data sent over the socket are stored in the @c buf_.
+ void doSend();
+
+ /// @brief Asynchronously receives data over the socket.
+ ///
+ /// The data received over the socket are store into the @c input_buf_.
+ void doReceive();
+
+ /// @brief Local callback invoked when the connection is established.
+ ///
+ /// If the connection is successfully established, this callback will start
+ /// to asynchronously send the request over the socket.
+ ///
+ /// @param request_timeout Request timeout specified for this transaction.
+ /// @param ec Error code being a result of the connection attempt.
+ void connectCallback(const long request_timeout,
+ const boost::system::error_code& ec);
+
+ /// @brief Local callback invoked when an attempt to send a portion of data
+ /// over the socket has ended.
+ ///
+ /// The portion of data that has been sent is removed from the buffer. If all
+ /// data from the buffer were sent, the callback will start to asynchronously
+ /// receive a response from the server.
+ ///
+ /// @param ec Error code being a result of sending the data.
+ /// @param length Number of bytes sent.
+ void sendCallback(const boost::system::error_code& ec, size_t length);
+
+ /// @brief Local callback invoked when an attempt to receive a portion of data
+ /// over the socket has ended.
+ ///
+ /// @param ec Error code being a result of receiving the data.
+ /// @param length Number of bytes received.
+ void receiveCallback(const boost::system::error_code& ec, size_t length);
+
+ /// @brief Local callback invoked when request timeout occurs.
+ void timerCallback();
+
+ /// @brief Pointer to the connection pool owning this connection.
+ ///
+ /// This is a weak pointer to avoid circular dependency between the
+ /// Connection and ConnectionPool.
+ boost::weak_ptr<ConnectionPool> conn_pool_;
+
+ /// @brief URL for this connection.
+ Url url_;
+
+ /// @brief Socket to be used for this connection.
+ TCPSocket<SocketCallback> socket_;
+
+ /// @brief Interval timer used for detecting request timeouts.
+ IntervalTimer timer_;
+
+ /// @brief Holds currently sent request.
+ HttpRequestPtr current_request_;
+
+ /// @brief Holds pointer to an object where response is to be stored.
+ HttpResponsePtr current_response_;
+
+ /// @brief Pointer to the HTTP response parser.
+ HttpResponseParserPtr parser_;
+
+ /// @brief User supplied callback.
+ HttpClient::RequestHandler current_callback_;
+
+ /// @brief Output buffer.
+ std::string buf_;
+
+ /// @brief Input buffer.
+ std::array<char, 4096> input_buf_;
+};
+
+/// @brief Shared pointer to the connection.
+typedef boost::shared_ptr<Connection> ConnectionPtr;
+
+/// @brief Connection pool for managing multiple connections.
+///
+/// Connection pool creates and destroys connections. It holds pointers
+/// to all created connections and can verify whether the particular
+/// connection is currently busy or idle. If the connection is idle, it
+/// uses this connection for new requests. If the connection is busy, it
+/// queues new requests until the connection becomes available.
+class ConnectionPool : public boost::enable_shared_from_this<ConnectionPool> {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// @param io_service Reference to the IO service to be used by the
+ /// connections.
+ explicit ConnectionPool(IOService& io_service)
+ : io_service_(io_service), conns_(), queue_() {
+ }
+
+ /// @brief Destructor.
+ ///
+ /// Closes all connections.
+ ~ConnectionPool() {
+ closeAll();
+ }
+
+ /// @brief Returns next queued request for the given URL.
+ ///
+ /// @param url URL for which next queued request should be retrieved.
+ /// @param [out] request Pointer to the queued request.
+ /// @param [out] response Pointer to the object into which response should
+ /// be stored.
+ /// @param request_timeout Requested timeout for the transaction.
+ /// @param callback Pointer to the user callback for this request.
+ ///
+ /// @return true if the request for the given URL has been retrieved,
+ /// false if there are no more requests queued for this URL.
+ bool getNextRequest(const Url& url,
+ HttpRequestPtr& request,
+ HttpResponsePtr& response,
+ long& request_timeout,
+ HttpClient::RequestHandler& callback) {
+ // Check if the is a queue for this URL. If there is no queue, there
+ // is no request queued either.
+ auto it = queue_.find(url);
+ if (it != queue_.end()) {
+ // If the queue is non empty, we take the oldest request.
+ if (!it->second.empty()) {
+ RequestDescriptor desc = it->second.front();
+ it->second.pop();
+ request = desc.request_;
+ response = desc.response_;
+ request_timeout = desc.request_timeout_,
+ callback = desc.callback_;
+ return (true);
+ }
+ }
+
+ return (false);
+ }
+
+ /// @brief Queue next request for sending to the server.
+ ///
+ /// A new transaction is started immediatelly, if there is no other request
+ /// in progress for the given URL. Otherwise, the request is queued.
+ ///
+ /// @param url Destination where the request should be sent.
+ /// @param request Pointer to the request to be sent to the server.
+ /// @param response Pointer to the object into which the response should be
+ /// stored.
+ /// @param request_timeout Requested timeout for the transaction in
+ /// milliseconds.
+ /// @param callback Pointer to the user callback to be invoked when the
+ /// transaction ends.
+ void queueRequest(const Url& url,
+ const HttpRequestPtr& request,
+ const HttpResponsePtr& response,
+ const long request_timeout,
+ const HttpClient::RequestHandler& callback) {
+ auto it = conns_.find(url);
+ if (it != conns_.end()) {
+ ConnectionPtr conn = it->second;
+ // There is a connection for this URL already. Check if it is idle.
+ if (conn->isTransactionOngoing()) {
+ // Connection is busy, so let's queue the request.
+ queue_[url].push(RequestDescriptor(request, response,
+ request_timeout,
+ callback));
+
+ } else {
+ // Connection is idle, so we can start the transaction.
+ conn->doTransaction(request, response, request_timeout,
+ callback);
+ }
+
+ } else {
+ // There is no connection with this destination yet. Let's create
+ // it and start the transaction.
+ ConnectionPtr conn(new Connection(io_service_, shared_from_this(),
+ url));
+ conn->doTransaction(request, response, request_timeout, callback);
+ conns_[url] = conn;
+ }
+ }
+
+ /// @brief Closes connection and removes associated information from the
+ /// connection pool.
+ ///
+ /// @param url URL for which connection shuld be closed.
+ void closeConnection(const Url& url) {
+ // Close connection for the specified URL.
+ auto conns_it = conns_.find(url);
+ if (conns_it != conns_.end()) {
+ conns_it->second->close();
+ conns_.erase(conns_it);
+ }
+
+ // Remove requests from the queue.
+ auto queue_it = queue_.find(url);
+ if (queue_it != queue_.end()) {
+ queue_.erase(queue_it);
+ }
+ }
+
+ /// @brief Closes all connections and removes associated information from
+ /// the connection pool.
+ void closeAll() {
+ for (auto conns_it = conns_.begin(); conns_it != conns_.end();
+ ++conns_it) {
+ conns_it->second->close();
+ }
+
+ conns_.clear();
+ queue_.clear();
+ }
+
+private:
+
+ /// @brief Holds reference to the IO service.
+ IOService& io_service_;
+
+ /// @brief Holds mapping of URLs to connections.
+ std::map<Url, ConnectionPtr> conns_;
+
+ /// @brief Request descriptor holds parameters associated with the
+ /// particular request.
+ struct RequestDescriptor {
+ /// @brief Constructor.
+ ///
+ /// @param request Pointer to the request to be sent.
+ /// @param response Pointer to the object into which the response will
+ /// be stored.
+ /// @param request_timeout Requested timeout for the transaction.
+ /// @param callback Pointer to the user callback.
+ RequestDescriptor(const HttpRequestPtr& request,
+ const HttpResponsePtr& response,
+ const long request_timeout,
+ const HttpClient::RequestHandler& callback)
+ : request_(request), response_(response),
+ request_timeout_(request_timeout),
+ callback_(callback) {
+ }
+
+ /// @brief Holds pointer to the request.
+ HttpRequestPtr request_;
+ /// @brief Holds pointer to the response.
+ HttpResponsePtr response_;
+ /// @brief Holds requested timeout value.
+ long request_timeout_;
+ /// @brief Holds pointer to the user callback.
+ HttpClient::RequestHandler callback_;
+ };
+
+ /// @brief Holds the queue of requests for different URLs.
+ std::map<Url, std::queue<RequestDescriptor> > queue_;
+};
+
+Connection::Connection(IOService& io_service,
+ const ConnectionPoolPtr& conn_pool,
+ const Url& url)
+ : conn_pool_(conn_pool), url_(url), socket_(io_service), timer_(io_service),
+ current_request_(), current_response_(), parser_(), current_callback_(),
+ buf_(), input_buf_() {
+}
+
+Connection::~Connection() {
+ close();
+}
+
+void
+Connection::resetState() {
+ current_request_.reset();
+ current_response_.reset();
+ parser_.reset();
+ current_callback_ = HttpClient::RequestHandler();
+}
+
+void
+Connection::doTransaction(const HttpRequestPtr& request,
+ const HttpResponsePtr& response,
+ const long request_timeout,
+ const HttpClient::RequestHandler& callback) {
+ try {
+ current_request_ = request;
+ current_response_ = response;
+ parser_.reset(new HttpResponseParser(*current_response_));
+ parser_->initModel();
+ current_callback_ = callback;
+
+ buf_ = request->toString();
+
+ // If the socket is open we check if it is possible to transmit the data
+ // over this socket by reading from it with message peeking. If the socket
+ // is not usable, we close it and then re-open it. There is a narrow window of
+ // time between checking the socket usability and actually transmitting the
+ // data over this socket, when the peer may close the connection. In this
+ // case we'll need to re-transmit but we don't handle it here.
+ if (socket_.getASIOSocket().is_open() && !socket_.isUsable()) {
+ socket_.close();
+ }
+
+ /// @todo We're getting a hostname but in fact it is expected to be an IP address.
+ /// We should extend the TCPEndpoint to also accept names. Currently, it will fall
+ /// over for names.
+ TCPEndpoint endpoint(url_.getHostname(), static_cast<unsigned short>(url_.getPort()));
+ SocketCallback socket_cb(boost::bind(&Connection::connectCallback, shared_from_this(),
+ request_timeout, _1));
+
+ // Establish new connection or use existing connection.
+ socket_.open(&endpoint, socket_cb);
+
+ } catch (const std::exception& ex) {
+ // Re-throw with the expected exception type.
+ isc_throw(HttpClientError, ex.what());
+ }
+}
+
+void
+Connection::stop() {
+ ConnectionPoolPtr conn_pool = conn_pool_.lock();
+ conn_pool->closeConnection(url_);
+}
+
+void
+Connection::close() {
+ timer_.cancel();
+ socket_.close();
+ resetState();
+}
+
+bool
+Connection::isTransactionOngoing() const {
+ return (static_cast<bool>(current_request_));
+}
+
+void
+Connection::terminate(const boost::system::error_code& ec,
+ const std::string& parsing_error) {
+ timer_.cancel();
+ socket_.cancel();
+
+ HttpResponsePtr response;
+
+ if (!ec && current_response_->isFinalized()) {
+ response = current_response_;
+ }
+
+ try {
+ // The callback should take care of its own exceptions but one
+ // never knows.
+ current_callback_(ec, response, parsing_error);
+
+ } catch (...) {
+ }
+
+ // If we're not requesting connection persistence, we should close the socket.
+ // We're going to reconnect for the next transaction.
+ if (!current_request_->isPersistent()) {
+ close();
+ }
+
+ resetState();
+
+ // Check if there are any requests queued for this connection and start
+ // another transaction if there is at least one.
+ HttpRequestPtr request;
+ long request_timeout;
+ HttpClient::RequestHandler callback;
+ ConnectionPoolPtr conn_pool = conn_pool_.lock();
+ if (conn_pool && conn_pool->getNextRequest(url_, request, response, request_timeout,
+ callback)) {
+ doTransaction(request, response, request_timeout, callback);
+ }
+}
+
+void
+Connection::doSend() {
+ SocketCallback socket_cb(boost::bind(&Connection::sendCallback, shared_from_this(),
+ _1, _2));
+ socket_.asyncSend(&buf_[0], buf_.size(), socket_cb);
+}
+
+void
+Connection::doReceive() {
+ TCPEndpoint endpoint;
+ SocketCallback socket_cb(boost::bind(&Connection::receiveCallback, shared_from_this(),
+ _1, _2));
+ socket_.asyncReceive(static_cast<void*>(input_buf_.data()), input_buf_.size(), 0,
+ &endpoint, socket_cb);
+}
+
+void
+Connection::connectCallback(const long request_timeout, const boost::system::error_code& ec) {
+ // In some cases the "in progress" status code may be returned. It doesn't
+ // indicate an error. Sending the request over the socket is expected to
+ // be successful. Getting such status appears to be highly dependent on
+ // the operating system.
+ if (ec &&
+ (ec.value() != boost::asio::error::in_progress) &&
+ (ec.value() != boost::asio::error::already_connected)) {
+ terminate(ec);
+
+ } else {
+ // Setup request timer.
+ timer_.setup(boost::bind(&Connection::timerCallback, this), request_timeout,
+ IntervalTimer::ONE_SHOT);
+ // Start sending the request asynchronously.
+ doSend();
+ }
+}
+
+void
+Connection::sendCallback(const boost::system::error_code& ec, size_t length) {
+ if (ec) {
+ // EAGAIN and EWOULDBLOCK don't really indicate an error. The length
+ // should be 0 in this case but let's be sure.
+ if ((ec.value() == boost::asio::error::would_block) ||
+ (ec.value() == boost::asio::error::try_again)) {
+ length = 0;
+
+ } else {
+ // Any other error should cause the transaction to terminate.
+ terminate(ec);
+ }
+ }
+
+ // If any data have been sent, remove it from the buffer and only leave the
+ // portion that still has to be sent.
+ if (length > 0) {
+ buf_.erase(0, length);
+ }
+
+ // If there is no more data to be sent, start receiving a response. Otherwise,
+ // continue receiving.
+ if (buf_.empty()) {
+ doReceive();
+
+ } else {
+ doSend();
+ }
+}
+
+void
+Connection::receiveCallback(const boost::system::error_code& ec, size_t length) {
+ if (ec) {
+ // EAGAIN and EWOULDBLOCK don't indicate an error in this case. All
+ // other errors should terminate the transaction.
+ if ((ec.value() != boost::asio::error::try_again) &&
+ (ec.value() != boost::asio::error::would_block)) {
+ terminate(ec);
+
+ } else {
+ // For EAGAIN and EWOULDBLOCK the length should be 0 anyway, but let's
+ // make sure.
+ length = 0;
+ }
+ }
+
+ // If we have received any data, let's feed the parser with it.
+ if (length != 0) {
+ parser_->postBuffer(static_cast<void*>(input_buf_.data()), length);
+ parser_->poll();
+ }
+
+ // If the parser still needs data, let's schedule another receive.
+ if (parser_->needData()) {
+ doReceive();
+
+ } else if (parser_->httpParseOk()) {
+ // No more data needed and parsing has been successful so far. Let's
+ // try to finalize the response parsing.
+ try {
+ current_response_->finalize();
+ terminate(ec);
+
+ } catch (const std::exception& ex) {
+ // If there is an error here, we need to return the error message.
+ terminate(ec, ex.what());
+ }
+
+ } else {
+ // Parsing was unsuccessul. Let's pass the error message held in the
+ // parser.
+ terminate(ec, parser_->getErrorMessage());
+ }
+}
+
+void
+Connection::timerCallback() {
+ // Request timeout occured.
+ terminate(boost::asio::error::timed_out);
+}
+
+}
+
+namespace isc {
+namespace http {
+
+/// @brief HttpClient implementation.
+class HttpClientImpl {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// Creates new connection pool.
+ HttpClientImpl(IOService& io_service)
+ : conn_pool_(new ConnectionPool(io_service)) {
+ }
+
+ /// @brief Holds a pointer to the connection pool.
+ ConnectionPoolPtr conn_pool_;
+
+};
+
+HttpClient::HttpClient(IOService& io_service)
+ : impl_(new HttpClientImpl(io_service)) {
+}
+
+void
+HttpClient::asyncSendRequest(const Url& url, const HttpRequestPtr& request,
+ const HttpResponsePtr& response,
+ const HttpClient::RequestHandler& callback) {
+ asyncSendRequest(url, request, response,
+ HttpClient::RequestTimeout(REQUEST_TIMEOUT),
+ callback);
+}
+
+void
+HttpClient::asyncSendRequest(const Url& url, const HttpRequestPtr& request,
+ const HttpResponsePtr& response,
+ const HttpClient::RequestTimeout& request_timeout,
+ const HttpClient::RequestHandler& callback) {
+ if (!url.isValid()) {
+ isc_throw(HttpClientError, "invalid URL specified for the HTTP client");
+ }
+
+ if (!request) {
+ isc_throw(HttpClientError, "HTTP request must not be null");
+ }
+
+ if (!response) {
+ isc_throw(HttpClientError, "HTTP response must not be null");
+ }
+
+ if (!callback) {
+ isc_throw(HttpClientError, "callback for HTTP transaction must not be null");
+ }
+
+ impl_->conn_pool_->queueRequest(url, request, response, request_timeout.value_,
+ callback);
+}
+
+void
+HttpClient::stop() {
+ impl_->conn_pool_->closeAll();
+}
+
+} // end of namespace isc::http
+} // end of namespace isc
diff --git a/src/lib/http/client.h b/src/lib/http/client.h
new file mode 100644
index 0000000000..943b19defb
--- /dev/null
+++ b/src/lib/http/client.h
@@ -0,0 +1,142 @@
+// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#ifndef HTTP_CLIENT_H
+#define HTTP_CLIENT_H
+
+#include <asiolink/io_service.h>
+#include <exceptions/exceptions.h>
+#include <http/url.h>
+#include <http/request.h>
+#include <http/response.h>
+#include <boost/shared_ptr.hpp>
+#include <functional>
+#include <string>
+
+namespace isc {
+namespace http {
+
+/// @brief A generic error raised by the @ref HttpClient class.
+class HttpClientError : public Exception {
+public:
+ HttpClientError(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+class HttpClientImpl;
+
+/// @brief HTTP client class.
+class HttpClient {
+public:
+
+ /// @brief HTTP request/response timeout value.
+ struct RequestTimeout {
+ /// @brief Constructor.
+ ///
+ /// @param value Reuqest/response timeout value in milliseconds.
+ explicit RequestTimeout(long value)
+ : value_(value) {
+ }
+ long value_; ///< Timeout value specified.
+ };
+
+ /// @brief Callback type used in call to @ref HttpClient::asyncSendRequest.
+ typedef std::function<void(const boost::system::error_code&,
+ const HttpResponsePtr&,
+ const std::string&)> RequestHandler;
+
+ /// @brief Constructor.
+ ///
+ /// @param io_service IO service to be used by the HTTP client.
+ explicit HttpClient(asiolink::IOService& io_service);
+
+ /// @brief Queues new asynchronous HTTP request.
+ ///
+ /// The client creates one connection for the specified URL. If the connection
+ /// with the particular destination already exists, it will be re-used for the
+ /// new transaction scheduled with this call. If another transaction is still
+ /// in progress, the new transaction is queued. The queued transactions are
+ /// started in the FIFO order one after another. If the connection is idle or
+ /// the connection doesn't exist, the new transaction is started immediatelly.
+ ///
+ /// The existing connection is tested before it is used for the new transaction
+ /// by attempting to read (with message peeking) from the open TCP socket. If the
+ /// read attempt is successful, the client will transmit the HTTP request to
+ /// the server using this connection. It is possible that the server closes the
+ /// connection between the connection test and sending the request. In such case,
+ /// an error will be returned and the caller will need to try re-sending the
+ /// request.
+ ///
+ /// If the connection test fails, the client will close the socket and reconnect
+ /// to the server prior to sending the request.
+ ///
+ /// Pointers to the request and response objects are provided as arguments of
+ /// this method. These pointers should have appropriate types derived from the
+ /// @ref HttpRequest and @ref HttpResponse classes. For example, if the request
+ /// has content type "application/json", a pointer to the
+ /// @ref HttpResponseJson should be passed. In this case, the response type
+ /// should be @ref HttpResponseJson. These types are used to validate both the
+ /// request provided by the caller and the response received from the server.
+ ///
+ /// The callback function provided by the caller is invoked when the transaction
+ /// terminates, i.e. when the server has responded or when an error occured. The
+ /// callback is expected to be exception safe, but the client internally guards
+ /// against exceptions thrown by the callback.
+ ///
+ /// The first argument of the callback indicates an IO error during communication
+ /// with the server. If the communication is successful the error code of 0 is
+ /// returned. However, in this case it is still possible that the transaction is
+ /// unsuccessful due to HTTP response parsing error, e.g. invalid content type,
+ /// malformed response etc. Such errors are indicated via third argument.
+ ///
+ /// If message parsing was successful the second argument of the callback contains
+ /// a pointer to the parsed response (the same pointer as provided by the caller
+ /// as the argument). If parsing was unsuccessful, the null pointer is returned.
+ ///
+ /// The default timeout for the transaction is set to 10 seconds (10 000 ms). This
+ /// variant of the method doesn't allow for specifying a custom timeout. If the
+ /// timeout occurs, the callback is invoked with the error code of
+ /// @c boost::asio::error::timed_out.
+ ///
+ /// @param url URL where the request should be send.
+ /// @param request Pointer to the object holding a request.
+ /// @param response Pointer to the object where response should be stored.
+ /// @param callback Pointer to the user callback function.
+ ///
+ /// @throw HttpClientError If invalid arguments were provided.
+ void asyncSendRequest(const Url& url,
+ const HttpRequestPtr& request,
+ const HttpResponsePtr& response,
+ const RequestHandler& callback);
+
+ /// @brief Queues new asynchronous HTTP request with timeout.
+ ///
+ /// @param url URL where the request should be send.
+ /// @param request Pointer to the object holding a request.
+ /// @param response Pointer to the object where response should be stored.
+ /// @param request_timeout Timeout for the transaction in milliseconds.
+ /// @param callback Pointer to the user callback function.
+ ///
+ /// @throw HttpClientError If invalid arguments were provided.
+ void asyncSendRequest(const Url& url,
+ const HttpRequestPtr& request,
+ const HttpResponsePtr& response,
+ const RequestTimeout& request_timeout,
+ const RequestHandler& callback);
+
+ /// @brief Closes all connections.
+ void stop();
+
+private:
+
+ /// @brief Pointer to the HTTP client implementation.
+ boost::shared_ptr<HttpClientImpl> impl_;
+};
+
+} // end of namespace isc::http
+} // end of namespace isc
+
+#endif
diff --git a/src/lib/http/post_request.cc b/src/lib/http/post_request.cc
index 3cc8d810b8..62e4a34a73 100644
--- a/src/lib/http/post_request.cc
+++ b/src/lib/http/post_request.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2016-2017 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2016-2018 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -16,5 +16,14 @@ PostHttpRequest::PostHttpRequest()
requireHeader("Content-Type");
}
+PostHttpRequest::PostHttpRequest(const Method& method, const std::string& uri,
+ const HttpVersion& version)
+ : HttpRequest(method, uri, version) {
+ requireHttpMethod(Method::HTTP_POST);
+ requireHeader("Content-Length");
+ requireHeader("Content-Type");
+}
+
+
} // namespace http
} // namespace isc
diff --git a/src/lib/http/post_request.h b/src/lib/http/post_request.h
index dbfe7d7026..733a4ae975 100644
--- a/src/lib/http/post_request.h
+++ b/src/lib/http/post_request.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2016-2017 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2016-2018 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -31,6 +31,14 @@ public:
/// @brief Constructor for inbound HTTP request.
PostHttpRequest();
+
+ /// @brief Constructor for outbound HTTP request.
+ ///
+ /// @param method HTTP method, e.g. POST.
+ /// @param uri URI.
+ /// @param version HTTP version.
+ PostHttpRequest(const Method& method, const std::string& uri, const HttpVersion& version);
+
};
diff --git a/src/lib/http/post_request_json.cc b/src/lib/http/post_request_json.cc
index 0a93312720..0c7d74f7dc 100644
--- a/src/lib/http/post_request_json.cc
+++ b/src/lib/http/post_request_json.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2016-2017 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2016-2018 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -16,6 +16,14 @@ PostHttpRequestJson::PostHttpRequestJson()
requireHeaderValue("Content-Type", "application/json");
}
+PostHttpRequestJson::PostHttpRequestJson(const Method& method, const std::string& uri,
+ const HttpVersion& version)
+ : PostHttpRequest(method, uri, version) {
+ requireHeaderValue("Content-Type", "application/json");
+ context()->headers_.push_back(HttpHeaderContext("Content-Type", "application/json"));
+}
+
+
void
PostHttpRequestJson::finalize() {
if (!created_) {
diff --git a/src/lib/http/post_request_json.h b/src/lib/http/post_request_json.h
index 3a6f37ae0d..e3d9829ff6 100644
--- a/src/lib/http/post_request_json.h
+++ b/src/lib/http/post_request_json.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2016-2017 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2016-2018 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -42,6 +42,17 @@ public:
/// @brief Constructor for inbound HTTP request.
explicit PostHttpRequestJson();
+ /// @brief Constructor for outbound HTTP request.
+ ///
+ /// This constructor adds "Content-Type" header with the value of
+ /// "application/json" to the context.
+ ///
+ /// @param method HTTP method, e.g. POST.
+ /// @param uri URI.
+ /// @param version HTTP version.
+ explicit PostHttpRequestJson(const Method& method, const std::string& uri,
+ const HttpVersion& version);
+
/// @brief Complete parsing of the HTTP request.
///
/// This method parses the JSON body into the structure of
diff --git a/src/lib/http/request.cc b/src/lib/http/request.cc
index 0d3186d118..65847f8343 100644
--- a/src/lib/http/request.cc
+++ b/src/lib/http/request.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2016-2017 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2016-2018 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -169,10 +169,6 @@ HttpRequest::toString() const {
bool
HttpRequest::isPersistent() const {
- if (getDirection() == OUTBOUND) {
- isc_throw(InvalidOperation, "can't call isPersistent for the outbound request");
- }
-
HttpHeaderPtr conn = getHeaderSafe("connection");
std::string conn_value;
if (conn) {
diff --git a/src/lib/http/request.h b/src/lib/http/request.h
index 0cad579df5..387a109519 100644
--- a/src/lib/http/request.h
+++ b/src/lib/http/request.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2016-2017 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2016-2018 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -63,7 +63,7 @@ public:
/// @brief Constructor for inbound HTTP request.
HttpRequest();
- /// @brief Constructor for oubtound HTTP request.
+ /// @brief Constructor for outbound HTTP request.
///
/// @param method HTTP method, e.g. POST.
/// @param uri URI.
@@ -135,7 +135,6 @@ public:
///
/// @return true if the client has requested persistent connection, false
/// otherwise.
- /// @throw InvalidOperation if the method is called for the outbound message.
bool isPersistent() const;
protected:
diff --git a/src/lib/http/response_parser.cc b/src/lib/http/response_parser.cc
index 677c3aa764..aa7fb720ac 100644
--- a/src/lib/http/response_parser.cc
+++ b/src/lib/http/response_parser.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2017 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2017-2018 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -6,7 +6,6 @@
#include <http/response_parser.h>
#include <boost/bind.hpp>
-#include <iostream>
using namespace isc::util;
@@ -336,7 +335,7 @@ HttpResponseParser::headerLineStartHandler() {
" in header name");
} else {
- // Update header name with the parse letter.
+ // Update header name with the parsed letter.
context_->headers_.push_back(HttpHeaderContext());
context_->headers_.back().name_.push_back(c);
transition(HEADER_NAME_ST, DATA_READ_OK_EVT);
diff --git a/src/lib/http/tests/Makefile.am b/src/lib/http/tests/Makefile.am
index 3a0a937d25..4d6e03250e 100644
--- a/src/lib/http/tests/Makefile.am
+++ b/src/lib/http/tests/Makefile.am
@@ -35,6 +35,7 @@ libhttp_unittests_SOURCES += request_unittests.cc
libhttp_unittests_SOURCES += response_unittests.cc
libhttp_unittests_SOURCES += response_json_unittests.cc
libhttp_unittests_SOURCES += run_unittests.cc
+libhttp_unittests_SOURCES += url_unittests.cc
libhttp_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
libhttp_unittests_CXXFLAGS = $(AM_CXXFLAGS)
diff --git a/src/lib/http/tests/listener_unittests.cc b/src/lib/http/tests/listener_unittests.cc
index 7b35baed81..d8979a08dd 100644
--- a/src/lib/http/tests/listener_unittests.cc
+++ b/src/lib/http/tests/listener_unittests.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2017 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2017-2018 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -7,6 +7,8 @@
#include <config.h>
#include <asiolink/asio_wrapper.h>
#include <asiolink/interval_timer.h>
+#include <cc/data.h>
+#include <http/client.h>
#include <http/http_types.h>
#include <http/listener.h>
#include <http/post_request_json.h>
@@ -14,16 +16,20 @@
#include <http/response_creator_factory.h>
#include <http/response_json.h>
#include <http/tests/response_test.h>
+#include <http/url.h>
#include <boost/asio/buffer.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/bind.hpp>
+#include <boost/pointer_cast.hpp>
#include <gtest/gtest.h>
+#include <functional>
#include <list>
#include <sstream>
#include <string>
using namespace boost::asio::ip;
using namespace isc::asiolink;
+using namespace isc::data;
using namespace isc::http;
using namespace isc::http::test;
@@ -41,6 +47,10 @@ const long REQUEST_TIMEOUT = 10000;
/// @brief Persistent connection idle timeout used in most of the tests (ms).
const long IDLE_TIMEOUT = 10000;
+/// @brief Persistent connection idle timeout used in tests where idle connections
+/// are tested (ms).
+const long SHORT_IDLE_TIMEOUT = 200;
+
/// @brief Test timeout (ms).
const long TEST_TIMEOUT = 10000;
@@ -50,6 +60,12 @@ typedef TestHttpResponseBase<HttpResponseJson> Response;
/// @brief Pointer to test HTTP response.
typedef boost::shared_ptr<Response> ResponsePtr;
+/// @brief Generic test HTTP response.
+typedef TestHttpResponseBase<HttpResponse> GenericResponse;
+
+/// @brief Pointer to generic test HTTP response.
+typedef boost::shared_ptr<GenericResponse> GenericResponsePtr;
+
/// @brief Implementation of the @ref HttpResponseCreator.
class TestHttpResponseCreator : public HttpResponseCreator {
public:
@@ -85,14 +101,74 @@ private:
/// @brief Creates HTTP response.
///
+ /// This method generates 3 types of responses:
+ /// - response with a requested content type,
+ /// - partial response with incomplete JSON body,
+ /// - response with JSON body copied from the request.
+ ///
+ /// The first one is useful to test situations when received response can't
+ /// be parsed because of the content type mismatch. The second one is useful
+ /// to test request timeouts. The third type is used by most of the unit tests
+ /// to test successful transactions.
+ ///
/// @param request Pointer to the HTTP request.
/// @return Pointer to the generated HTTP OK response with no content.
virtual HttpResponsePtr
createDynamicHttpResponse(const ConstHttpRequestPtr& request) {
- // The simplest thing is to create a response with no content.
- // We don't need content to test our class.
+ // Request must always be JSON.
+ ConstPostHttpRequestJsonPtr request_json =
+ boost::dynamic_pointer_cast<const PostHttpRequestJson>(request);
+ ConstElementPtr body;
+ if (request_json) {
+ body = request_json->getBodyAsJson();
+ if (body) {
+ // Check if the client requested one of the two first response
+ // types.
+ GenericResponsePtr response;
+ ConstElementPtr content_type = body->get("requested-content-type");
+ ConstElementPtr partial_response = body->get("partial-response");
+ if (content_type || partial_response) {
+ // The first two response types can only be generated using the
+ // generic response as we have to explicitly modify some of the
+ // values.
+ response.reset(new GenericResponse(request->getHttpVersion(),
+ HttpStatusCode::OK));
+ HttpResponseContextPtr ctx = response->context();
+
+ if (content_type) {
+ // Provide requested content type.
+ ctx->headers_.push_back(HttpHeaderContext("Content-Type",
+ content_type->stringValue()));
+ // It doesn't matter what body is there.
+ ctx->body_ = "abcd";
+ response->finalize();
+
+ } else {
+ // Generate JSON response.
+ ctx->headers_.push_back(HttpHeaderContext("Content-Type",
+ "application/json"));
+ // The body lacks '}' so the client will be waiting for it and
+ // eventually should time out.
+ ctx->body_ = "{";
+ response->finalize();
+ // The auto generated Content-Length header would be based on the
+ // body size (so set to 1 byte). We have to override it to
+ // account for the missing '}' character.
+ response->setContentLength(2);
+ }
+ return (response);
+ }
+ }
+ }
+
+ // Third type of response is requested.
ResponsePtr response(new Response(request->getHttpVersion(),
HttpStatusCode::OK));
+ // If body was included in the request. Let's copy it.
+ if (body) {
+ response->setBodyAsJson(body);
+ }
+
response->finalize();
return (response);
}
@@ -112,7 +188,7 @@ public:
};
/// @brief Entity which can connect to the HTTP server endpoint.
-class HttpClient : public boost::noncopyable {
+class TestHttpClient : public boost::noncopyable {
public:
/// @brief Constructor.
@@ -121,7 +197,7 @@ public:
/// connect() to connect to the server.
///
/// @param io_service IO service to be stopped on error.
- explicit HttpClient(IOService& io_service)
+ explicit TestHttpClient(IOService& io_service)
: io_service_(io_service.get_io_service()), socket_(io_service_),
buf_(), response_() {
}
@@ -129,7 +205,7 @@ public:
/// @brief Destructor.
///
/// Closes the underlying socket if it is open.
- ~HttpClient() {
+ ~TestHttpClient() {
close();
}
@@ -305,8 +381,8 @@ private:
std::string response_;
};
-/// @brief Pointer to the HttpClient.
-typedef boost::shared_ptr<HttpClient> HttpClientPtr;
+/// @brief Pointer to the TestHttpClient.
+typedef boost::shared_ptr<TestHttpClient> TestHttpClientPtr;
/// @brief Test fixture class for @ref HttpListener.
class HttpListenerTest : public ::testing::Test {
@@ -334,12 +410,12 @@ public:
/// @brief Connect to the endpoint.
///
- /// This method creates HttpClient instance and retains it in the clients_
+ /// This method creates TestHttpClient instance and retains it in the clients_
/// list.
///
/// @param request String containing the HTTP request to be sent.
void startRequest(const std::string& request) {
- HttpClientPtr client(new HttpClient(io_service_));
+ TestHttpClientPtr client(new TestHttpClient(io_service_));
clients_.push_back(client);
clients_.back()->startRequest(request);
}
@@ -361,6 +437,8 @@ public:
/// @param timeout Optional value specifying for how long the io service
/// should be ran.
void runIOService(long timeout = 0) {
+ io_service_.get_io_service().reset();
+
if (timeout > 0) {
run_io_service_timer_.setup(boost::bind(&HttpListenerTest::timeoutHandler,
this, false),
@@ -379,10 +457,11 @@ public:
std::string httpOk(const HttpVersion& http_version) {
std::ostringstream s;
s << "HTTP/" << http_version.major_ << "." << http_version.minor_ << " 200 OK\r\n"
- "Content-Length: 0\r\n"
+ "Content-Length: 4\r\n"
"Content-Type: application/json\r\n"
"Date: Tue, 19 Dec 2016 18:53:35 GMT\r\n"
- "\r\n";
+ "\r\n"
+ "{ }";
return (s.str());
}
@@ -400,7 +479,7 @@ public:
IntervalTimer run_io_service_timer_;
/// @brief List of client connections.
- std::list<HttpClientPtr> clients_;
+ std::list<TestHttpClientPtr> clients_;
};
// This test verifies that HTTP connection can be established and used to
@@ -420,7 +499,7 @@ TEST_F(HttpListenerTest, listen) {
ASSERT_NO_THROW(startRequest(request));
ASSERT_NO_THROW(runIOService());
ASSERT_EQ(1, clients_.size());
- HttpClientPtr client = *clients_.begin();
+ TestHttpClientPtr client = *clients_.begin();
ASSERT_TRUE(client);
EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse());
@@ -450,7 +529,7 @@ TEST_F(HttpListenerTest, keepAlive) {
ASSERT_NO_THROW(startRequest(request));
ASSERT_NO_THROW(runIOService());
ASSERT_EQ(1, clients_.size());
- HttpClientPtr client = *clients_.begin();
+ TestHttpClientPtr client = *clients_.begin();
ASSERT_TRUE(client);
EXPECT_EQ(httpOk(HttpVersion::HTTP_10()), client->getResponse());
@@ -498,7 +577,7 @@ TEST_F(HttpListenerTest, persistentConnection) {
ASSERT_NO_THROW(startRequest(request));
ASSERT_NO_THROW(runIOService());
ASSERT_EQ(1, clients_.size());
- HttpClientPtr client = *clients_.begin();
+ TestHttpClientPtr client = *clients_.begin();
ASSERT_TRUE(client);
EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse());
@@ -549,7 +628,7 @@ TEST_F(HttpListenerTest, keepAliveTimeout) {
ASSERT_NO_THROW(startRequest(request));
ASSERT_NO_THROW(runIOService());
ASSERT_EQ(1, clients_.size());
- HttpClientPtr client = *clients_.begin();
+ TestHttpClientPtr client = *clients_.begin();
ASSERT_TRUE(client);
EXPECT_EQ(httpOk(HttpVersion::HTTP_10()), client->getResponse());
@@ -605,7 +684,7 @@ TEST_F(HttpListenerTest, persistentConnectionTimeout) {
ASSERT_NO_THROW(startRequest(request));
ASSERT_NO_THROW(runIOService());
ASSERT_EQ(1, clients_.size());
- HttpClientPtr client = *clients_.begin();
+ TestHttpClientPtr client = *clients_.begin();
ASSERT_TRUE(client);
EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse());
@@ -660,7 +739,7 @@ TEST_F(HttpListenerTest, persistentConnectionBadBody) {
ASSERT_NO_THROW(startRequest(request));
ASSERT_NO_THROW(runIOService());
ASSERT_EQ(1, clients_.size());
- HttpClientPtr client = *clients_.begin();
+ TestHttpClientPtr client = *clients_.begin();
ASSERT_TRUE(client);
EXPECT_EQ("HTTP/1.1 400 Bad Request\r\n"
"Content-Length: 40\r\n"
@@ -717,7 +796,7 @@ TEST_F(HttpListenerTest, badRequest) {
ASSERT_NO_THROW(startRequest(request));
ASSERT_NO_THROW(runIOService());
ASSERT_EQ(1, clients_.size());
- HttpClientPtr client = *clients_.begin();
+ TestHttpClientPtr client = *clients_.begin();
ASSERT_TRUE(client);
EXPECT_EQ("HTTP/1.1 400 Bad Request\r\n"
"Content-Length: 40\r\n"
@@ -795,7 +874,7 @@ TEST_F(HttpListenerTest, requestTimeout) {
ASSERT_NO_THROW(startRequest(request));
ASSERT_NO_THROW(runIOService());
ASSERT_EQ(1, clients_.size());
- HttpClientPtr client = *clients_.begin();
+ TestHttpClientPtr client = *clients_.begin();
ASSERT_TRUE(client);
// The server should wait for the missing part of the request for 1 second.
@@ -810,4 +889,363 @@ TEST_F(HttpListenerTest, requestTimeout) {
client->getResponse());
}
+/// @brief Test fixture class for testing HTTP client.
+class HttpClientTest : public HttpListenerTest {
+public:
+
+ /// @brief Constructor.
+ HttpClientTest()
+ : HttpListenerTest(),
+ listener_(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT,
+ factory_, HttpListener::RequestTimeout(REQUEST_TIMEOUT),
+ HttpListener::IdleTimeout(IDLE_TIMEOUT)),
+ listener2_(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT + 1,
+ factory_, HttpListener::RequestTimeout(REQUEST_TIMEOUT),
+ HttpListener::IdleTimeout(IDLE_TIMEOUT)),
+ listener3_(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT + 2,
+ factory_, HttpListener::RequestTimeout(REQUEST_TIMEOUT),
+ HttpListener::IdleTimeout(SHORT_IDLE_TIMEOUT)) {
+ }
+
+ /// @brief Destructor.
+ ~HttpClientTest() {
+ listener_.stop();
+ listener2_.stop();
+ listener3_.stop();
+ io_service_.poll();
+ }
+
+ /// @brief Creates HTTP request with JSON body.
+ ///
+ /// It includes a JSON parameter with a specified value.
+ ///
+ /// @param parameter_name JSON parameter to be included.
+ /// @param value JSON parameter value.
+ /// @param version HTTP version to be used. Default is HTTP/1.1.
+ template<typename ValueType>
+ PostHttpRequestJsonPtr createRequest(const std::string& parameter_name,
+ const ValueType& value,
+ const HttpVersion& version = HttpVersion(1, 1)) {
+ // Create POST request with JSON body.
+ PostHttpRequestJsonPtr request(new PostHttpRequestJson(HttpRequest::Method::HTTP_POST,
+ "/", version));
+ // Body is a map with a specified parameter included.
+ ElementPtr body = Element::createMap();
+ body->set(parameter_name, Element::create(value));
+ request->setBodyAsJson(body);
+ try {
+ request->finalize();
+
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "failed to create request: " << ex.what();
+ }
+
+ return (request);
+ }
+
+ /// @brief Test that two consecutive requests can be sent over the same connection.
+ ///
+ /// @param version HTTP version to be used.
+ void testConsecutiveRequests(const HttpVersion& version) {
+ // Start the server.
+ ASSERT_NO_THROW(listener_.start());
+
+ // Create a client and specify the URL on which the server can be reached.
+ HttpClient client(io_service_);
+ Url url("http://127.0.0.1:18123");
+
+ // Initiate request to the server.
+ PostHttpRequestJsonPtr request1 = createRequest("sequence", 1, version);
+ HttpResponseJsonPtr response1(new HttpResponseJson());
+ unsigned resp_num = 0;
+ ASSERT_NO_THROW(client.asyncSendRequest(url, request1, response1,
+ [this, &resp_num](const boost::system::error_code& ec,
+ const HttpResponsePtr&,
+ const std::string&) {
+ if (++resp_num > 1) {
+ io_service_.stop();
+ }
+ EXPECT_FALSE(ec);
+ }));
+
+ // Initiate another request to the destination.
+ PostHttpRequestJsonPtr request2 = createRequest("sequence", 2, version);
+ HttpResponseJsonPtr response2(new HttpResponseJson());
+ ASSERT_NO_THROW(client.asyncSendRequest(url, request2, response2,
+ [this, &resp_num](const boost::system::error_code& ec,
+ const HttpResponsePtr&,
+ const std::string&) {
+ if (++resp_num > 1) {
+ io_service_.stop();
+ }
+ EXPECT_FALSE(ec);
+ }));
+
+ // Actually trigger the requests. The requests should be handlded by the
+ // server one after another. While the first request is being processed
+ // the server should queue another one.
+ ASSERT_NO_THROW(runIOService());
+
+ // Make sure that the received responses are different. We check that by
+ // comparing value of the sequence parameters.
+ ASSERT_TRUE(response1);
+ ConstElementPtr sequence1 = response1->getJsonElement("sequence");
+ ASSERT_TRUE(sequence1);
+
+ ASSERT_TRUE(response2);
+ ConstElementPtr sequence2 = response2->getJsonElement("sequence");
+ ASSERT_TRUE(sequence2);
+
+ EXPECT_NE(sequence1->intValue(), sequence2->intValue());
+ }
+
+ /// @brief Instance of the listener used in the tests.
+ HttpListener listener_;
+
+ /// @brief Instance of the second listener used in the tests.
+ HttpListener listener2_;
+
+ /// @brief Instance of the third listener used in the tests (with short idle
+ /// timeout).
+ HttpListener listener3_;
+};
+
+// Test that two conscutive requests can be sent over the same (persistent)
+// connection.
+TEST_F(HttpClientTest, consecutiveRequests) {
+ ASSERT_NO_FATAL_FAILURE(testConsecutiveRequests(HttpVersion(1, 1)));
+}
+
+// Test that two consecutive requests can be sent over non-persistent connection.
+// This is achieved by sending HTTP/1.0 requests, which are non-persistent by
+// default. The client should close the connection right after receiving a response
+// from the server.
+TEST_F(HttpClientTest, closeBetweenRequests) {
+ ASSERT_NO_FATAL_FAILURE(testConsecutiveRequests(HttpVersion(1, 0)));
+}
+
+// Test that the client can communicate with two different destinations
+// simultaneously.
+TEST_F(HttpClientTest, multipleDestinations) {
+ // Start two servers running on different ports.
+ ASSERT_NO_THROW(listener_.start());
+ ASSERT_NO_THROW(listener2_.start());
+
+ // Create the client. It will be communicating with the two servers.
+ HttpClient client(io_service_);
+
+ // Specify the URLs on which the servers are available.
+ Url url1("http://127.0.0.1:18123");
+ Url url2("http://127.0.0.1:18124");
+
+ // Create a request to the first server.
+ PostHttpRequestJsonPtr request1 = createRequest("sequence", 1);
+ HttpResponseJsonPtr response1(new HttpResponseJson());
+ unsigned resp_num = 0;
+ ASSERT_NO_THROW(client.asyncSendRequest(url1, request1, response1,
+ [this, &resp_num](const boost::system::error_code& ec,
+ const HttpResponsePtr&,
+ const std::string&) {
+ if (++resp_num > 1) {
+ io_service_.stop();
+ }
+ EXPECT_FALSE(ec);
+ }));
+
+ // Create a request to the second server.
+ PostHttpRequestJsonPtr request2 = createRequest("sequence", 2);
+ HttpResponseJsonPtr response2(new HttpResponseJson());
+ ASSERT_NO_THROW(client.asyncSendRequest(url2, request2, response2,
+ [this, &resp_num](const boost::system::error_code& ec,
+ const HttpResponsePtr&,
+ const std::string&) {
+ if (++resp_num > 1) {
+ io_service_.stop();
+ }
+ EXPECT_FALSE(ec);
+ }));
+
+ // Actually trigger the requests.
+ ASSERT_NO_THROW(runIOService());
+
+ // Make sure we have received two different responses.
+ ASSERT_TRUE(response1);
+ ConstElementPtr sequence1 = response1->getJsonElement("sequence");
+ ASSERT_TRUE(sequence1);
+
+ ASSERT_TRUE(response2);
+ ConstElementPtr sequence2 = response2->getJsonElement("sequence");
+ ASSERT_TRUE(sequence2);
+
+ EXPECT_NE(sequence1->intValue(), sequence2->intValue());
+}
+
+// Test that idle connection can be resumed for second request.
+TEST_F(HttpClientTest, idleConnection) {
+ // Start the server that has short idle timeout. It closes the idle connection
+ // after 200ms.
+ ASSERT_NO_THROW(listener3_.start());
+
+ // Create the client that will communicate with this server.
+ HttpClient client(io_service_);
+
+ // Specify the URL of this server.
+ Url url("http://127.0.0.1:18125");
+
+ // Create the first request.
+ PostHttpRequestJsonPtr request1 = createRequest("sequence", 1);
+ HttpResponseJsonPtr response1(new HttpResponseJson());
+ ASSERT_NO_THROW(client.asyncSendRequest(url, request1, response1,
+ [this](const boost::system::error_code& ec, const HttpResponsePtr&,
+ const std::string&) {
+ io_service_.stop();
+ EXPECT_FALSE(ec);
+ }));
+
+ // Run the IO service until the response is received.
+ ASSERT_NO_THROW(runIOService());
+
+ // Make sure the response has been received.
+ ASSERT_TRUE(response1);
+ ConstElementPtr sequence1 = response1->getJsonElement("sequence");
+ ASSERT_TRUE(sequence1);
+
+ // Delay the generation of the second request by 2x server idle timeout.
+ // This should be enough to cause the server to close the connection.
+ ASSERT_NO_THROW(runIOService(SHORT_IDLE_TIMEOUT * 2));
+
+ // Create another request.
+ PostHttpRequestJsonPtr request2 = createRequest("sequence", 2);
+ HttpResponseJsonPtr response2(new HttpResponseJson());
+ ASSERT_NO_THROW(client.asyncSendRequest(url, request2, response2,
+ [this](const boost::system::error_code& ec, const HttpResponsePtr&,
+ const std::string&) {
+ io_service_.stop();
+ EXPECT_FALSE(ec);
+ }));
+
+ // Actually trigger the second request.
+ ASSERT_NO_THROW(runIOService());
+
+ // Make sire that the server has responded.
+ ASSERT_TRUE(response2);
+ ConstElementPtr sequence2 = response2->getJsonElement("sequence");
+ ASSERT_TRUE(sequence2);
+
+ // Make sure that two different responses have been received.
+ EXPECT_NE(sequence1->intValue(), sequence2->intValue());
+}
+
+// This test verifies that the client returns IO error code when the
+// server is unreachable.
+TEST_F(HttpClientTest, unreachable) {
+ // Create the client.
+ HttpClient client(io_service_);
+
+ // Specify the URL of the server. This server is down.
+ Url url("http://127.0.0.1:18123");
+
+ // Create the request.
+ PostHttpRequestJsonPtr request = createRequest("sequence", 1);
+ HttpResponseJsonPtr response(new HttpResponseJson());
+ ASSERT_NO_THROW(client.asyncSendRequest(url, request, response,
+ [this](const boost::system::error_code& ec,
+ const HttpResponsePtr&,
+ const std::string&) {
+ io_service_.stop();
+ // The server should have returned an IO error.
+ EXPECT_TRUE(ec);
+ }));
+
+ // Actually trigger the request.
+ ASSERT_NO_THROW(runIOService());
+}
+
+// Test that an error is returned by the client if the server response is
+// malformed.
+TEST_F(HttpClientTest, malformedResponse) {
+ // Start the server.
+ ASSERT_NO_THROW(listener_.start());
+
+ // Create the client.
+ HttpClient client(io_service_);
+
+ // Specify the URL of the server.
+ Url url("http://127.0.0.1:18123");
+
+ // The response is going to be malformed in such a way that it holds
+ // an invalid content type. We affect the content type by creating
+ // a request that holds a JSON parameter requesting a specific
+ // content type.
+ PostHttpRequestJsonPtr request = createRequest("requested-content-type", "text/html");
+ HttpResponseJsonPtr response(new HttpResponseJson());
+ unsigned resp_num = 0;
+ ASSERT_NO_THROW(client.asyncSendRequest(url, request, response,
+ [this, &resp_num](const boost::system::error_code& ec,
+ const HttpResponsePtr& response,
+ const std::string& parsing_error) {
+ io_service_.stop();
+ // There should be no IO error (answer from the server is received).
+ EXPECT_FALSE(ec);
+ // The response object is NULL because it couldn't be finalized.
+ EXPECT_FALSE(response);
+ // The message parsing error should be returned.
+ EXPECT_FALSE(parsing_error.empty());
+ }));
+
+ // Actually trigger the request.
+ ASSERT_NO_THROW(runIOService());
+}
+
+// Test that client times out when it doesn't receive the entire response
+// from the server within a desired time.
+TEST_F(HttpClientTest, clientRequestTimeout) {
+ // Start the server.
+ ASSERT_NO_THROW(listener_.start());
+
+ // Create the client.
+ HttpClient client(io_service_);
+
+ // Specify the URL of the server.
+ Url url("http://127.0.0.1:18123");
+
+ unsigned cb_num = 0;
+
+ // Create the request which asks the server to generate a partial
+ // (although well formed) response. The client will be waiting for the
+ // rest of the response to be provided and will eventually time out.
+ PostHttpRequestJsonPtr request1 = createRequest("partial-response", true);
+ HttpResponseJsonPtr response1(new HttpResponseJson());
+ ASSERT_NO_THROW(client.asyncSendRequest(url, request1, response1,
+ HttpClient::RequestTimeout(100),
+ [this, &cb_num](const boost::system::error_code& ec,
+ const HttpResponsePtr& response,
+ const std::string&) {
+ if (++cb_num > 1) {
+ io_service_.stop();
+ }
+ // In this particular case we know exactly the type of the
+ // IO error returned, because the client explicitly sets this
+ // error code.
+ EXPECT_TRUE(ec.value() == boost::asio::error::timed_out);
+ // There should be no response returned.
+ EXPECT_FALSE(response);
+ }));
+
+ // Create another request after the timeout. It should be handled ok.
+ PostHttpRequestJsonPtr request2 = createRequest("sequence", 1);
+ HttpResponseJsonPtr response2(new HttpResponseJson());
+ ASSERT_NO_THROW(client.asyncSendRequest(url, request2, response2,
+ [this, &cb_num](const boost::system::error_code& ec, const HttpResponsePtr&,
+ const std::string&) {
+ if (++cb_num > 1) {
+ io_service_.stop();
+ }
+ }));
+
+ // Actually trigger the requests.
+ ASSERT_NO_THROW(runIOService());
+}
+
+
}
diff --git a/src/lib/http/tests/response_test.h b/src/lib/http/tests/response_test.h
index 8d4e24e1c6..d342a645a6 100644
--- a/src/lib/http/tests/response_test.h
+++ b/src/lib/http/tests/response_test.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2016-2017 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2016-2018 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -9,26 +9,50 @@
#include <http/http_types.h>
#include <http/response.h>
+#include <boost/lexical_cast.hpp>
+#include <cstdint>
namespace isc {
namespace http {
namespace test {
+/// @brief Base class for test HTTP response.
template<typename HttpResponseType>
class TestHttpResponseBase : public HttpResponseType {
public:
- TestHttpResponseBase(const HttpVersion& version, const HttpStatusCode& status_code)
+ /// @brief Constructor.
+ ///
+ /// @param version HTTP version for the response.
+ /// @param status_code HTTP status code.
+ TestHttpResponseBase(const HttpVersion& version,
+ const HttpStatusCode& status_code)
: HttpResponseType(version, status_code) {
}
+ /// @brief Returns fixed header value.
+ ///
+ /// Including fixed header value in the response makes the
+ /// response deterministic, which is critical for the unit
+ /// tests.
virtual std::string getDateHeaderValue() const {
return ("Tue, 19 Dec 2016 18:53:35 GMT");
}
+ /// @brief Returns date header value.
std::string generateDateHeaderValue() const {
return (HttpResponseType::getDateHeaderValue());
}
+
+ /// @brief Sets custom content length.
+ ///
+ /// @param content_length Content length value.
+ void setContentLength(const uint64_t content_length) {
+ HttpHeaderPtr length_header(new HttpHeader("Content-Length",
+ boost::lexical_cast<std::string>
+ (content_length)));
+ HttpResponseType::headers_["content-length"] = length_header;
+ }
};
} // namespace test
diff --git a/src/lib/http/tests/url_unittests.cc b/src/lib/http/tests/url_unittests.cc
new file mode 100644
index 0000000000..9ba615a212
--- /dev/null
+++ b/src/lib/http/tests/url_unittests.cc
@@ -0,0 +1,115 @@
+// Copyright (C) 2017 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <http/url.h>
+#include <gtest/gtest.h>
+#include <string>
+
+using namespace isc::http;
+
+namespace {
+
+/// @brief Test fixture class for @c Url class.
+class UrlTest : public ::testing::Test {
+public:
+
+ /// @brief Test valid URL.
+ ///
+ /// @param text_url URL is the text form.
+ /// @param expected_scheme Expected scheme.
+ /// @param expected_hostname Expected hostname.
+ /// @param expected_port Expected port.
+ /// @param expected_path Expected path.
+ void testValidUrl(const std::string& text_url,
+ const Url::Scheme& expected_scheme,
+ const std::string& expected_hostname,
+ const unsigned expected_port,
+ const std::string& expected_path) {
+ Url url(text_url);
+ ASSERT_TRUE(url.isValid()) << url.getErrorMessage();
+ EXPECT_EQ(expected_scheme, url.getScheme());
+ EXPECT_EQ(expected_hostname, url.getHostname());
+ EXPECT_EQ(expected_port, url.getPort());
+ EXPECT_EQ(expected_path, url.getPath());
+ }
+
+ /// @brief Test invalid URL.
+ ///
+ /// @param text_url URL is the text form.
+ void testInvalidUrl(const std::string& text_url) {
+ Url url(text_url);
+ EXPECT_FALSE(url.isValid());
+ }
+};
+
+// URL contains scheme and hostname.
+TEST_F(UrlTest, schemeHostname) {
+ testValidUrl("http://example.org", Url::HTTP, "example.org", 0, "");
+}
+
+// URL contains scheme, hostname and slash.
+TEST_F(UrlTest, schemeHostnameSlash) {
+ testValidUrl("http://example.org/", Url::HTTP, "example.org", 0, "/");
+}
+
+// URL contains scheme, IPv6 address and slash.
+TEST_F(UrlTest, schemeIPv6AddressSlash) {
+ testValidUrl("http://[2001:db8:1::100]/", Url::HTTP, "[2001:db8:1::100]", 0, "/");
+}
+
+// URL contains scheme, IPv4 address and slash.
+TEST_F(UrlTest, schemeIPv4AddressSlash) {
+ testValidUrl("http://192.0.2.2/", Url::HTTP, "192.0.2.2", 0, "/");
+}
+
+// URL contains scheme, hostname and path.
+TEST_F(UrlTest, schemeHostnamePath) {
+ testValidUrl("http://example.org/some/path", Url::HTTP, "example.org", 0,
+ "/some/path");
+}
+
+// URL contains scheme, hostname and port.
+TEST_F(UrlTest, schemeHostnamePort) {
+ testValidUrl("http://example.org:8080/", Url::HTTP, "example.org", 8080, "/");
+}
+
+// URL contains scheme, hostname, port and slash.
+TEST_F(UrlTest, schemeHostnamePortSlash) {
+ testValidUrl("http://example.org:8080/", Url::HTTP, "example.org", 8080, "/");
+}
+
+// URL contains scheme, IPv6 address and port.
+TEST_F(UrlTest, schemeIPv6AddressPort) {
+ testValidUrl("http://[2001:db8:1::1]:8080/", Url::HTTP, "[2001:db8:1::1]", 8080, "/");
+}
+
+// URL contains scheme, hostname, port and path.
+TEST_F(UrlTest, schemeHostnamePortPath) {
+ testValidUrl("http://example.org:8080/path/", Url::HTTP, "example.org", 8080,
+ "/path/");
+}
+
+// URL contains https scheme, hostname, port and path.
+TEST_F(UrlTest, secureSchemeHostnamePortPath) {
+ testValidUrl("https://example.org:8080/path/", Url::HTTPS, "example.org", 8080,
+ "/path/");
+}
+
+// Tests various invalid URLS.
+TEST_F(UrlTest, invalidUrls) {
+ testInvalidUrl("example.org");
+ testInvalidUrl("file://example.org");
+ testInvalidUrl("http//example.org");
+ testInvalidUrl("http:/example.org");
+ testInvalidUrl("http://");
+ testInvalidUrl("http://[]");
+ testInvalidUrl("http://[2001:db8:1::1");
+ testInvalidUrl("http://example.org:");
+ testInvalidUrl("http://example.org:abc");
+}
+
+}
diff --git a/src/lib/http/url.cc b/src/lib/http/url.cc
new file mode 100644
index 0000000000..196bcdbec6
--- /dev/null
+++ b/src/lib/http/url.cc
@@ -0,0 +1,208 @@
+// Copyright (C) 2017 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <exceptions/exceptions.h>
+#include <http/url.h>
+#include <boost/lexical_cast.hpp>
+#include <sstream>
+
+#include <iostream>
+
+namespace isc {
+namespace http {
+
+Url::Url(const std::string& url)
+ : url_(url), valid_(false), error_message_(), scheme_(Url::HTTPS),
+ hostname_(), port_(0), path_() {
+ parse();
+}
+
+bool
+Url::operator<(const Url& url) const {
+ return (toText() < url.toText());
+}
+
+Url::Scheme
+Url::getScheme() const {
+ checkValid();
+ return (scheme_);
+}
+
+std::string
+Url::getHostname() const {
+ checkValid();
+ return (hostname_);
+}
+
+unsigned
+Url::getPort() const {
+ checkValid();
+ return (port_);
+}
+
+std::string
+Url::getPath() const {
+ checkValid();
+ return (path_);
+}
+
+std::string
+Url::toText() const {
+ std::ostringstream s;
+ s << (getScheme() == HTTP ? "http" : "https");
+ s << "://" << getHostname();
+
+ if (getPort() != 0) {
+ s << ":" << getPort();
+ }
+
+ s << getPath();
+
+ return (s.str());
+}
+
+void
+Url::checkValid() const {
+ if (!isValid()) {
+ isc_throw(InvalidOperation, "invalid URL " << url_ << ": " << error_message_);
+ }
+}
+
+void
+Url::parse() {
+ valid_ = false;
+ error_message_.clear();
+ scheme_ = Url::HTTPS;
+ hostname_.clear();
+ port_ = 0;
+ path_.clear();
+
+ std::ostringstream e;
+
+ // Retrieve scheme
+ size_t p = url_.find(":");
+ if ((p == 0) || (p == std::string::npos)) {
+ e << "url " << url_ << " lacks http or https scheme";
+ error_message_ = e.str();
+ return;
+ }
+
+ // Validate scheme.
+ std::string scheme = url_.substr(0, p);
+ if (scheme == "http") {
+ scheme_ = Url::HTTP;
+
+ } else if (scheme == "https") {
+ scheme_ = Url::HTTPS;
+
+ } else {
+ e << "invalid scheme " << scheme << " in " << url_;
+ error_message_ = e.str();
+ return;
+ }
+
+ // Colon and two slashes should follow the scheme
+ if (url_.substr(p, 3) != "://") {
+ e << "expected :// after scheme in " << url_;
+ error_message_ = e.str();
+ return;
+ }
+
+ // Move forward to hostname.
+ p += 3;
+ if (p >= url_.length()) {
+ e << "hostname missing in " << url_;
+ error_message_ = e.str();
+ return;
+ }
+
+ size_t h = 0;
+
+ // IPv6 address is specified within [ ].
+ if (url_.at(p) == '[') {
+ h = url_.find(']', p);
+ if (h == std::string::npos) {
+ e << "expected ] after IPv6 address in " << url_;
+ error_message_ = e.str();
+ return;
+
+ } else if (h == p + 1) {
+ e << "expected IPv6 address within [] in " << url_;
+ error_message_ = e.str();
+ return;
+ }
+
+ // Move one character beyond the ].
+ ++h;
+
+ } else {
+ // There is a normal hostname or IPv4 address. It is terminated
+ // by the colon (for port number), a slash (if no port number) or
+ // goes up to the end of the URL.
+ h = url_.find(":", p);
+ if (h == std::string::npos) {
+ h = url_.find("/", p);
+ if (h == std::string::npos) {
+ // No port number and no slash.
+ h = url_.length();
+ }
+ }
+ }
+
+ // Extract the hostname.
+ hostname_ = url_.substr(p, h - p);
+
+ // If there is no port number and no path, simply return and mark the
+ // URL as valid.
+ if (h == url_.length()) {
+ valid_ = true;
+ return;
+ }
+
+ // If there is a port number, we need to read it and convert to
+ // numeric value.
+ if (url_.at(h) == ':') {
+ if (h == url_.length() - 1) {
+ e << "expected port number after : in " << url_;
+ error_message_ = e.str();
+ return;
+ }
+ // Move to the port number.
+ ++h;
+
+ // Port number may be terminated by a slash or by the end of URL.
+ size_t s = url_.find('/', h);
+ std::string port_str;
+ if (s == std::string::npos) {
+ port_str = url_.substr(h);
+ } else {
+ port_str = url_.substr(h, s - h);
+ }
+
+ try {
+ // Try to convert the port number to numeric value.
+ port_ = boost::lexical_cast<unsigned>(port_str);
+
+ } catch (...) {
+ e << "invalid port number " << port_str << " in " << url_;
+ error_message_ = e.str();
+ return;
+ }
+
+ // Go to the end of the port section.
+ h = s;
+ }
+
+ // If there is anything left in the URL, we consider it a path.
+ if (h != std::string::npos) {
+ path_ = url_.substr(h);
+ }
+
+ valid_ = true;
+}
+
+} // end of namespace isc::http
+} // end of namespace isc
diff --git a/src/lib/http/url.h b/src/lib/http/url.h
new file mode 100644
index 0000000000..a688cdcd9d
--- /dev/null
+++ b/src/lib/http/url.h
@@ -0,0 +1,110 @@
+// Copyright (C) 2017 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#ifndef KEA_URL_H
+#define KEA_URL_H
+
+#include <asiolink/io_address.h>
+#include <string>
+
+namespace isc {
+namespace http {
+
+/// @brief Represents URL.
+///
+/// It parses the provided URL and allows for retrieving the parts
+/// of it after parsing.
+class Url {
+public:
+
+ /// @brief Scheme: https or http.
+ enum Scheme {
+ HTTP,
+ HTTPS
+ };
+
+ /// @brief Constructor.
+ ///
+ /// Parses provided URL.
+ ///
+ /// @param url URL.
+ explicit Url(const std::string& url);
+
+ bool operator<(const Url& url) const;
+
+ /// @brief Checks if the URL is valid.
+ ///
+ /// @return true if the URL is valid, false otherwise.
+ bool isValid() const {
+ return (valid_);
+ }
+
+ /// @brief Returns parsing error message.
+ std::string getErrorMessage() const {
+ return (error_message_);
+ }
+
+ /// @brief Returns parsed scheme.
+ ///
+ /// @throw InvalidOperation if URL is invalid.
+ Scheme getScheme() const;
+
+ /// @brief Returns hostname.
+ ///
+ /// @throw InvalidOperation if URL is invalid.
+ std::string getHostname() const;
+
+ /// @brief Returns port number.
+ ///
+ /// @return Port number or 0 if URL doesn't contain port number.
+ /// @throw InvalidOperation if URL is invalid.
+ unsigned getPort() const;
+
+ /// @brief Returns path.
+ ///
+ /// @throw InvalidOperation if URL is invalid.
+ std::string getPath() const;
+
+ /// @brief Returns textual representation of the URL.
+ std::string toText() const;
+
+private:
+
+ /// @brief Returns boolean value indicating if the URL is valid.
+ void checkValid() const;
+
+ /// @brief Parses URL.
+ ///
+ /// This method doesn't throw an exception. Call @c isValid to see
+ /// if the URL is valid.
+ void parse();
+
+ /// @brief Holds specified URL.
+ std::string url_;
+
+ /// @brief A flag indicating if the URL is valid.
+ bool valid_;
+
+ /// @brief Holds error message after parsing.
+ std::string error_message_;
+
+ /// @brief Parsed scheme.
+ Scheme scheme_;
+
+ /// @brief Parsed hostname.
+ std::string hostname_;
+
+ /// @brief Parsed port number.
+ unsigned port_;
+
+ /// @brief Parsed path.
+ std::string path_;
+};
+
+} // end of namespace isc::http
+} // end of namespace isc
+
+#endif // endif