/* * Copyright 2010, Christophe Huriaux * Copyright 2014-2020, Haiku, inc. * Distributed under the terms of the MIT licence */ #include "HttpTest.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "TestServer.h" using namespace BPrivate::Network; namespace { typedef std::map 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(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 requestDeleter( BUrlProtocolRoster::MakeRequest(testUrl, &listener, &listener, &context)); BHttpRequest* request = dynamic_cast(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(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) { char *testFileSource = strdup(__FILE__); MemoryDeleter _(testFileSource); std::string testSrcDir(::dirname(testFileSource)); return testSrcDir + "/" + relativePath; } template void AddCommonTests(BThreadedTestCaller& 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(), "/"); 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 requestDeleter( BUrlProtocolRoster::MakeRequest(testUrl, &listener, &listener, context)); BHttpRequest* request = dynamic_cast(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(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(), "/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 requestDeleter( BUrlProtocolRoster::MakeRequest(testUrl, &listener, &listener, context)); BHttpRequest* request = dynamic_cast(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(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(), "/"); 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 requestDeleter( BUrlProtocolRoster::MakeRequest(testUrl, &listener, &listener, context)); BHttpRequest* request = dynamic_cast(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(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=<>\r\n" "Content-Length: 1404\r\n" "\r\n" "Request body:\r\n" "-------------\r\n" "--<>\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" "--<>\r\n" "Content-Disposition: form-data; name=\"hello\"\r\n" "\r\n" "world\r\n" "--<>--\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(), "/post"); BUrlContext context; ObjectDeleter requestDeleter( BUrlProtocolRoster::MakeRequest(testUrl, &listener, &listener, &context)); BHttpRequest* request = dynamic_cast(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(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(), "/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(), "/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 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* httpTestCaller = new BThreadedTestCaller("HttpTest::", httpTest); // HTTP + HTTPs AddCommonTests(*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* httpsTestCaller = new BThreadedTestCaller("HttpsTest::", httpsTest); // HTTP + HTTPs AddCommonTests(*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 requestDeleter( BUrlProtocolRoster::MakeRequest(testUrl, &listener, &listener, context)); BHttpRequest* request = dynamic_cast(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(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) { }