From: Francis Dupont Date: Fri, 2 Aug 2024 15:26:17 +0000 (+0200) Subject: [#1764] Shared common setting X-Git-Tag: Kea-2.7.2~38 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=8b3f4baba5f3aeb4e5f31b46ae75c4431f634dca;p=thirdparty%2Fkea.git [#1764] Shared common setting --- diff --git a/src/lib/http/tests/Makefile.am b/src/lib/http/tests/Makefile.am index 92f1bb7c5e..cb07e41bfc 100644 --- a/src/lib/http/tests/Makefile.am +++ b/src/lib/http/tests/Makefile.am @@ -43,7 +43,9 @@ 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 += server_client_unittests.cc +libhttp_unittests_SOURCES += http_tests.h +libhttp_unittests_SOURCES += http_client_unittests.cc +libhttp_unittests_SOURCES += http_server_unittests.cc if HAVE_OPENSSL libhttp_unittests_SOURCES += tls_server_unittests.cc libhttp_unittests_SOURCES += tls_client_unittests.cc diff --git a/src/lib/http/tests/server_client_unittests.cc b/src/lib/http/tests/http_client_unittests.cc similarity index 61% rename from src/lib/http/tests/server_client_unittests.cc rename to src/lib/http/tests/http_client_unittests.cc index b34e440477..355f6f0b84 100644 --- a/src/lib/http/tests/server_client_unittests.cc +++ b/src/lib/http/tests/http_client_unittests.cc @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -42,40 +43,6 @@ namespace ph = std::placeholders; namespace { -/// @brief IP address to which HTTP service is bound. -const std::string SERVER_ADDRESS = "127.0.0.1"; - -/// @brief IPv6 address to whch HTTP service is bound. -const std::string IPV6_SERVER_ADDRESS = "::1"; - -/// @brief Port number to which HTTP service is bound. -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 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; - -/// @brief Test HTTP response. -typedef TestHttpResponseBase Response; - -/// @brief Pointer to test HTTP response. -typedef boost::shared_ptr ResponsePtr; - -/// @brief Generic test HTTP response. -typedef TestHttpResponseBase GenericResponse; - -/// @brief Pointer to generic test HTTP response. -typedef boost::shared_ptr GenericResponsePtr; - /// @brief Implementation of the @ref HttpResponseCreator. class TestHttpResponseCreator : public HttpResponseCreator { public: @@ -197,200 +164,6 @@ public: } }; -/// @brief Implementation of the HTTP listener used in tests. -/// -/// This implementation replaces the @c HttpConnection type with a custom -/// implementation. -/// -/// @tparam HttpConnectionType Type of the connection object to be used by -/// the listener implementation. -template -class HttpListenerImplCustom : public HttpListenerImpl { -public: - - HttpListenerImplCustom(const IOServicePtr& io_service, - const IOAddress& server_address, - const unsigned short server_port, - const TlsContextPtr& tls_context, - const HttpResponseCreatorFactoryPtr& creator_factory, - const long request_timeout, - const long idle_timeout) - : HttpListenerImpl(io_service, server_address, server_port, - tls_context, creator_factory, request_timeout, - idle_timeout) { - } - -protected: - - /// @brief Creates an instance of the @c HttpConnection. - /// - /// This method is virtual so as it can be overridden when customized - /// connections are to be used, e.g. in case of unit testing. - /// - /// @param response_creator Pointer to the response creator object used to - /// create HTTP response from the HTTP request received. - /// @param callback Callback invoked when new connection is accepted. - /// - /// @return Pointer to the created connection. - virtual HttpConnectionPtr createConnection(const HttpResponseCreatorPtr& response_creator, - const HttpAcceptorCallback& callback) { - HttpConnectionPtr - conn(new HttpConnectionType(io_service_, acceptor_, - tls_context_, connections_, - response_creator, callback, - request_timeout_, idle_timeout_)); - return (conn); - } -}; - -/// @brief Derivation of the @c HttpListener used in tests. -/// -/// This class replaces the default implementation instance with the -/// @c HttpListenerImplCustom using the customized connection type. -/// -/// @tparam HttpConnectionType Type of the connection object to be used by -/// the listener implementation. -template -class HttpListenerCustom : public HttpListener { -public: - - /// @brief Constructor. - /// - /// @param io_service IO service to be used by the listener. - /// @param server_address Address on which the HTTP service should run. - /// @param server_port Port number on which the HTTP service should run. - /// @param tls_context TLS context. - /// @param creator_factory Pointer to the caller-defined - /// @ref HttpResponseCreatorFactory derivation which should be used to - /// 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. - HttpListenerCustom(const IOServicePtr& io_service, - const IOAddress& server_address, - const unsigned short server_port, - const TlsContextPtr& tls_context, - const HttpResponseCreatorFactoryPtr& creator_factory, - const HttpListener::RequestTimeout& request_timeout, - const HttpListener::IdleTimeout& idle_timeout) - : HttpListener(io_service, server_address, server_port, - tls_context, creator_factory, - request_timeout, idle_timeout) { - // Replace the default implementation with the customized version - // using the custom derivation of the HttpConnection. - impl_.reset(new HttpListenerImplCustom - (io_service, server_address, server_port, - tls_context, creator_factory, request_timeout.value_, - idle_timeout.value_)); - } -}; - -/// @brief Implementation of the @c HttpConnection which injects greater -/// length value than the buffer size into the write socket callback. -class HttpConnectionLongWriteBuffer : public HttpConnection { -public: - - /// @brief Constructor. - /// - /// @param io_service IO service to be used by the connection. - /// @param acceptor Pointer to the TCP acceptor object used to listen for - /// new HTTP connections. - /// @param tls_context TLS context. - /// @param connection_pool Connection pool in which this connection is - /// stored. - /// @param response_creator Pointer to the response creator object used to - /// 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. - HttpConnectionLongWriteBuffer(const IOServicePtr& io_service, - const HttpAcceptorPtr& acceptor, - const TlsContextPtr& tls_context, - HttpConnectionPool& connection_pool, - const HttpResponseCreatorPtr& response_creator, - const HttpAcceptorCallback& callback, - const long request_timeout, - const long idle_timeout) - : HttpConnection(io_service, acceptor, tls_context, connection_pool, - response_creator, callback, request_timeout, - idle_timeout) { - } - - /// @brief Callback invoked when data is sent over the socket. - /// - /// @param transaction Pointer to the transaction for which the callback - /// is invoked. - /// @param ec Error code. - /// @param length Length of the data sent. - virtual void socketWriteCallback(HttpConnection::TransactionPtr transaction, - boost::system::error_code ec, - size_t length) { - // Pass greater length of the data written. The callback should deal - // with this and adjust the data length. - HttpConnection::socketWriteCallback(transaction, ec, length + 1); - } -}; - -/// @brief Implementation of the @c HttpConnection which replaces -/// transaction instance prior to calling write socket callback. -class HttpConnectionTransactionChange : public HttpConnection { -public: - - /// @brief Constructor. - /// - /// @param io_service IO service to be used by the connection. - /// @param acceptor Pointer to the TCP acceptor object used to listen for - /// new HTTP connections. - /// @param context TLS tls_context. - /// @param connection_pool Connection pool in which this connection is - /// stored. - /// @param response_creator Pointer to the response creator object used to - /// 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. - HttpConnectionTransactionChange(const IOServicePtr& io_service, - const HttpAcceptorPtr& acceptor, - const TlsContextPtr& tls_context, - HttpConnectionPool& connection_pool, - const HttpResponseCreatorPtr& response_creator, - const HttpAcceptorCallback& callback, - const long request_timeout, - const long idle_timeout) - : HttpConnection(io_service, acceptor, tls_context, connection_pool, - response_creator, callback, request_timeout, - idle_timeout) { - } - - /// @brief Callback invoked when data is sent over the socket. - /// - /// @param transaction Pointer to the transaction for which the callback - /// is invoked. - /// @param ec Error code. - /// @param length Length of the data sent. - virtual void socketWriteCallback(HttpConnection::TransactionPtr transaction, - boost::system::error_code ec, - size_t length) { - // Replace the transaction. The socket callback should deal with this - // gracefully. It should detect that the output buffer is empty. Then - // try to see if the connection is persistent. This check should fail, - // because the request hasn't been created/finalized. The exception - // thrown upon checking the persistence should be caught and the - // connection closed. - transaction = HttpConnection::Transaction::create(response_creator_); - HttpConnection::socketWriteCallback(transaction, ec, length); - } -}; - -/// @brief Pointer to the TestHttpClient. -typedef boost::shared_ptr TestHttpClientPtr; - /// @brief Test fixture class for @ref HttpListener. class HttpListenerTest : public ::testing::Test { public: @@ -400,7 +173,7 @@ public: /// Starts test timer which detects timeouts. HttpListenerTest() : io_service_(new IOService()), factory_(new TestHttpResponseCreatorFactory()), - test_timer_(io_service_), run_io_service_timer_(io_service_), clients_() { + test_timer_(io_service_), run_io_service_timer_(io_service_) { test_timer_.setup(std::bind(&HttpListenerTest::timeoutHandler, this, true), TEST_TIMEOUT, IntervalTimer::ONE_SHOT); } @@ -409,25 +182,10 @@ public: /// /// Removes active HTTP clients. virtual ~HttpListenerTest() { - for (auto const& client : clients_) { - client->close(); - } test_timer_.cancel(); io_service_->stopAndPoll(); } - /// @brief Connect to the endpoint. - /// - /// 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) { - TestHttpClientPtr client(new TestHttpClient(io_service_)); - clients_.push_back(client); - clients_.back()->startRequest(request); - } - /// @brief Callback function invoke upon test timeout. /// /// It stops the IO service and reports test timeout. @@ -457,104 +215,6 @@ public: io_service_->stopAndPoll(false); } - /// @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: 33\r\n" - "Content-Type: application/json\r\n" - "Date: Tue, 19 Dec 2016 18:53:35 GMT\r\n" - "\r\n" - "{ \"remote-address\": \"127.0.0.1\" }"; - return (s.str()); - } - - /// @brief Tests that HTTP request timeout status is returned when the - /// server does not receive the entire request. - /// - /// @param request Partial request for which the parser will be waiting for - /// the next chunks of data. - /// @param expected_version HTTP version expected in the response. - void testRequestTimeout(const std::string& request, - const HttpVersion& expected_version) { - // Open the listener with the Request Timeout of 1 sec and post the - // partial request. - HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), - SERVER_PORT, TlsContextPtr(), - factory_, HttpListener::RequestTimeout(1000), - HttpListener::IdleTimeout(IDLE_TIMEOUT)); - ASSERT_NO_THROW(listener.start()); - ASSERT_NO_THROW(startRequest(request)); - ASSERT_NO_THROW(runIOService()); - ASSERT_EQ(1, clients_.size()); - TestHttpClientPtr client = *clients_.begin(); - ASSERT_TRUE(client); - - // Build the reference response. - std::ostringstream expected_response; - expected_response - << "HTTP/" << expected_version.major_ << "." << expected_version.minor_ - << " 408 Request Timeout\r\n" - "Content-Length: 44\r\n" - "Content-Type: application/json\r\n" - "Date: Tue, 19 Dec 2016 18:53:35 GMT\r\n" - "\r\n" - "{ \"result\": 408, \"text\": \"Request Timeout\" }"; - - // The server should wait for the missing part of the request for 1 second. - // The missing part never arrives so the server should respond with the - // HTTP Request Timeout status. - EXPECT_EQ(expected_response.str(), client->getResponse()); - } - - /// @brief Tests various cases when unexpected data is passed to the - /// socket write handler. - /// - /// This test uses the custom listener and the test specific derivations of - /// the @c HttpConnection class to enforce injection of the unexpected - /// data to the socket write callback. The two example applications of - /// this test are: - /// - injecting greater length value than the output buffer size, - /// - replacing the transaction with another transaction. - /// - /// It is expected that the socket write callback deals gracefully with - /// those situations. - /// - /// @tparam HttpConnectionType Test specific derivation of the - /// @c HttpConnection class. - template - void testWriteBufferIssues() { - // 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" - "{ }"; - - // Use custom listener and the specialized connection object. - HttpListenerCustom - listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, - TlsContextPtr(), factory_, - HttpListener::RequestTimeout(REQUEST_TIMEOUT), - HttpListener::IdleTimeout(IDLE_TIMEOUT)); - - ASSERT_NO_THROW(listener.start()); - - // Send the request. - ASSERT_NO_THROW(startRequest(request)); - - // Injecting unexpected data should not result in an exception. - ASSERT_NO_THROW(runIOService()); - - ASSERT_EQ(1, clients_.size()); - TestHttpClientPtr client = *clients_.begin(); - ASSERT_TRUE(client); - EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse()); - } - /// @brief IO service used in the tests. IOServicePtr io_service_; @@ -567,435 +227,8 @@ public: /// @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_; }; -// This test verifies that HTTP connection can be established and used to -// transmit HTTP request and receive a response. -TEST_F(HttpListenerTest, listen) { - const 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, - TlsContextPtr(), 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(runIOService()); - ASSERT_EQ(1, clients_.size()); - TestHttpClientPtr client = *clients_.begin(); - ASSERT_TRUE(client); - EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse()); - - listener.stop(); - io_service_->poll(); -} - - -// This test verifies that persistent HTTP connection can be established when -// "Connection: 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, - TlsContextPtr(), 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()); - TestHttpClientPtr 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_TRUE(client->isConnectionClosed()); - - 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, - TlsContextPtr(), 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()); - TestHttpClientPtr 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_TRUE(client->isConnectionClosed()); - - 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, - TlsContextPtr(), 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()); - TestHttpClientPtr 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_TRUE(client->isConnectionClosed()); - - // 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_TRUE(client->isConnectionClosed()); - - 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\r\n" - "{ }"; - - // Specify the idle timeout of 500ms. - HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, - TlsContextPtr(), 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()); - TestHttpClientPtr 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_TRUE(client->isConnectionClosed()); - - // 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_TRUE(client->isConnectionClosed()); - - listener.stop(); - io_service_->poll(); -} - -// This test verifies that HTTP/1.1 connection remains open even if there is an -// error in the message body. -TEST_F(HttpListenerTest, persistentConnectionBadBody) { - - // 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: 12\r\n\r\n" - "{ \"a\": abc }"; - - HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, - TlsContextPtr(), factory_, - HttpListener::RequestTimeout(REQUEST_TIMEOUT), - HttpListener::IdleTimeout(IDLE_TIMEOUT)); - - ASSERT_NO_THROW(listener.start()); - - // Send the request. - ASSERT_NO_THROW(startRequest(request)); - ASSERT_NO_THROW(runIOService()); - ASSERT_EQ(1, clients_.size()); - TestHttpClientPtr client = *clients_.begin(); - ASSERT_TRUE(client); - EXPECT_EQ("HTTP/1.1 400 Bad Request\r\n" - "Content-Length: 40\r\n" - "Content-Type: application/json\r\n" - "Date: Tue, 19 Dec 2016 18:53:35 GMT\r\n" - "\r\n" - "{ \"result\": 400, \"text\": \"Bad Request\" }", - client->getResponse()); - - // The connection should remain active. - ASSERT_TRUE(client->isConnectionAlive()); - - // Make sure that we can send another request. This time we specify the - // "close" connection-token to force the connection to close. - 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()); - - EXPECT_TRUE(client->isConnectionClosed()); - - listener.stop(); - io_service_->poll(); -} - -// 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, - TlsContextPtr(), factory_, - HttpListener::RequestTimeout(REQUEST_TIMEOUT), - HttpListener::IdleTimeout(IDLE_TIMEOUT)); - ASSERT_NO_THROW(listener.start()); - EXPECT_THROW(listener.start(), HttpListenerError); -} - -// This test verifies that Bad Request status is returned when the request -// is malformed. -TEST_F(HttpListenerTest, badRequest) { - // Content-Type is wrong. This should result in Bad Request status. - const std::string request = "POST /foo/bar HTTP/1.1\r\n" - "Content-Type: foo\r\n" - "Content-Length: 3\r\n\r\n" - "{ }"; - - HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, - TlsContextPtr(), factory_, - HttpListener::RequestTimeout(REQUEST_TIMEOUT), - HttpListener::IdleTimeout(IDLE_TIMEOUT)); - ASSERT_NO_THROW(listener.start()); - ASSERT_NO_THROW(startRequest(request)); - ASSERT_NO_THROW(runIOService()); - ASSERT_EQ(1, clients_.size()); - TestHttpClientPtr client = *clients_.begin(); - ASSERT_TRUE(client); - EXPECT_EQ("HTTP/1.1 400 Bad Request\r\n" - "Content-Length: 40\r\n" - "Content-Type: application/json\r\n" - "Date: Tue, 19 Dec 2016 18:53:35 GMT\r\n" - "\r\n" - "{ \"result\": 400, \"text\": \"Bad Request\" }", - client->getResponse()); -} - -// This test verifies that NULL pointer can't be specified for the -// HttpResponseCreatorFactory. -TEST_F(HttpListenerTest, invalidFactory) { - EXPECT_THROW(HttpListener(io_service_, IOAddress(SERVER_ADDRESS), - SERVER_PORT, TlsContextPtr(), - HttpResponseCreatorFactoryPtr(), - HttpListener::RequestTimeout(REQUEST_TIMEOUT), - HttpListener::IdleTimeout(IDLE_TIMEOUT)), - HttpListenerError); -} - -// This test verifies that the timeout of 0 can't be specified for the -// Request Timeout. -TEST_F(HttpListenerTest, invalidRequestTimeout) { - EXPECT_THROW(HttpListener(io_service_, IOAddress(SERVER_ADDRESS), - SERVER_PORT, TlsContextPtr(), 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, TlsContextPtr(), factory_, - HttpListener::RequestTimeout(REQUEST_TIMEOUT), - HttpListener::IdleTimeout(0)), - HttpListenerError); -} - -// This test verifies that listener can't be bound to the port to which -// other server is bound. -TEST_F(HttpListenerTest, addressInUse) { - tcp::acceptor acceptor(io_service_->getInternalIOService()); - // Use other port than SERVER_PORT to make sure that this TCP connection - // doesn't affect subsequent tests. - tcp::endpoint endpoint(address::from_string(SERVER_ADDRESS), - SERVER_PORT + 1); - acceptor.open(endpoint.protocol()); - acceptor.bind(endpoint); - - // 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, TlsContextPtr(), factory_, - HttpListener::RequestTimeout(REQUEST_TIMEOUT), - HttpListener::IdleTimeout(IDLE_TIMEOUT)); - EXPECT_THROW(listener.start(), HttpListenerError); -} - -// This test verifies that HTTP Request Timeout status is returned as -// expected when the read part of the request contains the HTTP -// version number. The timeout response should contain the same -// HTTP version number as the partial request. -TEST_F(HttpListenerTest, requestTimeoutHttpVersionFound) { - // The part of the request specified here is correct but it is not - // a complete request. - const std::string request = "POST /foo/bar HTTP/1.1\r\n" - "Content-Type: application/json\r\n" - "Content-Length:"; - - testRequestTimeout(request, HttpVersion::HTTP_11()); -} - -// This test verifies that HTTP Request Timeout status is returned as -// expected when the read part of the request does not contain -// the HTTP version number. The timeout response should by default -// contain HTTP/1.0 version number. -TEST_F(HttpListenerTest, requestTimeoutHttpVersionNotFound) { - // The part of the request specified here is correct but it is not - // a complete request. - const std::string request = "POST /foo/bar HTTP"; - - testRequestTimeout(request, HttpVersion::HTTP_10()); -} - -// This test verifies that injecting length value greater than the -// output buffer length to the socket write callback does not cause -// an exception. -TEST_F(HttpListenerTest, tooLongWriteBuffer) { - testWriteBufferIssues(); -} - -// This test verifies that changing the transaction before calling -// the socket write callback does not cause an exception. -TEST_F(HttpListenerTest, transactionChangeDuringWrite) { - testWriteBufferIssues(); -} - /// @brief Test fixture class for testing HTTP client. class HttpClientTest : public HttpListenerTest { public: diff --git a/src/lib/http/tests/http_server_unittests.cc b/src/lib/http/tests/http_server_unittests.cc new file mode 100644 index 0000000000..60e7fc6637 --- /dev/null +++ b/src/lib/http/tests/http_server_unittests.cc @@ -0,0 +1,966 @@ +// Copyright (C) 2017-2024 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace boost::asio::ip; +using namespace isc::asiolink; +using namespace isc::data; +using namespace isc::http; +using namespace isc::http::test; +using namespace isc::util; +namespace ph = std::placeholders; + +namespace { + +/// @brief Implementation of the @ref HttpResponseCreator. +class TestHttpResponseCreator : public HttpResponseCreator { +public: + + /// @brief Create a new request. + /// + /// @return Pointer to the new instance of the @ref HttpRequest. + virtual HttpRequestPtr + createNewHttpRequest() const { + return (HttpRequestPtr(new PostHttpRequestJson())); + } + +private: + + /// @brief Creates HTTP response. + /// + /// @param request Pointer to the HTTP request. + /// @return Pointer to the generated HTTP response. + virtual HttpResponsePtr + createStockHttpResponse(const HttpRequestPtr& request, + const HttpStatusCode& status_code) const { + // The request hasn't been finalized so the request object + // doesn't contain any information about the HTTP version number + // used. But, the context should have this data (assuming the + // HTTP version is parsed ok). + HttpVersion http_version(request->context()->http_version_major_, + request->context()->http_version_minor_); + // This will generate the response holding JSON content. + ResponsePtr response(new Response(http_version, status_code)); + response->finalize(); + return (response); + } + + /// @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(HttpRequestPtr request) { + // Request must always be JSON. + PostHttpRequestJsonPtr request_json = + boost::dynamic_pointer_cast(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); + } +}; + +/// @brief Implementation of the test @ref HttpResponseCreatorFactory. +/// +/// This factory class creates @ref TestHttpResponseCreator instances. +class TestHttpResponseCreatorFactory : public HttpResponseCreatorFactory { +public: + + /// @brief Creates @ref TestHttpResponseCreator instance. + virtual HttpResponseCreatorPtr create() const { + HttpResponseCreatorPtr response_creator(new TestHttpResponseCreator()); + return (response_creator); + } +}; + +/// @brief Implementation of the HTTP listener used in tests. +/// +/// This implementation replaces the @c HttpConnection type with a custom +/// implementation. +/// +/// @tparam HttpConnectionType Type of the connection object to be used by +/// the listener implementation. +template +class HttpListenerImplCustom : public HttpListenerImpl { +public: + + HttpListenerImplCustom(const IOServicePtr& io_service, + const IOAddress& server_address, + const unsigned short server_port, + const TlsContextPtr& tls_context, + const HttpResponseCreatorFactoryPtr& creator_factory, + const long request_timeout, + const long idle_timeout) + : HttpListenerImpl(io_service, server_address, server_port, + tls_context, creator_factory, request_timeout, + idle_timeout) { + } + +protected: + + /// @brief Creates an instance of the @c HttpConnection. + /// + /// This method is virtual so as it can be overridden when customized + /// connections are to be used, e.g. in case of unit testing. + /// + /// @param response_creator Pointer to the response creator object used to + /// create HTTP response from the HTTP request received. + /// @param callback Callback invoked when new connection is accepted. + /// + /// @return Pointer to the created connection. + virtual HttpConnectionPtr createConnection(const HttpResponseCreatorPtr& response_creator, + const HttpAcceptorCallback& callback) { + HttpConnectionPtr + conn(new HttpConnectionType(io_service_, acceptor_, + tls_context_, connections_, + response_creator, callback, + request_timeout_, idle_timeout_)); + return (conn); + } +}; + +/// @brief Derivation of the @c HttpListener used in tests. +/// +/// This class replaces the default implementation instance with the +/// @c HttpListenerImplCustom using the customized connection type. +/// +/// @tparam HttpConnectionType Type of the connection object to be used by +/// the listener implementation. +template +class HttpListenerCustom : public HttpListener { +public: + + /// @brief Constructor. + /// + /// @param io_service IO service to be used by the listener. + /// @param server_address Address on which the HTTP service should run. + /// @param server_port Port number on which the HTTP service should run. + /// @param tls_context TLS context. + /// @param creator_factory Pointer to the caller-defined + /// @ref HttpResponseCreatorFactory derivation which should be used to + /// 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. + HttpListenerCustom(const IOServicePtr& io_service, + const IOAddress& server_address, + const unsigned short server_port, + const TlsContextPtr& tls_context, + const HttpResponseCreatorFactoryPtr& creator_factory, + const HttpListener::RequestTimeout& request_timeout, + const HttpListener::IdleTimeout& idle_timeout) + : HttpListener(io_service, server_address, server_port, + tls_context, creator_factory, + request_timeout, idle_timeout) { + // Replace the default implementation with the customized version + // using the custom derivation of the HttpConnection. + impl_.reset(new HttpListenerImplCustom + (io_service, server_address, server_port, + tls_context, creator_factory, request_timeout.value_, + idle_timeout.value_)); + } +}; + +/// @brief Implementation of the @c HttpConnection which injects greater +/// length value than the buffer size into the write socket callback. +class HttpConnectionLongWriteBuffer : public HttpConnection { +public: + + /// @brief Constructor. + /// + /// @param io_service IO service to be used by the connection. + /// @param acceptor Pointer to the TCP acceptor object used to listen for + /// new HTTP connections. + /// @param tls_context TLS context. + /// @param connection_pool Connection pool in which this connection is + /// stored. + /// @param response_creator Pointer to the response creator object used to + /// 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. + HttpConnectionLongWriteBuffer(const IOServicePtr& io_service, + const HttpAcceptorPtr& acceptor, + const TlsContextPtr& tls_context, + HttpConnectionPool& connection_pool, + const HttpResponseCreatorPtr& response_creator, + const HttpAcceptorCallback& callback, + const long request_timeout, + const long idle_timeout) + : HttpConnection(io_service, acceptor, tls_context, connection_pool, + response_creator, callback, request_timeout, + idle_timeout) { + } + + /// @brief Callback invoked when data is sent over the socket. + /// + /// @param transaction Pointer to the transaction for which the callback + /// is invoked. + /// @param ec Error code. + /// @param length Length of the data sent. + virtual void socketWriteCallback(HttpConnection::TransactionPtr transaction, + boost::system::error_code ec, + size_t length) { + // Pass greater length of the data written. The callback should deal + // with this and adjust the data length. + HttpConnection::socketWriteCallback(transaction, ec, length + 1); + } +}; + +/// @brief Implementation of the @c HttpConnection which replaces +/// transaction instance prior to calling write socket callback. +class HttpConnectionTransactionChange : public HttpConnection { +public: + + /// @brief Constructor. + /// + /// @param io_service IO service to be used by the connection. + /// @param acceptor Pointer to the TCP acceptor object used to listen for + /// new HTTP connections. + /// @param context TLS tls_context. + /// @param connection_pool Connection pool in which this connection is + /// stored. + /// @param response_creator Pointer to the response creator object used to + /// 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. + HttpConnectionTransactionChange(const IOServicePtr& io_service, + const HttpAcceptorPtr& acceptor, + const TlsContextPtr& tls_context, + HttpConnectionPool& connection_pool, + const HttpResponseCreatorPtr& response_creator, + const HttpAcceptorCallback& callback, + const long request_timeout, + const long idle_timeout) + : HttpConnection(io_service, acceptor, tls_context, connection_pool, + response_creator, callback, request_timeout, + idle_timeout) { + } + + /// @brief Callback invoked when data is sent over the socket. + /// + /// @param transaction Pointer to the transaction for which the callback + /// is invoked. + /// @param ec Error code. + /// @param length Length of the data sent. + virtual void socketWriteCallback(HttpConnection::TransactionPtr transaction, + boost::system::error_code ec, + size_t length) { + // Replace the transaction. The socket callback should deal with this + // gracefully. It should detect that the output buffer is empty. Then + // try to see if the connection is persistent. This check should fail, + // because the request hasn't been created/finalized. The exception + // thrown upon checking the persistence should be caught and the + // connection closed. + transaction = HttpConnection::Transaction::create(response_creator_); + HttpConnection::socketWriteCallback(transaction, ec, length); + } +}; + +/// @brief Pointer to the TestHttpClient. +typedef boost::shared_ptr TestHttpClientPtr; + +/// @brief Test fixture class for @ref HttpListener. +class HttpListenerTest : public ::testing::Test { +public: + + /// @brief Constructor. + /// + /// Starts test timer which detects timeouts. + HttpListenerTest() + : io_service_(new IOService()), factory_(new TestHttpResponseCreatorFactory()), + test_timer_(io_service_), run_io_service_timer_(io_service_), clients_() { + test_timer_.setup(std::bind(&HttpListenerTest::timeoutHandler, this, true), + TEST_TIMEOUT, IntervalTimer::ONE_SHOT); + } + + /// @brief Destructor. + /// + /// Removes active HTTP clients. + virtual ~HttpListenerTest() { + for (auto const& client : clients_) { + client->close(); + } + test_timer_.cancel(); + io_service_->stopAndPoll(); + } + + /// @brief Connect to the endpoint. + /// + /// 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) { + TestHttpClientPtr client(new TestHttpClient(io_service_)); + clients_.push_back(client); + clients_.back()->startRequest(request); + } + + /// @brief Callback function invoke upon test timeout. + /// + /// It stops the IO service and reports test timeout. + /// + /// @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) { + io_service_->stop(); + io_service_->restart(); + + if (timeout > 0) { + run_io_service_timer_.setup(std::bind(&HttpListenerTest::timeoutHandler, + this, false), + timeout, IntervalTimer::ONE_SHOT); + } + io_service_->run(); + io_service_->stopAndPoll(false); + } + + /// @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: 33\r\n" + "Content-Type: application/json\r\n" + "Date: Tue, 19 Dec 2016 18:53:35 GMT\r\n" + "\r\n" + "{ \"remote-address\": \"127.0.0.1\" }"; + return (s.str()); + } + + /// @brief Tests that HTTP request timeout status is returned when the + /// server does not receive the entire request. + /// + /// @param request Partial request for which the parser will be waiting for + /// the next chunks of data. + /// @param expected_version HTTP version expected in the response. + void testRequestTimeout(const std::string& request, + const HttpVersion& expected_version) { + // Open the listener with the Request Timeout of 1 sec and post the + // partial request. + HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), + SERVER_PORT, TlsContextPtr(), + factory_, HttpListener::RequestTimeout(1000), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); + ASSERT_NO_THROW(listener.start()); + ASSERT_NO_THROW(startRequest(request)); + ASSERT_NO_THROW(runIOService()); + ASSERT_EQ(1, clients_.size()); + TestHttpClientPtr client = *clients_.begin(); + ASSERT_TRUE(client); + + // Build the reference response. + std::ostringstream expected_response; + expected_response + << "HTTP/" << expected_version.major_ << "." << expected_version.minor_ + << " 408 Request Timeout\r\n" + "Content-Length: 44\r\n" + "Content-Type: application/json\r\n" + "Date: Tue, 19 Dec 2016 18:53:35 GMT\r\n" + "\r\n" + "{ \"result\": 408, \"text\": \"Request Timeout\" }"; + + // The server should wait for the missing part of the request for 1 second. + // The missing part never arrives so the server should respond with the + // HTTP Request Timeout status. + EXPECT_EQ(expected_response.str(), client->getResponse()); + } + + /// @brief Tests various cases when unexpected data is passed to the + /// socket write handler. + /// + /// This test uses the custom listener and the test specific derivations of + /// the @c HttpConnection class to enforce injection of the unexpected + /// data to the socket write callback. The two example applications of + /// this test are: + /// - injecting greater length value than the output buffer size, + /// - replacing the transaction with another transaction. + /// + /// It is expected that the socket write callback deals gracefully with + /// those situations. + /// + /// @tparam HttpConnectionType Test specific derivation of the + /// @c HttpConnection class. + template + void testWriteBufferIssues() { + // 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" + "{ }"; + + // Use custom listener and the specialized connection object. + HttpListenerCustom + listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, + TlsContextPtr(), factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); + + ASSERT_NO_THROW(listener.start()); + + // Send the request. + ASSERT_NO_THROW(startRequest(request)); + + // Injecting unexpected data should not result in an exception. + ASSERT_NO_THROW(runIOService()); + + ASSERT_EQ(1, clients_.size()); + TestHttpClientPtr client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse()); + } + + /// @brief IO service used in the tests. + IOServicePtr io_service_; + + /// @brief Pointer to the response creator factory. + HttpResponseCreatorFactoryPtr factory_; + + /// @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_; +}; + +// This test verifies that HTTP connection can be established and used to +// transmit HTTP request and receive a response. +TEST_F(HttpListenerTest, listen) { + const 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, + TlsContextPtr(), 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(runIOService()); + ASSERT_EQ(1, clients_.size()); + TestHttpClientPtr client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse()); + + listener.stop(); + io_service_->poll(); +} + + +// This test verifies that persistent HTTP connection can be established when +// "Connection: 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, + TlsContextPtr(), 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()); + TestHttpClientPtr 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_TRUE(client->isConnectionClosed()); + + 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, + TlsContextPtr(), 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()); + TestHttpClientPtr 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_TRUE(client->isConnectionClosed()); + + 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, + TlsContextPtr(), 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()); + TestHttpClientPtr 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_TRUE(client->isConnectionClosed()); + + // 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_TRUE(client->isConnectionClosed()); + + 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\r\n" + "{ }"; + + // Specify the idle timeout of 500ms. + HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, + TlsContextPtr(), 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()); + TestHttpClientPtr 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_TRUE(client->isConnectionClosed()); + + // 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_TRUE(client->isConnectionClosed()); + + listener.stop(); + io_service_->poll(); +} + +// This test verifies that HTTP/1.1 connection remains open even if there is an +// error in the message body. +TEST_F(HttpListenerTest, persistentConnectionBadBody) { + + // 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: 12\r\n\r\n" + "{ \"a\": abc }"; + + HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, + TlsContextPtr(), factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); + + ASSERT_NO_THROW(listener.start()); + + // Send the request. + ASSERT_NO_THROW(startRequest(request)); + ASSERT_NO_THROW(runIOService()); + ASSERT_EQ(1, clients_.size()); + TestHttpClientPtr client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ("HTTP/1.1 400 Bad Request\r\n" + "Content-Length: 40\r\n" + "Content-Type: application/json\r\n" + "Date: Tue, 19 Dec 2016 18:53:35 GMT\r\n" + "\r\n" + "{ \"result\": 400, \"text\": \"Bad Request\" }", + client->getResponse()); + + // The connection should remain active. + ASSERT_TRUE(client->isConnectionAlive()); + + // Make sure that we can send another request. This time we specify the + // "close" connection-token to force the connection to close. + 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()); + + EXPECT_TRUE(client->isConnectionClosed()); + + listener.stop(); + io_service_->poll(); +} + +// 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, + TlsContextPtr(), factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); + ASSERT_NO_THROW(listener.start()); + EXPECT_THROW(listener.start(), HttpListenerError); +} + +// This test verifies that Bad Request status is returned when the request +// is malformed. +TEST_F(HttpListenerTest, badRequest) { + // Content-Type is wrong. This should result in Bad Request status. + const std::string request = "POST /foo/bar HTTP/1.1\r\n" + "Content-Type: foo\r\n" + "Content-Length: 3\r\n\r\n" + "{ }"; + + HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, + TlsContextPtr(), factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); + ASSERT_NO_THROW(listener.start()); + ASSERT_NO_THROW(startRequest(request)); + ASSERT_NO_THROW(runIOService()); + ASSERT_EQ(1, clients_.size()); + TestHttpClientPtr client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ("HTTP/1.1 400 Bad Request\r\n" + "Content-Length: 40\r\n" + "Content-Type: application/json\r\n" + "Date: Tue, 19 Dec 2016 18:53:35 GMT\r\n" + "\r\n" + "{ \"result\": 400, \"text\": \"Bad Request\" }", + client->getResponse()); +} + +// This test verifies that NULL pointer can't be specified for the +// HttpResponseCreatorFactory. +TEST_F(HttpListenerTest, invalidFactory) { + EXPECT_THROW(HttpListener(io_service_, IOAddress(SERVER_ADDRESS), + SERVER_PORT, TlsContextPtr(), + HttpResponseCreatorFactoryPtr(), + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)), + HttpListenerError); +} + +// This test verifies that the timeout of 0 can't be specified for the +// Request Timeout. +TEST_F(HttpListenerTest, invalidRequestTimeout) { + EXPECT_THROW(HttpListener(io_service_, IOAddress(SERVER_ADDRESS), + SERVER_PORT, TlsContextPtr(), 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, TlsContextPtr(), factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(0)), + HttpListenerError); +} + +// This test verifies that listener can't be bound to the port to which +// other server is bound. +TEST_F(HttpListenerTest, addressInUse) { + tcp::acceptor acceptor(io_service_->getInternalIOService()); + // Use other port than SERVER_PORT to make sure that this TCP connection + // doesn't affect subsequent tests. + tcp::endpoint endpoint(address::from_string(SERVER_ADDRESS), + SERVER_PORT + 1); + acceptor.open(endpoint.protocol()); + acceptor.bind(endpoint); + + // 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, TlsContextPtr(), factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); + EXPECT_THROW(listener.start(), HttpListenerError); +} + +// This test verifies that HTTP Request Timeout status is returned as +// expected when the read part of the request contains the HTTP +// version number. The timeout response should contain the same +// HTTP version number as the partial request. +TEST_F(HttpListenerTest, requestTimeoutHttpVersionFound) { + // The part of the request specified here is correct but it is not + // a complete request. + const std::string request = "POST /foo/bar HTTP/1.1\r\n" + "Content-Type: application/json\r\n" + "Content-Length:"; + + testRequestTimeout(request, HttpVersion::HTTP_11()); +} + +// This test verifies that HTTP Request Timeout status is returned as +// expected when the read part of the request does not contain +// the HTTP version number. The timeout response should by default +// contain HTTP/1.0 version number. +TEST_F(HttpListenerTest, requestTimeoutHttpVersionNotFound) { + // The part of the request specified here is correct but it is not + // a complete request. + const std::string request = "POST /foo/bar HTTP"; + + testRequestTimeout(request, HttpVersion::HTTP_10()); +} + +// This test verifies that injecting length value greater than the +// output buffer length to the socket write callback does not cause +// an exception. +TEST_F(HttpListenerTest, tooLongWriteBuffer) { + testWriteBufferIssues(); +} + +// This test verifies that changing the transaction before calling +// the socket write callback does not cause an exception. +TEST_F(HttpListenerTest, transactionChangeDuringWrite) { + testWriteBufferIssues(); +} + +} diff --git a/src/lib/http/tests/http_tests.h b/src/lib/http/tests/http_tests.h new file mode 100644 index 0000000000..08219a96d9 --- /dev/null +++ b/src/lib/http/tests/http_tests.h @@ -0,0 +1,45 @@ +// Copyright (C) 2017-2024 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/. + +namespace isc { +namespace http { + +/// @brief IP address to which HTTP service is bound. +const std::string SERVER_ADDRESS = "127.0.0.1"; + +/// @brief IPv6 address to whch HTTP service is bound. +const std::string IPV6_SERVER_ADDRESS = "::1"; + +/// @brief Port number to which HTTP service is bound. +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 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; + +/// @brief Test HTTP response. +typedef test::TestHttpResponseBase Response; + +/// @brief Pointer to test HTTP response. +typedef boost::shared_ptr ResponsePtr; + +/// @brief Generic test HTTP response. +typedef test::TestHttpResponseBase GenericResponse; + +/// @brief Pointer to generic test HTTP response. +typedef boost::shared_ptr GenericResponsePtr; + +} +} diff --git a/src/lib/http/tests/tls_client_unittests.cc b/src/lib/http/tests/tls_client_unittests.cc index 5135d9256c..66638ace76 100644 --- a/src/lib/http/tests/tls_client_unittests.cc +++ b/src/lib/http/tests/tls_client_unittests.cc @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -51,44 +52,8 @@ using namespace isc::http::test; using namespace isc::util; namespace ph = std::placeholders; -/// @todo: put the common part of client and server tests in its own file(s). - namespace { -/// @brief IP address to which HTTP service is bound. -const std::string SERVER_ADDRESS = "127.0.0.1"; - -/// @brief IPv6 address to whch HTTP service is bound. -const std::string IPV6_SERVER_ADDRESS = "::1"; - -/// @brief Port number to which HTTP service is bound. -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 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; - -/// @brief Test HTTP response. -typedef TestHttpResponseBase Response; - -/// @brief Pointer to test HTTP response. -typedef boost::shared_ptr ResponsePtr; - -/// @brief Generic test HTTP response. -typedef TestHttpResponseBase GenericResponse; - -/// @brief Pointer to generic test HTTP response. -typedef boost::shared_ptr GenericResponsePtr; - /// @brief Implementation of the @ref HttpResponseCreator. class TestHttpResponseCreator : public HttpResponseCreator { public: diff --git a/src/lib/http/tests/tls_server_unittests.cc b/src/lib/http/tests/tls_server_unittests.cc index 10b82641f9..82a77abc36 100644 --- a/src/lib/http/tests/tls_server_unittests.cc +++ b/src/lib/http/tests/tls_server_unittests.cc @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -42,37 +43,8 @@ using namespace isc::http; using namespace isc::http::test; using namespace isc::util; -/// @todo: put the common part of client and server tests in its own file(s). - namespace { -/// @brief IP address to which HTTP service is bound. -const std::string SERVER_ADDRESS = "127.0.0.1"; - -/// @brief Port number to which HTTP service is bound. -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; - -/// @brief Test HTTP response. -typedef TestHttpResponseBase Response; - -/// @brief Pointer to test HTTP response. -typedef boost::shared_ptr ResponsePtr; - -/// @brief Generic test HTTP response. -typedef TestHttpResponseBase GenericResponse; - -/// @brief Pointer to generic test HTTP response. -typedef boost::shared_ptr GenericResponsePtr; - /// @brief Implementation of the @ref HttpResponseCreator. class TestHttpResponseCreator : public HttpResponseCreator { public: