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