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.
135 IsProtocol("stack") ||
136 IsProtocol("virtualpath") ||
137 IsProtocol("multipath") ||
138 IsProtocol("special") ||
139 IsProtocol("resource")
142 SetFileName(std::move(strURL
).substr(iPos
));
146 if (IsProtocol("udf") || IsProtocol("iso9660"))
148 std::string
lower(strURL
);
149 StringUtils::ToLower(lower
);
150 size_t isoPos
= lower
.find(".iso\\", iPos
);
151 if (isoPos
== std::string::npos
)
152 isoPos
= lower
.find(".udf\\", iPos
);
153 if (isoPos
!= std::string::npos
)
155 strURL
.replace(isoPos
+ 4, 1, "/");
159 // check for username/password - should occur before first /
160 if (iPos
== std::string::npos
) iPos
= 0;
162 // for protocols supporting options, chop that part off here
163 // maybe we should invert this list instead?
164 size_t iEnd
= strURL
.length();
165 const char* sep
= NULL
;
167 //! @todo fix all Addon paths
168 std::string strProtocol2
= GetTranslatedProtocol();
169 if(IsProtocol("rss") ||
170 IsProtocol("rsss") ||
175 IsProtocol("addons") ||
176 IsProtocol("image") ||
177 IsProtocol("videodb") ||
178 IsProtocol("musicdb") ||
179 IsProtocol("androidapp") ||
183 if( IsProtocolEqual(strProtocol2
, "http")
184 || IsProtocolEqual(strProtocol2
, "https")
185 || IsProtocolEqual(strProtocol2
, "plugin")
186 || IsProtocolEqual(strProtocol2
, "addons")
187 || IsProtocolEqual(strProtocol2
, "rtsp"))
189 else if(IsProtocolEqual(strProtocol2
, "ftp")
190 || IsProtocolEqual(strProtocol2
, "ftps"))
195 size_t iOptions
= strURL
.find_first_of(sep
, iPos
);
196 if (iOptions
!= std::string::npos
)
198 // we keep the initial char as it can be any of the above
199 size_t iProto
= strURL
.find_first_of('|', iOptions
);
200 if (iProto
!= std::string::npos
)
202 SetProtocolOptions(strURL
.substr(iProto
+1));
203 SetOptions(strURL
.substr(iOptions
,iProto
-iOptions
));
206 SetOptions(strURL
.substr(iOptions
));
211 size_t iSlash
= strURL
.find('/', iPos
);
213 iSlash
= std::string::npos
; // was an invalid slash as it was contained in options
215 // also skip parsing username:password@ for udp/rtp as it not valid
216 // and conflicts with the following example: rtp://sourceip@multicastip
217 size_t iAlphaSign
= strURL
.find('@', iPos
);
218 if (iAlphaSign
!= std::string::npos
&& iAlphaSign
< iEnd
&&
219 (iAlphaSign
< iSlash
|| iSlash
== std::string::npos
) &&
220 !IsProtocol("udp") && !IsProtocol("rtp"))
222 // username/password found
223 std::string strUserNamePassword
= strURL
.substr(iPos
, iAlphaSign
- iPos
);
225 // first extract domain, if protocol is smb
226 if (IsProtocol("smb"))
228 size_t iSemiColon
= strUserNamePassword
.find(';');
230 if (iSemiColon
!= std::string::npos
)
232 m_strDomain
= strUserNamePassword
.substr(0, iSemiColon
);
233 strUserNamePassword
.erase(0, iSemiColon
+ 1);
238 size_t iColon
= strUserNamePassword
.find(':');
239 if (iColon
!= std::string::npos
)
241 m_strUserName
= strUserNamePassword
.substr(0, iColon
);
242 m_strPassword
= strUserNamePassword
.substr(iColon
+ 1);
247 m_strUserName
= std::move(strUserNamePassword
);
250 iPos
= iAlphaSign
+ 1;
251 iSlash
= strURL
.find('/', iAlphaSign
);
254 iSlash
= std::string::npos
;
257 std::string strHostNameAndPort
= strURL
.substr(iPos
, (iSlash
== std::string::npos
) ? iEnd
- iPos
: iSlash
- iPos
);
258 // check for IPv6 numerical representation inside [].
259 // if [] found, let's store string inside as hostname
260 // and remove that parsed part from strHostNameAndPort
261 size_t iBrk
= strHostNameAndPort
.rfind(']');
262 if (iBrk
!= std::string::npos
&& strHostNameAndPort
.find('[') == 0)
264 m_strHostName
= strHostNameAndPort
.substr(1, iBrk
-1);
265 strHostNameAndPort
.erase(0, iBrk
+1);
268 // detect hostname:port/ or just :port/ if previous step found [IPv6] format
269 size_t iColon
= strHostNameAndPort
.rfind(':');
270 if (iColon
!= std::string::npos
&& iColon
== strHostNameAndPort
.find(':'))
272 if (m_strHostName
.empty())
273 m_strHostName
= strHostNameAndPort
.substr(0, iColon
);
274 m_iPort
= atoi(strHostNameAndPort
.substr(iColon
+ 1).c_str());
277 // if we still don't have hostname, the strHostNameAndPort substring
278 // is 'just' hostname without :port specification - so use it as is.
279 if (m_strHostName
.empty())
280 m_strHostName
= std::move(strHostNameAndPort
);
282 if (iSlash
!= std::string::npos
)
286 m_strFileName
= strURL
.substr(iPos
, iEnd
- iPos
);
289 if (IsProtocol("musicdb") || IsProtocol("videodb") || IsProtocol("sources") || IsProtocol("pvr"))
291 if (m_strHostName
!= "" && m_strFileName
!= "")
293 m_strFileName
= StringUtils::Format("{}/{}", m_strHostName
, m_strFileName
);
298 if (!m_strHostName
.empty() && strURL
[iEnd
-1]=='/')
299 m_strFileName
= m_strHostName
+ "/";
301 m_strFileName
= m_strHostName
;
306 StringUtils::Replace(m_strFileName
, '\\', '/');
308 /* update extension + sharename */
309 SetFileName(m_strFileName
);
311 /* decode urlencoding on this stuff */
312 if(URIUtils::HasEncodedHostname(*this))
314 m_strHostName
= Decode(m_strHostName
);
315 SetHostName(m_strHostName
);
318 m_strUserName
= Decode(m_strUserName
);
319 m_strPassword
= Decode(m_strPassword
);
322 void CURL::SetFileName(std::string strFileName
)
324 m_strFileName
= std::move(strFileName
);
326 size_t slash
= m_strFileName
.find_last_of(GetDirectorySeparator());
327 size_t period
= m_strFileName
.find_last_of('.');
328 if(period
!= std::string::npos
&& (slash
== std::string::npos
|| period
> slash
))
329 m_strFileType
= m_strFileName
.substr(period
+1);
333 slash
= m_strFileName
.find_first_of(GetDirectorySeparator());
334 if(slash
== std::string::npos
)
335 m_strShareName
= m_strFileName
;
337 m_strShareName
= m_strFileName
.substr(0, slash
);
339 StringUtils::Trim(m_strFileType
);
340 StringUtils::ToLower(m_strFileType
);
343 void CURL::SetProtocol(std::string strProtocol
)
345 m_strProtocol
= std::move(strProtocol
);
346 StringUtils::ToLower(m_strProtocol
);
349 void CURL::SetOptions(std::string strOptions
)
351 m_strOptions
.clear();
353 if( strOptions
.length() > 0)
355 if(strOptions
[0] == '?' ||
356 strOptions
[0] == '#' ||
357 strOptions
[0] == ';' ||
358 strOptions
.find("xml") != std::string::npos
)
360 m_strOptions
= std::move(strOptions
);
361 m_options
.AddOptions(m_strOptions
);
364 CLog::Log(LOGWARNING
, "{} - Invalid options specified for url {}", __FUNCTION__
, strOptions
);
368 void CURL::SetProtocolOptions(std::string strOptions
)
370 m_strProtocolOptions
.clear();
371 m_protocolOptions
.Clear();
372 if (strOptions
.length() > 0)
374 if (strOptions
[0] == '|')
375 m_strProtocolOptions
= std::move(strOptions
).substr(1);
377 m_strProtocolOptions
= std::move(strOptions
);
378 m_protocolOptions
.AddOptions(m_strProtocolOptions
);
382 std::string
CURL::GetTranslatedProtocol() const
384 if (IsProtocol("shout")
386 || IsProtocol("rss"))
389 if (IsProtocol("davs")
390 || IsProtocol("rsss"))
393 return GetProtocol();
396 std::string
CURL::GetFileNameWithoutPath() const
398 // *.zip and *.rar store the actual zip/rar path in the hostname of the url
399 if ((IsProtocol("rar") ||
402 IsProtocol("apk")) &&
403 m_strFileName
.empty())
404 return URIUtils::GetFileName(m_strHostName
);
406 // otherwise, we've already got the filepath, so just grab the filename portion
407 std::string
file(m_strFileName
);
408 URIUtils::RemoveSlashAtEnd(file
);
409 return URIUtils::GetFileName(file
);
413 void protectIPv6(std::string
&hn
)
415 if (!hn
.empty() && hn
.find(':') != hn
.rfind(':') && hn
.find(':') != std::string::npos
)
421 char CURL::GetDirectorySeparator() const
424 //We don't want to use IsLocal here, it can return true
425 //for network protocols that matches localhost or hostname
426 //we only ever want to use \ for win32 local filesystem
427 if ( m_strProtocol
.empty() )
434 std::string
CURL::Get() const
436 if (m_strProtocol
.empty())
437 return m_strFileName
;
439 unsigned int sizeneed
= m_strProtocol
.length()
440 + m_strDomain
.length()
441 + m_strUserName
.length()
442 + m_strPassword
.length()
443 + m_strHostName
.length()
444 + m_strFileName
.length()
445 + m_strOptions
.length()
446 + m_strProtocolOptions
.length()
450 strURL
.reserve(sizeneed
);
452 strURL
= GetWithoutOptions();
454 if( !m_strOptions
.empty() )
455 strURL
+= m_strOptions
;
457 if (!m_strProtocolOptions
.empty())
458 strURL
+= "|"+m_strProtocolOptions
;
463 std::string
CURL::GetWithoutOptions() const
465 if (m_strProtocol
.empty())
466 return m_strFileName
;
468 std::string strGet
= GetWithoutFilename();
470 // Prevent double slash when concatenating host part and filename part
471 if (m_strFileName
.size() && (m_strFileName
[0] == '/' || m_strFileName
[0] == '\\') && URIUtils::HasSlashAtEnd(strGet
))
472 URIUtils::RemoveSlashAtEnd(strGet
);
474 return strGet
+ m_strFileName
;
477 std::string
CURL::GetWithoutUserDetails(bool redact
) const
481 if (IsProtocol("stack"))
484 XFILE::CStackDirectory dir
;
485 dir
.GetDirectory(*this,items
);
486 std::vector
<std::string
> newItems
;
487 for (int i
=0;i
<items
.Size();++i
)
489 CURL
url(items
[i
]->GetPath());
490 items
[i
]->SetPath(url
.GetWithoutUserDetails(redact
));
491 newItems
.push_back(items
[i
]->GetPath());
493 dir
.ConstructStackPath(newItems
, strURL
);
497 unsigned int sizeneed
= m_strProtocol
.length()
498 + m_strHostName
.length()
499 + m_strFileName
.length()
500 + m_strOptions
.length()
501 + m_strProtocolOptions
.length()
504 if (redact
&& !m_strUserName
.empty())
506 sizeneed
+= sizeof("USERNAME");
507 if (!m_strPassword
.empty())
508 sizeneed
+= sizeof(":PASSWORD@");
509 if (!m_strDomain
.empty())
510 sizeneed
+= sizeof("DOMAIN;");
513 strURL
.reserve(sizeneed
);
515 if (m_strProtocol
.empty())
516 return m_strFileName
;
518 strURL
= m_strProtocol
;
521 if (redact
&& !m_strUserName
.empty())
523 if (!m_strDomain
.empty())
525 strURL
+= "USERNAME";
526 if (!m_strPassword
.empty())
527 strURL
+= ":PASSWORD";
531 if (!m_strHostName
.empty())
533 std::string strHostName
;
535 if (URIUtils::HasParentInHostname(*this))
536 strHostName
= CURL(m_strHostName
).GetWithoutUserDetails();
538 strHostName
= m_strHostName
;
540 if (URIUtils::HasEncodedHostname(*this))
541 strHostName
= Encode(strHostName
);
545 protectIPv6(strHostName
);
546 strURL
+= strHostName
+ StringUtils::Format(":{}", m_iPort
);
549 strURL
+= strHostName
;
553 strURL
+= m_strFileName
;
555 if( m_strOptions
.length() > 0 )
556 strURL
+= m_strOptions
;
557 if( m_strProtocolOptions
.length() > 0 )
558 strURL
+= "|"+m_strProtocolOptions
;
563 std::string
CURL::GetWithoutFilename() const
565 if (m_strProtocol
.empty())
568 unsigned int sizeneed
= m_strProtocol
.length()
569 + m_strDomain
.length()
570 + m_strUserName
.length()
571 + m_strPassword
.length()
572 + m_strHostName
.length()
576 strURL
.reserve(sizeneed
);
578 strURL
= m_strProtocol
;
581 if (!m_strUserName
.empty())
583 if (!m_strDomain
.empty())
585 strURL
+= Encode(m_strDomain
);
588 strURL
+= Encode(m_strUserName
);
589 if (!m_strPassword
.empty())
592 strURL
+= Encode(m_strPassword
);
597 if (!m_strHostName
.empty())
599 std::string hostname
;
601 if( URIUtils::HasEncodedHostname(*this) )
602 hostname
= Encode(m_strHostName
);
604 hostname
= m_strHostName
;
608 protectIPv6(hostname
);
609 strURL
+= hostname
+ StringUtils::Format(":{}", m_iPort
);
620 std::string
CURL::GetRedacted() const
622 return GetWithoutUserDetails(true);
625 std::string
CURL::GetRedacted(std::string path
)
627 return CURL(std::move(path
)).GetRedacted();
630 bool CURL::IsLocal() const
632 return (m_strProtocol
.empty() || IsLocalHost() || IsProtocol("win-lib"));
635 bool CURL::IsLocalHost() const
637 return CServiceBroker::GetNetwork().IsLocalHost(m_strHostName
);
640 bool CURL::IsFileOnly(const std::string
&url
)
642 return url
.find_first_of("/\\") == std::string::npos
;
645 bool CURL::IsFullPath(const std::string
&url
)
647 if (url
.size() && url
[0] == '/') return true; // /foo/bar.ext
648 if (url
.find("://") != std::string::npos
) return true; // foo://bar.ext
649 if (url
.size() > 1 && url
[1] == ':') return true; // c:\\foo\\bar\\bar.ext
650 if (StringUtils::StartsWith(url
, "\\\\")) return true; // \\UNC\path\to\file
654 std::string
CURL::Decode(std::string_view strURLData
)
655 //modified to be more accommodating - if a non hex value follows a % take the characters directly and don't raise an error.
656 // However % characters should really be escaped like any other non safe character (www.rfc-editor.org/rfc/rfc1738.txt)
658 std::string strResult
;
660 /* result will always be less than source */
661 strResult
.reserve( strURLData
.length() );
663 const char* const iterEnd
= strURLData
.data() + strURLData
.size();
664 for (const char* iter
= strURLData
.data(); iter
< iterEnd
; ++iter
)
668 else if (*iter
== '%')
670 if (std::distance(iter
, iterEnd
) >= 3)
673 const std::from_chars_result res
= std::from_chars(iter
+ 1, iter
+ 3, dec_num
, 16);
674 if (res
.ec
!= std::errc() || res
.ptr
!= iter
+ 3)
678 strResult
+= (char)dec_num
;
692 std::string
CURL::Encode(std::string_view strURLData
)
694 std::string strResult
;
696 /* wonder what a good value is here is, depends on how often it occurs */
697 strResult
.reserve( strURLData
.length() * 2 );
699 for (auto kar
: strURLData
)
701 // Don't URL encode "-_.!()" according to RFC1738
702 //! @todo Update it to "-_.~" after Gotham according to RFC3986
703 if (StringUtils::isasciialphanum(kar
) || kar
== '-' || kar
== '.' || kar
== '_' || kar
== '!' || kar
== '(' || kar
== ')')
704 strResult
.push_back(kar
);
706 fmt::format_to(std::back_insert_iterator(strResult
), "%{:02x}",
707 (unsigned int)((unsigned char)kar
));
713 bool CURL::IsProtocolEqual(const std::string
&protocol
, const char *type
)
716 NOTE: We're currently using == here as m_strProtocol is assigned as lower-case in SetProtocol(),
717 and we've assumed all other callers are calling with protocol lower-case otherwise.
718 We possibly shouldn't do this (as CURL(foo).Get() != foo, though there are other reasons for this as well)
719 but it handles the requirements of RFC-1738 which allows the scheme to be case-insensitive.
722 return protocol
== type
;
726 void CURL::GetOptions(std::map
<std::string
, std::string
> &options
) const
728 CUrlOptions::UrlOptions optionsMap
= m_options
.GetOptions();
729 for (CUrlOptions::UrlOptions::const_iterator option
= optionsMap
.begin();
730 option
!= optionsMap
.end(); ++option
)
731 options
[option
->first
] = option
->second
.asString();
734 bool CURL::HasOption(const std::string
&key
) const
736 return m_options
.HasOption(key
);
739 bool CURL::GetOption(const std::string
&key
, std::string
&value
) const
742 if (!m_options
.GetOption(key
, valueObj
))
745 value
= valueObj
.asString();
749 std::string
CURL::GetOption(const std::string
&key
) const
752 if (!GetOption(key
, value
))
758 void CURL::SetOption(const std::string
&key
, const std::string
&value
)
760 m_options
.AddOption(key
, value
);
761 SetOptions(m_options
.GetOptionsString(true));
764 void CURL::RemoveOption(const std::string
&key
)
766 m_options
.RemoveOption(key
);
767 SetOptions(m_options
.GetOptionsString(true));
770 void CURL::GetProtocolOptions(std::map
<std::string
, std::string
> &options
) const
772 CUrlOptions::UrlOptions optionsMap
= m_protocolOptions
.GetOptions();
773 for (CUrlOptions::UrlOptions::const_iterator option
= optionsMap
.begin();
774 option
!= optionsMap
.end(); ++option
)
775 options
[option
->first
] = option
->second
.asString();
778 bool CURL::HasProtocolOption(const std::string
&key
) const
780 return m_protocolOptions
.HasOption(key
);
783 bool CURL::GetProtocolOption(const std::string
&key
, std::string
&value
) const
786 if (!m_protocolOptions
.GetOption(key
, valueObj
))
789 value
= valueObj
.asString();
793 std::string
CURL::GetProtocolOption(const std::string
&key
) const
796 if (!GetProtocolOption(key
, value
))
802 void CURL::SetProtocolOption(const std::string
&key
, const std::string
&value
)
804 m_protocolOptions
.AddOption(key
, value
);
805 m_strProtocolOptions
= m_protocolOptions
.GetOptionsString(false);
808 void CURL::RemoveProtocolOption(const std::string
&key
)
810 m_protocolOptions
.RemoveOption(key
);
811 m_strProtocolOptions
= m_protocolOptions
.GetOptionsString(false);