1 /*---------------------------------------------------------------------\
3 | |__ / \ / / . \ . \ |
8 \---------------------------------------------------------------------*/
9 /** \file zypp/media/MediaCurl.cc
15 #include "zypp/base/Logger.h"
16 #include "zypp/ExternalProgram.h"
17 #include "zypp/base/String.h"
19 #include "zypp/media/MediaCurl.h"
20 #include "zypp/media/proxyinfo/ProxyInfos.h"
21 #include "zypp/media/ProxyInfo.h"
22 #include "zypp/thread/Once.h"
24 #include <sys/types.h>
26 #include <sys/mount.h>
33 using namespace zypp::base;
37 zypp::thread::OnceFlag g_InitOnceFlag = PTHREAD_ONCE_INIT;
38 zypp::thread::OnceFlag g_FreeOnceFlag = PTHREAD_ONCE_INIT;
40 extern "C" void _do_free_once()
42 curl_global_cleanup();
45 extern "C" void globalFreeOnce()
47 zypp::thread::callOnce(g_FreeOnceFlag, _do_free_once);
50 extern "C" void _do_init_once()
52 CURLcode ret = curl_global_init( CURL_GLOBAL_ALL );
55 WAR << "curl global init failed" << endl;
59 // register at exit handler ?
60 // this may cause trouble, because we can protect it
61 // against ourself only.
62 // if the app sets an atexit handler as well, it will
63 // cause a double free while the second of them runs.
65 //std::atexit( globalFreeOnce);
68 inline void globalInitOnce()
70 zypp::thread::callOnce(g_InitOnceFlag, _do_init_once);
77 Pathname MediaCurl::_cookieFile = "/var/lib/YaST2/cookies";
78 std::string MediaCurl::_agent = "Novell ZYPP Installer";
80 ///////////////////////////////////////////////////////////////////
82 static inline void escape( string & str_r,
83 const char char_r, const string & escaped_r ) {
84 for ( string::size_type pos = str_r.find( char_r );
85 pos != string::npos; pos = str_r.find( char_r, pos ) ) {
86 str_r.replace( pos, 1, escaped_r );
90 static inline string escapedPath( string path_r ) {
91 escape( path_r, ' ', "%20" );
95 static inline string unEscape( string text_r ) {
96 char * tmp = curl_unescape( text_r.c_str(), 0 );
102 ///////////////////////////////////////////////////////////////////
104 // CLASS NAME : MediaCurl
106 ///////////////////////////////////////////////////////////////////
108 MediaCurl::MediaCurl( const Url & url_r,
109 const Pathname & attach_point_hint_r )
110 : MediaHandler( url_r, attach_point_hint_r,
111 "/", // urlpath at attachpoint
112 true ), // does_download
115 MIL << "MediaCurl::MediaCurl(" << url_r << ", " << attach_point_hint_r << ")" << endl;
119 if( !attachPoint().empty())
121 PathInfo ainfo(attachPoint());
122 Pathname apath(attachPoint() + "XXXXXX");
123 char *atemp = ::strdup( apath.asString().c_str());
125 if( !ainfo.isDir() || !ainfo.userMayRWX() ||
126 atemp == NULL || (atest=::mkdtemp(atemp)) == NULL)
128 WAR << "attach point " << ainfo.path()
129 << " is not useable for " << url_r.getScheme() << endl;
130 setAttachPoint("", true);
132 else if( atest != NULL)
140 void MediaCurl::setCookieFile( const Pathname &fileName )
142 _cookieFile = fileName;
145 ///////////////////////////////////////////////////////////////////
148 // METHOD NAME : MediaCurl::attachTo
149 // METHOD TYPE : PMError
151 // DESCRIPTION : Asserted that not already attached, and attachPoint is a directory.
153 void MediaCurl::attachTo (bool next)
156 ZYPP_THROW(MediaNotSupportedException(_url));
158 if ( !_url.isValid() )
159 ZYPP_THROW(MediaBadUrlException(_url));
161 curl_version_info_data *curl_info = NULL;
162 curl_info = curl_version_info(CURLVERSION_NOW);
163 // curl_info does not need any free (is static)
164 if (curl_info->protocols)
166 const char * const *proto;
167 std::string scheme( _url.getScheme());
169 for(proto=curl_info->protocols; !found && *proto; ++proto)
171 if( scheme == std::string((const char *)*proto))
176 std::string msg("Unsupported protocol '");
179 ZYPP_THROW(MediaBadUrlException(_url, msg));
183 if( !isUseableAttachPoint(attachPoint()))
185 std::string mountpoint = createAttachPoint().asString();
187 if( mountpoint.empty())
188 ZYPP_THROW( MediaBadAttachPointException(url()));
190 setAttachPoint( mountpoint, true);
193 disconnectFrom(); // clean _curl if needed
194 _curl = curl_easy_init();
196 ZYPP_THROW(MediaCurlInitException(_url));
199 CURLcode ret = curl_easy_setopt( _curl, CURLOPT_ERRORBUFFER, _curlError );
202 ZYPP_THROW(MediaCurlSetOptException(_url, "Error setting error buffer"));
205 ret = curl_easy_setopt( _curl, CURLOPT_FAILONERROR, true );
208 ZYPP_THROW(MediaCurlSetOptException(_url, _curlError));
211 ret = curl_easy_setopt( _curl, CURLOPT_NOSIGNAL, 1 );
214 ZYPP_THROW(MediaCurlSetOptException(_url, _curlError));
218 ** Don't block "forever" on system calls. Curl seems to
219 ** recover nicely, if the ftp server has e.g. a 30sec
220 ** timeout. If required, it closes the connection, trys
221 ** to reopen and fetch it - this works in many cases
222 ** without to report any error to us.
224 ** Disabled, because it breaks normal operations over a
227 ret = curl_easy_setopt( _curl, CURLOPT_TIMEOUT, 600 );
230 ZYPP_THROW(MediaCurlSetOptException(_url, _curlError));
234 if ( _url.getScheme() == "http" ) {
235 // follow any Location: header that the server sends as part of
236 // an HTTP header (#113275)
237 ret = curl_easy_setopt ( _curl, CURLOPT_FOLLOWLOCATION, true );
240 ZYPP_THROW(MediaCurlSetOptException(_url, _curlError));
242 ret = curl_easy_setopt ( _curl, CURLOPT_MAXREDIRS, 3L );
245 ZYPP_THROW(MediaCurlSetOptException(_url, _curlError));
247 ret = curl_easy_setopt ( _curl, CURLOPT_USERAGENT, _agent.c_str() );
250 ZYPP_THROW(MediaCurlSetOptException(_url, _curlError));
254 if ( _url.getScheme() == "https" ) {
255 ret = curl_easy_setopt( _curl, CURLOPT_SSL_VERIFYPEER, 1 );
258 ZYPP_THROW(MediaCurlSetOptException(_url, _curlError));
260 ret = curl_easy_setopt( _curl, CURLOPT_CAPATH, "/etc/ssl/certs/" );
263 ZYPP_THROW(MediaCurlSetOptException(_url, _curlError));
265 ret = curl_easy_setopt( _curl, CURLOPT_SSL_VERIFYHOST, 2 );
268 ZYPP_THROW(MediaCurlSetOptException(_url, _curlError));
270 ret = curl_easy_setopt ( _curl, CURLOPT_USERAGENT, _agent.c_str() );
273 ZYPP_THROW(MediaCurlSetOptException(_url, _curlError));
278 /*---------------------------------------------------------------*
279 CURLOPT_USERPWD: [user name]:[password]
281 Url::username/password -> CURLOPT_USERPWD
282 If not provided, anonymous FTP identification
283 *---------------------------------------------------------------*/
285 if ( _url.getUsername().empty() ) {
286 if ( _url.getScheme() == "ftp" ) {
287 string id = "yast2@";
289 DBG << "Anonymous FTP identification: '" << id << "'" << endl;
290 _userpwd = "anonymous:" + id;
293 _userpwd = _url.getUsername();
294 if ( _url.getPassword().size() ) {
295 _userpwd += ":" + _url.getPassword();
299 if ( _userpwd.size() ) {
300 _userpwd = unEscape( _userpwd );
301 ret = curl_easy_setopt( _curl, CURLOPT_USERPWD, _userpwd.c_str() );
304 ZYPP_THROW(MediaCurlSetOptException(_url, _curlError));
307 if( !_url.getQueryParam("auth").empty() &&
308 (_url.getScheme() == "http" || _url.getScheme() == "https"))
310 std::vector<std::string> list;
311 std::vector<std::string>::const_iterator it;
312 str::split(_url.getQueryParam("auth"), std::back_inserter(list), ",");
314 long auth = CURLAUTH_NONE;
315 for(it = list.begin(); it != list.end(); ++it)
319 auth |= CURLAUTH_BASIC;
324 auth |= CURLAUTH_DIGEST;
327 if((curl_info && (curl_info->features & CURL_VERSION_NTLM)) &&
330 auth |= CURLAUTH_NTLM;
333 if((curl_info && (curl_info->features & CURL_VERSION_SPNEGO)) &&
334 (*it == "spnego" || *it == "negotiate"))
336 // there is no separate spnego flag for auth
337 auth |= CURLAUTH_GSSNEGOTIATE;
340 if((curl_info && (curl_info->features & CURL_VERSION_GSSNEGOTIATE)) &&
341 (*it == "gssnego" || *it == "negotiate"))
343 auth |= CURLAUTH_GSSNEGOTIATE;
347 std::string msg("Unsupported HTTP authentication method '");
351 ZYPP_THROW(MediaBadUrlException(_url, msg));
355 if( auth != CURLAUTH_NONE)
357 DBG << "Enabling HTTP authentication methods: "
358 << _url.getQueryParam("auth") << std::endl;
360 ret = curl_easy_setopt( _curl, CURLOPT_HTTPAUTH, auth);
363 ZYPP_THROW(MediaCurlSetOptException(_url, _curlError));
369 /*---------------------------------------------------------------*
370 CURLOPT_PROXY: host[:port]
372 Url::option(proxy and proxyport) -> CURLOPT_PROXY
373 If not provided, /etc/sysconfig/proxy is evaluated
374 *---------------------------------------------------------------*/
376 _proxy = _url.getQueryParam( "proxy" );
378 if ( ! _proxy.empty() ) {
379 string proxyport( _url.getQueryParam( "proxyport" ) );
380 if ( ! proxyport.empty() ) {
381 _proxy += ":" + proxyport;
385 ProxyInfo proxy_info (ProxyInfo::ImplPtr(new ProxyInfoSysconfig("proxy")));
387 if ( proxy_info.enabled())
389 bool useproxy = true;
391 std::list<std::string> nope = proxy_info.noProxy();
392 for (ProxyInfo::NoProxyIterator it = proxy_info.noProxyBegin();
393 it != proxy_info.noProxyEnd();
396 std::string host( str::toLower(_url.getHost()));
397 std::string temp( str::toLower(*it));
399 // no proxy if it points to a suffix
400 // preceeded by a '.', that maches
401 // the trailing portion of the host.
402 if( temp.size() > 1 && temp.at(0) == '.')
404 if(host.size() > temp.size() &&
405 host.compare(host.size() - temp.size(), temp.size(), temp) == 0)
407 DBG << "NO_PROXY: '" << *it << "' matches host '"
408 << host << "'" << endl;
414 // no proxy if we have an exact match
417 DBG << "NO_PROXY: '" << *it << "' matches host '"
418 << host << "'" << endl;
425 _proxy = proxy_info.proxy(_url.getScheme());
431 DBG << "Proxy: " << (_proxy.empty() ? "-none-" : _proxy) << endl;
433 if ( ! _proxy.empty() ) {
435 ret = curl_easy_setopt( _curl, CURLOPT_PROXY, _proxy.c_str() );
438 ZYPP_THROW(MediaCurlSetOptException(_url, _curlError));
441 /*---------------------------------------------------------------*
442 CURLOPT_PROXYUSERPWD: [user name]:[password]
444 Url::option(proxyuser and proxypassword) -> CURLOPT_PROXYUSERPWD
445 If not provided, $HOME/.curlrc is evaluated
446 *---------------------------------------------------------------*/
448 _proxyuserpwd = _url.getQueryParam( "proxyuser" );
450 if ( ! _proxyuserpwd.empty() ) {
452 string proxypassword( _url.getQueryParam( "proxypassword" ) );
453 if ( ! proxypassword.empty() ) {
454 _proxyuserpwd += ":" + proxypassword;
459 string curlrcFile = string( getenv("HOME") ) + string( "/.curlrc" );
460 map<string,string> rc_data
461 = proxyinfo::sysconfigRead(Pathname(curlrcFile));
462 map<string,string>::const_iterator it = rc_data.find("proxy-user");
463 if (it != rc_data.end())
464 _proxyuserpwd = it->second;
467 _proxyuserpwd = unEscape( _proxyuserpwd );
468 ret = curl_easy_setopt( _curl, CURLOPT_PROXYUSERPWD, _proxyuserpwd.c_str() );
471 ZYPP_THROW(MediaCurlSetOptException(_url, _curlError));
475 /*---------------------------------------------------------------*
476 *---------------------------------------------------------------*/
478 _currentCookieFile = _cookieFile.asString();
480 ret = curl_easy_setopt( _curl, CURLOPT_COOKIEFILE,
481 _currentCookieFile.c_str() );
484 ZYPP_THROW(MediaCurlSetOptException(_url, _curlError));
487 ret = curl_easy_setopt( _curl, CURLOPT_COOKIEJAR,
488 _currentCookieFile.c_str() );
491 ZYPP_THROW(MediaCurlSetOptException(_url, _curlError));
494 ret = curl_easy_setopt( _curl, CURLOPT_PROGRESSFUNCTION,
498 ZYPP_THROW(MediaCurlSetOptException(_url, _curlError));
501 ret = curl_easy_setopt( _curl, CURLOPT_NOPROGRESS, false );
504 ZYPP_THROW(MediaCurlSetOptException(_url, _curlError));
507 // FIXME: need a derived class to propelly compare url's
508 MediaSourceRef media( new MediaSource(_url.getScheme(), _url.asString()));
509 setMediaSource(media);
513 MediaCurl::checkAttachPoint(const Pathname &apoint) const
515 return MediaHandler::checkAttachPoint( apoint, true, true);
518 ///////////////////////////////////////////////////////////////////
521 // METHOD NAME : MediaCurl::disconnectFrom
522 // METHOD TYPE : PMError
524 void MediaCurl::disconnectFrom()
528 curl_easy_cleanup( _curl );
533 ///////////////////////////////////////////////////////////////////
536 // METHOD NAME : MediaCurl::releaseFrom
537 // METHOD TYPE : PMError
539 // DESCRIPTION : Asserted that media is attached.
541 void MediaCurl::releaseFrom( bool eject )
547 ///////////////////////////////////////////////////////////////////
549 // METHOD NAME : MediaCurl::getFile
550 // METHOD TYPE : PMError
553 void MediaCurl::getFile( const Pathname & filename ) const
555 // Use absolute file name to prevent access of files outside of the
556 // hierarchy below the attach point.
557 getFileCopy(filename, localPath(filename).absolutename());
561 void MediaCurl::getFileCopy( const Pathname & filename , const Pathname & target) const
563 callback::SendReport<DownloadProgressReport> report;
568 doGetFileCopy(filename, target, report);
570 catch (MediaException & excpt_r)
572 // FIXME: this will not match the first URL
573 // FIXME: error number fix
574 report->finish(url, zypp::media::DownloadProgressReport::NOT_FOUND, excpt_r.msg());
575 ZYPP_RETHROW(excpt_r);
577 report->finish(url, zypp::media::DownloadProgressReport::NO_ERROR, "");
580 void MediaCurl::doGetFileCopy( const Pathname & filename , const Pathname & target, callback::SendReport<DownloadProgressReport> & report) const
582 DBG << filename.asString() << endl;
585 ZYPP_THROW(MediaBadUrlException(_url));
587 if(_url.getHost().empty())
588 ZYPP_THROW(MediaBadUrlEmptyHostException(_url));
590 string path = _url.getPathName();
591 if ( !path.empty() && path != "/" && *path.rbegin() == '/' &&
592 filename.absolute() ) {
593 // If url has a path with trailing slash, remove the leading slash from
594 // the absolute file name
595 path += filename.asString().substr( 1, filename.asString().size() - 1 );
596 } else if ( filename.relative() ) {
597 // Add trailing slash to path, if not already there
598 if ( !path.empty() && *path.rbegin() != '/' ) path += "/";
599 // Remove "./" from begin of relative file name
600 path += filename.asString().substr( 2, filename.asString().size() - 2 );
602 path += filename.asString();
606 url.setPathName( path );
608 Pathname dest = target.absolutename();
609 if( assert_dir( dest.dirname() ) )
611 DBG << "assert_dir " << dest.dirname() << " failed" << endl;
612 ZYPP_THROW( MediaSystemException(_url, "System error on " + dest.dirname().asString()) );
615 DBG << "URL: " << url.asString().c_str() << endl;
616 // Use URL without options (not RFC conform) and without
617 // username and passwd (some proxies dislike them in the URL.
618 // Curloptions for these were set in attachTo().
620 curlUrl.setUsername( "" );
621 curlUrl.setPassword( "" );
622 #warning Check whether the call is correct
623 // string urlBuffer = curlUrl.asString(true,false,true); // without options
624 string urlBuffer = curlUrl.asString(); // without options
626 CURLcode ret = curl_easy_setopt( _curl, CURLOPT_URL,
629 ZYPP_THROW(MediaCurlSetOptException(_url, _curlError));
632 string destNew = target.asString() + ".new.zypp.XXXXXX";
633 char *buf = ::strdup( destNew.c_str());
636 ERR << "out of memory for temp file name" << endl;
637 ZYPP_THROW(MediaSystemException(
638 _url, "out of memory for temp file name"
642 int tmp_fd = ::mkstemp( buf );
646 ERR << "mkstemp failed for file '" << destNew << "'" << endl;
647 ZYPP_THROW(MediaWriteException(destNew));
652 FILE *file = ::fdopen( tmp_fd, "w" );
655 filesystem::unlink( destNew );
656 ERR << "fopen failed for file '" << destNew << "'" << endl;
657 ZYPP_THROW(MediaWriteException(destNew));
660 DBG << "dest: " << dest << endl;
661 DBG << "temp: " << destNew << endl;
663 ret = curl_easy_setopt( _curl, CURLOPT_WRITEDATA, file );
666 filesystem::unlink( destNew );
667 ZYPP_THROW(MediaCurlSetOptException(_url, _curlError));
670 // Set callback and perform.
671 report->start(url, dest);
672 if ( curl_easy_setopt( _curl, CURLOPT_PROGRESSDATA, &report ) != 0 ) {
673 WAR << "Can't set CURLOPT_PROGRESSDATA: " << _curlError << endl;;
676 ret = curl_easy_perform( _curl );
678 if ( curl_easy_setopt( _curl, CURLOPT_PROGRESSDATA, NULL ) != 0 ) {
679 WAR << "Can't unset CURLOPT_PROGRESSDATA: " << _curlError << endl;;
684 filesystem::unlink( destNew );
686 ERR << "curl error: " << ret << ": " << _curlError << endl;
690 case CURLE_UNSUPPORTED_PROTOCOL:
691 case CURLE_URL_MALFORMAT:
692 case CURLE_URL_MALFORMAT_USER:
694 case CURLE_HTTP_NOT_FOUND:
697 CURLcode infoRet = curl_easy_getinfo( _curl, CURLINFO_HTTP_CODE,
699 if ( infoRet == CURLE_OK ) {
700 string msg = "HTTP return code: " +
701 str::numstring( httpReturnCode ) +
702 " (URL: " + url.asString() + ")";
704 if ( httpReturnCode == 401 )
706 msg = "URL: " + url.asString();
707 err = "Login failed";
711 err = "File not found";
713 ZYPP_THROW( MediaCurlException(_url, err, _curlError));
717 case CURLE_FTP_COULDNT_RETR_FILE:
718 case CURLE_FTP_ACCESS_DENIED:
719 err = "File not found";
721 case CURLE_BAD_PASSWORD_ENTERED:
722 case CURLE_FTP_USER_PASSWORD_INCORRECT:
723 err = "Login failed";
725 case CURLE_COULDNT_RESOLVE_PROXY:
726 case CURLE_COULDNT_RESOLVE_HOST:
727 case CURLE_COULDNT_CONNECT:
728 case CURLE_FTP_CANT_GET_HOST:
729 err = "Connection failed";
731 case CURLE_WRITE_ERROR:
734 case CURLE_ABORTED_BY_CALLBACK:
737 case CURLE_SSL_PEER_CERTIFICATE:
739 err = "Unrecognized error";
742 ZYPP_THROW(MediaCurlException(_url, err, _curlError));
744 catch (const MediaException & excpt_r)
746 ZYPP_RETHROW(excpt_r);
751 // getumask() would be fine, but does not exist
752 // [ the linker can't find it in glibc :-( ].
753 mask = ::umask(0022); ::umask(mask);
754 if ( ::fchmod( ::fileno(file), 0644 & ~mask))
756 ERR << "Failed to chmod file " << destNew << endl;
760 if ( rename( destNew, dest ) != 0 ) {
761 ERR << "Rename failed" << endl;
762 ZYPP_THROW(MediaWriteException(dest));
767 ///////////////////////////////////////////////////////////////////
770 // METHOD NAME : MediaCurl::getDir
771 // METHOD TYPE : PMError
773 // DESCRIPTION : Asserted that media is attached
775 void MediaCurl::getDir( const Pathname & dirname, bool recurse_r ) const
777 filesystem::DirContent content;
778 getDirInfo( content, dirname, /*dots*/false );
780 for ( filesystem::DirContent::const_iterator it = content.begin(); it != content.end(); ++it ) {
781 Pathname filename = dirname + it->name;
784 switch ( it->type ) {
785 case filesystem::FT_NOT_AVAIL: // old directory.yast contains no typeinfo at all
786 case filesystem::FT_FILE:
789 case filesystem::FT_DIR: // newer directory.yast contain at least directory info
791 getDir( filename, recurse_r );
793 res = assert_dir( localPath( filename ) );
795 WAR << "Ignore error (" << res << ") on creating local directory '" << localPath( filename ) << "'" << endl;
800 // don't provide devices, sockets, etc.
806 ///////////////////////////////////////////////////////////////////
809 // METHOD NAME : MediaCurl::getDirInfo
810 // METHOD TYPE : PMError
812 // DESCRIPTION : Asserted that media is attached and retlist is empty.
814 void MediaCurl::getDirInfo( std::list<std::string> & retlist,
815 const Pathname & dirname, bool dots ) const
817 getDirectoryYast( retlist, dirname, dots );
820 ///////////////////////////////////////////////////////////////////
823 // METHOD NAME : MediaCurl::getDirInfo
824 // METHOD TYPE : PMError
826 // DESCRIPTION : Asserted that media is attached and retlist is empty.
828 void MediaCurl::getDirInfo( filesystem::DirContent & retlist,
829 const Pathname & dirname, bool dots ) const
831 getDirectoryYast( retlist, dirname, dots );
834 ///////////////////////////////////////////////////////////////////
837 // METHOD NAME : MediaCurl::progressCallback
840 // DESCRIPTION : Progress callback triggered from MediaCurl::getFile
842 int MediaCurl::progressCallback( void *clientp, double dltotal, double dlnow,
843 double ultotal, double ulnow )
845 callback::SendReport<DownloadProgressReport> *report
846 = reinterpret_cast<callback::SendReport<DownloadProgressReport>*>( clientp );
850 if (! (*report)->progress(int( dlnow * 100 / dltotal ), Url() ))