1/*
2 * Copyright 2010, Christophe Huriaux
3 * Copyright 2014-2020, Haiku, inc.
4 * Distributed under the terms of the MIT licence
5 */
6
7
8#include "HttpTest.h"
9
10#include <algorithm>
11#include <cstdio>
12#include <cstdlib>
13#include <cstring>
14#include <fstream>
15#include <map>
16#include <posix/libgen.h>
17#include <string>
18
19#include <AutoDeleter.h>
20#include <HttpRequest.h>
21#include <NetworkKit.h>
22#include <UrlProtocolListener.h>
23#include <UrlProtocolRoster.h>
24
25#include <tools/cppunit/ThreadedTestCaller.h>
26
27#include "TestServer.h"
28
29
30using namespace BPrivate::Network;
31
32
33namespace {
34
35typedef std::map<std::string, std::string> HttpHeaderMap;
36
37
38class TestListener : public BUrlProtocolListener, public BDataIO {
39public:
40	TestListener(const std::string& expectedResponseBody,
41				 const HttpHeaderMap& expectedResponseHeaders)
42		:
43		fExpectedResponseBody(expectedResponseBody),
44		fExpectedResponseHeaders(expectedResponseHeaders)
45	{
46	}
47
48	virtual ssize_t Write(
49		const void *data,
50		size_t size)
51	{
52		std::copy_n(
53			(const char*)data,
54			size,
55			std::back_inserter(fActualResponseBody));
56		return size;
57	}
58
59	virtual void HeadersReceived(
60		BUrlRequest* caller)
61	{
62		const BHttpResult& http_result
63			= dynamic_cast<const BHttpResult&>(caller->Result());
64		const BHttpHeaders& headers = http_result.Headers();
65
66		for (int32 i = 0; i < headers.CountHeaders(); ++i) {
67			const BHttpHeader& header = headers.HeaderAt(i);
68			fActualResponseHeaders[std::string(header.Name())]
69				= std::string(header.Value());
70		}
71	}
72
73
74	virtual bool CertificateVerificationFailed(
75		BUrlRequest* caller,
76		BCertificate& certificate,
77		const char* message)
78	{
79		// TODO: Add tests that exercize this behavior.
80		//
81		// At the moment there doesn't seem to be any public API for providing
82		// an alternate certificate authority, or for constructing a
83		// BCertificate to be sent to BUrlContext::AddCertificateException().
84		// Once we have such a public API then it will be useful to create
85		// test scenarios that exercize the validation performed by the
86		// undrelying TLS implementaiton to verify that it is configured
87		// to do so.
88		//
89		// For now we just disable TLS certificate validation entirely because
90		// we are generating a self-signed TLS certificate for these tests.
91		return true;
92	}
93
94
95	void Verify()
96	{
97		CPPUNIT_ASSERT_EQUAL(fExpectedResponseBody, fActualResponseBody);
98
99		for (HttpHeaderMap::iterator iter = fActualResponseHeaders.begin();
100			 iter != fActualResponseHeaders.end();
101			 ++iter)
102		{
103			CPPUNIT_ASSERT_EQUAL_MESSAGE(
104				"(header " + iter->first + ")",
105				fExpectedResponseHeaders[iter->first],
106				iter->second);
107		}
108		CPPUNIT_ASSERT_EQUAL(
109			fExpectedResponseHeaders.size(),
110			fActualResponseHeaders.size());
111	}
112
113private:
114	std::string fExpectedResponseBody;
115	std::string fActualResponseBody;
116
117	HttpHeaderMap fExpectedResponseHeaders;
118	HttpHeaderMap fActualResponseHeaders;
119};
120
121
122void SendAuthenticatedRequest(
123	BUrlContext &context,
124	BUrl &testUrl,
125	const std::string& expectedResponseBody,
126	const HttpHeaderMap &expectedResponseHeaders)
127{
128	TestListener listener(expectedResponseBody, expectedResponseHeaders);
129
130	ObjectDeleter<BUrlRequest> requestDeleter(
131		BUrlProtocolRoster::MakeRequest(testUrl, &listener, &listener,
132			&context));
133	BHttpRequest* request = dynamic_cast<BHttpRequest*>(requestDeleter.Get());
134	CPPUNIT_ASSERT(request != NULL);
135
136	request->SetUserName("walter");
137	request->SetPassword("secret");
138
139	CPPUNIT_ASSERT(request->Run());
140
141	while (request->IsRunning())
142		snooze(1000);
143
144	CPPUNIT_ASSERT_EQUAL(B_OK, request->Status());
145
146	const BHttpResult &result =
147		dynamic_cast<const BHttpResult &>(request->Result());
148	CPPUNIT_ASSERT_EQUAL(200, result.StatusCode());
149	CPPUNIT_ASSERT_EQUAL(BString("OK"), result.StatusText());
150
151	listener.Verify();
152}
153
154
155// Return the path of a file path relative to this source file.
156std::string TestFilePath(const std::string& relativePath)
157{
158	char *testFileSource = strdup(__FILE__);
159	MemoryDeleter _(testFileSource);
160
161	std::string testSrcDir(::dirname(testFileSource));
162
163	return testSrcDir + "/" + relativePath;
164}
165
166
167template <typename T>
168void AddCommonTests(BThreadedTestCaller<T>& testCaller)
169{
170	testCaller.addThread("GetTest", &T::GetTest);
171	testCaller.addThread("HeadTest", &T::HeadTest);
172	testCaller.addThread("NoContentTest", &T::NoContentTest);
173	testCaller.addThread("UploadTest", &T::UploadTest);
174	testCaller.addThread("BasicAuthTest", &T::AuthBasicTest);
175	testCaller.addThread("DigestAuthTest", &T::AuthDigestTest);
176	testCaller.addThread("AutoRedirectTest", &T::AutoRedirectTest);
177}
178
179}
180
181
182HttpTest::HttpTest(TestServerMode mode)
183	:
184	fTestServer(mode)
185{
186}
187
188
189HttpTest::~HttpTest()
190{
191}
192
193
194void
195HttpTest::setUp()
196{
197	CPPUNIT_ASSERT_EQUAL_MESSAGE(
198		"Starting up test server",
199		B_OK,
200		fTestServer.Start());
201}
202
203
204void
205HttpTest::GetTest()
206{
207	_GetTest("/");
208}
209
210
211void
212HttpTest::HeadTest()
213{
214	BUrl testUrl(fTestServer.BaseUrl(), "/");
215	BUrlContext* context = new BUrlContext();
216	context->AcquireReference();
217
218	std::string expectedResponseBody("");
219	HttpHeaderMap expectedResponseHeaders;
220	expectedResponseHeaders["Content-Encoding"] = "gzip";
221	expectedResponseHeaders["Content-Length"] = "144";
222	expectedResponseHeaders["Content-Type"] = "text/plain";
223	expectedResponseHeaders["Date"] = "Sun, 09 Feb 2020 19:32:42 GMT";
224	expectedResponseHeaders["Server"] = "Test HTTP Server for Haiku";
225
226	TestListener listener(expectedResponseBody, expectedResponseHeaders);
227
228	ObjectDeleter<BUrlRequest> requestDeleter(
229		BUrlProtocolRoster::MakeRequest(testUrl, &listener, &listener,
230			context));
231	BHttpRequest* request = dynamic_cast<BHttpRequest*>(requestDeleter.Get());
232	CPPUNIT_ASSERT(request != NULL);
233
234	request->SetAutoReferrer(false);
235	request->SetMethod("HEAD");
236
237	CPPUNIT_ASSERT(request->Run());
238	while (request->IsRunning())
239		snooze(1000);
240
241	CPPUNIT_ASSERT_EQUAL(B_OK, request->Status());
242
243	const BHttpResult& result
244		= dynamic_cast<const BHttpResult&>(request->Result());
245	CPPUNIT_ASSERT_EQUAL(200, result.StatusCode());
246	CPPUNIT_ASSERT_EQUAL(BString("OK"), result.StatusText());
247
248	CPPUNIT_ASSERT_EQUAL(144, result.Length());
249
250	listener.Verify();
251
252	CPPUNIT_ASSERT(!context->GetCookieJar().GetIterator().HasNext());
253		// This page should not set cookies
254
255	context->ReleaseReference();
256}
257
258
259void
260HttpTest::NoContentTest()
261{
262	BUrl testUrl(fTestServer.BaseUrl(), "/204");
263	BUrlContext* context = new BUrlContext();
264	context->AcquireReference();
265
266	std::string expectedResponseBody("");
267	HttpHeaderMap expectedResponseHeaders;
268	expectedResponseHeaders["Date"] = "Sun, 09 Feb 2020 19:32:42 GMT";
269	expectedResponseHeaders["Server"] = "Test HTTP Server for Haiku";
270
271	TestListener listener(expectedResponseBody, expectedResponseHeaders);
272
273	ObjectDeleter<BUrlRequest> requestDeleter(
274		BUrlProtocolRoster::MakeRequest(testUrl, &listener, &listener,
275			context));
276	BHttpRequest* request = dynamic_cast<BHttpRequest*>(requestDeleter.Get());
277	CPPUNIT_ASSERT(request != NULL);
278
279	request->SetAutoReferrer(false);
280
281	CPPUNIT_ASSERT(request->Run());
282	while (request->IsRunning())
283		snooze(1000);
284
285	CPPUNIT_ASSERT_EQUAL(B_OK, request->Status());
286
287	const BHttpResult& result
288		= dynamic_cast<const BHttpResult&>(request->Result());
289	CPPUNIT_ASSERT_EQUAL(204, result.StatusCode());
290	CPPUNIT_ASSERT_EQUAL(BString("No Content"), result.StatusText());
291
292	listener.Verify();
293
294	CPPUNIT_ASSERT(!context->GetCookieJar().GetIterator().HasNext());
295		// This page should not set cookies
296
297	context->ReleaseReference();
298}
299
300
301void
302HttpTest::ProxyTest()
303{
304	BUrl testUrl(fTestServer.BaseUrl(), "/");
305
306	TestProxyServer proxy;
307	CPPUNIT_ASSERT_EQUAL_MESSAGE(
308		"Test proxy server startup",
309		B_OK,
310		proxy.Start());
311
312	BUrlContext* context = new BUrlContext();
313	context->AcquireReference();
314	context->SetProxy("127.0.0.1", proxy.Port());
315
316	std::string expectedResponseBody(
317		"Path: /\r\n"
318		"\r\n"
319		"Headers:\r\n"
320		"--------\r\n"
321		"Host: 127.0.0.1:PORT\r\n"
322		"Content-Length: 0\r\n"
323		"Accept: */*\r\n"
324		"Accept-Encoding: gzip\r\n"
325		"Connection: close\r\n"
326		"User-Agent: Services Kit (Haiku)\r\n"
327		"X-Forwarded-For: 127.0.0.1:PORT\r\n");
328	HttpHeaderMap expectedResponseHeaders;
329	expectedResponseHeaders["Content-Encoding"] = "gzip";
330	expectedResponseHeaders["Content-Length"] = "169";
331	expectedResponseHeaders["Content-Type"] = "text/plain";
332	expectedResponseHeaders["Date"] = "Sun, 09 Feb 2020 19:32:42 GMT";
333	expectedResponseHeaders["Server"] = "Test HTTP Server for Haiku";
334
335	TestListener listener(expectedResponseBody, expectedResponseHeaders);
336
337	ObjectDeleter<BUrlRequest> requestDeleter(
338		BUrlProtocolRoster::MakeRequest(testUrl, &listener, &listener,
339			context));
340	BHttpRequest* request = dynamic_cast<BHttpRequest*>(requestDeleter.Get());
341	CPPUNIT_ASSERT(request != NULL);
342
343	CPPUNIT_ASSERT(request->Run());
344
345	while (request->IsRunning())
346		snooze(1000);
347
348	CPPUNIT_ASSERT_EQUAL(B_OK, request->Status());
349
350	const BHttpResult& response
351		= dynamic_cast<const BHttpResult&>(request->Result());
352	CPPUNIT_ASSERT_EQUAL(200, response.StatusCode());
353	CPPUNIT_ASSERT_EQUAL(BString("OK"), response.StatusText());
354	CPPUNIT_ASSERT_EQUAL(169, response.Length());
355		// Fixed size as we know the response format.
356	CPPUNIT_ASSERT(!context->GetCookieJar().GetIterator().HasNext());
357		// This page should not set cookies
358
359	listener.Verify();
360
361	context->ReleaseReference();
362}
363
364
365void
366HttpTest::UploadTest()
367{
368	std::string testFilePath = TestFilePath("testfile.txt");
369
370	// The test server will echo the POST body back to us in the HTTP response,
371	// so here we load it into memory so that we can compare to make sure that
372	// the server received it.
373	std::string fileContents;
374	{
375		std::ifstream inputStream(
376			testFilePath.c_str(),
377			std::ios::in | std::ios::binary);
378		CPPUNIT_ASSERT(inputStream);
379
380		inputStream.seekg(0, std::ios::end);
381		fileContents.resize(inputStream.tellg());
382
383		inputStream.seekg(0, std::ios::beg);
384		inputStream.read(&fileContents[0], fileContents.size());
385		inputStream.close();
386
387		CPPUNIT_ASSERT(!fileContents.empty());
388	}
389
390	std::string expectedResponseBody(
391		"Path: /post\r\n"
392		"\r\n"
393		"Headers:\r\n"
394		"--------\r\n"
395		"Host: 127.0.0.1:PORT\r\n"
396		"Accept: */*\r\n"
397		"Accept-Encoding: gzip\r\n"
398		"Connection: close\r\n"
399		"User-Agent: Services Kit (Haiku)\r\n"
400		"Content-Type: multipart/form-data; boundary=<<BOUNDARY-ID>>\r\n"
401		"Content-Length: 1404\r\n"
402		"\r\n"
403		"Request body:\r\n"
404		"-------------\r\n"
405		"--<<BOUNDARY-ID>>\r\n"
406		"Content-Disposition: form-data; name=\"_uploadfile\";"
407		" filename=\"testfile.txt\"\r\n"
408		"Content-Type: application/octet-stream\r\n"
409		"\r\n"
410		+ fileContents
411		+ "\r\n"
412		"--<<BOUNDARY-ID>>\r\n"
413		"Content-Disposition: form-data; name=\"hello\"\r\n"
414		"\r\n"
415		"world\r\n"
416		"--<<BOUNDARY-ID>>--\r\n"
417		"\r\n");
418	HttpHeaderMap expectedResponseHeaders;
419	expectedResponseHeaders["Content-Encoding"] = "gzip";
420	expectedResponseHeaders["Content-Length"] = "913";
421	expectedResponseHeaders["Content-Type"] = "text/plain";
422	expectedResponseHeaders["Date"] = "Sun, 09 Feb 2020 19:32:42 GMT";
423	expectedResponseHeaders["Server"] = "Test HTTP Server for Haiku";
424	TestListener listener(expectedResponseBody, expectedResponseHeaders);
425
426	BUrl testUrl(fTestServer.BaseUrl(), "/post");
427
428	BUrlContext context;
429
430	ObjectDeleter<BUrlRequest> requestDeleter(
431		BUrlProtocolRoster::MakeRequest(testUrl, &listener, &listener,
432			&context));
433	BHttpRequest* request = dynamic_cast<BHttpRequest*>(requestDeleter.Get());
434	CPPUNIT_ASSERT(request != NULL);
435
436	BHttpForm form;
437	form.AddString("hello", "world");
438	CPPUNIT_ASSERT_EQUAL(
439		B_OK,
440		form.AddFile("_uploadfile", BPath(testFilePath.c_str())));
441
442	request->SetPostFields(form);
443
444	CPPUNIT_ASSERT(request->Run());
445
446	while (request->IsRunning())
447		snooze(1000);
448
449	CPPUNIT_ASSERT_EQUAL(B_OK, request->Status());
450
451	const BHttpResult &result =
452		dynamic_cast<const BHttpResult &>(request->Result());
453	CPPUNIT_ASSERT_EQUAL(200, result.StatusCode());
454	CPPUNIT_ASSERT_EQUAL(BString("OK"), result.StatusText());
455	CPPUNIT_ASSERT_EQUAL(913, result.Length());
456
457	listener.Verify();
458}
459
460
461void
462HttpTest::AuthBasicTest()
463{
464	BUrlContext context;
465
466	BUrl testUrl(fTestServer.BaseUrl(), "/auth/basic/walter/secret");
467
468	std::string expectedResponseBody(
469		"Path: /auth/basic/walter/secret\r\n"
470		"\r\n"
471		"Headers:\r\n"
472		"--------\r\n"
473		"Host: 127.0.0.1:PORT\r\n"
474		"Accept: */*\r\n"
475		"Accept-Encoding: gzip\r\n"
476		"Connection: close\r\n"
477		"User-Agent: Services Kit (Haiku)\r\n"
478		"Referer: SCHEME://127.0.0.1:PORT/auth/basic/walter/secret\r\n"
479		"Authorization: Basic d2FsdGVyOnNlY3JldA==\r\n");
480
481	HttpHeaderMap expectedResponseHeaders;
482	expectedResponseHeaders["Content-Encoding"] = "gzip";
483	expectedResponseHeaders["Content-Length"] = "212";
484	expectedResponseHeaders["Content-Type"] = "text/plain";
485	expectedResponseHeaders["Date"] = "Sun, 09 Feb 2020 19:32:42 GMT";
486	expectedResponseHeaders["Server"] = "Test HTTP Server for Haiku";
487	expectedResponseHeaders["Www-Authenticate"] = "Basic realm=\"Fake Realm\"";
488
489	SendAuthenticatedRequest(context, testUrl, expectedResponseBody,
490		expectedResponseHeaders);
491
492	CPPUNIT_ASSERT(!context.GetCookieJar().GetIterator().HasNext());
493		// This page should not set cookies
494}
495
496
497void
498HttpTest::AuthDigestTest()
499{
500	BUrlContext context;
501
502	BUrl testUrl(fTestServer.BaseUrl(), "/auth/digest/walter/secret");
503
504	std::string expectedResponseBody(
505		"Path: /auth/digest/walter/secret\r\n"
506		"\r\n"
507		"Headers:\r\n"
508		"--------\r\n"
509		"Host: 127.0.0.1:PORT\r\n"
510		"Accept: */*\r\n"
511		"Accept-Encoding: gzip\r\n"
512		"Connection: close\r\n"
513		"User-Agent: Services Kit (Haiku)\r\n"
514		"Referer: SCHEME://127.0.0.1:PORT/auth/digest/walter/secret\r\n"
515		"Authorization: Digest username=\"walter\","
516		" realm=\"user@shredder\","
517		" nonce=\"f3a95f20879dd891a5544bf96a3e5518\","
518		" algorithm=MD5,"
519		" opaque=\"f0bb55f1221a51b6d38117c331611799\","
520		" uri=\"/auth/digest/walter/secret\","
521		" qop=auth,"
522		" cnonce=\"60a3d95d286a732374f0f35fb6d21e79\","
523		" nc=00000001,"
524		" response=\"f4264de468aa1a91d81ac40fa73445f3\"\r\n"
525		"Cookie: stale_after=never; fake=fake_value\r\n");
526
527	HttpHeaderMap expectedResponseHeaders;
528	expectedResponseHeaders["Content-Encoding"] = "gzip";
529	expectedResponseHeaders["Content-Length"] = "403";
530	expectedResponseHeaders["Content-Type"] = "text/plain";
531	expectedResponseHeaders["Date"] = "Sun, 09 Feb 2020 19:32:42 GMT";
532	expectedResponseHeaders["Server"] = "Test HTTP Server for Haiku";
533	expectedResponseHeaders["Set-Cookie"] = "fake=fake_value; Path=/";
534	expectedResponseHeaders["Www-Authenticate"]
535		= "Digest realm=\"user@shredder\", "
536		"nonce=\"f3a95f20879dd891a5544bf96a3e5518\", "
537		"qop=\"auth\", "
538		"opaque=f0bb55f1221a51b6d38117c331611799, "
539		"algorithm=MD5, "
540		"stale=FALSE";
541
542	SendAuthenticatedRequest(context, testUrl, expectedResponseBody,
543		expectedResponseHeaders);
544
545	std::map<BString, BString> cookies;
546	BNetworkCookieJar::Iterator iter
547		= context.GetCookieJar().GetIterator();
548	while (iter.HasNext()) {
549		const BNetworkCookie* cookie = iter.Next();
550		cookies[cookie->Name()] = cookie->Value();
551	}
552	CPPUNIT_ASSERT_EQUAL(2, cookies.size());
553	CPPUNIT_ASSERT_EQUAL(BString("fake_value"), cookies["fake"]);
554	CPPUNIT_ASSERT_EQUAL(BString("never"), cookies["stale_after"]);
555}
556
557
558void
559HttpTest::AutoRedirectTest()
560{
561	_GetTest("/302");
562}
563
564
565/* static */ void
566HttpTest::AddTests(BTestSuite& parent)
567{
568	{
569		CppUnit::TestSuite& suite = *new CppUnit::TestSuite("HttpTest");
570
571		HttpTest* httpTest = new HttpTest();
572		BThreadedTestCaller<HttpTest>* httpTestCaller
573			= new BThreadedTestCaller<HttpTest>("HttpTest::", httpTest);
574
575		// HTTP + HTTPs
576		AddCommonTests<HttpTest>(*httpTestCaller);
577
578		httpTestCaller->addThread("ProxyTest", &HttpTest::ProxyTest);
579
580		suite.addTest(httpTestCaller);
581		parent.addTest("HttpTest", &suite);
582	}
583
584	{
585		CppUnit::TestSuite& suite = *new CppUnit::TestSuite("HttpsTest");
586
587		HttpsTest* httpsTest = new HttpsTest();
588		BThreadedTestCaller<HttpsTest>* httpsTestCaller
589			= new BThreadedTestCaller<HttpsTest>("HttpsTest::", httpsTest);
590
591		// HTTP + HTTPs
592		AddCommonTests<HttpsTest>(*httpsTestCaller);
593
594		suite.addTest(httpsTestCaller);
595		parent.addTest("HttpsTest", &suite);
596	}
597}
598
599
600void
601HttpTest::_GetTest(const BString& path)
602{
603	BUrl testUrl(fTestServer.BaseUrl(), path);
604	BUrlContext* context = new BUrlContext();
605	context->AcquireReference();
606
607	std::string expectedResponseBody(
608		"Path: /\r\n"
609		"\r\n"
610		"Headers:\r\n"
611		"--------\r\n"
612		"Host: 127.0.0.1:PORT\r\n"
613		"Accept: */*\r\n"
614		"Accept-Encoding: gzip\r\n"
615		"Connection: close\r\n"
616		"User-Agent: Services Kit (Haiku)\r\n");
617	HttpHeaderMap expectedResponseHeaders;
618	expectedResponseHeaders["Content-Encoding"] = "gzip";
619	expectedResponseHeaders["Content-Length"] = "144";
620	expectedResponseHeaders["Content-Type"] = "text/plain";
621	expectedResponseHeaders["Date"] = "Sun, 09 Feb 2020 19:32:42 GMT";
622	expectedResponseHeaders["Server"] = "Test HTTP Server for Haiku";
623
624	TestListener listener(expectedResponseBody, expectedResponseHeaders);
625
626	ObjectDeleter<BUrlRequest> requestDeleter(
627		BUrlProtocolRoster::MakeRequest(testUrl, &listener, &listener,
628			context));
629	BHttpRequest* request = dynamic_cast<BHttpRequest*>(requestDeleter.Get());
630	CPPUNIT_ASSERT(request != NULL);
631
632	request->SetAutoReferrer(false);
633
634	CPPUNIT_ASSERT(request->Run());
635	while (request->IsRunning())
636		snooze(1000);
637
638	CPPUNIT_ASSERT_EQUAL(B_OK, request->Status());
639
640	const BHttpResult& result
641		= dynamic_cast<const BHttpResult&>(request->Result());
642	CPPUNIT_ASSERT_EQUAL(200, result.StatusCode());
643	CPPUNIT_ASSERT_EQUAL(BString("OK"), result.StatusText());
644
645	CPPUNIT_ASSERT_EQUAL(144, result.Length());
646
647	listener.Verify();
648
649	CPPUNIT_ASSERT(!context->GetCookieJar().GetIterator().HasNext());
650		// This page should not set cookies
651
652	context->ReleaseReference();
653}
654
655
656// # pragma mark - HTTPS
657
658
659HttpsTest::HttpsTest()
660	:
661	HttpTest(TEST_SERVER_MODE_HTTPS)
662{
663}
664