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 #include "threads/SystemClock.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"
34 #pragma comment(lib, "ssh.lib")
38 #define S_ISDIR(m) ((m & _S_IFDIR) != 0)
41 #define S_ISREG(m) ((m & _S_IFREG) != 0)
44 #define O_RDONLY _O_RDONLY
47 using namespace XFILE;
51 static CStdString CorrectPath(const CStdString path)
55 else if (path.substr(0, 2) == "~/")
56 return "./" + path.substr(2);
61 static const char * SFTPErrorText(int sftp_error)
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";
90 return "No media in remote drive";
92 return "Not a valid error code, probably called on an invalid session";
94 CLog::Log(LOGERROR, "SFTPErrorText: Unknown error code: %d", sftp_error);
96 return "Unknown error code";
101 CSFTPSession::CSFTPSession(const CStdString &host, unsigned int port, const CStdString &username, const CStdString &password)
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))
108 m_LastActive = XbmcThreads::SystemClockMillis();
111 CSFTPSession::~CSFTPSession()
113 CSingleLock lock(m_critSect);
117 sftp_file CSFTPSession::CreateFileHande(const CStdString &file)
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);
126 sftp_file_set_blocking(handle);
130 CLog::Log(LOGERROR, "SFTPSession: Was connected but couldn't create filehandle for '%s'", file.c_str());
133 CLog::Log(LOGERROR, "SFTPSession: Not connected and can't create file handle for '%s'", file.c_str());
138 void CSFTPSession::CloseFileHandle(sftp_file handle)
140 CSingleLock lock(m_critSect);
144 bool CSFTPSession::GetDirectory(const CStdString &base, const CStdString &folder, CFileItemList &items)
146 int sftp_error = SSH_FX_OK;
152 CSingleLock lock(m_critSect);
153 m_LastActive = XbmcThreads::SystemClockMillis();
154 dir = sftp_opendir(m_sftp_session, CorrectPath(folder).c_str());
156 //Doing as little work as possible within the critical section
158 sftp_error = sftp_get_error(m_sftp_session);
163 CLog::Log(LOGERROR, "%s: %s for '%s'", __FUNCTION__, SFTPErrorText(sftp_error), folder.c_str());
170 sftp_attributes attributes = NULL;
173 CSingleLock lock(m_critSect);
174 read = sftp_dir_eof(dir) == 0;
175 attributes = sftp_readdir(m_sftp_session, dir);
178 if (attributes && (attributes->name == NULL || strcmp(attributes->name, "..") == 0 || strcmp(attributes->name, ".") == 0))
180 CSingleLock lock(m_critSect);
181 sftp_attributes_free(attributes);
187 CStdString itemName = attributes->name;
188 CStdString localPath = folder;
189 localPath.append(itemName);
191 if (attributes->type == SSH_FILEXFER_TYPE_SYMLINK)
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)
200 CFileItemPtr pItem(new CFileItem);
201 pItem->SetLabel(itemName);
203 if (itemName[0] == '.')
204 pItem->SetProperty("file:hidden", true);
206 if (attributes->flags & SSH_FILEXFER_ATTR_ACMODTIME)
207 pItem->m_dateTime = attributes->mtime;
209 if (attributes->type & SSH_FILEXFER_TYPE_DIRECTORY)
211 localPath.append("/");
212 pItem->m_bIsFolder = true;
217 pItem->m_dwSize = attributes->size;
220 pItem->SetPath(base + localPath);
224 CSingleLock lock(m_critSect);
225 sftp_attributes_free(attributes);
233 CSingleLock lock(m_critSect);
241 CLog::Log(LOGERROR, "SFTPSession: Not connected, can't list directory '%s'", folder.c_str());
246 bool CSFTPSession::DirectoryExists(const char *path)
249 uint32_t permissions = 0;
250 exists = GetItemPermissions(path, permissions);
251 return exists && S_ISDIR(permissions);
254 bool CSFTPSession::FileExists(const char *path)
257 uint32_t permissions = 0;
258 exists = GetItemPermissions(path, permissions);
259 return exists && S_ISREG(permissions);
262 int CSFTPSession::Stat(const char *path, struct __stat64* buffer)
264 CSingleLock lock(m_critSect);
267 m_LastActive = XbmcThreads::SystemClockMillis();
268 sftp_attributes attributes = sftp_stat(m_sftp_session, CorrectPath(path).c_str());
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;
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;
282 sftp_attributes_free(attributes);
287 CLog::Log(LOGERROR, "SFTPSession::Stat - Failed to get attributes for '%s'", path);
293 CLog::Log(LOGERROR, "SFTPSession::Stat - Failed because not connected for '%s'", path);
298 int CSFTPSession::Seek(sftp_file handle, uint64_t position)
300 CSingleLock lock(m_critSect);
301 m_LastActive = XbmcThreads::SystemClockMillis();
302 return sftp_seek64(handle, position);
305 int CSFTPSession::Read(sftp_file handle, void *buffer, size_t length)
307 CSingleLock lock(m_critSect);
308 m_LastActive = XbmcThreads::SystemClockMillis();
309 return sftp_read(handle, buffer, length);
312 int64_t CSFTPSession::GetPosition(sftp_file handle)
314 CSingleLock lock(m_critSect);
315 m_LastActive = XbmcThreads::SystemClockMillis();
316 return sftp_tell64(handle);
319 bool CSFTPSession::IsIdle()
321 return (XbmcThreads::SystemClockMillis() - m_LastActive) > 90000;
324 bool CSFTPSession::VerifyKnownHost(ssh_session session)
326 switch (ssh_is_server_known(session))
328 case SSH_SERVER_KNOWN_OK:
330 case SSH_SERVER_KNOWN_CHANGED:
331 CLog::Log(LOGERROR, "SFTPSession: Server that was known has changed");
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");
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)
342 CLog::Log(LOGERROR, "CSFTPSession: Failed to save host '%s'", strerror(errno));
347 case SSH_SERVER_ERROR:
348 CLog::Log(LOGERROR, "SFTPSession: Failed to verify host '%s'", ssh_get_error(session));
355 bool CSFTPSession::Connect(const CStdString &host, unsigned int port, const CStdString &username, const CStdString &password)
357 int timeout = SFTP_TIMEOUT;
360 m_sftp_session = NULL;
363 if (m_session == NULL)
365 CLog::Log(LOGERROR, "SFTPSession: Failed to initialize session for host '%s'", host.c_str());
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)
372 CLog::Log(LOGERROR, "SFTPSession: Failed to set username '%s' for session", username.c_str());
376 if (ssh_options_set(m_session, SSH_OPTIONS_HOST, host.c_str()) < 0)
378 CLog::Log(LOGERROR, "SFTPSession: Failed to set host '%s' for session", host.c_str());
382 if (ssh_options_set(m_session, SSH_OPTIONS_PORT, &port) < 0)
384 CLog::Log(LOGERROR, "SFTPSession: Failed to set port '%d' for session", port);
388 ssh_options_set(m_session, SSH_OPTIONS_LOG_VERBOSITY, 0);
389 ssh_options_set(m_session, SSH_OPTIONS_TIMEOUT, &timeout);
391 SSH_OPTIONS* options = ssh_options_new();
393 if (ssh_options_set_username(options, username.c_str()) < 0)
395 CLog::Log(LOGERROR, "SFTPSession: Failed to set username '%s' for session", username.c_str());
399 if (ssh_options_set_host(options, host.c_str()) < 0)
401 CLog::Log(LOGERROR, "SFTPSession: Failed to set host '%s' for session", host.c_str());
405 if (ssh_options_set_port(options, port) < 0)
407 CLog::Log(LOGERROR, "SFTPSession: Failed to set port '%d' for session", port);
411 ssh_options_set_timeout(options, timeout, 0);
413 ssh_options_set_log_verbosity(options, 0);
415 ssh_set_options(m_session, options);
418 if(ssh_connect(m_session))
420 CLog::Log(LOGERROR, "SFTPSession: Failed to connect '%s'", ssh_get_error(m_session));
424 if (!VerifyKnownHost(m_session))
426 CLog::Log(LOGERROR, "SFTPSession: Host is not known '%s'", ssh_get_error(m_session));
431 int noAuth = SSH_AUTH_DENIED;
432 if ((noAuth = ssh_userauth_none(m_session, NULL)) == SSH_AUTH_ERROR)
434 CLog::Log(LOGERROR, "SFTPSession: Failed to authenticate via guest '%s'", ssh_get_error(m_session));
438 int method = ssh_auth_list(m_session);
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)
444 CLog::Log(LOGERROR, "SFTPSession: Failed to authenticate via publickey '%s'", ssh_get_error(m_session));
448 // Try to authenticate with password
449 int passwordAuth = SSH_AUTH_DENIED;
450 if (method & SSH_AUTH_METHOD_PASSWORD)
452 if (publicKeyAuth != SSH_AUTH_SUCCESS &&
453 (passwordAuth = ssh_userauth_password(m_session, username.c_str(), password.c_str())) == SSH_AUTH_ERROR)
455 CLog::Log(LOGERROR, "SFTPSession: Failed to authenticate via password '%s'", ssh_get_error(m_session));
459 else if (!password.empty())
461 CLog::Log(LOGERROR, "SFTPSession: Password present, but server does not support password authentication");
464 if (noAuth == SSH_AUTH_SUCCESS || publicKeyAuth == SSH_AUTH_SUCCESS || passwordAuth == SSH_AUTH_SUCCESS)
466 m_sftp_session = sftp_new(m_session);
468 if (m_sftp_session == NULL)
470 CLog::Log(LOGERROR, "SFTPSession: Failed to initialize channel '%s'", ssh_get_error(m_session));
474 if (sftp_init(m_sftp_session))
476 CLog::Log(LOGERROR, "SFTPSession: Failed to initialize sftp '%s'", ssh_get_error(m_session));
484 CLog::Log(LOGERROR, "SFTPSession: No authentication method successful");
490 void CSFTPSession::Disconnect()
493 sftp_free(m_sftp_session);
496 ssh_disconnect(m_session);
498 m_sftp_session = NULL;
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.
508 bool CSFTPSession::GetItemPermissions(const char *path, uint32_t &permissions)
510 bool gotPermissions = false;
511 CSingleLock lock(m_critSect);
514 sftp_attributes attributes = sftp_stat(m_sftp_session, CorrectPath(path).c_str());
517 if (attributes->flags & SSH_FILEXFER_ATTR_PERMISSIONS)
519 permissions = attributes->permissions;
520 gotPermissions = true;
523 sftp_attributes_free(attributes);
526 return gotPermissions;
529 CCriticalSection CSFTPSessionManager::m_critSect;
530 map<CStdString, CSFTPSessionPtr> CSFTPSessionManager::sessions;
532 CSFTPSessionPtr CSFTPSessionManager::CreateSession(const CURL &url)
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;
539 return CSFTPSessionManager::CreateSession(hostname, port, username, password);
542 CSFTPSessionPtr CSFTPSessionManager::CreateSession(const CStdString &host, unsigned int port, const CStdString &username, const CStdString &password)
544 // Convert port number to string
547 CStdString portstr = itoa.str();
549 CSingleLock lock(m_critSect);
550 CStdString key = username + ":" + password + "@" + host + ":" + portstr;
551 CSFTPSessionPtr ptr = sessions[key];
554 ptr = CSFTPSessionPtr(new CSFTPSession(host, port, username, password));
561 void CSFTPSessionManager::ClearOutIdleSessions()
563 CSingleLock lock(m_critSect);
564 for(map<CStdString, CSFTPSessionPtr>::iterator iter = sessions.begin(); iter != sessions.end();)
566 if (iter->second->IsIdle())
567 sessions.erase(iter++);
573 void CSFTPSessionManager::DisconnectAllSessions()
575 CSingleLock lock(m_critSect);
579 CSFTPFile::CSFTPFile()
581 m_sftp_handle = NULL;
584 CSFTPFile::~CSFTPFile()
589 bool CSFTPFile::Open(const CURL& url)
591 m_session = CSFTPSessionManager::CreateSession(url);
594 m_file = url.GetFileName().c_str();
595 m_sftp_handle = m_session->CreateFileHande(m_file);
597 return (m_sftp_handle != NULL);
601 CLog::Log(LOGERROR, "SFTPFile: Failed to allocate session");
606 void CSFTPFile::Close()
608 if (m_session && m_sftp_handle)
610 m_session->CloseFileHandle(m_sftp_handle);
611 m_sftp_handle = NULL;
612 m_session = CSFTPSessionPtr();
616 int64_t CSFTPFile::Seek(int64_t iFilePosition, int iWhence)
618 if (m_session && m_sftp_handle)
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;
628 if (m_session->Seek(m_sftp_handle, position) == 0)
629 return GetPosition();
635 CLog::Log(LOGERROR, "SFTPFile: Can't seek without a filehandle");
640 unsigned int CSFTPFile::Read(void* lpBuf, int64_t uiBufSize)
642 if (m_session && m_sftp_handle)
644 int rc = m_session->Read(m_sftp_handle, lpBuf, (size_t)uiBufSize);
649 CLog::Log(LOGERROR, "SFTPFile: Failed to read %i", rc);
652 CLog::Log(LOGERROR, "SFTPFile: Can't read without a filehandle");
657 bool CSFTPFile::Exists(const CURL& url)
659 CSFTPSessionPtr session = CSFTPSessionManager::CreateSession(url);
661 return session->FileExists(url.GetFileName().c_str());
664 CLog::Log(LOGERROR, "SFTPFile: Failed to create session to check exists for '%s'", url.GetFileName().c_str());
669 int CSFTPFile::Stat(const CURL& url, struct __stat64* buffer)
671 CSFTPSessionPtr session = CSFTPSessionManager::CreateSession(url);
673 return session->Stat(url.GetFileName().c_str(), buffer);
676 CLog::Log(LOGERROR, "SFTPFile: Failed to create session to stat for '%s'", url.GetFileName().c_str());
681 int CSFTPFile::Stat(struct __stat64* buffer)
684 return m_session->Stat(m_file.c_str(), buffer);
686 CLog::Log(LOGERROR, "SFTPFile: Can't stat without a session for '%s'", m_file.c_str());
690 int64_t CSFTPFile::GetLength()
692 struct __stat64 buffer;
693 if (Stat(&buffer) != 0)
697 int64_t length = buffer.st_size;
702 int64_t CSFTPFile::GetPosition()
704 if (m_session && m_sftp_handle)
705 return m_session->GetPosition(m_sftp_handle);
707 CLog::Log(LOGERROR, "SFTPFile: Can't get position without a filehandle for '%s'", m_file.c_str());
711 int CSFTPFile::IoControl(EIoControl request, void* param)
713 if(request == IOCTRL_SEEK_POSSIBLE)