⛏️ index : haiku.git

/*
 * 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)
{
	char *testFileSource = strdup(__FILE__);
	MemoryDeleter _(testFileSource);

	std::string testSrcDir(::dirname(testFileSource));

	return testSrcDir + "/" + 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(), "/");
	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(), "/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(), "/");

	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(), "/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(), "/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<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)
{
}