Merge pull request #24470 from fuzzard/release_20.3
[xbmc.git] / xbmc / addons / Repository.cpp
blob6708e71c2aff74d2fd297212bbd3b5c9eb17f535
1 /*
2 * Copyright (C) 2005-2018 Team Kodi
3 * This file is part of Kodi - https://kodi.tv
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 * See LICENSES/README.md for more information.
7 */
9 #include "Repository.h"
11 #include "FileItem.h"
12 #include "ServiceBroker.h"
13 #include "URL.h"
14 #include "addons/AddonDatabase.h"
15 #include "addons/AddonInstaller.h"
16 #include "addons/AddonManager.h"
17 #include "addons/RepositoryUpdater.h"
18 #include "addons/addoninfo/AddonInfo.h"
19 #include "addons/addoninfo/AddonType.h"
20 #include "filesystem/CurlFile.h"
21 #include "filesystem/File.h"
22 #include "filesystem/ZipFile.h"
23 #include "messaging/helpers/DialogHelper.h"
24 #include "utils/Base64.h"
25 #include "utils/Digest.h"
26 #include "utils/Mime.h"
27 #include "utils/StringUtils.h"
28 #include "utils/URIUtils.h"
29 #include "utils/XBMCTinyXML.h"
30 #include "utils/log.h"
32 #include <algorithm>
33 #include <iterator>
34 #include <tuple>
35 #include <utility>
37 using namespace XFILE;
38 using namespace ADDON;
39 using namespace KODI::MESSAGING;
40 using KODI::UTILITY::CDigest;
41 using KODI::UTILITY::TypedDigest;
44 CRepository::ResolveResult CRepository::ResolvePathAndHash(const AddonPtr& addon) const
46 std::string const& path = addon->Path();
48 auto dirIt = std::find_if(m_dirs.begin(), m_dirs.end(), [&path](RepositoryDirInfo const& dir) {
49 return URIUtils::PathHasParent(path, dir.datadir, true);
50 });
51 if (dirIt == m_dirs.end())
53 CLog::Log(LOGERROR, "Requested path {} not found in known repository directories", path);
54 return {};
57 if (dirIt->hashType == CDigest::Type::INVALID)
59 // We have a path, but need no hash
60 return {path, {}};
63 // Do not follow mirror redirect, we want the headers of the redirect response
64 CURL url{path};
65 url.SetProtocolOption("redirect-limit", "0");
66 CCurlFile file;
67 if (!file.Open(url))
69 CLog::Log(LOGERROR, "Could not fetch addon location and hash from {}", path);
70 return {};
73 std::string hashTypeStr = CDigest::TypeToString(dirIt->hashType);
75 // Return the location from the header so we don't have to look it up again
76 // (saves one request per addon install)
77 std::string location = file.GetRedirectURL();
78 // content-* headers are base64, convert to base16
79 TypedDigest hash{dirIt->hashType, StringUtils::ToHexadecimal(Base64::Decode(file.GetHttpHeader().GetValue(std::string("content-") + hashTypeStr)))};
81 if (hash.Empty())
83 int tmp;
84 // Expected hash, but none found -> fall back to old method
85 if (!FetchChecksum(path + "." + hashTypeStr, hash.value, tmp) || hash.Empty())
87 CLog::Log(LOGERROR, "Failed to find hash for {} from HTTP header and in separate file", path);
88 return {};
91 if (location.empty())
93 // Fall back to original URL if we do not get a redirect
94 location = path;
97 CLog::Log(LOGDEBUG, "Resolved addon path {} to {} hash {}", path, location, hash.value);
99 return {location, hash};
102 CRepository::CRepository(const AddonInfoPtr& addonInfo) : CAddon(addonInfo, AddonType::REPOSITORY)
104 RepositoryDirList dirs;
105 CAddonVersion version;
106 AddonInfoPtr addonver =
107 CServiceBroker::GetAddonMgr().GetAddonInfo("xbmc.addon", AddonType::UNKNOWN);
108 if (addonver)
109 version = addonver->Version();
111 for (const auto& element : Type(AddonType::REPOSITORY)->GetElements("dir"))
113 RepositoryDirInfo dir = ParseDirConfiguration(element.second);
114 if ((dir.minversion.empty() || version >= dir.minversion) &&
115 (dir.maxversion.empty() || version <= dir.maxversion))
116 m_dirs.push_back(std::move(dir));
119 // old (dharma compatible) way of defining the addon repository structure, is no longer supported
120 // we error out so the user knows how to migrate. The <dir> way is supported since gotham.
121 //! @todo remove if block completely in v21
122 if (!Type(AddonType::REPOSITORY)->GetValue("info").empty())
124 CLog::Log(LOGERROR,
125 "Repository add-on {} uses old schema definition for the repository extension point! "
126 "This is no longer supported, please update your addon to use <dir> definitions.",
127 ID());
130 if (m_dirs.empty())
132 CLog::Log(LOGERROR,
133 "Repository add-on {} does not have any directory and won't be able to update/serve "
134 "addons! Please fix the addon.xml definition",
135 ID());
138 for (auto const& dir : m_dirs)
140 CURL datadir(dir.datadir);
141 if (datadir.IsProtocol("http"))
143 CLog::Log(LOGWARNING, "Repository add-on {} uses plain HTTP for add-on downloads in path {} - this is insecure and will make your Kodi installation vulnerable to attacks if enabled!", ID(), datadir.GetRedacted());
145 else if (datadir.IsProtocol("https") && datadir.HasProtocolOption("verifypeer") && datadir.GetProtocolOption("verifypeer") == "false")
147 CLog::Log(LOGWARNING, "Repository add-on {} disabled peer verification for add-on downloads in path {} - this is insecure and will make your Kodi installation vulnerable to attacks if enabled!", ID(), datadir.GetRedacted());
152 bool CRepository::FetchChecksum(const std::string& url,
153 std::string& checksum,
154 int& recheckAfter) noexcept
156 CFile file;
157 if (!file.Open(url))
158 return false;
160 // we intentionally avoid using file.GetLength() for
161 // Transfer-Encoding: chunked servers.
162 std::stringstream ss;
163 char temp[1024];
164 int read;
165 while ((read = file.Read(temp, sizeof(temp))) > 0)
166 ss.write(temp, read);
167 if (read <= -1)
168 return false;
169 checksum = ss.str();
170 std::size_t pos = checksum.find_first_of(" \n");
171 if (pos != std::string::npos)
173 checksum = checksum.substr(0, pos);
176 // Determine update interval from (potential) HTTP response
177 // Default: 24 h
178 recheckAfter = 24 * 60 * 60;
179 // This special header is set by the Kodi mirror redirector to control client update frequency
180 // depending on the load on the mirrors
181 const std::string recheckAfterHeader{
182 file.GetProperty(FILE_PROPERTY_RESPONSE_HEADER, "X-Kodi-Recheck-After")};
183 if (!recheckAfterHeader.empty())
187 // Limit value range to sensible values (1 hour to 1 week)
188 recheckAfter =
189 std::max(std::min(std::stoi(recheckAfterHeader), 24 * 7 * 60 * 60), 1 * 60 * 60);
191 catch (...)
193 CLog::Log(LOGWARNING, "Could not parse X-Kodi-Recheck-After header value '{}' from {}",
194 recheckAfterHeader, url);
198 return true;
201 bool CRepository::FetchIndex(const RepositoryDirInfo& repo,
202 std::string const& digest,
203 std::vector<AddonInfoPtr>& addons) noexcept
205 XFILE::CCurlFile http;
207 std::string response;
208 if (!http.Get(repo.info, response))
210 CLog::Log(LOGERROR, "CRepository: failed to read {}", repo.info);
211 return false;
214 if (repo.checksumType != CDigest::Type::INVALID)
216 std::string actualDigest = CDigest::Calculate(repo.checksumType, response);
217 if (!StringUtils::EqualsNoCase(digest, actualDigest))
219 CLog::Log(LOGERROR, "CRepository: {} index has wrong digest {}, expected: {}", repo.info, actualDigest, digest);
220 return false;
224 if (URIUtils::HasExtension(repo.info, ".gz")
225 || CMime::GetFileTypeFromMime(http.GetProperty(XFILE::FILE_PROPERTY_MIME_TYPE)) == CMime::EFileType::FileTypeGZip)
227 CLog::Log(LOGDEBUG, "CRepository '{}' is gzip. decompressing", repo.info);
228 std::string buffer;
229 if (!CZipFile::DecompressGzip(response, buffer))
231 CLog::Log(LOGERROR, "CRepository: failed to decompress gzip from '{}'", repo.info);
232 return false;
234 response = std::move(buffer);
237 return CServiceBroker::GetAddonMgr().AddonsFromRepoXML(repo, response, addons);
240 CRepository::FetchStatus CRepository::FetchIfChanged(const std::string& oldChecksum,
241 std::string& checksum,
242 std::vector<AddonInfoPtr>& addons,
243 int& recheckAfter) const
245 checksum = "";
246 std::vector<std::tuple<RepositoryDirInfo const&, std::string>> dirChecksums;
247 std::vector<int> recheckAfterTimes;
249 for (const auto& dir : m_dirs)
251 if (!dir.checksum.empty())
253 std::string part;
254 int recheckAfterThisDir;
255 if (!FetchChecksum(dir.checksum, part, recheckAfterThisDir))
257 recheckAfter = 1 * 60 * 60; // retry after 1 hour
258 CLog::Log(LOGERROR, "CRepository: failed read '{}'", dir.checksum);
259 return STATUS_ERROR;
261 dirChecksums.emplace_back(dir, part);
262 recheckAfterTimes.push_back(recheckAfterThisDir);
263 checksum += part;
267 // Default interval: 24 h
268 recheckAfter = 24 * 60 * 60;
269 if (dirChecksums.size() == m_dirs.size() && !dirChecksums.empty())
271 // Use smallest update interval out of all received (individual intervals per directory are
272 // not possible)
273 recheckAfter = *std::min_element(recheckAfterTimes.begin(), recheckAfterTimes.end());
274 // If all directories have checksums and they match the last one, nothing has changed
275 if (dirChecksums.size() == m_dirs.size() && oldChecksum == checksum)
276 return STATUS_NOT_MODIFIED;
279 for (const auto& dirTuple : dirChecksums)
281 std::vector<AddonInfoPtr> tmp;
282 if (!FetchIndex(std::get<0>(dirTuple), std::get<1>(dirTuple), tmp))
283 return STATUS_ERROR;
284 addons.insert(addons.end(), tmp.begin(), tmp.end());
286 return STATUS_OK;
289 RepositoryDirInfo CRepository::ParseDirConfiguration(const CAddonExtensions& configuration)
291 RepositoryDirInfo dir;
292 dir.checksum = configuration.GetValue("checksum").asString();
293 std::string checksumStr = configuration.GetValue("checksum@verify").asString();
294 if (!checksumStr.empty())
296 dir.checksumType = CDigest::TypeFromString(checksumStr);
298 dir.info = configuration.GetValue("info").asString();
299 dir.datadir = configuration.GetValue("datadir").asString();
300 dir.artdir = configuration.GetValue("artdir").asString();
301 if (dir.artdir.empty())
303 dir.artdir = dir.datadir;
306 std::string hashStr = configuration.GetValue("hashes").asString();
307 StringUtils::ToLower(hashStr);
308 if (hashStr == "true")
310 // Deprecated alias
311 hashStr = "md5";
313 if (!hashStr.empty() && hashStr != "false")
315 dir.hashType = CDigest::TypeFromString(hashStr);
316 if (dir.hashType == CDigest::Type::MD5)
318 CLog::Log(LOGWARNING, "CRepository::{}: Repository has MD5 hashes enabled - this hash function is broken and will only guard against unintentional data corruption", __FUNCTION__);
322 dir.minversion = CAddonVersion{configuration.GetValue("@minversion").asString()};
323 dir.maxversion = CAddonVersion{configuration.GetValue("@maxversion").asString()};
325 return dir;