root/src/tests/kits/net/service/HttpTest.cpp
/*
 * Copyright 2010, Christophe Huriaux
 * Copyright 2014-2020, Haiku, inc.
 * Distributed under the terms of the MIT licence
 */


#include "HttpTest.h"

#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <map>
#include <posix/libgen.h>
#include <string>

#include <AutoDeleter.h>
#include <HttpRequest.h>
#include <NetworkKit.h>
#include <UrlProtocolListener.h>
#include <UrlProtocolRoster.h>

#include <tools/cppunit/ThreadedTestCaller.h>

#include "TestServer.h"


using namespace BPrivate::Network;


namespace {

typedef std::map<std::string, std::string> HttpHeaderMap;


class TestListener : public BUrlProtocolListener, public BDataIO {
public:
        TestListener(const std::string& expectedResponseBody,
                                 const HttpHeaderMap& expectedResponseHeaders)
                :
                fExpectedResponseBody(expectedResponseBody),
                fExpectedResponseHeaders(expectedResponseHeaders)
        {
        }

        virtual ssize_t Write(
                const void *data,
                size_t size)
        {
                std::copy_n(
                        (const char*)data,
                        size,
                        std::back_inserter(fActualResponseBody));
                return size;
        }

        virtual void HeadersReceived(
                BUrlRequest* caller)
        {
                const BHttpResult& http_result
                        = dynamic_cast<const BHttpResult&>(caller->Result());
                const BHttpHeaders& headers = http_result.Headers();

                for (int32 i = 0; i < headers.CountHeaders(); ++i) {
                        const BHttpHeader& header = headers.HeaderAt(i);
                        fActualResponseHeaders[std::string(header.Name())]
                                = std::string(header.Value());
                }
        }


        virtual bool CertificateVerificationFailed(
                BUrlRequest* caller,
                BCertificate& certificate,
                const char* message)
        {
                // TODO: Add tests that exercize this behavior.
                //
                // At the moment there doesn't seem to be any public API for providing
                // an alternate certificate authority, or for constructing a
                // BCertificate to be sent to BUrlContext::AddCertificateException().
                // Once we have such a public API then it will be useful to create
                // test scenarios that exercize the validation performed by the
                // undrelying TLS implementaiton to verify that it is configured
                // to do so.
                //
                // For now we just disable TLS certificate validation entirely because
                // we are generating a self-signed TLS certificate for these tests.
                return true;
        }


        void Verify()
        {
                CPPUNIT_ASSERT_EQUAL(fExpectedResponseBody, fActualResponseBody);

                for (HttpHeaderMap::iterator iter = fActualResponseHeaders.begin();
                         iter != fActualResponseHeaders.end();
                         ++iter)
                {
                        CPPUNIT_ASSERT_EQUAL_MESSAGE(
                                "(header " + iter->first + ")",
                                fExpectedResponseHeaders[iter->first],
                                iter->second);
                }
                CPPUNIT_ASSERT_EQUAL(
                        fExpectedResponseHeaders.size(),
                        fActualResponseHeaders.size());
        }

private:
        std::string fExpectedResponseBody;
        std::string fActualResponseBody;

