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 if (settings
->GetInt(CSettings::SETTING_SMB_MINPROTOCOL
) > 0)
138 if (settings
->GetInt(CSettings::SETTING_SMB_MINPROTOCOL
) == 1)
139 fprintf(f
, "\tclient min protocol = NT1\n");
141 fprintf(f
, "\tclient min protocol = SMB%d\n", settings
->GetInt(CSettings::SETTING_SMB_MINPROTOCOL
));
144 // set maximum smbclient protocol version
145 if (settings
->GetInt(CSettings::SETTING_SMB_MAXPROTOCOL
) > 0)
147 if (settings
->GetInt(CSettings::SETTING_SMB_MAXPROTOCOL
) == 1)
148 fprintf(f
, "\tclient max protocol = NT1\n");
150 fprintf(f
, "\tclient max protocol = SMB%d\n", settings
->GetInt(CSettings::SETTING_SMB_MAXPROTOCOL
));
153 // set legacy security options
154 if (settings
->GetBool(CSettings::SETTING_SMB_LEGACYSECURITY
) && (settings
->GetInt(CSettings::SETTING_SMB_MAXPROTOCOL
) == 1))
156 fprintf(f
, "\tclient NTLMv2 auth = no\n");
157 fprintf(f
, "\tclient use spnego = no\n");
160 // set wins server if there's one. name resolve order defaults to 'lmhosts host wins bcast'.
161 // if no WINS server has been specified the wins method will be ignored.
162 if (settings
->GetString(CSettings::SETTING_SMB_WINSSERVER
).length() > 0 && !StringUtils::EqualsNoCase(settings
->GetString(CSettings::SETTING_SMB_WINSSERVER
), "0.0.0.0") )
164 fprintf(f
, "\twins server = %s\n", settings
->GetString(CSettings::SETTING_SMB_WINSSERVER
).c_str());
165 fprintf(f
, "\tname resolve order = bcast wins host\n");
168 fprintf(f
, "\tname resolve order = bcast host\n");
170 // use user-configured charset. if no charset is specified,
171 // samba tries to use charset 850 but falls back to ASCII in case it is not available
172 if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_sambadoscodepage
.length() > 0)
173 fprintf(f
, "\tdos charset = %s\n", CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_sambadoscodepage
.c_str());
175 // include users configuration if available
176 fprintf(f
, "\tinclude = %s/.smb/user.conf\n", home
.c_str());
182 // reads smb.conf so this MUST be after we create smb.conf
183 // multiple smbc_init calls are ignored by libsmbclient.
184 // note: this is important as it initializes the smb old
185 // interface compatibility. Samba 3.4.0 or higher has the new interface.
186 // note: we leak the following here once, not sure why yet.
187 // 48 bytes -> smb_xmalloc_array
188 // 32 bytes -> set_param_opt
189 // 16 bytes -> set_param_opt
190 smbc_init(xb_smbc_auth
, 0);
193 m_context
= smbc_new_context();
196 setenv("HOME", truehome
.c_str(), 1);
198 #ifdef DEPRECATED_SMBC_INTERFACE
199 smbc_setDebug(m_context
, CServiceBroker::GetLogging().CanLogComponent(LOGSAMBA
) ? 10 : 0);
200 smbc_setLogCallback(m_context
, this, xb_smbc_log
);
201 smbc_setFunctionAuthData(m_context
, xb_smbc_auth
);
202 orig_cache
= smbc_getFunctionGetCachedServer(m_context
);
203 smbc_setFunctionGetCachedServer(m_context
, xb_smbc_cache
);
204 smbc_setOptionOneSharePerServer(m_context
, false);
205 smbc_setOptionBrowseMaxLmbCount(m_context
, 0);
206 smbc_setTimeout(m_context
, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_sambaclienttimeout
* 1000);
207 // we do not need to strdup these, smbc_setXXX below will make their own copies
208 if (settings
->GetString(CSettings::SETTING_SMB_WORKGROUP
).length() > 0)
209 //! @bug libsmbclient < 4.9 isn't const correct
210 smbc_setWorkgroup(m_context
, const_cast<char*>(settings
->GetString(CSettings::SETTING_SMB_WORKGROUP
).c_str()));
211 std::string guest
= "guest";
212 //! @bug libsmbclient < 4.8 isn't const correct
213 smbc_setUser(m_context
, const_cast<char*>(guest
.c_str()));
215 m_context
->debug
= (CServiceBroker::GetLogging().CanLogComponent(LOGSAMBA
) ? 10 : 0);
216 m_context
->callbacks
.auth_fn
= xb_smbc_auth
;
217 orig_cache
= m_context
->callbacks
.get_cached_srv_fn
;
218 m_context
->callbacks
.get_cached_srv_fn
= xb_smbc_cache
;
219 m_context
->options
.one_share_per_server
= false;
220 m_context
->options
.browse_max_lmb_count
= 0;
221 m_context
->timeout
= CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_sambaclienttimeout
* 1000;
222 // we need to strdup these, they will get free'd on smbc_free_context
223 if (settings
->GetString(CSettings::SETTING_SMB_WORKGROUP
).length() > 0)
224 m_context
->workgroup
= strdup(settings
->GetString(CSettings::SETTING_SMB_WORKGROUP
).c_str());
225 m_context
->user
= strdup("guest");
228 // initialize samba and do some hacking into the settings
229 if (smbc_init_context(m_context
))
231 // setup context using the smb old interface compatibility
232 SMBCCTX
*old_context
= smbc_set_context(m_context
);
233 // free previous context or we leak it, this comes from smbc_init above.
234 // there is a bug in smbclient (old interface), if we init/set a context
235 // then set(null)/free it in DeInit above, the next smbc_set_context
236 // return the already freed previous context, free again and bang, crash.
237 // so we setup a stic bool to track the first init so we can free the
238 // context associated with the initial smbc_init.
239 if (old_context
&& IsFirstInit
)
241 smbc_free_context(old_context
, 1);
247 smbc_free_context(m_context
, 1);
254 std::string
CSMB::URLEncode(const CURL
&url
)
256 /* due to smb wanting encoded urls we have to build it manually */
258 std::string flat
= "smb://";
260 /* samba messes up of password is set but no username is set. don't know why yet */
261 /* probably the url parser that goes crazy */
262 if(url
.GetUserName().length() > 0 /* || url.GetPassWord().length() > 0 */)
264 if(!url
.GetDomain().empty())
266 flat
+= URLEncode(url
.GetDomain());
269 flat
+= URLEncode(url
.GetUserName());
270 if(url
.GetPassWord().length() > 0)
273 flat
+= URLEncode(url
.GetPassWord());
277 flat
+= URLEncode(url
.GetHostName());
281 flat
+= StringUtils::Format(":{}", url
.GetPort());
284 /* okey sadly since a slash is an invalid name we have to tokenize */
285 std::vector
<std::string
> parts
;
286 StringUtils::Tokenize(url
.GetFileName(), parts
, "/");
287 for (const std::string
& it
: parts
)
290 flat
+= URLEncode((it
));
293 /* okey options should go here, thou current samba doesn't support any */
298 std::string
CSMB::URLEncode(const std::string
&value
)
300 return CURL::Encode(value
);
303 /* This is called from CApplication::ProcessSlow() and is used to tell if smbclient have been idle for too long */
304 void CSMB::CheckIfIdle()
306 /* We check if there are open connections. This is done without a lock to not halt the mainthread. It should be thread safe as
307 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. */
308 if (m_OpenConnections
== 0)
309 { /* I've set the the maximum IDLE time to be 1 min and 30 sec. */
310 std::unique_lock
<CCriticalSection
> lock(*this);
311 if (m_OpenConnections
== 0 /* check again - when locked */ && m_context
!= NULL
)
313 if (m_IdleTimeout
> 0)
319 CLog::Log(LOGINFO
, "Samba is idle. Closing the remaining connections");
326 void CSMB::SetActivityTime()
328 /* Since we get called every 500ms from ProcessSlow we limit the tick count to 180 */
329 /* That means we have 2 ticks per second which equals 180/2 == 90 seconds */
333 /* The following two function is used to keep track on how many Opened files/directories there are.
334 This makes the idle timer not count if a movie is paused for example */
335 void CSMB::AddActiveConnection()
337 std::unique_lock
<CCriticalSection
> lock(*this);
340 void CSMB::AddIdleConnection()
342 std::unique_lock
<CCriticalSection
> lock(*this);
344 /* If we close a file we reset the idle timer so that we don't have any weird behaviours if a user
345 leaves the movie paused for a long while and then press stop */
349 CURL
CSMB::GetResolvedUrl(const CURL
& url
)
352 std::string resolvedHostName
;
354 if (CDNSNameCache::Lookup(tmpUrl
.GetHostName(), resolvedHostName
))
355 tmpUrl
.SetHostName(resolvedHostName
);
366 smb
.AddActiveConnection();
370 CSMBFile::~CSMBFile()
373 smb
.AddIdleConnection();
376 int64_t CSMBFile::GetPosition()
380 std::unique_lock
<CCriticalSection
> lock(smb
);
381 if (!smb
.IsSmbValid())
383 return smbc_lseek(m_fd
, 0, SEEK_CUR
);
386 int64_t CSMBFile::GetLength()
393 bool CSMBFile::Open(const CURL
& url
)
397 // we can't open files like smb://file.f or smb://server/file.f
398 // if a file matches the if below return false, it can't exist on a samba share.
399 if (!IsValidFile(url
.GetFileName()))
401 CLog::Log(LOGINFO
, "SMBFile->Open: Bad URL : '{}'", url
.GetRedacted());
406 // opening a file to another computer share will create a new session
407 // when opening smb://server xbms will try to find folder.jpg in all shares
408 // listed, which will create lot's of open sessions.
410 std::string strFileName
;
411 m_fd
= OpenFile(url
, strFileName
);
413 CLog::Log(LOGDEBUG
, "CSMBFile::Open - opened {}, fd={}", url
.GetRedacted(), m_fd
);
416 // write error to logfile
417 CLog::Log(LOGERROR
, "SMBFile->Open: Unable to open file : '{}'\nunix_err:'{:x}' error : '{}'",
418 CURL::GetRedacted(strFileName
), errno
, strerror(errno
));
422 std::unique_lock
<CCriticalSection
> lock(smb
);
423 if (!smb
.IsSmbValid())
425 struct stat tmpBuffer
;
426 if (smbc_stat(strFileName
.c_str(), &tmpBuffer
) < 0)
433 m_fileSize
= tmpBuffer
.st_size
;
435 int64_t ret
= smbc_lseek(m_fd
, 0, SEEK_SET
);
442 // We've successfully opened the file!
447 /// \brief Checks authentication against SAMBA share. Reads password cache created in CSMBDirectory::OpenDir().
448 /// \param strAuth The SMB style path
449 /// \return SMB file descriptor
451 int CSMBFile::OpenFile(std::string& strAuth)
455 std::string strPath = g_passwordManager.GetSMBAuthFilename(strAuth);
457 fd = smbc_open(strPath.c_str(), O_RDONLY, 0);
458 //! @todo Run a loop here that prompts for our username/password as appropriate?
459 //! We have the ability to run a file (eg from a button action) without browsing to
460 //! the directory first. In the case of a password protected share that we do
461 //! not have the authentication information for, the above smbc_open() will have
462 //! returned negative, and the file will not be opened. While this is not a particular
463 //! likely scenario, we might want to implement prompting for the password in this case.
464 //! The code from SMBDirectory can be used for this.
472 int CSMBFile::OpenFile(const CURL
&url
, std::string
& strAuth
)
477 strAuth
= GetAuthenticatedPath(CSMB::GetResolvedUrl(url
));
478 std::string strPath
= strAuth
;
481 std::unique_lock
<CCriticalSection
> lock(smb
);
482 if (smb
.IsSmbValid())
483 fd
= smbc_open(strPath
.c_str(), O_RDONLY
, 0);
492 bool CSMBFile::Exists(const CURL
& url
)
494 // we can't open files like smb://file.f or smb://server/file.f
495 // if a file matches the if below return false, it can't exist on a samba share.
496 if (!IsValidFile(url
.GetFileName())) return false;
499 std::string strFileName
= GetAuthenticatedPath(CSMB::GetResolvedUrl(url
));
503 std::unique_lock
<CCriticalSection
> lock(smb
);
504 if (!smb
.IsSmbValid())
506 int iResult
= smbc_stat(strFileName
.c_str(), &info
);
508 if (iResult
< 0) return false;
512 int CSMBFile::Stat(struct __stat64
* buffer
)
517 struct stat tmpBuffer
= {};
519 std::unique_lock
<CCriticalSection
> lock(smb
);
520 if (!smb
.IsSmbValid())
522 int iResult
= smbc_fstat(m_fd
, &tmpBuffer
);
523 CUtil::StatToStat64(buffer
, &tmpBuffer
);
527 int CSMBFile::Stat(const CURL
& url
, struct __stat64
* buffer
)
530 std::string strFileName
= GetAuthenticatedPath(CSMB::GetResolvedUrl(url
));
531 std::unique_lock
<CCriticalSection
> lock(smb
);
533 if (!smb
.IsSmbValid())
535 struct stat tmpBuffer
= {};
536 int iResult
= smbc_stat(strFileName
.c_str(), &tmpBuffer
);
537 CUtil::StatToStat64(buffer
, &tmpBuffer
);
541 int CSMBFile::Truncate(int64_t size
)
543 if (m_fd
== -1) return 0;
545 * This would force us to be dependant on SMBv3.2 which is GPLv3
546 * This is only used by the TagLib writers, which are not currently in use
547 * So log and warn until we implement TagLib writing & can re-implement this better.
548 std::unique_lock<CCriticalSection> lock(smb); // Init not called since it has to be "inited" by now
550 #if defined(TARGET_ANDROID)
553 int iResult = smbc_ftruncate(m_fd, size);
556 CLog::Log(LOGWARNING
, "{} - Warning(smbc_ftruncate called and not implemented)", __FUNCTION__
);
560 ssize_t
CSMBFile::Read(void *lpBuf
, size_t uiBufSize
)
562 if (uiBufSize
> SSIZE_MAX
)
563 uiBufSize
= SSIZE_MAX
;
568 // Some external libs (libass) use test read with zero size and
569 // null buffer pointer to check whether file is readable, but
570 // libsmbclient always return "-1" if called with null buffer
571 // regardless of buffer size.
572 // To overcome this, force return "0" in that case.
573 if (uiBufSize
== 0 && lpBuf
== NULL
)
576 std::unique_lock
<CCriticalSection
> lock(
577 smb
); // Init not called since it has to be "inited" by now
578 if (!smb
.IsSmbValid())
580 smb
.SetActivityTime();
582 ssize_t bytesRead
= smbc_read(m_fd
, lpBuf
, (int)uiBufSize
);
584 if (m_allowRetry
&& bytesRead
< 0 && errno
== EINVAL
)
586 CLog::Log(LOGERROR
, "{} - Error( {}, {}, {} ) - Retrying", __FUNCTION__
, bytesRead
, errno
,
588 bytesRead
= smbc_read(m_fd
, lpBuf
, (int)uiBufSize
);
592 CLog::Log(LOGERROR
, "{} - Error( {}, {}, {} )", __FUNCTION__
, bytesRead
, errno
,
598 int64_t CSMBFile::Seek(int64_t iFilePosition
, int iWhence
)
600 if (m_fd
== -1) return -1;
602 std::unique_lock
<CCriticalSection
> lock(
603 smb
); // Init not called since it has to be "inited" by now
604 if (!smb
.IsSmbValid())
606 smb
.SetActivityTime();
607 int64_t pos
= smbc_lseek(m_fd
, iFilePosition
, iWhence
);
611 CLog::Log(LOGERROR
, "{} - Error( {}, {}, {} )", __FUNCTION__
, pos
, errno
, strerror(errno
));
618 void CSMBFile::Close()
622 CLog::Log(LOGDEBUG
, "CSMBFile::Close closing fd {}", m_fd
);
623 std::unique_lock
<CCriticalSection
> lock(smb
);
624 if (!smb
.IsSmbValid())
631 ssize_t
CSMBFile::Write(const void* lpBuf
, size_t uiBufSize
)
633 if (m_fd
== -1) return -1;
635 // lpBuf can be safely casted to void* since xbmc_write will only read from it.
636 std::unique_lock
<CCriticalSection
> lock(smb
);
637 if (!smb
.IsSmbValid())
640 return smbc_write(m_fd
, lpBuf
, uiBufSize
);
643 bool CSMBFile::Delete(const CURL
& url
)
646 std::string strFile
= GetAuthenticatedPath(CSMB::GetResolvedUrl(url
));
648 std::unique_lock
<CCriticalSection
> lock(smb
);
649 if (!smb
.IsSmbValid())
652 int result
= smbc_unlink(strFile
.c_str());
655 CLog::Log(LOGERROR
, "{} - Error( {} )", __FUNCTION__
, strerror(errno
));
657 return (result
== 0);
660 bool CSMBFile::Rename(const CURL
& url
, const CURL
& urlnew
)
663 std::string strFile
= GetAuthenticatedPath(CSMB::GetResolvedUrl(url
));
664 std::string strFileNew
= GetAuthenticatedPath(CSMB::GetResolvedUrl(urlnew
));
665 std::unique_lock
<CCriticalSection
> lock(smb
);
666 if (!smb
.IsSmbValid())
669 int result
= smbc_rename(strFile
.c_str(), strFileNew
.c_str());
672 CLog::Log(LOGERROR
, "{} - Error( {} )", __FUNCTION__
, strerror(errno
));
674 return (result
== 0);
677 bool CSMBFile::OpenForWrite(const CURL
& url
, bool bOverWrite
)
683 // we can't open files like smb://file.f or smb://server/file.f
684 // if a file matches the if below return false, it can't exist on a samba share.
685 if (!IsValidFile(url
.GetFileName())) return false;
687 std::string strFileName
= GetAuthenticatedPath(CSMB::GetResolvedUrl(url
));
688 std::unique_lock
<CCriticalSection
> lock(smb
);
689 if (!smb
.IsSmbValid())
694 CLog::Log(LOGWARNING
, "SMBFile::OpenForWrite() called with overwriting enabled! - {}",
695 CURL::GetRedacted(strFileName
));
696 m_fd
= smbc_creat(strFileName
.c_str(), 0);
700 m_fd
= smbc_open(strFileName
.c_str(), O_RDWR
, 0);
705 // write error to logfile
706 CLog::Log(LOGERROR
, "SMBFile->Open: Unable to open file : '{}'\nunix_err:'{:x}' error : '{}'",
707 CURL::GetRedacted(strFileName
), errno
, strerror(errno
));
711 // We've successfully opened the file!
715 bool CSMBFile::IsValidFile(const std::string
& strFileName
)
717 if (strFileName
.find('/') == std::string::npos
|| /* doesn't have sharename */
718 StringUtils::EndsWith(strFileName
, "/.") || /* not current folder */
719 StringUtils::EndsWith(strFileName
, "/..")) /* not parent folder */
724 std::string
CSMBFile::GetAuthenticatedPath(const CURL
&url
)
726 CURL
authURL(CSMB::GetResolvedUrl(url
));
727 CPasswordManager::GetInstance().AuthenticateURL(authURL
);
728 return smb
.URLEncode(authURL
);
731 int CSMBFile::IoControl(EIoControl request
, void* param
)
733 if (request
== IOCTRL_SEEK_POSSIBLE
)
736 if (request
== IOCTRL_SET_RETRY
)
738 m_allowRetry
= *(bool*) param
;
745 int CSMBFile::GetChunkSize()
747 const auto settings
= CServiceBroker::GetSettingsComponent()->GetSettings();
752 int chunkSize
= settings
->GetInt(CSettings::SETTING_SMB_CHUNKSIZE
) * 1024;
754 if (settings
->GetInt(CSettings::SETTING_SMB_MINPROTOCOL
) == 1 &&
755 settings
->GetInt(CSettings::SETTING_SMB_MAXPROTOCOL
) == 1)
757 if (chunkSize
> 64 * 1024)
758 chunkSize
= 64 * 1024;