[haiku-commits] haiku: hrev53924 - src/tests/kits/net/service

  • From: waddlesplash <waddlesplash@xxxxxxxxx>
  • To: haiku-commits@xxxxxxxxxxxxx
  • Date: Fri, 28 Feb 2020 19:27:41 -0500 (EST)

hrev53924 adds 1 changeset to branch 'master'
old head: a5544d0a215ae88af0bc8c0bebb1b6112fd2218b
new head: 2ac34dee51d95edbdf045837704466274740378d
overview: 
https://git.haiku-os.org/haiku/log/?qt=range&q=2ac34dee51d9+%5Ea5544d0a215a

----------------------------------------------------------------------------

2ac34dee51d9: tests/net: HTTP proxy client test
  
  With this patch, ProxyTest is implemented and all of the tests in
  HttpTest are enabled.
  
  Adding a transparent proxy server implementation proxy.py. Like
  testserver.py, this can be provided a socket file descriptor and port
  via command-line arguments.
  
  TestServer was refactored to extract ChildProcess and
  RandomTCPServerPort, which are now also used by TestProxyServer.
  
  ProxyTest starts TestProxyServer and validates that the request is
  sent to the proxy and is routed to the appropriate endpoint of the
  downstream server.
  
  The template which adds common tests between HttpTest and HttpsTest
  was changed slightly to just take a BThreadedTestCaller<T>&, which
  made it simpler to add additional test cases to one suite which are
  not appropriate to the other. There wasn't much point in keeping that
  template as a member function so I moved it into HttpTest.cpp as a
  free function template.
  
  Change-Id: Ied32d6e10bb195d111cae7bbcf0e93168118088b
  Reviewed-on: https://review.haiku-os.org/c/haiku/+/2291
  Reviewed-by: Adrien Destugues <pulkomandy@xxxxxxxxx>

                                  [ Kyle Ambroff-Kao <kyle@xxxxxxxxxxxxxx> ]

----------------------------------------------------------------------------

Revision:    hrev53924
Commit:      2ac34dee51d95edbdf045837704466274740378d
URL:         https://git.haiku-os.org/haiku/commit/?id=2ac34dee51d9
Author:      Kyle Ambroff-Kao <kyle@xxxxxxxxxxxxxx>
Date:        Sat Feb 22 09:23:24 2020 UTC
Committer:   waddlesplash <waddlesplash@xxxxxxxxx>
Commit-Date: Sat Feb 29 00:27:36 2020 UTC

----------------------------------------------------------------------------

6 files changed, 531 insertions(+), 181 deletions(-)
src/tests/kits/net/service/HttpTest.cpp   | 109 +++++----
src/tests/kits/net/service/HttpTest.h     |  26 +--
src/tests/kits/net/service/TestServer.cpp | 317 ++++++++++++++++----------
src/tests/kits/net/service/TestServer.h   |  57 ++++-
src/tests/kits/net/service/proxy.py       | 201 ++++++++++++++++
src/tests/kits/net/service/testserver.py  |   2 +-

----------------------------------------------------------------------------

diff --git a/src/tests/kits/net/service/HttpTest.cpp 
b/src/tests/kits/net/service/HttpTest.cpp
index 928fa9538c..1dc17dfd07 100644
--- a/src/tests/kits/net/service/HttpTest.cpp
+++ b/src/tests/kits/net/service/HttpTest.cpp
@@ -159,6 +159,16 @@ std::string TestFilePath(const std::string& relativePath)
        return testSrcDir + "/" + relativePath;
 }
 
+
+template <typename T>
+void AddCommonTests(BThreadedTestCaller<T>& testCaller)
+{
+       testCaller.addThread("GetTest", &T::GetTest);
+       testCaller.addThread("UploadTest", &T::UploadTest);
+       testCaller.addThread("BasicAuthTest", &T::AuthBasicTest);
+       testCaller.addThread("DigestAuthTest", &T::AuthDigestTest);
+}
+
 }
 
 
@@ -180,7 +190,7 @@ HttpTest::setUp()
        CPPUNIT_ASSERT_EQUAL_MESSAGE(
                "Starting up test server",
                B_OK,
-               fTestServer.StartIfNotRunning());
+               fTestServer.Start());
 }
 
 