        HttpHeaderMap fExpectedResponseHeaders;
        HttpHeaderMap fActualResponseHeaders;
};


void SendAuthenticatedRequest(
        BUrlContext &context,
        BUrl &testUrl,
        const std::string& expectedResponseBody,
        const HttpHeaderMap &expectedResponseHeaders)
{
        TestListener listener(expectedResponseBody, expectedResponseHeaders);

        ObjectDeleter<BUrlRequest> requestDeleter(
                BUrlProtocolRoster::MakeRequest(testUrl, &listener, &listener,
                        &context));
        BHttpRequest* request = dynamic_cast<BHttpRequest*>(requestDeleter.Get());
        CPPUNIT_ASSERT(request != NULL);

        request->SetUserName("walter");
        request->SetPassword("secret");

        CPPUNIT_ASSERT(request->Run());

        while (request->IsRunning())
                snooze(1000);

        CPPUNIT_ASSERT_EQUAL(B_OK, request->Status());

        const BHttpResult &result =
                dynamic_cast<const BHttpResult &>(request->Result());
        CPPUNIT_ASSERT_EQUAL(200, result.StatusCode());
        CPPUNIT_ASSERT_EQUAL(BString("OK"), result.StatusText());

        listener.Verify();
}


// Return the path of a file path relative to this source file.
std::string TestFilePath(const std::string& relativePath)
{
        return "resources/kits/service/" + relativePath;
}


template <typename T>
void AddCommonTests(BThreadedTestCaller<T>& testCaller)
{
        testCaller.addThread("GetTest", &T::GetTest);
        testCaller.addThread("HeadTest", &T::HeadTest);
        testCaller.addThread("NoContentTest", &T::NoContentTest);
        testCaller.addThread("UploadTest", &T::UploadTest);
        testCaller.addThread("BasicAuthTest", &T::AuthBasicTest);
        testCaller.addThread("DigestAuthTest", &T::AuthDigestTest);
        testCaller.addThread("AutoRedirectTest", &T::AutoRedirectTest);
}

}


HttpTest::HttpTest(TestServerMode mode)
        :
        fTestServer(mode)
{
}


HttpTest::~HttpTest()
{
}


void
HttpTest::setUp()
{
        CPPUNIT_ASSERT_EQUAL_MESSAGE(
                "Starting up test server",
                B_OK,
                fTestServer.Start());
}


void
HttpTest::GetTest()
{
        _GetTest("/");
}


void
HttpTest::HeadTest()
{
        BUrl testUrl(fTestServer.BaseUrl(), BString("/"));
        BUrlContext* context = new BUrlContext();
        context->AcquireReference();

        std::string expectedResponseBody("");
        HttpHeaderMap expectedResponseHeaders;
        expectedResponseHeaders["Content-Encoding"] = "gzip";
        expectedResponseHeaders["Content-Length"] = "144";
        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);

        ObjectDeleter<BUrlRequest> requestDeleter(
                BUrlProtocolRoster::MakeRequest(testUrl, &listener, &listener,
                        context));
        BHttpRequest* request = dynamic_cast<BHttpRequest*>(requestDeleter.Get());
        CPPUNIT_ASSERT(request != NULL);

        request->SetAutoReferrer(false);
        request->SetMethod("HEAD");

        CPPUNIT_ASSERT(request->Run());
        while (request->IsRunning())
                snooze(1000);

        CPPUNIT_ASSERT_EQUAL(B_OK, request->Status());

        const BHttpResult& result
                = dynamic_cast<const BHttpResult&>(request->Result());
        CPPUNIT_ASSERT_EQUAL(200, result.StatusCode());
        CPPUNIT_ASSERT_EQUAL(BString("OK"), result.StatusText());

        CPPUNIT_ASSERT_EQUAL(144, result.Length());

        listener.Verify();

        CPPUNIT_ASSERT(!context->GetCookieJar().GetIterator().HasNext());
                // This page should not set cookies

        context->ReleaseReference();
}


void
HttpTest::NoContentTest()
{
        BUrl testUrl(fTestServer.BaseUrl(), BString("/204"));
        BUrlContext* context = new BUrlContext();
        context->AcquireReference();

        std::string expectedResponseBody("");
        HttpHeaderMap expectedResponseHeaders;
        expectedResponseHeaders["Date"] = "Sun, 09 Feb 2020 19:32:42 GMT";
        expectedResponseHeaders["Server"] = "Test HTTP Server for Haiku";

        TestListener listener(expectedResponseBody, expectedResponseHeaders);

        ObjectDeleter<BUrlRequest> requestDeleter(
                BUrlProtocolRoster::MakeRequest(testUrl, &listener, &listener,
                        context));
        BHttpRequest* request = dynamic_cast<BHttpRequest*>(requestDeleter.Get());
        CPPUNIT_ASSERT(request != NULL);

        request->SetAutoReferrer(false);

        CPPUNIT_ASSERT(request->Run());
        while (request->IsRunning())
                snooze(1000);

        CPPUNIT_ASSERT_EQUAL(B_OK, request->Status());

        const BHttpResult& result
                = dynamic_cast<const BHttpResult&>(request->Result());
        CPPUNIT_ASSERT_EQUAL(204, result.StatusCode());
        CPPUNIT_ASSERT_EQUAL(BString("No Content"), result.StatusText());

        listener.Verify();

        CPPUNIT_ASSERT(!context->GetCookieJar().GetIterator().HasNext());
                // This page should not set cookies

        context->ReleaseReference();
}


