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.
9 #include "Repository.h"
12 #include "ServiceBroker.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/log.h"
36 using namespace XFILE
;
37 using namespace ADDON
;
38 using namespace KODI::MESSAGING
;
39 using KODI::UTILITY::CDigest
;
40 using KODI::UTILITY::TypedDigest
;
43 CRepository::ResolveResult
CRepository::ResolvePathAndHash(const AddonPtr
& addon
) const
45 std::string
const& path
= addon
->Path();
47 auto dirIt
= std::find_if(m_dirs
.begin(), m_dirs
.end(), [&path
](RepositoryDirInfo
const& dir
) {
48 return URIUtils::PathHasParent(path
, dir
.datadir
, true);
50 if (dirIt
== m_dirs
.end())
52 CLog::Log(LOGERROR
, "Requested path {} not found in known repository directories", path
);
56 if (dirIt
->hashType
== CDigest::Type::INVALID
)
58 // We have a path, but need no hash
62 // Do not follow mirror redirect, we want the headers of the redirect response
64 url
.SetProtocolOption("redirect-limit", "0");
68 CLog::Log(LOGERROR
, "Could not fetch addon location and hash from {}", path
);
72 std::string hashTypeStr
= CDigest::TypeToString(dirIt
->hashType
);
74 // Return the location from the header so we don't have to look it up again
75 // (saves one request per addon install)
76 std::string location
= file
.GetRedirectURL();
77 // content-* headers are base64, convert to base16
78 TypedDigest hash
{dirIt
->hashType
, StringUtils::ToHexadecimal(Base64::Decode(file
.GetHttpHeader().GetValue(std::string("content-") + hashTypeStr
)))};
83 // Expected hash, but none found -> fall back to old method
84 if (!FetchChecksum(path
+ "." + hashTypeStr
, hash
.value
, tmp
) || hash
.Empty())
86 CLog::Log(LOGERROR
, "Failed to find hash for {} from HTTP header and in separate file", path
);
92 // Fall back to original URL if we do not get a redirect
96 CLog::Log(LOGDEBUG
, "Resolved addon path {} to {} hash {}", path
, location
, hash
.value
);
98 return {location
, hash
};
101 CRepository::CRepository(const AddonInfoPtr
& addonInfo
) : CAddon(addonInfo
, AddonType::REPOSITORY
)
103 RepositoryDirList dirs
;
104 CAddonVersion version
;
105 AddonInfoPtr addonver
=
106 CServiceBroker::GetAddonMgr().GetAddonInfo("xbmc.addon", AddonType::UNKNOWN
);
108 version
= addonver
->Version();
110 for (const auto& element
: Type(AddonType::REPOSITORY
)->GetElements("dir"))
112 RepositoryDirInfo dir
= ParseDirConfiguration(element
.second
);
113 if ((dir
.minversion
.empty() || version
>= dir
.minversion
) &&
114 (dir
.maxversion
.empty() || version
<= dir
.maxversion
))
115 m_dirs
.push_back(std::move(dir
));
118 // old (dharma compatible) way of defining the addon repository structure, is no longer supported
119 // we error out so the user knows how to migrate. The <dir> way is supported since gotham.
120 //! @todo remove if block completely in v21
121 if (!Type(AddonType::REPOSITORY
)->GetValue("info").empty())
124 "Repository add-on {} uses old schema definition for the repository extension point! "
125 "This is no longer supported, please update your addon to use <dir> definitions.",
132 "Repository add-on {} does not have any directory matching {} and won't be able to "
133 "update/serve addons! Please fix the addon.xml definition",
134 ID(), version
.asString());
137 for (auto const& dir
: m_dirs
)
139 CURL
datadir(dir
.datadir
);
140 if (datadir
.IsProtocol("http"))
142 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());
144 else if (datadir
.IsProtocol("https") && datadir
.HasProtocolOption("verifypeer") && datadir
.GetProtocolOption("verifypeer") == "false")
146 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());
151 bool CRepository::FetchChecksum(const std::string
& url
,
152 std::string
& checksum
,
153 int& recheckAfter
) noexcept
159 // we intentionally avoid using file.GetLength() for
160 // Transfer-Encoding: chunked servers.
161 std::stringstream ss
;
164 while ((read
= file
.Read(temp
, sizeof(temp
))) > 0)
165 ss
.write(temp
, read
);
169 std::size_t pos
= checksum
.find_first_of(" \n");
170 if (pos
!= std::string::npos
)
172 checksum
.resize(pos
);
175 // Determine update interval from (potential) HTTP response
177 recheckAfter
= 24 * 60 * 60;
178 // This special header is set by the Kodi mirror redirector to control client update frequency
179 // depending on the load on the mirrors
180 const std::string recheckAfterHeader
{
181 file
.GetProperty(FILE_PROPERTY_RESPONSE_HEADER
, "X-Kodi-Recheck-After")};
182 if (!recheckAfterHeader
.empty())
186 // Limit value range to sensible values (1 hour to 1 week)
188 std::max(std::min(std::stoi(recheckAfterHeader
), 24 * 7 * 60 * 60), 1 * 60 * 60);
192 CLog::Log(LOGWARNING
, "Could not parse X-Kodi-Recheck-After header value '{}' from {}",
193 recheckAfterHeader
, url
);
200 bool CRepository::FetchIndex(const RepositoryDirInfo
& repo
,
201 std::string
const& digest
,
202 std::vector
<AddonInfoPtr
>& addons
) noexcept
204 XFILE::CCurlFile http
;
206 std::string response
;
207 if (!http
.Get(repo
.info
, response
))
209 CLog::Log(LOGERROR
, "CRepository: failed to read {}", repo
.info
);
213 if (repo
.checksumType
!= CDigest::Type::INVALID
)
215 std::string actualDigest
= CDigest::Calculate(repo
.checksumType
, response
);
216 if (!StringUtils::EqualsNoCase(digest
, actualDigest
))
218 CLog::Log(LOGERROR
, "CRepository: {} index has wrong digest {}, expected: {}", repo
.info
, actualDigest
, digest
);
223 if (URIUtils::HasExtension(repo
.info
, ".gz")
224 || CMime::GetFileTypeFromMime(http
.GetProperty(XFILE::FILE_PROPERTY_MIME_TYPE
)) == CMime::EFileType::FileTypeGZip
)
226 CLog::Log(LOGDEBUG
, "CRepository '{}' is gzip. decompressing", repo
.info
);
228 if (!CZipFile::DecompressGzip(response
, buffer
))
230 CLog::Log(LOGERROR
, "CRepository: failed to decompress gzip from '{}'", repo
.info
);
233 response
= std::move(buffer
);
236 return CServiceBroker::GetAddonMgr().AddonsFromRepoXML(repo
, response
, addons
);
239 CRepository::FetchStatus
CRepository::FetchIfChanged(const std::string
& oldChecksum
,
240 std::string
& checksum
,
241 std::vector
<AddonInfoPtr
>& addons
,
242 int& recheckAfter
) const
245 std::vector
<std::tuple
<RepositoryDirInfo
const&, std::string
>> dirChecksums
;
246 std::vector
<int> recheckAfterTimes
;
248 for (const auto& dir
: m_dirs
)
250 if (!dir
.checksum
.empty())
253 int recheckAfterThisDir
;
254 if (!FetchChecksum(dir
.checksum
, part
, recheckAfterThisDir
))
256 recheckAfter
= 1 * 60 * 60; // retry after 1 hour
257 CLog::Log(LOGERROR
, "CRepository: failed read '{}'", dir
.checksum
);
260 dirChecksums
.emplace_back(dir
, part
);
261 recheckAfterTimes
.push_back(recheckAfterThisDir
);
266 // Default interval: 24 h
267 recheckAfter
= 24 * 60 * 60;
268 if (dirChecksums
.size() == m_dirs
.size() && !dirChecksums
.empty())
270 // Use smallest update interval out of all received (individual intervals per directory are
272 recheckAfter
= *std::min_element(recheckAfterTimes
.begin(), recheckAfterTimes
.end());
273 // If all directories have checksums and they match the last one, nothing has changed
274 if (dirChecksums
.size() == m_dirs
.size() && oldChecksum
== checksum
)
275 return STATUS_NOT_MODIFIED
;
278 for (const auto& dirTuple
: dirChecksums
)
280 std::vector
<AddonInfoPtr
> tmp
;
281 if (!FetchIndex(std::get
<0>(dirTuple
), std::get
<1>(dirTuple
), tmp
))
283 addons
.insert(addons
.end(), tmp
.begin(), tmp
.end());
288 RepositoryDirInfo
CRepository::ParseDirConfiguration(const CAddonExtensions
& configuration
)
290 RepositoryDirInfo dir
;
291 dir
.checksum
= configuration
.GetValue("checksum").asString();
292 std::string checksumStr
= configuration
.GetValue("checksum@verify").asString();
293 if (!checksumStr
.empty())
295 dir
.checksumType
= CDigest::TypeFromString(checksumStr
);
297 dir
.info
= configuration
.GetValue("info").asString();
298 dir
.datadir
= configuration
.GetValue("datadir").asString();
299 dir
.artdir
= configuration
.GetValue("artdir").asString();
300 if (dir
.artdir
.empty())
302 dir
.artdir
= dir
.datadir
;
305 std::string hashStr
= configuration
.GetValue("hashes").asString();
306 StringUtils::ToLower(hashStr
);
307 if (hashStr
== "true")
312 if (!hashStr
.empty() && hashStr
!= "false")
314 dir
.hashType
= CDigest::TypeFromString(hashStr
);
315 if (dir
.hashType
== CDigest::Type::MD5
)
317 CLog::Log(LOGWARNING
, "CRepository::{}: Repository has MD5 hashes enabled - this hash function is broken and will only guard against unintentional data corruption", __FUNCTION__
);
321 dir
.minversion
= CAddonVersion
{configuration
.GetValue("@minversion").asString()};
322 dir
.maxversion
= CAddonVersion
{configuration
.GetValue("@maxversion").asString()};