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