/** * @file llupdatedownloader.cpp * * $LicenseInfo:firstyear=2010&license=viewerlgpl$ * Second Life Viewer Source Code * Copyright (C) 2010, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License only. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA * $/LicenseInfo$ */ #include "linden_common.h" #include #include #include #include #include "lldir.h" #include "llfile.h" #include "llmd5.h" #include "llsd.h" #include "llsdserialize.h" #include "llthread.h" #include "llupdatedownloader.h" class LLUpdateDownloader::Implementation: public LLThread { public: Implementation(LLUpdateDownloader::Client & client); ~Implementation(); void cancel(void); void download(LLURI const & uri, std::string const & hash); bool isDownloading(void); size_t onHeader(void * header, size_t size); size_t onBody(void * header, size_t size); void resume(void); private: bool mCancelled; LLUpdateDownloader::Client & mClient; CURL * mCurl; LLSD mDownloadData; llofstream mDownloadStream; std::string mDownloadRecordPath; void initializeCurlGet(std::string const & url, bool processHeader); void resumeDownloading(size_t startByte); void run(void); void startDownloading(LLURI const & uri, std::string const & hash); void throwOnCurlError(CURLcode code); bool validateDownload(void); LOG_CLASS(LLUpdateDownloader::Implementation); }; namespace { class DownloadError: public std::runtime_error { public: DownloadError(const char * message): std::runtime_error(message) { ; // No op. } }; const char * gSecondLifeUpdateRecord = "SecondLifeUpdateDownload.xml"; }; // LLUpdateDownloader //----------------------------------------------------------------------------- std::string LLUpdateDownloader::downloadMarkerPath(void) { return gDirUtilp->getExpandedFilename(LL_PATH_LOGS, gSecondLifeUpdateRecord); } LLUpdateDownloader::LLUpdateDownloader(Client & client): mImplementation(new LLUpdateDownloader::Implementation(client)) { ; // No op. } void LLUpdateDownloader::cancel(void) { mImplementation->cancel(); } void LLUpdateDownloader::download(LLURI const & uri, std::string const & hash) { mImplementation->download(uri, hash); } bool LLUpdateDownloader::isDownloading(void) { return mImplementation->isDownloading(); } void LLUpdateDownloader::resume(void) { mImplementation->resume(); } // LLUpdateDownloader::Implementation //----------------------------------------------------------------------------- namespace { size_t write_function(void * data, size_t blockSize, size_t blocks, void * downloader) { size_t bytes = blockSize * blocks; return reinterpret_cast(downloader)->onBody(data, bytes); } size_t header_function(void * data, size_t blockSize, size_t blocks, void * downloader) { size_t bytes = blockSize * blocks; return reinterpret_cast(downloader)->onHeader(data, bytes); } } LLUpdateDownloader::Implementation::Implementation(LLUpdateDownloader::Client & client): LLThread("LLUpdateDownloader"), mCancelled(false), mClient(client), mCurl(0) { CURLcode code = curl_global_init(CURL_GLOBAL_ALL); // Just in case. llverify(code == CURLE_OK); // TODO: real error handling here. } LLUpdateDownloader::Implementation::~Implementation() { if(isDownloading()) { cancel(); shutdown(); } else { ; // No op. } if(mCurl) curl_easy_cleanup(mCurl); } void LLUpdateDownloader::Implementation::cancel(void) { mCancelled = true; } void LLUpdateDownloader::Implementation::download(LLURI const & uri, std::string const & hash) { if(isDownloading()) mClient.downloadError("download in progress"); mDownloadRecordPath = downloadMarkerPath(); mDownloadData = LLSD(); try { startDownloading(uri, hash); } catch(DownloadError const & e) { mClient.downloadError(e.what()); } } bool LLUpdateDownloader::Implementation::isDownloading(void) { return !isStopped(); } void LLUpdateDownloader::Implementation::resume(void) { if(isDownloading()) mClient.downloadError("download in progress"); mDownloadRecordPath = downloadMarkerPath(); llifstream dataStream(mDownloadRecordPath); if(!dataStream) { mClient.downloadError("no download marker"); return; } LLSDSerialize::fromXMLDocument(mDownloadData, dataStream); if(!mDownloadData.asBoolean()) { mClient.downloadError("no download information in marker"); return; } std::string filePath = mDownloadData["path"].asString(); try { if(LLFile::isfile(filePath)) { llstat fileStatus; LLFile::stat(filePath, &fileStatus); if(fileStatus.st_size != mDownloadData["size"].asInteger()) { resumeDownloading(fileStatus.st_size); } else if(!validateDownload()) { LLFile::remove(filePath); download(LLURI(mDownloadData["url"].asString()), mDownloadData["hash"].asString()); } else { mClient.downloadComplete(mDownloadData); } } else { download(LLURI(mDownloadData["url"].asString()), mDownloadData["hash"].asString()); } } catch(DownloadError & e) { mClient.downloadError(e.what()); } } size_t LLUpdateDownloader::Implementation::onHeader(void * buffer, size_t size) { char const * headerPtr = reinterpret_cast (buffer); std::string header(headerPtr, headerPtr + size); size_t colonPosition = header.find(':'); if(colonPosition == std::string::npos) return size; // HTML response; ignore. if(header.substr(0, colonPosition) == "Content-Length") { try { size_t firstDigitPos = header.find_first_of("0123456789", colonPosition); size_t lastDigitPos = header.find_last_of("0123456789"); std::string contentLength = header.substr(firstDigitPos, lastDigitPos - firstDigitPos + 1); size_t size = boost::lexical_cast(contentLength); LL_INFOS("UpdateDownload") << "download size is " << size << LL_ENDL; mDownloadData["size"] = LLSD(LLSD::Integer(size)); llofstream odataStream(mDownloadRecordPath); LLSDSerialize::toPrettyXML(mDownloadData, odataStream); } catch (std::exception const & e) { LL_WARNS("UpdateDownload") << "unable to read content length (" << e.what() << ")" << LL_ENDL; } } else { ; // No op. } return size; } size_t LLUpdateDownloader::Implementation::onBody(void * buffer, size_t size) { if(mCancelled) return 0; // Forces a write error which will halt curl thread. mDownloadStream.write(reinterpret_cast(buffer), size); return size; } void LLUpdateDownloader::Implementation::run(void) { CURLcode code = curl_easy_perform(mCurl); mDownloadStream.close(); if(code == CURLE_OK) { LLFile::remove(mDownloadRecordPath); if(validateDownload()) { LL_INFOS("UpdateDownload") << "download successful" << LL_ENDL; mClient.downloadComplete(mDownloadData); } else { LL_INFOS("UpdateDownload") << "download failed hash check" << LL_ENDL; std::string filePath = mDownloadData["path"].asString(); if(filePath.size() != 0) LLFile::remove(filePath); mClient.downloadError("failed hash check"); } } else if(mCancelled && (code == CURLE_WRITE_ERROR)) { LL_INFOS("UpdateDownload") << "download canceled by user" << LL_ENDL; // Do not call back client. } else { LL_WARNS("UpdateDownload") << "download failed with error '" << curl_easy_strerror(code) << "'" << LL_ENDL; LLFile::remove(mDownloadRecordPath); mClient.downloadError("curl error"); } } void LLUpdateDownloader::Implementation::initializeCurlGet(std::string const & url, bool processHeader) { if(mCurl == 0) { mCurl = curl_easy_init(); } else { curl_easy_reset(mCurl); } if(mCurl == 0) throw DownloadError("failed to initialize curl"); throwOnCurlError(curl_easy_setopt(mCurl, CURLOPT_NOSIGNAL, true)); throwOnCurlError(curl_easy_setopt(mCurl, CURLOPT_FOLLOWLOCATION, true)); throwOnCurlError(curl_easy_setopt(mCurl, CURLOPT_WRITEFUNCTION, &write_function)); throwOnCurlError(curl_easy_setopt(mCurl, CURLOPT_WRITEDATA, this)); if(processHeader) { throwOnCurlError(curl_easy_setopt(mCurl, CURLOPT_HEADERFUNCTION, &header_function)); throwOnCurlError(curl_easy_setopt(mCurl, CURLOPT_HEADERDATA, this)); } throwOnCurlError(curl_easy_setopt(mCurl, CURLOPT_HTTPGET, true)); throwOnCurlError(curl_easy_setopt(mCurl, CURLOPT_URL, url.c_str())); } void LLUpdateDownloader::Implementation::resumeDownloading(size_t startByte) { initializeCurlGet(mDownloadData["url"].asString(), false); // The header 'Range: bytes n-' will request the bytes remaining in the // source begining with byte n and ending with the last byte. boost::format rangeHeaderFormat("Range: bytes=%u-"); rangeHeaderFormat % startByte; curl_slist * headerList = 0; headerList = curl_slist_append(headerList, rangeHeaderFormat.str().c_str()); if(headerList == 0) throw DownloadError("cannot add Range header"); throwOnCurlError(curl_easy_setopt(mCurl, CURLOPT_HTTPHEADER, headerList)); curl_slist_free_all(headerList); mDownloadStream.open(mDownloadData["path"].asString(), std::ios_base::out | std::ios_base::binary | std::ios_base::app); start(); } void LLUpdateDownloader::Implementation::startDownloading(LLURI const & uri, std::string const & hash) { mDownloadData["url"] = uri.asString(); mDownloadData["hash"] = hash; LLSD path = uri.pathArray(); if(path.size() == 0) throw DownloadError("no file path"); std::string fileName = path[path.size() - 1].asString(); std::string filePath = gDirUtilp->getExpandedFilename(LL_PATH_TEMP, fileName); mDownloadData["path"] = filePath; LL_INFOS("UpdateDownload") << "downloading " << filePath << " from " << uri.asString() << LL_ENDL; LL_INFOS("UpdateDownload") << "hash of file is " << hash << LL_ENDL; llofstream dataStream(mDownloadRecordPath); LLSDSerialize::toPrettyXML(mDownloadData, dataStream); mDownloadStream.open(filePath, std::ios_base::out | std::ios_base::binary); initializeCurlGet(uri.asString(), true); start(); } void LLUpdateDownloader::Implementation::throwOnCurlError(CURLcode code) { if(code != CURLE_OK) { const char * errorString = curl_easy_strerror(code); if(errorString != 0) { throw DownloadError(curl_easy_strerror(code)); } else { throw DownloadError("unknown curl error"); } } else { ; // No op. } } bool LLUpdateDownloader::Implementation::validateDownload(void) { std::string filePath = mDownloadData["path"].asString(); llifstream fileStream(filePath, std::ios_base::in | std::ios_base::binary); if(!fileStream) return false; std::string hash = mDownloadData["hash"].asString(); if(hash.size() != 0) { LL_INFOS("UpdateDownload") << "checking hash..." << LL_ENDL; char digest[33]; LLMD5(fileStream).hex_digest(digest); if(hash != digest) { LL_WARNS("UpdateDownload") << "download hash mismatch; expeted " << hash << " but download is " << digest << LL_ENDL; } return hash == digest; } else { return true; // No hash check provided. } }