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/XBMCTinyXML.h"
30 #include "utils/log.h"
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);
51 if (dirIt
== m_dirs
.end())
53 CLog::Log(LOGERROR
, "Requested path {} not found in known repository directories", path
);
57 if (dirIt
->hashType
== CDigest::Type::INVALID
)
59 // We have a path, but need no hash
63 // Do not follow mirror redirect, we want the headers of the redirect response
65 url
.SetProtocolOption("redirect-limit", "0");
69 CLog::Log(LOGERROR
, "Could not fetch addon location and hash from {}", path
);
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
)))};
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
);
93 // Fall back to original URL if we do not get a redirect
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
);
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())
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.",
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",
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
160 // we intentionally avoid using file.GetLength() for
161 // Transfer-Encoding: chunked servers.
162 std::stringstream ss
;
165 while ((read
= file
.Read(temp
, sizeof(temp
))) > 0)
166 ss
.write(temp
, read
);
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
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)
189 std::max(std::min(std::stoi(recheckAfterHeader
), 24 * 7 * 60 * 60), 1 * 60 * 60);
193 CLog::Log(LOGWARNING
, "Could not parse X-Kodi-Recheck-After header value '{}' from {}",
194 recheckAfterHeader
, url
);
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
);
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
);
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
);
229 if (!CZipFile::DecompressGzip(response
, buffer
))
231 CLog::Log(LOGERROR
, "CRepository: failed to decompress gzip from '{}'", repo
.info
);
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
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())
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
);
261 dirChecksums
.emplace_back(dir
, part
);
262 recheckAfterTimes
.push_back(recheckAfterThisDir
);
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
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
))
284 addons
.insert(addons
.end(), tmp
.begin(), tmp
.end());
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")
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()};