diff options
author | Marcin Siodelski <marcin@isc.org> | 2018-01-05 21:43:49 +0100 |
---|---|---|
committer | Marcin Siodelski <marcin@isc.org> | 2018-01-05 21:43:49 +0100 |
commit | 95923b6c408da923e4556a18b32660ac51539711 (patch) | |
tree | 6b0662978d0c21f8b367f17382c01b5c105451da /src/lib | |
parent | [5451] Implemented HTTP response parser. (diff) | |
download | kea-95923b6c408da923e4556a18b32660ac51539711.tar.xz kea-95923b6c408da923e4556a18b32660ac51539711.zip |
[5451] Implemented HTTP client.
Diffstat (limited to 'src/lib')
-rw-r--r-- | src/lib/asiolink/tcp_socket.h | 49 | ||||
-rw-r--r-- | src/lib/http/Makefile.am | 4 | ||||
-rw-r--r-- | src/lib/http/client.cc | 708 | ||||
-rw-r--r-- | src/lib/http/client.h | 142 | ||||
-rw-r--r-- | src/lib/http/post_request.cc | 11 | ||||
-rw-r--r-- | src/lib/http/post_request.h | 10 | ||||
-rw-r--r-- | src/lib/http/post_request_json.cc | 10 | ||||
-rw-r--r-- | src/lib/http/post_request_json.h | 13 | ||||
-rw-r--r-- | src/lib/http/request.cc | 6 | ||||
-rw-r--r-- | src/lib/http/request.h | 5 | ||||
-rw-r--r-- | src/lib/http/response_parser.cc | 5 | ||||
-rw-r--r-- | src/lib/http/tests/Makefile.am | 1 | ||||
-rw-r--r-- | src/lib/http/tests/listener_unittests.cc | 480 | ||||
-rw-r--r-- | src/lib/http/tests/response_test.h | 28 | ||||
-rw-r--r-- | src/lib/http/tests/url_unittests.cc | 115 | ||||
-rw-r--r-- | src/lib/http/url.cc | 208 | ||||
-rw-r--r-- | src/lib/http/url.h | 110 |
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 |