Merge pull request #5095 from koying/fixdroidappcrash
[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   return CURL::Encode(value);
248 }
249
250 /* This is called from CApplication::ProcessSlow() and is used to tell if smbclient have been idle for too long */
251 void CSMB::CheckIfIdle()
252 {
253 /* We check if there are open connections. This is done without a lock to not halt the mainthread. It should be thread safe as
254    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.  */
255   if (m_OpenConnections == 0)
256   { /* I've set the the maxiumum IDLE time to be 1 min and 30 sec. */
257     CSingleLock lock(*this);
258     if (m_OpenConnections == 0 /* check again - when locked */ && m_context != NULL)
259     {
260       if (m_IdleTimeout > 0)
261           {
262         m_IdleTimeout--;
263       }
264           else
265           {
266         CLog::Log(LOGNOTICE, "Samba is idle. Closing the remaining connections");
267         smb.Deinit();
268       }
269     }
270   }
271 }
272
273 void CSMB::SetActivityTime()
274 {
275   /* Since we get called every 500ms from ProcessSlow we limit the tick count to 180 */
276   /* That means we have 2 ticks per second which equals 180/2 == 90 seconds */
277   m_IdleTimeout = 180;
278 }
279
280 /* The following two function is used to keep track on how many Opened files/directories there are.
281    This makes the idle timer not count if a movie is paused for example */
282 void CSMB::AddActiveConnection()
283 {
284   CSingleLock lock(*this);
285   m_OpenConnections++;
286 }
287 void CSMB::AddIdleConnection()
288 {
289   CSingleLock lock(*this);
290   m_OpenConnections--;
291   /* If we close a file we reset the idle timer so that we don't have any wierd behaviours if a user
292      leaves the movie paused for a long while and then press stop */
293   m_IdleTimeout = 180;
294 }
295
296 CSMB smb;
297
298 CSmbFile::CSmbFile()
299 {
300   smb.Init();
301   m_fd = -1;
302   smb.AddActiveConnection();
303 }
304
305 CSmbFile::~CSmbFile()
306 {
307   Close();
308   smb.AddIdleConnection();
309 }
310
311 int64_t CSmbFile::GetPosition()
312 {
313   if (m_fd == -1) return 0;
314   smb.Init();
315   CSingleLock lock(smb);
316   int64_t pos = smbc_lseek(m_fd, 0, SEEK_CUR);
317   if ( pos < 0 )
318     return 0;
319   return pos;
320 }
321
322 int64_t CSmbFile::GetLength()
323 {
324   if (m_fd == -1) return 0;
325   return m_fileSize;
326 }
327
328 bool CSmbFile::Open(const CURL& url)
329 {
330   Close();
331
332   // we can't open files like smb://file.f or smb://server/file.f
333   // if a file matches the if below return false, it can't exist on a samba share.
334   if (!IsValidFile(url.GetFileName()))
335   {
336       CLog::Log(LOGNOTICE,"FileSmb->Open: Bad URL : '%s'",url.GetFileName().c_str());
337       return false;
338   }
339   m_url = url;
340
341   // opening a file to another computer share will create a new session
342   // when opening smb://server xbms will try to find folder.jpg in all shares
343   // listed, which will create lot's of open sessions.
344
345   CStdString strFileName;
346   m_fd = OpenFile(url, strFileName);
347
348   CLog::Log(LOGDEBUG,"CSmbFile::Open - opened %s, fd=%d",url.GetFileName().c_str(), m_fd);
349   if (m_fd == -1)
350   {
351     // write error to logfile
352     CLog::Log(LOGINFO, "FileSmb->Open: Unable to open file : '%s'\nunix_err:'%x' error : '%s'", CURL::GetRedacted(strFileName).c_str(), errno, strerror(errno));
353     return false;
354   }
355
356   CSingleLock lock(smb);
357   struct stat tmpBuffer;
358   if (smbc_stat(strFileName, &tmpBuffer) < 0)
359   {
360     smbc_close(m_fd);
361     m_fd = -1;
362     return false;
363   }
364
365   m_fileSize = tmpBuffer.st_size;
366
367   int64_t ret = smbc_lseek(m_fd, 0, SEEK_SET);
368   if ( ret < 0 )
369   {
370     smbc_close(m_fd);
371     m_fd = -1;
372     return false;
373   }
374   // We've successfully opened the file!
375   return true;
376 }
377
378
379 /// \brief Checks authentication against SAMBA share. Reads password cache created in CSMBDirectory::OpenDir().
380 /// \param strAuth The SMB style path
381 /// \return SMB file descriptor
382 /*
383 int CSmbFile::OpenFile(CStdString& strAuth)
384 {
385   int fd = -1;
386
387   CStdString strPath = g_passwordManager.GetSMBAuthFilename(strAuth);
388
389   fd = smbc_open(strPath.c_str(), O_RDONLY, 0);
390   // TODO: Run a loop here that prompts for our username/password as appropriate?
391   // We have the ability to run a file (eg from a button action) without browsing to
392   // the directory first.  In the case of a password protected share that we do
393   // not have the authentication information for, the above smbc_open() will have
394   // returned negative, and the file will not be opened.  While this is not a particular
395   // likely scenario, we might want to implement prompting for the password in this case.
396   // The code from SMBDirectory can be used for this.
397   if(fd >= 0)
398     strAuth = strPath;
399
400   return fd;
401 }
402 */
403
404 int CSmbFile::OpenFile(const CURL &url, CStdString& strAuth)
405 {
406   int fd = -1;
407   smb.Init();
408
409   strAuth = GetAuthenticatedPath(url);
410   CStdString strPath = strAuth;
411
412   {
413     CSingleLock lock(smb);
414     fd = smbc_open(strPath.c_str(), O_RDONLY, 0);
415   }
416
417   if (fd >= 0)
418     strAuth = strPath;
419
420   return fd;
421 }
422
423 bool CSmbFile::Exists(const CURL& url)
424 {
425   // we can't open files like smb://file.f or smb://server/file.f
426   // if a file matches the if below return false, it can't exist on a samba share.
427   if (!IsValidFile(url.GetFileName())) return false;
428
429   smb.Init();
430   CStdString strFileName = GetAuthenticatedPath(url);
431
432   struct stat info;
433
434   CSingleLock lock(smb);
435   int iResult = smbc_stat(strFileName, &info);
436
437   if (iResult < 0) return false;
438   return true;
439 }
440
441 int CSmbFile::Stat(struct __stat64* buffer)
442 {
443   if (m_fd == -1)
444     return -1;
445
446   struct stat tmpBuffer = {0};
447
448   CSingleLock lock(smb);
449   int iResult = smbc_fstat(m_fd, &tmpBuffer);
450
451   memset(buffer, 0, sizeof(struct __stat64));
452   buffer->st_dev = tmpBuffer.st_dev;
453   buffer->st_ino = tmpBuffer.st_ino;
454   buffer->st_mode = tmpBuffer.st_mode;
455   buffer->st_nlink = tmpBuffer.st_nlink;
456   buffer->st_uid = tmpBuffer.st_uid;
457   buffer->st_gid = tmpBuffer.st_gid;
458   buffer->st_rdev = tmpBuffer.st_rdev;
459   buffer->st_size = tmpBuffer.st_size;
460   buffer->st_atime = tmpBuffer.st_atime;
461   buffer->st_mtime = tmpBuffer.st_mtime;
462   buffer->st_ctime = tmpBuffer.st_ctime;
463
464   return iResult;
465 }
466
467 int CSmbFile::Stat(const CURL& url, struct __stat64* buffer)
468 {
469   smb.Init();
470   CStdString strFileName = GetAuthenticatedPath(url);
471   CSingleLock lock(smb);
472
473   struct stat tmpBuffer = {0};
474   int iResult = smbc_stat(strFileName, &tmpBuffer);
475
476   memset(buffer, 0, sizeof(struct __stat64));
477   buffer->st_dev = tmpBuffer.st_dev;
478   buffer->st_ino = tmpBuffer.st_ino;
479   buffer->st_mode = tmpBuffer.st_mode;
480   buffer->st_nlink = tmpBuffer.st_nlink;
481   buffer->st_uid = tmpBuffer.st_uid;
482   buffer->st_gid = tmpBuffer.st_gid;
483   buffer->st_rdev = tmpBuffer.st_rdev;
484   buffer->st_size = tmpBuffer.st_size;
485   buffer->st_atime = tmpBuffer.st_atime;
486   buffer->st_mtime = tmpBuffer.st_mtime;
487   buffer->st_ctime = tmpBuffer.st_ctime;
488
489   return iResult;
490 }
491
492 int CSmbFile::Truncate(int64_t size)
493 {
494   if (m_fd == -1) return 0;
495 /* 
496  * This would force us to be dependant on SMBv3.2 which is GPLv3
497  * This is only used by the TagLib writers, which are not currently in use
498  * So log and warn until we implement TagLib writing & can re-implement this better.
499   CSingleLock lock(smb); // Init not called since it has to be "inited" by now
500
501 #if defined(TARGET_ANDROID)
502   int iResult = 0;
503 #else
504   int iResult = smbc_ftruncate(m_fd, size);
505 #endif
506 */
507   CLog::Log(LOGWARNING, "%s - Warning(smbc_ftruncate called and not implemented)", __FUNCTION__);
508   return 0;
509 }
510
511 unsigned int CSmbFile::Read(void *lpBuf, int64_t uiBufSize)
512 {
513   if (m_fd == -1) return 0;
514   CSingleLock lock(smb); // Init not called since it has to be "inited" by now
515   smb.SetActivityTime();
516   /* work around stupid bug in samba */
517   /* some samba servers has a bug in it where the */
518   /* 17th bit will be ignored in a request of data */
519   /* this can lead to a very small return of data */
520   /* also worse, a request of exactly 64k will return */
521   /* as if eof, client has a workaround for windows */
522   /* thou it seems other servers are affected too */
523   if( uiBufSize >= 64*1024-2 )
524     uiBufSize = 64*1024-2;
525
526   int bytesRead = smbc_read(m_fd, lpBuf, (int)uiBufSize);
527
528   if ( bytesRead < 0 && errno == EINVAL )
529   {
530     CLog::Log(LOGERROR, "%s - Error( %d, %d, %s ) - Retrying", __FUNCTION__, bytesRead, errno, strerror(errno));
531     bytesRead = smbc_read(m_fd, lpBuf, (int)uiBufSize);
532   }
533
534   if ( bytesRead < 0 )
535   {
536     CLog::Log(LOGERROR, "%s - Error( %d, %d, %s )", __FUNCTION__, bytesRead, errno, strerror(errno));
537     return 0;
538   }
539
540   return (unsigned int)bytesRead;
541 }
542
543 int64_t CSmbFile::Seek(int64_t iFilePosition, int iWhence)
544 {
545   if (m_fd == -1) return -1;
546
547   CSingleLock lock(smb); // Init not called since it has to be "inited" by now
548   smb.SetActivityTime();
549   int64_t pos = smbc_lseek(m_fd, iFilePosition, iWhence);
550
551   if ( pos < 0 )
552   {
553     CLog::Log(LOGERROR, "%s - Error( %"PRId64", %d, %s )", __FUNCTION__, pos, errno, strerror(errno));
554     return -1;
555   }
556
557   return (int64_t)pos;
558 }
559
560 void CSmbFile::Close()
561 {
562   if (m_fd != -1)
563   {
564     CLog::Log(LOGDEBUG,"CSmbFile::Close closing fd %d", m_fd);
565     CSingleLock lock(smb);
566     smbc_close(m_fd);
567   }
568   m_fd = -1;
569 }
570
571 int CSmbFile::Write(const void* lpBuf, int64_t uiBufSize)
572 {
573   if (m_fd == -1) return -1;
574   DWORD dwNumberOfBytesWritten = 0;
575
576   // lpBuf can be safely casted to void* since xbmc_write will only read from it.
577   smb.Init();
578   CSingleLock lock(smb);
579   dwNumberOfBytesWritten = smbc_write(m_fd, (void*)lpBuf, (DWORD)uiBufSize);
580
581   return (int)dwNumberOfBytesWritten;
582 }
583
584 bool CSmbFile::Delete(const CURL& url)
585 {
586   smb.Init();
587   CStdString strFile = GetAuthenticatedPath(url);
588
589   CSingleLock lock(smb);
590
591   int result = smbc_unlink(strFile.c_str());
592
593   if(result != 0)
594     CLog::Log(LOGERROR, "%s - Error( %s )", __FUNCTION__, strerror(errno));
595
596   return (result == 0);
597 }
598
599 bool CSmbFile::Rename(const CURL& url, const CURL& urlnew)
600 {
601   smb.Init();
602   CStdString strFile = GetAuthenticatedPath(url);
603   CStdString strFileNew = GetAuthenticatedPath(urlnew);
604   CSingleLock lock(smb);
605
606   int result = smbc_rename(strFile.c_str(), strFileNew.c_str());
607
608   if(result != 0)
609     CLog::Log(LOGERROR, "%s - Error( %s )", __FUNCTION__, strerror(errno));
610
611   return (result == 0);
612 }
613
614 bool CSmbFile::OpenForWrite(const CURL& url, bool bOverWrite)
615 {
616   m_fileSize = 0;
617
618   Close();
619   smb.Init();
620   // we can't open files like smb://file.f or smb://server/file.f
621   // if a file matches the if below return false, it can't exist on a samba share.
622   if (!IsValidFile(url.GetFileName())) return false;
623
624   CStdString strFileName = GetAuthenticatedPath(url);
625   CSingleLock lock(smb);
626
627   if (bOverWrite)
628   {
629     CLog::Log(LOGWARNING, "FileSmb::OpenForWrite() called with overwriting enabled! - %s", strFileName.c_str());
630     m_fd = smbc_creat(strFileName.c_str(), 0);
631   }
632   else
633   {
634     m_fd = smbc_open(strFileName.c_str(), O_RDWR, 0);
635   }
636
637   if (m_fd == -1)
638   {
639     // write error to logfile
640     CLog::Log(LOGERROR, "FileSmb->Open: Unable to open file : '%s'\nunix_err:'%x' error : '%s'", strFileName.c_str(), errno, strerror(errno));
641     return false;
642   }
643
644   // We've successfully opened the file!
645   return true;
646 }
647
648 bool CSmbFile::IsValidFile(const CStdString& strFileName)
649 {
650   if (strFileName.find('/') == std::string::npos || /* doesn't have sharename */
651       StringUtils::EndsWith(strFileName, "/.") || /* not current folder */
652       StringUtils::EndsWith(strFileName, "/.."))  /* not parent folder */
653       return false;
654   return true;
655 }
656
657 CStdString CSmbFile::GetAuthenticatedPath(const CURL &url)
658 {
659   CURL authURL(url);
660   CPasswordManager::GetInstance().AuthenticateURL(authURL);
661   return smb.URLEncode(authURL);
662 }