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