Merge pull request #4857 from t-nelson/Gotham_13.2_backports
[vuplus_xbmc] / xbmc / filesystem / SFTPFile.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 #include "threads/SystemClock.h"
22 #include "SFTPFile.h"
23 #ifdef HAS_FILESYSTEM_SFTP
24 #include "threads/SingleLock.h"
25 #include "utils/log.h"
26 #include "utils/TimeUtils.h"
27 #include "utils/Variant.h"
28 #include "Util.h"
29 #include "URL.h"
30 #include <fcntl.h>
31 #include <sstream>
32
33 #ifdef TARGET_WINDOWS
34 #pragma comment(lib, "ssh.lib")
35 #endif
36
37 #ifndef S_ISDIR
38 #define S_ISDIR(m) ((m & _S_IFDIR) != 0)
39 #endif
40 #ifndef S_ISREG
41 #define S_ISREG(m) ((m & _S_IFREG) != 0)
42 #endif
43 #ifndef O_RDONLY
44 #define O_RDONLY _O_RDONLY
45 #endif
46
47 using namespace XFILE;
48 using namespace std;
49
50
51 static CStdString CorrectPath(const CStdString path)
52 {
53   if (path == "~")
54     return "./";
55   else if (path.substr(0, 2) == "~/")
56     return "./" + path.substr(2);
57   else
58     return "/" + path;
59 }
60
61 static const char * SFTPErrorText(int sftp_error)
62 {
63   switch(sftp_error)
64   {
65     case SSH_FX_OK:
66       return "No error";
67     case SSH_FX_EOF:
68       return "End-of-file encountered";
69     case SSH_FX_NO_SUCH_FILE:
70       return "File doesn't exist";
71     case SSH_FX_PERMISSION_DENIED:
72       return "Permission denied";
73     case SSH_FX_BAD_MESSAGE:
74       return "Garbage received from server";
75     case SSH_FX_NO_CONNECTION:
76       return "No connection has been set up";
77     case SSH_FX_CONNECTION_LOST:
78       return "There was a connection, but we lost it";
79     case SSH_FX_OP_UNSUPPORTED:
80       return "Operation not supported by the server";
81     case SSH_FX_INVALID_HANDLE:
82       return "Invalid file handle";
83     case SSH_FX_NO_SUCH_PATH:
84       return "No such file or directory path exists";
85     case SSH_FX_FILE_ALREADY_EXISTS:
86       return "An attempt to create an already existing file or directory has been made";
87     case SSH_FX_WRITE_PROTECT:
88       return "We are trying to write on a write-protected filesystem";
89     case SSH_FX_NO_MEDIA:
90       return "No media in remote drive";
91     case -1:
92       return "Not a valid error code, probably called on an invalid session";
93     default:
94       CLog::Log(LOGERROR, "SFTPErrorText: Unknown error code: %d", sftp_error);
95   }
96   return "Unknown error code";
97 }
98
99
100
101 CSFTPSession::CSFTPSession(const CStdString &host, unsigned int port, const CStdString &username, const CStdString &password)
102 {
103   CLog::Log(LOGINFO, "SFTPSession: Creating new session on host '%s:%d' with user '%s'", host.c_str(), port, username.c_str());
104   CSingleLock lock(m_critSect);
105   if (!Connect(host, port, username, password))
106     Disconnect();
107
108   m_LastActive = XbmcThreads::SystemClockMillis();
109 }
110
111 CSFTPSession::~CSFTPSession()
112 {
113   CSingleLock lock(m_critSect);
114   Disconnect();
115 }
116
117 sftp_file CSFTPSession::CreateFileHande(const CStdString &file)
118 {
119   if (m_connected)
120   {
121     CSingleLock lock(m_critSect);
122     m_LastActive = XbmcThreads::SystemClockMillis();
123     sftp_file handle = sftp_open(m_sftp_session, CorrectPath(file).c_str(), O_RDONLY, 0);
124     if (handle)
125     {
126       sftp_file_set_blocking(handle);
127       return handle;
128     }
129     else
130       CLog::Log(LOGERROR, "SFTPSession: Was connected but couldn't create filehandle for '%s'", file.c_str());
131   }
132   else
133     CLog::Log(LOGERROR, "SFTPSession: Not connected and can't create file handle for '%s'", file.c_str());
134
135   return NULL;
136 }
137
138 void CSFTPSession::CloseFileHandle(sftp_file handle)
139 {
140   CSingleLock lock(m_critSect);
141   sftp_close(handle);
142 }
143
144 bool CSFTPSession::GetDirectory(const CStdString &base, const CStdString &folder, CFileItemList &items)
145 {
146   int sftp_error = SSH_FX_OK;
147   if (m_connected)
148   {
149     sftp_dir dir = NULL;
150
151     {
152       CSingleLock lock(m_critSect);
153       m_LastActive = XbmcThreads::SystemClockMillis();
154       dir = sftp_opendir(m_sftp_session, CorrectPath(folder).c_str());
155
156       //Doing as little work as possible within the critical section
157       if (!dir)
158         sftp_error = sftp_get_error(m_sftp_session);
159     }
160
161     if (!dir)
162     {
163       CLog::Log(LOGERROR, "%s: %s for '%s'", __FUNCTION__, SFTPErrorText(sftp_error), folder.c_str());
164     }
165     else
166     {
167       bool read = true;
168       while (read)
169       {
170         sftp_attributes attributes = NULL;
171
172         {
173           CSingleLock lock(m_critSect);
174           read = sftp_dir_eof(dir) == 0;
175           attributes = sftp_readdir(m_sftp_session, dir);
176         }
177
178         if (attributes && (attributes->name == NULL || strcmp(attributes->name, "..") == 0 || strcmp(attributes->name, ".") == 0))
179         {
180           CSingleLock lock(m_critSect);
181           sftp_attributes_free(attributes);
182           continue;
183         }
184         
185         if (attributes)
186         {
187           CStdString itemName = attributes->name;
188           CStdString localPath = folder;
189           localPath.append(itemName);
190
191           if (attributes->type == SSH_FILEXFER_TYPE_SYMLINK)
192           {
193             CSingleLock lock(m_critSect);
194             sftp_attributes_free(attributes);
195             attributes = sftp_stat(m_sftp_session, CorrectPath(localPath).c_str());
196             if (attributes == NULL)
197               continue;
198           }
199
200           CFileItemPtr pItem(new CFileItem);
201           pItem->SetLabel(itemName);
202
203           if (itemName[0] == '.')
204             pItem->SetProperty("file:hidden", true);
205
206           if (attributes->flags & SSH_FILEXFER_ATTR_ACMODTIME)
207             pItem->m_dateTime = attributes->mtime;
208
209           if (attributes->type & SSH_FILEXFER_TYPE_DIRECTORY)
210           {
211             localPath.append("/");
212             pItem->m_bIsFolder = true;
213             pItem->m_dwSize = 0;
214           }
215           else
216           {
217             pItem->m_dwSize = attributes->size;
218           }
219
220           pItem->SetPath(base + localPath);
221           items.Add(pItem);
222
223           {
224             CSingleLock lock(m_critSect);
225             sftp_attributes_free(attributes);
226           }
227         }
228         else
229           read = false;
230       }
231
232       {
233         CSingleLock lock(m_critSect);
234         sftp_closedir(dir);
235       }
236
237       return true;
238     }
239   }
240   else
241     CLog::Log(LOGERROR, "SFTPSession: Not connected, can't list directory '%s'", folder.c_str());
242
243   return false;
244 }
245
246 bool CSFTPSession::DirectoryExists(const char *path)
247 {
248   bool exists = false;
249   uint32_t permissions = 0;
250   exists = GetItemPermissions(path, permissions);
251   return exists && S_ISDIR(permissions);
252 }
253
254 bool CSFTPSession::FileExists(const char *path)
255 {
256   bool exists = false;
257   uint32_t permissions = 0;
258   exists = GetItemPermissions(path, permissions);
259   return exists && S_ISREG(permissions);
260 }
261
262 int CSFTPSession::Stat(const char *path, struct __stat64* buffer)
263 {
264   CSingleLock lock(m_critSect);
265   if(m_connected)
266   {
267     m_LastActive = XbmcThreads::SystemClockMillis();
268     sftp_attributes attributes = sftp_stat(m_sftp_session, CorrectPath(path).c_str());
269
270     if (attributes)
271     {
272       memset(buffer, 0, sizeof(struct __stat64));
273       buffer->st_size = attributes->size;
274       buffer->st_mtime = attributes->mtime;
275       buffer->st_atime = attributes->atime;
276
277       if S_ISDIR(attributes->permissions)
278         buffer->st_mode = _S_IFDIR;
279       else if S_ISREG(attributes->permissions)
280         buffer->st_mode = _S_IFREG;
281
282       sftp_attributes_free(attributes);
283       return 0;
284     }
285     else
286     {
287       CLog::Log(LOGERROR, "SFTPSession::Stat - Failed to get attributes for '%s'", path);
288       return -1;
289     }
290   }
291   else
292   {
293     CLog::Log(LOGERROR, "SFTPSession::Stat - Failed because not connected for '%s'", path);
294     return -1;
295   }
296 }
297
298 int CSFTPSession::Seek(sftp_file handle, uint64_t position)
299 {
300   CSingleLock lock(m_critSect);
301   m_LastActive = XbmcThreads::SystemClockMillis();
302   return sftp_seek64(handle, position);
303 }
304
305 int CSFTPSession::Read(sftp_file handle, void *buffer, size_t length)
306 {
307   CSingleLock lock(m_critSect);
308   m_LastActive = XbmcThreads::SystemClockMillis();
309   return sftp_read(handle, buffer, length);
310 }
311
312 int64_t CSFTPSession::GetPosition(sftp_file handle)
313 {
314   CSingleLock lock(m_critSect);
315   m_LastActive = XbmcThreads::SystemClockMillis();
316   return sftp_tell64(handle);
317 }
318
319 bool CSFTPSession::IsIdle()
320 {
321   return (XbmcThreads::SystemClockMillis() - m_LastActive) > 90000;
322 }
323
324 bool CSFTPSession::VerifyKnownHost(ssh_session session)
325 {
326   switch (ssh_is_server_known(session))
327   {
328     case SSH_SERVER_KNOWN_OK:
329       return true;
330     case SSH_SERVER_KNOWN_CHANGED:
331       CLog::Log(LOGERROR, "SFTPSession: Server that was known has changed");
332       return false;
333     case SSH_SERVER_FOUND_OTHER:
334       CLog::Log(LOGERROR, "SFTPSession: The host key for this server was not found but an other type of key exists. An attacker might change the default server key to confuse your client into thinking the key does not exist");
335       return false;
336     case SSH_SERVER_FILE_NOT_FOUND:
337       CLog::Log(LOGINFO, "SFTPSession: Server file was not found, creating a new one");
338     case SSH_SERVER_NOT_KNOWN:
339       CLog::Log(LOGINFO, "SFTPSession: Server unkown, we trust it for now");
340       if (ssh_write_knownhost(session) < 0)
341       {
342         CLog::Log(LOGERROR, "CSFTPSession: Failed to save host '%s'", strerror(errno));
343         return false;
344       }
345
346       return true;
347     case SSH_SERVER_ERROR:
348       CLog::Log(LOGERROR, "SFTPSession: Failed to verify host '%s'", ssh_get_error(session));
349       return false;
350   }
351
352   return false;
353 }
354
355 bool CSFTPSession::Connect(const CStdString &host, unsigned int port, const CStdString &username, const CStdString &password)
356 {
357   int timeout     = SFTP_TIMEOUT;
358   m_connected     = false;
359   m_session       = NULL;
360   m_sftp_session  = NULL;
361
362   m_session=ssh_new();
363   if (m_session == NULL)
364   {
365     CLog::Log(LOGERROR, "SFTPSession: Failed to initialize session for host '%s'", host.c_str());
366     return false;
367   }
368
369 #if LIBSSH_VERSION_INT >= SSH_VERSION_INT(0,4,0)
370   if (ssh_options_set(m_session, SSH_OPTIONS_USER, username.c_str()) < 0)
371   {
372     CLog::Log(LOGERROR, "SFTPSession: Failed to set username '%s' for session", username.c_str());
373     return false;
374   }
375
376   if (ssh_options_set(m_session, SSH_OPTIONS_HOST, host.c_str()) < 0)
377   {
378     CLog::Log(LOGERROR, "SFTPSession: Failed to set host '%s' for session", host.c_str());
379     return false;
380   }
381
382   if (ssh_options_set(m_session, SSH_OPTIONS_PORT, &port) < 0)
383   {
384     CLog::Log(LOGERROR, "SFTPSession: Failed to set port '%d' for session", port);
385     return false;
386   }
387
388   ssh_options_set(m_session, SSH_OPTIONS_LOG_VERBOSITY, 0);
389   ssh_options_set(m_session, SSH_OPTIONS_TIMEOUT, &timeout);  
390 #else
391   SSH_OPTIONS* options = ssh_options_new();
392
393   if (ssh_options_set_username(options, username.c_str()) < 0)
394   {
395     CLog::Log(LOGERROR, "SFTPSession: Failed to set username '%s' for session", username.c_str());
396     return false;
397   }
398
399   if (ssh_options_set_host(options, host.c_str()) < 0)
400   {
401     CLog::Log(LOGERROR, "SFTPSession: Failed to set host '%s' for session", host.c_str());
402     return false;
403   }
404
405   if (ssh_options_set_port(options, port) < 0)
406   {
407     CLog::Log(LOGERROR, "SFTPSession: Failed to set port '%d' for session", port);
408     return false;
409   }
410   
411   ssh_options_set_timeout(options, timeout, 0);
412
413   ssh_options_set_log_verbosity(options, 0);
414
415   ssh_set_options(m_session, options);
416 #endif
417
418   if(ssh_connect(m_session))
419   {
420     CLog::Log(LOGERROR, "SFTPSession: Failed to connect '%s'", ssh_get_error(m_session));
421     return false;
422   }
423
424   if (!VerifyKnownHost(m_session))
425   {
426     CLog::Log(LOGERROR, "SFTPSession: Host is not known '%s'", ssh_get_error(m_session));
427     return false;
428   }
429
430
431   int noAuth = SSH_AUTH_DENIED;
432   if ((noAuth = ssh_userauth_none(m_session, NULL)) == SSH_AUTH_ERROR)
433   {
434     CLog::Log(LOGERROR, "SFTPSession: Failed to authenticate via guest '%s'", ssh_get_error(m_session));
435     return false;
436   }
437
438   int method = ssh_auth_list(m_session);
439
440   // Try to authenticate with public key first
441   int publicKeyAuth = SSH_AUTH_DENIED;
442   if (method & SSH_AUTH_METHOD_PUBLICKEY && (publicKeyAuth = ssh_userauth_autopubkey(m_session, NULL)) == SSH_AUTH_ERROR)
443   {
444     CLog::Log(LOGERROR, "SFTPSession: Failed to authenticate via publickey '%s'", ssh_get_error(m_session));
445     return false;
446   }
447
448   // Try to authenticate with password
449   int passwordAuth = SSH_AUTH_DENIED;
450   if (method & SSH_AUTH_METHOD_PASSWORD)
451   {
452     if (publicKeyAuth != SSH_AUTH_SUCCESS &&
453         (passwordAuth = ssh_userauth_password(m_session, username.c_str(), password.c_str())) == SSH_AUTH_ERROR)
454       {
455         CLog::Log(LOGERROR, "SFTPSession: Failed to authenticate via password '%s'", ssh_get_error(m_session));
456         return false;
457       }
458   }
459   else if (!password.empty())
460   {
461     CLog::Log(LOGERROR, "SFTPSession: Password present, but server does not support password authentication");
462   }
463
464   if (noAuth == SSH_AUTH_SUCCESS || publicKeyAuth == SSH_AUTH_SUCCESS || passwordAuth == SSH_AUTH_SUCCESS)
465   {
466     m_sftp_session = sftp_new(m_session);
467
468     if (m_sftp_session == NULL)
469     {
470       CLog::Log(LOGERROR, "SFTPSession: Failed to initialize channel '%s'", ssh_get_error(m_session));
471       return false;
472     }
473
474     if (sftp_init(m_sftp_session))
475     {
476       CLog::Log(LOGERROR, "SFTPSession: Failed to initialize sftp '%s'", ssh_get_error(m_session));
477       return false;
478     }
479
480     m_connected = true;
481   }
482   else
483   {
484     CLog::Log(LOGERROR, "SFTPSession: No authentication method successful");
485   }
486
487   return m_connected;
488 }
489
490 void CSFTPSession::Disconnect()
491 {
492   if (m_sftp_session)
493     sftp_free(m_sftp_session);
494
495   if (m_session)
496     ssh_disconnect(m_session);
497
498   m_sftp_session = NULL;
499   m_session = NULL;
500 }
501
502 /*!
503  \brief Gets POSIX compatible permissions information about the specified file or directory.
504  \param path Remote SSH path to the file or directory.
505  \param permissions POSIX compatible permissions information for the file or directory (if it exists). i.e. can use macros S_ISDIR() etc.
506  \return Returns \e true, if it was possible to get permissions for the file or directory, \e false otherwise.
507  */
508 bool CSFTPSession::GetItemPermissions(const char *path, uint32_t &permissions)
509 {
510   bool gotPermissions = false;
511   CSingleLock lock(m_critSect);
512   if(m_connected)
513   {
514     sftp_attributes attributes = sftp_stat(m_sftp_session, CorrectPath(path).c_str());
515     if (attributes)
516     {
517       if (attributes->flags & SSH_FILEXFER_ATTR_PERMISSIONS)
518       {
519         permissions = attributes->permissions;
520         gotPermissions = true;
521       }
522
523       sftp_attributes_free(attributes);
524     }
525   }
526   return gotPermissions;
527 }
528
529 CCriticalSection CSFTPSessionManager::m_critSect;
530 map<CStdString, CSFTPSessionPtr> CSFTPSessionManager::sessions;
531
532 CSFTPSessionPtr CSFTPSessionManager::CreateSession(const CURL &url)
533 {
534   string username = url.GetUserName().c_str();
535   string password = url.GetPassWord().c_str();
536   string hostname = url.GetHostName().c_str();
537   unsigned int port = url.HasPort() ? url.GetPort() : 22;
538
539   return CSFTPSessionManager::CreateSession(hostname, port, username, password);
540 }
541
542 CSFTPSessionPtr CSFTPSessionManager::CreateSession(const CStdString &host, unsigned int port, const CStdString &username, const CStdString &password)
543 {
544   // Convert port number to string
545   stringstream itoa;
546   itoa << port;
547   CStdString portstr = itoa.str();
548
549   CSingleLock lock(m_critSect);
550   CStdString key = username + ":" + password + "@" + host + ":" + portstr;
551   CSFTPSessionPtr ptr = sessions[key];
552   if (ptr == NULL)
553   {
554     ptr = CSFTPSessionPtr(new CSFTPSession(host, port, username, password));
555     sessions[key] = ptr;
556   }
557
558   return ptr;
559 }
560
561 void CSFTPSessionManager::ClearOutIdleSessions()
562 {
563   CSingleLock lock(m_critSect);
564   for(map<CStdString, CSFTPSessionPtr>::iterator iter = sessions.begin(); iter != sessions.end();)
565   {
566     if (iter->second->IsIdle())
567       sessions.erase(iter++);
568     else
569       iter++;
570   }
571 }
572
573 void CSFTPSessionManager::DisconnectAllSessions()
574 {
575   CSingleLock lock(m_critSect);
576   sessions.clear();
577 }
578
579 CSFTPFile::CSFTPFile()
580 {
581   m_sftp_handle = NULL;
582 }
583
584 CSFTPFile::~CSFTPFile()
585 {
586   Close();
587 }
588
589 bool CSFTPFile::Open(const CURL& url)
590 {
591   m_session = CSFTPSessionManager::CreateSession(url);
592   if (m_session)
593   {
594     m_file = url.GetFileName().c_str();
595     m_sftp_handle = m_session->CreateFileHande(m_file);
596
597     return (m_sftp_handle != NULL);
598   }
599   else
600   {
601     CLog::Log(LOGERROR, "SFTPFile: Failed to allocate session");
602     return false;
603   }
604 }
605
606 void CSFTPFile::Close()
607 {
608   if (m_session && m_sftp_handle)
609   {
610     m_session->CloseFileHandle(m_sftp_handle);
611     m_sftp_handle = NULL;
612     m_session = CSFTPSessionPtr();
613   }
614 }
615
616 int64_t CSFTPFile::Seek(int64_t iFilePosition, int iWhence)
617 {
618   if (m_session && m_sftp_handle)
619   {
620     uint64_t position = 0;
621     if (iWhence == SEEK_SET)
622       position = iFilePosition;
623     else if (iWhence == SEEK_CUR)
624       position = GetPosition() + iFilePosition;
625     else if (iWhence == SEEK_END)
626       position = GetLength() + iFilePosition;
627
628     if (m_session->Seek(m_sftp_handle, position) == 0)
629       return GetPosition();
630     else
631       return -1;
632   }
633   else
634   {
635     CLog::Log(LOGERROR, "SFTPFile: Can't seek without a filehandle");
636     return -1;
637   }
638 }
639
640 unsigned int CSFTPFile::Read(void* lpBuf, int64_t uiBufSize)
641 {
642   if (m_session && m_sftp_handle)
643   {
644     int rc = m_session->Read(m_sftp_handle, lpBuf, (size_t)uiBufSize);
645
646     if (rc >= 0)
647       return rc;
648     else
649       CLog::Log(LOGERROR, "SFTPFile: Failed to read %i", rc);
650   }
651   else
652     CLog::Log(LOGERROR, "SFTPFile: Can't read without a filehandle");
653
654   return 0;
655 }
656
657 bool CSFTPFile::Exists(const CURL& url)
658 {
659   CSFTPSessionPtr session = CSFTPSessionManager::CreateSession(url);
660   if (session)
661     return session->FileExists(url.GetFileName().c_str());
662   else
663   {
664     CLog::Log(LOGERROR, "SFTPFile: Failed to create session to check exists for '%s'", url.GetFileName().c_str());
665     return false;
666   }
667 }
668
669 int CSFTPFile::Stat(const CURL& url, struct __stat64* buffer)
670 {
671   CSFTPSessionPtr session = CSFTPSessionManager::CreateSession(url);
672   if (session)
673     return session->Stat(url.GetFileName().c_str(), buffer);
674   else
675   {
676     CLog::Log(LOGERROR, "SFTPFile: Failed to create session to stat for '%s'", url.GetFileName().c_str());
677     return -1;
678   }
679 }
680
681 int CSFTPFile::Stat(struct __stat64* buffer)
682 {
683   if (m_session)
684     return m_session->Stat(m_file.c_str(), buffer);
685
686   CLog::Log(LOGERROR, "SFTPFile: Can't stat without a session for '%s'", m_file.c_str());
687   return -1;
688 }
689
690 int64_t CSFTPFile::GetLength()
691 {
692   struct __stat64 buffer;
693   if (Stat(&buffer) != 0)
694     return 0;
695   else
696   {
697     int64_t length = buffer.st_size;
698     return length;
699   }
700 }
701
702 int64_t CSFTPFile::GetPosition()
703 {
704   if (m_session && m_sftp_handle)
705     return m_session->GetPosition(m_sftp_handle);
706
707   CLog::Log(LOGERROR, "SFTPFile: Can't get position without a filehandle for '%s'", m_file.c_str());
708   return 0;
709 }
710
711 int CSFTPFile::IoControl(EIoControl request, void* param)
712 {
713   if(request == IOCTRL_SEEK_POSSIBLE)
714     return 1;
715
716   return -1;
717 }
718
719 #endif