[release] version bump to 13.0 beta1
[vuplus_xbmc] / xbmc / filesystem / SmbFile.cpp
1 /*
2  *      Copyright (C) 2005-2013 Team XBMC
3  *      http://xbmc.org
4  *
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)
8  *  any later version.
9  *
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.
14  *
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/>.
18  *
19  */
20
21 // FileSmb.cpp: implementation of the CSmbFile class.
22 //
23 //////////////////////////////////////////////////////////////////////
24
25 #include "system.h"
26 #include "SmbFile.h"
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"
34 #include "Util.h"
35 #include "utils/StringUtils.h"
36 #include "utils/TimeUtils.h"
37 #include "commons/Exception.h"
38
39 using namespace XFILE;
40
41 void xb_smbc_log(const char* msg)
42 {
43   CLog::Log(LOGINFO, "%s%s", "smb: ", msg);
44 }
45
46 void xb_smbc_auth(const char *srv, const char *shr, char *wg, int wglen,
47                   char *un, int unlen, char *pw, int pwlen)
48 {
49   return ;
50 }
51
52 smbc_get_cached_srv_fn orig_cache;
53
54 SMBCSRV* xb_smbc_cache(SMBCCTX* c, const char* server, const char* share, const char* workgroup, const char* username)
55 {
56   return orig_cache(c, server, share, workgroup, username);
57 }
58
59 CSMB::CSMB()
60 {
61   m_IdleTimeout = 0;
62   m_context = NULL;
63 }
64
65 CSMB::~CSMB()
66 {
67   Deinit();
68 }
69
70 void CSMB::Deinit()
71 {
72   CSingleLock lock(*this);
73
74   /* samba goes loco if deinited while it has some files opened */
75   if (m_context)
76   {
77     try
78     {
79       smbc_set_context(NULL);
80       smbc_free_context(m_context, 1);
81     }
82     XBMCCOMMONS_HANDLE_UNCHECKED
83     catch(...)
84     {
85       CLog::Log(LOGERROR,"exception on CSMB::Deinit. errno: %d", errno);
86     }
87     m_context = NULL;
88   }
89 }
90
91 void CSMB::Init()
92 {
93   CSingleLock lock(*this);
94   if (!m_context)
95   {
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)
102     {
103       snprintf(smb_conf, sizeof(smb_conf), "%s/.smb/smb.conf", getenv("HOME"));
104       FILE* f = fopen(smb_conf, "w");
105       if (f != NULL)
106       {
107         fprintf(f, "[global]\n");
108
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");
113
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");
117
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"));
120
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") )
124         {
125           fprintf(f, "\twins server = %s\n", CSettings::Get().GetString("smb.winsserver").c_str());
126           fprintf(f, "\tname resolve order = bcast wins host\n");
127         }
128         else
129           fprintf(f, "\tname resolve order = bcast host\n");
130
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());
135
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());
139         fclose(f);
140       }
141     }
142
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);
146
147     // setup our context
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"));
158 #else
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");
167 #endif
168
169     // initialize samba and do some hacking into the settings
170     if (smbc_init_context(m_context))
171     {
172       /* setup old interface to use this context */
173       smbc_set_context(m_context);
174     }
175     else
176     {
177       smbc_free_context(m_context, 1);
178       m_context = NULL;
179     }
180   }
181   m_IdleTimeout = 180;
182 }
183
184 void CSMB::Purge()
185 {
186 }
187
188 /*
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!)
195  *
196  * We try to avoid lot's of purge commands because it slow samba down.
197  */
198 void CSMB::PurgeEx(const CURL& url)
199 {
200   CSingleLock lock(*this);
201   CStdString strShare = url.GetFileName().substr(0, url.GetFileName().find('/'));
202
203   m_strLastShare = strShare;
204   m_strLastHost = url.GetHostName();
205 }
206
207 CStdString CSMB::URLEncode(const CURL &url)
208 {
209   /* due to smb wanting encoded urls we have to build it manually */
210
211   CStdString flat = "smb://";
212
213   if(url.GetDomain().length() > 0)
214   {
215     flat += URLEncode(url.GetDomain());
216     flat += ";";
217   }
218
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 */)
222   {
223     flat += URLEncode(url.GetUserName());
224     flat += ":";
225     flat += URLEncode(url.GetPassWord());
226     flat += "@";
227   }
228   flat += URLEncode(url.GetHostName());
229
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++ )
235   {
236     flat += "/";
237     flat += URLEncode((*it));
238   }
239
240   /* okey options should go here, thou current samba doesn't support any */
241
242   return flat;
243 }
244
245 CStdString CSMB::URLEncode(const CStdString &value)
246 {
247   CStdString encoded(value);
248   CURL::Encode(encoded);
249   return encoded;
250 }
251
252 /* This is called from CApplication::ProcessSlow() and is used to tell if smbclient have been idle for too long */
253 void CSMB::CheckIfIdle()
254 {
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)
261     {
262       if (m_IdleTimeout > 0)
263           {
264         m_IdleTimeout--;
265       }
266           else
267           {
268         CLog::Log(LOGNOTICE, "Samba is idle. Closing the remaining connections");
269         smb.Deinit();
270       }
271     }
272   }
273 }
274
275 void CSMB::SetActivityTime()
276 {
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 */
279   m_IdleTimeout = 180;
280 }
281
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()
285 {
286   CSingleLock lock(*this);
287   m_OpenConnections++;
288 }
289 void CSMB::AddIdleConnection()
290 {
291   CSingleLock lock(*this);
292   m_OpenConnections--;
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 */
295   m_IdleTimeout = 180;
296 }
297
298 CSMB smb;
299
300 CSmbFile::CSmbFile()
301 {
302   smb.Init();
303   m_fd = -1;
304   smb.AddActiveConnection();
305 }
306
307 CSmbFile::~CSmbFile()
308 {
309   Close();
310   smb.AddIdleConnection();
311 }
312
313 int64_t CSmbFile::GetPosition()
314 {
315   if (m_fd == -1) return 0;
316   smb.Init();
317   CSingleLock lock(smb);
318   int64_t pos = smbc_lseek(m_fd, 0, SEEK_CUR);
319   if ( pos < 0 )
320     return 0;
321   return pos;
322 }
323
324 int64_t CSmbFile::GetLength()
325 {
326   if (m_fd == -1) return 0;
327   return m_fileSize;
328 }
329
330 bool CSmbFile::Open(const CURL& url)
331 {
332   Close();
333
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()))
337   {
338       CLog::Log(LOGNOTICE,"FileSmb->Open: Bad URL : '%s'",url.GetFileName().c_str());
339       return false;
340   }
341   m_url = url;
342
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.
346
347   CStdString strFileName;
348   m_fd = OpenFile(url, strFileName);
349
350   CLog::Log(LOGDEBUG,"CSmbFile::Open - opened %s, fd=%d",url.GetFileName().c_str(), m_fd);
351   if (m_fd == -1)
352   {
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));
355     return false;
356   }
357
358   CSingleLock lock(smb);
359   struct stat tmpBuffer;
360   if (smbc_stat(strFileName, &tmpBuffer) < 0)
361   {
362     smbc_close(m_fd);
363     m_fd = -1;
364     return false;
365   }
366
367   m_fileSize = tmpBuffer.st_size;
368
369   int64_t ret = smbc_lseek(m_fd, 0, SEEK_SET);
370   if ( ret < 0 )
371   {
372     smbc_close(m_fd);
373     m_fd = -1;
374     return false;
375   }
376   // We've successfully opened the file!
377   return true;
378 }
379
380
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
384 /*
385 int CSmbFile::OpenFile(CStdString& strAuth)
386 {
387   int fd = -1;
388
389   CStdString strPath = g_passwordManager.GetSMBAuthFilename(strAuth);
390
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.
399   if(fd >= 0)
400     strAuth = strPath;
401
402   return fd;
403 }
404 */
405
406 int CSmbFile::OpenFile(const CURL &url, CStdString& strAuth)
407 {
408   int fd = -1;
409   smb.Init();
410
411   strAuth = GetAuthenticatedPath(url);
412   CStdString strPath = strAuth;
413
414   {
415     CSingleLock lock(smb);
416     fd = smbc_open(strPath.c_str(), O_RDONLY, 0);
417   }
418
419   if (fd >= 0)
420     strAuth = strPath;
421
422   return fd;
423 }
424
425 bool CSmbFile::Exists(const CURL& url)
426 {
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;
430
431   smb.Init();
432   CStdString strFileName = GetAuthenticatedPath(url);
433
434   struct stat info;
435
436   CSingleLock lock(smb);
437   int iResult = smbc_stat(strFileName, &info);
438
439   if (iResult < 0) return false;
440   return true;
441 }
442
443 int CSmbFile::Stat(struct __stat64* buffer)
444 {
445   if (m_fd == -1)
446     return -1;
447
448   struct stat tmpBuffer = {0};
449
450   CSingleLock lock(smb);
451   int iResult = smbc_fstat(m_fd, &tmpBuffer);
452
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;
465
466   return iResult;
467 }
468
469 int CSmbFile::Stat(const CURL& url, struct __stat64* buffer)
470 {
471   smb.Init();
472   CStdString strFileName = GetAuthenticatedPath(url);
473   CSingleLock lock(smb);
474
475   struct stat tmpBuffer = {0};
476   int iResult = smbc_stat(strFileName, &tmpBuffer);
477
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;
490
491   return iResult;
492 }
493
494 int CSmbFile::Truncate(int64_t size)
495 {
496   if (m_fd == -1) return 0;
497 /* 
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
502
503 #if defined(TARGET_ANDROID)
504   int iResult = 0;
505 #else
506   int iResult = smbc_ftruncate(m_fd, size);
507 #endif
508 */
509   CLog::Log(LOGWARNING, "%s - Warning(smbc_ftruncate called and not implemented)", __FUNCTION__);
510   return 0;
511 }
512
513 unsigned int CSmbFile::Read(void *lpBuf, int64_t uiBufSize)
514 {
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;
527
528   int bytesRead = smbc_read(m_fd, lpBuf, (int)uiBufSize);
529
530   if ( bytesRead < 0 && errno == EINVAL )
531   {
532     CLog::Log(LOGERROR, "%s - Error( %d, %d, %s ) - Retrying", __FUNCTION__, bytesRead, errno, strerror(errno));
533     bytesRead = smbc_read(m_fd, lpBuf, (int)uiBufSize);
534   }
535
536   if ( bytesRead < 0 )
537   {
538     CLog::Log(LOGERROR, "%s - Error( %d, %d, %s )", __FUNCTION__, bytesRead, errno, strerror(errno));
539     return 0;
540   }
541
542   return (unsigned int)bytesRead;
543 }
544
545 int64_t CSmbFile::Seek(int64_t iFilePosition, int iWhence)
546 {
547   if (m_fd == -1) return -1;
548
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);
552
553   if ( pos < 0 )
554   {
555     CLog::Log(LOGERROR, "%s - Error( %"PRId64", %d, %s )", __FUNCTION__, pos, errno, strerror(errno));
556     return -1;
557   }
558
559   return (int64_t)pos;
560 }
561
562 void CSmbFile::Close()
563 {
564   if (m_fd != -1)
565   {
566     CLog::Log(LOGDEBUG,"CSmbFile::Close closing fd %d", m_fd);
567     CSingleLock lock(smb);
568     smbc_close(m_fd);
569   }
570   m_fd = -1;
571 }
572
573 int CSmbFile::Write(const void* lpBuf, int64_t uiBufSize)
574 {
575   if (m_fd == -1) return -1;
576   DWORD dwNumberOfBytesWritten = 0;
577
578   // lpBuf can be safely casted to void* since xmbc_write will only read from it.
579   smb.Init();
580   CSingleLock lock(smb);
581   dwNumberOfBytesWritten = smbc_write(m_fd, (void*)lpBuf, (DWORD)uiBufSize);
582
583   return (int)dwNumberOfBytesWritten;
584 }
585
586 bool CSmbFile::Delete(const CURL& url)
587 {
588   smb.Init();
589   CStdString strFile = GetAuthenticatedPath(url);
590
591   CSingleLock lock(smb);
592
593   int result = smbc_unlink(strFile.c_str());
594
595   if(result != 0)
596     CLog::Log(LOGERROR, "%s - Error( %s )", __FUNCTION__, strerror(errno));
597
598   return (result == 0);
599 }
600
601 bool CSmbFile::Rename(const CURL& url, const CURL& urlnew)
602 {
603   smb.Init();
604   CStdString strFile = GetAuthenticatedPath(url);
605   CStdString strFileNew = GetAuthenticatedPath(urlnew);
606   CSingleLock lock(smb);
607
608   int result = smbc_rename(strFile.c_str(), strFileNew.c_str());
609
610   if(result != 0)
611     CLog::Log(LOGERROR, "%s - Error( %s )", __FUNCTION__, strerror(errno));
612
613   return (result == 0);
614 }
615
616 bool CSmbFile::OpenForWrite(const CURL& url, bool bOverWrite)
617 {
618   m_fileSize = 0;
619
620   Close();
621   smb.Init();
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;
625
626   CStdString strFileName = GetAuthenticatedPath(url);
627   CSingleLock lock(smb);
628
629   if (bOverWrite)
630   {
631     CLog::Log(LOGWARNING, "FileSmb::OpenForWrite() called with overwriting enabled! - %s", strFileName.c_str());
632     m_fd = smbc_creat(strFileName.c_str(), 0);
633   }
634   else
635   {
636     m_fd = smbc_open(strFileName.c_str(), O_RDWR, 0);
637   }
638
639   if (m_fd == -1)
640   {
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));
643     return false;
644   }
645
646   // We've successfully opened the file!
647   return true;
648 }
649
650 bool CSmbFile::IsValidFile(const CStdString& strFileName)
651 {
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 */
655       return false;
656   return true;
657 }
658
659 CStdString CSmbFile::GetAuthenticatedPath(const CURL &url)
660 {
661   CURL authURL(url);
662   CPasswordManager::GetInstance().AuthenticateURL(authURL);
663   return smb.URLEncode(authURL);
664 }