]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[5451] Implemented HTTP client.
authorMarcin Siodelski <marcin@isc.org>
Fri, 5 Jan 2018 20:43:49 +0000 (21:43 +0100)
committerMarcin Siodelski <marcin@isc.org>
Fri, 5 Jan 2018 20:43:49 +0000 (21:43 +0100)
17 files changed:
src/lib/asiolink/tcp_socket.h
src/lib/http/Makefile.am
src/lib/http/client.cc [new file with mode: 0644]
src/lib/http/client.h [new file with mode: 0644]
src/lib/http/post_request.cc
src/lib/http/post_request.h
src/lib/http/post_request_json.cc
src/lib/http/post_request_json.h
src/lib/http/request.cc
src/lib/http/request.h
src/lib/http/response_parser.cc
src/lib/http/tests/Makefile.am
src/lib/http/tests/listener_unittests.cc
src/lib/http/tests/response_test.h
src/lib/http/tests/url_unittests.cc [new file with mode: 0644]
src/lib/http/url.cc [new file with mode: 0644]
src/lib/http/url.h [new file with mode: 0644]

index adf74d1f0f2fc12c3acb3b34fd11d23ce666d149..8465c0705ed392dab724c7ae30cf72a164af0829 100644 (file)
@@ -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.
index ebdaa25f131c9582e208e15ef0a9d9e69ea6767c..f5d9ded7a2f622d3a013bdd323d33fb3dd163b3a 100644 (file)
@@ -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 (file)
index 0000000..b1a2b9e
--- /dev/null
@@ -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 (file)
index 0000000..943b19d
--- /dev/null
@@ -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
index 3cc8d810b8277b6d926fd7f9b583bd3aeb92772e..62e4a34a738d3373313048f7d6554050102113e2 100644 (file)
@@ -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
index dbfe7d7026ae286f489cd92d649c5aa3b7a4103a..733a4ae975cfd5fbaf0c1f342d84092f9b71a220 100644 (file)
@@ -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);
+
 };
 
 
index 0a93312720c58bd58b6268bb1358dd1452c22a91..0c7d74f7dca1fcbd1b7f80a80aa8ff269e6dadac 100644 (file)
@@ -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_) {
index 3a6f37ae0d3b6f1382513e8c74bfd2e6035d5a60..e3d9829ff60b4555155b37f3b3519738d17f30dd 100644 (file)
@@ -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
index 0d3186d118b26ccd9ea9f905642bac3ee8792996..65847f8343bd7469ecca7b9fd8ff5768a8cc18d3 100644 (file)
@@ -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) {
index 0cad579df571aaa944e6579b9673c40c8dfe1633..387a109519428ab5741c1c6826ca371a2eba8fc1 100644 (file)
@@ -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:
index 677c3aa7642427756bd7dab767561a40a242ea4c..aa7fb720ac28872eee2e730800f6c04bd763f8fc 100644 (file)
@@ -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);
index 3a0a937d254ac68f9473a22d9b24ab71c8606ecd..4d6e03250e0e2fc6352ee97660ba2dc44c9194b7 100644 (file)
@@ -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)
index 7b35baed81dcf78a326ec7c3bcb5b990c2fe3d86..d8979a08ddc8aa7ab9d27585572f65d2d705a486 100644 (file)
@@ -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>
 #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());
+}
+
+
 }
index 8d4e24e1c6e73269c68319c6d507826dbf4da73a..d342a645a69e2d14d8ce8661da8b6d807c73f715 100644 (file)
@@ -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 (file)
index 0000000..9ba615a
--- /dev/null
@@ -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 (file)
index 0000000..196bcdb
--- /dev/null
@@ -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 (file)
index 0000000..a688cdc
--- /dev/null
@@ -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