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