@@ -239,34 +249,62 @@ HttpTest::GetTest()
 void
 HttpTest::ProxyTest()
 {
-       BUrl testUrl(fTestServer.BaseUrl(), "/user-agent");
+       BUrl testUrl(fTestServer.BaseUrl(), "/");
 
-       BUrlContext* c = new BUrlContext();
-       c->AcquireReference();
-       c->SetProxy("120.203.214.182", 83);
+       TestProxyServer proxy;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "Test proxy server startup",
+               B_OK,
+               proxy.Start());
 
-       BHttpRequest t(testUrl, testUrl.Protocol() == "https");
-       t.SetContext(c);
+       BUrlContext* context = new BUrlContext();
+       context->AcquireReference();
+       context->SetProxy("127.0.0.1", proxy.Port());
 
-       BUrlProtocolListener l;
-       t.SetListener(&l);
+       std::string expectedResponseBody(
+               "Path: /\r\n"
+               "\r\n"
+               "Headers:\r\n"
+               "--------\r\n"
+               "Host: 127.0.0.1:PORT\r\n"
+               "Content-Length: 0\r\n"
+               "Accept: */*\r\n"
+               "Accept-Encoding: gzip\r\n"
+               "Connection: close\r\n"
+               "User-Agent: Services Kit (Haiku)\r\n"
+               "X-Forwarded-For: 127.0.0.1:PORT\r\n");
+       HttpHeaderMap expectedResponseHeaders;
+       expectedResponseHeaders["Content-Encoding"] = "gzip";
+       expectedResponseHeaders["Content-Length"] = "169";
+       expectedResponseHeaders["Content-Type"] = "text/plain";
+       expectedResponseHeaders["Date"] = "Sun, 09 Feb 2020 19:32:42 GMT";
+       expectedResponseHeaders["Server"] = "Test HTTP Server for Haiku";
+
+       TestListener listener(expectedResponseBody, expectedResponseHeaders);
 
-       CPPUNIT_ASSERT(t.Run());
+       BHttpRequest request(testUrl);
+       request.SetContext(context);
+       request.SetListener(&listener);
 
-       while (t.IsRunning())
+       CPPUNIT_ASSERT(request.Run());
+
+       while (request.IsRunning())
                snooze(1000);
 
-       CPPUNIT_ASSERT_EQUAL(B_OK, t.Status());
+       CPPUNIT_ASSERT_EQUAL(B_OK, request.Status());
 
-       const BHttpResult& r = dynamic_cast<const BHttpResult&>(t.Result());
-       CPPUNIT_ASSERT_EQUAL(200, r.StatusCode());
-       CPPUNIT_ASSERT_EQUAL(BString("OK"), r.StatusText());
-       CPPUNIT_ASSERT_EQUAL(42, r.Length());
+       const BHttpResult& response
+               = dynamic_cast<const BHttpResult&>(request.Result());
+       CPPUNIT_ASSERT_EQUAL(200, response.StatusCode());
+       CPPUNIT_ASSERT_EQUAL(BString("OK"), response.StatusText());
+       CPPUNIT_ASSERT_EQUAL(169, response.Length());
                // Fixed size as we know the response format.
-       CPPUNIT_ASSERT(!c->GetCookieJar().GetIterator().HasNext());
+       CPPUNIT_ASSERT(!context->GetCookieJar().GetIterator().HasNext());
                // This page should not set cookies
 
-       c->ReleaseReference();
+       listener.Verify();
+
+       context->ReleaseReference();
 }
 
 
@@ -454,45 +492,36 @@ HttpTest::AuthDigestTest()
 }
 
 
