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 // SMBFile.cpp: implementation of the CSMBFile class.
11 //////////////////////////////////////////////////////////////////////
15 #include "PasswordManager.h"
16 #include "SMBDirectory.h"
17 #include "ServiceBroker.h"
19 #include "commons/Exception.h"
20 #include "filesystem/SpecialProtocol.h"
21 #include "network/DNSNameCache.h"
22 #include "settings/AdvancedSettings.h"
23 #include "settings/Settings.h"
24 #include "settings/SettingsComponent.h"
25 #include "utils/StringUtils.h"
26 #include "utils/TimeUtils.h"
27 #include "utils/URIUtils.h"
28 #include "utils/log.h"
35 #include <libsmbclient.h>
37 using namespace XFILE
;
39 void xb_smbc_log(void* private_ptr
, int level
, const char* msg
)
41 const int logLevel
= [level
]()
54 if (std::strchr(msg
, '@'))
56 // redact User/pass in URLs
57 static const std::regex
redact("(\\w+://)\\S+:\\S+@");
58 CLog::Log(logLevel
, "smb: {}", std::regex_replace(msg
, redact
, "$1USERNAME:PASSWORD@"));
61 CLog::Log(logLevel
, "smb: {}", msg
);
64 void xb_smbc_auth(const char *srv
, const char *shr
, char *wg
, int wglen
,
65 char *un
, int unlen
, char *pw
, int pwlen
)
69 // WTF is this ?, we get the original server cache only
70 // to set the server cache to this function which call the
71 // original one anyway. Seems quite silly.
72 smbc_get_cached_srv_fn orig_cache
;
73 SMBCSRV
* xb_smbc_cache(SMBCCTX
* c
, const char* server
, const char* share
, const char* workgroup
, const char* username
)
75 return orig_cache(c
, server
, share
, workgroup
, username
);
78 bool CSMB::IsFirstInit
= true;
83 m_OpenConnections
= 0;
94 std::unique_lock
<CCriticalSection
> lock(*this);
96 /* samba goes loco if deinited while it has some files opened */
99 smbc_set_context(NULL
);
100 smbc_free_context(m_context
, 1);
107 std::unique_lock
<CCriticalSection
> lock(*this);
111 const std::shared_ptr
<CSettings
> settings
= CServiceBroker::GetSettingsComponent()->GetSettings();
113 // force libsmbclient to use our own smb.conf by overriding HOME
114 std::string
truehome(getenv("HOME"));
115 setenv("HOME", CSpecialProtocol::TranslatePath("special://home").c_str(), 1);
117 // Create ~/.kodi/.smb/smb.conf. This file is used by libsmbclient.
118 // http://us1.samba.org/samba/docs/man/manpages-3/libsmbclient.7.html
119 // http://us1.samba.org/samba/docs/man/manpages-3/smb.conf.5.html
120 std::string smb_conf
;
121 std::string
home(getenv("HOME"));
122 URIUtils::RemoveSlashAtEnd(home
);
123 smb_conf
= home
+ "/.smb";
124 int result
= mkdir(smb_conf
.c_str(), 0755);
125 if (result
== 0 || (errno
== EEXIST
&& IsFirstInit
))
127 smb_conf
+= "/smb.conf";
128 FILE* f
= fopen(smb_conf
.c_str(), "w");
131 fprintf(f
, "[global]\n");
133 fprintf(f
, "\tlock directory = %s/.smb/\n", home
.c_str());
135 // set minimum smbclient protocol version
136 switch (settings
->GetInt(CSettings::SETTING_SMB_MINPROTOCOL
))
142 fprintf(f
, "\tclient min protocol = NT1\n");
145 fprintf(f
, "\tclient min protocol = SMB2_02\n");
148 fprintf(f
, "\tclient min protocol = SMB2_10\n");
151 fprintf(f
, "\tclient min protocol = SMB3\n");
155 // set maximum smbclient protocol version
156 switch (settings
->GetInt(CSettings::SETTING_SMB_MAXPROTOCOL
))
162 fprintf(f
, "\tclient max protocol = NT1\n");
165 fprintf(f
, "\tclient max protocol = SMB2_02\n");
168 fprintf(f
, "\tclient max protocol = SMB2_10\n");
171 fprintf(f
, "\tclient max protocol = SMB3\n");
175 // set legacy security options
176 if (settings
->GetBool(CSettings::SETTING_SMB_LEGACYSECURITY
) && (settings
->GetInt(CSettings::SETTING_SMB_MAXPROTOCOL
) == 1))
178 fprintf(f
, "\tclient NTLMv2 auth = no\n");
179 fprintf(f
, "\tclient use spnego = no\n");
182 // set wins server if there's one. name resolve order defaults to 'lmhosts host wins bcast'.
183 // if no WINS server has been specified the wins method will be ignored.
184 if (settings
->GetString(CSettings::SETTING_SMB_WINSSERVER
).length() > 0 && !StringUtils::EqualsNoCase(settings
->GetString(CSettings::SETTING_SMB_WINSSERVER
), "0.0.0.0") )
186 fprintf(f
, "\twins server = %s\n", settings
->GetString(CSettings::SETTING_SMB_WINSSERVER
).c_str());
187 fprintf(f
, "\tname resolve order = bcast wins host\n");
190 fprintf(f
, "\tname resolve order = bcast host\n");
192 // use user-configured charset. if no charset is specified,
193 // samba tries to use charset 850 but falls back to ASCII in case it is not available
194 if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_sambadoscodepage
.length() > 0)
195 fprintf(f
, "\tdos charset = %s\n", CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_sambadoscodepage
.c_str());
197 // include users configuration if available
198 fprintf(f
, "\tinclude = %s/.smb/user.conf\n", home
.c_str());
204 // reads smb.conf so this MUST be after we create smb.conf
205 // multiple smbc_init calls are ignored by libsmbclient.
206 // note: this is important as it initializes the smb old
207 // interface compatibility. Samba 3.4.0 or higher has the new interface.
208 // note: we leak the following here once, not sure why yet.
209 // 48 bytes -> smb_xmalloc_array
210 // 32 bytes -> set_param_opt
211 // 16 bytes -> set_param_opt
212 smbc_init(xb_smbc_auth
, 0);
215 m_context
= smbc_new_context();
218 setenv("HOME", truehome
.c_str(), 1);
220 #ifdef DEPRECATED_SMBC_INTERFACE
221 smbc_setDebug(m_context
, CServiceBroker::GetLogging().CanLogComponent(LOGSAMBA
) ? 10 : 0);
222 smbc_setLogCallback(m_context
, this, xb_smbc_log
);
223 smbc_setFunctionAuthData(m_context
, xb_smbc_auth
);
224 orig_cache
= smbc_getFunctionGetCachedServer(m_context
);
225 smbc_setFunctionGetCachedServer(m_context
, xb_smbc_cache
);
226 smbc_setOptionOneSharePerServer(m_context
, false);
227 smbc_setOptionBrowseMaxLmbCount(m_context
, 0);
228 smbc_setTimeout(m_context
, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_sambaclienttimeout
* 1000);
229 // we do not need to strdup these, smbc_setXXX below will make their own copies
230 if (settings
->GetString(CSettings::SETTING_SMB_WORKGROUP
).length() > 0)
231 //! @bug libsmbclient < 4.9 isn't const correct
232 smbc_setWorkgroup(m_context
, const_cast<char*>(settings
->GetString(CSettings::SETTING_SMB_WORKGROUP
).c_str()));
233 std::string guest
= "guest";
234 //! @bug libsmbclient < 4.8 isn't const correct
235 smbc_setUser(m_context
, const_cast<char*>(guest
.c_str()));
237 m_context
->debug
= (CServiceBroker::GetLogging().CanLogComponent(LOGSAMBA
) ? 10 : 0);
238 m_context
->callbacks
.auth_fn
= xb_smbc_auth
;
239 orig_cache
= m_context
->callbacks
.get_cached_srv_fn
;
240 m_context
->callbacks
.get_cached_srv_fn
= xb_smbc_cache
;
241 m_context
->options
.one_share_per_server
= false;
242 m_context
->options
.browse_max_lmb_count
= 0;
243 m_context
->timeout
= CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_sambaclienttimeout
* 1000;
244 // we need to strdup these, they will get free'd on smbc_free_context
245 if (settings
->GetString(CSettings::SETTING_SMB_WORKGROUP
).length() > 0)
246 m_context
->workgroup
= strdup(settings
->GetString(CSettings::SETTING_SMB_WORKGROUP
).c_str());
247 m_context
->user
= strdup("guest");
250 // initialize samba and do some hacking into the settings
251 if (smbc_init_context(m_context
))
253 // setup context using the smb old interface compatibility
254 SMBCCTX
*old_context
= smbc_set_context(m_context
);
255 // free previous context or we leak it, this comes from smbc_init above.
256 // there is a bug in smbclient (old interface), if we init/set a context
257 // then set(null)/free it in DeInit above, the next smbc_set_context
258 // return the already freed previous context, free again and bang, crash.
259 // so we setup a stic bool to track the first init so we can free the
260 // context associated with the initial smbc_init.
261 if (old_context
&& IsFirstInit
)
263 smbc_free_context(old_context
, 1);
269 smbc_free_context(m_context
, 1);
276 std::string
CSMB::URLEncode(const CURL
&url
)
278 /* due to smb wanting encoded urls we have to build it manually */
280 std::string flat
= "smb://";
282 /* samba messes up of password is set but no username is set. don't know why yet */
283 /* probably the url parser that goes crazy */
284 if(url
.GetUserName().length() > 0 /* || url.GetPassWord().length() > 0 */)
286 if(!url
.GetDomain().empty())
288 flat
+= URLEncode(url
.GetDomain());
291 flat
+= URLEncode(url
.GetUserName());
292 if(url
.GetPassWord().length() > 0)
295 flat
+= URLEncode(url
.GetPassWord());
299 flat
+= URLEncode(url
.GetHostName());
303 flat
+= StringUtils::Format(":{}", url
.GetPort());
306 /* okey sadly since a slash is an invalid name we have to tokenize */
307 std::vector
<std::string
> parts
;
308 StringUtils::Tokenize(url
.GetFileName(), parts
, "/");
309 for (const std::string
& it
: parts
)
312 flat
+= URLEncode((it
));
315 /* okey options should go here, thou current samba doesn't support any */
320 std::string
CSMB::URLEncode(const std::string
&value
)
322 return CURL::Encode(value
);
325 /* This is called from CApplication::ProcessSlow() and is used to tell if smbclient have been idle for too long */
326 void CSMB::CheckIfIdle()
328 /* We check if there are open connections. This is done without a lock to not halt the mainthread. It should be thread safe as
329 worst case scenario is that m_OpenConnections could read 0 and then changed to 1 if this happens it will enter the if which will lead to another check, which is locked. */
330 if (m_OpenConnections
== 0)
331 { /* I've set the the maximum IDLE time to be 1 min and 30 sec. */
332 std::unique_lock
<CCriticalSection
> lock(*this);
333 if (m_OpenConnections
== 0 /* check again - when locked */ && m_context
!= NULL
)
335 if (m_IdleTimeout
> 0)
341 CLog::Log(LOGINFO
, "Samba is idle. Closing the remaining connections");
348 void CSMB::SetActivityTime()
350 /* Since we get called every 500ms from ProcessSlow we limit the tick count to 180 */
351 /* That means we have 2 ticks per second which equals 180/2 == 90 seconds */
355 /* The following two function is used to keep track on how many Opened files/directories there are.
356 This makes the idle timer not count if a movie is paused for example */
357 void CSMB::AddActiveConnection()
359 std::unique_lock
<CCriticalSection
> lock(*this);
362 void CSMB::AddIdleConnection()
364 std::unique_lock
<CCriticalSection
> lock(*this);
366 /* If we close a file we reset the idle timer so that we don't have any weird behaviours if a user
367 leaves the movie paused for a long while and then press stop */
371 CURL
CSMB::GetResolvedUrl(const CURL
& url
)
374 std::string resolvedHostName
;
376 if (CDNSNameCache::Lookup(tmpUrl
.GetHostName(), resolvedHostName
))
377 tmpUrl
.SetHostName(resolvedHostName
);
388 smb
.AddActiveConnection();
392 CSMBFile::~CSMBFile()
395 smb
.AddIdleConnection();
398 int64_t CSMBFile::GetPosition()
402 std::unique_lock
<CCriticalSection
> lock(smb
);
403 if (!smb
.IsSmbValid())
405 return smbc_lseek(m_fd
, 0, SEEK_CUR
);
408 int64_t CSMBFile::GetLength()
415 bool CSMBFile::Open(const CURL
& url
)
419 // we can't open files like smb://file.f or smb://server/file.f
420 // if a file matches the if below return false, it can't exist on a samba share.
421 if (!IsValidFile(url
.GetFileName()))
423 CLog::Log(LOGINFO
, "SMBFile->Open: Bad URL : '{}'", url
.GetRedacted());
428 // opening a file to another computer share will create a new session
429 // when opening smb://server xbms will try to find folder.jpg in all shares
430 // listed, which will create lot's of open sessions.
432 std::string strFileName
;
433 m_fd
= OpenFile(url
, strFileName
);
435 CLog::Log(LOGDEBUG
, "CSMBFile::Open - opened {}, fd={}", url
.GetRedacted(), m_fd
);
438 // write error to logfile
439 CLog::Log(LOGERROR
, "SMBFile->Open: Unable to open file : '{}'\nunix_err:'{:x}' error : '{}'",
440 CURL::GetRedacted(strFileName
), errno
, strerror(errno
));
444 std::unique_lock
<CCriticalSection
> lock(smb
);
445 if (!smb
.IsSmbValid())
447 struct stat tmpBuffer
;
448 if (smbc_stat(strFileName
.c_str(), &tmpBuffer
) < 0)
455 m_fileSize
= tmpBuffer
.st_size
;
457 int64_t ret
= smbc_lseek(m_fd
, 0, SEEK_SET
);
464 // We've successfully opened the file!
469 /// \brief Checks authentication against SAMBA share. Reads password cache created in CSMBDirectory::OpenDir().
470 /// \param strAuth The SMB style path
471 /// \return SMB file descriptor
473 int CSMBFile::OpenFile(std::string& strAuth)
477 std::string strPath = g_passwordManager.GetSMBAuthFilename(strAuth);
479 fd = smbc_open(strPath.c_str(), O_RDONLY, 0);
480 //! @todo Run a loop here that prompts for our username/password as appropriate?
481 //! We have the ability to run a file (eg from a button action) without browsing to
482 //! the directory first. In the case of a password protected share that we do
483 //! not have the authentication information for, the above smbc_open() will have
484 //! returned negative, and the file will not be opened. While this is not a particular
485 //! likely scenario, we might want to implement prompting for the password in this case.
486 //! The code from SMBDirectory can be used for this.
494 int CSMBFile::OpenFile(const CURL
&url
, std::string
& strAuth
)
499 strAuth
= GetAuthenticatedPath(CSMB::GetResolvedUrl(url
));
500 std::string strPath
= strAuth
;
503 std::unique_lock
<CCriticalSection
> lock(smb
);
504 if (smb
.IsSmbValid())
505 fd
= smbc_open(strPath
.c_str(), O_RDONLY
, 0);
514 bool CSMBFile::Exists(const CURL
& url
)
516 // we can't open files like smb://file.f or smb://server/file.f
517 // if a file matches the if below return false, it can't exist on a samba share.
518 if (!IsValidFile(url
.GetFileName())) return false;
521 std::string strFileName
= GetAuthenticatedPath(CSMB::GetResolvedUrl(url
));
525 std::unique_lock
<CCriticalSection
> lock(smb
);
526 if (!smb
.IsSmbValid())
528 int iResult
= smbc_stat(strFileName
.c_str(), &info
);
530 if (iResult
< 0) return false;
534 int CSMBFile::Stat(struct __stat64
* buffer
)
539 struct stat tmpBuffer
= {};
541 std::unique_lock
<CCriticalSection
> lock(smb
);
542 if (!smb
.IsSmbValid())
544 int iResult
= smbc_fstat(m_fd
, &tmpBuffer
);
545 CUtil::StatToStat64(buffer
, &tmpBuffer
);
549 int CSMBFile::Stat(const CURL
& url
, struct __stat64
* buffer
)
552 std::string strFileName
= GetAuthenticatedPath(CSMB::GetResolvedUrl(url
));
553 std::unique_lock
<CCriticalSection
> lock(smb
);
555 if (!smb
.IsSmbValid())
557 struct stat tmpBuffer
= {};
558 int iResult
= smbc_stat(strFileName
.c_str(), &tmpBuffer
);
559 CUtil::StatToStat64(buffer
, &tmpBuffer
);
563 int CSMBFile::Truncate(int64_t size
)
565 if (m_fd
== -1) return 0;
567 * This would force us to be dependant on SMBv3.2 which is GPLv3
568 * This is only used by the TagLib writers, which are not currently in use
569 * So log and warn until we implement TagLib writing & can re-implement this better.
570 std::unique_lock<CCriticalSection> lock(smb); // Init not called since it has to be "inited" by now
572 #if defined(TARGET_ANDROID)
575 int iResult = smbc_ftruncate(m_fd, size);
578 CLog::Log(LOGWARNING
, "{} - Warning(smbc_ftruncate called and not implemented)", __FUNCTION__
);
582 ssize_t
CSMBFile::Read(void *lpBuf
, size_t uiBufSize
)
584 if (uiBufSize
> SSIZE_MAX
)
585 uiBufSize
= SSIZE_MAX
;
590 // Some external libs (libass) use test read with zero size and
591 // null buffer pointer to check whether file is readable, but
592 // libsmbclient always return "-1" if called with null buffer
593 // regardless of buffer size.
594 // To overcome this, force return "0" in that case.
595 if (uiBufSize
== 0 && lpBuf
== NULL
)
598 std::unique_lock
<CCriticalSection
> lock(
599 smb
); // Init not called since it has to be "inited" by now
600 if (!smb
.IsSmbValid())
602 smb
.SetActivityTime();
604 ssize_t bytesRead
= smbc_read(m_fd
, lpBuf
, (int)uiBufSize
);
606 if (m_allowRetry
&& bytesRead
< 0 && errno
== EINVAL
)
608 CLog::Log(LOGERROR
, "{} - Error( {}, {}, {} ) - Retrying", __FUNCTION__
, bytesRead
, errno
,
610 bytesRead
= smbc_read(m_fd
, lpBuf
, (int)uiBufSize
);
614 CLog::Log(LOGERROR
, "{} - Error( {}, {}, {} )", __FUNCTION__
, bytesRead
, errno
,
620 int64_t CSMBFile::Seek(int64_t iFilePosition
, int iWhence
)
622 if (m_fd
== -1) return -1;
624 std::unique_lock
<CCriticalSection
> lock(
625 smb
); // Init not called since it has to be "inited" by now
626 if (!smb
.IsSmbValid())
628 smb
.SetActivityTime();
629 int64_t pos
= smbc_lseek(m_fd
, iFilePosition
, iWhence
);
633 CLog::Log(LOGERROR
, "{} - Error( {}, {}, {} )", __FUNCTION__
, pos
, errno
, strerror(errno
));
640 void CSMBFile::Close()
644 CLog::Log(LOGDEBUG
, "CSMBFile::Close closing fd {}", m_fd
);
645 std::unique_lock
<CCriticalSection
> lock(smb
);
646 if (!smb
.IsSmbValid())
653 ssize_t
CSMBFile::Write(const void* lpBuf
, size_t uiBufSize
)
655 if (m_fd
== -1) return -1;
657 // lpBuf can be safely casted to void* since xbmc_write will only read from it.
658 std::unique_lock
<CCriticalSection
> lock(smb
);
659 if (!smb
.IsSmbValid())
662 return smbc_write(m_fd
, lpBuf
, uiBufSize
);
665 bool CSMBFile::Delete(const CURL
& url
)
668 std::string strFile
= GetAuthenticatedPath(CSMB::GetResolvedUrl(url
));
670 std::unique_lock
<CCriticalSection
> lock(smb
);
671 if (!smb
.IsSmbValid())
674 int result
= smbc_unlink(strFile
.c_str());
677 CLog::Log(LOGERROR
, "{} - Error( {} )", __FUNCTION__
, strerror(errno
));
679 return (result
== 0);
682 bool CSMBFile::Rename(const CURL
& url
, const CURL
& urlnew
)
685 std::string strFile
= GetAuthenticatedPath(CSMB::GetResolvedUrl(url
));
686 std::string strFileNew
= GetAuthenticatedPath(CSMB::GetResolvedUrl(urlnew
));
687 std::unique_lock
<CCriticalSection
> lock(smb
);
688 if (!smb
.IsSmbValid())
691 int result
= smbc_rename(strFile
.c_str(), strFileNew
.c_str());
694 CLog::Log(LOGERROR
, "{} - Error( {} )", __FUNCTION__
, strerror(errno
));
696 return (result
== 0);
699 bool CSMBFile::OpenForWrite(const CURL
& url
, bool bOverWrite
)
705 // we can't open files like smb://file.f or smb://server/file.f
706 // if a file matches the if below return false, it can't exist on a samba share.
707 if (!IsValidFile(url
.GetFileName())) return false;
709 std::string strFileName
= GetAuthenticatedPath(CSMB::GetResolvedUrl(url
));
710 std::unique_lock
<CCriticalSection
> lock(smb
);
711 if (!smb
.IsSmbValid())
716 CLog::Log(LOGWARNING
, "SMBFile::OpenForWrite() called with overwriting enabled! - {}",
717 CURL::GetRedacted(strFileName
));
718 m_fd
= smbc_creat(strFileName
.c_str(), 0);
722 m_fd
= smbc_open(strFileName
.c_str(), O_RDWR
, 0);
727 // write error to logfile
728 CLog::Log(LOGERROR
, "SMBFile->Open: Unable to open file : '{}'\nunix_err:'{:x}' error : '{}'",
729 CURL::GetRedacted(strFileName
), errno
, strerror(errno
));
733 // We've successfully opened the file!
737 bool CSMBFile::IsValidFile(const std::string
& strFileName
)
739 if (strFileName
.find('/') == std::string::npos
|| /* doesn't have sharename */
740 StringUtils::EndsWith(strFileName
, "/.") || /* not current folder */
741 StringUtils::EndsWith(strFileName
, "/..")) /* not parent folder */
746 std::string
CSMBFile::GetAuthenticatedPath(const CURL
&url
)
748 CURL
authURL(CSMB::GetResolvedUrl(url
));
749 CPasswordManager::GetInstance().AuthenticateURL(authURL
);
750 return smb
.URLEncode(authURL
);
753 int CSMBFile::IoControl(IOControl request
, void* param
)
755 if (request
== IOControl::SEEK_POSSIBLE
)
758 if (request
== IOControl::SET_RETRY
)
760 m_allowRetry
= *(bool*) param
;
767 int CSMBFile::GetChunkSize()
769 const auto settings
= CServiceBroker::GetSettingsComponent()->GetSettings();
774 // Only SMBv2.1 and SMBv3 supports large MTU
775 if (settings
->GetInt(CSettings::SETTING_SMB_MINPROTOCOL
) > 2)
777 return (settings
->GetInt(CSettings::SETTING_SMB_CHUNKSIZE
) * 1024);