kde-workspace/kioslave/sftp/kio_sftp.cpp
Ivailo Monev b7bab330a0 kioslave: remove redundant comment in sftpProtocol::sftpPut()
someone figured exit() is not the way to go there, see the following commit
in kdelibs repo:
cf6ac06d91a0befd01361ce7a01655bcfb62d0ec

Signed-off-by: Ivailo Monev <xakepa10@gmail.com>
2023-06-21 22:26:03 +03:00

2237 lines
70 KiB
C++

/*
* Copyright (c) 2001 Lucas Fisher <ljfisher@purdue.edu>
* Copyright (c) 2009 Andreas Schneider <mail@cynapses.org>
*
* 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 "kio_sftp.h"
#include <config-workspace.h>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <QtCore/QBuffer>
#include <QtCore/QByteArray>
#include <QtCore/QDir>
#include <QtCore/QFile>
#include <QtCore/QObject>
#include <QtCore/QString>
#include <QtCore/QVarLengthArray>
#include <QtCore/QDateTime>
#include <QtGui/QApplication>
#include <kapplication.h>
#include <kuser.h>
#include <kdebug.h>
#include <kmessagebox.h>
#include <kcomponentdata.h>
#include <kglobal.h>
#include <kstandarddirs.h>
#include <klocale.h>
#include <kurl.h>
#include <kmimetype.h>
#include <kde_file.h>
#include <kconfiggroup.h>
#include <kio/ioslave_defaults.h>
#include <kdemacros.h>
#define KIO_SFTP_SPECIAL_TIMEOUT 30
#define ZERO_STRUCTP(x) do { if ((x) != NULL) memset((char *)(x), 0, sizeof(*(x))); } while(0)
// How big should each data packet be? Definitely not bigger than 64kb or
// you will overflow the 2 byte size variable in a sftp packet.
#define MAX_XFER_BUF_SIZE (60 * 1024)
#define KIO_SFTP_DB 7120
// Maximum amount of data which can be sent from the KIOSlave in one chunk
// see TransferJob::slotDataReq (max_size variable) for the value
#define MAX_TRANSFER_SIZE (14 * 1024 * 1024)
using namespace KIO;
int main( int argc, char **argv )
{
if (argc != 2) {
kDebug(KIO_SFTP_DB) << "Usage: kio_sftp app-socket";
exit(-1);
}
QApplication app(argc, argv);
KComponentData componentData( "kio_sftp" );
(void) KGlobal::locale();
kDebug(KIO_SFTP_DB) << "*** Starting kio_sftp ";
sftpProtocol slave(argv[1]);
slave.dispatchLoop();
kDebug(KIO_SFTP_DB) << "*** kio_sftp Done";
return 0;
}
// Converts SSH error into KIO error
static int toKIOError (const int err)
{
switch (err) {
case SSH_FX_OK:
break;
case SSH_FX_NO_SUCH_FILE:
case SSH_FX_NO_SUCH_PATH:
return KIO::ERR_DOES_NOT_EXIST;
case SSH_FX_PERMISSION_DENIED:
return KIO::ERR_ACCESS_DENIED;
case SSH_FX_FILE_ALREADY_EXISTS:
return KIO::ERR_FILE_ALREADY_EXIST;
case SSH_FX_INVALID_HANDLE:
return KIO::ERR_MALFORMED_URL;
case SSH_FX_OP_UNSUPPORTED:
return KIO::ERR_UNSUPPORTED_ACTION;
case SSH_FX_BAD_MESSAGE:
return KIO::ERR_UNKNOWN;
default:
return KIO::ERR_INTERNAL;
}
return 0;
}
// Writes 'len' bytes from 'buf' to the file handle 'fd'.
static int writeToFile(int fd, const char *buf, size_t len)
{
while (len > 0) {
ssize_t written = write(fd, buf, len);
if (written >= 0) {
buf += written;
len -= written;
continue;
}
switch(errno) {
case EINTR:
case EAGAIN:
continue;
case EPIPE:
return ERR_CONNECTION_BROKEN;
case ENOSPC:
return ERR_DISK_FULL;
default:
return ERR_COULD_NOT_WRITE;
}
}
return 0;
}
static int seekPos(int fd, KIO::fileoffset_t pos, int mode)
{
KIO::fileoffset_t offset = -1;
while ((offset=KDE_lseek(fd, pos, mode)) == EAGAIN);
return offset;
}
static bool wasUsernameChanged(const QString& username, const KIO::AuthInfo& info)
{
QString loginName (username);
// If username is empty, assume the current logged in username. Why ?
// Because libssh's SSH_OPTIONS_USER will default to that when it is not
// set and it won't be set unless the user explicitly typed a user user
// name as part of the request URL.
if (loginName.isEmpty()) {
KUser u;
loginName = u.loginName();
}
return (loginName != info.username);
}
// The callback function for libssh
static 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;
}
static void log_callback(int priority, const char *function, const char *buffer,
void *userdata)
{
if (userdata == NULL) {
return;
}
sftpProtocol *slave = (sftpProtocol *) userdata;
slave->log_callback(priority, function, buffer, userdata);
}
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;
QString errMsg;
if (!mPublicKeyAuthInfo) {
mPublicKeyAuthInfo = new KIO::AuthInfo;
} else {
errMsg = i18n("Incorrect or invalid passphrase");
}
mPublicKeyAuthInfo->url.setProtocol(QLatin1String("sftp"));
mPublicKeyAuthInfo->url.setHost(mHost);
if (mPort > 0 && mPort != DEFAULT_SFTP_PORT) {
mPublicKeyAuthInfo->url.setPort(mPort);
}
mPublicKeyAuthInfo->url.setUser(mUsername);
KUrl u (mPublicKeyAuthInfo->url);
u.setPath(QString());
mPublicKeyAuthInfo->comment = u.url();
mPublicKeyAuthInfo->readOnly = true;
mPublicKeyAuthInfo->prompt = QString::fromUtf8(prompt);
mPublicKeyAuthInfo->keepPassword = false; // don't save passwords for public key,
// that's the task of ssh-agent.
mPublicKeyAuthInfo->setExtraField(QLatin1String("hide-username-line"), true);
kDebug(KIO_SFTP_DB) << "Entering authentication callback, prompt=" << mPublicKeyAuthInfo->prompt;
if (!openPasswordDialog(*mPublicKeyAuthInfo, errMsg)) {
kDebug(KIO_SFTP_DB) << "User canceled public key passpharse dialog";
return -1;
}
strncpy(buf, mPublicKeyAuthInfo->password.toUtf8().constData(), len - 1);
mPublicKeyAuthInfo->password.fill('x');
mPublicKeyAuthInfo->password.clear();
return 0;
}
void sftpProtocol::log_callback(int priority, const char *function, const char *buffer,
void *userdata)
{
(void) userdata;
kDebug(KIO_SFTP_DB) << "[" << function << "] (" << priority << ") " << buffer;
}
int sftpProtocol::authenticateKeyboardInteractive(AuthInfo &info) {
int err = ssh_userauth_kbdint(mSession, NULL, NULL);
while (err == SSH_AUTH_INFO) {
const QString name = QString::fromUtf8(ssh_userauth_kbdint_getname(mSession));
const QString instruction = QString::fromUtf8(ssh_userauth_kbdint_getinstruction(mSession));
const int n = ssh_userauth_kbdint_getnprompts(mSession);
kDebug(KIO_SFTP_DB) << "name=" << name << " instruction=" << instruction << " prompts=" << n;
for (int i = 0; i < n; ++i) {
char echo;
const char *answer = "";
const QString prompt = QString::fromUtf8(ssh_userauth_kbdint_getprompt(mSession, i, &echo));
kDebug(KIO_SFTP_DB) << "prompt=" << prompt << " echo=" << QString::number(echo);
if (echo) {
// See RFC4256 Section 3.3 User Interface
KIO::AuthInfo infoKbdInt;
infoKbdInt.url.setProtocol("sftp");
infoKbdInt.url.setHost(mHost);
if (mPort > 0 && mPort != DEFAULT_SFTP_PORT) {
infoKbdInt.url.setPort(mPort);
}
if (!name.isEmpty()) {
infoKbdInt.caption = QString(i18n("SFTP Login") + " - " + name);
} else {
infoKbdInt.caption = i18n("SFTP Login");
}
infoKbdInt.comment = "sftp://" + mUsername + "@" + mHost;
QString newPrompt;
if (!instruction.isEmpty()) {
newPrompt = instruction + "<br /><br />";
}
newPrompt.append(prompt);
infoKbdInt.prompt = newPrompt;
infoKbdInt.readOnly = false;
infoKbdInt.keepPassword = false;
if (openPasswordDialog(infoKbdInt, i18n("Use the username input field to answer this question."))) {
kDebug(KIO_SFTP_DB) << "Got the answer from the password dialog";
answer = info.username.toUtf8().constData();
}
if (ssh_userauth_kbdint_setanswer(mSession, i, answer) < 0) {
kDebug(KIO_SFTP_DB) << "An error occurred setting the answer: "
<< ssh_get_error(mSession);
return SSH_AUTH_ERROR;
}
break;
} else {
if (prompt.startsWith(QLatin1String("password:"), Qt::CaseInsensitive)) {
info.prompt = i18n("Please enter your password.");
} else {
info.prompt = prompt;
}
info.comment = info.url.url();
info.commentLabel = i18n("Site:");
info.setExtraField(QLatin1String("hide-username-line"), true);
if (openPasswordDialog(info)) {
kDebug(KIO_SFTP_DB) << "Got the answer from the password dialog";
answer = info.password.toUtf8().constData();
}
if (ssh_userauth_kbdint_setanswer(mSession, i, answer) < 0) {
kDebug(KIO_SFTP_DB) << "An error occurred setting the answer: "
<< ssh_get_error(mSession);
return SSH_AUTH_ERROR;
}
}
}
err = ssh_userauth_kbdint(mSession, NULL, NULL);
}
return err;
}
void sftpProtocol::reportError(const KUrl &url, const int err)
{
kDebug(KIO_SFTP_DB) << "url = " << url << " - err=" << err;
const int kioError = toKIOError(err);
if (kioError) {
error(kioError, url.prettyUrl());
}
}
bool sftpProtocol::createUDSEntry(const QString &filename, const QByteArray &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.constData());
if (sb == NULL) {
return false;
}
entry.insert(KIO::UDSEntry::UDS_NAME, filename);
if (sb->type == SSH_FILEXFER_TYPE_SYMLINK) {
entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG);
link = sftp_readlink(mSftp, path.constData());
if (link == NULL) {
sftp_attributes_free(sb);
return false;
}
entry.insert(KIO::UDSEntry::UDS_LINK_DEST, QFile::decodeName(link));
delete link;
// A symlink -> follow it only if details > 1
if (details > 1) {
sftp_attributes sb2 = sftp_stat(mSftp, path.constData());
if (sb2 == NULL) {
// It is a link pointing to nowhere
type = S_IFMT - 1;
access = S_IRWXU | S_IRWXG | S_IRWXO;
entry.insert( KIO::UDSEntry::UDS_FILE_TYPE, type);
entry.insert( KIO::UDSEntry::UDS_ACCESS, access);
entry.insert( KIO::UDSEntry::UDS_SIZE, 0LL );
goto notype;
}
sftp_attributes_free(sb);
sb = sb2;
}
}
switch (sb->type) {
case SSH_FILEXFER_TYPE_REGULAR:
entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG);
break;
case SSH_FILEXFER_TYPE_DIRECTORY:
entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
break;
case SSH_FILEXFER_TYPE_SYMLINK:
entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFLNK);
break;
case SSH_FILEXFER_TYPE_SPECIAL:
case SSH_FILEXFER_TYPE_UNKNOWN:
entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFMT - 1);
break;
}
access = sb->permissions & 07777;
entry.insert(KIO::UDSEntry::UDS_ACCESS, access);
entry.insert(KIO::UDSEntry::UDS_SIZE, sb->size);
notype:
if (details > 0) {
if (sb->owner) {
entry.insert(KIO::UDSEntry::UDS_USER, QString::fromUtf8(sb->owner));
} else {
entry.insert(KIO::UDSEntry::UDS_USER, QString::number(sb->uid));
}
if (sb->group) {
entry.insert(KIO::UDSEntry::UDS_GROUP, QString::fromUtf8(sb->group));
} else {
entry.insert(KIO::UDSEntry::UDS_GROUP, QString::number(sb->gid));
}
entry.insert(KIO::UDSEntry::UDS_ACCESS_TIME, sb->atime);
entry.insert(KIO::UDSEntry::UDS_MODIFICATION_TIME, sb->mtime);
entry.insert(KIO::UDSEntry::UDS_CREATION_TIME, sb->createtime);
}
sftp_attributes_free(sb);
return true;
}
QString sftpProtocol::canonicalizePath(const QString &path)
{
kDebug(KIO_SFTP_DB) << "Path to canonicalize: " << path;
QString cPath;
char *sPath = NULL;
if (path.isEmpty()) {
return cPath;
}
sPath = sftp_canonicalize_path(mSftp, path.toUtf8().constData());
if (sPath == NULL) {
kDebug(KIO_SFTP_DB) << "Could not canonicalize path: " << path;
return cPath;
}
cPath = QFile::decodeName(sPath);
delete sPath;
kDebug(KIO_SFTP_DB) << "Canonicalized path: " << cPath;
return cPath;
}
sftpProtocol::sftpProtocol(const QByteArray &app_socket)
: SlaveBase("kio_sftp", app_socket),
mConnected(false), mPort(-1), mSession(NULL), mSftp(NULL), mPublicKeyAuthInfo(0)
{
kDebug(KIO_SFTP_DB) << "pid = " << getpid();
kDebug(KIO_SFTP_DB) << "debug = " << getenv("KIO_SFTP_LOG_VERBOSITY");
mCallbacks = (ssh_callbacks) malloc(sizeof(struct ssh_callbacks_struct));
if (mCallbacks == NULL) {
error(KIO::ERR_OUT_OF_MEMORY, i18n("Could not allocate callbacks"));
return;
}
ZERO_STRUCTP(mCallbacks);
mCallbacks->userdata = this;
mCallbacks->auth_function = ::auth_callback;
ssh_callbacks_init(mCallbacks);
char *verbosity = getenv("KIO_SFTP_LOG_VERBOSITY");
if (verbosity != NULL) {
int level = atoi(verbosity);
int rc;
rc = ssh_set_log_level(level);
if (rc != SSH_OK) {
error(KIO::ERR_INTERNAL, i18n("Could not set log verbosity."));
return;
}
rc = ssh_set_log_userdata(this);
if (rc != SSH_OK) {
error(KIO::ERR_INTERNAL, i18n("Could not set log userdata."));
return;
}
rc = ssh_set_log_callback(::log_callback);
if (rc != SSH_OK) {
error(KIO::ERR_INTERNAL, i18n("Could not set log callback."));
return;
}
}
}
sftpProtocol::~sftpProtocol()
{
kDebug(KIO_SFTP_DB) << "pid = " << getpid();
closeConnection();
delete mCallbacks;
delete mPublicKeyAuthInfo; // for precaution
/* cleanup and shut down cryto stuff */
ssh_finalize();
}
void sftpProtocol::setHost(const QString& host, quint16 port, const QString& user, const QString& pass)
{
kDebug(KIO_SFTP_DB) << user << "@" << host << ":" << port;
// Close connection if the request is to another server...
if (host != mHost || port != mPort ||
user != mUsername || pass != mPassword) {
closeConnection();
}
mHost = host;
mPort = port;
mUsername = user;
mPassword = pass;
}
bool sftpProtocol::sftpOpenConnection (const AuthInfo& info)
{
mSession = ssh_new();
if (mSession == NULL) {
error(KIO::ERR_OUT_OF_MEMORY, i18n("Could not create a new SSH session."));
return false;
}
long timeout_sec = 30, timeout_usec = 0;
kDebug(KIO_SFTP_DB) << "Creating the SSH session and setting options";
// Set timeout
int rc = ssh_options_set(mSession, SSH_OPTIONS_TIMEOUT, &timeout_sec);
if (rc < 0) {
error(KIO::ERR_INTERNAL, i18n("Could not set a timeout."));
return false;
}
rc = ssh_options_set(mSession, SSH_OPTIONS_TIMEOUT_USEC, &timeout_usec);
if (rc < 0) {
error(KIO::ERR_INTERNAL, i18n("Could not set a timeout."));
return false;
}
// Don't use any compression
rc = ssh_options_set(mSession, SSH_OPTIONS_COMPRESSION_C_S, "none");
if (rc < 0) {
error(KIO::ERR_INTERNAL, i18n("Could not set compression."));
return false;
}
rc = ssh_options_set(mSession, SSH_OPTIONS_COMPRESSION_S_C, "none");
if (rc < 0) {
error(KIO::ERR_INTERNAL, i18n("Could not set compression."));
return false;
}
// Set host and port
rc = ssh_options_set(mSession, SSH_OPTIONS_HOST, mHost.toUtf8().constData());
if (rc < 0) {
error(KIO::ERR_INTERNAL, i18n("Could not set host."));
return false;
}
if (mPort > 0) {
rc = ssh_options_set(mSession, SSH_OPTIONS_PORT, &mPort);
if (rc < 0) {
error(KIO::ERR_INTERNAL, i18n("Could not set port."));
return false;
}
}
// Set the username
if (!info.username.isEmpty()) {
rc = ssh_options_set(mSession, SSH_OPTIONS_USER, info.username.toUtf8().constData());
if (rc < 0) {
error(KIO::ERR_INTERNAL, i18n("Could not set username."));
return false;
}
}
// Read ~/.ssh/config
rc = ssh_options_parse_config(mSession, NULL);
if (rc < 0) {
error(KIO::ERR_INTERNAL, i18n("Could not parse the config file."));
return false;
}
ssh_set_callbacks(mSession, mCallbacks);
kDebug(KIO_SFTP_DB) << "Trying to connect to the SSH server";
/* try to connect */
rc = ssh_connect(mSession);
if (rc < 0) {
error(KIO::ERR_SLAVE_DEFINED, QString::fromUtf8(ssh_get_error(mSession)));
closeConnection();
return false;
}
return true;
}
void sftpProtocol::openConnection()
{
if (mConnected) {
return;
}
const int effectivePort = mPort > 0 ? mPort : DEFAULT_SFTP_PORT;
kDebug(KIO_SFTP_DB) << "username=" << mUsername << ", host=" << mHost << ", port=" << effectivePort;
infoMessage(i18n("Opening SFTP connection to host %1:<numid>%2</numid>", mHost, effectivePort));
if (mHost.isEmpty()) {
kDebug(KIO_SFTP_DB) << "openConnection(): Need hostname...";
error(KIO::ERR_UNKNOWN_HOST, QString());
return;
}
AuthInfo info;
info.url.setProtocol("sftp");
info.url.setHost(mHost);
if ( mPort > 0 && mPort != DEFAULT_SFTP_PORT ) {
info.url.setPort(mPort);
}
info.url.setUser(mUsername);
info.username = mUsername;
// Check for cached authentication info if no password is specified...
if (mPassword.isEmpty()) {
kDebug(KIO_SFTP_DB) << "checking cache: info.username =" << info.username
<< ", info.url =" << info.url.prettyUrl();
checkCachedAuthentication(info);
} else {
info.password = mPassword;
}
// Start the ssh connection.
QString msg; // msg for dialog box
QString caption; // dialog box caption
unsigned char *hash = NULL; // the server hash
ssh_key srv_pubkey;
char *hexa;
size_t hlen;
int rc, state;
// Attempt to start a ssh session and establish a connection with the server.
if (!sftpOpenConnection(info)) {
return;
}
kDebug(KIO_SFTP_DB) << "Getting the SSH server hash";
/* get the hash */
rc = ssh_get_publickey(mSession, &srv_pubkey);
if (rc < 0) {
error(KIO::ERR_SLAVE_DEFINED, QString::fromUtf8(ssh_get_error(mSession)));
closeConnection();
return;
}
rc = ssh_get_publickey_hash(srv_pubkey,
SSH_PUBLICKEY_HASH_SHA1,
&hash,
&hlen);
ssh_key_free(srv_pubkey);
if (rc < 0) {
error(KIO::ERR_SLAVE_DEFINED,
i18n("Could not create hash from server public key"));
closeConnection();
return;
}
kDebug(KIO_SFTP_DB) << "Checking if the SSH server is known";
/* check the server public key hash */
state = ssh_is_server_known(mSession);
switch (state) {
case SSH_SERVER_KNOWN_OK: {
break;
}
case SSH_SERVER_FOUND_OTHER: {
ssh_string_free_char((char *)hash);
error(KIO::ERR_SLAVE_DEFINED, 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", QString::fromUtf8(ssh_get_error(mSession))));
closeConnection();
return;
}
case SSH_SERVER_KNOWN_CHANGED: {
hexa = ssh_get_hexa(hash, hlen);
ssh_string_free_char((char *)hash);
/* TODO print known_hosts file, port? */
error(KIO::ERR_SLAVE_DEFINED, 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",
mHost, QString::fromUtf8(hexa), QString::fromUtf8(ssh_get_error(mSession))));
ssh_string_free_char(hexa);
closeConnection();
return;
}
case SSH_SERVER_FILE_NOT_FOUND:
case SSH_SERVER_NOT_KNOWN: {
hexa = ssh_get_hexa(hash, hlen);
ssh_string_free_char((char *)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?", mHost, hexa);
ssh_string_free_char(hexa);
if (KMessageBox::Yes != messageBox(WarningYesNo, msg, caption)) {
closeConnection();
error(KIO::ERR_USER_CANCELED, QString());
return;
}
/* write the known_hosts file */
kDebug(KIO_SFTP_DB) << "Adding server to known_hosts file.";
if (ssh_write_knownhost(mSession) < 0) {
error(KIO::ERR_USER_CANCELED, QString::fromUtf8(ssh_get_error(mSession)));
closeConnection();
return;
}
break;
}
case SSH_SERVER_ERROR: {
ssh_string_free_char((char *)hash);
error(KIO::ERR_SLAVE_DEFINED, QString::fromUtf8(ssh_get_error(mSession)));
return;
}
}
kDebug(KIO_SFTP_DB) << "Trying to authenticate with the server";
// Try to login without authentication
rc = ssh_userauth_none(mSession, NULL);
if (rc == SSH_AUTH_ERROR) {
closeConnection();
error(KIO::ERR_COULD_NOT_LOGIN, i18n("Authentication failed."));
return;
}
// This NEEDS to be called after ssh_userauth_none() !!!
int method = ssh_auth_list(mSession);
if (rc != SSH_AUTH_SUCCESS && method == 0) {
closeConnection();
error(KIO::ERR_COULD_NOT_LOGIN, i18n("Authentication failed. The server "
"didn't send any authentication methods"));
return;
}
// Try to authenticate with public key first
if (rc != SSH_AUTH_SUCCESS && (method & SSH_AUTH_METHOD_PUBLICKEY)) {
kDebug(KIO_SFTP_DB) << "Trying to authenticate with public key";
for(;;) {
rc = ssh_userauth_publickey_auto(mSession, NULL, NULL);
if (rc == SSH_AUTH_ERROR) {
kDebug(KIO_SFTP_DB) << "Public key authentication failed:" <<
QString::fromUtf8(ssh_get_error(mSession));
closeConnection();
clearPubKeyAuthInfo();
error(KIO::ERR_COULD_NOT_LOGIN, i18n("Authentication failed."));
return;
} else if (rc != SSH_AUTH_DENIED || !mPublicKeyAuthInfo) {
clearPubKeyAuthInfo();
break;
}
}
}
// Try to authenticate with GSSAPI
if (rc != SSH_AUTH_SUCCESS && (method & SSH_AUTH_METHOD_GSSAPI_MIC)) {
kDebug(KIO_SFTP_DB) << "Trying to authenticate with GSSAPI";
rc = ssh_userauth_gssapi(mSession);
if (rc == SSH_AUTH_ERROR) {
kDebug(KIO_SFTP_DB) << "Public key authentication failed:" <<
QString::fromUtf8(ssh_get_error(mSession));
closeConnection();
error(KIO::ERR_COULD_NOT_LOGIN, i18n("Authentication failed."));
return;
}
}
// Try to authenticate with keyboard interactive
if (rc != SSH_AUTH_SUCCESS && (method & SSH_AUTH_METHOD_INTERACTIVE)) {
kDebug(KIO_SFTP_DB) << "Trying to authenticate with keyboard interactive";
AuthInfo info2 (info);
rc = authenticateKeyboardInteractive(info2);
if (rc == SSH_AUTH_SUCCESS) {
info = info2;
} else if (rc == SSH_AUTH_ERROR) {
kDebug(KIO_SFTP_DB) << "Keyboard interactive authentication failed:"
<< QString::fromUtf8(ssh_get_error(mSession));
closeConnection();
error(KIO::ERR_COULD_NOT_LOGIN, i18n("Authentication failed."));
return;
}
}
// Try to authenticate with password
if (rc != SSH_AUTH_SUCCESS && (method & SSH_AUTH_METHOD_PASSWORD)) {
kDebug(KIO_SFTP_DB) << "Trying to authenticate with password";
info.caption = i18n("SFTP Login");
info.prompt = i18n("Please enter your username and password.");
info.comment = info.url.url();
info.commentLabel = i18n("Site:");
bool isFirstLoginAttempt = true;
for(;;) {
if (!isFirstLoginAttempt || info.password.isEmpty()) {
info.keepPassword = true; // make the "keep Password" check box visible to the user.
QString username (info.username);
const QString errMsg(isFirstLoginAttempt ? QString() : i18n("Incorrect username or password"));
kDebug(KIO_SFTP_DB) << "Username:" << username << "first attempt?"
<< isFirstLoginAttempt << "error:" << errMsg;
// Handle user canceled or dialog failed to open...
if (!openPasswordDialog(info, errMsg)) {
kDebug(KIO_SFTP_DB) << "User canceled password/retry dialog";
closeConnection();
error(KIO::ERR_USER_CANCELED, QString());
return;
}
// If the user name changes, we have to restablish connection again
// since the user name must always be set before calling ssh_connect.
if (wasUsernameChanged(username, info)) {
kDebug(KIO_SFTP_DB) << "Username changed to" << info.username;
if (!info.url.user().isEmpty()) {
info.url.setUser(info.username);
}
closeConnection();
if (!sftpOpenConnection(info)) {
return;
}
}
}
rc = ssh_userauth_password(mSession, info.username.toUtf8().constData(), info.password.toUtf8().constData());
if (rc == SSH_AUTH_SUCCESS) {
break;
} else if (rc == SSH_AUTH_ERROR) {
kDebug(KIO_SFTP_DB) << "Password authentication failed:"
<< QString::fromUtf8(ssh_get_error(mSession));
closeConnection();
error(KIO::ERR_COULD_NOT_LOGIN, i18n("Authentication failed."));
return;
}
isFirstLoginAttempt = false; // failed attempt to login.
info.password.clear(); // clear the password after failed attempts.
}
}
// If we're still not authenticated then we need to leave.
if (rc != SSH_AUTH_SUCCESS) {
error(KIO::ERR_COULD_NOT_LOGIN, i18n("Authentication failed."));
return;
}
// start sftp session
kDebug(KIO_SFTP_DB) << "Trying to request the sftp session";
mSftp = sftp_new(mSession);
if (mSftp == NULL) {
closeConnection();
error(KIO::ERR_COULD_NOT_LOGIN, i18n("Unable to request the SFTP subsystem. "
"Make sure SFTP is enabled on the server."));
return;
}
kDebug(KIO_SFTP_DB) << "Trying to initialize the sftp session";
if (sftp_init(mSftp) < 0) {
closeConnection();
error(KIO::ERR_COULD_NOT_LOGIN, i18n("Could not initialize the SFTP session."));
return;
}
// Login succeeded!
infoMessage(i18n("Successfully connected to %1", mHost));
if (info.keepPassword) {
kDebug(KIO_SFTP_DB) << "Caching info.username = " << info.username
<< ", info.url = " << info.url.prettyUrl();
cacheAuthentication(info);
}
// Update the original username in case it was changed!
if (!mUsername.isEmpty()) {
mUsername = info.username;
}
setTimeoutSpecialCommand(KIO_SFTP_SPECIAL_TIMEOUT);
mConnected = true;
info.password.fill('x');
info.password.clear();
}
void sftpProtocol::closeConnection()
{
kDebug(KIO_SFTP_DB);
if (mSftp) {
sftp_free(mSftp);
mSftp = NULL;
}
if (mSession) {
ssh_disconnect(mSession);
mSession = NULL;
}
mConnected = false;
}
void sftpProtocol::special(const QByteArray &)
{
int rc;
kDebug(KIO_SFTP_DB) << "special(): polling";
if (!mSftp) {
return;
}
/*
* 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 = ssh_channel_poll(mSftp->channel, 0);
if (rc > 0) {
rc = ssh_channel_poll(mSftp->channel, 1);
}
if (rc < 0) {
kDebug(KIO_SFTP_DB) << "ssh_channel_poll failed: " << ssh_get_error(mSession);
}
setTimeoutSpecialCommand(KIO_SFTP_SPECIAL_TIMEOUT);
}
void sftpProtocol::open(const KUrl &url, QIODevice::OpenMode mode)
{
close();
kDebug(KIO_SFTP_DB) << "open: " << url;
if (!sftpLogin()) {
return;
}
const QString path = url.path();
const QByteArray path_c = path.toUtf8();
sftp_attributes sb = sftp_lstat(mSftp, path_c.constData());
if (sb == NULL) {
reportError(url, sftp_get_error(mSftp));
return;
}
switch (sb->type) {
case SSH_FILEXFER_TYPE_DIRECTORY: {
error(KIO::ERR_IS_DIRECTORY, url.prettyUrl());
sftp_attributes_free(sb);
return;
}
case SSH_FILEXFER_TYPE_SPECIAL:
case SSH_FILEXFER_TYPE_UNKNOWN: {
error(KIO::ERR_CANNOT_OPEN_FOR_READING, url.prettyUrl());
sftp_attributes_free(sb);
return;
}
case SSH_FILEXFER_TYPE_SYMLINK:
case SSH_FILEXFER_TYPE_REGULAR: {
break;
}
}
KIO::filesize_t fileSize = sb->size;
sftp_attributes_free(sb);
int flags = 0;
if (mode & QIODevice::ReadOnly) {
if (mode & QIODevice::WriteOnly) {
flags = O_RDWR | O_CREAT;
} else {
flags = O_RDONLY;
}
} else if (mode & QIODevice::WriteOnly) {
flags = O_WRONLY | O_CREAT;
}
if (mode & QIODevice::Append) {
flags |= O_APPEND;
} else if (mode & QIODevice::Truncate) {
flags |= O_TRUNC;
}
if (flags & O_CREAT) {
mOpenFile = sftp_open(mSftp, path_c.constData(), flags, 0644);
} else {
mOpenFile = sftp_open(mSftp, path_c.constData(), flags, 0);
}
if (mOpenFile == NULL) {
error(KIO::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.
if (mode & QIODevice::ReadOnly) {
size_t bytesRequested = 1024;
ssize_t bytesRead = 0;
QVarLengthArray<char> buffer(bytesRequested);
bytesRead = sftp_read(mOpenFile, buffer.data(), bytesRequested);
if (bytesRead < 0) {
error(KIO::ERR_COULD_NOT_READ, mOpenUrl.prettyUrl());
close();
return;
} else {
QByteArray fileData = QByteArray::fromRawData(buffer.data(), bytesRead);
KMimeType::Ptr p_mimeType = KMimeType::findByNameAndContent(mOpenUrl.fileName(), fileData);
emit mimeType(p_mimeType->name());
// Go back to the beginning of the file.
sftp_rewind(mOpenFile);
}
}
mOpenUrl = url;
openOffset = 0;
totalSize(fileSize);
}
void sftpProtocol::close()
{
sftp_close(mOpenFile);
mOpenFile = NULL;
finished();
}
void sftpProtocol::get(const KUrl& url)
{
kDebug(KIO_SFTP_DB) << url;
int errorCode = 0;
const sftpProtocol::StatusCode cs = sftpGet(url, errorCode);
if (cs == sftpProtocol::Success) {
finished();
return;
}
// The call to sftpGet should only return server side errors since the file
// descriptor parameter is set to -1.
if (cs == sftpProtocol::ServerError && errorCode) {
error(errorCode, url.prettyUrl());
}
}
sftpProtocol::StatusCode sftpProtocol::sftpGet(const KUrl& url, int& errorCode, KIO::fileoffset_t offset, int fd)
{
kDebug(KIO_SFTP_DB) << url;
if (!sftpLogin()) {
return sftpProtocol::ServerError;
}
QByteArray path = url.path().toUtf8();
sftp_file file = NULL;
KIO::filesize_t totalbytesread = 0;
QByteArray filedata;
sftp_attributes sb = sftp_lstat(mSftp, path.constData());
if (sb == NULL) {
errorCode = toKIOError(sftp_get_error(mSftp));
return sftpProtocol::ServerError;
}
switch (sb->type) {
case SSH_FILEXFER_TYPE_DIRECTORY: {
errorCode = KIO::ERR_IS_DIRECTORY;
sftp_attributes_free(sb);
return sftpProtocol::ServerError;
}
case SSH_FILEXFER_TYPE_SPECIAL:
case SSH_FILEXFER_TYPE_UNKNOWN: {
errorCode = KIO::ERR_CANNOT_OPEN_FOR_READING;
sftp_attributes_free(sb);
return sftpProtocol::ServerError;
}
case SSH_FILEXFER_TYPE_SYMLINK:
case SSH_FILEXFER_TYPE_REGULAR: {
break;
}
}
// Open file
file = sftp_open(mSftp, path.constData(), O_RDONLY, 0);
if (file == NULL) {
errorCode = KIO::ERR_CANNOT_OPEN_FOR_READING;
sftp_attributes_free(sb);
return sftpProtocol::ServerError;
}
char mimeTypeBuf[1024];
ssize_t bytesread = sftp_read(file, mimeTypeBuf, sizeof(mimeTypeBuf));
if (bytesread < 0) {
errorCode = KIO::ERR_COULD_NOT_READ;
return sftpProtocol::ServerError;
} else {
int accuracy = 0;
KMimeType::Ptr mime = KMimeType::findByNameAndContent(url.fileName(), QByteArray(mimeTypeBuf, bytesread), 0, &accuracy);
if (!mime->isDefault() && accuracy == 100) {
emit mimeType(mime->name());
} else {
accuracy = 0;
mime = KMimeType::findByUrl(url.fileName(), 0, false, true, &accuracy);
emit mimeType(mime->name());
}
sftp_rewind(file);
}
// Set the total size
totalSize(sb->size);
// If offset is not specified, check the "resume" meta-data.
if (offset < 0) {
const QString resumeOffsetStr = metaData(QLatin1String("resume"));
if (!resumeOffsetStr.isEmpty()) {
bool ok;
qlonglong resumeOffset = resumeOffsetStr.toLongLong(&ok);
if (ok) {
offset = resumeOffset;
}
}
}
// If we can resume, offset the buffer properly.
if (offset > 0 && ((unsigned long long) offset < sb->size)) {
if (sftp_seek64(file, offset) == 0) {
canResume();
totalbytesread = offset;
kDebug(KIO_SFTP_DB) << "Resume offset: " << QString::number(offset);
}
}
bytesread = 0;
sftpProtocol::GetRequest request(file, sb);
for (;;) {
// Enqueue get requests
if (!request.enqueueChunks()) {
errorCode = KIO::ERR_COULD_NOT_READ;
return sftpProtocol::ServerError;
}
filedata.clear();
bytesread = request.readChunks(filedata);
// Read pending get requests
if (bytesread == -1) {
errorCode = KIO::ERR_COULD_NOT_READ;
return sftpProtocol::ServerError;
} else if (bytesread == 0) {
if (file->eof) {
break;
} else {
continue;
}
}
if (fd == -1) {
data(filedata);
} else if ((errorCode = writeToFile(fd, filedata.constData(), filedata.size()) != 0)) {
return sftpProtocol::ClientError;
}
// increment total bytes read
totalbytesread += filedata.length();
processedSize(totalbytesread);
}
if (fd == -1) {
data(QByteArray());
}
processedSize(static_cast<KIO::filesize_t>(sb->size));
return sftpProtocol::Success;
}
void sftpProtocol::put(const KUrl& url, int permissions, KIO::JobFlags flags)
{
kDebug(KIO_SFTP_DB) << url << ", permissions =" << permissions
<< ", overwrite =" << (flags & KIO::Overwrite)
<< ", resume =" << (flags & KIO::Resume);
kDebug(KIO_SFTP_DB) << url;
int errorCode = 0;
const sftpProtocol::StatusCode cs = sftpPut(url, permissions, flags, errorCode);
if (cs == sftpProtocol::Success) {
finished();
return;
}
// The call to sftpPut should only return server side errors since the file
// descriptor parameter is set to -1.
if (cs == sftpProtocol::ServerError && errorCode) {
error(errorCode, url.prettyUrl());
}
}
sftpProtocol::StatusCode sftpProtocol::sftpPut(const KUrl& url, int permissions, JobFlags flags, int& errorCode, int fd)
{
kDebug(KIO_SFTP_DB) << url << ", permissions =" << permissions
<< ", overwrite =" << (flags & KIO::Overwrite)
<< ", resume =" << (flags & KIO::Resume);
if (!sftpLogin()) {
return sftpProtocol::ServerError;
}
const QString dest_orig = url.path();
const QByteArray dest_orig_c = dest_orig.toUtf8();
const QString dest_part = dest_orig + ".part";
const QByteArray dest_part_c = dest_part.toUtf8();
uid_t owner = 0;
gid_t group = 0;
sftp_attributes sb = sftp_lstat(mSftp, dest_orig_c.constData());
const bool bOrigExists = (sb != NULL);
bool bPartExists = false;
const bool bMarkPartial = config()->readEntry("MarkPartial", 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.constData());
bPartExists = (sbPart != NULL);
if (bPartExists && !(flags & KIO::Resume) && !(flags & KIO::Overwrite) &&
sbPart->size > 0 && sbPart->type == SSH_FILEXFER_TYPE_REGULAR) {
if (fd == -1) {
// 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.
kDebug(KIO_SFTP_DB) << "calling canResume with " << sbPart->size;
flags |= canResume(sbPart->size) ? KIO::Resume : KIO::DefaultFlags;
kDebug(KIO_SFTP_DB) << "put got answer " << (flags & KIO::Resume);
} else {
KIO::filesize_t pos = seekPos(fd, sbPart->size, SEEK_SET);
if (pos != sbPart->size) {
kDebug(KIO_SFTP_DB) << "Failed to seek to" << sbPart->size << "bytes in source file. Reason given" << strerror(errno);
sftp_attributes_free(sb);
sftp_attributes_free(sbPart);
errorCode = ERR_COULD_NOT_SEEK;
return sftpProtocol::ClientError;
}
flags |= KIO::Resume;
}
kDebug(KIO_SFTP_DB) << "Resuming at" << sbPart->size;
sftp_attributes_free(sbPart);
}
}
if (bOrigExists && !(flags & KIO::Overwrite) && !(flags & KIO::Resume)) {
if (sb->type == SSH_FILEXFER_TYPE_DIRECTORY) {
errorCode = KIO::ERR_DIR_ALREADY_EXIST;
} else {
errorCode = KIO::ERR_FILE_ALREADY_EXIST;
}
sftp_attributes_free(sb);
return sftpProtocol::ServerError;
}
QByteArray dest;
int result = -1;
sftp_file file = NULL;
StatusCode cs = sftpProtocol::Success;
KIO::fileoffset_t totalBytesSent = 0;
// Loop until we got 0 (end of data)
do {
QByteArray buffer;
if (fd == -1) {
dataReq(); // Request for data
result = readData(buffer);
} else {
char buf[MAX_XFER_BUF_SIZE]; //
result = ::read(fd, buf, sizeof(buf));
if (result < 0) {
errorCode = ERR_COULD_NOT_READ;
cs = sftpProtocol::ClientError;
break;
}
buffer = QByteArray(buf, result);
}
if (result >= 0) {
if (dest.isEmpty()) {
if (bMarkPartial) {
kDebug(KIO_SFTP_DB) << "Appending .part extension to" << dest_orig;
dest = dest_part_c;
if (bPartExists && !(flags & KIO::Resume)) {
kDebug(KIO_SFTP_DB) << "Deleting partial file" << dest_part;
sftp_unlink(mSftp, dest_part_c.constData());
// Catch errors when we try to open the file.
}
} else {
dest = dest_orig_c; // Will be automatically truncated below...
} // bMarkPartial
if ((flags & KIO::Resume)) {
sftp_attributes fstat;
kDebug(KIO_SFTP_DB) << "Trying to append: " << dest;
file = sftp_open(mSftp, dest.constData(), O_RDWR, 0); // append if resuming
if (file) {
fstat = sftp_fstat(file);
if (fstat) {
sftp_seek64(file, fstat->size); // Seek to end TODO
totalBytesSent += fstat->size;
sftp_attributes_free(fstat);
}
}
} else {
mode_t initialMode;
if (permissions != -1) {
initialMode = permissions | S_IWUSR | S_IRUSR;
} else {
initialMode = 0644;
}
kDebug(KIO_SFTP_DB) << "Trying to open:" << QString(dest) << ", mode=" << QString::number(initialMode);
file = sftp_open(mSftp, dest.constData(), O_CREAT | O_TRUNC | O_WRONLY, initialMode);
} // flags & KIO::Resume
if (file == NULL) {
kDebug(KIO_SFTP_DB) << "COULD NOT WRITE " << QString(dest)
<< ", permissions=" << permissions
<< ", error=" << ssh_get_error(mSession);
if (sftp_get_error(mSftp) == SSH_FX_PERMISSION_DENIED) {
errorCode = KIO::ERR_WRITE_ACCESS_DENIED;
} else {
errorCode = KIO::ERR_CANNOT_OPEN_FOR_WRITING;
}
cs = sftpProtocol::ServerError;
result = -1;
continue;
} // file
} // dest.isEmpty
ssize_t bytesWritten = sftp_write(file, buffer.data(), buffer.size());
if (bytesWritten < 0) {
errorCode = KIO::ERR_COULD_NOT_WRITE;
result = -1;
} else {
totalBytesSent += bytesWritten;
emit processedSize(totalBytesSent);
}
} // result
} while (result > 0);
sftp_attributes_free(sb);
// An error occurred deal with it.
if (result < 0) {
kDebug(KIO_SFTP_DB) << "Error during 'put'. Aborting.";
if (file != NULL) {
sftp_close(file);
sftp_attributes attr = sftp_stat(mSftp, dest.constData());
if (bMarkPartial && attr != NULL) {
size_t size = config()->readEntry("MinimumKeepSize", DEFAULT_MINIMUM_KEEP_SIZE);
if (attr->size < size) {
sftp_unlink(mSftp, dest.constData());
}
}
delete attr;
sftp_attributes_free(attr);
}
return cs;
}
if (file == NULL) { // we got nothing to write out, so we never opened the file
return sftpProtocol::Success;
}
if (sftp_close(file) < 0) {
kWarning(KIO_SFTP_DB) << "Error when closing file descriptor";
error(KIO::ERR_COULD_NOT_WRITE, dest_orig);
return sftpProtocol::ServerError;
}
// 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 ((flags & KIO::Overwrite)) {
sftp_unlink(mSftp, dest_orig_c.constData());
}
if (sftp_rename(mSftp, dest.constData(), dest_orig_c.constData()) < 0) {
kWarning(KIO_SFTP_DB) << " Couldn't rename " << dest << " to " << dest_orig;
errorCode = KIO::ERR_CANNOT_RENAME_PARTIAL;
return sftpProtocol::ServerError;
}
}
// set final permissions
if (permissions != -1 && !(flags & KIO::Resume)) {
kDebug(KIO_SFTP_DB) << "Trying to set final permissions of " << dest_orig << " to " << QString::number(permissions);
if (sftp_chmod(mSftp, dest_orig_c.constData(), permissions) < 0) {
errorCode = -1; // force copy to call sftpSendWarning...
return sftpProtocol::ServerError;
}
}
// set original owner and group
if (bOrigExists) {
kDebug(KIO_SFTP_DB) << "Trying to restore original owner and group of " << dest_orig;
if (sftp_chown(mSftp, dest_orig_c.constData(), owner, group) < 0) {
kWarning(KIO_SFTP_DB) << "Could not change owner and group for" << dest_orig;
// warning(i18n( "Could not change owner and group for\n%1", dest_orig));
}
}
// set modification time
const QString mtimeStr = metaData("modified");
if (!mtimeStr.isEmpty()) {
QDateTime dt = QDateTime::fromString(mtimeStr, Qt::ISODate);
if (dt.isValid()) {
struct timeval times[2];
sftp_attributes attr = sftp_lstat(mSftp, dest_orig_c.constData());
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;
kDebug(KIO_SFTP_DB) << "Trying to restore mtime for " << dest_orig << " to: " << mtimeStr;
result = sftp_utimes(mSftp, dest_orig_c.constData(), times);
if (result < 0) {
kWarning(KIO_SFTP_DB) << "Failed to set mtime for" << dest_orig;
}
sftp_attributes_free(attr);
}
}
}
return sftpProtocol::Success;
}
void sftpProtocol::copy(const KUrl &src, const KUrl &dest, int permissions, KIO::JobFlags flags)
{
kDebug(KIO_SFTP_DB) << src << " -> " << dest << " , permissions = " << QString::number(permissions)
<< ", overwrite = " << (flags & KIO::Overwrite)
<< ", resume = " << (flags & KIO::Resume);
QString sCopyFile;
int errorCode = 0;
StatusCode cs = sftpProtocol::ClientError;
const bool isSourceLocal = src.isLocalFile();
const bool isDestinationLocal = dest.isLocalFile();
if (!isSourceLocal && isDestinationLocal) { // sftp -> file
sCopyFile = dest.toLocalFile();
cs = sftpCopyGet(src, sCopyFile, permissions, flags, errorCode);
if (cs == sftpProtocol::ServerError) {
sCopyFile = src.url();
}
} else if (isSourceLocal && !isDestinationLocal) { // file -> sftp
sCopyFile = src.toLocalFile();
cs = sftpCopyPut(dest, sCopyFile, permissions, flags, errorCode);
if (cs == sftpProtocol::ServerError) {
sCopyFile = dest.url();
}
} else {
errorCode = KIO::ERR_UNSUPPORTED_ACTION;
sCopyFile.clear();
}
// On success or errorcode < 0, emit the finished signal and
// send a warning message to the client if errorCode < 0.
if (cs == sftpProtocol::Success || errorCode < 0) {
if (errorCode < 0) {
sftpSendWarning(errorCode, sCopyFile);
}
finished();
return;
}
if (errorCode) {
error(errorCode, sCopyFile);
}
}
sftpProtocol::StatusCode sftpProtocol::sftpCopyGet(const KUrl& url, const QString& sCopyFile, int permissions, KIO::JobFlags flags, int& errorCode)
{
kDebug(KIO_SFTP_DB) << url << "->" << sCopyFile << ", permissions=" << permissions;
// check if destination is ok ...
KDE_struct_stat buff;
const bool bDestExists = (KDE::stat(sCopyFile, &buff) != -1);
if (bDestExists) {
if (S_ISDIR(buff.st_mode)) {
errorCode = ERR_IS_DIRECTORY;
return sftpProtocol::ClientError;
}
if (!(flags & KIO::Overwrite)) {
errorCode = ERR_FILE_ALREADY_EXIST;
return sftpProtocol::ClientError;
}
}
bool bResume = false;
const QString sPart = sCopyFile + QLatin1String(".part"); // do we have a ".part" file?
const bool bPartExists = (KDE::stat(sPart, &buff) != -1);
const bool bMarkPartial = config()->readEntry("MarkPartial", true);
const QString dest = (bMarkPartial ? sPart : sCopyFile);
if (bMarkPartial && bPartExists && buff.st_size > 0) {
if (S_ISDIR(buff.st_mode)) {
errorCode = ERR_DIR_ALREADY_EXIST;
return sftpProtocol::ClientError; // client side error
}
bResume = canResume( buff.st_size );
}
if (bPartExists && !bResume) { // get rid of an unwanted ".part" file
QFile::remove(sPart);
}
// WABA: Make sure that we keep writing permissions ourselves,
// otherwise we can be in for a surprise on NFS.
mode_t initialMode;
if (permissions != -1) {
initialMode = permissions | S_IWUSR;
} else {
initialMode = 0666;
}
// open the output file ...
int fd = -1;
KIO::fileoffset_t offset = 0;
if (bResume) {
fd = KDE::open( sPart, O_RDWR ); // append if resuming
offset = seekPos(fd, 0, SEEK_END);
if (offset < 0) {
errorCode = ERR_CANNOT_RESUME;
::close(fd);
return sftpProtocol::ClientError; // client side error
}
kDebug(KIO_SFTP_DB) << "resuming at" << offset;
} else {
fd = KDE::open(dest, O_CREAT | O_TRUNC | O_WRONLY, initialMode);
}
if (fd == -1) {
kDebug(KIO_SFTP_DB) << "could not write to" << sCopyFile;
errorCode = (errno == EACCES) ? ERR_WRITE_ACCESS_DENIED : ERR_CANNOT_OPEN_FOR_WRITING;
return sftpProtocol::ClientError;
}
StatusCode result = sftpGet(url, errorCode, offset, fd);
if( ::close(fd) && result == sftpProtocol::Success ) {
errorCode = ERR_COULD_NOT_WRITE;
result = sftpProtocol::ClientError;
}
// handle renaming or deletion of a partial file ...
if (bMarkPartial) {
if (result == sftpProtocol::Success) { // rename ".part" on success
if ( KDE::rename( sPart, sCopyFile ) ) {
// If rename fails, try removing the destination first if it exists.
if (!bDestExists || !(QFile::remove(sCopyFile) && KDE::rename(sPart, sCopyFile) == 0)) {
kDebug(KIO_SFTP_DB) << "cannot rename " << sPart << " to " << sCopyFile;
errorCode = ERR_CANNOT_RENAME_PARTIAL;
result = sftpProtocol::ClientError;
}
}
} else if (KDE::stat( sPart, &buff ) == 0) { // should a very small ".part" be deleted?
const int size = config()->readEntry("MinimumKeepSize", DEFAULT_MINIMUM_KEEP_SIZE);
if (buff.st_size < size) {
QFile::remove(sPart);
}
}
}
const QString mtimeStr = metaData("modified");
if (!mtimeStr.isEmpty()) {
QDateTime dt = QDateTime::fromString(mtimeStr, Qt::ISODate);
if (dt.isValid()) {
struct utimbuf utbuf;
utbuf.actime = buff.st_atime; // access time, unchanged
utbuf.modtime = dt.toTime_t(); // modification time
KDE::utime(sCopyFile, &utbuf);
}
}
return result;
}
sftpProtocol::StatusCode sftpProtocol::sftpCopyPut(const KUrl& url, const QString& sCopyFile, int permissions, JobFlags flags, int& errorCode)
{
kDebug(KIO_SFTP_DB) << sCopyFile << "->" << url << ", permissions=" << permissions << ", flags" << flags;
// check if source is ok ...
KDE_struct_stat buff;
bool bSrcExists = (KDE::stat(sCopyFile, &buff) != -1);
if (bSrcExists) {
if (S_ISDIR(buff.st_mode)) {
errorCode = ERR_IS_DIRECTORY;
return sftpProtocol::ClientError;
}
} else {
errorCode = ERR_DOES_NOT_EXIST;
return sftpProtocol::ClientError;
}
const int fd = KDE::open(sCopyFile, O_RDONLY);
if (fd == -1) {
errorCode = ERR_CANNOT_OPEN_FOR_READING;
return sftpProtocol::ClientError;
}
totalSize(buff.st_size);
// delegate the real work (errorCode gets status) ...
StatusCode ret = sftpPut(url, permissions, flags, errorCode, fd);
::close(fd);
return ret;
}
void sftpProtocol::stat(const KUrl& url)
{
kDebug(KIO_SFTP_DB) << url;
if (!sftpLogin()) {
return;
}
if (! url.hasPath() || QDir::isRelativePath(url.path()) ||
url.path().contains("/./") || url.path().contains("/../")) {
QString cPath;
if (url.hasPath()) {
cPath = canonicalizePath(url.path());
} else {
cPath = canonicalizePath(QLatin1String("."));
}
if (cPath.isEmpty()) {
error(KIO::ERR_MALFORMED_URL, url.prettyUrl());
return;
}
KUrl redir(url);
redir.setPath(cPath);
redirection(redir);
kDebug(KIO_SFTP_DB) << "redirecting to " << redir.url();
finished();
return;
}
QByteArray path = url.path().toUtf8();
const QString sDetails = metaData(QLatin1String("details"));
const int details = sDetails.isEmpty() ? 2 : sDetails.toInt();
UDSEntry entry;
entry.clear();
if (!createUDSEntry(url.fileName(), path, entry, details)) {
error(KIO::ERR_DOES_NOT_EXIST, url.prettyUrl());
return;
}
statEntry(entry);
finished();
}
void sftpProtocol::mimetype(const KUrl& url)
{
kDebug(KIO_SFTP_DB) << url;
if (!sftpLogin()) {
return;
}
// open() feeds the mimetype
open(url, QIODevice::ReadOnly);
close();
finished();
}
void sftpProtocol::listDir(const KUrl& url)
{
kDebug(KIO_SFTP_DB) << "list directory: " << url;
if (!sftpLogin()) {
return;
}
if (! url.hasPath() || QDir::isRelativePath(url.path()) ||
url.path().contains("/./") || url.path().contains("/../")) {
QString cPath;
if (url.hasPath()) {
cPath = canonicalizePath(url.path());
} else {
cPath = canonicalizePath(QString("."));
}
if (cPath.isEmpty()) {
error(KIO::ERR_MALFORMED_URL, url.prettyUrl());
return;
}
KUrl redir(url);
redir.setPath(cPath);
redirection(redir);
kDebug(KIO_SFTP_DB) << "redirecting to " << redir.url();
finished();
return;
}
QByteArray path = url.path().toUtf8();
sftp_dir dp = sftp_opendir(mSftp, path.constData());
if (dp == NULL) {
reportError(url, sftp_get_error(mSftp));
return;
}
sftp_attributes dirent = NULL;
const QString sDetails = metaData(QLatin1String("details"));
const int details = sDetails.isEmpty() ? 2 : sDetails.toInt();
UDSEntry entry;
kDebug(KIO_SFTP_DB) << "readdir: " << path << ", details: " << QString::number(details);
for (;;) {
mode_t access;
mode_t type;
char *link;
dirent = sftp_readdir(mSftp, dp);
if (dirent == NULL) {
break;
}
entry.clear();
entry.insert(KIO::UDSEntry::UDS_NAME, QFile::decodeName(dirent->name));
if (dirent->type == SSH_FILEXFER_TYPE_SYMLINK) {
QByteArray file = path + '/' + QFile::decodeName(dirent->name).toUtf8();
entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG);
link = sftp_readlink(mSftp, file.constData());
if (link == NULL) {
sftp_attributes_free(dirent);
error(KIO::ERR_INTERNAL, i18n("Could not read link: %1", QString::fromUtf8(file)));
return;
}
entry.insert(KIO::UDSEntry::UDS_LINK_DEST, QFile::decodeName(link));
delete link;
// A symlink -> follow it only if details > 1
if (details > 1) {
sftp_attributes sb = sftp_stat(mSftp, file.constData());
if (sb == NULL) {
// It is a link pointing to nowhere
type = S_IFMT - 1;
access = S_IRWXU | S_IRWXG | S_IRWXO;
entry.insert( KIO::UDSEntry::UDS_FILE_TYPE, type);
entry.insert( KIO::UDSEntry::UDS_ACCESS, access);
entry.insert( KIO::UDSEntry::UDS_SIZE, 0LL );
goto notype;
}
sftp_attributes_free(dirent);
dirent = sb;
}
}
switch (dirent->type) {
case SSH_FILEXFER_TYPE_REGULAR: {
entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG);
break;
}
case SSH_FILEXFER_TYPE_DIRECTORY: {
entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
break;
}
case SSH_FILEXFER_TYPE_SYMLINK: {
entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFLNK);
break;
}
case SSH_FILEXFER_TYPE_SPECIAL:
case SSH_FILEXFER_TYPE_UNKNOWN: {
break;
}
}
access = dirent->permissions & 07777;
entry.insert(KIO::UDSEntry::UDS_ACCESS, access);
entry.insert(KIO::UDSEntry::UDS_SIZE, dirent->size);
notype:
if (details > 0) {
if (dirent->owner) {
entry.insert(KIO::UDSEntry::UDS_USER, QString::fromUtf8(dirent->owner));
} else {
entry.insert(KIO::UDSEntry::UDS_USER, QString::number(dirent->uid));
}
if (dirent->group) {
entry.insert(KIO::UDSEntry::UDS_GROUP, QString::fromUtf8(dirent->group));
} else {
entry.insert(KIO::UDSEntry::UDS_GROUP, QString::number(dirent->gid));
}
entry.insert(KIO::UDSEntry::UDS_ACCESS_TIME, dirent->atime);
entry.insert(KIO::UDSEntry::UDS_MODIFICATION_TIME, dirent->mtime);
entry.insert(KIO::UDSEntry::UDS_CREATION_TIME, dirent->createtime);
}
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)
{
kDebug(KIO_SFTP_DB) << "create directory: " << url;
if (!sftpLogin()) {
return;
}
if (url.path().isEmpty()) {
error(KIO::ERR_MALFORMED_URL, url.prettyUrl());
return;
}
const QString path = url.path();
const QByteArray path_c = path.toUtf8();
// Remove existing file or symlink, if requested.
if (metaData(QLatin1String("overwrite")) == QLatin1String("true")) {
kDebug(KIO_SFTP_DB) << "overwrite set, remove existing file or symlink: " << url;
sftp_unlink(mSftp, path_c.constData());
}
kDebug(KIO_SFTP_DB) << "Trying to create directory: " << path;
sftp_attributes sb = sftp_lstat(mSftp, path_c.constData());
if (sb == NULL) {
if (sftp_mkdir(mSftp, path_c.constData(), 0777) < 0) {
reportError(url, sftp_get_error(mSftp));
sftp_attributes_free(sb);
return;
} else {
kDebug(KIO_SFTP_DB) << "Successfully created directory: " << url;
if (permissions != -1) {
chmod(url, permissions);
} else {
finished();
}
sftp_attributes_free(sb);
return;
}
}
if (sb->type == SSH_FILEXFER_TYPE_DIRECTORY) {
error(KIO::ERR_DIR_ALREADY_EXIST, path);
} else {
error(KIO::ERR_FILE_ALREADY_EXIST, path);
}
sftp_attributes_free(sb);
return;
}
void sftpProtocol::rename(const KUrl& src, const KUrl& dest, KIO::JobFlags flags)
{
kDebug(KIO_SFTP_DB) << "rename " << src << " to " << dest << flags;
if (!sftpLogin()) {
return;
}
QByteArray qsrc = src.path().toUtf8();
QByteArray qdest = dest.path().toUtf8();
sftp_attributes sb = sftp_lstat(mSftp, qdest.constData());
if (sb != NULL) {
if (!(flags & KIO::Overwrite)) {
if (sb->type == SSH_FILEXFER_TYPE_DIRECTORY) {
error(KIO::ERR_DIR_ALREADY_EXIST, dest.url());
} else {
error(KIO::ERR_FILE_ALREADY_EXIST, dest.url());
}
sftp_attributes_free(sb);
return;
}
// Delete the existing destination file/dir...
if (sb->type == SSH_FILEXFER_TYPE_DIRECTORY) {
if (sftp_rmdir(mSftp, qdest.constData()) < 0) {
reportError(dest, sftp_get_error(mSftp));
return;
}
} else {
if (sftp_unlink(mSftp, qdest.constData()) < 0) {
reportError(dest, sftp_get_error(mSftp));
return;
}
}
}
sftp_attributes_free(sb);
if (sftp_rename(mSftp, qsrc.constData(), qdest.constData()) < 0) {
reportError(dest, sftp_get_error(mSftp));
return;
}
finished();
}
void sftpProtocol::symlink(const QString &target, const KUrl &dest, KIO::JobFlags flags)
{
kDebug(KIO_SFTP_DB) << "link " << target << "->" << dest
<< ", overwrite = " << (flags & KIO::Overwrite)
<< ", resume = " << (flags & KIO::Resume);
if (!sftpLogin()) {
return;
}
QByteArray t = target.toUtf8();
QByteArray d = dest.path().toUtf8();
bool failed = false;
if (sftp_symlink(mSftp, t.constData(), d.constData()) < 0) {
if (flags == KIO::Overwrite) {
sftp_attributes sb = sftp_lstat(mSftp, d.constData());
if (sb == NULL) {
failed = true;
} else {
if (sftp_unlink(mSftp, d.constData()) < 0) {
failed = true;
} else {
if (sftp_symlink(mSftp, t.constData(), d.constData()) < 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)
{
kDebug(KIO_SFTP_DB) << "change permission of " << url << " to " << QString::number(permissions);
if (!sftpLogin()) {
return;
}
QByteArray path = url.path().toUtf8();
if (sftp_chmod(mSftp, path.constData(), permissions) < 0) {
reportError(url, sftp_get_error(mSftp));
return;
}
finished();
}
void sftpProtocol::del(const KUrl &url, bool isfile)
{
kDebug(KIO_SFTP_DB) << "deleting " << (isfile ? "file: " : "directory: ") << url;
if (!sftpLogin()) {
return;
}
QByteArray path = url.path().toUtf8();
if (isfile) {
if (sftp_unlink(mSftp, path.constData()) < 0) {
reportError(url, sftp_get_error(mSftp));
return;
}
} else {
if (sftp_rmdir(mSftp, path.constData()) < 0) {
reportError(url, sftp_get_error(mSftp));
return;
}
}
finished();
}
sftpProtocol::GetRequest::GetRequest(sftp_file file, sftp_attributes sb, ushort maxPendingRequests)
: mFile(file), mSb(sb), mMaxPendingRequests(maxPendingRequests)
{
}
bool sftpProtocol::GetRequest::enqueueChunks()
{
sftpProtocol::GetRequest::Request request;
kDebug(KIO_SFTP_DB) << "enqueueChunks";
while (pendingRequests.count() < mMaxPendingRequests) {
request.expectedLength = MAX_XFER_BUF_SIZE;
request.startOffset = mFile->offset;
request.id = sftp_async_read_begin(mFile, request.expectedLength);
if (request.id < 0) {
if (pendingRequests.isEmpty()) {
return false;
} else {
break;
}
}
pendingRequests.enqueue(request);
if (mFile->offset >= mSb->size) {
// Do not add any more chunks if the offset is larger than the given file size.
// However this is done after adding a request as the remote file size may
// have changed in the meantime.
break;
}
}
kDebug(KIO_SFTP_DB) << "enqueueChunks done" << QString::number(pendingRequests.size());
return true;
}
int sftpProtocol::GetRequest::readChunks(QByteArray &data)
{
int totalRead = 0;
ssize_t bytesread = 0;
while (!pendingRequests.isEmpty()) {
sftpProtocol::GetRequest::Request &request = pendingRequests.head();
int dataSize = data.size() + request.expectedLength;
data.resize(dataSize);
if (data.size() < dataSize) {
// Could not allocate enough memory - skip current chunk
data.resize(dataSize - request.expectedLength);
break;
}
bytesread = sftp_async_read(mFile, data.data() + totalRead, request.expectedLength, request.id);
// kDebug(KIO_SFTP_DB) << "bytesread=" << QString::number(bytesread);
if (bytesread == 0 || bytesread == SSH_AGAIN) {
// Done reading or timeout
data.resize(data.size() - request.expectedLength);
if (bytesread == 0) {
pendingRequests.dequeue(); // This frees QByteArray &data!
}
break;
} else if (bytesread == SSH_ERROR) {
return -1;
}
totalRead += bytesread;
if (bytesread < request.expectedLength) {
int rc;
// If less data is read than expected - requeue the request
data.resize(data.size() - (request.expectedLength - bytesread));
// Modify current request
request.expectedLength -= bytesread;
request.startOffset += bytesread;
rc = sftp_seek64(mFile, request.startOffset);
if (rc < 0) {
// Failed to continue reading
return -1;
}
request.id = sftp_async_read_begin(mFile, request.expectedLength);
if (request.id < 0) {
// Failed to dispatch rerequest
return -1;
}
return totalRead;
}
pendingRequests.dequeue();
}
return totalRead;
}
sftpProtocol::GetRequest::~GetRequest()
{
sftpProtocol::GetRequest::Request request;
char buf[MAX_XFER_BUF_SIZE];
// Remove pending reads to avoid memory leaks
while (!pendingRequests.isEmpty()) {
request = pendingRequests.dequeue();
sftp_async_read(mFile, buf, request.expectedLength, request.id);
}
// Close channel & free attributes
sftp_close(mFile);
sftp_attributes_free(mSb);
}
void sftpProtocol::requiresUserNameRedirection()
{
KUrl redirectUrl;
redirectUrl.setProtocol( QLatin1String("sftp") );
redirectUrl.setUser( mUsername );
redirectUrl.setPass( mPassword );
redirectUrl.setHost( mHost );
if (mPort > 0 && mPort != DEFAULT_SFTP_PORT) {
redirectUrl.setPort( mPort );
}
kDebug(KIO_SFTP_DB) << "redirecting to" << redirectUrl;
redirection( redirectUrl );
}
bool sftpProtocol::sftpLogin()
{
const QString origUsername = mUsername;
openConnection();
kDebug(KIO_SFTP_DB) << "connected ?" << mConnected << "username: old=" << origUsername << "new=" << mUsername;
if (!origUsername.isEmpty() && origUsername != mUsername) {
requiresUserNameRedirection();
finished();
return false;
}
return mConnected;
}
void sftpProtocol::sftpSendWarning(int errorCode, const QString& url)
{
switch (errorCode) {
case -1: {
warning(i18n( "Could not change permissions for\n%1", url));
break;
}
default: {
break;
}
}
}
void sftpProtocol::clearPubKeyAuthInfo()
{
if (mPublicKeyAuthInfo) {
delete mPublicKeyAuthInfo;
mPublicKeyAuthInfo = 0;
}
}