-/* static */ template<class T> void
-HttpTest::_AddCommonTests(BString prefix, CppUnit::TestSuite& suite)
-{
-       T* test = new T();
-       BThreadedTestCaller<T>* testCaller
-               = new BThreadedTestCaller<T>(prefix.String(), test);
-
-       testCaller->addThread("GetTest", &T::GetTest);
-       testCaller->addThread("UploadTest", &T::UploadTest);
-       testCaller->addThread("BasicAuthTest", &T::AuthBasicTest);
-       testCaller->addThread("DigestAuthTest", &T::AuthDigestTest);
-
-       suite.addTest(testCaller);
-}
-
-
 /* static */ void
 HttpTest::AddTests(BTestSuite& parent)
 {
        {
                CppUnit::TestSuite& suite = *new CppUnit::TestSuite("HttpTest");
 
+               HttpTest* httpTest = new HttpTest();
+               BThreadedTestCaller<HttpTest>* httpTestCaller
+                       = new BThreadedTestCaller<HttpTest>("HttpTest::", 
httpTest);
+
                // HTTP + HTTPs
-               _AddCommonTests<HttpTest>("HttpTest::", suite);
+               AddCommonTests<HttpTest>(*httpTestCaller);
 
-               // TODO: reaches out to some mysterious IP 120.203.214.182 
which does
-               // not respond anymore?
-               //suite.addTest(new 
CppUnit::TestCaller<HttpTest>("HttpTest::ProxyTest",
-               //      &HttpTest::ProxyTest));
+               httpTestCaller->addThread("ProxyTest", &HttpTest::ProxyTest);
 
+               suite.addTest(httpTestCaller);
                parent.addTest("HttpTest", &suite);
        }
 
        {
                CppUnit::TestSuite& suite = *new 
CppUnit::TestSuite("HttpsTest");
 
+               HttpsTest* httpsTest = new HttpsTest();
+               BThreadedTestCaller<HttpsTest>* httpsTestCaller
+                       = new BThreadedTestCaller<HttpsTest>("HttpsTest::", 
httpsTest);
+
                // HTTP + HTTPs
-               _AddCommonTests<HttpsTest>("HttpsTest::", suite);
+               AddCommonTests<HttpsTest>(*httpsTestCaller);
 
+               suite.addTest(httpsTestCaller);
                parent.addTest("HttpsTest", &suite);
        }
 }
diff --git a/src/tests/kits/net/service/HttpTest.h 
b/src/tests/kits/net/service/HttpTest.h
index 57a58f6bb4..19c4d99e17 100644
--- a/src/tests/kits/net/service/HttpTest.h
+++ b/src/tests/kits/net/service/HttpTest.h
@@ -19,31 +19,27 @@
 
 class HttpTest: public BThreadedTestCase {
 public:
-                                                                               
        HttpTest(TestServerMode mode
-                                                                               
                = TEST_SERVER_MODE_HTTP);
-       virtual                                                                 
~HttpTest();
+                                               HttpTest(TestServerMode mode = 
TEST_SERVER_MODE_HTTP);
+       virtual                         ~HttpTest();
 
-       virtual                                         void            setUp();
+       virtual void            setUp();
 
-                                                               void            
GetTest();
-                                                               void            
UploadTest();
-                                                               void            
AuthBasicTest();
-                                                               void            
AuthDigestTest();
-                                                               void            
ProxyTest();
+                       void            GetTest();
+                       void            UploadTest();
+                       void            AuthBasicTest();
+                       void            AuthDigestTest();
+                       void            ProxyTest();
 
-       static                                          void            
AddTests(BTestSuite& suite);
+       static  void            AddTests(BTestSuite& suite);
 
 private:
-       template<class T> static        void            _AddCommonTests(BString 
prefix,
-                                                                               
                CppUnit::TestSuite& suite);
-
-                                                               TestServer      
fTestServer;
+                       TestServer      fTestServer;
 };
 
 
 class HttpsTest: public HttpTest {
 public:
-                                                               HttpsTest();
+                                               HttpsTest();
 };
 
 
diff --git a/src/tests/kits/net/service/TestServer.cpp 
b/src/tests/kits/net/service/TestServer.cpp
index f02277715b..240c6972f6 100644
--- a/src/tests/kits/net/service/TestServer.cpp
+++ b/src/tests/kits/net/service/TestServer.cpp
@@ -44,170 +44,221 @@ void exec(const std::vector<std::string>& args)
        execv(args[0].c_str(), const_cast<char* const*>(argv));
 }
 
+
+// Return the path of a file path relative to this source file.
+std::string TestFilePath(const std::string& relativePath)
+{
+       char *testFileSource = strdup(__FILE__);
+       MemoryDeleter _(testFileSource);
+
+       std::string testSrcDir(::dirname(testFileSource));
+
+       return testSrcDir + "/" + relativePath;
+}
+
 }
 
 
