1 #include <zypp/zyppng/base/EventDispatcher>
2 #include <zypp/zyppng/media/network/downloader.h>
3 #include <zypp/zyppng/media/network/networkrequesterror.h>
4 #include <zypp/zyppng/media/network/networkrequestdispatcher.h>
5 #include <zypp/zyppng/media/network/request.h>
6 #include <zypp/media/CredentialManager.h>
7 #include <zypp/TmpPath.h>
8 #include <zypp/PathInfo.h>
9 #include <zypp/ZConfig.h>
13 #include "WebServer.h"
15 #include <boost/test/unit_test.hpp>
16 #include <boost/test/data/test_case.hpp>
18 #define BOOST_TEST_REQ_SUCCESS(REQ) \
20 BOOST_REQUIRE_MESSAGE( REQ->state() == zyppng::Download::Success, zypp::str::Format(" %1% != zyppng::Download::Success (%2%)") % REQ->state() % REQ->errorString() ); \
21 BOOST_REQUIRE_EQUAL( REQ->lastRequestError().type(), zyppng::NetworkRequestError::NoError ); \
22 BOOST_REQUIRE( REQ->errorString().empty() ); \
25 #define BOOST_TEST_REQ_FAILED(REQ) \
27 BOOST_REQUIRE_EQUAL( REQ->state(), zyppng::Download::Failed ); \
28 BOOST_REQUIRE_NE( REQ->lastRequestError().type(), zyppng::NetworkRequestError::NoError ); \
29 BOOST_REQUIRE( !REQ->errorString().empty() ); \
32 namespace bdata = boost::unit_test::data;
34 bool withSSL[] = {true, false};
36 //read all contents of a file into a string)
37 std::string readFile ( const zypp::Pathname &file )
39 if ( ! zypp::PathInfo( file ).isFile() ) {
42 std::ifstream istr( file.asString().c_str() );
47 std::string str((std::istreambuf_iterator<char>(istr)),
48 std::istreambuf_iterator<char>());
53 BOOST_DATA_TEST_CASE( dltest_basic, bdata::make( withSSL ), withSSL)
55 auto ev = zyppng::EventDispatcher::createMain();
57 zyppng::Downloader downloader;
59 //make sure the data here is big enough to cross the threshold of 256 bytes so we get a progress signal emitted and not only the alive signal.
60 std::string dummyContent = "This is just some dummy content,\nto test downloading and signals.\n"
61 "This is just some dummy content,\nto test downloading and signals.\n"
62 "This is just some dummy content,\nto test downloading and signals.\n"
63 "This is just some dummy content,\nto test downloading and signals.\n";
65 WebServer web((zypp::Pathname(TESTS_SRC_DIR)/"data"/"dummywebroot").c_str(), 10001, withSSL );
66 web.addRequestHandler("getData", WebServer::makeResponse("200", dummyContent ) );
67 BOOST_REQUIRE( web.start() );
69 zypp::filesystem::TmpFile targetFile;
70 zyppng::Url weburl (web.url());
71 weburl.setPathName("/handler/getData");
72 zyppng::TransferSettings set = web.transferSettings();
74 bool gotStarted = false;
75 bool gotFinished = false;
76 bool gotProgress = false;
77 bool gotAlive = false;
78 off_t lastProgress = 0;
81 std::vector<zyppng::Download::State> allStates;
84 zyppng::Download::Ptr dl = downloader.downloadFile( weburl, targetFile.path(), dummyContent.length() );
87 dl->sigFinished().connect([&]( zyppng::Download & ){
92 dl->sigStarted().connect([&]( zyppng::Download & ){
96 dl->sigAlive().connect([&]( zyppng::Download &, off_t ){
100 dl->sigStateChanged().connect([&]( zyppng::Download &, zyppng::Download::State state ){
101 allStates.push_back( state );
104 dl->sigProgress().connect([&]( zyppng::Download &, off_t dltotal, off_t dlnow ){
106 lastProgress = dlnow;
112 BOOST_TEST_REQ_SUCCESS( dl );
113 BOOST_REQUIRE( gotStarted );
114 BOOST_REQUIRE( gotFinished );
115 BOOST_REQUIRE( gotProgress );
116 BOOST_REQUIRE( gotAlive );
117 BOOST_REQUIRE_EQUAL( totalDL, dummyContent.length() );
118 BOOST_REQUIRE_EQUAL( lastProgress, dummyContent.length() );
119 BOOST_REQUIRE ( allStates == std::vector<zyppng::Download::State>({zyppng::Download::Initializing,zyppng::Download::Running, zyppng::Download::Success}) );
125 std::string name; //<dataset name, used only in debug output if the test fails
126 std::string handlerPath; //< the webhandler path used to query the resource
127 std::vector< std::pair<int, std::string> > mirrors; //all mirrors injected into the metalink file
128 int expectedFileDownloads; //< how many downloads are direct file downloads
129 int expectedHandlerDownloads; //< how many started downloads are handler requests
130 std::vector<zyppng::Download::State> expectedStates;
131 bool expectSuccess; //< should the download work out?
133 std::ostream & operator<<( std::ostream & str ) const {
134 str << "MirrorSet{ " << name << " }";
139 namespace boost{ namespace test_tools{ namespace tt_detail{
141 struct print_log_value< MirrorSet > {
142 void operator()( std::ostream& str, MirrorSet const& set) {
143 set.operator<<( str );
148 std::vector< MirrorSet > generateMirr ()
150 std::vector< MirrorSet > res;
153 res.push_back( MirrorSet() );
154 res.back().name = "All good mirrors";
155 res.back().expectSuccess = true;
156 res.back().expectedHandlerDownloads = 1;
157 res.back().expectedFileDownloads = 9;
158 res.back().expectedStates = {zyppng::Download::Initializing, zyppng::Download::RunningMulti, zyppng::Download::Success};
159 for ( int i = 100 ; i >= 10; i -= 10 )
160 res.back().mirrors.push_back( std::make_pair( i, "/test.txt") );
163 res.push_back( MirrorSet() );
164 res.back().name = "Empty mirrors";
165 res.back().expectSuccess = true;
166 res.back().expectedHandlerDownloads = 10;
167 res.back().expectedFileDownloads = 0;
168 res.back().expectedStates = {zyppng::Download::Initializing, zyppng::Download::RunningMulti, zyppng::Download::Success};
170 //only broken mirrors:
171 res.push_back( MirrorSet() );
172 res.back().name = "All broken mirrors";
173 res.back().expectSuccess = true;
174 res.back().expectedHandlerDownloads = 2; //has to fall back to url handler download
175 res.back().expectedFileDownloads = 10; //should try all mirrors and fail
176 res.back().expectedStates = {zyppng::Download::Initializing, zyppng::Download::RunningMulti, zyppng::Download::Running, zyppng::Download::Success};
177 for ( int i = 100 ; i >= 10; i -= 10 )
178 res.back().mirrors.push_back( std::make_pair( i, "/doesnotexist.txt") );
180 //some broken mirrors:
181 res.push_back( MirrorSet() );
182 res.back().name = "Some broken mirrors less URLs than blocks";
183 res.back().expectSuccess = true;
184 res.back().expectedHandlerDownloads = 1;
185 res.back().expectedFileDownloads = 9 + 3; // 3 should fail due to broken mirrors
186 res.back().expectedStates = {zyppng::Download::Initializing, zyppng::Download::RunningMulti, zyppng::Download::Success};
187 for ( int i = 10 ; i >= 5; i-- ) {
189 res.back().mirrors.push_back( std::make_pair( i*10, "/doesnotexist.txt") );
191 res.back().mirrors.push_back( std::make_pair( i*10, "/test.txt") );
195 //some broken mirrors with more URLs than blocks:
196 res.push_back( MirrorSet() );
197 res.back().name = "Some broken mirrors more URLs than blocks";
198 res.back().expectSuccess = true;
199 res.back().expectedHandlerDownloads = 1;
200 //its not really possible to know how many times the downloads will fail, there are
201 //5 broken mirrors in the set, but if a working mirror is done before the last broken
202 //URL is picked from the dataset not all broken URLs will be used
203 res.back().expectedFileDownloads = -1;
204 res.back().expectedStates = {zyppng::Download::Initializing, zyppng::Download::RunningMulti, zyppng::Download::Success};
205 for ( int i = 10 ; i >= 1; i-- ) {
207 res.back().mirrors.push_back( std::make_pair( i*10, "/doesnotexist.txt") );
209 res.back().mirrors.push_back( std::make_pair( i*10, "/test.txt") );
213 //mirrors where some return a invalid block
214 res.push_back( MirrorSet() );
215 res.back().name = "Some mirrors return broken blocks";
216 res.back().expectSuccess = true;
217 res.back().expectedHandlerDownloads = 1;
218 //its not really possible to know how many times the downloads will fail, there are
219 //5 broken mirrors in the set, but if a working mirror is done before the last broken
220 //URL is picked from the dataset not all broken URLs will be used
221 res.back().expectedFileDownloads = -1;
222 res.back().expectedStates = {zyppng::Download::Initializing, zyppng::Download::RunningMulti, zyppng::Download::Success};
223 for ( int i = 10 ; i >= 1; i-- ) {
225 res.back().mirrors.push_back( std::make_pair( i*10, "/handler/random") );
227 res.back().mirrors.push_back( std::make_pair( i*10, "/test.txt") );
234 //create one URL line for a metalink template file
235 std::string makeUrl ( int pref, const zyppng::Url &url )
237 return ( zypp::str::Format( "<url preference=\"%1%\" location=\"de\" type=\"%2%\">%3%</url>" ) % pref % url.getScheme() % url );
241 static bool requestWantsMetaLink ( const WebServer::Request &req )
243 auto it = req.params.find( "HTTP_ACCEPT" );
244 if ( it != req.params.end() ) {
245 if ( (*it).second.find("application/metalink+xml") != std::string::npos ||
246 (*it).second.find("application/metalink4+xml") != std::string::npos ) {
253 //creates a request handler for the Mock WebServer that returns the metalink data
254 //specified in \a data if the request has the metalink accept handler
255 WebServer::RequestHandler makeMetaFileHandler ( const std::string *data )
257 return [ data ]( WebServer::Request &req ){
258 if ( requestWantsMetaLink( req ) ) {
259 req.rout << WebServer::makeResponseString( "200", { "Content-Type: application/metalink+xml; charset=utf-8\r\n" }, *data );
262 req.rout << "Location: /test.txt\r\n\r\n";
267 //creates a request handler for the Mock WebServer that returns a junk block of
268 //data for a range request, otherwise relocates the request
269 WebServer::RequestHandler makeJunkBlockHandler ( )
271 return [ ]( WebServer::Request &req ){
272 auto it = req.params.find( "HTTP_RANGE" );
273 if ( it != req.params.end() && zypp::str::startsWith( it->second, "bytes=" ) ) {
274 //bytes=786432-1048575
275 std::string range = it->second.substr( 6 ); //remove bytes=
276 size_t dash = range.find_first_of( "-" );
279 if ( dash != std::string::npos ) {
280 start = std::stoll( range.substr( 0, dash ) );
281 end = std::stoll( range.substr( dash+1 ) );
284 if ( start != -1 && end != -1 ) {
286 for ( off_t curr = 0; curr < ( end - start); curr++ ) {
289 req.rout << "Status: 206 Partial Content\r\n"
290 << "Accept-Ranges: bytes\r\n"
291 << "Content-Length: "<<( end - start)<<"\r\n\r\n"
296 req.rout << "Location: /test.txt\r\n\r\n";
301 int maxConcurrentDLs[] = { 1, 2, 4, 8, 10, 15 };
303 BOOST_DATA_TEST_CASE( test1, bdata::make( generateMirr() ) * bdata::make( withSSL ) * bdata::make( maxConcurrentDLs ) , elem, withSSL, maxDLs )
305 //each URL in the metalink file has a preference , a schema and of course the URL, we need to adapt those to our test setup
306 //so we generate the file on the fly from a template in the test data
307 std::string metaTempl = readFile ( zypp::Pathname(TESTS_SRC_DIR)/"/zyppng/data/downloader/test.txt.meta" );
308 BOOST_REQUIRE( !metaTempl.empty() );
310 auto ev = zyppng::EventDispatcher::createMain();
312 WebServer web((zypp::Pathname(TESTS_SRC_DIR)/"/zyppng/data/downloader").c_str(), 10001, withSSL );
313 BOOST_REQUIRE( web.start() );
315 zypp::filesystem::TmpFile targetFile;
316 zyppng::Downloader downloader;
317 downloader.requestDispatcher()->setMaximumConcurrentConnections( maxDLs );
319 //first metalink download, generate a fully valid one
320 zyppng::Url weburl (web.url());
321 weburl.setPathName("/handler/test.txt");
324 if ( elem.mirrors.size() ) {
325 for ( const auto &mirr : elem.mirrors ) {
326 zyppng::Url mirrUrl (web.url());
327 mirrUrl.setPathName( mirr.second );
328 urls += makeUrl( mirr.first, mirrUrl ) + "\n";
332 std::string metaFile = zypp::str::Format( metaTempl ) % urls;
333 web.addRequestHandler("test.txt", makeMetaFileHandler( &metaFile ) );
334 web.addRequestHandler("random", makeJunkBlockHandler( ) );
336 int expectedDownloads = elem.expectedHandlerDownloads + elem.expectedFileDownloads;
337 int startedDownloads = 0;
338 int finishedDownloads = 0;
339 bool downloadHadError = false;
340 bool gotProgress = false;
341 bool gotAlive = false;
342 bool gotMultiDLState = false;
343 std::vector<zyppng::Download::State> allStates;
345 off_t lastProgress = 0;
347 int countHandlerReq = 0; //the requests made to the handler slot
348 int countFileReq = 0; //the requests made to the file directly, e.g. a mirror read from the metalink file
350 auto dl = downloader.downloadFile( weburl, targetFile );
351 dl->settings() = web.transferSettings();
353 dl->dispatcher().sigDownloadStarted().connect( [&]( zyppng::NetworkRequestDispatcher &, zyppng::NetworkRequest &req){
355 if ( req.url() == weburl )
361 dl->dispatcher().sigDownloadFinished().connect( [&]( zyppng::NetworkRequestDispatcher &, zyppng::NetworkRequest &req ){
364 if ( !downloadHadError )
365 downloadHadError = req.hasError();
368 dl->sigFinished().connect([&]( zyppng::Download & ){
372 dl->sigAlive().connect([&]( zyppng::Download &, off_t dlnow ){
374 lastProgress = dlnow;
377 dl->sigProgress().connect([&]( zyppng::Download &, off_t dltotal, off_t dlnow ){
380 lastProgress = dlnow;
383 dl->sigStateChanged().connect([&]( zyppng::Download &, zyppng::Download::State state ){
384 if ( state == zyppng::Download::RunningMulti )
385 gotMultiDLState = true;
388 dl->sigStateChanged().connect([&]( zyppng::Download &, zyppng::Download::State state ){
389 allStates.push_back( state );
395 //std::cout << dl->errorString() << std::endl;
397 if ( elem.expectSuccess )
398 BOOST_TEST_REQ_SUCCESS( dl );
400 BOOST_TEST_REQ_FAILED ( dl );
402 if ( elem.expectedHandlerDownloads > -1 && elem.expectedFileDownloads > -1 )
403 BOOST_REQUIRE_EQUAL( startedDownloads, expectedDownloads );
405 BOOST_REQUIRE_EQUAL( startedDownloads, finishedDownloads );
406 BOOST_REQUIRE( gotAlive );
407 BOOST_REQUIRE( gotProgress );
408 BOOST_REQUIRE( gotMultiDLState );
409 BOOST_REQUIRE_EQUAL( lastProgress, 2148018 );
410 BOOST_REQUIRE_EQUAL( lastProgress, gotTotal );
412 if ( elem.expectedHandlerDownloads > -1 )
413 BOOST_REQUIRE_EQUAL( countHandlerReq, elem.expectedHandlerDownloads );
415 if ( elem.expectedFileDownloads > -1 )
416 BOOST_REQUIRE_EQUAL( countFileReq, elem.expectedFileDownloads );
418 if ( !elem.expectedStates.empty() )
419 BOOST_REQUIRE( elem.expectedStates == allStates );
424 // - correct expected filesize
425 // - invalid filesize
426 // - password handling and propagation
429 //creates a request handler that requires a authentication to work
430 WebServer::RequestHandler createAuthHandler ( )
432 return [ ]( WebServer::Request &req ){
434 auto it = req.params.find( "HTTP_AUTHORIZATION" );
435 bool authorized = false;
436 if ( it != req.params.end() )
437 authorized = ( it->second == "Basic dGVzdDp0ZXN0" );
440 req.rout << "Status: 401 Unauthorized\r\n"
441 "Content-Type: text/html; charset=utf-8\r\n"
442 "WWW-Authenticate: Basic realm=\"User Visible Realm\", charset=\"UTF-8\" \r\n"
444 "Sorry you are not authorized.";
448 if ( requestWantsMetaLink( req ) ) {
449 it = req.params.find( "HTTPS" );
450 if ( it != req.params.end() && it->second == "on" ) {
451 req.rout << "Status: 307 Temporary Redirect\r\n"
452 << "Cache-Control: no-cache\r\n"
453 << "Location: /auth-https.meta\r\n\r\n";
455 req.rout << "Status: 307 Temporary Redirect\r\n"
456 << "Cache-Control: no-cache\r\n"
457 << "Location: /auth-http.meta\r\n\r\n";
461 req.rout << "Status: 307 Temporary Redirect\r\n"
462 "Location: /test.txt\r\n\r\n";
467 BOOST_DATA_TEST_CASE( dltest_auth, bdata::make( withSSL ), withSSL )
469 //don't write or read creds from real settings dir
470 zypp::filesystem::TmpDir repoManagerRoot;
471 zypp::ZConfig::instance().setRepoManagerRoot( repoManagerRoot.path() );
473 auto ev = zyppng::EventDispatcher::createMain();
475 zyppng::Downloader downloader;
477 WebServer web((zypp::Pathname(TESTS_SRC_DIR)/"/zyppng/data/downloader").c_str(), 10001, withSSL );
478 BOOST_REQUIRE( web.start() );
480 zypp::filesystem::TmpFile targetFile;
481 zyppng::Url weburl (web.url());
482 weburl.setPathName("/handler/test.txt");
483 zyppng::TransferSettings set = web.transferSettings();
485 web.addRequestHandler( "test.txt", createAuthHandler() );
486 web.addRequestHandler( "quit", [ &ev ]( WebServer::Request & ){ ev->quit();} );
489 auto dl = downloader.downloadFile( weburl, targetFile.path() );
490 dl->settings() = set;
492 dl->sigFinished( ).connect([ &ev ]( zyppng::Download & ){
498 BOOST_TEST_REQ_FAILED( dl );
499 BOOST_REQUIRE_EQUAL( dl->lastRequestError().type(), zyppng::NetworkRequestError::Unauthorized );
503 auto dl = downloader.downloadFile( weburl, targetFile.path() );
504 dl->settings() = set;
506 int gotAuthRequest = 0;
508 dl->sigFinished( ).connect([ &ev ]( zyppng::Download & ){
512 dl->sigAuthRequired().connect( [&]( zyppng::Download &, zyppng::NetworkAuthData &auth, const std::string &availAuth ){
514 if ( gotAuthRequest >= 2 )
516 auth.setUsername("wrong");
517 auth.setPassword("credentials");
522 BOOST_TEST_REQ_FAILED( dl );
523 BOOST_REQUIRE_EQUAL( gotAuthRequest, 2 );
524 BOOST_REQUIRE_EQUAL( dl->lastRequestError().type(), zyppng::NetworkRequestError::AuthFailed );
528 int gotAuthRequest = 0;
529 std::vector<zyppng::Download::State> allStates;
530 auto dl = downloader.downloadFile( weburl, targetFile.path() );
531 dl->settings() = set;
533 dl->sigFinished( ).connect([ &ev ]( zyppng::Download & ){
537 dl->sigAuthRequired().connect( [&]( zyppng::Download &, zyppng::NetworkAuthData &auth, const std::string &availAuth ){
539 auth.setUsername("test");
540 auth.setPassword("test");
543 dl->sigStateChanged().connect([&]( zyppng::Download &, zyppng::Download::State state ){
544 allStates.push_back( state );
550 BOOST_TEST_REQ_SUCCESS( dl );
551 BOOST_REQUIRE_EQUAL( gotAuthRequest, 1 );
552 BOOST_REQUIRE ( allStates == std::vector<zyppng::Download::State>({zyppng::Download::Initializing, zyppng::Download::RunningMulti, zyppng::Download::Success}) );
556 //the creds should be in the credential manager now, we should not need to specify them again in the slot
557 bool gotAuthRequest = false;
558 auto dl = downloader.downloadFile( weburl, targetFile.path() );
559 dl->settings() = set;
561 dl->sigFinished( ).connect([ &ev ]( zyppng::Download & ){
565 dl->sigAuthRequired().connect( [&]( zyppng::Download &, zyppng::NetworkAuthData &auth, const std::string &availAuth ){
566 gotAuthRequest = true;
571 BOOST_TEST_REQ_SUCCESS( dl );
572 BOOST_REQUIRE( !gotAuthRequest );
577 * Test for bsc#1174011 auth=basic ignored in some cases
579 * If the URL specifes ?auth=basic libzypp should proactively send credentials we have available in the cred store
581 BOOST_DATA_TEST_CASE( dltest_auth_basic, bdata::make( withSSL ), withSSL )
583 //don't write or read creds from real settings dir
584 zypp::filesystem::TmpDir repoManagerRoot;
585 zypp::ZConfig::instance().setRepoManagerRoot( repoManagerRoot.path() );
587 auto ev = zyppng::EventDispatcher::createMain();
589 zyppng::Downloader downloader;
591 WebServer web((zypp::Pathname(TESTS_SRC_DIR)/"/zyppng/data/downloader").c_str(), 10001, withSSL );
592 BOOST_REQUIRE( web.start() );
594 zypp::filesystem::TmpFile targetFile;
595 zyppng::Url weburl (web.url());
596 weburl.setPathName("/handler/test.txt");
597 weburl.setQueryParam("auth", "basic");
598 weburl.setUsername("test");
600 // make sure the creds are already available
601 zypp::media::CredentialManager cm( repoManagerRoot.path() );
602 zypp::media::AuthData data ("test", "test");
603 data.setUrl( weburl );
607 zyppng::TransferSettings set = web.transferSettings();
609 web.addRequestHandler( "test.txt", createAuthHandler() );
610 web.addRequestHandler( "quit", [ &ev ]( WebServer::Request & ){ ev->quit();} );
613 // simply check by request count if the test was successfull:
614 // if the proactive code adding the credentials to the first request is not executed we will
615 // have more than 1 request.
617 auto dispatcher = downloader.requestDispatcher();
618 dispatcher->sigDownloadStarted().connect([&]( zyppng::NetworkRequestDispatcher &, zyppng::NetworkRequest & ){
623 auto dl = downloader.downloadFile( weburl, targetFile.path() );
624 dl->setMultiPartHandlingEnabled( false );
626 dl->settings() = set;
628 dl->sigFinished( ).connect([ &ev ]( zyppng::Download & ){
634 BOOST_TEST_REQ_SUCCESS( dl );
635 BOOST_REQUIRE_EQUAL( reqCount, 1 );