xref: /haiku/src/tests/kits/net/service/HttpTest.cpp (revision 410ed2fbba58819ac21e27d3676739728416761d)
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 
30 using namespace BPrivate::Network;
31 
32 
33 namespace {
34 
35 typedef std::map<std::string, std::string> HttpHeaderMap;
36 
37 
38 class TestListener : public BUrlProtocolListener, public BDataIO {
39 public:
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 
113 private:
114 	std::string fExpectedResponseBody;
115 	std::string fActualResponseBody;
116 
117 	HttpHeaderMap fExpectedResponseHeaders;
118 	HttpHeaderMap fActualResponseHeaders;
119 };
120 
121 
122 void 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.
156 std::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 
167 template <typename T>
168 void AddCommonTests(BThreadedTestCaller<T>& testCaller)
169 {
170 	testCaller.addThread("GetTest", &T::GetTest);
171 	testCaller.addThread("UploadTest", &T::UploadTest);
172 	testCaller.addThread("BasicAuthTest", &T::AuthBasicTest);
173 	testCaller.addThread("DigestAuthTest", &T::AuthDigestTest);
174 	testCaller.addThread("AutoRedirectTest", &T::AutoRedirectTest);
175 }
176 
177 }
178 
179 
180 HttpTest::HttpTest(TestServerMode mode)
181 	:
182 	fTestServer(mode)
183 {
184 }
185 
186 
187 HttpTest::~HttpTest()
188 {
189 }
190 
191 
192 void
193 HttpTest::setUp()
194 {
195 	CPPUNIT_ASSERT_EQUAL_MESSAGE(
196 		"Starting up test server",
197 		B_OK,
198 		fTestServer.Start());
199 }
200 
201 
202 void
203 HttpTest::GetTest()
204 {
205 	_GetTest("/");
206 }
207 
208 
209 void
210 HttpTest::ProxyTest()
211 {
212 	BUrl testUrl(fTestServer.BaseUrl(), "/");
213 
214 	TestProxyServer proxy;
215 	CPPUNIT_ASSERT_EQUAL_MESSAGE(
216 		"Test proxy server startup",
217 		B_OK,
218 		proxy.Start());
219 
220 	BUrlContext* context = new BUrlContext();
221 	context->AcquireReference();
222 	context->SetProxy("127.0.0.1", proxy.Port());
223 
224 	std::string expectedResponseBody(
225 		"Path: /\r\n"
226 		"\r\n"
227 		"Headers:\r\n"
228 		"--------\r\n"
229 		"Host: 127.0.0.1:PORT\r\n"
230 		"Content-Length: 0\r\n"
231 		"Accept: */*\r\n"
232 		"Accept-Encoding: gzip\r\n"
233 		"Connection: close\r\n"
234 		"User-Agent: Services Kit (Haiku)\r\n"
235 		"X-Forwarded-For: 127.0.0.1:PORT\r\n");
236 	HttpHeaderMap expectedResponseHeaders;
237 	expectedResponseHeaders["Content-Encoding"] = "gzip";
238 	expectedResponseHeaders["Content-Length"] = "169";
239 	expectedResponseHeaders["Content-Type"] = "text/plain";
240 	expectedResponseHeaders["Date"] = "Sun, 09 Feb 2020 19:32:42 GMT";
241 	expectedResponseHeaders["Server"] = "Test HTTP Server for Haiku";
242 
243 	TestListener listener(expectedResponseBody, expectedResponseHeaders);
244 
245 	ObjectDeleter<BUrlRequest> requestDeleter(
246 		BUrlProtocolRoster::MakeRequest(testUrl, &listener, &listener,
247 			context));
248 	BHttpRequest* request = dynamic_cast<BHttpRequest*>(requestDeleter.Get());
249 	CPPUNIT_ASSERT(request != NULL);
250 
251 	CPPUNIT_ASSERT(request->Run());
252 
253 	while (request->IsRunning())
254 		snooze(1000);
255 
256 	CPPUNIT_ASSERT_EQUAL(B_OK, request->Status());
257 
258 	const BHttpResult& response
259 		= dynamic_cast<const BHttpResult&>(request->Result());
260 	CPPUNIT_ASSERT_EQUAL(200, response.StatusCode());
261 	CPPUNIT_ASSERT_EQUAL(BString("OK"), response.StatusText());
262 	CPPUNIT_ASSERT_EQUAL(169, response.Length());
263 		// Fixed size as we know the response format.
264 	CPPUNIT_ASSERT(!context->GetCookieJar().GetIterator().HasNext());
265 		// This page should not set cookies
266 
267 	listener.Verify();
268 
269 	context->ReleaseReference();
270 }
271 
272 
273 void
274 HttpTest::UploadTest()
275 {
276 	std::string testFilePath = TestFilePath("testfile.txt");
277 
278 	// The test server will echo the POST body back to us in the HTTP response,
279 	// so here we load it into memory so that we can compare to make sure that
280 	// the server received it.
281 	std::string fileContents;
282 	{
283 		std::ifstream inputStream(
284 			testFilePath.c_str(),
285 			std::ios::in | std::ios::binary);
286 		CPPUNIT_ASSERT(inputStream);
287 
288 		inputStream.seekg(0, std::ios::end);
289 		fileContents.resize(inputStream.tellg());
290 
291 		inputStream.seekg(0, std::ios::beg);
292 		inputStream.read(&fileContents[0], fileContents.size());
293 		inputStream.close();
294 
295 		CPPUNIT_ASSERT(!fileContents.empty());
296 	}
297 
298 	std::string expectedResponseBody(
299 		"Path: /post\r\n"
300 		"\r\n"
301 		"Headers:\r\n"
302 		"--------\r\n"
303 		"Host: 127.0.0.1:PORT\r\n"
304 		"Accept: */*\r\n"
305 		"Accept-Encoding: gzip\r\n"
306 		"Connection: close\r\n"
307 		"User-Agent: Services Kit (Haiku)\r\n"
308 		"Content-Type: multipart/form-data; boundary=<<BOUNDARY-ID>>\r\n"
309 		"Content-Length: 1404\r\n"
310 		"\r\n"
311 		"Request body:\r\n"
312 		"-------------\r\n"
313 		"--<<BOUNDARY-ID>>\r\n"
314 		"Content-Disposition: form-data; name=\"_uploadfile\";"
315 		" filename=\"testfile.txt\"\r\n"
316 		"Content-Type: application/octet-stream\r\n"
317 		"\r\n"
318 		+ fileContents
319 		+ "\r\n"
320 		"--<<BOUNDARY-ID>>\r\n"
321 		"Content-Disposition: form-data; name=\"hello\"\r\n"
322 		"\r\n"
323 		"world\r\n"
324 		"--<<BOUNDARY-ID>>--\r\n"
325 		"\r\n");
326 	HttpHeaderMap expectedResponseHeaders;
327 	expectedResponseHeaders["Content-Encoding"] = "gzip";
328 	expectedResponseHeaders["Content-Length"] = "913";
329 	expectedResponseHeaders["Content-Type"] = "text/plain";
330 	expectedResponseHeaders["Date"] = "Sun, 09 Feb 2020 19:32:42 GMT";
331 	expectedResponseHeaders["Server"] = "Test HTTP Server for Haiku";
332 	TestListener listener(expectedResponseBody, expectedResponseHeaders);
333 
334 	BUrl testUrl(fTestServer.BaseUrl(), "/post");
335 
336 	BUrlContext context;
337 
338 	ObjectDeleter<BUrlRequest> requestDeleter(
339 		BUrlProtocolRoster::MakeRequest(testUrl, &listener, &listener,
340 			&context));
341 	BHttpRequest* request = dynamic_cast<BHttpRequest*>(requestDeleter.Get());
342 	CPPUNIT_ASSERT(request != NULL);
343 
344 	BHttpForm form;
345 	form.AddString("hello", "world");
346 	CPPUNIT_ASSERT_EQUAL(
347 		B_OK,
348 		form.AddFile("_uploadfile", BPath(testFilePath.c_str())));
349 
350 	request->SetPostFields(form);
351 
352 	CPPUNIT_ASSERT(request->Run());
353 
354 	while (request->IsRunning())
355 		snooze(1000);
356 
357 	CPPUNIT_ASSERT_EQUAL(B_OK, request->Status());
358 
359 	const BHttpResult &result =
360 		dynamic_cast<const BHttpResult &>(request->Result());
361 	CPPUNIT_ASSERT_EQUAL(200, result.StatusCode());
362 	CPPUNIT_ASSERT_EQUAL(BString("OK"), result.StatusText());
363 	CPPUNIT_ASSERT_EQUAL(913, result.Length());
364 
365 	listener.Verify();
366 }
367 
368 
369 void
370 HttpTest::AuthBasicTest()
371 {
372 	BUrlContext context;
373 
374 	BUrl testUrl(fTestServer.BaseUrl(), "/auth/basic/walter/secret");
375 
376 	std::string expectedResponseBody(
377 		"Path: /auth/basic/walter/secret\r\n"
378 		"\r\n"
379 		"Headers:\r\n"
380 		"--------\r\n"
381 		"Host: 127.0.0.1:PORT\r\n"
382 		"Accept: */*\r\n"
383 		"Accept-Encoding: gzip\r\n"
384 		"Connection: close\r\n"
385 		"User-Agent: Services Kit (Haiku)\r\n"
386 		"Referer: SCHEME://127.0.0.1:PORT/auth/basic/walter/secret\r\n"
387 		"Authorization: Basic d2FsdGVyOnNlY3JldA==\r\n");
388 
389 	HttpHeaderMap expectedResponseHeaders;
390 	expectedResponseHeaders["Content-Encoding"] = "gzip";
391 	expectedResponseHeaders["Content-Length"] = "212";
392 	expectedResponseHeaders["Content-Type"] = "text/plain";
393 	expectedResponseHeaders["Date"] = "Sun, 09 Feb 2020 19:32:42 GMT";
394 	expectedResponseHeaders["Server"] = "Test HTTP Server for Haiku";
395 	expectedResponseHeaders["Www-Authenticate"] = "Basic realm=\"Fake Realm\"";
396 
397 	SendAuthenticatedRequest(context, testUrl, expectedResponseBody,
398 		expectedResponseHeaders);
399 
400 	CPPUNIT_ASSERT(!context.GetCookieJar().GetIterator().HasNext());
401 		// This page should not set cookies
402 }
403 
404 
405 void
406 HttpTest::AuthDigestTest()
407 {
408 	BUrlContext context;
409 
410 	BUrl testUrl(fTestServer.BaseUrl(), "/auth/digest/walter/secret");
411 
412 	std::string expectedResponseBody(
413 		"Path: /auth/digest/walter/secret\r\n"
414 		"\r\n"
415 		"Headers:\r\n"
416 		"--------\r\n"
417 		"Host: 127.0.0.1:PORT\r\n"
418 		"Accept: */*\r\n"
419 		"Accept-Encoding: gzip\r\n"
420 		"Connection: close\r\n"
421 		"User-Agent: Services Kit (Haiku)\r\n"
422 		"Referer: SCHEME://127.0.0.1:PORT/auth/digest/walter/secret\r\n"
423 		"Authorization: Digest username=\"walter\","
424 		" realm=\"user@shredder\","
425 		" nonce=\"f3a95f20879dd891a5544bf96a3e5518\","
426 		" algorithm=MD5,"
427 		" opaque=\"f0bb55f1221a51b6d38117c331611799\","
428 		" uri=\"/auth/digest/walter/secret\","
429 		" qop=auth,"
430 		" cnonce=\"60a3d95d286a732374f0f35fb6d21e79\","
431 		" nc=00000001,"
432 		" response=\"f4264de468aa1a91d81ac40fa73445f3\"\r\n"
433 		"Cookie: stale_after=never; fake=fake_value\r\n");
434 
435 	HttpHeaderMap expectedResponseHeaders;
436 	expectedResponseHeaders["Content-Encoding"] = "gzip";
437 	expectedResponseHeaders["Content-Length"] = "403";
438 	expectedResponseHeaders["Content-Type"] = "text/plain";
439 	expectedResponseHeaders["Date"] = "Sun, 09 Feb 2020 19:32:42 GMT";
440 	expectedResponseHeaders["Server"] = "Test HTTP Server for Haiku";
441 	expectedResponseHeaders["Set-Cookie"] = "fake=fake_value; Path=/";
442 	expectedResponseHeaders["Www-Authenticate"]
443 		= "Digest realm=\"user@shredder\", "
444 		"nonce=\"f3a95f20879dd891a5544bf96a3e5518\", "
445 		"qop=\"auth\", "
446 		"opaque=f0bb55f1221a51b6d38117c331611799, "
447 		"algorithm=MD5, "
448 		"stale=FALSE";
449 
450 	SendAuthenticatedRequest(context, testUrl, expectedResponseBody,
451 		expectedResponseHeaders);
452 
453 	std::map<BString, BString> cookies;
454 	BNetworkCookieJar::Iterator iter
455 		= context.GetCookieJar().GetIterator();
456 	while (iter.HasNext()) {
457 		const BNetworkCookie* cookie = iter.Next();
458 		cookies[cookie->Name()] = cookie->Value();
459 	}
460 	CPPUNIT_ASSERT_EQUAL(2, cookies.size());
461 	CPPUNIT_ASSERT_EQUAL(BString("fake_value"), cookies["fake"]);
462 	CPPUNIT_ASSERT_EQUAL(BString("never"), cookies["stale_after"]);
463 }
464 
465 
466 void
467 HttpTest::AutoRedirectTest()
468 {
469 	_GetTest("/302");
470 }
471 
472 
473 /* static */ void
474 HttpTest::AddTests(BTestSuite& parent)
475 {
476 	{
477 		CppUnit::TestSuite& suite = *new CppUnit::TestSuite("HttpTest");
478 
479 		HttpTest* httpTest = new HttpTest();
480 		BThreadedTestCaller<HttpTest>* httpTestCaller
481 			= new BThreadedTestCaller<HttpTest>("HttpTest::", httpTest);
482 
483 		// HTTP + HTTPs
484 		AddCommonTests<HttpTest>(*httpTestCaller);
485 
486 		httpTestCaller->addThread("ProxyTest", &HttpTest::ProxyTest);
487 
488 		suite.addTest(httpTestCaller);
489 		parent.addTest("HttpTest", &suite);
490 	}
491 
492 	{
493 		CppUnit::TestSuite& suite = *new CppUnit::TestSuite("HttpsTest");
494 
495 		HttpsTest* httpsTest = new HttpsTest();
496 		BThreadedTestCaller<HttpsTest>* httpsTestCaller
497 			= new BThreadedTestCaller<HttpsTest>("HttpsTest::", httpsTest);
498 
499 		// HTTP + HTTPs
500 		AddCommonTests<HttpsTest>(*httpsTestCaller);
501 
502 		suite.addTest(httpsTestCaller);
503 		parent.addTest("HttpsTest", &suite);
504 	}
505 }
506 
507 
508 void
509 HttpTest::_GetTest(const BString& path)
510 {
511 	BUrl testUrl(fTestServer.BaseUrl(), path);
512 	BUrlContext* context = new BUrlContext();
513 	context->AcquireReference();
514 
515 	std::string expectedResponseBody(
516 		"Path: /\r\n"
517 		"\r\n"
518 		"Headers:\r\n"
519 		"--------\r\n"
520 		"Host: 127.0.0.1:PORT\r\n"
521 		"Accept: */*\r\n"
522 		"Accept-Encoding: gzip\r\n"
523 		"Connection: close\r\n"
524 		"User-Agent: Services Kit (Haiku)\r\n");
525 	HttpHeaderMap expectedResponseHeaders;
526 	expectedResponseHeaders["Content-Encoding"] = "gzip";
527 	expectedResponseHeaders["Content-Length"] = "144";
528 	expectedResponseHeaders["Content-Type"] = "text/plain";
529 	expectedResponseHeaders["Date"] = "Sun, 09 Feb 2020 19:32:42 GMT";
530 	expectedResponseHeaders["Server"] = "Test HTTP Server for Haiku";
531 
532 	TestListener listener(expectedResponseBody, expectedResponseHeaders);
533 
534 	ObjectDeleter<BUrlRequest> requestDeleter(
535 		BUrlProtocolRoster::MakeRequest(testUrl, &listener, &listener,
536 			context));
537 	BHttpRequest* request = dynamic_cast<BHttpRequest*>(requestDeleter.Get());
538 	CPPUNIT_ASSERT(request != NULL);
539 
540 	request->SetAutoReferrer(false);
541 
542 	CPPUNIT_ASSERT(request->Run());
543 	while (request->IsRunning())
544 		snooze(1000);
545 
546 	CPPUNIT_ASSERT_EQUAL(B_OK, request->Status());
547 
548 	const BHttpResult& result
549 		= dynamic_cast<const BHttpResult&>(request->Result());
550 	CPPUNIT_ASSERT_EQUAL(200, result.StatusCode());
551 	CPPUNIT_ASSERT_EQUAL(BString("OK"), result.StatusText());
552 
553 	CPPUNIT_ASSERT_EQUAL(144, result.Length());
554 
555 	listener.Verify();
556 
557 	CPPUNIT_ASSERT(!context->GetCookieJar().GetIterator().HasNext());
558 		// This page should not set cookies
559 
560 	context->ReleaseReference();
561 }
562 
563 
564 // # pragma mark - HTTPS
565 
566 
567 HttpsTest::HttpsTest()
568 	:
569 	HttpTest(TEST_SERVER_MODE_HTTPS)
570 {
571 }
572