in Source/cmFileCommand.cxx [1871:2359]
bool HandleDownloadCommand(std::vector<std::string> const& args,
cmExecutionStatus& status)
{
#if !defined(CMAKE_BOOTSTRAP)
auto i = args.begin();
if (args.size() < 2) {
status.SetError("DOWNLOAD must be called with at least two arguments.");
return false;
}
++i; // Get rid of subcommand
std::string url = *i;
++i;
std::string file;
long timeout = 0;
long inactivity_timeout = 0;
std::string logVar;
std::string statusVar;
cm::optional<std::string> tlsVersionOpt;
cm::optional<bool> tlsVerifyOpt;
cmValue cainfo = status.GetMakefile().GetDefinition("CMAKE_TLS_CAINFO");
std::string netrc_level =
status.GetMakefile().GetSafeDefinition("CMAKE_NETRC");
std::string netrc_file =
status.GetMakefile().GetSafeDefinition("CMAKE_NETRC_FILE");
std::string expectedHash;
std::string hashMatchMSG;
std::unique_ptr<cmCryptoHash> hash;
bool showProgress = false;
std::string userpwd;
std::vector<std::string> curl_headers;
std::vector<std::pair<std::string, cm::optional<std::string>>> curl_ranges;
while (i != args.end()) {
if (*i == "TIMEOUT") {
++i;
if (i != args.end()) {
timeout = atol(i->c_str());
} else {
status.SetError("DOWNLOAD missing time for TIMEOUT.");
return false;
}
} else if (*i == "INACTIVITY_TIMEOUT") {
++i;
if (i != args.end()) {
inactivity_timeout = atol(i->c_str());
} else {
status.SetError("DOWNLOAD missing time for INACTIVITY_TIMEOUT.");
return false;
}
} else if (*i == "LOG") {
++i;
if (i == args.end()) {
status.SetError("DOWNLOAD missing VAR for LOG.");
return false;
}
logVar = *i;
} else if (*i == "STATUS") {
++i;
if (i == args.end()) {
status.SetError("DOWNLOAD missing VAR for STATUS.");
return false;
}
statusVar = *i;
} else if (*i == "TLS_VERSION") {
++i;
if (i != args.end()) {
tlsVersionOpt = *i;
} else {
status.SetError("DOWNLOAD missing value for TLS_VERSION.");
return false;
}
} else if (*i == "TLS_VERIFY") {
++i;
if (i != args.end()) {
tlsVerifyOpt = cmIsOn(*i);
} else {
status.SetError("DOWNLOAD missing bool value for TLS_VERIFY.");
return false;
}
} else if (*i == "TLS_CAINFO") {
++i;
if (i != args.end()) {
cainfo = cmValue(*i);
} else {
status.SetError("DOWNLOAD missing file value for TLS_CAINFO.");
return false;
}
} else if (*i == "NETRC_FILE") {
++i;
if (i != args.end()) {
netrc_file = *i;
} else {
status.SetError("DOWNLOAD missing file value for NETRC_FILE.");
return false;
}
} else if (*i == "NETRC") {
++i;
if (i != args.end()) {
netrc_level = *i;
} else {
status.SetError("DOWNLOAD missing level value for NETRC.");
return false;
}
} else if (*i == "EXPECTED_MD5") {
++i;
if (i == args.end()) {
status.SetError("DOWNLOAD missing sum value for EXPECTED_MD5.");
return false;
}
hash = cm::make_unique<cmCryptoHash>(cmCryptoHash::AlgoMD5);
hashMatchMSG = "MD5 sum";
expectedHash = cmSystemTools::LowerCase(*i);
} else if (*i == "SHOW_PROGRESS") {
showProgress = true;
} else if (*i == "EXPECTED_HASH") {
++i;
if (i == args.end()) {
status.SetError("DOWNLOAD missing ALGO=value for EXPECTED_HASH.");
return false;
}
std::string::size_type pos = i->find("=");
if (pos == std::string::npos) {
std::string err =
cmStrCat("DOWNLOAD EXPECTED_HASH expects ALGO=value but got: ", *i);
status.SetError(err);
return false;
}
std::string algo = i->substr(0, pos);
expectedHash = cmSystemTools::LowerCase(i->substr(pos + 1));
hash = cmCryptoHash::New(algo);
if (!hash) {
std::string err =
cmStrCat("DOWNLOAD EXPECTED_HASH given unknown ALGO: ", algo);
status.SetError(err);
return false;
}
hashMatchMSG = algo + " hash";
} else if (*i == "USERPWD") {
++i;
if (i == args.end()) {
status.SetError("DOWNLOAD missing string for USERPWD.");
return false;
}
userpwd = *i;
} else if (*i == "HTTPHEADER") {
++i;
if (i == args.end()) {
status.SetError("DOWNLOAD missing string for HTTPHEADER.");
return false;
}
curl_headers.push_back(*i);
} else if (*i == "RANGE_START") {
++i;
if (i == args.end()) {
status.SetError("DOWNLOAD missing value for RANGE_START.");
return false;
}
curl_ranges.emplace_back(*i, cm::nullopt);
} else if (*i == "RANGE_END") {
++i;
if (curl_ranges.empty()) {
curl_ranges.emplace_back("0", *i);
} else {
auto& last_range = curl_ranges.back();
if (!last_range.second.has_value()) {
last_range.second = *i;
} else {
status.SetError("Multiple RANGE_END values is provided without "
"the corresponding RANGE_START.");
return false;
}
}
} else if (file.empty()) {
file = *i;
} else {
// Do not return error for compatibility reason.
std::string err = cmStrCat("Unexpected argument: ", *i);
status.GetMakefile().IssueMessage(MessageType::AUTHOR_WARNING, err);
}
++i;
}
if (!tlsVerifyOpt.has_value()) {
if (cmValue v = status.GetMakefile().GetDefinition("CMAKE_TLS_VERIFY")) {
tlsVerifyOpt = v.IsOn();
}
}
if (!tlsVerifyOpt.has_value()) {
if (cm::optional<std::string> v =
cmSystemTools::GetEnvVar("CMAKE_TLS_VERIFY")) {
tlsVerifyOpt = cmIsOn(*v);
}
}
bool tlsVerifyDefaulted = false;
if (!tlsVerifyOpt.has_value()) {
tlsVerifyOpt = TLS_VERIFY_DEFAULT;
tlsVerifyDefaulted = true;
}
if (!tlsVersionOpt.has_value()) {
if (cmValue v = status.GetMakefile().GetDefinition("CMAKE_TLS_VERSION")) {
tlsVersionOpt = *v;
}
}
if (!tlsVersionOpt.has_value()) {
if (cm::optional<std::string> v =
cmSystemTools::GetEnvVar("CMAKE_TLS_VERSION")) {
tlsVersionOpt = std::move(v);
}
}
bool tlsVersionDefaulted = false;
if (!tlsVersionOpt.has_value()) {
tlsVersionOpt = TLS_VERSION_DEFAULT;
tlsVersionDefaulted = true;
}
// Can't calculate hash if we don't save the file.
// TODO Incrementally calculate hash in the write callback as the file is
// being downloaded so this check can be relaxed.
if (file.empty() && hash) {
status.SetError("DOWNLOAD cannot calculate hash if file is not saved.");
return false;
}
// If file exists already, and caller specified an expected md5 or sha,
// and the existing file already has the expected hash, then simply
// return.
//
if (!file.empty() && cmSystemTools::FileExists(file) && hash.get()) {
std::string msg;
std::string actualHash = hash->HashFile(file);
if (actualHash == expectedHash) {
msg = cmStrCat("skipping download as file already exists with expected ",
hashMatchMSG, '"');
if (!statusVar.empty()) {
status.GetMakefile().AddDefinition(statusVar, cmStrCat(0, ";\"", msg));
}
return true;
}
}
// Make sure parent directory exists so we can write to the file
// as we receive downloaded bits from curl...
//
if (!file.empty()) {
std::string dir = cmSystemTools::GetFilenamePath(file);
if (!dir.empty() && !cmSystemTools::FileExists(dir) &&
!cmSystemTools::MakeDirectory(dir)) {
std::string errstring = "DOWNLOAD error: cannot create directory '" +
dir +
"' - Specify file by full path name and verify that you "
"have directory creation and file write privileges.";
status.SetError(errstring);
return false;
}
}
cmsys::ofstream fout;
if (!file.empty()) {
fout.open(file.c_str(), std::ios::binary);
if (!fout) {
status.SetError("DOWNLOAD cannot open file for write.");
return false;
}
}
url = cmCurlFixFileURL(url);
::CURL* curl;
cm_curl_global_init(CURL_GLOBAL_DEFAULT);
curl = cm_curl_easy_init();
if (!curl) {
status.SetError("DOWNLOAD error initializing curl.");
return false;
}
cURLEasyGuard g_curl(curl);
::CURLcode res = ::curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
check_curl_result(res, "DOWNLOAD cannot set url: ");
// enable HTTP ERROR parsing
res = ::curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1);
check_curl_result(res, "DOWNLOAD cannot set http failure option: ");
curl_version_info_data* cv = curl_version_info(CURLVERSION_FIRST);
res = ::curl_easy_setopt(
curl, CURLOPT_USERAGENT,
cmStrCat("curl/", cv ? cv->version : LIBCURL_VERSION).c_str());
check_curl_result(res, "DOWNLOAD cannot set user agent option: ");
res = ::curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, cmWriteToFileCallback);
check_curl_result(res, "DOWNLOAD cannot set write function: ");
res = ::curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION,
cmFileCommandCurlDebugCallback);
check_curl_result(res, "DOWNLOAD cannot set debug function: ");
if (tlsVersionOpt.has_value()) {
if (cm::optional<int> v = cmCurlParseTLSVersion(*tlsVersionOpt)) {
res = ::curl_easy_setopt(curl, CURLOPT_SSLVERSION, *v);
if (tlsVersionDefaulted && res == CURLE_NOT_BUILT_IN) {
res = CURLE_OK;
}
check_curl_result(res,
cmStrCat("DOWNLOAD cannot set TLS/SSL version ",
*tlsVersionOpt, ": "));
} else {
status.SetError(
cmStrCat("DOWNLOAD given unknown TLS/SSL version ", *tlsVersionOpt));
return false;
}
}
// check to see if TLS verification is requested
if (tlsVerifyOpt.has_value() && tlsVerifyOpt.value()) {
res = ::curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1);
check_curl_result(res, "DOWNLOAD cannot set TLS/SSL Verify on: ");
} else {
res = ::curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
check_curl_result(res, "DOWNLOAD cannot set TLS/SSL Verify off: ");
}
for (auto const& range : curl_ranges) {
std::string curl_range = range.first + '-' +
(range.second.has_value() ? range.second.value() : "");
res = ::curl_easy_setopt(curl, CURLOPT_RANGE, curl_range.c_str());
check_curl_result(res, "DOWNLOAD cannot set range: ");
}
// check to see if a CAINFO file has been specified
// command arg comes first
std::string const& cainfo_err = cmCurlSetCAInfo(curl, cainfo);
if (!cainfo_err.empty()) {
status.SetError(cainfo_err);
return false;
}
// check to see if netrc parameters have been specified
// local command args takes precedence over CMAKE_NETRC*
netrc_level = cmSystemTools::UpperCase(netrc_level);
std::string const& netrc_option_err =
cmCurlSetNETRCOption(curl, netrc_level, netrc_file);
if (!netrc_option_err.empty()) {
status.SetError(netrc_option_err);
return false;
}
cmFileCommandVectorOfChar chunkDebug;
res = ::curl_easy_setopt(curl, CURLOPT_WRITEDATA,
file.empty() ? nullptr : &fout);
check_curl_result(res, "DOWNLOAD cannot set write data: ");
res = ::curl_easy_setopt(curl, CURLOPT_DEBUGDATA, &chunkDebug);
check_curl_result(res, "DOWNLOAD cannot set debug data: ");
res = ::curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
check_curl_result(res, "DOWNLOAD cannot set follow-redirect option: ");
if (!logVar.empty()) {
res = ::curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
check_curl_result(res, "DOWNLOAD cannot set verbose: ");
}
if (timeout > 0) {
res = ::curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout);
check_curl_result(res, "DOWNLOAD cannot set timeout: ");
}
if (inactivity_timeout > 0) {
// Give up if there is no progress for a long time.
::curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 1);
::curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, inactivity_timeout);
}
// Need the progress helper's scope to last through the duration of
// the curl_easy_perform call... so this object is declared at function
// scope intentionally, rather than inside the "if(showProgress)"
// block...
//
cURLProgressHelper helper(&status.GetMakefile(), "download");
if (showProgress) {
res = ::curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0);
check_curl_result(res, "DOWNLOAD cannot set noprogress value: ");
res = ::curl_easy_setopt(curl, CM_CURLOPT_XFERINFOFUNCTION,
cmFileDownloadProgressCallback);
check_curl_result(res, "DOWNLOAD cannot set progress function: ");
res = ::curl_easy_setopt(curl, CURLOPT_PROGRESSDATA,
reinterpret_cast<void*>(&helper));
check_curl_result(res, "DOWNLOAD cannot set progress data: ");
}
if (!userpwd.empty()) {
res = ::curl_easy_setopt(curl, CURLOPT_USERPWD, userpwd.c_str());
check_curl_result(res, "DOWNLOAD cannot set user password: ");
}
struct curl_slist* headers = nullptr;
for (std::string const& h : curl_headers) {
headers = ::curl_slist_append(headers, h.c_str());
}
::curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
res = ::curl_easy_perform(curl);
::curl_slist_free_all(headers);
/* always cleanup */
g_curl.release();
::curl_easy_cleanup(curl);
if (!statusVar.empty()) {
std::string m = curl_easy_strerror(res);
if ((res == CURLE_SSL_CONNECT_ERROR ||
res == CURLE_PEER_FAILED_VERIFICATION) &&
tlsVerifyDefaulted) {
m = cmStrCat(
std::move(m),
". If this is due to https certificate verification failure, one may "
"set environment variable CMAKE_TLS_VERIFY=0 to suppress it.");
}
status.GetMakefile().AddDefinition(
statusVar, cmStrCat(static_cast<int>(res), ";\"", std::move(m), '"'));
}
::curl_global_cleanup();
// Ensure requested curl logs are returned (especially in case of failure)
//
if (!logVar.empty()) {
chunkDebug.push_back(0);
status.GetMakefile().AddDefinition(logVar, chunkDebug.data());
}
// Explicitly flush/close so we can measure the md5 accurately.
//
if (!file.empty()) {
fout.flush();
fout.close();
}
// Verify MD5 sum if requested:
//
if (hash) {
if (res != CURLE_OK) {
status.SetError(cmStrCat(
"DOWNLOAD cannot compute hash on failed download\n"
" status: [",
static_cast<int>(res), ";\"", ::curl_easy_strerror(res), "\"]"));
return false;
}
std::string actualHash = hash->HashFile(file);
if (actualHash.empty()) {
status.SetError("DOWNLOAD cannot compute hash on downloaded file");
return false;
}
if (expectedHash != actualHash) {
if (!statusVar.empty() && res == 0) {
status.GetMakefile().AddDefinition(statusVar,
"1;HASH mismatch: "
"expected: " +
expectedHash +
" actual: " + actualHash);
}
status.SetError(cmStrCat("DOWNLOAD HASH mismatch\n"
" for file: [",
file,
"]\n"
" expected hash: [",
expectedHash,
"]\n"
" actual hash: [",
actualHash, "]\n"));
return false;
}
}
return true;
#else
status.SetError("DOWNLOAD not supported by bootstrap cmake.");
return false;
#endif
}