2 * Copyright (C) 2005-2013 Team XBMC
5 * This Program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2, or (at your option)
10 * This Program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with XBMC; see the file COPYING. If not, see
17 * <http://www.gnu.org/licenses/>.
21 // FileSmb.cpp: implementation of the CSmbFile class.
23 //////////////////////////////////////////////////////////////////////
27 #include "PasswordManager.h"
28 #include "SMBDirectory.h"
29 #include <libsmbclient.h>
30 #include "settings/AdvancedSettings.h"
31 #include "settings/Settings.h"
32 #include "threads/SingleLock.h"
33 #include "utils/log.h"
35 #include "utils/StringUtils.h"
36 #include "utils/TimeUtils.h"
37 #include "commons/Exception.h"
39 using namespace XFILE;
41 void xb_smbc_log(const char* msg)
43 CLog::Log(LOGINFO, "%s%s", "smb: ", msg);
46 void xb_smbc_auth(const char *srv, const char *shr, char *wg, int wglen,
47 char *un, int unlen, char *pw, int pwlen)
52 smbc_get_cached_srv_fn orig_cache;
54 SMBCSRV* xb_smbc_cache(SMBCCTX* c, const char* server, const char* share, const char* workgroup, const char* username)
56 return orig_cache(c, server, share, workgroup, username);
72 CSingleLock lock(*this);
74 /* samba goes loco if deinited while it has some files opened */
79 smbc_set_context(NULL);
80 smbc_free_context(m_context, 1);
82 XBMCCOMMONS_HANDLE_UNCHECKED
85 CLog::Log(LOGERROR,"exception on CSMB::Deinit. errno: %d", errno);
93 CSingleLock lock(*this);
96 // Create ~/.smb/smb.conf. This file is used by libsmbclient.
97 // http://us1.samba.org/samba/docs/man/manpages-3/libsmbclient.7.html
98 // http://us1.samba.org/samba/docs/man/manpages-3/smb.conf.5.html
99 char smb_conf[MAX_PATH];
100 snprintf(smb_conf, sizeof(smb_conf), "%s/.smb", getenv("HOME"));
101 if (mkdir(smb_conf, 0755) == 0)
103 snprintf(smb_conf, sizeof(smb_conf), "%s/.smb/smb.conf", getenv("HOME"));
104 FILE* f = fopen(smb_conf, "w");
107 fprintf(f, "[global]\n");
109 // make sure we're not acting like a server
110 fprintf(f, "\tpreferred master = no\n");
111 fprintf(f, "\tlocal master = no\n");
112 fprintf(f, "\tdomain master = no\n");
114 // use the weaker LANMAN password hash in order to be compatible with older servers
115 fprintf(f, "\tclient lanman auth = yes\n");
116 fprintf(f, "\tlanman auth = yes\n");
118 fprintf(f, "\tsocket options = TCP_NODELAY IPTOS_LOWDELAY SO_RCVBUF=65536 SO_SNDBUF=65536\n");
119 fprintf(f, "\tlock directory = %s/.smb/\n", getenv("HOME"));
121 // set wins server if there's one. name resolve order defaults to 'lmhosts host wins bcast'.
122 // if no WINS server has been specified the wins method will be ignored.
123 if (CSettings::Get().GetString("smb.winsserver").length() > 0 && !StringUtils::EqualsNoCase(CSettings::Get().GetString("smb.winsserver"), "0.0.0.0") )
125 fprintf(f, "\twins server = %s\n", CSettings::Get().GetString("smb.winsserver").c_str());
126 fprintf(f, "\tname resolve order = bcast wins host\n");
129 fprintf(f, "\tname resolve order = bcast host\n");
131 // use user-configured charset. if no charset is specified,
132 // samba tries to use charset 850 but falls back to ASCII in case it is not available
133 if (g_advancedSettings.m_sambadoscodepage.length() > 0)
134 fprintf(f, "\tdos charset = %s\n", g_advancedSettings.m_sambadoscodepage.c_str());
136 // if no workgroup string is specified, samba will use the default value 'WORKGROUP'
137 if ( CSettings::Get().GetString("smb.workgroup").length() > 0 )
138 fprintf(f, "\tworkgroup = %s\n", CSettings::Get().GetString("smb.workgroup").c_str());
143 // reads smb.conf so this MUST be after we create smb.conf
144 // multiple smbc_init calls are ignored by libsmbclient.
145 smbc_init(xb_smbc_auth, 0);
148 m_context = smbc_new_context();
149 #ifdef DEPRECATED_SMBC_INTERFACE
150 smbc_setDebug(m_context, (g_advancedSettings.m_extraLogLevels & LOGSAMBA)?10:0);
151 smbc_setFunctionAuthData(m_context, xb_smbc_auth);
152 orig_cache = smbc_getFunctionGetCachedServer(m_context);
153 smbc_setFunctionGetCachedServer(m_context, xb_smbc_cache);
154 smbc_setOptionOneSharePerServer(m_context, false);
155 smbc_setOptionBrowseMaxLmbCount(m_context, 0);
156 smbc_setTimeout(m_context, g_advancedSettings.m_sambaclienttimeout * 1000);
157 smbc_setUser(m_context, strdup("guest"));
159 m_context->debug = (g_advancedSettings.m_extraLogLevels & LOGSAMBA?10:0);
160 m_context->callbacks.auth_fn = xb_smbc_auth;
161 orig_cache = m_context->callbacks.get_cached_srv_fn;
162 m_context->callbacks.get_cached_srv_fn = xb_smbc_cache;
163 m_context->options.one_share_per_server = false;
164 m_context->options.browse_max_lmb_count = 0;
165 m_context->timeout = g_advancedSettings.m_sambaclienttimeout * 1000;
166 m_context->user = strdup("guest");
169 // initialize samba and do some hacking into the settings
170 if (smbc_init_context(m_context))
172 /* setup old interface to use this context */
173 smbc_set_context(m_context);
177 smbc_free_context(m_context, 1);
189 * For each new connection samba creates a new session
190 * But this is not what we want, we just want to have one session at the time
191 * This means that we have to call smbc_purge() if samba created a new session
192 * Samba will create a new session when:
193 * - connecting to another server
194 * - connecting to another share on the same server (share, not a different folder!)
196 * We try to avoid lot's of purge commands because it slow samba down.
198 void CSMB::PurgeEx(const CURL& url)
200 CSingleLock lock(*this);
201 CStdString strShare = url.GetFileName().substr(0, url.GetFileName().find('/'));
203 m_strLastShare = strShare;
204 m_strLastHost = url.GetHostName();
207 CStdString CSMB::URLEncode(const CURL &url)
209 /* due to smb wanting encoded urls we have to build it manually */
211 CStdString flat = "smb://";
213 if(url.GetDomain().length() > 0)
215 flat += URLEncode(url.GetDomain());
219 /* samba messes up of password is set but no username is set. don't know why yet */
220 /* probably the url parser that goes crazy */
221 if(url.GetUserName().length() > 0 /* || url.GetPassWord().length() > 0 */)
223 flat += URLEncode(url.GetUserName());
225 flat += URLEncode(url.GetPassWord());
228 flat += URLEncode(url.GetHostName());
230 /* okey sadly since a slash is an invalid name we have to tokenize */
231 std::vector<std::string> parts;
232 std::vector<std::string>::iterator it;
233 StringUtils::Tokenize(url.GetFileName(), parts, "/");
234 for( it = parts.begin(); it != parts.end(); it++ )
237 flat += URLEncode((*it));
240 /* okey options should go here, thou current samba doesn't support any */
245 CStdString CSMB::URLEncode(const CStdString &value)
247 CStdString encoded(value);
248 CURL::Encode(encoded);
252 /* This is called from CApplication::ProcessSlow() and is used to tell if smbclient have been idle for too long */
253 void CSMB::CheckIfIdle()
255 /* We check if there are open connections. This is done without a lock to not halt the mainthread. It should be thread safe as
256 worst case scenario is that m_OpenConnections could read 0 and then changed to 1 if this happens it will enter the if wich will lead to another check, wich is locked. */
257 if (m_OpenConnections == 0)
258 { /* I've set the the maxiumum IDLE time to be 1 min and 30 sec. */
259 CSingleLock lock(*this);
260 if (m_OpenConnections == 0 /* check again - when locked */ && m_context != NULL)
262 if (m_IdleTimeout > 0)
268 CLog::Log(LOGNOTICE, "Samba is idle. Closing the remaining connections");
275 void CSMB::SetActivityTime()
277 /* Since we get called every 500ms from ProcessSlow we limit the tick count to 180 */
278 /* That means we have 2 ticks per second which equals 180/2 == 90 seconds */
282 /* The following two function is used to keep track on how many Opened files/directories there are.
283 This makes the idle timer not count if a movie is paused for example */
284 void CSMB::AddActiveConnection()
286 CSingleLock lock(*this);
289 void CSMB::AddIdleConnection()
291 CSingleLock lock(*this);
293 /* If we close a file we reset the idle timer so that we don't have any wierd behaviours if a user
294 leaves the movie paused for a long while and then press stop */
304 smb.AddActiveConnection();
307 CSmbFile::~CSmbFile()
310 smb.AddIdleConnection();
313 int64_t CSmbFile::GetPosition()
315 if (m_fd == -1) return 0;
317 CSingleLock lock(smb);
318 int64_t pos = smbc_lseek(m_fd, 0, SEEK_CUR);
324 int64_t CSmbFile::GetLength()
326 if (m_fd == -1) return 0;
330 bool CSmbFile::Open(const CURL& url)
334 // we can't open files like smb://file.f or smb://server/file.f
335 // if a file matches the if below return false, it can't exist on a samba share.
336 if (!IsValidFile(url.GetFileName()))
338 CLog::Log(LOGNOTICE,"FileSmb->Open: Bad URL : '%s'",url.GetFileName().c_str());
343 // opening a file to another computer share will create a new session
344 // when opening smb://server xbms will try to find folder.jpg in all shares
345 // listed, which will create lot's of open sessions.
347 CStdString strFileName;
348 m_fd = OpenFile(url, strFileName);
350 CLog::Log(LOGDEBUG,"CSmbFile::Open - opened %s, fd=%d",url.GetFileName().c_str(), m_fd);
353 // write error to logfile
354 CLog::Log(LOGINFO, "FileSmb->Open: Unable to open file : '%s'\nunix_err:'%x' error : '%s'", CURL::GetRedacted(strFileName).c_str(), errno, strerror(errno));
358 CSingleLock lock(smb);
359 struct stat tmpBuffer;
360 if (smbc_stat(strFileName, &tmpBuffer) < 0)
367 m_fileSize = tmpBuffer.st_size;
369 int64_t ret = smbc_lseek(m_fd, 0, SEEK_SET);
376 // We've successfully opened the file!
381 /// \brief Checks authentication against SAMBA share. Reads password cache created in CSMBDirectory::OpenDir().
382 /// \param strAuth The SMB style path
383 /// \return SMB file descriptor
385 int CSmbFile::OpenFile(CStdString& strAuth)
389 CStdString strPath = g_passwordManager.GetSMBAuthFilename(strAuth);
391 fd = smbc_open(strPath.c_str(), O_RDONLY, 0);
392 // TODO: Run a loop here that prompts for our username/password as appropriate?
393 // We have the ability to run a file (eg from a button action) without browsing to
394 // the directory first. In the case of a password protected share that we do
395 // not have the authentication information for, the above smbc_open() will have
396 // returned negative, and the file will not be opened. While this is not a particular
397 // likely scenario, we might want to implement prompting for the password in this case.
398 // The code from SMBDirectory can be used for this.
406 int CSmbFile::OpenFile(const CURL &url, CStdString& strAuth)
411 strAuth = GetAuthenticatedPath(url);
412 CStdString strPath = strAuth;
415 CSingleLock lock(smb);
416 fd = smbc_open(strPath.c_str(), O_RDONLY, 0);
425 bool CSmbFile::Exists(const CURL& url)
427 // we can't open files like smb://file.f or smb://server/file.f
428 // if a file matches the if below return false, it can't exist on a samba share.
429 if (!IsValidFile(url.GetFileName())) return false;
432 CStdString strFileName = GetAuthenticatedPath(url);
436 CSingleLock lock(smb);
437 int iResult = smbc_stat(strFileName, &info);
439 if (iResult < 0) return false;
443 int CSmbFile::Stat(struct __stat64* buffer)
448 struct stat tmpBuffer = {0};
450 CSingleLock lock(smb);
451 int iResult = smbc_fstat(m_fd, &tmpBuffer);
453 memset(buffer, 0, sizeof(struct __stat64));
454 buffer->st_dev = tmpBuffer.st_dev;
455 buffer->st_ino = tmpBuffer.st_ino;
456 buffer->st_mode = tmpBuffer.st_mode;
457 buffer->st_nlink = tmpBuffer.st_nlink;
458 buffer->st_uid = tmpBuffer.st_uid;
459 buffer->st_gid = tmpBuffer.st_gid;
460 buffer->st_rdev = tmpBuffer.st_rdev;
461 buffer->st_size = tmpBuffer.st_size;
462 buffer->st_atime = tmpBuffer.st_atime;
463 buffer->st_mtime = tmpBuffer.st_mtime;
464 buffer->st_ctime = tmpBuffer.st_ctime;
469 int CSmbFile::Stat(const CURL& url, struct __stat64* buffer)
472 CStdString strFileName = GetAuthenticatedPath(url);
473 CSingleLock lock(smb);
475 struct stat tmpBuffer = {0};
476 int iResult = smbc_stat(strFileName, &tmpBuffer);
478 memset(buffer, 0, sizeof(struct __stat64));
479 buffer->st_dev = tmpBuffer.st_dev;
480 buffer->st_ino = tmpBuffer.st_ino;
481 buffer->st_mode = tmpBuffer.st_mode;
482 buffer->st_nlink = tmpBuffer.st_nlink;
483 buffer->st_uid = tmpBuffer.st_uid;
484 buffer->st_gid = tmpBuffer.st_gid;
485 buffer->st_rdev = tmpBuffer.st_rdev;
486 buffer->st_size = tmpBuffer.st_size;
487 buffer->st_atime = tmpBuffer.st_atime;
488 buffer->st_mtime = tmpBuffer.st_mtime;
489 buffer->st_ctime = tmpBuffer.st_ctime;
494 int CSmbFile::Truncate(int64_t size)
496 if (m_fd == -1) return 0;
498 * This would force us to be dependant on SMBv3.2 which is GPLv3
499 * This is only used by the TagLib writers, which are not currently in use
500 * So log and warn until we implement TagLib writing & can re-implement this better.
501 CSingleLock lock(smb); // Init not called since it has to be "inited" by now
503 #if defined(TARGET_ANDROID)
506 int iResult = smbc_ftruncate(m_fd, size);
509 CLog::Log(LOGWARNING, "%s - Warning(smbc_ftruncate called and not implemented)", __FUNCTION__);
513 unsigned int CSmbFile::Read(void *lpBuf, int64_t uiBufSize)
515 if (m_fd == -1) return 0;
516 CSingleLock lock(smb); // Init not called since it has to be "inited" by now
517 smb.SetActivityTime();
518 /* work around stupid bug in samba */
519 /* some samba servers has a bug in it where the */
520 /* 17th bit will be ignored in a request of data */
521 /* this can lead to a very small return of data */
522 /* also worse, a request of exactly 64k will return */
523 /* as if eof, client has a workaround for windows */
524 /* thou it seems other servers are affected too */
525 if( uiBufSize >= 64*1024-2 )
526 uiBufSize = 64*1024-2;
528 int bytesRead = smbc_read(m_fd, lpBuf, (int)uiBufSize);
530 if ( bytesRead < 0 && errno == EINVAL )
532 CLog::Log(LOGERROR, "%s - Error( %d, %d, %s ) - Retrying", __FUNCTION__, bytesRead, errno, strerror(errno));
533 bytesRead = smbc_read(m_fd, lpBuf, (int)uiBufSize);
538 CLog::Log(LOGERROR, "%s - Error( %d, %d, %s )", __FUNCTION__, bytesRead, errno, strerror(errno));
542 return (unsigned int)bytesRead;
545 int64_t CSmbFile::Seek(int64_t iFilePosition, int iWhence)
547 if (m_fd == -1) return -1;
549 CSingleLock lock(smb); // Init not called since it has to be "inited" by now
550 smb.SetActivityTime();
551 int64_t pos = smbc_lseek(m_fd, iFilePosition, iWhence);
555 CLog::Log(LOGERROR, "%s - Error( %"PRId64", %d, %s )", __FUNCTION__, pos, errno, strerror(errno));
562 void CSmbFile::Close()
566 CLog::Log(LOGDEBUG,"CSmbFile::Close closing fd %d", m_fd);
567 CSingleLock lock(smb);
573 int CSmbFile::Write(const void* lpBuf, int64_t uiBufSize)
575 if (m_fd == -1) return -1;
576 DWORD dwNumberOfBytesWritten = 0;
578 // lpBuf can be safely casted to void* since xmbc_write will only read from it.
580 CSingleLock lock(smb);
581 dwNumberOfBytesWritten = smbc_write(m_fd, (void*)lpBuf, (DWORD)uiBufSize);
583 return (int)dwNumberOfBytesWritten;
586 bool CSmbFile::Delete(const CURL& url)
589 CStdString strFile = GetAuthenticatedPath(url);
591 CSingleLock lock(smb);
593 int result = smbc_unlink(strFile.c_str());
596 CLog::Log(LOGERROR, "%s - Error( %s )", __FUNCTION__, strerror(errno));
598 return (result == 0);
601 bool CSmbFile::Rename(const CURL& url, const CURL& urlnew)
604 CStdString strFile = GetAuthenticatedPath(url);
605 CStdString strFileNew = GetAuthenticatedPath(urlnew);
606 CSingleLock lock(smb);
608 int result = smbc_rename(strFile.c_str(), strFileNew.c_str());
611 CLog::Log(LOGERROR, "%s - Error( %s )", __FUNCTION__, strerror(errno));
613 return (result == 0);
616 bool CSmbFile::OpenForWrite(const CURL& url, bool bOverWrite)
622 // we can't open files like smb://file.f or smb://server/file.f
623 // if a file matches the if below return false, it can't exist on a samba share.
624 if (!IsValidFile(url.GetFileName())) return false;
626 CStdString strFileName = GetAuthenticatedPath(url);
627 CSingleLock lock(smb);
631 CLog::Log(LOGWARNING, "FileSmb::OpenForWrite() called with overwriting enabled! - %s", strFileName.c_str());
632 m_fd = smbc_creat(strFileName.c_str(), 0);
636 m_fd = smbc_open(strFileName.c_str(), O_RDWR, 0);
641 // write error to logfile
642 CLog::Log(LOGERROR, "FileSmb->Open: Unable to open file : '%s'\nunix_err:'%x' error : '%s'", strFileName.c_str(), errno, strerror(errno));
646 // We've successfully opened the file!
650 bool CSmbFile::IsValidFile(const CStdString& strFileName)
652 if (strFileName.find('/') == std::string::npos || /* doesn't have sharename */
653 StringUtils::EndsWith(strFileName, "/.") || /* not current folder */
654 StringUtils::EndsWith(strFileName, "/..")) /* not parent folder */
659 CStdString CSmbFile::GetAuthenticatedPath(const CURL &url)
662 CPasswordManager::GetInstance().AuthenticateURL(authURL);
663 return smb.URLEncode(authURL);