From: Marcin Siodelski Date: Wed, 13 Dec 2017 13:20:37 +0000 (+0100) Subject: [5448] Added support for persistent connections and keep alives. X-Git-Tag: trac5452_base~5^2~1 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=3f0c9dd159c0747c43e6abe320cce9e23318e1a0;p=thirdparty%2Fkea.git [5448] Added support for persistent connections and keep alives. --- diff --git a/src/bin/agent/ca_process.cc b/src/bin/agent/ca_process.cc index e1fd8dac61..2f2e157bba 100644 --- a/src/bin/agent/ca_process.cc +++ b/src/bin/agent/ca_process.cc @@ -26,6 +26,8 @@ namespace { const long REQUEST_TIMEOUT = 10000; +const long IDLE_TIMEOUT = 30000; + } namespace isc { @@ -151,10 +153,11 @@ CtrlAgentProcess::configure(isc::data::ConstElementPtr config_set, // Create http listener. It will open up a TCP socket and be // prepared to accept incoming connection. - HttpListenerPtr http_listener(new HttpListener(*getIoService(), - server_address, - server_port, rcf, - REQUEST_TIMEOUT)); + HttpListenerPtr + http_listener(new HttpListener(*getIoService(), server_address, + server_port, rcf, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT))); // Instruct the http listener to actually open socket, install // callback and start listening. diff --git a/src/lib/http/connection.cc b/src/lib/http/connection.cc index 1e5f5cb2ec..418715ea8a 100644 --- a/src/lib/http/connection.cc +++ b/src/lib/http/connection.cc @@ -30,9 +30,12 @@ HttpConnection:: HttpConnection(asiolink::IOService& io_service, HttpConnectionPool& connection_pool, const HttpResponseCreatorPtr& response_creator, const HttpAcceptorCallback& callback, - const long request_timeout) + const long request_timeout, + const long idle_timeout) : request_timer_(io_service), + request_timer_setup_(false), request_timeout_(request_timeout), + idle_timeout_(idle_timeout), socket_(io_service), acceptor_(acceptor), connection_pool_(connection_pool), @@ -50,6 +53,7 @@ HttpConnection::~HttpConnection() { void HttpConnection::close() { + request_timer_setup_ = false; request_timer_.cancel(); socket_.close(); } @@ -117,7 +121,13 @@ HttpConnection::doWrite() { output_buf_.length(), cb); } else { - stopThisConnection(); + if (!request_->isPersistent()) { + stopThisConnection(); + + } else { + reinitProcessingState(); + doRead(); + } } } catch (const std::exception& ex) { stopThisConnection(); @@ -148,13 +158,8 @@ HttpConnection::acceptorCallback(const boost::system::error_code& ec) { HTTP_REQUEST_RECEIVE_START) .arg(getRemoteEndpointAddressAsText()) .arg(static_cast(request_timeout_/1000)); - // Pass raw pointer rather than shared_ptr to this object, - // because IntervalTimer already passes shared pointer to the - // IntervalTimerImpl to make sure that the callback remains - // valid. - request_timer_.setup(boost::bind(&HttpConnection::requestTimeoutCallback, - this), - request_timeout_, IntervalTimer::ONE_SHOT); + + setupRequestTimer(); doRead(); } } @@ -241,10 +246,47 @@ HttpConnection::socketWriteCallback(boost::system::error_code ec, size_t length) } else { output_buf_.clear(); - stopThisConnection(); + + if (!request_->isPersistent()) { + stopThisConnection(); + + } else { + reinitProcessingState(); + doRead(); + } } } +void +HttpConnection::reinitProcessingState() { + request_ = response_creator_->createNewHttpRequest(); + parser_.reset(new HttpRequestParser(*request_)); + parser_->initModel(); + setupIdleTimer(); +} + +void +HttpConnection::setupRequestTimer() { + // Pass raw pointer rather than shared_ptr to this object, + // because IntervalTimer already passes shared pointer to the + // IntervalTimerImpl to make sure that the callback remains + // valid. + if (!request_timer_setup_) { + request_timer_setup_ = true; + request_timer_.setup(boost::bind(&HttpConnection::requestTimeoutCallback, + this), + request_timeout_, IntervalTimer::ONE_SHOT); + } +} + +void +HttpConnection::setupIdleTimer() { + request_timer_setup_ = false; + request_timer_.setup(boost::bind(&HttpConnection::idleTimeoutCallback, + this), + idle_timeout_, IntervalTimer::ONE_SHOT); +} + void HttpConnection::requestTimeoutCallback() { LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_DETAIL, @@ -256,6 +298,14 @@ HttpConnection::requestTimeoutCallback() { asyncSendResponse(response); } +void +HttpConnection::idleTimeoutCallback() { + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_DETAIL, + HTTP_IDLE_CONNECTION_TIMEOUT_OCCURRED) + .arg(getRemoteEndpointAddressAsText()); + stopThisConnection(); +} + std::string HttpConnection::getRemoteEndpointAddressAsText() const { try { diff --git a/src/lib/http/connection.h b/src/lib/http/connection.h index 6305d55159..fd387244e9 100644 --- a/src/lib/http/connection.h +++ b/src/lib/http/connection.h @@ -92,12 +92,15 @@ public: /// create HTTP response from the HTTP request received. /// @param callback Callback invoked when new connection is accepted. /// @param request_timeout Configured timeout for a HTTP request. + /// @param idle_timeout Timeout after which persistent HTTP connection is + /// closed by the server. HttpConnection(asiolink::IOService& io_service, HttpAcceptor& acceptor, HttpConnectionPool& connection_pool, const HttpResponseCreatorPtr& response_creator, const HttpAcceptorCallback& callback, - const long request_timeout); + const long request_timeout, + const long idle_timeout); /// @brief Destructor. /// @@ -166,12 +169,28 @@ private: void socketWriteCallback(boost::system::error_code ec, size_t length); + /// @brief Reinitializes request processing state after sending a response. + /// + /// This method is only called for persistent connections, when the response + /// to a previous command has been sent. It initializes the state machine to + /// be able to process the next request. It also sets the persistent connection + /// idle timer to monitor the connection timeout. + void reinitProcessingState(); + + /// @brief Reset timer for detecting request timeouts. + void setupRequestTimer(); + + /// @brief Reset timer for detecing idle timeout in persistent connections. + void setupIdleTimer(); + /// @brief Callback invoked when the HTTP Request Timeout occurs. /// /// This callback creates HTTP response with Request Timeout error code /// and sends it to the client. void requestTimeoutCallback(); + void idleTimeoutCallback(); + /// @brief Stops current connection. void stopThisConnection(); @@ -181,9 +200,15 @@ private: /// @brief Timer used to detect Request Timeout. asiolink::IntervalTimer request_timer_; + bool request_timer_setup_; + /// @brief Configured Request Timeout in milliseconds. long request_timeout_; + /// @brief Timeout after which the persistent HTTP connection is closed + /// by the server. + long idle_timeout_; + /// @brief Socket used by this connection. asiolink::TCPSocket socket_; diff --git a/src/lib/http/http_messages.mes b/src/lib/http/http_messages.mes index ad317fbf0b..414eab53df 100644 --- a/src/lib/http/http_messages.mes +++ b/src/lib/http/http_messages.mes @@ -22,6 +22,10 @@ of the request. The first argument specifies the amount of received data. The second argument specifies an address of the remote endpoint which produced the data. +% HTTP_IDLE_CONNECTION_TIMEOUT_OCCURRED closing persistent connection with %1 as a result of a timeout +This debug message is issued when the persistent HTTP connection is being +closed as a result of being idle. + % HTTP_REQUEST_RECEIVED received HTTP request from %1 This debug message is issued when the server finished receiving a HTTP request from the remote endpoint. The address of the remote endpoint is diff --git a/src/lib/http/listener.cc b/src/lib/http/listener.cc index b0e9d2c061..643376942f 100644 --- a/src/lib/http/listener.cc +++ b/src/lib/http/listener.cc @@ -37,6 +37,8 @@ public: /// create @ref HttpResponseCreator instances. /// @param request_timeout Timeout after which the HTTP Request Timeout /// is generated. + /// @param idle_timeout Timeout after which an idle persistent HTTP + /// connection is closed by the server. /// /// @throw HttpListenerError when any of the specified parameters is /// invalid. @@ -44,7 +46,8 @@ public: const asiolink::IOAddress& server_address, const unsigned short server_port, const HttpResponseCreatorFactoryPtr& creator_factory, - const long request_timeout); + const long request_timeout, + const long idle_timeout); /// @brief Returns reference to the current listener endpoint. const TCPEndpoint& getEndpoint() const; @@ -97,16 +100,21 @@ private: /// @brief Timeout for HTTP Request Timeout desired. long request_timeout_; + + /// @brief Timeout after which idle persistent connection is closed by + /// the server. + long idle_timeout_; }; HttpListenerImpl::HttpListenerImpl(IOService& io_service, const asiolink::IOAddress& server_address, const unsigned short server_port, const HttpResponseCreatorFactoryPtr& creator_factory, - const long request_timeout) + const long request_timeout, + const long idle_timeout) : io_service_(io_service), acceptor_(io_service), endpoint_(), creator_factory_(creator_factory), - request_timeout_(request_timeout) { + request_timeout_(request_timeout), idle_timeout_(idle_timeout) { // Try creating an endpoint. This may cause exceptions. try { endpoint_.reset(new TCPEndpoint(server_address, server_port)); @@ -127,6 +135,12 @@ HttpListenerImpl::HttpListenerImpl(IOService& io_service, isc_throw(HttpListenerError, "Invalid desired HTTP request timeout " << request_timeout_); } + + // Idle persistent connection timeout is signed and must be greater than 0. + if (idle_timeout_ <= 0) { + isc_throw(HttpListenerError, "Invalid desired HTTP idle persistent connection" + " timeout " << idle_timeout_); + } } const TCPEndpoint& @@ -169,7 +183,8 @@ HttpListenerImpl::accept() { connections_, response_creator, acceptor_callback, - request_timeout_)); + request_timeout_, + idle_timeout_)); // Add this new connection to the pool. connections_.start(conn); } @@ -185,9 +200,11 @@ HttpListener::HttpListener(IOService& io_service, const asiolink::IOAddress& server_address, const unsigned short server_port, const HttpResponseCreatorFactoryPtr& creator_factory, - const long request_timeout) + const HttpListener::RequestTimeout& request_timeout, + const HttpListener::IdleTimeout& idle_timeout) : impl_(new HttpListenerImpl(io_service, server_address, server_port, - creator_factory, request_timeout)) { + creator_factory, request_timeout.value_, + idle_timeout.value_)) { } HttpListener::~HttpListener() { diff --git a/src/lib/http/listener.h b/src/lib/http/listener.h index 1659d8868b..7c41bc2007 100644 --- a/src/lib/http/listener.h +++ b/src/lib/http/listener.h @@ -51,6 +51,28 @@ class HttpListenerImpl; class HttpListener { public: + /// @brief HTTP request timeout value. + struct RequestTimeout { + /// @brief Constructor. + /// + /// @param value Request timeout value in milliseconds. + explicit RequestTimeout(long value) + : value_(value) { + } + long value_; ///< Request timeout value specified. + }; + + /// @brief Idle connection timeout. + struct IdleTimeout { + /// @brief Constructor. + /// + /// @param value Connection idle timeout value in milliseconds. + explicit IdleTimeout(long value) + : value_(value) { + } + long value_; ///< Connection idle timeout value specified. + }; + /// @brief Constructor. /// /// This constructor creates new server endpoint using the specified IP @@ -67,6 +89,8 @@ public: /// create @ref HttpResponseCreator instances. /// @param request_timeout Timeout after which the HTTP Request Timeout /// is generated. + /// @param idle_timeout Timeout after which an idle persistent HTTP + /// connection is closed by the server. /// /// @throw HttpListenerError when any of the specified parameters is /// invalid. @@ -74,7 +98,8 @@ public: const asiolink::IOAddress& server_address, const unsigned short server_port, const HttpResponseCreatorFactoryPtr& creator_factory, - const long request_timeout); + const RequestTimeout& request_timeout, + const IdleTimeout& idle_timeout); /// @brief Destructor. /// diff --git a/src/lib/http/tests/connection_pool_unittests.cc b/src/lib/http/tests/connection_pool_unittests.cc index 3cf2797e20..42fda5ff22 100644 --- a/src/lib/http/tests/connection_pool_unittests.cc +++ b/src/lib/http/tests/connection_pool_unittests.cc @@ -30,6 +30,12 @@ typedef TestHttpResponseBase Response; /// @brief Pointer to test HTTP response. typedef boost::shared_ptr ResponsePtr; +/// @brief Request timeout used in tests. +const long CONN_REQUEST_TIMEOUT = 1000; + +/// @brief Idle connecion timeout used in tests. +const long CONN_IDLE_TIMEOUT = 1000; + /// @brief Implementation of the @ref HttpResponseCreator. class TestHttpResponseCreator : public HttpResponseCreator { public: @@ -114,12 +120,14 @@ TEST_F(HttpConnectionPoolTest, startStop) { connection_pool_, response_creator_, HttpAcceptorCallback(), - 1000)); + CONN_REQUEST_TIMEOUT, + CONN_IDLE_TIMEOUT)); HttpConnectionPtr conn2(new HttpConnection(io_service_, acceptor_, connection_pool_, response_creator_, HttpAcceptorCallback(), - 1000)); + CONN_REQUEST_TIMEOUT, + CONN_IDLE_TIMEOUT)); // The pool should be initially empty. TestHttpConnectionPool pool; ASSERT_TRUE(pool.connections_.empty()); @@ -152,12 +160,14 @@ TEST_F(HttpConnectionPoolTest, stopAll) { connection_pool_, response_creator_, HttpAcceptorCallback(), - 1000)); + CONN_REQUEST_TIMEOUT, + CONN_IDLE_TIMEOUT)); HttpConnectionPtr conn2(new HttpConnection(io_service_, acceptor_, connection_pool_, response_creator_, HttpAcceptorCallback(), - 1000)); + CONN_REQUEST_TIMEOUT, + CONN_IDLE_TIMEOUT)); TestHttpConnectionPool pool; ASSERT_NO_THROW(pool.start(conn1)); ASSERT_NO_THROW(pool.start(conn2)); @@ -176,12 +186,14 @@ TEST_F(HttpConnectionPoolTest, stopInvalid) { connection_pool_, response_creator_, HttpAcceptorCallback(), - 1000)); + CONN_REQUEST_TIMEOUT, + CONN_IDLE_TIMEOUT)); HttpConnectionPtr conn2(new HttpConnection(io_service_, acceptor_, connection_pool_, response_creator_, HttpAcceptorCallback(), - 1000)); + CONN_REQUEST_TIMEOUT, + CONN_IDLE_TIMEOUT)); TestHttpConnectionPool pool; ASSERT_NO_THROW(pool.start(conn1)); ASSERT_NO_THROW(pool.stop(conn2)); diff --git a/src/lib/http/tests/listener_unittests.cc b/src/lib/http/tests/listener_unittests.cc index 3b6aa025ce..28f7e8a1c3 100644 --- a/src/lib/http/tests/listener_unittests.cc +++ b/src/lib/http/tests/listener_unittests.cc @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -18,6 +19,7 @@ #include #include #include +#include #include using namespace boost::asio::ip; @@ -36,6 +38,9 @@ const unsigned short SERVER_PORT = 18123; /// @brief Request Timeout used in most of the tests (ms). const long REQUEST_TIMEOUT = 10000; +/// @brief Persistent connection idle timeout used in most of the tests (ms). +const long IDLE_TIMEOUT = 10000; + /// @brief Test timeout (ms). const long TEST_TIMEOUT = 10000; @@ -244,6 +249,36 @@ public: }); } + /// @brief Checks if the TCP connection is still open. + /// + /// Tests the TCP connection by trying to read from the socket. + /// + /// @return true if the TCP connection is open. + bool isConnectionAlive() { + // 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); + + // We need to provide a buffer for a call to read. + char data[2]; + boost::system::error_code ec; + boost::asio::read(socket_, boost::asio::buffer(data, sizeof(data)), 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)); + } + /// @brief Close connection. void close() { socket_.close(); @@ -280,8 +315,8 @@ public: /// Starts test timer which detects timeouts. HttpListenerTest() : io_service_(), factory_(new TestHttpResponseCreatorFactory()), - test_timer_(io_service_), clients_() { - test_timer_.setup(boost::bind(&HttpListenerTest::timeoutHandler, this), + test_timer_(io_service_), run_io_service_timer_(io_service_), clients_() { + test_timer_.setup(boost::bind(&HttpListenerTest::timeoutHandler, this, true), TEST_TIMEOUT, IntervalTimer::ONE_SHOT); } @@ -299,6 +334,8 @@ public: /// /// This method creates HttpClient 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_)); clients_.push_back(client); @@ -308,11 +345,45 @@ public: /// @brief Callback function invoke upon test timeout. /// /// It stops the IO service and reports test timeout. - void timeoutHandler() { - ADD_FAILURE() << "Timeout occurred while running the test!"; + /// + /// @param fail_on_timeout Specifies if test failure should be reported. + void timeoutHandler(const bool fail_on_timeout) { + if (fail_on_timeout) { + ADD_FAILURE() << "Timeout occurred while running the test!"; + } io_service_.stop(); } + /// @brief Runs IO service with optional timeout. + /// + /// @param timeout Optional value specifying for how long the io service + /// should be ran. + void runIOService(long timeout = 0) { + if (timeout > 0) { + run_io_service_timer_.setup(boost::bind(&HttpListenerTest::timeoutHandler, + this, false), + timeout, IntervalTimer::ONE_SHOT); + } + io_service_.run(); + io_service_.get_io_service().reset(); + io_service_.poll(); + } + + /// @brief Returns HTTP OK response expected by unit tests. + /// + /// @param http_version HTTP version. + /// + /// @return HTTP OK response expected by unit tests. + 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-Type: application/json\r\n" + "Date: Tue, 19 Dec 2016 18:53:35 GMT\r\n" + "\r\n"; + return (s.str()); + } + /// @brief IO service used in the tests. IOService io_service_; @@ -322,6 +393,10 @@ public: /// @brief Asynchronous timer service to detect timeouts. IntervalTimer test_timer_; + /// @brief Asynchronous timer for running IO service for a specified amount + /// of time. + IntervalTimer run_io_service_timer_; + /// @brief List of client connections. std::list clients_; }; @@ -335,21 +410,231 @@ TEST_F(HttpListenerTest, listen) { "{ }"; HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, - factory_, REQUEST_TIMEOUT); + factory_, HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); ASSERT_NO_THROW(listener.start()); ASSERT_EQ(SERVER_ADDRESS, listener.getLocalAddress().toText()); ASSERT_EQ(SERVER_PORT, listener.getLocalPort()); ASSERT_NO_THROW(startRequest(request)); - ASSERT_NO_THROW(io_service_.run()); + ASSERT_NO_THROW(runIOService()); ASSERT_EQ(1, clients_.size()); HttpClientPtr client = *clients_.begin(); ASSERT_TRUE(client); - EXPECT_EQ("HTTP/1.1 200 OK\r\n" - "Content-Length: 0\r\n" - "Content-Type: application/json\r\n" - "Date: Tue, 19 Dec 2016 18:53:35 GMT\r\n" - "\r\n", - client->getResponse()); + EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse()); + + listener.stop(); + io_service_.poll(); +} + +// This test verifies that persistent HTTP connection can be established when +// "Conection: Keep-Alive" header value is specified. +TEST_F(HttpListenerTest, keepAlive) { + + // The first request contains the keep-alive header which instructs the server + // to maintain the TCP connection after sending a response. + std::string request = "POST /foo/bar HTTP/1.0\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n" + "Connection: Keep-Alive\r\n\r\n" + "{ }"; + + HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, + factory_, HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); + + ASSERT_NO_THROW(listener.start()); + + // Send the request with the keep-alive header. + ASSERT_NO_THROW(startRequest(request)); + ASSERT_NO_THROW(runIOService()); + ASSERT_EQ(1, clients_.size()); + HttpClientPtr client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ(httpOk(HttpVersion::HTTP_10()), client->getResponse()); + + // We have sent keep-alive header so we expect that the connection with + // the server remains active. + ASSERT_TRUE(client->isConnectionAlive()); + + // Test that we can send another request via the same connection. This time + // it lacks the keep-alive header, so the server should close the connection + // after sending the response. + request = "POST /foo/bar HTTP/1.0\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n\r\n" + "{ }"; + + // Send request reusing the existing connection. + ASSERT_NO_THROW(client->sendRequest(request)); + ASSERT_NO_THROW(runIOService()); + EXPECT_EQ(httpOk(HttpVersion::HTTP_10()), client->getResponse()); + + // Connection should have been closed by the server. + EXPECT_FALSE(client->isConnectionAlive()); + + listener.stop(); + io_service_.poll(); +} + +// This test verifies that persistent HTTP connection is established by default +// when HTTP/1.1 is in use. +TEST_F(HttpListenerTest, persistentConnection) { + + // The HTTP/1.1 requests are by default persistent. + std::string request = "POST /foo/bar HTTP/1.1\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n\r\n" + "{ }"; + + HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, + factory_, HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); + + ASSERT_NO_THROW(listener.start()); + + // Send the first request. + ASSERT_NO_THROW(startRequest(request)); + ASSERT_NO_THROW(runIOService()); + ASSERT_EQ(1, clients_.size()); + HttpClientPtr client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse()); + + // HTTP/1.1 connection is persistent by default. + ASSERT_TRUE(client->isConnectionAlive()); + + // Test that we can send another request via the same connection. This time + // it includes the "Connection: close" header which instructs the server to + // close the connection after responding. + request = "POST /foo/bar HTTP/1.1\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n" + "Connection: close\r\n\r\n" + "{ }"; + + // Send request reusing the existing connection. + ASSERT_NO_THROW(client->sendRequest(request)); + ASSERT_NO_THROW(runIOService()); + EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse()); + + // Connection should have been closed by the server. + EXPECT_FALSE(client->isConnectionAlive()); + + listener.stop(); + io_service_.poll(); +} + +// This test verifies that "keep-alive" connection is closed by the server after +// an idle time. +TEST_F(HttpListenerTest, keepAliveTimeout) { + + // The first request contains the keep-alive header which instructs the server + // to maintain the TCP connection after sending a response. + std::string request = "POST /foo/bar HTTP/1.0\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n" + "Connection: Keep-Alive\r\n\r\n" + "{ }"; + + // Specify the idle timeout of 500ms. + HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, + factory_, HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(500)); + + ASSERT_NO_THROW(listener.start()); + + // Send the request with the keep-alive header. + ASSERT_NO_THROW(startRequest(request)); + ASSERT_NO_THROW(runIOService()); + ASSERT_EQ(1, clients_.size()); + HttpClientPtr client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ(httpOk(HttpVersion::HTTP_10()), client->getResponse()); + + // We have sent keep-alive header so we expect that the connection with + // the server remains active. + ASSERT_TRUE(client->isConnectionAlive()); + + // Run IO service for 1000ms. The idle time is set to 500ms, so the connection + // should be closed by the server while we wait here. + runIOService(1000); + + // Make sure the connection has been closed. + EXPECT_FALSE(client->isConnectionAlive()); + + // Check if we can re-establish the connection and send another request. + clients_.clear(); + request = "POST /foo/bar HTTP/1.0\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n\r\n" + "{ }"; + + ASSERT_NO_THROW(startRequest(request)); + ASSERT_NO_THROW(runIOService()); + ASSERT_EQ(1, clients_.size()); + client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ(httpOk(HttpVersion::HTTP_10()), client->getResponse()); + + EXPECT_FALSE(client->isConnectionAlive()); + + listener.stop(); + io_service_.poll(); +} + +// This test verifies that persistent connection is closed by the server after +// an idle time. +TEST_F(HttpListenerTest, persistentConnectionTimeout) { + + // The HTTP/1.1 requests are by default persistent. + std::string request = "POST /foo/bar HTTP/1.1\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n" + "Connection: Keep-Alive\r\n\r\n" + "{ }"; + + // Specify the idle timeout of 500ms. + HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, + factory_, HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(500)); + + ASSERT_NO_THROW(listener.start()); + + // Send the request. + ASSERT_NO_THROW(startRequest(request)); + ASSERT_NO_THROW(runIOService()); + ASSERT_EQ(1, clients_.size()); + HttpClientPtr client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse()); + + // The connection should remain active. + ASSERT_TRUE(client->isConnectionAlive()); + + // Run IO service for 1000ms. The idle time is set to 500ms, so the connection + // should be closed by the server while we wait here. + runIOService(1000); + + // Make sure the connection has been closed. + EXPECT_FALSE(client->isConnectionAlive()); + + // Check if we can re-establish the connection and send another request. + clients_.clear(); + request = "POST /foo/bar HTTP/1.1\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n" + "Connection: close\r\n\r\n" + "{ }"; + + ASSERT_NO_THROW(startRequest(request)); + ASSERT_NO_THROW(runIOService()); + ASSERT_EQ(1, clients_.size()); + client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse()); + + EXPECT_FALSE(client->isConnectionAlive()); + listener.stop(); io_service_.poll(); } @@ -357,7 +642,8 @@ TEST_F(HttpListenerTest, listen) { // This test verifies that the HTTP listener can't be started twice. TEST_F(HttpListenerTest, startTwice) { HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, - factory_, REQUEST_TIMEOUT); + factory_, HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); ASSERT_NO_THROW(listener.start()); EXPECT_THROW(listener.start(), HttpListenerError); } @@ -372,10 +658,11 @@ TEST_F(HttpListenerTest, badRequest) { "{ }"; HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, - factory_, REQUEST_TIMEOUT); + factory_, HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); ASSERT_NO_THROW(listener.start()); ASSERT_NO_THROW(startRequest(request)); - ASSERT_NO_THROW(io_service_.run()); + ASSERT_NO_THROW(runIOService()); ASSERT_EQ(1, clients_.size()); HttpClientPtr client = *clients_.begin(); ASSERT_TRUE(client); @@ -393,7 +680,8 @@ TEST_F(HttpListenerTest, badRequest) { TEST_F(HttpListenerTest, invalidFactory) { EXPECT_THROW(HttpListener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, HttpResponseCreatorFactoryPtr(), - REQUEST_TIMEOUT), + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)), HttpListenerError); } @@ -401,7 +689,18 @@ TEST_F(HttpListenerTest, invalidFactory) { // Request Timeout. TEST_F(HttpListenerTest, invalidRequestTimeout) { EXPECT_THROW(HttpListener(io_service_, IOAddress(SERVER_ADDRESS), - SERVER_PORT, factory_, 0), + SERVER_PORT, factory_, HttpListener::RequestTimeout(0), + HttpListener::IdleTimeout(IDLE_TIMEOUT)), + HttpListenerError); +} + +// This test verifies that the timeout of 0 can't be specified for the +// idle persistent connection timeout. +TEST_F(HttpListenerTest, invalidIdleTimeout) { + EXPECT_THROW(HttpListener(io_service_, IOAddress(SERVER_ADDRESS), + SERVER_PORT, factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(0)), HttpListenerError); } @@ -419,7 +718,9 @@ TEST_F(HttpListenerTest, addressInUse) { // Listener should report an error when we try to start it because another // acceptor is bound to that port and address. HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), - SERVER_PORT + 1, factory_, REQUEST_TIMEOUT); + SERVER_PORT + 1, factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); EXPECT_THROW(listener.start(), HttpListenerError); } @@ -435,10 +736,11 @@ TEST_F(HttpListenerTest, requestTimeout) { // Open the listener with the Request Timeout of 1 sec and post the // partial request. HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, - factory_, 1000); + factory_, HttpListener::RequestTimeout(1000), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); ASSERT_NO_THROW(listener.start()); ASSERT_NO_THROW(startRequest(request)); - ASSERT_NO_THROW(io_service_.run()); + ASSERT_NO_THROW(runIOService()); ASSERT_EQ(1, clients_.size()); HttpClientPtr client = *clients_.begin(); ASSERT_TRUE(client);