/* * Copyright (c) 2001 Lucas Fisher * Copyright (c) 2009 Andreas Schneider * Copyright (c) 2020 Martin Sandsmark * KDE2 port * Copyright (c) 2022 Mavridis Philippe * Trinity port * * Portions Copyright (c) 2020-2021 Harald Sitter * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License (LGPL) as published by the Free Software Foundation; * either version 2 of the License, or (at your option) any later * version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "tdeio_sftp.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define TDEIO_SFTP_SPECIAL_TIMEOUT 30 #define ZERO_STRUCTP(x) do { if ((x) != NULL) memset((char *)(x), 0, sizeof(*(x))); } while(0) using namespace TDEIO; extern "C" { int TDE_EXPORT kdemain( int argc, char **argv ) { TDEInstance instance( "tdeio_sftp" ); kdDebug(TDEIO_SFTP_DB) << "*** Starting tdeio_sftp " << endl; if (argc != 4) { kdDebug(TDEIO_SFTP_DB) << "Usage: tdeio_sftp protocol domain-socket1 domain-socket2" << endl; exit(-1); } sftpProtocol slave(argv[2], argv[3]); if (getenv("DEBUG_TDEIO_SFTP")) { // Give us a coredump in the journal signal(6, SIG_DFL); } slave.dispatchLoop(); kdDebug(TDEIO_SFTP_DB) << "*** tdeio_sftp Done" << endl; return 0; } } // Some helper functions/classes namespace { // A quick and dirty scope guard implementation class ExitGuard { public: template ExitGuard(Callable && undo_func) : f(std::forward(undo_func)) {} ExitGuard(ExitGuard && other) : f(std::move(other.f)) { other.f = nullptr; } ~ExitGuard() { run(); } void run() noexcept { if(f) { f(); f = nullptr; } } void abort() { f = nullptr; } ExitGuard(const ExitGuard&) = delete; void operator= (const ExitGuard&) = delete; private: std::function f; }; // A small helper to purge passwords. Paranoiac's note: this is not enough to guarantee the // complete purge of the password and all its copy from memory (ioslaves are sending the passwords // via dcop, so it's far beyond calling it "secure" in any way), but it's still better than nothing. void purgeString(TQString &s) { s.fill('\0'); s.setLength(0); s = TQString::null; } // A helper class to cleanup password when it goes out of the scope class PasswordPurger: public ExitGuard { public: PasswordPurger(TQString &pw) : ExitGuard( [&pw](){purgeString(pw);} ) {} }; } /* namespace */ // The callback function for libssh int auth_callback(const char *prompt, char *buf, size_t len, int echo, int verify, void *userdata) { if (userdata == NULL) { return -1; } sftpProtocol *slave = (sftpProtocol *) userdata; if (slave->auth_callback(prompt, buf, len, echo, verify, userdata) < 0) { return -1; } return 0; } void log_callback(ssh_session session, int priority, const char *message, void *userdata) { if (userdata == NULL) { return; } sftpProtocol *slave = (sftpProtocol *) userdata; slave->log_callback(session, priority, message, userdata); } class PublicKeyAuth: public SSHAuthMethod { public: unsigned flag() override {return SSH_AUTH_METHOD_PUBLICKEY;}; int authenticate(sftpProtocol *ioslave) const override { return ioslave->authenticatePublicKey(); } SSHAuthMethod* clone() override {return new PublicKeyAuth; } }; class KeyboardInteractiveAuth: public SSHAuthMethod { public: KeyboardInteractiveAuth(bool noPaswordQuery = false): mNoPaswordQuery(noPaswordQuery) {} unsigned flag() override {return SSH_AUTH_METHOD_INTERACTIVE;}; int authenticate(sftpProtocol *ioslave) const override { return ioslave->authenticateKeyboardInteractive(mNoPaswordQuery); } SSHAuthMethod* clone() override {return new KeyboardInteractiveAuth(mNoPaswordQuery); } private: const bool mNoPaswordQuery; }; class PasswordAuth: public SSHAuthMethod { public: PasswordAuth(bool noPaswordQuery = false): mNoPaswordQuery(noPaswordQuery) {} unsigned flag() override {return SSH_AUTH_METHOD_PASSWORD;}; int authenticate(sftpProtocol *ioslave) const override { return ioslave->authenticatePassword(mNoPaswordQuery); } SSHAuthMethod* clone() override {return new PasswordAuth(mNoPaswordQuery); } private: const bool mNoPaswordQuery; }; TQString SSHAuthMethod::flagToStr (unsigned m) { switch (m) { case SSH_AUTH_METHOD_NONE : return TQString::fromLatin1 ( "none" ); case SSH_AUTH_METHOD_PASSWORD : return TQString::fromLatin1 ( "password" ); case SSH_AUTH_METHOD_PUBLICKEY : return TQString::fromLatin1 ( "publickey" ); case SSH_AUTH_METHOD_HOSTBASED : return TQString::fromLatin1 ( "hostbased" ); case SSH_AUTH_METHOD_INTERACTIVE : return TQString::fromLatin1 ( "keyboard-interactive" ); case SSH_AUTH_METHOD_GSSAPI_MIC : return TQString::fromLatin1 ( "gssapi-with-mic" ); default : return TQString::fromLatin1 ( "unknown" ); } } TQStringList SSHAuthMethod::bitsetToStr (unsigned m) { TQStringList rv; for (int i=0; m>>i; i++) { unsigned flag = m & (1 << i); if (flag) { rv.append(flagToStr(flag)); } } return rv; } // Public key authentication int sftpProtocol::auth_callback(const char *prompt, char *buf, size_t len, int echo, int verify, void *userdata) { // unused variables (void) echo; (void) verify; (void) userdata; (void) prompt; Q_ASSERT(len>0); kdDebug(TDEIO_SFTP_DB) << "Entering public key authentication callback" << endl; int rc=0; bool firstTimeCalled = !mPubKeyAuthData.wasCalled; mPubKeyAuthData.wasCalled = true; AuthInfo pubKeyInfo = authInfo(); pubKeyInfo.keepPassword = false; // don't save passwords for public key, // that's the task of ssh-agent. pubKeyInfo.readOnly = true; // We don't want to handle user name change when authing with a key TQString errMsg; TQString keyFile; #if LIBSSH_VERSION_INT < SSH_VERSION_INT(0, 10, 0) // no way to determine keyfile name on older libssh #else char *ssh_key_file = 0; rc = ssh_userauth_publickey_auto_get_current_identity(mSession, &ssh_key_file); if (rc == 0 && ssh_key_file && ssh_key_file[0]) { keyFile = ssh_key_file; } ssh_string_free_char(ssh_key_file); #endif bool firstTry = !mPubKeyAuthData.attemptedKeys.contains(keyFile); if (firstTry) { SlaveBase::s_seqNr = mPubKeyAuthData.current_seqNr; } else { errMsg = i18n("Incorrect or invalid passphrase.").append('\n'); } // libssh prompt is trash and we know we use this function only for publickey auth, so we'll give // the user a descent prompt if (!keyFile.isEmpty()) { pubKeyInfo.prompt = i18n("Please enter the passphrase for next public key:\n%1").arg(keyFile); } else { // Generally shouldn't happend but on older libssh pubKeyInfo.prompt = i18n("Please enter the passphrase for your public key."); } // We don't want to clobber with normal passwords in kpasswdserver's cache pubKeyInfo.realmValue = "keyfile passphrase:" + keyFile; if (openPassDlg(pubKeyInfo, errMsg)) { if (len < pubKeyInfo.password.utf8().length()+1) { kdDebug(TDEIO_SFTP_DB) << "Insufficient buffer size for password: " << len << " (" << pubKeyInfo.password.utf8().length()+1 << "needed)" << endl; } strncpy(buf, pubKeyInfo.password.utf8().data(), len-1); buf[len-1]=0; // Just to be on the safe side purgeString(pubKeyInfo.password); // take a note that we already tried unlocking this keyfile if(firstTry) { mPubKeyAuthData.attemptedKeys.append(keyFile); } // we consider publickey auth canceled only if we cancel all the key dialogs mPubKeyAuthData.wasCanceled = false; } else { kdDebug(TDEIO_SFTP_DB) << "User canceled entry of public key passphrase" << endl; rc = -1; if (firstTimeCalled) { mPubKeyAuthData.wasCanceled = true; } } return rc; } void sftpProtocol::log_callback(ssh_session session, int priority, const char *message, void *userdata) { (void) session; (void) userdata; kdDebug(TDEIO_SFTP_DB) << "[" << priority << "] " << message << endl; } int sftpProtocol::authenticatePublicKey(){ kdDebug(TDEIO_SFTP_DB) << "Trying to authenticate with public key" << endl; // First let's do some cleanup mPubKeyAuthData.attemptedKeys.clear(); mPubKeyAuthData.current_seqNr = SlaveBase::s_seqNr; mPubKeyAuthData.wasCalled = 0; mPubKeyAuthData.wasCanceled = 0; int rc; while (1) { mPubKeyAuthData.wasCalled = 0; rc = ssh_userauth_publickey_auto(mSession, nullptr, nullptr); kdDebug(TDEIO_SFTP_DB) << "ssh_userauth_publickey_auto returned rc=" << rc << " ssh_err=" << ssh_get_error_code(mSession) << " (" << ssh_get_error(mSession) << ")" << endl; if (rc == SSH_AUTH_DENIED) { if (!mPubKeyAuthData.wasCalled) { kdDebug(TDEIO_SFTP_DB) << "Passkey auth denied because it has no matching key" << endl; break; /* rc == SSH_AUTH_DENIED */ } else if (mPubKeyAuthData.wasCanceled) { kdDebug(TDEIO_SFTP_DB) << "Passkey auth denied because user canceled" << endl; rc = sftpProtocol::SSH_AUTH_CANCELED; break; } else { kdDebug(TDEIO_SFTP_DB) << "User entered wrong passphrase for the key" << endl; mPubKeyAuthData.current_seqNr = SlaveBase::s_seqNr; // Try it again } } else { // every other rc is either error or success break; } } return rc; } int sftpProtocol::authenticateKeyboardInteractive(bool noPaswordQuery) { kdDebug(TDEIO_SFTP_DB) << "Entering keyboard interactive function" << endl; int rc = SSH_AUTH_ERROR; bool retryDenied = false; // a flag to avoid infinite looping TQString cachablePassword; PasswordPurger cachePurger(cachablePassword); // Different prompts during a single pass should be queried with the same s_seqNr value long current_seqNr = SlaveBase::s_seqNr; while (1) { int n = 0; int i = 0; rc = ssh_userauth_kbdint(mSession, NULL, NULL); if (rc == SSH_AUTH_DENIED) { // do nothing kdDebug(TDEIO_SFTP_DB) << "kb-interactive auth was denied; retrying again" << endl; if (retryDenied) { // If we were denied update the s_seqNr current_seqNr = SlaveBase::s_seqNr; continue; } else { break; } } else if (rc != SSH_AUTH_INFO) { kdDebug(TDEIO_SFTP_DB) << "Finishing kb-interactive auth rc=" << rc << " ssh_err=" << ssh_get_error_code(mSession) << " (" << ssh_get_error(mSession) << ")" << endl; break; } // See "RFC4256 Section 3.3 User Interface" for meaning of the values TQString name, instruction, prompt; name = TQString::fromUtf8(ssh_userauth_kbdint_getname(mSession)); instruction = TQString::fromUtf8(ssh_userauth_kbdint_getinstruction(mSession)); n = ssh_userauth_kbdint_getnprompts(mSession); if (n>0) { // If there is at least one prompt we will want to retry auth if we fail retryDenied = true; } kdDebug(TDEIO_SFTP_DB) << "name=" << name << " instruction=" << instruction << " prompts:" << n << endl; for (i = 0; i < n; ++i) { char echo; bool isPassword=false; TQString answer; TQString errMsg; // restore the s_seqNr so it would be the same for all the prompts SlaveBase::s_seqNr = current_seqNr; prompt = TQString::fromUtf8(ssh_userauth_kbdint_getprompt(mSession, i, &echo)); kdDebug(TDEIO_SFTP_DB) << "prompt=" << prompt << " echo=" << TQString::number(echo) << endl; TDEIO::AuthInfo infoKbdInt = authInfo(); infoKbdInt.realmValue = prompt; // each prompt will be treated on its own by kpasswdserver infoKbdInt.keepPassword = false; if (!name.isEmpty()) { infoKbdInt.caption = TQString(i18n("SFTP Login") + " - " + name); } // Those strings might or might not contain some sensitive information PasswordPurger answerPurger{answer}; PasswordPurger infoPurger{infoKbdInt.password}; if (!echo) { // ssh server requests us to ask user a question without displaying an answer. In normal // circumstances this is probably a password, but it might be something else depending // on the server configuration. if (prompt.lower().startsWith("password")) { // We can assume that the ssh server asks for a password and we will handle that case // with more care since it's what most users will see isPassword = true; if (noPaswordQuery) { // if we have a cached password we might use it kdDebug(TDEIO_SFTP_DB) << "Using cached password" << endl; answer = mPassword; cachablePassword = mPassword; purgeString(mPassword); // if we used up password purge it } else { infoKbdInt.prompt = i18n("Please enter your password."); infoKbdInt.realmValue = TQString(); // passwords use generic realm infoKbdInt.keepPassword = true; if (mPasswordWasPrompted) { errMsg = i18n("Login failed: incorrect password or username.").append('\n'); } mPasswordWasPrompted = true; } } else { // If the server's request doesn't look like a password, keep the servers prompt but // don't prompt for saving the answer infoKbdInt.prompt = i18n("Please enter answer for the next request:"); if (!instruction.isEmpty()) { infoKbdInt.prompt.append("\n\n").append(instruction); } infoKbdInt.prompt.append("\n\n").append(prompt); infoKbdInt.readOnly = true; // set username readonly (enable changing it only with password) } if (answer.isNull()) { if (openPassDlg(infoKbdInt, errMsg)) { answer = infoKbdInt.password; kdDebug(TDEIO_SFTP_DB) << "Got the answer from the password dialog" << endl; if (isPassword) { TQString sshUser=sshUsername(); if (infoKbdInt.username != sshUser) { kdDebug(TDEIO_SFTP_DB) << "Username changed from " << sshUser << " to " << infoKbdInt.username << endl; mCachedUsername = infoKbdInt.username; mPassword = infoKbdInt.password; return sftpProtocol::SSH_AUTH_NEED_RECONNECT; } } } else { return sftpProtocol::SSH_AUTH_CANCELED; } } } else { // ssh server asks for some clear-text information from a user (e.g. a one-time // identification code) which should be echoed while user enters it. As for now tdeio has // no means to handle that correctly, so we will have to be creative with the password // dialog. TQString newPrompt; if (!instruction.isEmpty()) { newPrompt = instruction + "\n\n"; } newPrompt.append(prompt).append("\n\n"); newPrompt.append(i18n("Use the username input field to answer this question.")); infoKbdInt.prompt = newPrompt; infoKbdInt.url.setUser(infoKbdInt.username); infoKbdInt.username = TQString::null; infoKbdInt.readOnly = false; if (openPassDlg(infoKbdInt)) { answer = infoKbdInt.username; kdDebug(TDEIO_SFTP_DB) << "Got the answer from the password dialog: " << answer << endl; } else { return sftpProtocol::SSH_AUTH_CANCELED; } } if (ssh_userauth_kbdint_setanswer(mSession, i, answer.utf8().data()) < 0) { kdDebug(TDEIO_SFTP_DB) << "An error occurred setting the answer: " << ssh_get_error(mSession) << endl; return SSH_AUTH_ERROR; } } // for each ssh_userauth_kbdint_getprompt() } // while (1) if (!mPasswordWasPrompted && !cachablePassword.isEmpty() && (rc == SSH_AUTH_SUCCESS || rc == SSH_AUTH_PARTIAL)) { // if the password was never prompted, it was never cached, so we should cache it manually TDEIO::AuthInfo info = authInfo(); info.password = cachablePassword; info.keepPassword = false; cacheAuthentication(info); purgeString(info.password); } return rc; } int sftpProtocol::authenticatePassword(bool noPaswordQuery) { kdDebug(TDEIO_SFTP_DB) << "Trying to authenticate with password" << endl; AuthInfo info = authInfo(); info.keepPassword = true; info.prompt = i18n("Please enter your username and password."); PasswordPurger pPurger(info.password); int rc; do { TQString errMsg; if(noPaswordQuery) { // on the first try use cached password info.password = mPassword; purgeString(mPassword); } else { if (mPasswordWasPrompted) { errMsg = i18n("Login failed: incorrect password or username.").append('\n'); } mPasswordWasPrompted = true; // Handle user canceled or dialog failed to open... if (!openPassDlg(info, errMsg)) { kdDebug(TDEIO_SFTP_DB) << "User canceled password dialog" << endl; return sftpProtocol::SSH_AUTH_CANCELED; } TQString sshUser=sshUsername(); if (info.username != sshUser) { kdDebug(TDEIO_SFTP_DB) << "Username changed from " << sshUser << " to " << info.username << endl; mCachedUsername = info.username; mPassword = info.password; // libssh doc says that most servers don't permit changing the username during // authentication, so we should reinitialize the session here return sftpProtocol::SSH_AUTH_NEED_RECONNECT; } } rc = ssh_userauth_password(mSession, NULL, info.password.utf8().data()); } while (rc == SSH_AUTH_DENIED && !noPaswordQuery); if (!mPasswordWasPrompted && (rc == SSH_AUTH_SUCCESS || rc == SSH_AUTH_PARTIAL)) { // if the password was never prompted, it was never cached, so we should cache it manually info.keepPassword = false; cacheAuthentication(info); } return rc; } TQString sftpProtocol::sshUsername() { int rc; TQString rv; char *ssh_username = NULL; rc = ssh_options_get(mSession, SSH_OPTIONS_USER, &ssh_username); if (rc == 0 && ssh_username && ssh_username[0]) { rv = TQString::fromUtf8(ssh_username); } ssh_string_free_char(ssh_username); return rv; } TQString sftpProtocol::sshError(TQString errMsg) { if (ssh_get_error_code(mSession)) { errMsg.append("\n\n").append(i18n("SSH error: \"%1\" (%2)") .arg(TQString::fromUtf8(ssh_get_error(mSession))).arg(ssh_get_error_code(mSession))); } return errMsg; } TDEIO::AuthInfo sftpProtocol::authInfo() { TDEIO::AuthInfo rv; rv.url.setProtocol("sftp"); rv.url.setHost(mHost); rv.url.setPort(mPort); rv.url.setUser(mUsername); rv.caption = i18n("SFTP Login"); rv.comment = "sftp://" + mHost + ':' + TQString::number(mPort); rv.commentLabel = i18n("site:"); if(!mUsername.isEmpty()) { rv.username = mUsername; } if(!mCachedUsername.isEmpty()) { rv.username = mCachedUsername; } else if (mSession) { rv.username = sshUsername(); } // if username was specified in the address string it shouldn't be changed if (!mUsername.isEmpty()) { rv.readOnly = true; } return rv; } void sftpProtocol::reportError(const KURL &url, const int err) { kdDebug(TDEIO_SFTP_DB) << "url = " << url.url() << " - err=" << err << endl; switch (err) { case SSH_FX_OK: break; case SSH_FX_NO_SUCH_FILE: case SSH_FX_NO_SUCH_PATH: error(TDEIO::ERR_DOES_NOT_EXIST, url.prettyURL()); break; case SSH_FX_PERMISSION_DENIED: error(TDEIO::ERR_ACCESS_DENIED, url.prettyURL()); break; case SSH_FX_FILE_ALREADY_EXISTS: error(TDEIO::ERR_FILE_ALREADY_EXIST, url.prettyURL()); break; case SSH_FX_INVALID_HANDLE: error(TDEIO::ERR_MALFORMED_URL, url.prettyURL()); break; case SSH_FX_OP_UNSUPPORTED: error(TDEIO::ERR_UNSUPPORTED_ACTION, url.prettyURL()); break; case SSH_FX_BAD_MESSAGE: error(TDEIO::ERR_UNKNOWN, url.prettyURL()); break; default: error(TDEIO::ERR_INTERNAL, url.prettyURL()); break; } } bool sftpProtocol::createUDSEntry(const TQString &filename, const TQByteArray &path, UDSEntry &entry, short int details) { mode_t type; mode_t access; char *link; Q_ASSERT(entry.count() == 0); sftp_attributes sb = sftp_lstat(mSftp, path.data()); if (sb == NULL) { return false; } UDSAtom atom; atom.m_uds = UDS_NAME; atom.m_str = filename; entry.append(atom); if (sb->type == SSH_FILEXFER_TYPE_SYMLINK) { atom.m_uds = UDS_FILE_TYPE; atom.m_long = S_IFREG; entry.append(atom); link = sftp_readlink(mSftp, path.data()); if (link == NULL) { sftp_attributes_free(sb); return false; } atom.m_uds = UDS_LINK_DEST; atom.m_str = TQFile::decodeName(link); entry.append(atom); delete link; // A symlink -> follow it only if details > 1 if (details > 1) { sftp_attributes sb2 = sftp_stat(mSftp, path.data()); if (sb2 == NULL) { // It is a link pointing to nowhere type = S_IFMT - 1; access = S_IRWXU | S_IRWXG | S_IRWXO; atom.m_uds = UDS_FILE_TYPE; atom.m_long = type; entry.append(atom); atom.m_uds = UDS_ACCESS; atom.m_long = access; entry.append(atom); atom.m_uds = UDS_SIZE; atom.m_long = 0LL; entry.append(atom); goto notype; } sftp_attributes_free(sb); sb = sb2; } } switch (sb->type) { case SSH_FILEXFER_TYPE_REGULAR: atom.m_uds = UDS_FILE_TYPE; atom.m_long = S_IFREG; entry.append(atom); break; case SSH_FILEXFER_TYPE_DIRECTORY: atom.m_uds = UDS_FILE_TYPE; atom.m_long = S_IFDIR; entry.append(atom); break; case SSH_FILEXFER_TYPE_SYMLINK: atom.m_uds = UDS_FILE_TYPE; atom.m_long = S_IFLNK; entry.append(atom); break; case SSH_FILEXFER_TYPE_SPECIAL: case SSH_FILEXFER_TYPE_UNKNOWN: atom.m_uds = UDS_FILE_TYPE; atom.m_long = S_IFMT - 1; entry.append(atom); break; } access = sb->permissions & 07777; atom.m_uds = UDS_ACCESS; atom.m_long = access; entry.append(atom); atom.m_uds = UDS_SIZE; atom.m_long = sb->size; entry.append(atom); notype: if (details > 0) { if (sb->owner) { atom.m_uds = UDS_USER; atom.m_str = TQString::fromUtf8(sb->owner); entry.append(atom); } else { atom.m_uds = UDS_USER; atom.m_str = TQString::number(sb->uid); entry.append(atom); } if (sb->group) { atom.m_uds = UDS_GROUP; atom.m_str = TQString::fromUtf8(sb->group); entry.append(atom); } else { atom.m_uds = UDS_GROUP; atom.m_str = TQString::number(sb->gid); entry.append(atom); } atom.m_uds = UDS_ACCESS_TIME; atom.m_long = sb->atime; entry.append(atom); atom.m_uds = UDS_MODIFICATION_TIME; atom.m_long = sb->mtime; entry.append(atom); atom.m_uds = UDS_MODIFICATION_TIME; atom.m_long = sb->createtime; entry.append(atom); } sftp_attributes_free(sb); return true; } TQString sftpProtocol::canonicalizePath(const TQString &path) { kdDebug(TDEIO_SFTP_DB) << "Path to canonicalize: " << path << endl; TQString cPath; char *sPath = NULL; if (path.isEmpty()) { return cPath; } sPath = sftp_canonicalize_path(mSftp, path.utf8().data()); if (sPath == NULL) { kdDebug(TDEIO_SFTP_DB) << "Could not canonicalize path: " << path << endl; return cPath; } cPath = TQFile::decodeName(sPath); delete sPath; kdDebug(TDEIO_SFTP_DB) << "Canonicalized path: " << cPath << endl; return cPath; } sftpProtocol::sftpProtocol(const TQCString &pool_socket, const TQCString &app_socket) : SlaveBase("tdeio_sftp", pool_socket, app_socket), mConnected(false), mPort(-1), mSession(NULL), mSftp(NULL) { #ifndef TQ_WS_WIN kdDebug(TDEIO_SFTP_DB) << "pid = " << getpid() << endl; kdDebug(TDEIO_SFTP_DB) << "debug = " << getenv("TDEIO_SFTP_LOG_VERBOSITY") << endl; #endif mCallbacks = (ssh_callbacks) malloc(sizeof(struct ssh_callbacks_struct)); if (mCallbacks == NULL) { error(TDEIO::ERR_OUT_OF_MEMORY, i18n("Could not allocate callbacks")); return; } ZERO_STRUCTP(mCallbacks); mCallbacks->userdata = this; mCallbacks->auth_function = ::auth_callback; if (getenv("TDEIO_SFTP_LOG_VERBOSITY")) { mCallbacks->log_function = ::log_callback; } ssh_callbacks_init(mCallbacks); } sftpProtocol::~sftpProtocol() { #ifndef TQ_WS_WIN kdDebug(TDEIO_SFTP_DB) << "pid = " << getpid() << endl; #endif closeConnection(); free(mCallbacks); /* cleanup and shut down cryto stuff */ ssh_finalize(); purgeString(mPassword); } void sftpProtocol::setHost(const TQString& h, int port, const TQString& user, const TQString& pass) { kdDebug(TDEIO_SFTP_DB) << "setHost(): " << user << "@" << h << ":" << port << endl; if (mConnected) { closeConnection(); } mHost = h; if (port > 0) { mPort = port; } else { struct servent *pse; if ((pse = getservbyname("ssh", "tcp") ) == NULL) { mPort = 22; } else { mPort = ntohs(pse->s_port); } } kdDebug(TDEIO_SFTP_DB) << "setHost(): mPort=" << mPort << endl; mUsername = user; mPassword = pass; mCachedUsername = TQString::null; } int sftpProtocol::initializeConnection() { unsigned char *hash = NULL; // the server hash char *hexa; char *verbosity; int rc, state; int timeout_sec = 30, timeout_usec = 0; mSession = ssh_new(); if (mSession == NULL) { error(TDEIO::ERR_INTERNAL, i18n("Could not create a new SSH session.")); return SSH_ERROR; } kdDebug(TDEIO_SFTP_DB) << "Creating the SSH session and setting options" << endl; // Set timeout rc = ssh_options_set(mSession, SSH_OPTIONS_TIMEOUT, &timeout_sec); if (rc < 0) { kdDebug(TDEIO_SFTP_DB) << "Could not set a timeout."; } rc = ssh_options_set(mSession, SSH_OPTIONS_TIMEOUT_USEC, &timeout_usec); if (rc < 0) { kdDebug(TDEIO_SFTP_DB) << "Could not set a timeout in usec."; } // Don't use any compression rc = ssh_options_set(mSession, SSH_OPTIONS_COMPRESSION_C_S, "none"); if (rc < 0) { kdDebug(TDEIO_SFTP_DB) << "Could not set compression client <- server."; } rc = ssh_options_set(mSession, SSH_OPTIONS_COMPRESSION_S_C, "none"); if (rc < 0) { kdDebug(TDEIO_SFTP_DB) << "Could not set compression server -> client."; } // Set host and port rc = ssh_options_set(mSession, SSH_OPTIONS_HOST, mHost.utf8().data()); if (rc < 0) { error(TDEIO::ERR_OUT_OF_MEMORY, i18n("Could not set host.")); return SSH_ERROR; } if (mPort > 0) { rc = ssh_options_set(mSession, SSH_OPTIONS_PORT, &mPort); if (rc < 0) { error(TDEIO::ERR_OUT_OF_MEMORY, i18n("Could not set port.")); return SSH_ERROR; } } // Set the username if (!mCachedUsername.isEmpty() || !mUsername.isEmpty()) { TQString username = !mCachedUsername.isEmpty() ? mCachedUsername : mUsername; rc = ssh_options_set(mSession, SSH_OPTIONS_USER, username.utf8().data()); if (rc < 0) { error(TDEIO::ERR_OUT_OF_MEMORY, i18n("Could not set username.")); return rc; } } verbosity = getenv("TDEIO_SFTP_LOG_VERBOSITY"); if (verbosity) { rc = ssh_options_set(mSession, SSH_OPTIONS_LOG_VERBOSITY_STR, verbosity); if (rc < 0) { error(TDEIO::ERR_OUT_OF_MEMORY, i18n("Could not set log verbosity.")); return rc; } } // Read ~/.ssh/config rc = ssh_options_parse_config(mSession, NULL); if (rc < 0) { error(TDEIO::ERR_INTERNAL, i18n("Could not parse the config file.")); return rc; } ssh_set_callbacks(mSession, mCallbacks); kdDebug(TDEIO_SFTP_DB) << "Trying to connect to the SSH server" << endl; /* try to connect */ rc = ssh_connect(mSession); if (rc < 0) { error(TDEIO::ERR_COULD_NOT_CONNECT, sshError()); return rc; } kdDebug(TDEIO_SFTP_DB) << "Getting the SSH server hash" << endl; /* get the hash */ ssh_key serverKey; #if LIBSSH_VERSION_INT < SSH_VERSION_INT(0, 7, 90) rc = ssh_get_publickey(mSession, &serverKey); #else rc = ssh_get_server_publickey(mSession, &serverKey); #endif if (rc<0) { error(TDEIO::ERR_COULD_NOT_CONNECT, sshError()); return rc; } size_t hlen; #if LIBSSH_VERSION_INT < SSH_VERSION_INT(0, 8, 90) rc = ssh_get_publickey_hash(serverKey, SSH_PUBLICKEY_HASH_MD5, &hash, &hlen); #else rc = ssh_get_publickey_hash(serverKey, SSH_PUBLICKEY_HASH_SHA256, &hash, &hlen); #endif if (rc<0) { error(TDEIO::ERR_COULD_NOT_CONNECT, sshError()); return rc; } kdDebug(TDEIO_SFTP_DB) << "Checking if the SSH server is known" << endl; /* check the server public key hash */ #if LIBSSH_VERSION_INT < SSH_VERSION_INT(0, 7, 90) state = ssh_is_server_known(mSession); #else state = ssh_session_is_known_server(mSession); #endif switch (state) { case TDEIO_SSH_KNOWN_HOSTS_OK: break; case TDEIO_SSH_KNOWN_HOSTS_OTHER: delete hash; error(TDEIO::ERR_CONNECTION_BROKEN, i18n("The host key for this server was " "not found, but another type of key exists.\n" "An attacker might change the default server key to confuse your " "client into thinking the key does not exist.\n" "Please contact your system administrator.\n%1").arg(TQString::fromUtf8(ssh_get_error(mSession)))); return SSH_ERROR; case TDEIO_SSH_KNOWN_HOSTS_CHANGED: hexa = ssh_get_hexa(hash, hlen); delete hash; /* TODO print known_hosts file, port? */ error(TDEIO::ERR_CONNECTION_BROKEN, i18n("The host key for the server %1 has changed.\n" "This could either mean that DNS SPOOFING is happening or the IP " "address for the host and its host key have changed at the same time.\n" "The fingerprint for the key sent by the remote host is:\n %2\n" "Please contact your system administrator.\n%3").arg( mHost).arg(TQString::fromUtf8(hexa)).arg(TQString::fromUtf8(ssh_get_error(mSession)))); delete hexa; return SSH_ERROR; case TDEIO_SSH_KNOWN_HOSTS_NOT_FOUND: case TDEIO_SSH_KNOWN_HOSTS_UNKNOWN: { TQString msg; // msg for dialog box TQString caption; // dialog box caption hexa = ssh_get_hexa(hash, hlen); delete hash; caption = i18n("Warning: Cannot verify host's identity."); msg = i18n("The authenticity of host %1 cannot be established.\n" "The key fingerprint is: %2\n" "Are you sure you want to continue connecting?").arg(mHost).arg(hexa); delete hexa; if (KMessageBox::Yes != messageBox(WarningYesNo, msg, caption)) { error(TDEIO::ERR_USER_CANCELED, TQString()); return SSH_ERROR; } /* write the known_hosts file */ kdDebug(TDEIO_SFTP_DB) << "Adding server to known_hosts file." << endl; #if LIBSSH_VERSION_INT < SSH_VERSION_INT(0, 7, 90) if (ssh_write_knownhost(mSession) != SSH_OK) { #else if (ssh_session_update_known_hosts(mSession) != SSH_OK) { #endif error(TDEIO::ERR_USER_CANCELED, sshError()); return SSH_ERROR; } break; } case TDEIO_SSH_KNOWN_HOSTS_ERROR: delete hash; error(TDEIO::ERR_COULD_NOT_CONNECT, sshError()); return SSH_ERROR; } kdDebug(TDEIO_SFTP_DB) << "Trying to authenticate with the server" << endl; return SSH_OK; } void sftpProtocol::openConnection() { if (mConnected) { return; } kdDebug(TDEIO_SFTP_DB) << "username=" << mUsername << ", host=" << mHost << ", port=" << mPort << endl; infoMessage(i18n("Opening SFTP connection to host %1:%2").arg(mHost).arg(mPort)); if (mHost.isEmpty()) { kdDebug(TDEIO_SFTP_DB) << "openConnection(): Need hostname..." << endl; error(TDEIO::ERR_UNKNOWN_HOST, i18n("No hostname specified.")); return; } // Check for cached authentication info if no password is specified... if (mPassword.isEmpty()) { AuthInfo info = authInfo(); kdDebug(TDEIO_SFTP_DB) << "checking cache: info.username = " << info.username << ", info.url = " << info.url.prettyURL() << endl; if (checkCachedAuthentication(info)) { kdDebug() << "using cached" << endl; mCachedUsername = info.username; mPassword = info.password; purgeString(info.password); //< not really necessary because of Qt's implicit data sharing } } mPasswordWasPrompted = false; PasswordPurger pwPurger{mPassword}; int rc; ExitGuard connectionCloser([this](){ closeConnection(); }); do { // A loop to restart connection when needed // Start the ssh connection. if (initializeConnection() < 0) { return; } // Try to authenticate (this required before calling ssh_auth_list()) rc = ssh_userauth_none(mSession, NULL); if (rc == SSH_AUTH_ERROR) { error(TDEIO::ERR_COULD_NOT_LOGIN, sshError(i18n("Authentication failed (method: %1).") .arg(i18n("none")))); return; } // Preinit the list of supported auth methods static const auto authMethodsNormal = [](){ std::vector> rv; rv.emplace_back(std::unique_ptr(new PublicKeyAuth)); rv.emplace_back(std::unique_ptr(new KeyboardInteractiveAuth)); rv.emplace_back(std::unique_ptr(new PasswordAuth)); return rv; }(); const static int supportedMethods = std::accumulate( authMethodsNormal.begin(), authMethodsNormal.end(), SSH_AUTH_METHOD_NONE, //< none is supported by default [](int acc, const std::unique_ptr &m){ return acc |= m->flag(); }); unsigned attemptedMethods = 0; // Backup of the value of the SlaveBase::s_seqNr. This is used to query different data values // with openPassDlg() with the same seqNr. Otherwise it will result in the prompting of the pass // dialog to the user in cases the values should be recovered from the cache. // This is a bit hacky but necessary long current_seqNr = SlaveBase::s_seqNr; while (rc != SSH_AUTH_SUCCESS) { // Note this loop can rerun in case of multistage ssh authentication e.g. "password,publickey" // which will require user to provide a valid password at first and then a valid public key. // see AuthenticationMethods in man 5 sshd_config for more info bool wasCanceled = false; unsigned availableMethodes = ssh_auth_list(mSession); SlaveBase::s_seqNr = current_seqNr; if (!availableMethodes) { // Technically libssh docs suggest that the server merely MAY send auth methods, but it's // highly unclear what we should do in such case and it looks like openssh doesn't have an // option for that, so let's just consider this server a jerk and don't talk to him anymore. error(TDEIO::ERR_COULD_NOT_LOGIN, i18n("Authentication failed.\n" "The server did not send any authentication methods!")); return; } else if (!(availableMethodes & supportedMethods)) { error(TDEIO::ERR_COULD_NOT_LOGIN, i18n("Authentication failed.\n" "The server sent only unsupported authentication methods (%1)!") .arg(SSHAuthMethod::bitsetToStr(availableMethodes).join(", "))); return; } const auto *authMethods = &authMethodsNormal; // If we have cached password we want try to use it before public key if(!mPassword.isEmpty()) { static const auto authMethodsWithPassword = []() { std::vector> rv; rv.emplace_back(std::unique_ptr( new KeyboardInteractiveAuth(/* noPasswordQuery = */true) ) ); rv.emplace_back(std::unique_ptr( new PasswordAuth(/* noPasswordQuery = */true) ) ); for (const auto &m: authMethodsNormal) { rv.emplace_back(m->clone()); } return rv; }(); authMethods = &authMethodsWithPassword; } // Actually iterate over the list of methods and try them out for (const auto &method: *authMethods) { if (!(availableMethodes & method->flag())) { continue; } rc = method->authenticate( this ); attemptedMethods |= method->flag(); if (rc == SSH_AUTH_SUCCESS || rc == SSH_AUTH_PARTIAL) { kdDebug(TDEIO_SFTP_DB) << "method=" << method->name() << ": auth " << (rc == SSH_AUTH_SUCCESS ? "success" : "partial") << endl; break; // either next auth method or continue on with the connect } else if (rc == SSH_AUTH_ERROR || rc == SSH_AUTH_AGAIN) { TQString errMsg = i18n("Authentication failed (method: %1).").arg(method->name()); // SSH_AUTH_AGAIN returned in case of some errors when server hangs up unexpectedly like // in case there were too many failed authentication attempts if (rc == SSH_AUTH_AGAIN) { errMsg.append("\n").append(i18n("Server is slow to respond or hung up unexpectedly.")); } error(TDEIO::ERR_COULD_NOT_LOGIN, sshError(errMsg)); return; } else if (rc == SSH_AUTH_CANCELED) { kdDebug(TDEIO_SFTP_DB) << "method=" << method->name() << " was canceled by user" << endl; // don't quit immediately due to that the user might have canceled one method to use another wasCanceled = true; } else if (rc == SSH_AUTH_NEED_RECONNECT) { kdDebug(TDEIO_SFTP_DB) << "method=" << method->name() << " requested reconnection" << endl; break; } else if (rc == SSH_AUTH_DENIED) { kdDebug(TDEIO_SFTP_DB) << "Auth for method=" << method->name() << " was denied" << endl; // do nothing, just proceed with next auth method } else { // Shouldn't happen, but to be on the safe side better handle it error(TDEIO::ERR_UNKNOWN, sshError(i18n("Authentication failed unexpectedly"))); return; } } // At this point rc values should be one of: // SSH_AUTH_SUCCESS, SSH_AUTH_PARTIAL, SSH_AUTH_DENIED, SSH_AUTH_CANCELED or SSH_AUTH_NEED_RECONNECT if(rc == SSH_AUTH_NEED_RECONNECT) { closeConnection(); //< have to do it manually break; } else if (wasCanceled && (rc == SSH_AUTH_CANCELED || rc == SSH_AUTH_DENIED)) { error(TDEIO::ERR_USER_CANCELED, TQString::null); return; } else if (rc != SSH_AUTH_SUCCESS && rc != SSH_AUTH_PARTIAL) { TQString errMsg = i18n("Authentication denied (attempted methods: %1).") .arg(SSHAuthMethod::bitsetToStr(attemptedMethods).join(", ")); if (availableMethodes & ~supportedMethods) { errMsg.append("\n") .append(i18n("Note: server also declares some unsupported authentication methods (%1)") .arg(SSHAuthMethod::bitsetToStr(availableMethodes & ~supportedMethods).join(", "))); } error(TDEIO::ERR_COULD_NOT_LOGIN, errMsg); return; } } // while (rc != SSH_AUTH_SUCCESS) } while(rc == SSH_AUTH_NEED_RECONNECT); // start sftp session kdDebug(TDEIO_SFTP_DB) << "Trying to request the sftp session" << endl; mSftp = sftp_new(mSession); if (mSftp == NULL) { error(TDEIO::ERR_COULD_NOT_LOGIN, i18n("Unable to request the SFTP subsystem. " "Make sure SFTP is enabled on the server.")); return; } kdDebug(TDEIO_SFTP_DB) << "Trying to initialize the sftp session" << endl; if (sftp_init(mSftp) < 0) { error(TDEIO::ERR_COULD_NOT_LOGIN, i18n("Could not initialize the SFTP session.")); return; } // Login succeeded! infoMessage(i18n("Successfully connected to %1").arg(mHost)); //setTimeoutSpecialCommand(TDEIO_SFTP_SPECIAL_TIMEOUT); mConnected = true; connectionCloser.abort(); connected(); return; } void sftpProtocol::closeConnection() { kdDebug(TDEIO_SFTP_DB) << "closeConnection()" << endl; sftp_free(mSftp); mSftp = NULL; ssh_disconnect(mSession); mSession = NULL; mConnected = false; } #if 0 void sftpProtocol::special(const TQByteArray &data) { int rc; kdDebug(TDEIO_SFTP_DB) << "special(): polling"; /* * channel_poll() returns the number of bytes that may be read on the * channel. It does so by checking the input buffer and eventually the * network socket for data to read. If the input buffer is not empty, it * will not probe the network (and such not read packets nor reply to * keepalives). * * As channel_poll can act on two specific buffers (a channel has two * different stream: stdio and stderr), polling for data on the stderr * stream has more chance of not being in the problematic case (data left * in the buffer). Checking the return value (for >0) would be a good idea * to debug the problem. */ rc = channel_poll(mSftp->channel, 0); if (rc > 0) { rc = channel_poll(mSftp->channel, 1); } if (rc < 0) { kdDebug(TDEIO_SFTP_DB) << "channel_poll failed: " << ssh_get_error(mSession) << endl; } setTimeoutSpecialCommand(TDEIO_SFTP_SPECIAL_TIMEOUT); } #endif void sftpProtocol::statMime(const KURL &url) { kdDebug(TDEIO_SFTP_DB) << "stat: " << url.url() << endl; openConnection(); if (!mConnected) { error(TDEIO::ERR_CONNECTION_BROKEN, url.prettyURL()); return; } const TQString path = url.path(); const TQByteArray path_c = path.utf8(); sftp_attributes sb = sftp_lstat(mSftp, path_c.data()); if (sb == NULL) { reportError(url, sftp_get_error(mSftp)); return; } switch (sb->type) { case SSH_FILEXFER_TYPE_DIRECTORY: sftp_attributes_free(sb); emit mimeType("inode/directory"); return; case SSH_FILEXFER_TYPE_SPECIAL: case SSH_FILEXFER_TYPE_UNKNOWN: error(TDEIO::ERR_CANNOT_OPEN_FOR_READING, url.prettyURL()); sftp_attributes_free(sb); return; case SSH_FILEXFER_TYPE_SYMLINK: case SSH_FILEXFER_TYPE_REGULAR: break; } size_t fileSize = sb->size; sftp_attributes_free(sb); int flags = 0; flags = O_RDONLY; mOpenFile = sftp_open(mSftp, path_c.data(), flags, 0); if (mOpenFile == NULL) { error(TDEIO::ERR_CANNOT_OPEN_FOR_READING, path); return; } // Determine the mimetype of the file to be retrieved, and emit it. // This is mandatory in all slaves (for KRun/BrowserRun to work). // If we're not opening the file ReadOnly or ReadWrite, don't attempt to // read the file and send the mimetype. size_t bytesRequested = 1024; ssize_t bytesRead = 0; TQByteArray buffer(bytesRequested); bytesRead = sftp_read(mOpenFile, buffer.data(), bytesRequested); if (bytesRead < 0) { error(TDEIO::ERR_COULD_NOT_READ, mOpenUrl.prettyURL()); closeFile(); return; } else { TQByteArray fileData; fileData.setRawData(buffer.data(), bytesRead); KMimeMagicResult *p_mimeType = KMimeMagic::self()->findBufferFileType(fileData, mOpenUrl.fileName()); emit mimeType(p_mimeType->mimeType()); } sftp_close(mOpenFile); mOpenFile = NULL; } #if 0 void sftpProtocol::read(TDEIO::filesize_t bytes) { kdDebug(TDEIO_SFTP_DB) << "read, offset = " << openOffset << ", bytes = " << bytes; Q_ASSERT(mOpenFile != NULL); TQVarLengthArray buffer(bytes); ssize_t bytesRead = sftp_read(mOpenFile, buffer.data(), bytes); Q_ASSERT(bytesRead <= static_cast(bytes)); if (bytesRead < 0) { kdDebug(TDEIO_SFTP_DB) << "Could not read " << mOpenUrl; error(TDEIO::ERR_COULD_NOT_READ, mOpenUrl.prettyURL()); close(); return; } TQByteArray fileData = TQByteArray::fromRawData(buffer.data(), bytesRead); data(fileData); } void sftpProtocol::write(const TQByteArray &data) { kdDebug(TDEIO_SFTP_DB) << "write, offset = " << openOffset << ", bytes = " << data.size(); Q_ASSERT(mOpenFile != NULL); ssize_t bytesWritten = sftp_write(mOpenFile, data.data(), data.size()); if (bytesWritten < 0) { kdDebug(TDEIO_SFTP_DB) << "Could not write to " << mOpenUrl; error(TDEIO::ERR_COULD_NOT_WRITE, mOpenUrl.prettyURL()); close(); return; } written(bytesWritten); } void sftpProtocol::seek(TDEIO::filesize_t offset) { kdDebug(TDEIO_SFTP_DB) << "seek, offset = " << offset; Q_ASSERT(mOpenFile != NULL); if (sftp_seek64(mOpenFile, static_cast(offset)) < 0) { error(TDEIO::ERR_COULD_NOT_SEEK, mOpenUrl.path()); close(); } position(sftp_tell64(mOpenFile)); } #endif void sftpProtocol::closeFile() { if (mOpenFile) { sftp_close(mOpenFile); mOpenFile = NULL; finished(); } } void sftpProtocol::get(const KURL& url) { kdDebug(TDEIO_SFTP_DB) << "get(): " << url.url() << endl; openConnection(); if (!mConnected) { return; } TQByteArray path = url.path().utf8(); char buf[MAX_XFER_BUF_SIZE] = {0}; sftp_file file = NULL; ssize_t bytesread = 0; // time_t curtime = 0; time_t lasttime = 0; time_t starttime = 0; ssize_t totalbytesread = 0; sftp_attributes sb = sftp_lstat(mSftp, path.data()); if (sb == NULL) { reportError(url, sftp_get_error(mSftp)); return; } switch (sb->type) { case SSH_FILEXFER_TYPE_DIRECTORY: error(TDEIO::ERR_IS_DIRECTORY, url.prettyURL()); sftp_attributes_free(sb); return; case SSH_FILEXFER_TYPE_SPECIAL: case SSH_FILEXFER_TYPE_UNKNOWN: error(TDEIO::ERR_CANNOT_OPEN_FOR_READING, url.prettyURL()); sftp_attributes_free(sb); return; case SSH_FILEXFER_TYPE_SYMLINK: case SSH_FILEXFER_TYPE_REGULAR: break; } // Open file file = sftp_open(mSftp, path.data(), O_RDONLY, 0); if (file == NULL) { error( TDEIO::ERR_CANNOT_OPEN_FOR_READING, url.prettyURL()); sftp_attributes_free(sb); return; } // Determine the mimetype of the file to be retrieved, and emit it. // This is mandatory in all slaves (for KRun/BrowserRun to work) // In real "remote" slaves, this is usually done using findByNameAndContent // after receiving some data. But we don't know how much data the mimemagic rules // need, so for local files, better use findByUrl with localUrl=true. KMimeType::Ptr mt = KMimeType::findByURL( url, sb->permissions, false /* remote URL */ ); emit mimeType( mt->name() ); // FIXME test me kdDebug(TDEIO_SFTP_DB) << "Total size: " << TQString::number(sb->size) << endl; // Set the total size totalSize(sb->size); const TQString resumeOffset = metaData(TQString("resume")); if (!resumeOffset.isEmpty()) { bool ok; ssize_t offset = resumeOffset.toLong(&ok); if (ok && (offset > 0) && ((unsigned long long) offset < sb->size)) { if (sftp_seek64(file, offset) == 0) { canResume(); totalbytesread = offset; kdDebug(TDEIO_SFTP_DB) << "Resume offset: " << TQString::number(offset) << endl; } } } if (file != NULL) { bool isFirstPacket = true; lasttime = starttime = time(NULL); for (;;) { bytesread = sftp_read(file, buf, MAX_XFER_BUF_SIZE); kdDebug(TDEIO_SFTP_DB) << "bytesread=" << TQString::number(bytesread) << endl; if (bytesread == 0) { // All done reading break; } else if (bytesread < 0) { kdDebug(TDEIO_SFTP_DB) << "Failed to read"; error(TDEIO::ERR_COULD_NOT_READ, url.prettyURL()); sftp_attributes_free(sb); return; } TQByteArray filedata; filedata.setRawData(buf, bytesread); if (isFirstPacket) { KMimeMagicResult *p_mimeType = KMimeMagic::self()->findBufferFileType(filedata, mOpenUrl.fileName()); mimeType(p_mimeType->mimeType()); kdDebug(TDEIO_SFTP_DB) << "mimetype=" << p_mimeType->mimeType() << endl; isFirstPacket = false; } data(filedata); filedata.resetRawData(buf, bytesread); // increment total bytes read totalbytesread += bytesread; processedSize(totalbytesread); } kdDebug(TDEIO_SFTP_DB) << "size processed=" << totalbytesread << endl; sftp_close(file); //data(TQByteArray()); processedSize((sb->size)); } sftp_attributes_free(sb); finished(); } void sftpProtocol::put(const KURL& url, int permissions, bool overwrite, bool resume) { kdDebug(TDEIO_SFTP_DB) << "put(): " << url.url() << " , permissions = " << TQString::number(permissions) << ", overwrite = " << overwrite << ", resume = " << resume << endl; openConnection(); if (!mConnected) { return; } const TQString dest_orig = url.path(); const TQByteArray dest_orig_c = dest_orig.utf8(); const TQString dest_part = dest_orig + ".part"; const TQByteArray dest_part_c = dest_part.utf8(); uid_t owner = 0; gid_t group = 0; sftp_attributes sb = sftp_lstat(mSftp, dest_orig_c.data()); const bool bOrigExists = (sb != NULL); bool bPartExists = false; const bool bMarkPartial = config()->readEntry("MarkPartial", "true") == "true"; // Don't change permissions of the original file if (bOrigExists) { permissions = sb->permissions; owner = sb->uid; group = sb->gid; } if (bMarkPartial) { sftp_attributes sbPart = sftp_lstat(mSftp, dest_part_c.data()); bPartExists = (sbPart != NULL); if (bPartExists && !resume && !overwrite && sbPart->size > 0 && sbPart->type == SSH_FILEXFER_TYPE_REGULAR) { kdDebug(TDEIO_SFTP_DB) << "put : calling canResume with " << TQString::number(sbPart->size) << endl; // Maybe we can use this partial file for resuming // Tell about the size we have, and the app will tell us // if it's ok to resume or not. if (canResume(sbPart->size)) { resume = true; } kdDebug(TDEIO_SFTP_DB) << "put got answer " << resume << endl; delete sbPart; } } if (bOrigExists && !(overwrite) && !(resume)) { if (sb->type == SSH_FILEXFER_TYPE_DIRECTORY) { error(TDEIO::ERR_DIR_ALREADY_EXIST, dest_orig); } else { error(TDEIO::ERR_FILE_ALREADY_EXIST, dest_orig); } sftp_attributes_free(sb); return; } int result; TQByteArray dest; sftp_file file = NULL; // Loop until we got 0 (end of data) do { TQByteArray buffer; dataReq(); // Request for data result = readData(buffer); if (result >= 0 && buffer.size()) { kdDebug(TDEIO_SFTP_DB) << TQString("Got %1 bytes of data").arg(buffer.size()) << endl; if (dest.isEmpty()) { if (bMarkPartial) { kdDebug(TDEIO_SFTP_DB) << "Appending .part extension to " << dest_orig << endl; dest = dest_part_c; if (bPartExists && !(resume)) { kdDebug(TDEIO_SFTP_DB) << "Deleting partial file " << dest_part << endl; sftp_unlink(mSftp, dest_part_c.data()); // Catch errors when we try to open the file. } } else { dest = dest_orig_c; if (bOrigExists && !(resume)) { kdDebug(TDEIO_SFTP_DB) << "Deleting destination file " << dest_orig << endl; sftp_unlink(mSftp, dest_orig_c.data()); // Catch errors when we try to open the file. } } // bMarkPartial if ((resume)) { sftp_attributes fstat; kdDebug(TDEIO_SFTP_DB) << "Trying to append: " << dest.data() << endl; file = sftp_open(mSftp, dest.data(), O_RDWR, 0); // append if resuming if (file) { fstat = sftp_fstat(file); if (fstat) { sftp_seek64(file, fstat->size); // Seek to end TODO sftp_attributes_free(fstat); } } } else { mode_t initialMode; if (permissions != -1) { initialMode = permissions | S_IWUSR | S_IRUSR; } else { initialMode = 0644; } kdDebug(TDEIO_SFTP_DB) << "Trying to open: " << dest.data() << ", mode=" << TQString::number(initialMode) << endl; file = sftp_open(mSftp, dest.data(), O_CREAT | O_TRUNC | O_WRONLY, initialMode); } // resume if (file == NULL) { kdDebug(TDEIO_SFTP_DB) << "COULD NOT WRITE " << dest.data() << " permissions=" << permissions << " error=" << ssh_get_error(mSession) << endl; if (sftp_get_error(mSftp) == SSH_FX_PERMISSION_DENIED) { error(TDEIO::ERR_WRITE_ACCESS_DENIED, TQString::fromUtf8(dest)); } else { error(TDEIO::ERR_CANNOT_OPEN_FOR_WRITING, TQString::fromUtf8(dest)); } sftp_attributes_free(sb); finished(); return; } // file } // dest.isEmpty ssize_t bytesWritten = sftp_write(file, buffer.data(), buffer.size()); kdDebug(TDEIO_SFTP_DB) << TQString("Written %1 bytes").arg(bytesWritten) << endl; if (bytesWritten < 0) { error(TDEIO::ERR_COULD_NOT_WRITE, dest_orig); result = -1; } } // result } while (result > 0); sftp_attributes_free(sb); // An error occurred deal with it. if (result < 0) { kdDebug(TDEIO_SFTP_DB) << "Error during 'put'. Aborting." << endl; if (file != NULL) { sftp_close(file); sftp_attributes attr = sftp_stat(mSftp, dest.data()); if (bMarkPartial && attr != NULL) { size_t size = config()->readLongNumEntry("MinimumKeepSize", DEFAULT_MINIMUM_KEEP_SIZE); if (attr->size < size) { sftp_unlink(mSftp, dest.data()); } } delete attr; sftp_attributes_free(attr); } //::exit(255); finished(); return; } if (file == NULL) { // we got nothing to write out, so we never opened the file finished(); return; } if (sftp_close(file) < 0) { kdWarning(TDEIO_SFTP_DB) << "Error when closing file descriptor" << endl; error(TDEIO::ERR_COULD_NOT_WRITE, dest_orig); return; } // after full download rename the file back to original name if (bMarkPartial) { // If the original URL is a symlink and we were asked to overwrite it, // remove the symlink first. This ensures that we do not overwrite the // current source if the symlink points to it. if ((overwrite)) { sftp_unlink(mSftp, dest_orig_c.data()); } if (sftp_rename(mSftp, dest.data(), dest_orig_c.data()) < 0) { kdWarning(TDEIO_SFTP_DB) << " Couldn't rename " << dest.data() << " to " << dest_orig << endl; error(TDEIO::ERR_CANNOT_RENAME_PARTIAL, dest_orig); return; } } // set final permissions if (permissions != -1 && !(resume)) { kdDebug(TDEIO_SFTP_DB) << "Trying to set final permissions of " << dest_orig << " to " << TQString::number(permissions) << endl; if (sftp_chmod(mSftp, dest_orig_c.data(), permissions) < 0) { warning(i18n( "Could not change permissions for\n%1").arg(dest_orig)); } } // set original owner and group if (bOrigExists) { kdDebug(TDEIO_SFTP_DB) << "Trying to restore original owner and group of " << dest_orig << endl; if (sftp_chown(mSftp, dest_orig_c.data(), owner, group) < 0) { // warning(i18n( "Could not change owner and group for\n%1", dest_orig)); } } // set modification time #if 0 const TQString mtimeStr = metaData("modified"); if (!mtimeStr.isEmpty()) { TQDateTime dt = TQDateTime::fromString(mtimeStr, TQt::ISODate); if (dt.isValid()) { struct timeval times[2]; sftp_attributes attr = sftp_lstat(mSftp, dest_orig_c.data()); if (attr != NULL) { times[0].tv_sec = attr->atime; //// access time, unchanged times[1].tv_sec = dt.toTime_t(); // modification time times[0].tv_usec = times[1].tv_usec = 0; sftp_utimes(mSftp, dest_orig_c.data(), times); sftp_attributes_free(attr); } } } #endif // We have done our job => finish finished(); } void sftpProtocol::copy(const KURL &src, const KURL &dest, int permissions, bool overwrite) { kdDebug(TDEIO_SFTP_DB) << src.url() << " -> " << dest.url() << " , permissions = " << TQString::number(permissions) << ", overwrite = " << overwrite << endl; error(TDEIO::ERR_UNSUPPORTED_ACTION, TQString()); } void sftpProtocol::stat(const KURL& url) { kdDebug(TDEIO_SFTP_DB) << url.url() << endl; openConnection(); if (!mConnected) { return; } if (! url.hasPath() || TQDir::isRelativePath(url.path()) || url.path().contains("/./") || url.path().contains("/../")) { TQString cPath; if (url.hasPath()) { cPath = canonicalizePath(url.path()); } else { cPath = canonicalizePath(TQString(".")); } if (cPath.isEmpty()) { error(TDEIO::ERR_MALFORMED_URL, url.prettyURL()); return; } KURL redir(url); redir.setPath(cPath); redirection(redir); kdDebug(TDEIO_SFTP_DB) << "redirecting to " << redir.url() << endl; finished(); return; } TQByteArray path = url.path().utf8(); const TQString sDetails = metaData(TQString("details")); const int details = sDetails.isEmpty() ? 2 : sDetails.toInt(); UDSEntry entry; entry.clear(); if (!createUDSEntry(url.fileName(), path, entry, details)) { error(TDEIO::ERR_DOES_NOT_EXIST, url.prettyURL()); return; } statEntry(entry); finished(); } void sftpProtocol::mimetype(const KURL& url){ kdDebug(TDEIO_SFTP_DB) << url.url() << endl; openConnection(); if (!mConnected) { return; } // stat() feeds the mimetype statMime(url); closeFile(); finished(); } void sftpProtocol::listDir(const KURL& url) { kdDebug(TDEIO_SFTP_DB) << "list directory: " << url.url() << endl; openConnection(); if (!mConnected) { return; } if (! url.hasPath() || TQDir::isRelativePath(url.path()) || url.path().contains("/./") || url.path().contains("/../")) { TQString cPath; if (url.hasPath()) { cPath = canonicalizePath(url.path()); } else { cPath = canonicalizePath(TQString(".")); } if (cPath.isEmpty()) { error(TDEIO::ERR_MALFORMED_URL, url.prettyURL()); return; } KURL redir(url); redir.setPath(cPath); redirection(redir); kdDebug(TDEIO_SFTP_DB) << "redirecting to " << redir.url() << endl; finished(); return; } TQByteArray path = url.path().utf8(); sftp_dir dp = sftp_opendir(mSftp, path.data()); if (dp == NULL) { reportError(url, sftp_get_error(mSftp)); return; } sftp_attributes dirent = NULL; const TQString sDetails = metaData(TQString("details")); const int details = sDetails.isEmpty() ? 2 : sDetails.toInt(); TQValueList entryNames; UDSEntry entry; kdDebug(TDEIO_SFTP_DB) << "readdir: " << path.data() << ", details: " << TQString::number(details) << endl; UDSAtom atom; for (;;) { mode_t access; mode_t type; char *link; dirent = sftp_readdir(mSftp, dp); if (dirent == NULL) { break; } entry.clear(); atom.m_uds = UDS_NAME; atom.m_str = TQFile::decodeName(dirent->name); entry.append(atom); if (dirent->type == SSH_FILEXFER_TYPE_SYMLINK) { TQCString file = (TQString::fromUtf8(path) + "/" + TQFile::decodeName(dirent->name)).utf8().data(); atom.m_uds = UDS_FILE_TYPE; atom.m_long = S_IFREG; entry.append(atom); link = sftp_readlink(mSftp, file.data()); if (link == NULL) { sftp_attributes_free(dirent); error(TDEIO::ERR_INTERNAL, i18n("Could not read link: %1").arg(TQString::fromUtf8(file))); return; } atom.m_uds = UDS_LINK_DEST; atom.m_str = TQFile::decodeName(link); entry.append(atom); delete link; // A symlink -> follow it only if details > 1 if (details > 1) { sftp_attributes sb = sftp_stat(mSftp, file.data()); if (sb == NULL) { // It is a link pointing to nowhere type = S_IFMT - 1; access = S_IRWXU | S_IRWXG | S_IRWXO; atom.m_uds = UDS_FILE_TYPE; atom.m_long = type; entry.append(atom); atom.m_uds = UDS_ACCESS; atom.m_long = access; entry.append(atom); atom.m_uds = UDS_SIZE; atom.m_long = 0; entry.append(atom); goto notype; } sftp_attributes_free(dirent); dirent = sb; } } switch (dirent->type) { case SSH_FILEXFER_TYPE_REGULAR: atom.m_uds = UDS_FILE_TYPE; atom.m_long = S_IFREG; entry.append(atom); break; case SSH_FILEXFER_TYPE_DIRECTORY: atom.m_uds = UDS_FILE_TYPE; atom.m_long = S_IFDIR; entry.append(atom); break; case SSH_FILEXFER_TYPE_SYMLINK: atom.m_uds = UDS_FILE_TYPE; atom.m_long = S_IFLNK; entry.append(atom); break; case SSH_FILEXFER_TYPE_SPECIAL: case SSH_FILEXFER_TYPE_UNKNOWN: break; } access = dirent->permissions & 07777; atom.m_uds = UDS_ACCESS; atom.m_long = access; entry.append(atom); atom.m_uds = UDS_SIZE; atom.m_long = dirent->size; entry.append(atom); notype: if (details > 0) { atom.m_uds = UDS_USER; if (dirent->owner) { atom.m_str = TQString::fromUtf8(dirent->owner); } else { atom.m_str = TQString::number(dirent->uid); } entry.append(atom); atom.m_uds = UDS_GROUP; if (dirent->group) { atom.m_str = TQString::fromUtf8(dirent->group); } else { atom.m_str = TQString::number(dirent->gid); } entry.append(atom); atom.m_uds = UDS_ACCESS_TIME; atom.m_long = dirent->atime; entry.append(atom); atom.m_uds = UDS_MODIFICATION_TIME; atom.m_long = dirent->mtime; entry.append(atom); atom.m_uds = UDS_MODIFICATION_TIME; atom.m_long = dirent->createtime; entry.append(atom); } sftp_attributes_free(dirent); listEntry(entry, false); } // for ever sftp_closedir(dp); listEntry(entry, true); // ready finished(); } void sftpProtocol::mkdir(const KURL &url, int permissions) { kdDebug(TDEIO_SFTP_DB) << "create directory: " << url.url() << endl; openConnection(); if (!mConnected) { return; } if (url.path().isEmpty()) { error(TDEIO::ERR_MALFORMED_URL, url.prettyURL()); return; } const TQString path = url.path(); const TQByteArray path_c = path.utf8(); // Remove existing file or symlink, if requested. if (metaData(TQString("overwrite")) == TQString("true")) { kdDebug(TDEIO_SFTP_DB) << "overwrite set, remove existing file or symlink: " << url.url() << endl; sftp_unlink(mSftp, path_c.data()); } kdDebug(TDEIO_SFTP_DB) << "Trying to create directory: " << path << endl; sftp_attributes sb = sftp_lstat(mSftp, path_c.data()); if (sb == NULL) { if (sftp_mkdir(mSftp, path_c.data(), 0777) < 0) { reportError(url, sftp_get_error(mSftp)); sftp_attributes_free(sb); return; } else { kdDebug(TDEIO_SFTP_DB) << "Successfully created directory: " << url.url() << endl; if (permissions != -1) { chmod(url, permissions); } else { finished(); } sftp_attributes_free(sb); return; } } if (sb->type == SSH_FILEXFER_TYPE_DIRECTORY) { error(TDEIO::ERR_DIR_ALREADY_EXIST, path); } else { error(TDEIO::ERR_FILE_ALREADY_EXIST, path); } sftp_attributes_free(sb); return; } void sftpProtocol::rename(const KURL& src, const KURL& dest, bool overwrite) { kdDebug(TDEIO_SFTP_DB) << "rename " << src.url() << " to " << dest.url() << endl; openConnection(); if (!mConnected) { return; } TQByteArray qsrc = src.path().utf8(); TQByteArray qdest = dest.path().utf8(); sftp_attributes sb = sftp_lstat(mSftp, qdest.data()); if (sb != NULL) { if (!overwrite) { if (sb->type == SSH_FILEXFER_TYPE_DIRECTORY) { error(TDEIO::ERR_DIR_ALREADY_EXIST, dest.url()); } else { error(TDEIO::ERR_FILE_ALREADY_EXIST, dest.url()); } sftp_attributes_free(sb); return; } del(dest, sb->type == SSH_FILEXFER_TYPE_DIRECTORY ? true : false); } sftp_attributes_free(sb); if (sftp_rename(mSftp, qsrc.data(), qdest.data()) < 0) { reportError(dest, sftp_get_error(mSftp)); return; } finished(); } void sftpProtocol::symlink(const TQString& target, const KURL& dest, bool overwrite) { kdDebug(TDEIO_SFTP_DB) << "link " << target << "->" << dest.url() << ", overwrite = " << overwrite << endl; openConnection(); if (!mConnected) { return; } TQByteArray t = target.utf8(); TQByteArray d = dest.path().utf8(); bool failed = false; if (sftp_symlink(mSftp, t.data(), d.data()) < 0) { if (overwrite) { sftp_attributes sb = sftp_lstat(mSftp, d.data()); if (sb == NULL) { failed = true; } else { if (sftp_unlink(mSftp, d.data()) < 0) { failed = true; } else { if (sftp_symlink(mSftp, t.data(), d.data()) < 0) { failed = true; } } } sftp_attributes_free(sb); } } if (failed) { reportError(dest, sftp_get_error(mSftp)); return; } finished(); } void sftpProtocol::chmod(const KURL& url, int permissions) { kdDebug(TDEIO_SFTP_DB) << "change permission of " << url.url() << " to " << TQString::number(permissions) << endl; openConnection(); if (!mConnected) { return; } TQByteArray path = url.path().utf8(); if (sftp_chmod(mSftp, path.data(), permissions) < 0) { reportError(url, sftp_get_error(mSftp)); return; } finished(); } void sftpProtocol::del(const KURL &url, bool isfile){ kdDebug(TDEIO_SFTP_DB) << "deleting " << (isfile ? "file: " : "directory: ") << url.url() << endl; openConnection(); if (!mConnected) { return; } TQByteArray path = url.path().utf8(); if (isfile) { if (sftp_unlink(mSftp, path.data()) < 0) { reportError(url, sftp_get_error(mSftp)); return; } } else { if (sftp_rmdir(mSftp, path.data()) < 0) { reportError(url, sftp_get_error(mSftp)); return; } } finished(); } void sftpProtocol::slave_status() { kdDebug(TDEIO_SFTP_DB) << "connected to " << mHost << "?: " << mConnected << endl; slaveStatus((mConnected ? mHost : TQString()), mConnected); }