void
HttpTest::ProxyTest()
{
        BUrl testUrl(fTestServer.BaseUrl(), BString("/"));

        TestProxyServer proxy;
        CPPUNIT_ASSERT_EQUAL_MESSAGE(
                "Test proxy server startup",
                B_OK,
                proxy.Start());

        BUrlContext* context = new BUrlContext();
        context->AcquireReference();
        context->SetProxy("127.0.0.1", proxy.Port());

        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);

        ObjectDeleter<BUrlRequest> requestDeleter(
                BUrlProtocolRoster::MakeRequest(testUrl, &listener, &listener,
                        context));
        BHttpRequest* request = dynamic_cast<BHttpRequest*>(requestDeleter.Get());
        CPPUNIT_ASSERT(request != NULL);

        CPPUNIT_ASSERT(request->Run());

        while (request->IsRunning())
                snooze(1000);

        CPPUNIT_ASSERT_EQUAL(B_OK, request->Status());

        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(!context->GetCookieJar().GetIterator().HasNext());
                // This page should not set cookies

        listener.Verify();

        context->ReleaseReference();
}


void
HttpTest::UploadTest()
{
        std::string testFilePath = TestFilePath("testfile.txt");

        // The test server will echo the POST body back to us in the HTTP response,
        // so here we load it into memory so that we can compare to make sure that
        // the server received it.
        std::string fileContents;
        {
                std::ifstream inputStream(
                        testFilePath.c_str(),
                        std::ios::in | std::ios::binary);
                CPPUNIT_ASSERT(inputStream);

                inputStream.seekg(0, std::ios::end);
                fileContents.resize(inputStream.tellg());

                inputStream.seekg(0, std::ios::beg);
                inputStream.read(&fileContents[0], fileContents.size());
                inputStream.close();

                CPPUNIT_ASSERT(!fileContents.empty());
        }

        std::string expectedResponseBody(
                "Path: /post\r\n"
                "\r\n"
                "Headers:\r\n"
                "--------\r\n"
                "Host: 127.0.0.1:PORT\r\n"
                "Accept: */*\r\n"
                "Accept-Encoding: gzip\r\n"
                "Connection: close\r\n"
                "User-Agent: Services Kit (Haiku)\r\n"
                "Content-Type: multipart/form-data; boundary=<<BOUNDARY-ID>>\r\n"
                "Content-Length: 1404\r\n"
                "\r\n"
                "Request body:\r\n"
                "-------------\r\n"
                "--<<BOUNDARY-ID>>\r\n"
                "Content-Disposition: form-data; name=\"_uploadfile\";"
                " filename=\"testfile.txt\"\r\n"
                "Content-Type: application/octet-stream\r\n"
                "\r\n"
                + fileContents
                + "\r\n"
                "--<<BOUNDARY-ID>>\r\n"
                "Content-Disposition: form-data; name=\"hello\"\r\n"
                "\r\n"
                "world\r\n"
                "--<<BOUNDARY-ID>>--\r\n"
                "\r\n");
        HttpHeaderMap expectedResponseHeaders;
        expectedResponseHeaders["Content-Encoding"] = "gzip";
        expectedResponseHeaders["Content-Length"] = "913";
        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);

        BUrl testUrl(fTestServer.BaseUrl(), BString("/post"));

        BUrlContext context;

        ObjectDeleter<BUrlRequest> requestDeleter(
                BUrlProtocolRoster::MakeRequest(testUrl, &listener, &listener,
                        &context));
        BHttpRequest* request = dynamic_cast<BHttpRequest*>(requestDeleter.Get());
        CPPUNIT_ASSERT(request != NULL);

        BHttpForm form;
        form.AddString("hello", "world");
        CPPUNIT_ASSERT_EQUAL(
                B_OK,
                form.AddFile("_uploadfile", BPath(testFilePath.c_str())));

        request->SetPostFields(form);

        CPPUNIT_ASSERT(request->Run());

        while (request->IsRunning())
                snooze(1000);

        CPPUNIT_ASSERT_EQUAL(B_OK, request->Status());

        const BHttpResult &result =
                dynamic_cast<const BHttpResult &>(request->Result());
        CPPUNIT_ASSERT_EQUAL(200, result.StatusCode());
        CPPUNIT_ASSERT_EQUAL(BString("OK"), result.StatusText());
        CPPUNIT_ASSERT_EQUAL(913, result.Length());

        listener.Verify();
}


