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