-TestServer::TestServer(TestServerMode mode)
+RandomTCPServerPort::RandomTCPServerPort()
        :
-       fMode(mode),
-       fRunning(false),
-       fChildPid(-1),
+       fInitStatus(B_NOT_INITIALIZED),
        fSocketFd(-1),
        fServerPort(0)
 {
-}
+       // Create socket with port 0 to get an unused one selected by the
+       // kernel.
+       int socket_fd = ::socket(AF_INET, SOCK_STREAM, 0);
+       if (socket_fd == -1) {
+               fprintf(
+                       stderr,
+                       "ERROR: Unable to create socket: %s\n",
+                       strerror(errno));
+               fInitStatus = B_ERROR;
+               return;
+       }
 
+       fSocketFd = socket_fd;
 
-TestServer::~TestServer()
-{
-       if (fChildPid != -1) {
-               ::kill(fChildPid, SIGTERM);
-
-               pid_t result = -1;
-               while (result != fChildPid) {
-                       result = ::waitpid(fChildPid, NULL, 0);
+       // We may quickly reclaim the same socket between test runs, so allow
+       // for reuse.
+       {
+               int reuse = 1;
+               int result = ::setsockopt(
+                       socket_fd,
+                       SOL_SOCKET,
+                       SO_REUSEPORT,
+                       &reuse,
+                       sizeof(reuse));
+               if (result == -1) {
+                       fInitStatus = errno;
+                       fprintf(
+                               stderr,
+                               "ERROR: Unable to set socket options on fd %d: 
%s\n",
+                               socket_fd,
+                               strerror(fInitStatus));
+                       return;
                }
        }
 
+       // Bind to loopback 127.0.0.1
+       struct sockaddr_in server_address;
+       server_address.sin_family = AF_INET;
+       server_address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+       int bind_result = ::bind(
+               socket_fd,
+               reinterpret_cast<struct sockaddr*>(&server_address),
+               sizeof(server_address));
+       if (bind_result == -1) {
+               fInitStatus = errno;
+               fprintf(
+                       stderr,
+                       "ERROR: Unable to bind to loopback interface: %s\n",
+                       strerror(fInitStatus));
+               return;
+       }
+
+       // Listen is apparently required before getsockname will work.
+       if (::listen(socket_fd, 32) == -1) {
+               fInitStatus = errno;
+               fprintf(stderr, "ERROR: listen() failed: %s\n", 
strerror(fInitStatus));
+
+               return;
+       }
+
+       // Now get the port from the socket.
+       socklen_t server_address_length = sizeof(server_address);
+       ::getsockname(
+               socket_fd,
+               reinterpret_cast<struct sockaddr*>(&server_address),
+               &server_address_length);
+       fServerPort = ntohs(server_address.sin_port);
+
+       fInitStatus = B_OK;
+}
+
+
+RandomTCPServerPort::~RandomTCPServerPort()
+{
        if (fSocketFd != -1) {
                ::close(fSocketFd);
                fSocketFd = -1;
+               fInitStatus = B_NOT_INITIALIZED;
        }
 }
 
 
-// The job of this method is to spawn a child process that will later be killed
-// by the destructor. The steps are roughly:
-//
-// 1. If the child server process is already running, return early
-// 2. Choose a random TCP port by binding to the loopback interface.
-// 3. Spawn a child Python process to run testserver.py.
-// 4. Return immediately allowing the tests to be performed by the caller of
-//    TestServer::StartIfNotRunning(). We don't have to wait for the child
-//    process to start up because the socket has already been created. The
-//    tests will block until accept() is called in the child.
-status_t TestServer::StartIfNotRunning()
-{
-       if (fRunning == true) {
-               return B_OK;
-       }
+status_t RandomTCPServerPort::InitCheck() const
+{
+       return fInitStatus;
+}
 
-       // Bind to a random unused TCP port.
-       {
-               // Create socket with port 0 to get an unused one selected by 
the
-               // kernel.
-               int socket_fd = ::socket(AF_INET, SOCK_STREAM, 0);
-               if (socket_fd == -1) {
-                       fprintf(
-                               stderr,
-                               "ERROR: Unable to create socket: %s\n",
-                               strerror(errno));
-                       return B_ERROR;
-               }
 
-               fSocketFd = socket_fd;
+int RandomTCPServerPort::FileDescriptor() const
+{
+       return fSocketFd;
+}
+
 
-               // We may quickly reclaim the same socket between test runs, so 
allow
-               // for reuse.
-               {
-                       int reuse = 1;
-                       int result = ::setsockopt(
-                               socket_fd,
-                               SOL_SOCKET,
-                               SO_REUSEPORT,
-                               &reuse,
-                               sizeof(reuse));
-                       if (result == -1) {
-                               fprintf(
-                                       stderr,
-                                       "ERROR: Unable to set socket options on 
fd %d: %s\n",
-                                       socket_fd,
-                                       strerror(errno));
-                               return B_ERROR;
-                       }
-               }
+uint16_t RandomTCPServerPort::Port() const
+{
+       return fServerPort;
+}
 
-               // Bind to loopback 127.0.0.1
-               struct sockaddr_in server_address;
-               server_address.sin_family = AF_INET;
-               server_address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
-               int bind_result = ::bind(
-                       socket_fd,
-                       reinterpret_cast<struct sockaddr*>(&server_address),
-                       sizeof(server_address));
-               if (bind_result == -1) {
-                       fprintf(
-                               stderr,
-                               "ERROR: Unable to bind to loopback interface: 
%s\n",
-                               strerror(errno));
-                       return B_ERROR;
-               }
 
-               // Listen is apparently required before getsockname will work.
-               if (::listen(socket_fd, 32) == -1) {
-                       fprintf(stderr, "ERROR: listen() failed: %s\n", 
strerror(errno));
-                       return B_ERROR;
-               }
+ChildProcess::ChildProcess()
+       :
+       fChildPid(-1)
+{
+}
 
-               // Now get the port from the socket.
-               socklen_t server_address_length = sizeof(server_address);
-               ::getsockname(
-                       socket_fd,
-                       reinterpret_cast<struct sockaddr*>(&server_address),
-                       &server_address_length);
-               fServerPort = ntohs(server_address.sin_port);
+
+ChildProcess::~ChildProcess()
+{
+       if (fChildPid != -1) {
+               ::kill(fChildPid, SIGTERM);
+
+               pid_t result = -1;
+               while (result != fChildPid) {
+                       result = ::waitpid(fChildPid, NULL, 0);
+               }
        }
+}
 
-       fprintf(stderr, "Binding to port %d for test server\n", fServerPort);
+
+// The job of this method is to spawn a child process that will later be killed
+// by the destructor.
+status_t ChildProcess::Start(const std::vector<std::string>& args)
+{
+       if (fChildPid != -1) {
+               return B_ALREADY_RUNNING;
+       }
 
        pid_t child = ::fork();
        if (child < 0)
                return B_ERROR;
 
        if (child > 0) {
-               // The child process has started. It may take a short amount of 
time
-               // before the child process is ready to call accept(), but 
that's OK.
-               //
-               // Since the socket has already been created above, the tests 
will not
-               // get ECONNREFUSED and will block until the child process calls
-               // accept(). So we don't have to busy loop here waiting for a
-               // connection to the child.
-               fRunning = true;
                fChildPid = child;
                return B_OK;
        }
 
-       // This is the child process. We can exec the server process.
-       char* testFileSource = strdup(__FILE__);
-       MemoryDeleter _(testFileSource);
+       // This is the child process. We can exec image provided in args.
+       exec(args);
+
+       // If we reach this point we failed to load the Python image.
+       std::ostringstream ostr;
 
-       std::string testSrcDir(dirname(testFileSource));
-       std::string testServerScript = testSrcDir + "/" + "testserver.py";
+       for (std::vector<std::string>::const_iterator iter = args.cbegin();
+                iter != args.end();
+                ++iter) {
+               ostr << " " << *iter;
+       }
+
+       fprintf(
+               stderr,
+               "Unable to spawn `%s': %s\n",
+               ostr.str().c_str(),
+               strerror(errno));
+       exit(1);
+}
 
-       std::string socket_fd_string = to_string(fSocketFd);
-       std::string server_port_string = to_string(fServerPort);
 
+TestServer::TestServer(TestServerMode mode)
+       :
+       fMode(mode)
+{
+}
+
+
+// Start a child testserver.py process with the random TCP port chosen by
+// fPort.
+status_t TestServer::Start()
+{
+       if (fPort.InitCheck() != B_OK) {
+               return fPort.InitCheck();
+       }
+
+       // This is the child process. We can exec the server process.
        std::vector<std::string> child_process_args;
        child_process_args.push_back("/bin/python3");
-       child_process_args.push_back(testServerScript);
+       child_process_args.push_back(TestFilePath("testserver.py"));
        child_process_args.push_back("--port");
-       child_process_args.push_back(server_port_string);
+       child_process_args.push_back(to_string(fPort.Port()));
        child_process_args.push_back("--fd");
-       child_process_args.push_back(socket_fd_string);
+       child_process_args.push_back(to_string(fPort.FileDescriptor()));
 
        if (fMode == TEST_SERVER_MODE_HTTPS) {
                child_process_args.push_back("--use-tls");
        }
 
-       exec(child_process_args);
-
-       // If we reach this point we failed to load the Python image.
-       fprintf(
-               stderr,
-               "Unable to spawn %s: %s\n",
-               testServerScript.c_str(),
-               strerror(errno));
-       exit(1);
+       // After this the child process has started. It may take a short amount 
of
+       // time before the child process is ready to call accept(), but that's 
OK.
+       //
+       // Since the socket has already been created above, the tests will not
+       // get ECONNREFUSED and will block until the child process calls
+       // accept(). So we don't have to busy loop here waiting for a
+       // connection to the child.
+       return fChildProcess.Start(child_process_args);
 }
 
 
@@ -224,6 +275,40 @@ BUrl TestServer::BaseUrl() const
                break;
        }
 
-       std::string baseUrl = scheme + "127.0.0.1:" + to_string(fServerPort) + 
"/";
+       std::string port_string = to_string(fPort.Port());
+
+       std::string baseUrl = scheme + "127.0.0.1:" + port_string + "/";
        return BUrl(baseUrl.c_str());
 }
+
+
+// Start a child proxy.py process using the random TCP port chosen by fPort.
+status_t TestProxyServer::Start()
+{
+       if (fPort.InitCheck() != B_OK) {
+               return fPort.InitCheck();
+       }
+
+       std::vector<std::string> child_process_args;
+       child_process_args.push_back("/bin/python3");
+       child_process_args.push_back(TestFilePath("proxy.py"));
+       child_process_args.push_back("--port");
+       child_process_args.push_back(to_string(fPort.Port()));
+       child_process_args.push_back("--fd");
+       child_process_args.push_back(to_string(fPort.FileDescriptor()));
+
+       // After this the child process has started. It may take a short amount 
of
+       // time before the child process is ready to call accept(), but that's 
OK.
+       //
+       // Since the socket has already been created above, the tests will not
+       // get ECONNREFUSED and will block until the child process calls
+       // accept(). So we don't have to busy loop here waiting for a
+       // connection to the child.
+       return fChildProcess.Start(child_process_args);
+}
+
+
+uint16_t TestProxyServer::Port() const
+{
+       return fPort.Port();
+}
diff --git a/src/tests/kits/net/service/TestServer.h 
b/src/tests/kits/net/service/TestServer.h
index 54be0472e9..a7b0ac6a7b 100644
--- a/src/tests/kits/net/service/TestServer.h
+++ b/src/tests/kits/net/service/TestServer.h
@@ -8,10 +8,41 @@
 #ifndef TEST_SERVER_H
 #define TEST_SERVER_H
 
+#include <string>
+#include <vector>
+
 #include <os/support/SupportDefs.h>
 #include <os/support/Url.h>
 
 
+// Binds to a random unused TCP port.
+class RandomTCPServerPort {
+public:
+                                               RandomTCPServerPort();
+                                               ~RandomTCPServerPort();
+
+       status_t                        InitCheck()                             
                        const;
+       int                                     FileDescriptor()                
                        const;
+       uint16_t                        Port()                                  
                        const;
+
+private:
+       status_t                        fInitStatus;
+       int                                     fSocketFd;
+       uint16_t                        fServerPort;
+};
+
+
+class ChildProcess {
+public:
+                                               ChildProcess();
+                                               ~ChildProcess();
+
+       status_t                        Start(const std::vector<std::string>& 
args);
+private:
+       pid_t                           fChildPid;
+};
+
+
 enum TestServerMode {
        TEST_SERVER_MODE_HTTP,
        TEST_SERVER_MODE_HTTPS,
@@ -20,18 +51,26 @@ enum TestServerMode {
 
 class TestServer {
 public:
-       TestServer(TestServerMode mode);
-       ~TestServer();
+                                               TestServer(TestServerMode mode);
+
+       status_t                        Start();
+       BUrl                            BaseUrl()                               
                        const;
 
-       status_t        StartIfNotRunning();
-       BUrl            BaseUrl()       const;
+private:
+       TestServerMode          fMode;
+       ChildProcess            fChildProcess;
+       RandomTCPServerPort fPort;
+};
+
+
+class TestProxyServer {
+public:
+       status_t                        Start();
+       uint16_t                        Port()                                  
                        const;
 
 private:
-       TestServerMode  fMode;
-       bool                    fRunning;
-       pid_t                   fChildPid;
-       int                             fSocketFd;
-       uint16_t                fServerPort;
+       ChildProcess            fChildProcess;
+       RandomTCPServerPort     fPort;
 };
 
 
diff --git a/src/tests/kits/net/service/proxy.py 
b/src/tests/kits/net/service/proxy.py
new file mode 100644
index 0000000000..5f1ddf0aec
--- /dev/null
+++ b/src/tests/kits/net/service/proxy.py
@@ -0,0 +1,201 @@
+#
+# Copyright 2020 Haiku, Inc. All rights reserved.
+# Distributed under the terms of the MIT License.
+#
+# Authors:
+#  Kyle Ambroff-Kao, kyle@xxxxxxxxxxxxxx
+#
+
+"""
+Transparent HTTP proxy.
+"""
+
+import http.client
+import http.server
+import optparse
+import socket
+import sys
+import urllib.parse
+
+
+class RequestHandler(http.server.BaseHTTPRequestHandler):
+    """
+    Implement the basic requirements for a transparent HTTP proxy as defined
+    by RFC 7230. Enough of the functionality is implemented to support the
+    integration tests in HttpTest that use the HTTP proxy feature.
+
+    There are many error conditions and failure modes which are not handled.
+    Those cases can be added as the test suite expands to handle more error
+    cases.
+    """
+    def __init__(self, *args, **kwargs):
+        # This is used to hold on to persistent connections to the downstream
+        # servers. This maps downstream_host:port => HTTPConnection
+        #
+        # This implementation is not thread safe, but that's OK we only have
+        # a single thread anyway.
+        self._connections = {}
+
+        super(RequestHandler, self).__init__(*args, **kwargs)
+
+    def _proxy_request(self):
+        # Extract the downstream server from the request path.
+        #
+        # Note that no attempt is made to prevent message forwarding loops
+        # here. This doesn't need to be a complete proxy implementation, just
+        # enough of one for integration tests. RFC 7230 section 5.7 says if
+        # this were a complete implementation, it would have to make sure that
+        # the target system was not this process to avoid a loop.
+        target = urllib.parse.urlparse(self.path)
+
+        # If Connection: close wasn't used, then we may still have a connection
+        # to this downstream server handy.
+        conn = self._connections.get(target.netloc, None)
+        if conn is None:
+            conn = http.client.HTTPConnection(target.netloc)
+
+        # Collect headers from client which will be sent to the downstream
+        # server.
+        client_headers = {}
+        for header_name in self.headers:
+            if header_name in ('Host', 'Content-Length'):
+                continue
+            for header_value in self.headers.get_all(header_name):
+                client_headers[header_name] = header_value
+
+        # Compute X-Forwarded-For header
+        client_address = '{}:{}'.format(*self.client_address)
+        x_forwarded_for_header = self.headers.get('X-Forwarded-For', None)
+        if x_forwarded_for_header is None:
+            client_headers['X-Forwarded-For'] = client_address
+        else:
+            client_headers['X-Forwarded-For'] = \
+                x_forwarded_for_header + ', ' + client_address
+
+        # Read the request body from client.
+        request_body_length = int(self.headers.get('Content-Length', '0'))
+        request_body = self.rfile.read(request_body_length)
+
+        # Send the request to the downstream server
+        if target.query:
+            target_path = target.path + '?' + target.query
+        else:
+            target_path = target.path
+        conn.request(self.command, target_path, request_body, client_headers)
+        response = conn.getresponse()
+
+        # Echo the response to the client.
+        self.send_response_only(response.status, response.reason)
+        for header_name, header_value in response.headers.items():
+            self.send_header(header_name, header_value)
+        self.end_headers()
+
+        # Read the response body from upstream and write it to downstream, if
+        # there is a response body at all.
+        response_content_length = \
+            int(response.headers.get('Content-Length', '0'))
+        if response_content_length > 0:
+            self.wfile.write(response.read(response_content_length))
+
+        # Cleanup, possibly hang on to persistent connection to target
+        # server.
+        connection_header_value = self.headers.get('Connection', None)
+        if response.will_close or connection_header_value == 'close':
+            conn.close()
+            self.close_connection = True
+        else:
+            # Hang on to this connection for future requests. This isn't
+            # really bulletproof but it's good enough for integration tests.
+            self._connections[target.netloc] = conn
+
+        self.log_message(
+            'Proxied request from %s to %s',
+            client_address,
+            self.path)
+
+    def do_GET(self):
+        self._proxy_request()
+
+    def do_HEAD(self):
+        self._proxy_request()
+
+    def do_POST(self):
+        self._proxy_request()
+
+    def do_PUT(self):
+        self._proxy_request()
+
+    def do_DELETE(self):
+        self._proxy_request()
+
+    def do_PATCH(self):
+        self._proxy_request()
+
+    def do_OPTIONS(self):
+        self._proxy_request()
+
+
+def main():
+    options = parse_args(sys.argv)
+
+    bind_addr = (
+        options.bind_addr,
+        0 if options.port is None else options.port)
+
+    server = http.server.HTTPServer(
+        bind_addr,
+        RequestHandler,
+        bind_and_activate=False)
+    if options.port is None:
+        server.server_port = server.socket.getsockname()[1]
+    else:
+        server.server_port = options.port
+
+    if options.server_socket_fd:
+        server.socket = socket.fromfd(
+            options.server_socket_fd,
+            socket.AF_INET,
+            socket.SOCK_STREAM)
+    else:
+        server.server_bind()
+        server.server_activate()
+
+    print(
+        'Transparent HTTP proxy listening on port',
+        server.server_port,
+        file=sys.stderr)
+    try:
+        server.serve_forever(0.01)
+    except KeyboardInterrupt:
+        server.server_close()
+
+
+def parse_args(argv):
+    parser = optparse.OptionParser(
+        usage='Usage: %prog [OPTIONS]',
+        description=__doc__)
+    parser.add_option(
+        '--bind-addr',
+        default='127.0.0.1',
+        dest='bind_addr',
+        help='By default only bind to loopback')
+    parser.add_option(
+        '--port',
+        dest='port',
+        default=None,
+        type='int',
+        help='If not specified a random port will be used.')
+    parser.add_option(
+        "--fd",
+        dest='server_socket_fd',
+        default=None,
+        type='int',
+        help='A socket FD to use for accept() instead of binding a new one.')
+    options, args = parser.parse_args(argv)
+    if len(args) > 1:
+        parser.error('Unexpected arguments: {}'.format(', '.join(args[1:])))
+    return options
+
+
+if __name__ == '__main__':
+    main()
diff --git a/src/tests/kits/net/service/testserver.py 
b/src/tests/kits/net/service/testserver.py
index f7a9cb0735..2d5ea8750f 100644
--- a/src/tests/kits/net/service/testserver.py
+++ b/src/tests/kits/net/service/testserver.py
@@ -144,7 +144,7 @@ class RequestHandler(http.server.BaseHTTPRequestHandler):
         output_stream.write(b'--------\r\n')
         for header in self.headers:
             for header_value in self.headers.get_all(header):
-                if header == 'Host' or header == 'Referer':
+                if header in ('Host', 'Referer', 'X-Forwarded-For'):
                     # The server port can change between runs which will change
                     # the size and contents of the response body. To make tests
                     # that verify the contents of the response body easier the


Other related posts:

  • » [haiku-commits] haiku: hrev53924 - src/tests/kits/net/service - waddlesplash