void
HttpTest::AuthBasicTest()
{
        BUrlContext context;

        BUrl testUrl(fTestServer.BaseUrl(), BString("/auth/basic/walter/secret"));

        std::string expectedResponseBody(
                "Path: /auth/basic/walter/secret\r\n"
                "\r\n"
                "Headers:\r\n"
                "--------\r\n"
                "Host: 127.0.0.1:PORT\r\n"
                "Accept: */*\r\n"
                "Accept-Encoding: gzip\r\n"
                "Connection: close\r\n"
                "User-Agent: Services Kit (Haiku)\r\n"
                "Referer: SCHEME://127.0.0.1:PORT/auth/basic/walter/secret\r\n"
                "Authorization: Basic d2FsdGVyOnNlY3JldA==\r\n");

        HttpHeaderMap expectedResponseHeaders;
        expectedResponseHeaders["Content-Encoding"] = "gzip";
        expectedResponseHeaders["Content-Length"] = "212";
        expectedResponseHeaders["Content-Type"] = "text/plain";
        expectedResponseHeaders["Date"] = "Sun, 09 Feb 2020 19:32:42 GMT";
        expectedResponseHeaders["Server"] = "Test HTTP Server for Haiku";
        expectedResponseHeaders["Www-Authenticate"] = "Basic realm=\"Fake Realm\"";

        SendAuthenticatedRequest(context, testUrl, expectedResponseBody,
                expectedResponseHeaders);

        CPPUNIT_ASSERT(!context.GetCookieJar().GetIterator().HasNext());
                // This page should not set cookies
}


