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.
12 #include "FileItemList.h"
13 #include "ServiceBroker.h"
15 #include "filesystem/File.h"
16 #include "filesystem/StackDirectory.h"
17 #include "network/Network.h"
18 #include "utils/StringUtils.h"
19 #include "utils/URIUtils.h"
20 #include "utils/log.h"
28 #include <system_error>
31 #include <fmt/xchar.h>
33 using namespace ADDON
;
35 CURL::~CURL() = default;
39 m_strHostName
.clear();
41 m_strUserName
.clear();
42 m_strPassword
.clear();
43 m_strShareName
.clear();
44 m_strFileName
.clear();
45 m_strProtocol
.clear();
46 m_strFileType
.clear();
48 m_strProtocolOptions
.clear();
50 m_protocolOptions
.Clear();
54 void CURL::Parse(std::string strURL1
)
57 // start by validating the path
58 std::string strURL
= CUtil::ValidatePath(std::move(strURL1
));
60 // strURL can be one of the following:
61 // format 1: protocol://[username:password]@hostname[:port]/directoryandfile
62 // format 2: protocol://file
63 // format 3: drive:directoryandfile
65 // first need 2 check if this is a protocol or just a normal drive & path
66 if (!strURL
.size()) return ;
67 if (strURL
== "?") return;
69 // form is format 1 or 2
70 // format 1: protocol://[domain;][username:password]@hostname[:port]/directoryandfile
71 // format 2: protocol://file
74 size_t iPos
= strURL
.find("://");
75 if (iPos
== std::string::npos
)
77 // This is an ugly hack that needs some work.
78 // example: filename /foo/bar.zip/alice.rar/bob.avi
79 // This should turn into zip://rar:///foo/bar.zip/alice.rar/bob.avi
81 bool is_apk
= (strURL
.find(".apk/", iPos
) != std::string::npos
);
85 iPos
= strURL
.find(".apk/", iPos
);
87 iPos
= strURL
.find(".zip/", iPos
);
90 if (iPos
== std::string::npos
)
92 /* set filename and update extension*/
93 SetFileName(std::move(strURL
));
97 std::string archiveName
= strURL
.substr(0, iPos
);
99 if (XFILE::CFile::Stat(archiveName
, &s
) == 0)
102 if (!S_ISDIR(s
.st_mode
))
104 if (!(s
.st_mode
& S_IFDIR
))
107 archiveName
= Encode(archiveName
);
110 CURL
c("apk://" + archiveName
+ "/" + std::move(strURL
).substr(iPos
+ 1));
115 CURL
c("zip://" + archiveName
+ "/" + std::move(strURL
).substr(iPos
+ 1));
125 SetProtocol(strURL
.substr(0, iPos
));
130 // why not handle all format 2 (protocol://file) style urls here?
131 // ones that come to mind are iso9660, cdda, musicdb, etc.
132 // they are all local protocols and have no server part, port number, special options, etc.
133 // this removes the need for special handling below.
136 IsProtocol("stack") ||
137 IsProtocol("virtualpath") ||
138 IsProtocol("multipath") ||
139 IsProtocol("special") ||
140 IsProtocol("resource") ||
145 SetFileName(std::move(strURL
).substr(iPos
));
149 if (IsProtocol("udf") || IsProtocol("iso9660"))
151 std::string
lower(strURL
);
152 StringUtils::ToLower(lower
);
153 size_t isoPos
= lower
.find(".iso\\", iPos
);
154 if (isoPos
== std::string::npos
)
155 isoPos
= lower
.find(".udf\\", iPos
);
156 if (isoPos
!= std::string::npos
)
158 strURL
.replace(isoPos
+ 4, 1, "/");
162 // check for username/password - should occur before first /
163 if (iPos
== std::string::npos
) iPos
= 0;
165 // for protocols supporting options, chop that part off here
166 // maybe we should invert this list instead?
167 size_t iEnd
= strURL
.length();
168 const char* sep
= NULL
;
170 //! @todo fix all Addon paths
171 std::string strProtocol2
= GetTranslatedProtocol();
172 if(IsProtocol("rss") ||
173 IsProtocol("rsss") ||
178 IsProtocol("addons") ||
179 IsProtocol("image") ||
180 IsProtocol("videodb") ||
181 IsProtocol("musicdb") ||
182 IsProtocol("androidapp") ||
186 if( IsProtocolEqual(strProtocol2
, "http")
187 || IsProtocolEqual(strProtocol2
, "https")
188 || IsProtocolEqual(strProtocol2
, "plugin")
189 || IsProtocolEqual(strProtocol2
, "addons")
190 || IsProtocolEqual(strProtocol2
, "rtsp"))
192 else if(IsProtocolEqual(strProtocol2
, "ftp")
193 || IsProtocolEqual(strProtocol2
, "ftps"))
198 size_t iOptions
= strURL
.find_first_of(sep
, iPos
);
199 if (iOptions
!= std::string::npos
)
201 // we keep the initial char as it can be any of the above
202 size_t iProto
= strURL
.find_first_of('|', iOptions
);
203 if (iProto
!= std::string::npos
)
205 SetProtocolOptions(strURL
.substr(iProto
+1));
206 SetOptions(strURL
.substr(iOptions
,iProto
-iOptions
));
209 SetOptions(strURL
.substr(iOptions
));
214 size_t iSlash
= strURL
.find('/', iPos
);
216 iSlash
= std::string::npos
; // was an invalid slash as it was contained in options
218 // also skip parsing username:password@ for udp/rtp as it not valid
219 // and conflicts with the following example: rtp://sourceip@multicastip
220 size_t iAlphaSign
= strURL
.find('@', iPos
);
221 if (iAlphaSign
!= std::string::npos
&& iAlphaSign
< iEnd
&&
222 (iAlphaSign
< iSlash
|| iSlash
== std::string::npos
) &&
223 !IsProtocol("udp") && !IsProtocol("rtp"))
225 // username/password found
226 std::string strUserNamePassword
= strURL
.substr(iPos
, iAlphaSign
- iPos
);
228 // first extract domain, if protocol is smb
229 if (IsProtocol("smb"))
231 size_t iSemiColon
= strUserNamePassword
.find(';');
233 if (iSemiColon
!= std::string::npos
)
235 m_strDomain
= strUserNamePassword
.substr(0, iSemiColon
);
236 strUserNamePassword
.erase(0, iSemiColon
+ 1);
241 size_t iColon
= strUserNamePassword
.find(':');
242 if (iColon
!= std::string::npos
)
244 m_strUserName
= strUserNamePassword
.substr(0, iColon
);
245 m_strPassword
= strUserNamePassword
.substr(iColon
+ 1);
250 m_strUserName
= std::move(strUserNamePassword
);
253 iPos
= iAlphaSign
+ 1;
254 iSlash
= strURL
.find('/', iAlphaSign
);
257 iSlash
= std::string::npos
;
260 std::string strHostNameAndPort
= strURL
.substr(iPos
, (iSlash
== std::string::npos
) ? iEnd
- iPos
: iSlash
- iPos
);
261 // check for IPv6 numerical representation inside [].
262 // if [] found, let's store string inside as hostname
263 // and remove that parsed part from strHostNameAndPort
264 size_t iBrk
= strHostNameAndPort
.rfind(']');
265 if (iBrk
!= std::string::npos
&& strHostNameAndPort
.starts_with('['))
267 m_strHostName
= strHostNameAndPort
.substr(1, iBrk
-1);
268 strHostNameAndPort
.erase(0, iBrk
+1);
271 // detect hostname:port/ or just :port/ if previous step found [IPv6] format
272 size_t iColon
= strHostNameAndPort
.rfind(':');
273 if (iColon
!= std::string::npos
&& iColon
== strHostNameAndPort
.find(':'))
275 if (m_strHostName
.empty())
276 m_strHostName
= strHostNameAndPort
.substr(0, iColon
);
277 m_iPort
= atoi(strHostNameAndPort
.substr(iColon
+ 1).c_str());
280 // if we still don't have hostname, the strHostNameAndPort substring
281 // is 'just' hostname without :port specification - so use it as is.
282 if (m_strHostName
.empty())
283 m_strHostName
= std::move(strHostNameAndPort
);
285 if (iSlash
!= std::string::npos
)
289 m_strFileName
= strURL
.substr(iPos
, iEnd
- iPos
);
292 if (IsProtocol("musicdb") || IsProtocol("videodb") || IsProtocol("sources") || IsProtocol("pvr"))
294 if (m_strHostName
!= "" && m_strFileName
!= "")
296 m_strFileName
= StringUtils::Format("{}/{}", m_strHostName
, m_strFileName
);
301 if (!m_strHostName
.empty() && strURL
[iEnd
-1]=='/')
302 m_strFileName
= m_strHostName
+ "/";
304 m_strFileName
= m_strHostName
;
309 StringUtils::Replace(m_strFileName
, '\\', '/');
311 /* update extension + sharename */
312 SetFileName(m_strFileName
);
314 /* decode urlencoding on this stuff */
315 if(URIUtils::HasEncodedHostname(*this))
317 m_strHostName
= Decode(m_strHostName
);
318 SetHostName(m_strHostName
);
321 m_strUserName
= Decode(m_strUserName
);
322 m_strPassword
= Decode(m_strPassword
);
325 void CURL::SetFileName(std::string strFileName
)
327 m_strFileName
= std::move(strFileName
);
329 size_t slash
= m_strFileName
.find_last_of(GetDirectorySeparator());
330 size_t period
= m_strFileName
.find_last_of('.');
331 if(period
!= std::string::npos
&& (slash
== std::string::npos
|| period
> slash
))
332 m_strFileType
= m_strFileName
.substr(period
+1);
336 slash
= m_strFileName
.find_first_of(GetDirectorySeparator());
337 if(slash
== std::string::npos
)
338 m_strShareName
= m_strFileName
;
340 m_strShareName
= m_strFileName
.substr(0, slash
);
342 StringUtils::Trim(m_strFileType
);
343 StringUtils::ToLower(m_strFileType
);
346 void CURL::SetProtocol(std::string strProtocol
)
348 m_strProtocol
= std::move(strProtocol
);
349 StringUtils::ToLower(m_strProtocol
);
352 void CURL::SetOptions(std::string strOptions
)
354 m_strOptions
.clear();
356 if( strOptions
.length() > 0)
358 if(strOptions
[0] == '?' ||
359 strOptions
[0] == '#' ||
360 strOptions
[0] == ';' ||
361 strOptions
.find("xml") != std::string::npos
)
363 m_strOptions
= std::move(strOptions
);
364 m_options
.AddOptions(m_strOptions
);
367 CLog::Log(LOGWARNING
, "{} - Invalid options specified for url {}", __FUNCTION__
, strOptions
);
371 void CURL::SetProtocolOptions(std::string strOptions
)
373 m_strProtocolOptions
.clear();
374 m_protocolOptions
.Clear();
375 if (strOptions
.length() > 0)
377 if (strOptions
[0] == '|')
378 m_strProtocolOptions
= std::move(strOptions
).substr(1);
380 m_strProtocolOptions
= std::move(strOptions
);
381 m_protocolOptions
.AddOptions(m_strProtocolOptions
);
385 std::string
CURL::GetTranslatedProtocol() const
387 if (IsProtocol("shout")
389 || IsProtocol("rss"))
392 if (IsProtocol("davs")
393 || IsProtocol("rsss"))
396 return GetProtocol();
399 std::string
CURL::GetFileNameWithoutPath() const
401 // *.zip and *.rar store the actual zip/rar path in the hostname of the url
402 if ((IsProtocol("rar") ||
405 IsProtocol("apk")) &&
406 m_strFileName
.empty())
407 return URIUtils::GetFileName(m_strHostName
);
409 // otherwise, we've already got the filepath, so just grab the filename portion
410 std::string
file(m_strFileName
);
411 URIUtils::RemoveSlashAtEnd(file
);
412 return URIUtils::GetFileName(file
);
416 void protectIPv6(std::string
&hn
)
418 if (!hn
.empty() && hn
.find(':') != hn
.rfind(':') && hn
.find(':') != std::string::npos
)
424 char CURL::GetDirectorySeparator() const
427 //We don't want to use IsLocal here, it can return true
428 //for network protocols that matches localhost or hostname
429 //we only ever want to use \ for win32 local filesystem
430 if ( m_strProtocol
.empty() )
437 std::string
CURL::Get() const
439 if (m_strProtocol
.empty())
440 return m_strFileName
;
442 unsigned int sizeneed
= m_strProtocol
.length()
443 + m_strDomain
.length()
444 + m_strUserName
.length()
445 + m_strPassword
.length()
446 + m_strHostName
.length()
447 + m_strFileName
.length()
448 + m_strOptions
.length()
449 + m_strProtocolOptions
.length()
453 strURL
.reserve(sizeneed
);
455 strURL
= GetWithoutOptions();
457 if( !m_strOptions
.empty() )
458 strURL
+= m_strOptions
;
460 if (!m_strProtocolOptions
.empty())
461 strURL
+= "|"+m_strProtocolOptions
;
466 std::string
CURL::GetWithoutOptions() const
468 if (m_strProtocol
.empty())
469 return m_strFileName
;
471 std::string strGet
= GetWithoutFilename();
473 // Prevent double slash when concatenating host part and filename part
474 if (m_strFileName
.size() && (m_strFileName
[0] == '/' || m_strFileName
[0] == '\\') && URIUtils::HasSlashAtEnd(strGet
))
475 URIUtils::RemoveSlashAtEnd(strGet
);
477 return strGet
+ m_strFileName
;
480 std::string
CURL::GetWithoutUserDetails(bool redact
) const
484 if (IsProtocol("stack"))
487 XFILE::CStackDirectory dir
;
488 dir
.GetDirectory(*this,items
);
489 std::vector
<std::string
> newItems
;
490 for (int i
=0;i
<items
.Size();++i
)
492 CURL
url(items
[i
]->GetPath());
493 items
[i
]->SetPath(url
.GetWithoutUserDetails(redact
));
494 newItems
.push_back(items
[i
]->GetPath());
496 dir
.ConstructStackPath(newItems
, strURL
);
500 unsigned int sizeneed
= m_strProtocol
.length()
501 + m_strHostName
.length()
502 + m_strFileName
.length()
503 + m_strOptions
.length()
504 + m_strProtocolOptions
.length()
507 if (redact
&& !m_strUserName
.empty())
509 sizeneed
+= sizeof("USERNAME");
510 if (!m_strPassword
.empty())
511 sizeneed
+= sizeof(":PASSWORD@");
512 if (!m_strDomain
.empty())
513 sizeneed
+= sizeof("DOMAIN;");
516 strURL
.reserve(sizeneed
);
518 if (m_strProtocol
.empty())
519 return m_strFileName
;
521 strURL
= m_strProtocol
;
524 if (redact
&& !m_strUserName
.empty())
526 if (!m_strDomain
.empty())
528 strURL
+= "USERNAME";
529 if (!m_strPassword
.empty())
530 strURL
+= ":PASSWORD";
534 if (!m_strHostName
.empty())
536 std::string strHostName
;
538 if (URIUtils::HasParentInHostname(*this))
539 strHostName
= CURL(m_strHostName
).GetWithoutUserDetails();
541 strHostName
= m_strHostName
;
543 if (URIUtils::HasEncodedHostname(*this))
544 strHostName
= Encode(strHostName
);
548 protectIPv6(strHostName
);
549 strURL
+= strHostName
+ StringUtils::Format(":{}", m_iPort
);
552 strURL
+= strHostName
;
556 strURL
+= m_strFileName
;
558 if( m_strOptions
.length() > 0 )
559 strURL
+= m_strOptions
;
560 if( m_strProtocolOptions
.length() > 0 )
561 strURL
+= "|"+m_strProtocolOptions
;
566 std::string
CURL::GetWithoutFilename() const
568 if (m_strProtocol
.empty())
571 unsigned int sizeneed
= m_strProtocol
.length()
572 + m_strDomain
.length()
573 + m_strUserName
.length()
574 + m_strPassword
.length()
575 + m_strHostName
.length()
579 strURL
.reserve(sizeneed
);
581 strURL
= m_strProtocol
;
584 if (!m_strUserName
.empty())
586 if (!m_strDomain
.empty())
588 strURL
+= Encode(m_strDomain
);
591 strURL
+= Encode(m_strUserName
);
592 if (!m_strPassword
.empty())
595 strURL
+= Encode(m_strPassword
);
600 if (!m_strHostName
.empty())
602 std::string hostname
;
604 if( URIUtils::HasEncodedHostname(*this) )
605 hostname
= Encode(m_strHostName
);
607 hostname
= m_strHostName
;
611 protectIPv6(hostname
);
612 strURL
+= hostname
+ StringUtils::Format(":{}", m_iPort
);
623 std::string
CURL::GetRedacted() const
625 return GetWithoutUserDetails(true);
628 std::string
CURL::GetRedacted(std::string path
)
630 return CURL(std::move(path
)).GetRedacted();
633 bool CURL::IsLocal() const
635 return (m_strProtocol
.empty() || IsLocalHost() || IsProtocol("win-lib"));
638 bool CURL::IsLocalHost() const
640 return CServiceBroker::GetNetwork().IsLocalHost(m_strHostName
);
643 bool CURL::IsFileOnly(const std::string
&url
)
645 return url
.find_first_of("/\\") == std::string::npos
;
648 bool CURL::IsFullPath(const std::string
&url
)
650 if (url
.size() && url
[0] == '/') return true; // /foo/bar.ext
651 if (url
.find("://") != std::string::npos
) return true; // foo://bar.ext
652 if (url
.size() > 1 && url
[1] == ':') return true; // c:\\foo\\bar\\bar.ext
653 if (StringUtils::StartsWith(url
, "\\\\")) return true; // \\UNC\path\to\file
657 std::string
CURL::Decode(std::string_view strURLData
)
658 //modified to be more accommodating - if a non hex value follows a % take the characters directly and don't raise an error.
659 // However % characters should really be escaped like any other non safe character (www.rfc-editor.org/rfc/rfc1738.txt)
661 std::string strResult
;
663 /* result will always be less than source */
664 strResult
.reserve( strURLData
.length() );
666 const char* const iterEnd
= strURLData
.data() + strURLData
.size();
667 for (const char* iter
= strURLData
.data(); iter
< iterEnd
; ++iter
)
671 else if (*iter
== '%')
673 if (std::distance(iter
, iterEnd
) >= 3)
676 const std::from_chars_result res
= std::from_chars(iter
+ 1, iter
+ 3, dec_num
, 16);
677 if (res
.ec
!= std::errc() || res
.ptr
!= iter
+ 3)
681 strResult
+= (char)dec_num
;
695 std::string
CURL::Encode(std::string_view strURLData
)
697 std::string strResult
;
699 /* wonder what a good value is here is, depends on how often it occurs */
700 strResult
.reserve( strURLData
.length() * 2 );
702 for (auto kar
: strURLData
)
704 // Don't URL encode "-_.!()" according to RFC1738
705 //! @todo Update it to "-_.~" after Gotham according to RFC3986
706 if (StringUtils::isasciialphanum(kar
) || kar
== '-' || kar
== '.' || kar
== '_' || kar
== '!' || kar
== '(' || kar
== ')')
707 strResult
.push_back(kar
);
709 fmt::format_to(std::back_insert_iterator(strResult
), "%{:02x}",
710 (unsigned int)((unsigned char)kar
));
716 bool CURL::IsProtocolEqual(const std::string
&protocol
, const char *type
)
719 NOTE: We're currently using == here as m_strProtocol is assigned as lower-case in SetProtocol(),
720 and we've assumed all other callers are calling with protocol lower-case otherwise.
721 We possibly shouldn't do this (as CURL(foo).Get() != foo, though there are other reasons for this as well)
722 but it handles the requirements of RFC-1738 which allows the scheme to be case-insensitive.
725 return protocol
== type
;
729 void CURL::GetOptions(std::map
<std::string
, std::string
> &options
) const
731 CUrlOptions::UrlOptions optionsMap
= m_options
.GetOptions();
732 for (CUrlOptions::UrlOptions::const_iterator option
= optionsMap
.begin();
733 option
!= optionsMap
.end(); ++option
)
734 options
[option
->first
] = option
->second
.asString();
737 bool CURL::HasOption(const std::string
&key
) const
739 return m_options
.HasOption(key
);
742 bool CURL::GetOption(const std::string
&key
, std::string
&value
) const
745 if (!m_options
.GetOption(key
, valueObj
))
748 value
= valueObj
.asString();
752 std::string
CURL::GetOption(const std::string
&key
) const
755 if (!GetOption(key
, value
))
761 void CURL::SetOption(const std::string
&key
, const std::string
&value
)
763 m_options
.AddOption(key
, value
);
764 SetOptions(m_options
.GetOptionsString(true));
767 void CURL::RemoveOption(const std::string
&key
)
769 m_options
.RemoveOption(key
);
770 SetOptions(m_options
.GetOptionsString(true));
773 void CURL::GetProtocolOptions(std::map
<std::string
, std::string
> &options
) const
775 CUrlOptions::UrlOptions optionsMap
= m_protocolOptions
.GetOptions();
776 for (CUrlOptions::UrlOptions::const_iterator option
= optionsMap
.begin();
777 option
!= optionsMap
.end(); ++option
)
778 options
[option
->first
] = option
->second
.asString();
781 bool CURL::HasProtocolOption(const std::string
&key
) const
783 return m_protocolOptions
.HasOption(key
);
786 bool CURL::GetProtocolOption(const std::string
&key
, std::string
&value
) const
789 if (!m_protocolOptions
.GetOption(key
, valueObj
))
792 value
= valueObj
.asString();
796 std::string
CURL::GetProtocolOption(const std::string
&key
) const
799 if (!GetProtocolOption(key
, value
))
805 void CURL::SetProtocolOption(const std::string
&key
, const std::string
&value
)
807 m_protocolOptions
.AddOption(key
, value
);
808 m_strProtocolOptions
= m_protocolOptions
.GetOptionsString(false);
811 void CURL::RemoveProtocolOption(const std::string
&key
)
813 m_protocolOptions
.RemoveOption(key
);
814 m_strProtocolOptions
= m_protocolOptions
.GetOptionsString(false);