void
HttpTest::AuthDigestTest()
{
        BUrlContext context;

        BUrl testUrl(fTestServer.BaseUrl(), BString("/auth/digest/walter/secret"));

        std::string expectedResponseBody(
                "Path: /auth/digest/walter/secret\r\n"
                "\r\n"
                "Headers:\r\n"
                "--------\r\n"
                "Host: 127.0.0.1:PORT\r\n"
                "Accept: */*\r\n"
                "Accept-Encoding: gzip\r\n"
                "Connection: close\r\n"
                "User-Agent: Services Kit (Haiku)\r\n"
                "Referer: SCHEME://127.0.0.1:PORT/auth/digest/walter/secret\r\n"
                "Authorization: Digest username=\"walter\","
                " realm=\"user@shredder\","
                " nonce=\"f3a95f20879dd891a5544bf96a3e5518\","
                " algorithm=MD5,"
                " opaque=\"f0bb55f1221a51b6d38117c331611799\","
                " uri=\"/auth/digest/walter/secret\","
                " qop=auth,"
                " cnonce=\"60a3d95d286a732374f0f35fb6d21e79\","
                " nc=00000001,"
                " response=\"f4264de468aa1a91d81ac40fa73445f3\"\r\n"
                "Cookie: stale_after=never; fake=fake_value\r\n");

        HttpHeaderMap expectedResponseHeaders;
        expectedResponseHeaders["Content-Encoding"] = "gzip";
        expectedResponseHeaders["Content-Length"] = "403";
        expectedResponseHeaders["Content-Type"] = "text/plain";
        expectedResponseHeaders["Date"] = "Sun, 09 Feb 2020 19:32:42 GMT";
        expectedResponseHeaders["Server"] = "Test HTTP Server for Haiku";
        expectedResponseHeaders["Set-Cookie"] = "fake=fake_value; Path=/";
        expectedResponseHeaders["Www-Authenticate"]
                = "Digest realm=\"user@shredder\", "
                "nonce=\"f3a95f20879dd891a5544bf96a3e5518\", "
                "qop=\"auth\", "
                "opaque=f0bb55f1221a51b6d38117c331611799, "
                "algorithm=MD5, "
                "stale=FALSE";

        SendAuthenticatedRequest(context, testUrl, expectedResponseBody,
                expectedResponseHeaders);

        std::map<BString, BString> cookies;
        BNetworkCookieJar::Iterator iter
                = context.GetCookieJar().GetIterator();
        while (iter.HasNext()) {
                const BNetworkCookie* cookie = iter.Next();
                cookies[cookie->Name()] = cookie->Value();
        }
        CPPUNIT_ASSERT_EQUAL(2, cookies.size());
        CPPUNIT_ASSERT_EQUAL(BString("fake_value"), cookies["fake"]);
        CPPUNIT_ASSERT_EQUAL(BString("never"), cookies["stale_after"]);
}


void
HttpTest::AutoRedirectTest()
{
        _GetTest("/302");
}


/* 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>(*httpTestCaller);

                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>(*httpsTestCaller);

                suite.addTest(httpsTestCaller);
                parent.addTest("HttpsTest", &suite);
        }
}


void
HttpTest::_GetTest(const BString& path)
{
        BUrl testUrl(fTestServer.BaseUrl(), path);
        BUrlContext* context = new BUrlContext();
        context->AcquireReference();

        std::string expectedResponseBody(
                "Path: /\r\n"
                "\r\n"
                "Headers:\r\n"
                "--------\r\n"
                "Host: 127.0.0.1:PORT\r\n"
                "Accept: */*\r\n"
                "Accept-Encoding: gzip\r\n"
                "Connection: close\r\n"
                "User-Agent: Services Kit (Haiku)\r\n");
        HttpHeaderMap expectedResponseHeaders;
        expectedResponseHeaders["Content-Encoding"] = "gzip";
        expectedResponseHeaders["Content-Length"] = "144";
        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);

        ObjectDeleter<BUrlRequest> requestDeleter(
                BUrlProtocolRoster::MakeRequest(testUrl, &listener, &listener,
                        context));
        BHttpRequest* request = dynamic_cast<BHttpRequest*>(requestDeleter.Get());
        CPPUNIT_ASSERT(request != NULL);

        request->SetAutoReferrer(false);

        CPPUNIT_ASSERT(request->Run());
        while (request->IsRunning())
                snooze(1000);

        CPPUNIT_ASSERT_EQUAL(B_OK, request->Status());

        const BHttpResult& result
                = dynamic_cast<const BHttpResult&>(request->Result());
        CPPUNIT_ASSERT_EQUAL(200, result.StatusCode());
        CPPUNIT_ASSERT_EQUAL(BString("OK"), result.StatusText());

        CPPUNIT_ASSERT_EQUAL(144, result.Length());

        listener.Verify();

        CPPUNIT_ASSERT(!context->GetCookieJar().GetIterator().HasNext());
                // This page should not set cookies

        context->ReleaseReference();
}


// # pragma mark - HTTPS


HttpsTest::HttpsTest()
        :
        HttpTest(TEST_SERVER_MODE_HTTPS)
{
}