diff options
Diffstat (limited to 'kioslave/sftp')
-rw-r--r-- | kioslave/sftp/AUTHORS | 3 | ||||
-rw-r--r-- | kioslave/sftp/CHANGELOG | 59 | ||||
-rw-r--r-- | kioslave/sftp/DEBUGGING | 12 | ||||
-rw-r--r-- | kioslave/sftp/Makefile.am | 25 | ||||
-rw-r--r-- | kioslave/sftp/TODO | 5 | ||||
-rw-r--r-- | kioslave/sftp/atomicio.cpp | 67 | ||||
-rw-r--r-- | kioslave/sftp/atomicio.h | 39 | ||||
-rw-r--r-- | kioslave/sftp/kio_sftp.cpp | 2286 | ||||
-rw-r--r-- | kioslave/sftp/kio_sftp.h | 149 | ||||
-rw-r--r-- | kioslave/sftp/ksshprocess.cpp | 1104 | ||||
-rw-r--r-- | kioslave/sftp/ksshprocess.h | 623 | ||||
-rw-r--r-- | kioslave/sftp/ksshprocesstest.cpp | 98 | ||||
-rw-r--r-- | kioslave/sftp/process.cpp | 493 | ||||
-rw-r--r-- | kioslave/sftp/process.h | 148 | ||||
-rw-r--r-- | kioslave/sftp/sftp.h | 91 | ||||
-rw-r--r-- | kioslave/sftp/sftp.protocol | 84 | ||||
-rw-r--r-- | kioslave/sftp/sftpfileattr.cpp | 346 | ||||
-rw-r--r-- | kioslave/sftp/sftpfileattr.h | 261 |
18 files changed, 5893 insertions, 0 deletions
diff --git a/kioslave/sftp/AUTHORS b/kioslave/sftp/AUTHORS new file mode 100644 index 000000000..c763d00bc --- /dev/null +++ b/kioslave/sftp/AUTHORS @@ -0,0 +1,3 @@ +Dawit Alemayehu <adawit@kde.org> +Lucas Fisher <ljfisher@iastate.edu> + diff --git a/kioslave/sftp/CHANGELOG b/kioslave/sftp/CHANGELOG new file mode 100644 index 000000000..b41c17019 --- /dev/null +++ b/kioslave/sftp/CHANGELOG @@ -0,0 +1,59 @@ +- add dialog to ask for username +- rename() causes SSH to die +- How to handle overwrite? +- After the user cancels with the stop button, we get ERR_CANNOT_LAUNCH_PROCESS + errors, until we kill the ioslave. Same thing after trying the wrong passwd + too many times. + This is happening because KProcess thinks that the ssh process is still running + even though it exited. +- How to handle password and caching? + - Write our own askpass program using kde + - set env SSH_ASKPASS_PROGRAM before launching + -how to do this? KProcess doesn't give us access to env variables. + - Our askpass program can probably talk to the kdesu daemon to implement caching. +- chmod() succeeds, but konqueror always puts permissions to 0 afterwards. The properties + dialog is right though. + Nevermind - ftp ioslave does this too! Maybe a bug with konqueror. +- stat does not give us group and owner names, only numbers. We could cache the uid/name and + gid/name so we can give names when doing a stat also. + +7-13-2001 - ReadLink stopped working. sftp server always retuns a file not found error + - Need to implement 64 bit file lengths-->write DataStream << for u_int64 + Still need to offer 32 bit size since this is what kde wants. ljf + - rename() isn't exactly causing ioslave to die. The stat of the file we are + going to rename is killing the slave. The slave dies in the statEntry() call. + I don't know what I am putting in the UDS entry that is causing this. ljf +7-14-2001 - got put, mimetype working ljf + - fixed readlink problem - I was sending the wrong path. doh! ljf +7-17-2001 - If the user changes the host, the slave doesn't change host! setHost() is not + called, nor is another ioslave spawned. I have not investigated the problem + yet. ljf +7-21-2001 - got slave working with kde 2.2 cvs +7-22-2001 - probable solution to getting password prompt -- open with controlling + but don't connect stdin/out to terminal. duh! +8-9-2001 - Doh! I haven't kept very good logs. Look at the cvs logs for better info. + - At this point kio_sftp is using KSshProcess which I wrote in order to make + a standard interface to the various version of ssh out there. So far it is + working fairly well. We also now report host key changes to the user and + allow them to choose whether or not to continue. This is a big improvement. + - Todo: support use of keys and ssh agent + put()'s resume functionality needs some work +1-26-2002 - Rewrote put() following the ftp::put() so it should behave the same way + - increase the size of the data packet we ask for in ::get up to 60k. + Through-put increases nicely. + - Call closeConnection() from construction. Keeps from having unused ssh + processes laying around after failed operations. +2-19-2002 - get() now emits mimetype, fixes problem with konqi not downloading file for + viewing in kpart. + - get port number using getservbyname instead of hard coding it. +2-27-2002 - testing before committing back to cvs, test with openssh 3, ssh 3 +6-?-2002 - rewrote openConnection() to using new KSshProcess connect proceedures +7-20-2002 - Don't put up a message box when auth fails because of now or changed key, + the call to error() will put up the dialog. + - Connect fails and no more password are prompted for when we get + ERR_AUTH_FAILED from KSshProcess. +9-15-2002 - stuff +9-29-2002 - the last i18n string updates, fixed problem with uploading files to + openssh server. +5-8-2003 - check whether operation types are supported by the negotiated sftp + protocol version diff --git a/kioslave/sftp/DEBUGGING b/kioslave/sftp/DEBUGGING new file mode 100644 index 000000000..89ae8fe18 --- /dev/null +++ b/kioslave/sftp/DEBUGGING @@ -0,0 +1,12 @@ +DEBUGGING + +The best way to debug this slave is to send debug info to a +file using 'kdebugDialog --fullmode'. Then you can 'tail -f' the file to +see debug messages in real-time. + +I also suggest getting the openssh source and recompiling sftp-server to +send messages to the auth log. This can be done in sftp-server.c be defining +DEBUG_SFTP_SERVER. + +You can do the same with the ssh client by finding the two calls to log_init() +in ssh.c and changing the last argument from 1 to 0 and recompiling. diff --git a/kioslave/sftp/Makefile.am b/kioslave/sftp/Makefile.am new file mode 100644 index 000000000..24ebc5aef --- /dev/null +++ b/kioslave/sftp/Makefile.am @@ -0,0 +1,25 @@ +## Makefile.am of kdebase/kioslave/sftp + +INCLUDES = $(all_includes) +AM_LDFLAGS = $(all_libraries) $(KDE_RPATH) +METASOURCES = AUTO + +####### Files + +check_PROGRAMS = ksshprocesstest + +ksshprocesstest_SOURCES = ksshprocesstest.cpp +ksshprocesstest_LDADD = $(LIB_KSYCOCA) ksshprocess.lo process.lo atomicio.lo + +kde_module_LTLIBRARIES = kio_sftp.la + +kio_sftp_la_SOURCES = process.cpp atomicio.cpp kio_sftp.cpp sftpfileattr.cpp ksshprocess.cpp +kio_sftp_la_LIBADD = $(LIB_KIO) +kio_sftp_la_LDFLAGS = $(all_libraries) -module $(KDE_PLUGIN) +noinst_HEADERS = atomicio.h kio_sftp.h ksshprocess.h process.h sftpfileattr.h sftp.h + +kdelnk_DATA = sftp.protocol +kdelnkdir = $(kde_servicesdir) + +messages: + $(XGETTEXT) *.cpp -o $(podir)/kio_sftp.pot diff --git a/kioslave/sftp/TODO b/kioslave/sftp/TODO new file mode 100644 index 000000000..0f1411317 --- /dev/null +++ b/kioslave/sftp/TODO @@ -0,0 +1,5 @@ +TODO: +===== + +- Support for use of public keys, maybe ssh-agent, a key management app, etc. + diff --git a/kioslave/sftp/atomicio.cpp b/kioslave/sftp/atomicio.cpp new file mode 100644 index 000000000..057f20fe9 --- /dev/null +++ b/kioslave/sftp/atomicio.cpp @@ -0,0 +1,67 @@ +/* + * Copyright (c) 1995,1999 Theo de Raadt. All rights reserved. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +//#include "includes.h" +//RCSID("$OpenBSD: atomicio.c,v 1.9 2001/03/02 18:54:30 deraadt Exp $"); + +//#include "xmalloc.h" +#include "atomicio.h" +#include <unistd.h> +#include <errno.h> +#include <kdebug.h> + +/* + * ensure all of data on socket comes through. f==read || f==write + */ + +ssize_t atomicio(int fd, char *_s, size_t n, bool read) +{ + char *s = _s; + ssize_t res; + ssize_t pos = 0; + + while (n > pos) { + if( read) + res = ::read(fd, s + pos, n - pos); + else + res = ::write(fd, s + pos, n - pos); + + switch (res) { + case -1: + kdDebug() << "atomicio(): errno=" << errno << endl; +#ifdef EWOULDBLOCK + if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) +#else + if (errno == EINTR || errno == EAGAIN) +#endif + continue; + case 0: + return (res); + default: + pos += res; + } + } + return (pos); +} diff --git a/kioslave/sftp/atomicio.h b/kioslave/sftp/atomicio.h new file mode 100644 index 000000000..4468757d5 --- /dev/null +++ b/kioslave/sftp/atomicio.h @@ -0,0 +1,39 @@ +#ifndef atomicio_h +#define atomicio_h + +/* $OpenBSD: atomicio.h,v 1.3 2001/03/02 18:54:30 deraadt Exp $ */ + +/* + * Copyright (c) 1995,1999 Theo de Raadt. All rights reserved. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <sys/types.h> +#include <unistd.h> + +/* + * Ensure all of data on socket comes through. f==read || f==write + */ +ssize_t atomicio(int fd, char *_s, size_t n, bool read = true); + +#endif diff --git a/kioslave/sftp/kio_sftp.cpp b/kioslave/sftp/kio_sftp.cpp new file mode 100644 index 000000000..e6aaaf532 --- /dev/null +++ b/kioslave/sftp/kio_sftp.cpp @@ -0,0 +1,2286 @@ +/*************************************************************************** + sftp.cpp - description + ------------------- + begin : Fri Jun 29 23:45:40 CDT 2001 + copyright : (C) 2001 by Lucas Fisher + email : ljfisher@purdue.edu + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +/* +DEBUGGING +We are pretty much left with kdDebug messages for debugging. We can't use a gdb +as described in the ioslave DEBUG.howto because kdeinit has to run in a terminal. +Ssh will detect this terminal and ask for a password there, but will just get garbage. +So we can't connect. +*/ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <fcntl.h> + +#include <qcstring.h> +#include <qstring.h> +#include <qobject.h> +#include <qstrlist.h> +#include <qfile.h> +#include <qbuffer.h> + +#include <stdlib.h> +#include <unistd.h> +#include <signal.h> +#include <errno.h> +#include <ctype.h> +#include <time.h> +#include <netdb.h> +#include <string.h> + +#include <netinet/in.h> +#include <arpa/inet.h> +#include <sys/time.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/wait.h> + +#include <kapplication.h> +#include <kuser.h> +#include <kdebug.h> +#include <kmessagebox.h> +#include <kinstance.h> +#include <kglobal.h> +#include <kstandarddirs.h> +#include <klocale.h> +#include <kurl.h> +#include <kio/ioslave_defaults.h> +#include <kmimetype.h> +#include <kmimemagic.h> +#include <klargefile.h> +#include <kremoteencoding.h> + +#include "sftp.h" +#include "kio_sftp.h" +#include "atomicio.h" +#include "sftpfileattr.h" +#include "ksshprocess.h" + + +using namespace KIO; +extern "C" +{ + int KDE_EXPORT kdemain( int argc, char **argv ) + { + KInstance instance( "kio_sftp" ); + + kdDebug(KIO_SFTP_DB) << "*** Starting kio_sftp " << endl; + + if (argc != 4) { + kdDebug(KIO_SFTP_DB) << "Usage: kio_sftp protocol domain-socket1 domain-socket2" << endl; + exit(-1); + } + + sftpProtocol slave(argv[2], argv[3]); + slave.dispatchLoop(); + + kdDebug(KIO_SFTP_DB) << "*** kio_sftp Done" << endl; + return 0; + } +} + + +/* + * This helper handles some special issues (blocking and interrupted + * system call) when writing to a file handle. + * + * @return 0 on success or an error code on failure (ERR_COULD_NOT_WRITE, + * ERR_DISK_FULL, ERR_CONNECTION_BROKEN). + */ +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: + continue; + case EPIPE: + return ERR_CONNECTION_BROKEN; + case ENOSPC: + return ERR_DISK_FULL; + default: + return ERR_COULD_NOT_WRITE; + } + } + return 0; +} + +sftpProtocol::sftpProtocol(const QCString &pool_socket, const QCString &app_socket) + : SlaveBase("kio_sftp", pool_socket, app_socket), + mConnected(false), mPort(-1), mMsgId(0) { + kdDebug(KIO_SFTP_DB) << "sftpProtocol(): pid = " << getpid() << endl; +} + + +sftpProtocol::~sftpProtocol() { + kdDebug(KIO_SFTP_DB) << "~sftpProtocol(): pid = " << getpid() << endl; + closeConnection(); +} + +/** + * Type is a sftp packet type found in .sftp.h'. + * Example: SSH2_FXP_READLINK, SSH2_FXP_RENAME, etc. + * + * Returns true if the type is supported by the sftp protocol + * version negotiated by the client and server (sftpVersion). + */ +bool sftpProtocol::isSupportedOperation(int type) { + switch (type) { + case SSH2_FXP_VERSION: + case SSH2_FXP_STATUS: + case SSH2_FXP_HANDLE: + case SSH2_FXP_DATA: + case SSH2_FXP_NAME: + case SSH2_FXP_ATTRS: + case SSH2_FXP_INIT: + case SSH2_FXP_OPEN: + case SSH2_FXP_CLOSE: + case SSH2_FXP_READ: + case SSH2_FXP_WRITE: + case SSH2_FXP_LSTAT: + case SSH2_FXP_FSTAT: + case SSH2_FXP_SETSTAT: + case SSH2_FXP_FSETSTAT: + case SSH2_FXP_OPENDIR: + case SSH2_FXP_READDIR: + case SSH2_FXP_REMOVE: + case SSH2_FXP_MKDIR: + case SSH2_FXP_RMDIR: + case SSH2_FXP_REALPATH: + case SSH2_FXP_STAT: + return true; + case SSH2_FXP_RENAME: + return sftpVersion >= 2 ? true : false; + case SSH2_FXP_EXTENDED: + case SSH2_FXP_EXTENDED_REPLY: + case SSH2_FXP_READLINK: + case SSH2_FXP_SYMLINK: + return sftpVersion >= 3 ? true : false; + default: + kdDebug(KIO_SFTP_DB) << "isSupportedOperation(type:" + << type << "): unrecognized operation type" << endl; + break; + } + + return false; +} + +void sftpProtocol::copy(const KURL &src, const KURL &dest, int permissions, bool overwrite) +{ + kdDebug(KIO_SFTP_DB) << "copy(): " << src << " -> " << dest << endl; + + bool srcLocal = src.isLocalFile(); + bool destLocal = dest.isLocalFile(); + + if ( srcLocal && !destLocal ) // Copy file -> sftp + sftpCopyPut(src, dest, permissions, overwrite); + else if ( destLocal && !srcLocal ) // Copy sftp -> file + sftpCopyGet(dest, src, permissions, overwrite); + else + error(ERR_UNSUPPORTED_ACTION, QString::null); +} + +void sftpProtocol::sftpCopyGet(const KURL& dest, const KURL& src, int mode, bool overwrite) +{ + kdDebug(KIO_SFTP_DB) << "sftpCopyGet(): " << src << " -> " << dest << endl; + + // Attempt to establish a connection... + openConnection(); + if( !mConnected ) + return; + + KDE_struct_stat buff_orig; + QCString dest_orig ( QFile::encodeName(dest.path()) ); + bool origExists = (KDE_lstat( dest_orig.data(), &buff_orig ) != -1); + + if (origExists) + { + if (S_ISDIR(buff_orig.st_mode)) + { + error(ERR_IS_DIRECTORY, dest.prettyURL()); + return; + } + + if (!overwrite) + { + error(ERR_FILE_ALREADY_EXIST, dest.prettyURL()); + return; + } + } + + KIO::filesize_t offset = 0; + QCString dest_part ( dest_orig + ".part" ); + + int fd = -1; + bool partExists = false; + bool markPartial = config()->readBoolEntry("MarkPartial", true); + + if (markPartial) + { + KDE_struct_stat buff_part; + partExists = (KDE_stat( dest_part.data(), &buff_part ) != -1); + + if (partExists && buff_part.st_size > 0 && S_ISREG(buff_part.st_mode)) + { + if (canResume( buff_part.st_size )) + { + offset = buff_part.st_size; + kdDebug(KIO_SFTP_DB) << "sftpCopyGet: Resuming @ " << offset << endl; + } + } + + if (offset > 0) + { + fd = KDE_open(dest_part.data(), O_RDWR); + offset = KDE_lseek(fd, 0, SEEK_END); + if (offset == 0) + { + error(ERR_CANNOT_RESUME, dest.prettyURL()); + return; + } + } + else + { + // Set up permissions properly, based on what is done in file io-slave + int openFlags = (O_CREAT | O_TRUNC | O_WRONLY); + int initialMode = (mode == -1) ? 0666 : (mode | S_IWUSR); + fd = KDE_open(dest_part.data(), openFlags, initialMode); + } + } + else + { + // Set up permissions properly, based on what is done in file io-slave + int openFlags = (O_CREAT | O_TRUNC | O_WRONLY); + int initialMode = (mode == -1) ? 0666 : (mode | S_IWUSR); + fd = KDE_open(dest_orig.data(), openFlags, initialMode); + } + + if(fd == -1) + { + kdDebug(KIO_SFTP_DB) << "sftpCopyGet: Unable to open (" << fd << ") for writting." << endl; + if (errno == EACCES) + error (ERR_WRITE_ACCESS_DENIED, dest.prettyURL()); + else + error (ERR_CANNOT_OPEN_FOR_WRITING, dest.prettyURL()); + return; + } + + Status info = sftpGet(src, offset, fd); + if ( info.code != 0 ) + { + // Should we keep the partially downloaded file ?? + KIO::filesize_t size = config()->readNumEntry("MinimumKeepSize", DEFAULT_MINIMUM_KEEP_SIZE); + if (info.size < size) + ::remove(dest_part.data()); + + error(info.code, info.text); + return; + } + + if (::close(fd) != 0) + { + error(ERR_COULD_NOT_WRITE, dest.prettyURL()); + return; + } + + // + if (markPartial) + { + if (::rename(dest_part.data(), dest_orig.data()) != 0) + { + error (ERR_CANNOT_RENAME_PARTIAL, dest_part); + return; + } + } + + data(QByteArray()); + kdDebug(KIO_SFTP_DB) << "sftpCopyGet(): emit finished()" << endl; + finished(); +} + +sftpProtocol::Status sftpProtocol::sftpGet( const KURL& src, KIO::filesize_t offset, int fd ) +{ + int code; + sftpFileAttr attr(remoteEncoding()); + + Status res; + res.code = 0; + res.size = 0; + + kdDebug(KIO_SFTP_DB) << "sftpGet(): " << src << endl; + + // stat the file first to get its size + if( (code = sftpStat(src, attr)) != SSH2_FX_OK ) { + return doProcessStatus(code, src.prettyURL()); + } + + // We cannot get file if it is a directory + if( attr.fileType() == S_IFDIR ) { + res.text = src.prettyURL(); + res.code = ERR_IS_DIRECTORY; + return res; + } + + KIO::filesize_t fileSize = attr.fileSize(); + Q_UINT32 pflags = SSH2_FXF_READ; + attr.clear(); + + QByteArray handle; + if( (code = sftpOpen(src, pflags, attr, handle)) != SSH2_FX_OK ) { + res.text = src.prettyURL(); + res.code = ERR_CANNOT_OPEN_FOR_READING; + return res; + } + + // needed for determining mimetype + // note: have to emit mimetype before emitting totalsize. + QByteArray buff; + QByteArray mimeBuffer; + + unsigned int oldSize; + bool foundMimetype = false; + + // 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. + Q_UINT32 len = 60*1024; + code = SSH2_FX_OK; + + kdDebug(KIO_SFTP_DB) << "sftpGet(): offset = " << offset << endl; + while( code == SSH2_FX_OK ) { + if( (code = sftpRead(handle, offset, len, buff)) == SSH2_FX_OK ) { + offset += buff.size(); + + // save data for mimetype. Pretty much follows what is in the ftp ioslave + if( !foundMimetype ) { + oldSize = mimeBuffer.size(); + mimeBuffer.resize(oldSize + buff.size()); + memcpy(mimeBuffer.data()+oldSize, buff.data(), buff.size()); + + if( mimeBuffer.size() > 1024 || offset == fileSize ) { + // determine mimetype + KMimeMagicResult* result = + KMimeMagic::self()->findBufferFileType(mimeBuffer, src.fileName()); + kdDebug(KIO_SFTP_DB) << "sftpGet(): mimetype is " << + result->mimeType() << endl; + mimeType(result->mimeType()); + + // Always send the total size after emitting mime-type... + totalSize(fileSize); + + if (fd == -1) + data(mimeBuffer); + else + { + if ( (res.code=writeToFile(fd, mimeBuffer.data(), mimeBuffer.size())) != 0 ) + return res; + } + + processedSize(mimeBuffer.size()); + mimeBuffer.resize(0); + foundMimetype = true; + } + } + else { + if (fd == -1) + data(buff); + else + { + if ( (res.code= writeToFile(fd, buff.data(), buff.size())) != 0 ) + return res; + } + processedSize(offset); + } + } + + /* + Check if slave was killed. According to slavebase.h we need to leave + the slave methods as soon as possible if the slave is killed. This + allows the slave to be cleaned up properly. + */ + if( wasKilled() ) { + res.text = i18n("An internal error occurred. Please retry the request again."); + res.code = ERR_UNKNOWN; + return res; + } + } + + if( code != SSH2_FX_EOF ) { + res.text = src.prettyURL(); + res.code = ERR_COULD_NOT_READ; // return here or still send empty array to indicate end of read? + } + + res.size = offset; + sftpClose(handle); + processedSize (offset); + return res; +} + +void sftpProtocol::get(const KURL& url) { + kdDebug(KIO_SFTP_DB) << "get(): " << url << endl ; + + openConnection(); + if( !mConnected ) + return; + + // Get resume offset + Q_UINT64 offset = config()->readUnsignedLongNumEntry("resume"); + if( offset > 0 ) { + canResume(); + kdDebug(KIO_SFTP_DB) << "get(): canResume(), offset = " << offset << endl; + } + + Status info = sftpGet(url, offset); + + if (info.code != 0) + { + error(info.code, info.text); + return; + } + + data(QByteArray()); + kdDebug(KIO_SFTP_DB) << "get(): emit finished()" << endl; + finished(); +} + + +void sftpProtocol::setHost (const QString& h, int port, const QString& user, const QString& pass) +{ + kdDebug(KIO_SFTP_DB) << "setHost(): " << user << "@" << h << ":" << port << endl; + + if( mHost != h || mPort != port || user != mUsername || mPassword != pass ) + 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); + } + + mUsername = user; + mPassword = pass; + + if (user.isEmpty()) + { + KUser u; + mUsername = u.loginName(); + } +} + + +void sftpProtocol::openConnection() { + + if(mConnected) + return; + + kdDebug(KIO_SFTP_DB) << "openConnection(): " << mUsername << "@" + << mHost << ":" << mPort << endl; + + infoMessage( i18n("Opening SFTP connection to host <b>%1:%2</b>").arg(mHost).arg(mPort)); + + if( mHost.isEmpty() ) { + kdDebug(KIO_SFTP_DB) << "openConnection(): Need hostname..." << endl; + error(ERR_UNKNOWN_HOST, i18n("No hostname specified")); + return; + } + + //////////////////////////////////////////////////////////////////////////// + // Setup AuthInfo for use with password caching and the + // password dialog box. + AuthInfo info; + info.url.setProtocol("sftp"); + info.url.setHost(mHost); + info.url.setPort(mPort); + info.url.setUser(mUsername); + info.caption = i18n("SFTP Login"); + info.comment = "sftp://" + mHost + ":" + QString::number(mPort); + info.commentLabel = i18n("site:"); + info.username = mUsername; + info.keepPassword = true; + + /////////////////////////////////////////////////////////////////////////// + // Check for cached authentication info if a username AND password were + // not specified in setHost(). + if( mUsername.isEmpty() && mPassword.isEmpty() ) { + kdDebug(KIO_SFTP_DB) << "openConnection(): checking cache " + << "info.username = " << info.username + << ", info.url = " << info.url.prettyURL() << endl; + + if( checkCachedAuthentication(info) ) { + mUsername = info.username; + mPassword = info.password; + } + } + + /////////////////////////////////////////////////////////////////////////// + // Now setup our ssh options. If we found a cached username + // and password we set the SSH_PASSWORD and SSH_USERNAME + // options right away. Otherwise we wait. The other options are + // necessary for running sftp over ssh. + KSshProcess::SshOpt opt; // a ssh option, this can be reused + KSshProcess::SshOptList opts; // list of SshOpts + KSshProcess::SshOptListIterator passwdIt; // points to the opt in opts that specifies the password + KSshProcess::SshOptListIterator usernameIt; + +// opt.opt = KSshProcess::SSH_VERBOSE; +// opts.append(opt); +// opts.append(opt); + + if( mPort != -1 ) { + opt.opt = KSshProcess::SSH_PORT; + opt.num = mPort; + opts.append(opt); + } + + opt.opt = KSshProcess::SSH_SUBSYSTEM; + opt.str = "sftp"; + opts.append(opt); + + opt.opt = KSshProcess::SSH_FORWARDX11; + opt.boolean = false; + opts.append(opt); + + opt.opt = KSshProcess::SSH_FORWARDAGENT; + opt.boolean = false; + opts.append(opt); + + opt.opt = KSshProcess::SSH_PROTOCOL; + opt.num = 2; + opts.append(opt); + + opt.opt = KSshProcess::SSH_HOST; + opt.str = mHost; + opts.append(opt); + + opt.opt = KSshProcess::SSH_ESCAPE_CHAR; + opt.num = -1; // don't use any escape character + opts.append(opt); + + // set the username and password if we have them + if( !mUsername.isEmpty() ) { + opt.opt = KSshProcess::SSH_USERNAME; + opt.str = mUsername; + usernameIt = opts.append(opt); + } + + if( !mPassword.isEmpty() ) { + opt.opt = KSshProcess::SSH_PASSWD; + opt.str = mPassword; + passwdIt = opts.append(opt); + } + + ssh.setOptions(opts); + ssh.printArgs(); + + /////////////////////////////////////////////////////////////////////////// + // Start the ssh connection process. + // + + int err; // error code from KSshProcess + QString msg; // msg for dialog box + QString caption; // dialog box caption + bool firstTime = true; + bool dlgResult; + + while( !(mConnected = ssh.connect()) ) { + err = ssh.error(); + kdDebug(KIO_SFTP_DB) << "openConnection(): " + "Got " << err << " from KSshProcess::connect()" << endl; + + switch(err) { + case KSshProcess::ERR_NEED_PASSWD: + case KSshProcess::ERR_NEED_PASSPHRASE: + // At this point we know that either we didn't set + // an username or password in the ssh options list, + // or what we did pass did not work. Therefore we + // must prompt the user. + if( err == KSshProcess::ERR_NEED_PASSPHRASE ) + info.prompt = i18n("Please enter your username and key passphrase."); + else + info.prompt = i18n("Please enter your username and password."); + + kdDebug(KIO_SFTP_DB) << "openConnection(): info.username = " << info.username + << ", info.url = " << info.url.prettyURL() << endl; + + if( firstTime ) + dlgResult = openPassDlg(info); + else + dlgResult = openPassDlg(info, i18n("Incorrect username or password")); + + if( dlgResult ) { + if( info.username.isEmpty() || info.password.isEmpty() ) { + error(ERR_COULD_NOT_AUTHENTICATE, + i18n("Please enter a username and password")); + continue; + } + } + else { + // user canceled or dialog failed to open + error(ERR_USER_CANCELED, QString::null); + kdDebug(KIO_SFTP_DB) << "openConnection(): user canceled, dlgResult = " << dlgResult << endl; + closeConnection(); + return; + } + + firstTime = false; + + // Check if the username has changed. SSH only accepts + // the username at startup. If the username has changed + // we must disconnect ssh, change the SSH_USERNAME + // option, and reset the option list. We will also set + // the password option so the user is not prompted for + // it again. + if( mUsername != info.username ) { + kdDebug(KIO_SFTP_DB) << "openConnection(): Username changed from " + << mUsername << " to " << info.username << endl; + + ssh.disconnect(); + + // if we haven't yet added the username + // or password option to the ssh options list then + // the iterators will be equal to the empty iterator. + // Create the opts now and add them to the opt list. + if( usernameIt == KSshProcess::SshOptListIterator() ) { + kdDebug(KIO_SFTP_DB) << "openConnection(): " + "Adding username to options list" << endl; + opt.opt = KSshProcess::SSH_USERNAME; + usernameIt = opts.append(opt); + } + + if( passwdIt == KSshProcess::SshOptListIterator() ) { + kdDebug(KIO_SFTP_DB) << "openConnection(): " + "Adding password to options list" << endl; + opt.opt = KSshProcess::SSH_PASSWD; + passwdIt = opts.append(opt); + } + + (*usernameIt).str = info.username; + (*passwdIt).str = info.password; + ssh.setOptions(opts); + ssh.printArgs(); + } + else { // just set the password + ssh.setPassword(info.password); + } + + mUsername = info.username; + mPassword = info.password; + + break; + + case KSshProcess::ERR_NEW_HOST_KEY: + caption = i18n("Warning: Cannot verify host's identity."); + msg = ssh.errorMsg(); + if( KMessageBox::Yes != messageBox(WarningYesNo, msg, caption) ) { + closeConnection(); + error(ERR_USER_CANCELED, QString::null); + return; + } + ssh.acceptHostKey(true); + break; + + case KSshProcess::ERR_DIFF_HOST_KEY: + caption = i18n("Warning: Host's identity changed."); + msg = ssh.errorMsg(); + if( KMessageBox::Yes != messageBox(WarningYesNo, msg, caption) ) { + closeConnection(); + error(ERR_USER_CANCELED, QString::null); + return; + } + ssh.acceptHostKey(true); + break; + + case KSshProcess::ERR_AUTH_FAILED: + infoMessage(i18n("Authentication failed.")); + error(ERR_COULD_NOT_LOGIN, i18n("Authentication failed.")); + return; + + case KSshProcess::ERR_AUTH_FAILED_NEW_KEY: + msg = ssh.errorMsg(); + error(ERR_COULD_NOT_LOGIN, msg); + return; + + case KSshProcess::ERR_AUTH_FAILED_DIFF_KEY: + msg = ssh.errorMsg(); + error(ERR_COULD_NOT_LOGIN, msg); + return; + + case KSshProcess::ERR_CLOSED_BY_REMOTE_HOST: + infoMessage(i18n("Connection failed.")); + caption = i18n("Connection closed by remote host."); + msg = ssh.errorMsg(); + messageBox(Information, msg, caption); + closeConnection(); + error(ERR_COULD_NOT_LOGIN, msg); + return; + + case KSshProcess::ERR_INTERACT: + case KSshProcess::ERR_INTERNAL: + case KSshProcess::ERR_UNKNOWN: + case KSshProcess::ERR_INVALID_STATE: + case KSshProcess::ERR_CANNOT_LAUNCH: + case KSshProcess::ERR_HOST_KEY_REJECTED: + default: + infoMessage(i18n("Connection failed.")); + caption = i18n("Unexpected SFTP error: %1").arg(err); + msg = ssh.errorMsg(); + messageBox(Information, msg, caption); + closeConnection(); + error(ERR_UNKNOWN, msg); + return; + } + } + + // catch all in case we did something wrong above + if( !mConnected ) { + error(ERR_INTERNAL, QString::null); + return; + } + + // Now send init packet. + kdDebug(KIO_SFTP_DB) << "openConnection(): Sending SSH2_FXP_INIT packet." << endl; + QByteArray p; + QDataStream packet(p, IO_WriteOnly); + packet << (Q_UINT32)5; // packet length + packet << (Q_UINT8) SSH2_FXP_INIT; // packet type + packet << (Q_UINT32)SSH2_FILEXFER_VERSION; // client version + + putPacket(p); + getPacket(p); + + QDataStream s(p, IO_ReadOnly); + Q_UINT32 version; + Q_UINT8 type; + s >> type; + kdDebug(KIO_SFTP_DB) << "openConnection(): Got type " << type << endl; + + if( type == SSH2_FXP_VERSION ) { + s >> version; + kdDebug(KIO_SFTP_DB) << "openConnection(): Got server version " << version << endl; + + // XXX Get extensions here + sftpVersion = version; + + /* Server should return lowest common version supported by + * client and server, but double check just in case. + */ + if( sftpVersion > SSH2_FILEXFER_VERSION ) { + error(ERR_UNSUPPORTED_PROTOCOL, + i18n("SFTP version %1").arg(version)); + closeConnection(); + return; + } + } + else { + error(ERR_UNKNOWN, i18n("Protocol error.")); + closeConnection(); + return; + } + + // Login succeeded! + infoMessage(i18n("Successfully connected to %1").arg(mHost)); + info.url.setProtocol("sftp"); + info.url.setHost(mHost); + info.url.setPort(mPort); + info.url.setUser(mUsername); + info.username = mUsername; + info.password = mPassword; + kdDebug(KIO_SFTP_DB) << "sftpProtocol(): caching info.username = " << info.username << + ", info.url = " << info.url.prettyURL() << endl; + cacheAuthentication(info); + mConnected = true; + connected(); + + mPassword.fill('x'); + info.password.fill('x'); + + return; +} + +void sftpProtocol::closeConnection() { + kdDebug(KIO_SFTP_DB) << "closeConnection()" << endl; + ssh.disconnect(); + mConnected = false; +} + +void sftpProtocol::sftpCopyPut(const KURL& src, const KURL& dest, int permissions, bool overwrite) { + + KDE_struct_stat buff; + QCString file (QFile::encodeName(src.path())); + + if (KDE_lstat(file.data(), &buff) == -1) { + error (ERR_DOES_NOT_EXIST, src.prettyURL()); + return; + } + + if (S_ISDIR (buff.st_mode)) { + error (ERR_IS_DIRECTORY, src.prettyURL()); + return; + } + + int fd = KDE_open (file.data(), O_RDONLY); + if (fd == -1) { + error (ERR_CANNOT_OPEN_FOR_READING, src.prettyURL()); + return; + } + + totalSize (buff.st_size); + + sftpPut (dest, permissions, false, overwrite, fd); + + // Close the file descriptor... + ::close( fd ); +} + +void sftpProtocol::sftpPut( const KURL& dest, int permissions, bool resume, bool overwrite, int fd ) { + + openConnection(); + if( !mConnected ) + return; + + kdDebug(KIO_SFTP_DB) << "sftpPut(): " << dest + << ", resume=" << resume + << ", overwrite=" << overwrite << endl; + + KURL origUrl( dest ); + sftpFileAttr origAttr(remoteEncoding()); + bool origExists = false; + + // Stat original (without part ext) to see if it already exists + int code = sftpStat(origUrl, origAttr); + + if( code == SSH2_FX_OK ) { + kdDebug(KIO_SFTP_DB) << "sftpPut(): <file> already exists" << endl; + + // Delete remote file if its size is zero + if( origAttr.fileSize() == 0 ) { + if( sftpRemove(origUrl, true) != SSH2_FX_OK ) { + error(ERR_CANNOT_DELETE_ORIGINAL, origUrl.prettyURL()); + return; + } + } + else { + origExists = true; + } + } + else if( code != SSH2_FX_NO_SUCH_FILE ) { + processStatus(code, origUrl.prettyURL()); + return; + } + + // Do not waste time/resources with more remote stat calls if the file exists + // and we weren't instructed to overwrite it... + if( origExists && !overwrite ) { + error(ERR_FILE_ALREADY_EXIST, origUrl.prettyURL()); + return; + } + + // Stat file with part ext to see if it already exists... + KURL partUrl( origUrl ); + partUrl.setFileName( partUrl.fileName() + ".part" ); + + Q_UINT64 offset = 0; + bool partExists = false; + bool markPartial = config()->readBoolEntry("MarkPartial", true); + + if( markPartial ) { + + sftpFileAttr partAttr(remoteEncoding()); + code = sftpStat(partUrl, partAttr); + + if( code == SSH2_FX_OK ) { + kdDebug(KIO_SFTP_DB) << "sftpPut(): .part file already exists" << endl; + partExists = true; + offset = partAttr.fileSize(); + + // If for some reason, both the original and partial files exist, + // skip resumption just like we would if the size of the partial + // file is zero... + if( origExists || offset == 0 ) + { + if( sftpRemove(partUrl, true) != SSH2_FX_OK ) { + error(ERR_CANNOT_DELETE_PARTIAL, partUrl.prettyURL()); + return; + } + + if( sftpRename(origUrl, partUrl) != SSH2_FX_OK ) { + error(ERR_CANNOT_RENAME_ORIGINAL, origUrl.prettyURL()); + return; + } + + offset = 0; + } + else if( !overwrite && !resume ) { + if (fd != -1) + resume = (KDE_lseek(fd, offset, SEEK_SET) != -1); + else + resume = canResume( offset ); + + kdDebug(KIO_SFTP_DB) << "sftpPut(): can resume = " << resume + << ", offset = " << offset; + + if( !resume ) { + error(ERR_FILE_ALREADY_EXIST, partUrl.prettyURL()); + return; + } + } + else { + offset = 0; + } + } + else if( code == SSH2_FX_NO_SUCH_FILE ) { + if( origExists && sftpRename(origUrl, partUrl) != SSH2_FX_OK ) { + error(ERR_CANNOT_RENAME_ORIGINAL, origUrl.prettyURL()); + return; + } + } + else { + processStatus(code, partUrl.prettyURL()); + return; + } + } + + // Determine the url we will actually write to... + KURL writeUrl (markPartial ? partUrl:origUrl); + + Q_UINT32 pflags = 0; + if( overwrite && !resume ) + pflags = SSH2_FXF_WRITE | SSH2_FXF_CREAT | SSH2_FXF_TRUNC; + else if( !overwrite && !resume ) + pflags = SSH2_FXF_WRITE | SSH2_FXF_CREAT | SSH2_FXF_EXCL; + else if( overwrite && resume ) + pflags = SSH2_FXF_WRITE | SSH2_FXF_CREAT; + else if( !overwrite && resume ) + pflags = SSH2_FXF_WRITE | SSH2_FXF_CREAT | SSH2_FXF_APPEND; + + sftpFileAttr attr(remoteEncoding()); + QByteArray handle; + + // Set the permissions of the file we write to if it didn't already exist + // and the permission info is supplied, i.e it is not -1 + if( !partExists && !origExists && permissions != -1) + attr.setPermissions(permissions); + + code = sftpOpen( writeUrl, pflags, attr, handle ); + if( code != SSH2_FX_OK ) { + + // Rename the file back to its original name if a + // put fails due to permissions problems... + if( markPartial && overwrite ) { + (void) sftpRename(partUrl, origUrl); + writeUrl = origUrl; + } + + if( code == SSH2_FX_FAILURE ) { // assume failure means file exists + error(ERR_FILE_ALREADY_EXIST, writeUrl.prettyURL()); + return; + } + else { + processStatus(code, writeUrl.prettyURL()); + return; + } + } + + long nbytes; + QByteArray buff; + + do { + + if( fd != -1 ) { + buff.resize( 16*1024 ); + if ( (nbytes = ::read(fd, buff.data(), buff.size())) > -1 ) + buff.resize( nbytes ); + } + else { + dataReq(); + nbytes = readData( buff ); + } + + if( nbytes >= 0 ) { + if( (code = sftpWrite(handle, offset, buff)) != SSH2_FX_OK ) { + error(ERR_COULD_NOT_WRITE, dest.prettyURL()); + return; + } + + offset += nbytes; + processedSize(offset); + + /* Check if slave was killed. According to slavebase.h we + * need to leave the slave methods as soon as possible if + * the slave is killed. This allows the slave to be cleaned + * up properly. + */ + if( wasKilled() ) { + sftpClose(handle); + closeConnection(); + error(ERR_UNKNOWN, i18n("An internal error occurred. Please try again.")); + return; + } + } + + } while( nbytes > 0 ); + + if( nbytes < 0 ) { + sftpClose(handle); + + if( markPartial ) { + // Remove remote file if it smaller than our keep size + uint minKeepSize = config()->readNumEntry("MinimumKeepSize", DEFAULT_MINIMUM_KEEP_SIZE); + + if( sftpStat(writeUrl, attr) == SSH2_FX_OK ) { + if( attr.fileSize() < minKeepSize ) { + sftpRemove(writeUrl, true); + } + } + } + + error( ERR_UNKNOWN, i18n("Unknown error was encountered while copying the file " + "to '%1'. Please try again.").arg(dest.host()) ); + return; + } + + if( (code = sftpClose(handle)) != SSH2_FX_OK ) { + error(ERR_COULD_NOT_WRITE, writeUrl.prettyURL()); + return; + } + + // If wrote to a partial file, then remove the part ext + if( markPartial ) { + if( sftpRename(partUrl, origUrl) != SSH2_FX_OK ) { + error(ERR_CANNOT_RENAME_PARTIAL, origUrl.prettyURL()); + return; + } + } + + finished(); +} + +void sftpProtocol::put ( const KURL& url, int permissions, bool overwrite, bool resume ){ + kdDebug(KIO_SFTP_DB) << "put(): " << url << ", overwrite = " << overwrite + << ", resume = " << resume << endl; + + sftpPut( url, permissions, resume, overwrite ); +} + +void sftpProtocol::stat ( const KURL& url ){ + kdDebug(KIO_SFTP_DB) << "stat(): " << url << endl; + + openConnection(); + if( !mConnected ) + return; + + // If the stat URL has no path, do not attempt to determine the real + // path and do a redirect. KRun will simply ignore such requests. + // Instead, simply return the mime-type as a directory... + if( !url.hasPath() ) { + UDSEntry entry; + UDSAtom atom; + + atom.m_uds = KIO::UDS_NAME; + atom.m_str = QString::null; + entry.append( atom ); + + atom.m_uds = KIO::UDS_FILE_TYPE; + atom.m_long = S_IFDIR; + entry.append( atom ); + + atom.m_uds = KIO::UDS_ACCESS; + atom.m_long = S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH; + entry.append( atom ); + + atom.m_uds = KIO::UDS_USER; + atom.m_str = mUsername; + entry.append( atom ); + atom.m_uds = KIO::UDS_GROUP; + entry.append( atom ); + + // no size + statEntry( entry ); + finished(); + return; + } + + int code; + sftpFileAttr attr(remoteEncoding()); + if( (code = sftpStat(url, attr)) != SSH2_FX_OK ) { + processStatus(code, url.prettyURL()); + return; + } + else { + //kdDebug() << "We sent and received stat packet ok" << endl; + attr.setFilename(url.fileName()); + statEntry(attr.entry()); + } + + finished(); + + kdDebug(KIO_SFTP_DB) << "stat: END" << endl; + return; +} + + +void sftpProtocol::mimetype ( const KURL& url ){ + kdDebug(KIO_SFTP_DB) << "mimetype(): " << url << endl; + + openConnection(); + if( !mConnected ) + return; + + Q_UINT32 pflags = SSH2_FXF_READ; + QByteArray handle, mydata; + sftpFileAttr attr(remoteEncoding()); + int code; + if( (code = sftpOpen(url, pflags, attr, handle)) != SSH2_FX_OK ) { + error(ERR_CANNOT_OPEN_FOR_READING, url.prettyURL()); + return; + } + + Q_UINT32 len = 1024; // Get first 1k for determining mimetype + Q_UINT64 offset = 0; + code = SSH2_FX_OK; + while( offset < len && code == SSH2_FX_OK ) { + if( (code = sftpRead(handle, offset, len, mydata)) == SSH2_FX_OK ) { + data(mydata); + offset += mydata.size(); + processedSize(offset); + + kdDebug(KIO_SFTP_DB) << "mimetype(): offset = " << offset << endl; + } + } + + + data(QByteArray()); + processedSize(offset); + sftpClose(handle); + finished(); + kdDebug(KIO_SFTP_DB) << "mimetype(): END" << endl; +} + + +void sftpProtocol::listDir(const KURL& url) { + kdDebug(KIO_SFTP_DB) << "listDir(): " << url << endl; + + openConnection(); + if( !mConnected ) + return; + + if( !url.hasPath() ) { + KURL newUrl ( url ); + if( sftpRealPath(url, newUrl) == SSH2_FX_OK ) { + kdDebug(KIO_SFTP_DB) << "listDir: Redirecting to " << newUrl << endl; + redirection(newUrl); + finished(); + return; + } + } + + int code; + QByteArray handle; + + if( (code = sftpOpenDirectory(url, handle)) != SSH2_FX_OK ) { + kdError(KIO_SFTP_DB) << "listDir(): open directory failed" << endl; + processStatus(code, url.prettyURL()); + return; + } + + + code = SSH2_FX_OK; + while( code == SSH2_FX_OK ) { + code = sftpReadDir(handle, url); + if( code != SSH2_FX_OK && code != SSH2_FX_EOF ) + processStatus(code, url.prettyURL()); + kdDebug(KIO_SFTP_DB) << "listDir(): return code = " << code << endl; + } + + if( (code = sftpClose(handle)) != SSH2_FX_OK ) { + kdError(KIO_SFTP_DB) << "listdir(): closing of directory failed" << endl; + processStatus(code, url.prettyURL()); + return; + } + + finished(); + kdDebug(KIO_SFTP_DB) << "listDir(): END" << endl; +} + +/** Make a directory. + OpenSSH does not follow the internet draft for sftp in this case. + The format of the mkdir request expected by OpenSSH sftp server is: + uint32 id + string path + ATTR attr + */ +void sftpProtocol::mkdir(const KURL&url, int permissions){ + + kdDebug(KIO_SFTP_DB) << "mkdir() creating dir: " << url.path() << endl; + + openConnection(); + if( !mConnected ) + return; + + QCString path = remoteEncoding()->encode(url.path()); + uint len = path.length(); + + sftpFileAttr attr(remoteEncoding()); + + if (permissions != -1) + attr.setPermissions(permissions); + + Q_UINT32 id, expectedId; + id = expectedId = mMsgId++; + + QByteArray p; + QDataStream s(p, IO_WriteOnly); + s << Q_UINT32(1 /*type*/ + 4 /*id*/ + 4 /*str length*/ + len + attr.size()); + s << (Q_UINT8)SSH2_FXP_MKDIR; + s << id; + s.writeBytes(path.data(), len); + s << attr; + + kdDebug(KIO_SFTP_DB) << "mkdir(): packet size is " << p.size() << endl; + + putPacket(p); + getPacket(p); + + Q_UINT8 type; + QDataStream r(p, IO_ReadOnly); + + r >> type >> id; + if( id != expectedId ) { + kdError(KIO_SFTP_DB) << "mkdir: sftp packet id mismatch" << endl; + error(ERR_COULD_NOT_MKDIR, path); + finished(); + return; + } + + if( type != SSH2_FXP_STATUS ) { + kdError(KIO_SFTP_DB) << "mkdir(): unexpected packet type of " << type << endl; + error(ERR_COULD_NOT_MKDIR, path); + finished(); + return; + } + + int code; + r >> code; + if( code != SSH2_FX_OK ) { + kdError(KIO_SFTP_DB) << "mkdir(): failed with code " << code << endl; + + // Check if mkdir failed because the directory already exists so that + // we can return the appropriate message... + sftpFileAttr dirAttr(remoteEncoding()); + if ( sftpStat(url, dirAttr) == SSH2_FX_OK ) + { + error( ERR_DIR_ALREADY_EXIST, url.prettyURL() ); + return; + } + + error(ERR_COULD_NOT_MKDIR, path); + } + + finished(); +} + +void sftpProtocol::rename(const KURL& src, const KURL& dest, bool overwrite){ + kdDebug(KIO_SFTP_DB) << "rename(" << src << " -> " << dest << ")" << endl; + + if (!isSupportedOperation(SSH2_FXP_RENAME)) { + error(ERR_UNSUPPORTED_ACTION, + i18n("The remote host does not support renaming files.")); + return; + } + + openConnection(); + if( !mConnected ) + return; + + // Always stat the destination before attempting to rename + // a file or a directory... + sftpFileAttr attr(remoteEncoding()); + int code = sftpStat(dest, attr); + + // If the destination directory, exists tell it to the job + // so it the proper action can be presented to the user... + if( code == SSH2_FX_OK ) + { + if (!overwrite) + { + if ( S_ISDIR(attr.permissions()) ) + error( KIO::ERR_DIR_ALREADY_EXIST, dest.url() ); + else + error( KIO::ERR_FILE_ALREADY_EXIST, dest.url() ); + return; + } + + // If overwrite is specified, then simply remove the existing file/dir first... + if( (code = sftpRemove( dest, !S_ISDIR(attr.permissions()) )) != SSH2_FX_OK ) + { + processStatus(code); + return; + } + } + + // Do the renaming... + if( (code = sftpRename(src, dest)) != SSH2_FX_OK ) { + processStatus(code); + return; + } + + finished(); + kdDebug(KIO_SFTP_DB) << "rename(): END" << endl; +} + +void sftpProtocol::symlink(const QString& target, const KURL& dest, bool overwrite){ + kdDebug(KIO_SFTP_DB) << "symlink()" << endl; + + if (!isSupportedOperation(SSH2_FXP_SYMLINK)) { + error(ERR_UNSUPPORTED_ACTION, + i18n("The remote host does not support creating symbolic links.")); + return; + } + + openConnection(); + if( !mConnected ) + return; + + int code; + bool failed = false; + if( (code = sftpSymLink(target, dest)) != SSH2_FX_OK ) { + if( overwrite ) { // try to delete the destination + sftpFileAttr attr(remoteEncoding()); + if( (code = sftpStat(dest, attr)) != SSH2_FX_OK ) { + failed = true; + } + else { + if( (code = sftpRemove(dest, !S_ISDIR(attr.permissions())) ) != SSH2_FX_OK ) { + failed = true; + } + else { + // XXX what if rename fails again? We have lost the file. + // Maybe rename dest to a temporary name first? If rename is + // successful, then delete? + if( (code = sftpSymLink(target, dest)) != SSH2_FX_OK ) + failed = true; + } + } + } + else if( code == SSH2_FX_FAILURE ) { + error(ERR_FILE_ALREADY_EXIST, dest.prettyURL()); + return; + } + else + failed = true; + } + + // What error code do we return? Code for the original symlink command + // or for the last command or for both? The second one is implemented here. + if( failed ) + processStatus(code); + + finished(); +} + +void sftpProtocol::chmod(const KURL& url, int permissions){ + QString perms; + perms.setNum(permissions, 8); + kdDebug(KIO_SFTP_DB) << "chmod(" << url << ", " << perms << ")" << endl; + + openConnection(); + if( !mConnected ) + return; + + sftpFileAttr attr(remoteEncoding()); + + if (permissions != -1) + attr.setPermissions(permissions); + + int code; + if( (code = sftpSetStat(url, attr)) != SSH2_FX_OK ) { + kdError(KIO_SFTP_DB) << "chmod(): sftpSetStat failed with error " << code << endl; + if( code == SSH2_FX_FAILURE ) + error(ERR_CANNOT_CHMOD, QString::null); + else + processStatus(code, url.prettyURL()); + } + finished(); +} + + +void sftpProtocol::del(const KURL &url, bool isfile){ + kdDebug(KIO_SFTP_DB) << "del(" << url << ", " << (isfile?"file":"dir") << ")" << endl; + + openConnection(); + if( !mConnected ) + return; + + int code; + if( (code = sftpRemove(url, isfile)) != SSH2_FX_OK ) { + kdError(KIO_SFTP_DB) << "del(): sftpRemove failed with error code " << code << endl; + processStatus(code, url.prettyURL()); + } + finished(); +} + +void sftpProtocol::slave_status() { + kdDebug(KIO_SFTP_DB) << "slave_status(): connected to " + << mHost << "? " << mConnected << endl; + + slaveStatus ((mConnected ? mHost : QString::null), mConnected); +} + +bool sftpProtocol::getPacket(QByteArray& msg) { + QByteArray buf(4096); + + // Get the message length... + ssize_t len = atomicio(ssh.stdioFd(), buf.data(), 4, true /*read*/); + + if( len == 0 || len == -1 ) { + kdDebug(KIO_SFTP_DB) << "getPacket(): read of packet length failed, ret = " + << len << ", error =" << strerror(errno) << endl; + closeConnection(); + error( ERR_CONNECTION_BROKEN, mHost); + msg.resize(0); + return false; + } + + uint msgLen; + QDataStream s(buf, IO_ReadOnly); + s >> msgLen; + + //kdDebug(KIO_SFTP_DB) << "getPacket(): Message size = " << msgLen << endl; + + msg.resize(0); + + QBuffer b( msg ); + b.open( IO_WriteOnly ); + + while( msgLen ) { + len = atomicio(ssh.stdioFd(), buf.data(), kMin(buf.size(), msgLen), true /*read*/); + + if( len == 0 || len == -1) { + QString errmsg; + if (len == 0) + errmsg = i18n("Connection closed"); + else + errmsg = i18n("Could not read SFTP packet"); + kdDebug(KIO_SFTP_DB) << "getPacket(): nothing to read, ret = " << + len << ", error =" << strerror(errno) << endl; + closeConnection(); + error(ERR_CONNECTION_BROKEN, errmsg); + b.close(); + return false; + } + + b.writeBlock(buf.data(), len); + + //kdDebug(KIO_SFTP_DB) << "getPacket(): Read Message size = " << len << endl; + //kdDebug(KIO_SFTP_DB) << "getPacket(): Copy Message size = " << msg.size() << endl; + + msgLen -= len; + } + + b.close(); + + return true; +} + +/** Send an sftp packet to stdin of the ssh process. */ +bool sftpProtocol::putPacket(QByteArray& p){ +// kdDebug(KIO_SFTP_DB) << "putPacket(): size == " << p.size() << endl; + int ret; + ret = atomicio(ssh.stdioFd(), p.data(), p.size(), false /*write*/); + if( ret <= 0 ) { + kdDebug(KIO_SFTP_DB) << "putPacket(): write failed, ret =" << ret << + ", error = " << strerror(errno) << endl; + return false; + } + + return true; +} + +/** Used to have the server canonicalize any given path name to an absolute path. +This is useful for converting path names containing ".." components or relative +pathnames without a leading slash into absolute paths. +Returns the canonicalized url. */ +int sftpProtocol::sftpRealPath(const KURL& url, KURL& newUrl){ + + kdDebug(KIO_SFTP_DB) << "sftpRealPath(" << url << ", newUrl)" << endl; + + QCString path = remoteEncoding()->encode(url.path()); + uint len = path.length(); + + Q_UINT32 id, expectedId; + id = expectedId = mMsgId++; + + QByteArray p; + QDataStream s(p, IO_WriteOnly); + s << Q_UINT32(1 /*type*/ + 4 /*id*/ + 4 /*str length*/ + len); + s << (Q_UINT8)SSH2_FXP_REALPATH; + s << id; + s.writeBytes(path.data(), len); + + putPacket(p); + getPacket(p); + + Q_UINT8 type; + QDataStream r(p, IO_ReadOnly); + + r >> type >> id; + if( id != expectedId ) { + kdError(KIO_SFTP_DB) << "sftpRealPath: sftp packet id mismatch" << endl; + return -1; + } + + if( type == SSH2_FXP_STATUS ) { + Q_UINT32 code; + r >> code; + return code; + } + + if( type != SSH2_FXP_NAME ) { + kdError(KIO_SFTP_DB) << "sftpRealPath(): unexpected packet type of " << type << endl; + return -1; + } + + Q_UINT32 count; + r >> count; + if( count != 1 ) { + kdError(KIO_SFTP_DB) << "sftpRealPath(): Bad number of file attributes for realpath command" << endl; + return -1; + } + + QCString newPath; + r >> newPath; + + newPath.truncate(newPath.size()); + if (newPath.isEmpty()) + newPath = "/"; + newUrl.setPath(newPath); + + return SSH2_FX_OK; +} + +sftpProtocol::Status sftpProtocol::doProcessStatus(Q_UINT8 code, const QString& message) +{ + Status res; + res.code = 0; + res.size = 0; + res.text = message; + + switch(code) + { + case SSH2_FX_OK: + case SSH2_FX_EOF: + break; + case SSH2_FX_NO_SUCH_FILE: + res.code = ERR_DOES_NOT_EXIST; + break; + case SSH2_FX_PERMISSION_DENIED: + res.code = ERR_ACCESS_DENIED; + break; + case SSH2_FX_FAILURE: + res.text = i18n("SFTP command failed for an unknown reason."); + res.code = ERR_UNKNOWN; + break; + case SSH2_FX_BAD_MESSAGE: + res.text = i18n("The SFTP server received a bad message."); + res.code = ERR_UNKNOWN; + break; + case SSH2_FX_OP_UNSUPPORTED: + res.text = i18n("You attempted an operation unsupported by the SFTP server."); + res.code = ERR_UNKNOWN; + break; + default: + res.text = i18n("Error code: %1").arg(code); + res.code = ERR_UNKNOWN; + } + + return res; +} + +/** Process SSH_FXP_STATUS packets. */ +void sftpProtocol::processStatus(Q_UINT8 code, const QString& message){ + Status st = doProcessStatus( code, message ); + if( st.code != 0 ) + error( st.code, st.text ); +} + +/** Opens a directory handle for url.path. Returns true if succeeds. */ +int sftpProtocol::sftpOpenDirectory(const KURL& url, QByteArray& handle){ + + kdDebug(KIO_SFTP_DB) << "sftpOpenDirectory(" << url << ", handle)" << endl; + + QCString path = remoteEncoding()->encode(url.path()); + uint len = path.length(); + + Q_UINT32 id, expectedId; + id = expectedId = mMsgId++; + + QByteArray p; + QDataStream s(p, IO_WriteOnly); + s << (Q_UINT32)(1 /*type*/ + 4 /*id*/ + 4 /*str length*/ + len); + s << (Q_UINT8)SSH2_FXP_OPENDIR; + s << (Q_UINT32)id; + s.writeBytes(path.data(), len); + + putPacket(p); + getPacket(p); + + QDataStream r(p, IO_ReadOnly); + Q_UINT8 type; + + r >> type >> id; + if( id != expectedId ) { + kdError(KIO_SFTP_DB) << "sftpOpenDirectory: sftp packet id mismatch: " << + "expected " << expectedId << ", got " << id << endl; + return -1; + } + + if( type == SSH2_FXP_STATUS ) { + Q_UINT32 errCode; + r >> errCode; + return errCode; + } + + if( type != SSH2_FXP_HANDLE ) { + kdError(KIO_SFTP_DB) << "sftpOpenDirectory: unexpected message type of " << type << endl; + return -1; + } + + r >> handle; + if( handle.size() > 256 ) { + kdError(KIO_SFTP_DB) << "sftpOpenDirectory: handle exceeds max length" << endl; + return -1; + } + + kdDebug(KIO_SFTP_DB) << "sftpOpenDirectory: handle (" << handle.size() << "): [" << handle << "]" << endl; + return SSH2_FX_OK; +} + +/** Closes a directory or file handle. */ +int sftpProtocol::sftpClose(const QByteArray& handle){ + + kdDebug(KIO_SFTP_DB) << "sftpClose()" << endl; + + Q_UINT32 id, expectedId; + id = expectedId = mMsgId++; + + QByteArray p; + QDataStream s(p, IO_WriteOnly); + s << (Q_UINT32)(1 /*type*/ + 4 /*id*/ + 4 /*str length*/ + handle.size()); + s << (Q_UINT8)SSH2_FXP_CLOSE; + s << (Q_UINT32)id; + s << handle; + + putPacket(p); + getPacket(p); + + QDataStream r(p, IO_ReadOnly); + Q_UINT8 type; + + r >> type >> id; + if( id != expectedId ) { + kdError(KIO_SFTP_DB) << "sftpClose: sftp packet id mismatch" << endl; + return -1; + } + + if( type != SSH2_FXP_STATUS ) { + kdError(KIO_SFTP_DB) << "sftpClose: unexpected message type of " << type << endl; + return -1; + } + + Q_UINT32 code; + r >> code; + if( code != SSH2_FX_OK ) { + kdError(KIO_SFTP_DB) << "sftpClose: close failed with err code " << code << endl; + } + + return code; +} + +/** Set a files attributes. */ +int sftpProtocol::sftpSetStat(const KURL& url, const sftpFileAttr& attr){ + + kdDebug(KIO_SFTP_DB) << "sftpSetStat(" << url << ", attr)" << endl; + + QCString path = remoteEncoding()->encode(url.path()); + uint len = path.length(); + + Q_UINT32 id, expectedId; + id = expectedId = mMsgId++; + + QByteArray p; + QDataStream s(p, IO_WriteOnly); + s << (Q_UINT32)(1 /*type*/ + 4 /*id*/ + 4 /*str length*/ + len + attr.size()); + s << (Q_UINT8)SSH2_FXP_SETSTAT; + s << (Q_UINT32)id; + s.writeBytes(path.data(), len); + s << attr; + + putPacket(p); + getPacket(p); + + QDataStream r(p, IO_ReadOnly); + Q_UINT8 type; + + r >> type >> id; + if( id != expectedId ) { + kdError(KIO_SFTP_DB) << "sftpSetStat(): sftp packet id mismatch" << endl; + return -1; + // XXX How do we do a fatal error? + } + + if( type != SSH2_FXP_STATUS ) { + kdError(KIO_SFTP_DB) << "sftpSetStat(): unexpected message type of " << type << endl; + return -1; + } + + Q_UINT32 code; + r >> code; + if( code != SSH2_FX_OK ) { + kdError(KIO_SFTP_DB) << "sftpSetStat(): set stat failed with err code " << code << endl; + } + + return code; +} + +/** Sends a sftp command to remove a file or directory. */ +int sftpProtocol::sftpRemove(const KURL& url, bool isfile){ + + kdDebug(KIO_SFTP_DB) << "sftpRemove(): " << url << ", isFile ? " << isfile << endl; + + QCString path = remoteEncoding()->encode(url.path()); + uint len = path.length(); + + Q_UINT32 id, expectedId; + id = expectedId = mMsgId++; + + QByteArray p; + QDataStream s(p, IO_WriteOnly); + s << (Q_UINT32)(1 /*type*/ + 4 /*id*/ + 4 /*str length*/ + len); + s << (Q_UINT8)(isfile ? SSH2_FXP_REMOVE : SSH2_FXP_RMDIR); + s << (Q_UINT32)id; + s.writeBytes(path.data(), len); + + putPacket(p); + getPacket(p); + + QDataStream r(p, IO_ReadOnly); + Q_UINT8 type; + + r >> type >> id; + if( id != expectedId ) { + kdError(KIO_SFTP_DB) << "del(): sftp packet id mismatch" << endl; + return -1; + } + + if( type != SSH2_FXP_STATUS ) { + kdError(KIO_SFTP_DB) << "del(): unexpected message type of " << type << endl; + return -1; + } + + Q_UINT32 code; + r >> code; + if( code != SSH2_FX_OK ) { + kdError(KIO_SFTP_DB) << "del(): del failed with err code " << code << endl; + } + + return code; +} + +/** Send a sftp command to rename a file or directoy. */ +int sftpProtocol::sftpRename(const KURL& src, const KURL& dest){ + + kdDebug(KIO_SFTP_DB) << "sftpRename(" << src << " -> " << dest << ")" << endl; + + QCString srcPath = remoteEncoding()->encode(src.path()); + QCString destPath = remoteEncoding()->encode(dest.path()); + + uint slen = srcPath.length(); + uint dlen = destPath.length(); + + Q_UINT32 id, expectedId; + id = expectedId = mMsgId++; + + QByteArray p; + QDataStream s(p, IO_WriteOnly); + s << (Q_UINT32)(1 /*type*/ + 4 /*id*/ + + 4 /*str length*/ + slen + + 4 /*str length*/ + dlen); + s << (Q_UINT8)SSH2_FXP_RENAME; + s << (Q_UINT32)id; + s.writeBytes(srcPath.data(), slen); + s.writeBytes(destPath.data(), dlen); + + putPacket(p); + getPacket(p); + + QDataStream r(p, IO_ReadOnly); + Q_UINT8 type; + + r >> type >> id; + if( id != expectedId ) { + kdError(KIO_SFTP_DB) << "sftpRename(): sftp packet id mismatch" << endl; + return -1; + } + + if( type != SSH2_FXP_STATUS ) { + kdError(KIO_SFTP_DB) << "sftpRename(): unexpected message type of " << type << endl; + return -1; + } + + int code; + r >> code; + if( code != SSH2_FX_OK ) { + kdError(KIO_SFTP_DB) << "sftpRename(): rename failed with err code " << code << endl; + } + + return code; +} +/** Get directory listings. */ +int sftpProtocol::sftpReadDir(const QByteArray& handle, const KURL& url){ + // url is needed so we can lookup the link destination + kdDebug(KIO_SFTP_DB) << "sftpReadDir(): " << url << endl; + + Q_UINT32 id, expectedId, count; + Q_UINT8 type; + + sftpFileAttr attr (remoteEncoding()); + attr.setDirAttrsFlag(true); + + QByteArray p; + QDataStream s(p, IO_WriteOnly); + id = expectedId = mMsgId++; + s << (Q_UINT32)(1 /*type*/ + 4 /*id*/ + 4 /*str length*/ + handle.size()); + s << (Q_UINT8)SSH2_FXP_READDIR; + s << (Q_UINT32)id; + s << handle; + + putPacket(p); + getPacket(p); + + QDataStream r(p, IO_ReadOnly); + r >> type >> id; + + if( id != expectedId ) { + kdError(KIO_SFTP_DB) << "sftpReadDir(): sftp packet id mismatch" << endl; + return -1; + } + + int code; + if( type == SSH2_FXP_STATUS ) { + r >> code; + return code; + } + + if( type != SSH2_FXP_NAME ) { + kdError(KIO_SFTP_DB) << "kio_sftpProtocl::sftpReadDir(): Unexpected message" << endl; + return -1; + } + + r >> count; + kdDebug(KIO_SFTP_DB) << "sftpReadDir(): got " << count << " entries" << endl; + + while(count--) { + r >> attr; + + if( S_ISLNK(attr.permissions()) ) { + KURL myurl ( url ); + myurl.addPath(attr.filename()); + + // Stat the symlink to find out its type... + sftpFileAttr attr2 (remoteEncoding()); + (void) sftpStat(myurl, attr2); + + attr.setLinkType(attr2.linkType()); + attr.setLinkDestination(attr2.linkDestination()); + } + + listEntry(attr.entry(), false); + } + + listEntry(attr.entry(), true); + + return SSH2_FX_OK; +} + +int sftpProtocol::sftpReadLink(const KURL& url, QString& target){ + + kdDebug(KIO_SFTP_DB) << "sftpReadLink(): " << url << endl; + + QCString path = remoteEncoding()->encode(url.path()); + uint len = path.length(); + + //kdDebug(KIO_SFTP_DB) << "sftpReadLink(): Encoded Path: " << path << endl; + //kdDebug(KIO_SFTP_DB) << "sftpReadLink(): Encoded Size: " << len << endl; + + Q_UINT32 id, expectedId; + id = expectedId = mMsgId++; + + QByteArray p; + QDataStream s(p, IO_WriteOnly); + s << (Q_UINT32)(1 /*type*/ + 4 /*id*/ + 4 /*str length*/ + len); + s << (Q_UINT8)SSH2_FXP_READLINK; + s << id; + s.writeBytes(path.data(), len); + + + putPacket(p); + getPacket(p); + + Q_UINT8 type; + QDataStream r(p, IO_ReadOnly); + + r >> type >> id; + if( id != expectedId ) { + kdError(KIO_SFTP_DB) << "sftpReadLink(): sftp packet id mismatch" << endl; + return -1; + } + + if( type == SSH2_FXP_STATUS ) { + Q_UINT32 code; + r >> code; + kdDebug(KIO_SFTP_DB) << "sftpReadLink(): read link failed with code " << code << endl; + return code; + } + + if( type != SSH2_FXP_NAME ) { + kdError(KIO_SFTP_DB) << "sftpReadLink(): unexpected packet type of " << type << endl; + return -1; + } + + Q_UINT32 count; + r >> count; + if( count != 1 ) { + kdError(KIO_SFTP_DB) << "sftpReadLink(): Bad number of file attributes for realpath command" << endl; + return -1; + } + + QCString linkAddress; + r >> linkAddress; + + linkAddress.truncate(linkAddress.size()); + kdDebug(KIO_SFTP_DB) << "sftpReadLink(): Link address: " << linkAddress << endl; + + target = remoteEncoding()->decode(linkAddress); + + return SSH2_FX_OK; +} + +int sftpProtocol::sftpSymLink(const QString& _target, const KURL& dest){ + + QCString destPath = remoteEncoding()->encode(dest.path()); + QCString target = remoteEncoding()->encode(_target); + uint dlen = destPath.length(); + uint tlen = target.length(); + + kdDebug(KIO_SFTP_DB) << "sftpSymLink(" << target << " -> " << destPath << ")" << endl; + + Q_UINT32 id, expectedId; + id = expectedId = mMsgId++; + + QByteArray p; + QDataStream s(p, IO_WriteOnly); + s << (Q_UINT32)(1 /*type*/ + 4 /*id*/ + + 4 /*str length*/ + tlen + + 4 /*str length*/ + dlen); + s << (Q_UINT8)SSH2_FXP_SYMLINK; + s << (Q_UINT32)id; + s.writeBytes(target.data(), tlen); + s.writeBytes(destPath.data(), dlen); + + putPacket(p); + getPacket(p); + + QDataStream r(p, IO_ReadOnly); + Q_UINT8 type; + + r >> type >> id; + if( id != expectedId ) { + kdError(KIO_SFTP_DB) << "sftpSymLink(): sftp packet id mismatch" << endl; + return -1; + } + + if( type != SSH2_FXP_STATUS ) { + kdError(KIO_SFTP_DB) << "sftpSymLink(): unexpected message type of " << type << endl; + return -1; + } + + Q_UINT32 code; + r >> code; + if( code != SSH2_FX_OK ) { + kdError(KIO_SFTP_DB) << "sftpSymLink(): rename failed with err code " << code << endl; + } + + return code; +} + +/** Stats a file. */ +int sftpProtocol::sftpStat(const KURL& url, sftpFileAttr& attr) { + + kdDebug(KIO_SFTP_DB) << "sftpStat(): " << url << endl; + + QCString path = remoteEncoding()->encode(url.path()); + uint len = path.length(); + + Q_UINT32 id, expectedId; + id = expectedId = mMsgId++; + + QByteArray p; + QDataStream s(p, IO_WriteOnly); + s << (Q_UINT32)(1 /*type*/ + 4 /*id*/ + 4 /*str length*/ + len); + s << (Q_UINT8)SSH2_FXP_LSTAT; + s << (Q_UINT32)id; + s.writeBytes(path.data(), len); + + putPacket(p); + getPacket(p); + + QDataStream r(p, IO_ReadOnly); + Q_UINT8 type; + + r >> type >> id; + if( id != expectedId ) { + kdError(KIO_SFTP_DB) << "sftpStat(): sftp packet id mismatch" << endl; + return -1; + } + + if( type == SSH2_FXP_STATUS ) { + Q_UINT32 errCode; + r >> errCode; + kdError(KIO_SFTP_DB) << "sftpStat(): stat failed with code " << errCode << endl; + return errCode; + } + + if( type != SSH2_FXP_ATTRS ) { + kdError(KIO_SFTP_DB) << "sftpStat(): unexpected message type of " << type << endl; + return -1; + } + + r >> attr; + attr.setFilename(url.fileName()); + kdDebug(KIO_SFTP_DB) << "sftpStat(): " << attr << endl; + + // If the stat'ed resource is a symlink, perform a recursive stat + // to determine the actual destination's type (file/dir). + if( S_ISLNK(attr.permissions()) && isSupportedOperation(SSH2_FXP_READLINK) ) { + + QString target; + int code = sftpReadLink( url, target ); + + if ( code != SSH2_FX_OK ) { + kdError(KIO_SFTP_DB) << "sftpStat(): Unable to stat symlink destination" << endl; + return -1; + } + + kdDebug(KIO_SFTP_DB) << "sftpStat(): Resource is a symlink to -> " << target << endl; + + KURL dest( url ); + if( target[0] == '/' ) + dest.setPath(target); + else + dest.setFileName(target); + + dest.cleanPath(); + + // Ignore symlinks that point to themselves... + if ( dest != url ) { + + sftpFileAttr attr2 (remoteEncoding()); + (void) sftpStat(dest, attr2); + + if (attr2.linkType() == 0) + attr.setLinkType(attr2.fileType()); + else + attr.setLinkType(attr2.linkType()); + + attr.setLinkDestination(target); + + kdDebug(KIO_SFTP_DB) << "sftpStat(): File type: " << attr.fileType() << endl; + } + } + + return SSH2_FX_OK; +} + + +int sftpProtocol::sftpOpen(const KURL& url, const Q_UINT32 pflags, + const sftpFileAttr& attr, QByteArray& handle) { + kdDebug(KIO_SFTP_DB) << "sftpOpen(" << url << ", handle" << endl; + + QCString path = remoteEncoding()->encode(url.path()); + uint len = path.length(); + + Q_UINT32 id, expectedId; + id = expectedId = mMsgId++; + + QByteArray p; + QDataStream s(p, IO_WriteOnly); + s << (Q_UINT32)(1 /*type*/ + 4 /*id*/ + + 4 /*str length*/ + len + + 4 /*pflags*/ + attr.size()); + s << (Q_UINT8)SSH2_FXP_OPEN; + s << (Q_UINT32)id; + s.writeBytes(path.data(), len); + s << pflags; + s << attr; + + putPacket(p); + getPacket(p); + + QDataStream r(p, IO_ReadOnly); + Q_UINT8 type; + + r >> type >> id; + if( id != expectedId ) { + kdError(KIO_SFTP_DB) << "sftpOpen(): sftp packet id mismatch" << endl; + return -1; + } + + if( type == SSH2_FXP_STATUS ) { + Q_UINT32 errCode; + r >> errCode; + return errCode; + } + + if( type != SSH2_FXP_HANDLE ) { + kdError(KIO_SFTP_DB) << "sftpOpen(): unexpected message type of " << type << endl; + return -1; + } + + r >> handle; + if( handle.size() > 256 ) { + kdError(KIO_SFTP_DB) << "sftpOpen(): handle exceeds max length" << endl; + return -1; + } + + kdDebug(KIO_SFTP_DB) << "sftpOpen(): handle (" << handle.size() << "): [" << handle << "]" << endl; + return SSH2_FX_OK; +} + + +int sftpProtocol::sftpRead(const QByteArray& handle, KIO::filesize_t offset, Q_UINT32 len, QByteArray& data) +{ + // kdDebug(KIO_SFTP_DB) << "sftpRead( offset = " << offset << ", len = " << len << ")" << endl; + QByteArray p; + QDataStream s(p, IO_WriteOnly); + + Q_UINT32 id, expectedId; + id = expectedId = mMsgId++; + s << (Q_UINT32)(1 /*type*/ + 4 /*id*/ + + 4 /*str length*/ + handle.size() + + 8 /*offset*/ + 4 /*length*/); + s << (Q_UINT8)SSH2_FXP_READ; + s << (Q_UINT32)id; + s << handle; + s << offset; // we don't have a convienient 64 bit int so set upper int to zero + s << len; + + putPacket(p); + getPacket(p); + + QDataStream r(p, IO_ReadOnly); + Q_UINT8 type; + + r >> type >> id; + if( id != expectedId ) { + kdError(KIO_SFTP_DB) << "sftpRead: sftp packet id mismatch" << endl; + return -1; + } + + if( type == SSH2_FXP_STATUS ) { + Q_UINT32 errCode; + r >> errCode; + kdError(KIO_SFTP_DB) << "sftpRead: read failed with code " << errCode << endl; + return errCode; + } + + if( type != SSH2_FXP_DATA ) { + kdError(KIO_SFTP_DB) << "sftpRead: unexpected message type of " << type << endl; + return -1; + } + + r >> data; + + return SSH2_FX_OK; +} + + +int sftpProtocol::sftpWrite(const QByteArray& handle, KIO::filesize_t offset, const QByteArray& data){ +// kdDebug(KIO_SFTP_DB) << "sftpWrite( offset = " << offset << +// ", data sz = " << data.size() << ")" << endl; + QByteArray p; + QDataStream s(p, IO_WriteOnly); + + Q_UINT32 id, expectedId; + id = expectedId = mMsgId++; + s << (Q_UINT32)(1 /*type*/ + 4 /*id*/ + + 4 /*str length*/ + handle.size() + + 8 /*offset*/ + + 4 /* data size */ + data.size()); + s << (Q_UINT8)SSH2_FXP_WRITE; + s << (Q_UINT32)id; + s << handle; + s << offset; // we don't have a convienient 64 bit int so set upper int to zero + s << data; + +// kdDebug(KIO_SFTP_DB) << "sftpWrite(): SSH2_FXP_WRITE, id:" +// << id << ", handle:" << handle << ", offset:" << offset << ", some data" << endl; + +// kdDebug(KIO_SFTP_DB) << "sftpWrite(): send packet [" << p << "]" << endl; + + putPacket(p); + getPacket(p); + +// kdDebug(KIO_SFTP_DB) << "sftpWrite(): received packet [" << p << "]" << endl; + + QDataStream r(p, IO_ReadOnly); + Q_UINT8 type; + + r >> type >> id; + if( id != expectedId ) { + kdError(KIO_SFTP_DB) << "sftpWrite(): sftp packet id mismatch, got " + << id << ", expected " << expectedId << endl; + return -1; + } + + if( type != SSH2_FXP_STATUS ) { + kdError(KIO_SFTP_DB) << "sftpWrite(): unexpected message type of " << type << endl; + return -1; + } + + Q_UINT32 code; + r >> code; + return code; +} + + diff --git a/kioslave/sftp/kio_sftp.h b/kioslave/sftp/kio_sftp.h new file mode 100644 index 000000000..ff99b4760 --- /dev/null +++ b/kioslave/sftp/kio_sftp.h @@ -0,0 +1,149 @@ +/*************************************************************************** + sftpProtocol.h - description + ------------------- + begin : Sat Jun 30 20:08:47 CDT 2001 + copyright : (C) 2001 by Lucas Fisher + email : ljfisher@purdue.edu +***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#ifndef __kio_sftp_h__ +#define __kio_sftp_h__ + +#include <qstring.h> +#include <qcstring.h> +#include <qobject.h> + +#include <kurl.h> +#include <kio/global.h> +#include <kio/slavebase.h> +#include <kdebug.h> + +#include "process.h" +#include "sftpfileattr.h" +#include "ksshprocess.h" + +#define KIO_SFTP_DB 7120 + + +class sftpProtocol : public KIO::SlaveBase +{ + +public: + sftpProtocol(const QCString &pool_socket, const QCString &app_socket); + virtual ~sftpProtocol(); + virtual void setHost(const QString& h, int port, const QString& user, const QString& pass); + virtual void get(const KURL& url); + virtual void listDir(const KURL& url) ; + virtual void mimetype(const KURL& url); + virtual void stat(const KURL& url); + virtual void copy(const KURL &src, const KURL &dest, int permissions, bool overwrite); + virtual void put(const KURL& url, int permissions, bool overwrite, bool resume); + virtual void closeConnection(); + virtual void slave_status(); + virtual void del(const KURL &url, bool isfile); + virtual void chmod(const KURL& url, int permissions); + virtual void symlink(const QString& target, const KURL& dest, bool overwrite); + virtual void rename(const KURL& src, const KURL& dest, bool overwrite); + virtual void mkdir(const KURL&url, int permissions); + virtual void openConnection(); + +private: // Private variables + /** True if ioslave is connected to sftp server. */ + bool mConnected; + + /** Host we are connected to. */ + QString mHost; + + /** Port we are connected to. */ + int mPort; + + /** Ssh process to which we send the sftp packets. */ + KSshProcess ssh; + + /** Username to use when connecting */ + QString mUsername; + + /** User's password */ + QString mPassword; + + /** Message id of the last sftp packet we sent. */ + unsigned int mMsgId; + + /** Type of packet we are expecting to receive next. */ + unsigned char mExpected; + + /** Version of the sftp protocol we are using. */ + int sftpVersion; + + struct Status + { + int code; + KIO::filesize_t size; + QString text; + }; + +private: // private methods + bool getPacket(QByteArray& msg); + + /* Type is a sftp packet type found in .sftp.h'. + * Example: SSH2_FXP_READLINK, SSH2_FXP_RENAME, etc. + * + * Returns true if the type is supported by the sftp protocol + * version negotiated by the client and server (sftpVersion). + */ + bool isSupportedOperation(int type); + /** Used to have the server canonicalize any given path name to an absolute path. + This is useful for converting path names containing ".." components or relative + pathnames without a leading slash into absolute paths. + Returns the canonicalized url. */ + int sftpRealPath(const KURL& url, KURL& newUrl); + + /** Send an sftp packet to stdin of the ssh process. */ + bool putPacket(QByteArray& p); + /** Process SSH_FXP_STATUS packets. */ + void processStatus(Q_UINT8, const QString& message = QString::null); + /** Process SSH_FXP_STATUS packes and return the result. */ + Status doProcessStatus(Q_UINT8, const QString& message = QString::null); + /** Opens a directory handle for url.path. Returns true if succeeds. */ + int sftpOpenDirectory(const KURL& url, QByteArray& handle); + /** Closes a directory or file handle. */ + int sftpClose(const QByteArray& handle); + /** Send a sftp command to rename a file or directoy. */ + int sftpRename(const KURL& src, const KURL& dest); + /** Set a files attributes. */ + int sftpSetStat(const KURL& url, const sftpFileAttr& attr); + /** Sends a sftp command to remove a file or directory. */ + int sftpRemove(const KURL& url, bool isfile); + /** Creates a symlink named dest to target. */ + int sftpSymLink(const QString& target, const KURL& dest); + /** Get directory listings. */ + int sftpReadDir(const QByteArray& handle, const KURL& url); + /** Retrieves the destination of a link. */ + int sftpReadLink(const KURL& url, QString& target); + /** Stats a file. */ + int sftpStat(const KURL& url, sftpFileAttr& attr); + /** No descriptions */ + int sftpOpen(const KURL& url, const Q_UINT32 pflags, const sftpFileAttr& attr, QByteArray& handle); + /** No descriptions */ + int sftpRead(const QByteArray& handle, KIO::filesize_t offset, Q_UINT32 len, QByteArray& data); + /** No descriptions */ + int sftpWrite(const QByteArray& handle, KIO::filesize_t offset, const QByteArray& data); + + /** Performs faster upload when the source is a local file... */ + void sftpCopyPut(const KURL& src, const KURL& dest, int mode, bool overwrite); + /** Performs faster download when the destination is a local file... */ + void sftpCopyGet(const KURL& dest, const KURL& src, int mode, bool overwrite); + + /** */ + Status sftpGet( const KURL& src, KIO::filesize_t offset = 0, int fd = -1); + void sftpPut( const KURL& dest, int permissions, bool resume, bool overwrite, int fd = -1); +}; +#endif diff --git a/kioslave/sftp/ksshprocess.cpp b/kioslave/sftp/ksshprocess.cpp new file mode 100644 index 000000000..c0393445d --- /dev/null +++ b/kioslave/sftp/ksshprocess.cpp @@ -0,0 +1,1104 @@ +/*************************************************************************** + ksshprocess.cpp - description + ------------------- + begin : Tue Jul 31 2001 + copyright : (C) 2001 by Lucas Fisher + email : ljfisher@purdue.edu + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +/* + * See the KSshProcess header for examples on use. + * + * This class uses a hacked version of the PTYProcess + * class. This was needed because the kdelibs PTYProcess does not provide + * access to the pty file descriptor which we need, because ssh prints the + * password prompt to the pty and reads the password from the pty. I don't + * feel I know enough about ptys to confidently modify the orignial + * PTYProcess class. + * + * To start ssh we take the arguments the user gave us + * in the SshOptList and build the ssh command arguments based on the version + * of ssh we are using. This command and its arguments are passed to + * PTYProcess for execution. Once ssh is started we scan each line of input + * from stdin, stderr, and the pty for recognizable strings. The recognizable + * strings are taken from several string tables. Each table contains a string + * for each specific version of ssh we support and a string for a generic + * version of OpenSSH and commercial SSH incase we don't recognized the + * specific ssh version strings (as when a new SSH version is released after + * a release of KSshProcess). There are tables for ssh version strings, + * password prompts, new host key errors, different host key errors, + * messages than indicate a successful connect, authentication errors, etc. + * If we find user interaction is necessary, for instance to provide a + * password or passphrase, we return a err code to the user who can send + * a message to KSshProcess, using one of several methods, to correct + * the error. + * + * Determining when the ssh connection has successfully authenticationed has + * proved to be the most difficult challenge. OpenSSH does not print a message + * on successful authentication, thus the only way to know is to send data + * and wait for a return. The problem here is sometimes it can take a bit + * to establish the connection (for example, do to DNS lookups). This means + * the user may be sitting there waiting for a connection that failed. + * Instead, ssh is always started with the verbose flag. Then we look for + * a message that indicates auth succeeded. This is hazardous because + * debug messages are more likely to change between OpenSSH releases. + * Thus, we could become incompatible with new OpenSSH releases. + */ + +#include <config.h> + +#include "ksshprocess.h" + +#include <stdio.h> +#include <errno.h> + +#ifdef HAVE_SYS_TIME_H +#include <sys/time.h> +#endif + +#include <kstandarddirs.h> +#include <klocale.h> +#include <qregexp.h> + +/* + * The following are tables of string and regexps we match + * against the output of ssh. An entry in each array + * corresponds the the version of ssh found in versionStrs[]. + * + * The version strings must be ordered in the array from most + * specific to least specific in cases where the beginning + * of several version strings are the similar. For example, + * consider the openssh version strings. The generic "OpenSSH" + * must be the last of the openssh version strings in the array + * so that is matched last. We use these generic version strings + * so we can do a best effor to support unknown ssh versions. + */ +QRegExp KSshProcess::versionStrs[] = { + QRegExp("OpenSSH_3\\.[6-9]|OpenSSH_[1-9]*[4-9]\\.[0-9]"), + QRegExp("OpenSSH"), + QRegExp("SSH Secure Shell") +}; + +const char * const KSshProcess::passwordPrompt[] = { + "password:", // OpenSSH + "password:", // OpenSSH + "password:" // SSH +}; + +const char * const KSshProcess::passphrasePrompt[] = { + "Enter passphrase for key", + "Enter passphrase for key", + "Passphrase for key" +}; + +const char * const KSshProcess::authSuccessMsg[] = { + "Authentication succeeded", + "ssh-userauth2 successful", + "Received SSH_CROSS_AUTHENTICATED packet" +}; + +const char* const KSshProcess::authFailedMsg[] = { + "Permission denied (", + "Permission denied (", + "Authentication failed." +}; + +const char* const KSshProcess::tryAgainMsg[] = { + "please try again", + "please try again", + "adjfhjsdhfdsjfsjdfhuefeufeuefe" +}; + +QRegExp KSshProcess::hostKeyMissingMsg[] = { + QRegExp("The authenticity of host|No (DSA|RSA) host key is known for"), + QRegExp("The authenticity of host|No (DSA|RSA) host key is known for"), + QRegExp("Host key not found from database") +}; + +const char* const KSshProcess::continuePrompt[] = { + "Are you sure you want to continue connecting (yes/no)?", + "Are you sure you want to continue connecting (yes/no)?", + "Are you sure you want to continue connecting (yes/no)?" +}; + +const char* const KSshProcess::hostKeyChangedMsg[] = { + "WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!", + "WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!", + "WARNING: HOST IDENTIFICATION HAS CHANGED!" +}; + +QRegExp KSshProcess::keyFingerprintMsg[] = { + QRegExp("..(:..){15}"), + QRegExp("..(:..){15}"), + QRegExp(".....(-.....){10}") +}; + +QRegExp KSshProcess::knownHostsFileMsg[] = { + QRegExp("Add correct host key in (.*) to get rid of this message."), + QRegExp("Add correct host key in (.*) to get rid of this message."), + QRegExp("Add correct host key to \"(.*)\"") +}; + + +// This prompt only applies to commerical ssh. +const char* const KSshProcess::changeHostKeyOnDiskPrompt[] = { + "as;jf;sajkfdslkfjas;dfjdsa;fj;dsajfdsajf", + "as;jf;sajkfdslkfjas;dfjdsa;fj;dsajfdsajf", + "Do you want to change the host key on disk (yes/no)?" +}; + +// We need this in addition the authFailedMsg because when +// OpenSSH gets a changed host key it will fail to connect +// depending on the StrictHostKeyChecking option. Depending +// how this option is set, it will print "Permission denied" +// and quit, or print "Host key verification failed." and +// quit. The later if StrictHostKeyChecking is "no". +// The former if StrictHostKeyChecking is +// "yes" or explicitly set to "ask". +QRegExp KSshProcess::hostKeyVerifyFailedMsg[] = { + QRegExp("Host key verification failed\\."), + QRegExp("Host key verification failed\\."), + QRegExp("Disconnected; key exchange or algorithm? negotiation failed \\(Key exchange failed\\.\\)\\.") +}; + +const char * const KSshProcess::connectionClosedMsg[] = { + "Connection closed by remote host", + "Connection closed by remote host", + "Connection closed by remote host" +}; + + +void KSshProcess::SIGCHLD_handler(int) { + while(waitpid(-1, NULL, WNOHANG) > 0); +} + +void KSshProcess::installSignalHandlers() { + struct sigaction act; + memset(&act,0,sizeof(act)); + act.sa_handler = SIGCHLD_handler; + act.sa_flags = 0 +#ifdef SA_NOCLDSTOP + | SA_NOCLDSTOP +#endif +#ifdef SA_RESTART + | SA_RESTART +#endif + ; + sigaction(SIGCHLD,&act,NULL); +} + +void KSshProcess::removeSignalHandlers() { + struct sigaction act; + memset(&act,0,sizeof(act)); + act.sa_handler = SIG_DFL; + sigaction(SIGCHLD,&act,NULL); +} + +KSshProcess::KSshProcess() + : mVersion(UNKNOWN_VER), mConnected(false), + mRunning(false), mConnectState(0) { + mSshPath = KStandardDirs::findExe(QString::fromLatin1("ssh")); + kdDebug(KSSHPROC) << "KSshProcess::KSshProcess(): ssh path [" << + mSshPath << "]" << endl; + + installSignalHandlers(); +} + +KSshProcess::KSshProcess(QString pathToSsh) + : mSshPath(pathToSsh), mVersion(UNKNOWN_VER), mConnected(false), + mRunning(false), mConnectState(0) { + installSignalHandlers(); +} + +KSshProcess::~KSshProcess(){ + disconnect(); + removeSignalHandlers(); + while(waitpid(-1, NULL, WNOHANG) > 0); +} + +bool KSshProcess::setSshPath(QString pathToSsh) { + mSshPath = pathToSsh; + version(); + if( mVersion == UNKNOWN_VER ) + return false; + + return true; +} + +KSshProcess::SshVersion KSshProcess::version() { + QString cmd; + cmd = mSshPath+" -V 2>&1"; + + // Get version string from ssh client. + FILE *p; + if( (p = popen(cmd.latin1(), "r")) == NULL ) { + kdDebug(KSSHPROC) << "KSshProcess::version(): " + "failed to start ssh: " << strerror(errno) << endl; + return UNKNOWN_VER; + } + + // Determine of the version from the version string. + size_t len; + char buf[128]; + if( (len = fread(buf, sizeof(char), sizeof(buf)-1, p)) == 0 ) { + kdDebug(KSSHPROC) << "KSshProcess::version(): " + "Read of ssh version string failed " << + strerror(ferror(p)) << endl; + return UNKNOWN_VER; + } + if( pclose(p) == -1 ) { + kdError(KSSHPROC) << "KSshProcess::version(): pclose failed." << endl; + } + buf[len] = '\0'; + QString ver; + ver = buf; + kdDebug(KSSHPROC) << "KSshProcess::version(): " + "got version string [" << ver << "]" << endl; + + mVersion = UNKNOWN_VER; + for(int i = 0; i < SSH_VER_MAX; i++) { + if( ver.find(versionStrs[i]) != -1 ) { + mVersion = (SshVersion)i; + break; + } + } + + kdDebug(KSSHPROC) << "KSshPRocess::version(): version number = " + << mVersion << endl; + + if( mVersion == UNKNOWN_VER ) { + kdDebug(KSSHPROC) << "KSshProcess::version(): " + "Sorry, I don't know about this version of ssh" << endl; + mError = ERR_UNKNOWN_VERSION; + return UNKNOWN_VER; + } + + return mVersion; +} +/* +QString KSshProcess::versionStr() { + if( mVersion == UNKNOWN_VER ) { + version(); + if( mVersion == UNKNOWN_VER ) + return QString::null; + } + + return QString::fromLatin1(versionStrs[mVersion]); +} +*/ + +bool KSshProcess::setOptions(const SshOptList& opts) { + kdDebug(KSSHPROC) << "KSshProcess::setOptions()" << endl; + mArgs.clear(); + SshOptListConstIterator it; + QString cmd, subsystem; + mPassword = mUsername = mHost = QString::null; + QCString tmp; + for(it = opts.begin(); it != opts.end(); ++it) { + //kdDebug(KSSHPROC) << "opt.opt = " << (*it).opt << endl; + //kdDebug(KSSHPROC) << "opt.str = " << (*it).str << endl; + //kdDebug(KSSHPROC) << "opt.num = " << (*it).num << endl; + switch( (*it).opt ) { + case SSH_VERBOSE: + mArgs.append("-v"); + break; + + case SSH_SUBSYSTEM: + subsystem = (*it).str; + break; + + case SSH_PORT: + mArgs.append("-p"); + tmp.setNum((*it).num); + mArgs.append(tmp); + mPort = (*it).num; + break; + + case SSH_HOST: + mHost = (*it).str; + break; + + case SSH_USERNAME: + mArgs.append("-l"); + mArgs.append((*it).str.latin1()); + mUsername = (*it).str; + break; + + case SSH_PASSWD: + mPassword = (*it).str; + break; + + case SSH_PROTOCOL: + if( mVersion <= OPENSSH ) { + tmp = "Protocol="; + tmp += QString::number((*it).num).latin1(); + mArgs.append("-o"); + mArgs.append(tmp); + } + else if( mVersion <= SSH ) { + if( (*it).num == 1 ) { + mArgs.append("-1"); + } + // else uses version 2 by default + } + break; + + case SSH_FORWARDX11: + tmp = "ForwardX11="; + tmp += (*it).boolean ? "yes" : "no"; + mArgs.append("-o"); + mArgs.append(tmp); + break; + + case SSH_FORWARDAGENT: + tmp = "ForwardAgent="; + tmp += (*it).boolean ? "yes" : "no"; + mArgs.append("-o"); + mArgs.append(tmp); + break; + + case SSH_ESCAPE_CHAR: + if( (*it).num == -1 ) + tmp = "none"; + else + tmp = (char)((*it).num); + mArgs.append("-e"); + mArgs.append(tmp); + break; + + case SSH_OPTION: + // don't allow NumberOfPasswordPrompts or StrictHostKeyChecking + // since KSshProcess depends on specific setting of these for + // preforming authentication correctly. + tmp = (*it).str.latin1(); + if( tmp.contains("NumberOfPasswordPrompts") || + tmp.contains("StrictHostKeyChecking") ) { + mError = ERR_INVALID_OPT; + return false; + } + else { + mArgs.append("-o"); + mArgs.append(tmp); + } + break; + + case SSH_COMMAND: + cmd = (*it).str; + break; + + default: + kdDebug(KSSHPROC) << "KSshProcess::setOptions(): " + "unrecognized ssh opt " << (*it).opt << endl; + } + } + + if( !subsystem.isEmpty() && !cmd.isEmpty() ) { + kdDebug(KSSHPROC) << "KSshProcess::setOptions(): " + "cannot use a subsystem and command at the same time" << endl; + mError = ERR_CMD_SUBSYS_CONFLICT; + mErrorMsg = i18n("Cannot specify a subsystem and command at the same time."); + return false; + } + + // These options govern the behavior of ssh and + // cannot be defined by the user + //mArgs.append("-o"); + //mArgs.append("StrictHostKeyChecking=ask"); + mArgs.append("-v"); // So we get a message that the + // connection was successful + if( mVersion <= OPENSSH ) { + // nothing + } + else if( mVersion <= SSH ) { + mArgs.append("-o"); // So we can check if the connection was successful + mArgs.append("AuthenticationSuccessMsg=yes"); + } + + if( mHost.isEmpty() ) { + kdDebug(KSSHPROC) << "KSshProcess::setOptions(): " + "a host name must be supplied" << endl; + return false; + } + else { + mArgs.append(mHost.latin1()); + } + + if( !subsystem.isEmpty() ) { + mArgs.append("-s"); + mArgs.append(subsystem.latin1()); + } + + if( !cmd.isEmpty() ) { + mArgs.append(cmd.latin1()); + } + + return true; +} + +void KSshProcess::printArgs() { + QValueListIterator<QCString> it; + for( it = mArgs.begin(); it != mArgs.end(); ++it) { + kdDebug(KSSHPROC) << "arg: " << *it << endl; + } +} + + +int KSshProcess::error(QString& msg) { + kdDebug(KSSHPROC) << "KSshProcess::error()" << endl; + kdDebug() << mErrorMsg << endl; + msg = mErrorMsg; + return mError; +} + +void KSshProcess::kill(int signal) { + int pid = ssh.pid(); + + kdDebug(KSSHPROC) << "KSshProcess::kill(signal:" << signal + << "): ssh pid is " << pid << endl; + kdDebug(KSSHPROC) << "KSshPRocess::kill(): we are " + << (mConnected ? "" : "not ") << "connected" << endl; + kdDebug(KSSHPROC) << "KSshProcess::kill(): we are " + << (mRunning ? "" : "not ") << "running a ssh process" << endl; + + if( mRunning && pid > 1 ) { + // Kill the child process... + if ( ::kill(pid, signal) == 0 ) { + // clean up if we tried to kill the process + if( signal == SIGTERM || signal == SIGKILL ) { + while(waitpid(-1, NULL, WNOHANG) > 0); + mConnected = false; + mRunning = false; + } + } + else + kdDebug(KSSHPROC) << "KSshProcess::kill(): kill failed" << endl; + } + else + kdDebug(KSSHPROC) << "KSshProcess::kill(): " + "Refusing to kill ssh process" << endl; +} + + + +/** + * Try to open an ssh connection. + * SSH prints certain messages to certain file descriptiors: + * passwordPrompt - pty + * passphrasePrompt - pty + * authSuccessMsg - stderr (OpenSSH), + * authFailedMsg - stderr + * hostKeyMissing - stderr + * hostKeyChanged - stderr + * continuePrompt - stderr + * + * We will use a select to wait for a line on each descriptor. Then get + * each line that available and take action based on it. The type + * of messages we are looking for and the action we take on each + * message are: + * passwordPrompt - Return false, set error to ERR_NEED_PASSWD. + * On the next call to connect() we expect a password + * to be available. + * + * passpharsePrompt - Return false, set error to ERR_NEED_PASSPHRASE. + * On the next call to connect() we expect a + * passphrase to be available. + * + * authSuccessMsg - Return true, as we have successfully established a + * ssh connection. + * + * authFailedMsg - Return false, set error to ERR_AUTH_FAILED. We + * were unable to authenticate the connection given + * the available authentication information. + * + * hostKeyMissing - Return false, set error to ERR_NEW_HOST_KEY. Caller + * must call KSshProcess.acceptHostKey(bool) to accept + * or reject the key before calling connect() again. + * + * hostKeyChanged - Return false, set error to ERR_DIFF_HOST_KEY. Caller + * must call KSshProcess.acceptHostKey(bool) to accept + * or reject the key before calling connect() again. + * + * continuePrompt - Send 'yes' or 'no' to accept or reject a key, + * respectively. + * + */ + + +void KSshProcess::acceptHostKey(bool accept) { + kdDebug(KSSHPROC) << "KSshProcess::acceptHostKey(accept:" + << accept << ")" << endl; + mAcceptHostKey = accept; +} + +void KSshProcess::setPassword(QString password) { + kdDebug(KSSHPROC) << "KSshProcess::setPassword(password:xxxxxxxx)" << endl; + mPassword = password; +} + +QString KSshProcess::getLine() { + static QStringList buffer; + QString line = QString::null; + QCString ptyLine, errLine; + + if( buffer.empty() ) { + // PtyProcess buffers lines. First check that there + // isn't something on the PtyProces buffer or that there + // is not data ready to be read from the pty or stderr. + ptyLine = ssh.readLineFromPty(false); + errLine = ssh.readLineFromStderr(false); + + // If PtyProcess did have something for us, get it and + // place it in our line buffer. + if( ! ptyLine.isEmpty() ) { + buffer.prepend(QString(ptyLine)); + } + + if( ! errLine.isEmpty() ) { + buffer.prepend(QString(errLine)); + } + + // If we still don't have anything in our buffer so there must + // not be anything on the pty or stderr. Setup a select() + // to wait for some data from SSH. + if( buffer.empty() ) { + //kdDebug(KSSHPROC) << "KSshProcess::getLine(): " << + // "Line buffer empty, calling select() to wait for data." << endl; + int errfd = ssh.stderrFd(); + int ptyfd = ssh.fd(); + fd_set rfds; + fd_set efds; + struct timeval tv; + + // find max file descriptor + int maxfd = ptyfd > errfd ? ptyfd : errfd; + + FD_ZERO(&rfds); + FD_SET(ptyfd, &rfds); // Add pty file descriptor + FD_SET(errfd, &rfds); // Add std error file descriptor + + FD_ZERO(&efds); + FD_SET(ptyfd, &efds); + FD_SET(errfd, &efds); + + tv.tv_sec = 60; tv.tv_usec = 0; // 60 second timeout + + // Wait for a message from ssh on stderr or the pty. + int ret = -1; + do + ret = ::select(maxfd+1, &rfds, NULL, &efds, &tv); + while( ret == -1 && errno == EINTR ); + + // Handle any errors from select + if( ret == 0 ) { + kdDebug(KSSHPROC) << "KSshProcess::connect(): " << + "timed out waiting for a response" << endl; + mError = ERR_TIMED_OUT; + return QString::null; + } + else if( ret == -1 ) { + kdDebug(KSSHPROC) << "KSshProcess::connect(): " + << "select error: " << strerror(errno) << endl; + mError = ERR_INTERNAL; + return QString::null; + } + + // We are not respecting any type of order in which the + // lines were received. Who knows whether pty or stderr + // had data on it first. + if( FD_ISSET(ptyfd, &rfds) ) { + ptyLine = ssh.readLineFromPty(false); + buffer.prepend(QString(ptyLine)); + //kdDebug(KSSHPROC) << "KSshProcess::getLine(): " + // "line from pty -" << ptyLine << endl; + } + + if( FD_ISSET(errfd, &rfds) ) { + errLine = ssh.readLineFromStderr(false); + buffer.prepend(QString(errLine)); + //kdDebug(KSSHPROC) << "KSshProcess::getLine(): " + // "line from err -" << errLine << endl; + } + + if( FD_ISSET(ptyfd, &efds) ) { + kdDebug(KSSHPROC) << "KSshProcess::getLine(): " + "Exception on pty file descriptor." << endl; + } + + if( FD_ISSET(errfd, &efds) ) { + kdDebug(KSSHPROC) << "KSshProcess::getLine(): " + "Exception on std err file descriptor." << endl; + } + + } + } + + // We should have something in our buffer now. + // Return the last line. + //it = buffer.end(); + //line = *it; + //buffer.remove(it); + + line = buffer.last(); + buffer.pop_back(); + + if( line.isNull() && buffer.count() > 0 ) { + line = buffer.last(); + buffer.pop_back(); + } + +// kdDebug(KSSHPROC) << "KSshProcess::getLine(): " << +// buffer.count() << " lines in buffer" << endl; + kdDebug(KSSHPROC) << "KSshProcess::getLine(): " + "ssh: " << line << endl; + + + return line; +} + +// All the different states we could go through while trying to connect. +enum sshConnectState { + STATE_START, STATE_TRY_PASSWD, STATE_WAIT_PROMPT, STATE_NEW_KEY_CONTINUE, + STATE_DIFF_KEY_CONTINUE, STATE_FATAL, STATE_WAIT_CONTINUE_PROMPT, + STATE_SEND_CONTINUE, STATE_AUTH_FAILED, STATE_NEW_KEY_WAIT_CONTINUE, + STATE_DIFF_KEY_WAIT_CONTINUE, STATE_TRY_PASSPHRASE +}; + +// Print the state as a string. Good for debugging +const char* stateStr(int state) { + switch(state) { + case STATE_START: + return "STATE_START"; + case STATE_TRY_PASSWD: + return "STATE_TRY_PASSWD"; + case STATE_WAIT_PROMPT: + return "STATE_WAIT_PROMPT"; + case STATE_NEW_KEY_CONTINUE: + return "STATE_NEW_KEY_CONTINUE"; + case STATE_DIFF_KEY_CONTINUE: + return "STATE_DIFF_KEY_CONTINUE"; + case STATE_FATAL: + return "STATE_FATAL"; + case STATE_WAIT_CONTINUE_PROMPT: + return "STATE_WAIT_CONTINUE_PROMPT"; + case STATE_SEND_CONTINUE: + return "STATE_SEND_CONTINE"; + case STATE_AUTH_FAILED: + return "STATE_AUTH_FAILED"; + case STATE_NEW_KEY_WAIT_CONTINUE: + return "STATE_NEW_KEY_WAIT_CONTINUE"; + case STATE_DIFF_KEY_WAIT_CONTINUE: + return "STATE_DIFF_KEY_WAIT_CONTINUE"; + case STATE_TRY_PASSPHRASE: + return "STATE_TRY_PASSPHRASE"; + } + return "UNKNOWN"; +} + +bool KSshProcess::connect() { + if( mVersion == UNKNOWN_VER ) { + // we don't know the ssh version yet, so find out + version(); + if( mVersion == -1 ) { + return false; + } + } + + // We'll put a limit on the number of state transitions + // to ensure we don't go out of control. + int transitionLimit = 500; + + while(--transitionLimit) { + kdDebug(KSSHPROC) << "KSshProcess::connect(): " + << "Connect state " << stateStr(mConnectState) << endl; + + QString line; // a line from ssh + QString msgBuf; // buffer for important messages from ssh + // which are to be returned to the user + + switch(mConnectState) { + // STATE_START: + // Executes the ssh binary with the options provided. If no options + // have been specified, sets error and returns false. Continue to + // state 1 if execution is successful, otherwise set error and + // return false. + case STATE_START: + // reset some key values to safe values + mAcceptHostKey = false; + mKeyFingerprint = QString::null; + mKnownHostsFile = QString::null; + + if( mArgs.isEmpty() ) { + kdDebug(KSSHPROC) << "KSshProcess::connect(): ssh options " + "need to be set first using setArgs()" << endl; + mError = ERR_NO_OPTIONS; + mErrorMsg = i18n("No options provided for ssh execution."); + return false; + } + + if( ssh.exec(mSshPath.latin1(), mArgs) ) { + kdDebug(KSSHPROC) << + "KSshProcess::connect(): ssh exec failed" << endl; + mError = ERR_CANNOT_LAUNCH; + mErrorMsg = i18n("Failed to execute ssh process."); + return false; + } + + kdDebug(KSSHPROC) << "KSshPRocess::connect(): ssh pid = " << ssh.pid() << endl; + + // set flag to indicate what have started a ssh process + mRunning = true; + mConnectState = STATE_WAIT_PROMPT; + break; + + // STATE_WAIT_PROMPT: + // Get a line of input from the ssh process. Check the contents + // of the line to determine the next state. Ignore the line + // if we don't recognize its contents. If the line contains + // the continue prompt, we have an error since we should never + // get that line in this state. Set ERR_INVALID_STATE error + // and return false. + case STATE_WAIT_PROMPT: + line = getLine(); + if( line.isNull() ) { + kdDebug(KSSHPROC) << "KSshProcess::connect(): " + "Got null line in STATE_WAIT_PROMPT." << endl; + mError = ERR_INTERACT; + mErrorMsg = + i18n("Error encountered while talking to ssh."); + mConnectState = STATE_FATAL; + } + else if( line.find(QString::fromLatin1(passwordPrompt[mVersion]), 0, false) != -1 ) { + mConnectState = STATE_TRY_PASSWD; + } + else if( line.find(passphrasePrompt[mVersion]) != -1 ) { + mConnectState = STATE_TRY_PASSPHRASE; + } + else if( line.find(authSuccessMsg[mVersion]) != -1 ) { + return true; + } + else if( line.find(authFailedMsg[mVersion]) != -1 + && line.find(tryAgainMsg[mVersion]) == -1 ) { + mConnectState = STATE_AUTH_FAILED; + } + else if( line.find(hostKeyMissingMsg[mVersion]) != -1 ) { + mConnectState = STATE_NEW_KEY_WAIT_CONTINUE; + } + else if( line.find(hostKeyChangedMsg[mVersion]) != -1 ) { + mConnectState = STATE_DIFF_KEY_WAIT_CONTINUE; + } + else if( line.find(continuePrompt[mVersion]) != -1 ) { + //mConnectState = STATE_SEND_CONTINUE; + kdDebug(KSSHPROC) << "KSshProcess:connect(): " + "Got continue prompt where we shouldn't (STATE_WAIT_PROMPT)" + << endl; + mError = ERR_INTERACT; + mErrorMsg = + i18n("Error encountered while talking to ssh."); + } + else if( line.find(connectionClosedMsg[mVersion]) != -1 ) { + mConnectState = STATE_FATAL; + mError = ERR_CLOSED_BY_REMOTE_HOST; + mErrorMsg = i18n("Connection closed by remote host."); + } + else if( line.find(changeHostKeyOnDiskPrompt[mVersion]) != -1 ) { + // always say yes to this. It always comes after commerical ssh + // prints a "continue to connect prompt". We assume that if the + // user choose to continue, then they also want to save the + // host key to disk. + ssh.writeLine("yes"); + } + else { + // ignore line + } + break; + + // STATE_TRY_PASSWD: + // If we have password send it to the ssh process, else + // set error ERR_NEED_PASSWD and return false to the caller. + // The caller then must then call KSshProcess::setPassword(QString) + // before calling KSshProcess::connect() again. + // + // Almost exactly liek STATE_TRY_PASSPHRASE. Check there if you + // make changes here. + case STATE_TRY_PASSWD: + // We have a password prompt waiting for us to supply + // a password. Send that password to ssh. If the caller + // did not supply a password like we asked, then ask + // again. + if( !mPassword.isEmpty() ) { +// ssh.WaitSlave(); + ssh.writeLine(mPassword.latin1()); + + // Overwrite the password so it isn't in memory. + mPassword.fill(QChar('X')); + + // Set the password to null so we will request another + // password if this one fails. + mPassword = QString::null; + + mConnectState = STATE_WAIT_PROMPT; + } + else { + kdDebug(KSSHPROC) << "KSshProcess::connect() " + "Need password from caller." << endl; + // The caller needs to supply a password before + // connecting can continue. + mError = ERR_NEED_PASSWD; + mErrorMsg = i18n("Please supply a password."); + mConnectState = STATE_TRY_PASSWD; + return false; + } + break; + + // STATE_TRY_KEY_PASSPHRASE: + // If we have passphrase send it to the ssh process, else + // set error ERR_NEED_PASSPHRASE and return false to the caller. + // The caller then must then call KSshProcess::setPassword(QString) + // before calling KSshProcess::connect() again. + // + // Almost exactly like STATE_TRY_PASSWD. The only difference is + // the error we set if we don't have a passphrase. We duplicate + // this code to keep in the spirit of the state machine. + case STATE_TRY_PASSPHRASE: + // We have a passphrase prompt waiting for us to supply + // a passphrase. Send that passphrase to ssh. If the caller + // did not supply a passphrase like we asked, then ask + // again. + if( !mPassword.isEmpty() ) { +// ssh.WaitSlave(); + ssh.writeLine(mPassword.latin1()); + + // Overwrite the password so it isn't in memory. + mPassword.fill(QChar('X')); + + // Set the password to null so we will request another + // password if this one fails. + mPassword = QString::null; + + mConnectState = STATE_WAIT_PROMPT; + } + else { + kdDebug(KSSHPROC) << "KSshProcess::connect() " + "Need passphrase from caller." << endl; + // The caller needs to supply a passphrase before + // connecting can continue. + mError = ERR_NEED_PASSPHRASE; + mErrorMsg = i18n("Please supply the passphrase for " + "your SSH private key."); + mConnectState = STATE_TRY_PASSPHRASE; + return false; + } + break; + + // STATE_AUTH_FAILED: + // Authentication has failed. Tell the caller by setting the + // ERR_AUTH_FAILED error and returning false. If + // auth has failed then ssh should have exited, but + // we will kill it to make sure. + case STATE_AUTH_FAILED: + mError = ERR_AUTH_FAILED; + mErrorMsg = i18n("Authentication to %1 failed").arg(mHost); + mConnectState = STATE_FATAL; + break; + + // STATE_NEW_KEY_WAIT_CONTINUE: + // Grab lines from ssh until we get a continue prompt or a auth + // denied. We will get the later if StrictHostKeyChecking is set + // to yes. Go to STATE_NEW_KEY_CONTINUE if we get a continue prompt. + case STATE_NEW_KEY_WAIT_CONTINUE: + line = getLine(); + if( line.isNull() ) { + kdDebug(KSSHPROC) << "KSshProcess::connect(): " + "Got null line in STATE_NEW_KEY_WAIT_CONTINUE." << endl; + mError = ERR_INTERACT; + mErrorMsg = + i18n("Error encountered while talking to ssh."); + mConnectState = STATE_FATAL; + } + else if( (line.find(authFailedMsg[mVersion]) != -1 + && line.find(tryAgainMsg[mVersion]) == -1) + || line.find(hostKeyVerifyFailedMsg[mVersion]) != -1 ) { + mError = ERR_AUTH_FAILED_NEW_KEY; + mErrorMsg = i18n( + "The identity of the remote host '%1' could not be verified " + "because the host's key is not in the \"known hosts\" file." + ).arg(mHost); + + if( mKnownHostsFile.isEmpty() ) { + mErrorMsg += i18n( + " Manually, add the host's key to the \"known hosts\" " + "file or contact your administrator." + ); + } + else { + mErrorMsg += i18n( + " Manually, add the host's key to %1 " + "or contact your administrator." + ).arg(mKnownHostsFile); + } + + mConnectState = STATE_FATAL; + } + else if( line.find(continuePrompt[mVersion]) != -1 ) { + mConnectState = STATE_NEW_KEY_CONTINUE; + } + else if( line.find(connectionClosedMsg[mVersion]) != -1 ) { + mConnectState = STATE_FATAL; + mError = ERR_CLOSED_BY_REMOTE_HOST; + mErrorMsg = i18n("Connection closed by remote host."); + } + else if( line.find(keyFingerprintMsg[mVersion]) != -1 ) { + mKeyFingerprint = keyFingerprintMsg[mVersion].cap(); + kdDebug(KSSHPROC) << "Found key fingerprint: " << mKeyFingerprint << endl; + mConnectState = STATE_NEW_KEY_WAIT_CONTINUE; + } + else { + // ignore line + } + break; + + + // STATE_NEW_KEY_CONTINUE: + // We got a continue prompt for the new key message. Set the error + // message to reflect this, return false and hope for caller response. + case STATE_NEW_KEY_CONTINUE: + mError = ERR_NEW_HOST_KEY; + mErrorMsg = i18n( + "The identity of the remote host '%1' could not be " + "verified. The host's key fingerprint is:\n%2\nYou should " + "verify the fingerprint with the host's administrator before " + "connecting.\n\n" + "Would you like to accept the host's key and connect anyway? " + ).arg(mHost).arg(mKeyFingerprint); + mConnectState = STATE_SEND_CONTINUE; + return false; + + // STATE_DIFF_KEY_WAIT_CONTINUE: + // Grab lines from ssh until we get a continue prompt or a auth + // denied. We will get the later if StrictHostKeyChecking is set + // to yes. Go to STATE_DIFF_KEY_CONTINUE if we get a continue prompt. + case STATE_DIFF_KEY_WAIT_CONTINUE: + line = getLine(); + if( line.isNull() ) { + kdDebug(KSSHPROC) << "KSshProcess::connect(): " + "Got null line in STATE_DIFF_KEY_WAIT_CONTINUE." << endl; + mError = ERR_INTERACT; + mErrorMsg = + i18n("Error encountered while talking to ssh."); + mConnectState = STATE_FATAL; + } + else if( (line.find(authFailedMsg[mVersion]) != -1 + && line.find(tryAgainMsg[mVersion]) == -1) + || line.find(hostKeyVerifyFailedMsg[mVersion]) != -1 ) { + mError = ERR_AUTH_FAILED_DIFF_KEY; + mErrorMsg = i18n( + "WARNING: The identity of the remote host '%1' has changed!\n\n" + "Someone could be eavesdropping on your connection, or the " + "administrator may have just changed the host's key. " + "Either way, you should verify the host's key fingerprint with the host's " + "administrator. The key fingerprint is:\n%2\n" + "Add the correct host key to \"%3\" to " + "get rid of this message." + ).arg(mHost).arg(mKeyFingerprint).arg(mKnownHostsFile); + mConnectState = STATE_FATAL; + } + else if( line.find(continuePrompt[mVersion]) != -1 ) { + mConnectState = STATE_DIFF_KEY_CONTINUE; + } + else if( line.find(keyFingerprintMsg[mVersion]) != -1 ) { + mKeyFingerprint = keyFingerprintMsg[mVersion].cap(); + kdDebug(KSSHPROC) << "Found key fingerprint: " << mKeyFingerprint << endl; + mConnectState = STATE_DIFF_KEY_WAIT_CONTINUE; + } + else if( line.find(knownHostsFileMsg[mVersion]) != -1 ) { + mKnownHostsFile = (knownHostsFileMsg[mVersion]).cap(1); + kdDebug(KSSHPROC) << "Found known hosts file name: " << mKnownHostsFile << endl; + mConnectState = STATE_DIFF_KEY_WAIT_CONTINUE; + } + else { + // ignore line + } + break; + + // STATE_DIFF_KEY_CONTINUE: + // We got a continue prompt for the different key message. + // Set ERR_DIFF_HOST_KEY error + // and return false to signal need to caller action. + case STATE_DIFF_KEY_CONTINUE: + mError = ERR_DIFF_HOST_KEY; + mErrorMsg = i18n( + "WARNING: The identity of the remote host '%1' has changed!\n\n" + "Someone could be eavesdropping on your connection, or the " + "administrator may have just changed the host's key. " + "Either way, you should verify the host's key fingerprint with the host's " + "administrator before connecting. The key fingerprint is:\n%2\n\n" + "Would you like to accept the host's new key and connect anyway?" + ).arg(mHost).arg(mKeyFingerprint); + mConnectState = STATE_SEND_CONTINUE; + return false; + + // STATE_SEND_CONTINUE: + // We found a continue prompt. Send our answer. + case STATE_SEND_CONTINUE: + if( mAcceptHostKey ) { + kdDebug(KSSHPROC) << "KSshProcess::connect(): " + "host key accepted" << endl; + ssh.writeLine("yes"); + mConnectState = STATE_WAIT_PROMPT; + } + else { + kdDebug(KSSHPROC) << "KSshProcess::connect(): " + "host key rejected" << endl; + ssh.writeLine("no"); + mError = ERR_HOST_KEY_REJECTED; + mErrorMsg = i18n("Host key was rejected."); + mConnectState = STATE_FATAL; + } + break; + + // STATE_FATAL: + // Something bad happened that we cannot recover from. + // Kill the ssh process and set flags to show we have + // ended the connection and killed ssh. + // + // mError and mErrorMsg should be set by the immediately + // previous state. + case STATE_FATAL: + kill(); + mConnected = false; + mRunning = false; + mConnectState = STATE_START; + // mError, mErroMsg set by last state + return false; + + default: + kdDebug(KSSHPROC) << "KSshProcess::connect(): " + "Invalid state number - " << mConnectState << endl; + mError = ERR_INVALID_STATE; + mConnectState = STATE_FATAL; + } + } + + // we should never get here + kdDebug(KSSHPROC) << "KSshProcess::connect(): " << + "After switch(). We shouldn't be here." << endl; + mError = ERR_INTERNAL; + return false; +} + +void KSshProcess::disconnect() { + kill(); + mConnected = false; + mRunning = false; + mConnectState = STATE_START; +} + diff --git a/kioslave/sftp/ksshprocess.h b/kioslave/sftp/ksshprocess.h new file mode 100644 index 000000000..2ec1abfd6 --- /dev/null +++ b/kioslave/sftp/ksshprocess.h @@ -0,0 +1,623 @@ +/*************************************************************************** + ksshprocess.h - description + ------------------- + begin : Tue Jul 31 2001 + copyright : (C) 2001 by Lucas Fisher + email : ljfisher@purdue.edu + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef KSSHPROCESS_H +#define KSSHPROCESS_H + +#include <sys/types.h> +#include <sys/wait.h> +#include <signal.h> +#include <unistd.h> + +#include <qvaluelist.h> + +#include <kdebug.h> + +#include "process.h" + +#define KSSHPROC 7120 + +/** + * Provides version independent access to ssh. Currently supported + * versions of SSH are: + * OpenSSH 2.9p1 + * OpenSSH 2.9p2 + * OpenSSH 3.0 + * OpenSSH 3.1 + * Commercial SSH 3.0.0 + * Other versions of OpenSSH and commerical SSH will probably work also. + * + * To setup a SSH connection first create a list of options to use and tell + * KSshProcess about your options. Then start the ssh connection. Once the + * connection is setup use the stdin, stdout, stderr, and pty file descriptors + * to communicate with ssh. For a detailed example of how to use, see + * ksshprocesstest.cpp. + * + * @author Lucas Fisher + * + * Example: Connect to ssh server on localhost + * KSshProcess::SshOpt opt; + * KSshProcess::SshOptList options; + * + * opt.opt = KSshProcess::SSH_HOST; + * opt.str = "localhost"; + * options.append(opt); + * + * opt.opt = KSshProcess::SSH_USERNAME; + * opt.str = "me"; + * options.append(opt); + * + * KSshProcess ssh; + * if( !ssh.setOptions(options) ) { + * int err = ssh.error(); + * // process error + * return false; + * } + * + * int err; + * QString errMsg; + * while( !ssh.connect() ) { + * err = ssh.error(errMsg); + * + * switch( err ) { + * case KSshProcess::ERR_NEW_HOST_KEY: + * case KSshProcess::ERR_DIFF_HOST_KEY: + * // ask user to accept key + * if( acceptHostKey ) { + * ssh.acceptKey(true); + * } + * break; + * + * case KSshProcess::ERR_NEED_PASSWORD: + * // ask user for password + * ssh.password(userPassword); + * break; + * + * case KSshProcess::ERR_NEED_KEY_PASSPHRASE: + * // ask user for their key passphrase + * ssh.keyPassphrase(keyPassphrase); + * break; + * + * default: + * // somethings wrong, alert user + * return; + * } + * } + * // We have an open ssh connection to localhost + * + */ + +class KSshProcess { +public: + /** + * SSH Option + * + * Stores SSH options for use with KSshProcess. + * + * SSH options are configured much like UDS entries. + * Each option is assigned a constant and a string, bool, + * or number is assigned based on the option. + * + * @author Lucas Fisher (ljfisher@iastate.edu) + */ + class SshOpt { + public: + Q_UINT32 opt; + QString str; + Q_INT32 num; + bool boolean; + }; + + /** + * List of SshOptions and associated iterators + */ + typedef QValueList<SshOpt> SshOptList; + typedef QValueListIterator<SshOpt> SshOptListIterator; + typedef QValueListConstIterator<SshOpt> SshOptListConstIterator; + + /** + * Ssh versions supported by KSshProcess. Subject to change + * at any time. + */ + enum SshVersion { + OPENSSH_3_6, + OPENSSH, + SSH, + SSH_VER_MAX, + UNKNOWN_VER + }; + + /** + * SSH options supported by KSshProcess. Set SshOpt::opt to one of these + * values. + */ + // we cannot do this like UDSAtomType (ORing the type with the name) because + // we have too many options for ssh and not enough bits. + enum SshOptType { + /** + * Request server to invoke subsystem. (str) + */ + SSH_SUBSYSTEM, + /** + * Connect to port on the server. (num) + */ + SSH_PORT, + /** + * Connect to host. (str) + */ + SSH_HOST, + /** + * connect using this username. (str) + */ + SSH_USERNAME, + /** + * connect using this password. (str) + */ + SSH_PASSWD, + /** + * connect using this version of the SSH protocol. num == 1 or 2 + */ + SSH_PROTOCOL, + /** + * whether to forward X11 connections. (boolean) + */ + SSH_FORWARDX11, + /** + * whether to do agent forwarding. (boolean) + */ + SSH_FORWARDAGENT, + /** + * use as escape character. 0 for none (num) + */ + SSH_ESCAPE_CHAR, + /** + * command for ssh to perform once it is connected (str) + */ + SSH_COMMAND, + /** + * Set ssh verbosity. This may be added multiple times. It may also cause KSSHProcess + * to fail since we don't understand all the debug messages. + */ + SSH_VERBOSE, + /** + * Set a ssh option as one would find in the ssh_config file + * The str member should be set to 'optName value' + */ + SSH_OPTION, + /** + * Set some other option not supported by KSSHProcess. The option should + * be specified in the str member of SshOpt. Careful with this since + * not all versions of SSH support the same options. + */ + SSH_OTHER, + SSH_OPT_MAX // always last + }; // that's all for now + + /** + * Errors that KSshProcess can encounter. When a member function returns + * false, call error() to retrieve one of these error codes. + */ + enum SshError { + /** + * Don't recognize the ssh version + */ + ERR_UNKNOWN_VERSION, + /** + * Cannot lauch ssh client + */ + ERR_CANNOT_LAUNCH, + /** + * Interaction with the ssh client failed. This happens when we can't + * find the password prompt or something similar + */ + ERR_INTERACT, + /** + * Arguments for both a remotely executed subsystem and command were provide. + * Only one or the other may be used + */ + ERR_CMD_SUBSYS_CONFLICT, + /** + * No password was supplied + */ + ERR_NEED_PASSWD, + /** + * No passphrase was supplied. + */ + ERR_NEED_PASSPHRASE, + /** + * No usename was supplied + */ + ERR_NEED_USERNAME, + /** + * Timed out waiting for a response from ssh or the server + */ + ERR_TIMED_OUT, + /** + * Internal error, probably from a system call + */ + ERR_INTERNAL, + /** + * ssh was disconnect from the host + */ + ERR_DISCONNECTED, + /** + * No ssh options have been set. Call setArgs() before calling connect. + */ + ERR_NO_OPTIONS, + /** + * A host key was received from an unknown host. + * Call connect() with the acceptHostKey argument to accept the key. + */ + ERR_NEW_HOST_KEY, + /** + * A host key different from what is stored in the user's known_hosts file + * has be received. This is an indication of an attack + */ + ERR_DIFF_HOST_KEY, + /** + * A new or different host key was rejected by the caller. The ssh + * connection was terminated and the ssh process killed. + */ + ERR_HOST_KEY_REJECTED, + /** + * An invalid option was found in the SSH option list + */ + ERR_INVALID_OPT, + /** + * SSH accepted host key without prompting user. + */ + ERR_ACCEPTED_KEY, + /** + * Authentication failed + */ + ERR_AUTH_FAILED, + /** + * Authentication failed because a new host key was detected and + * SSH is configured with strict host key checking enabled. + */ + ERR_AUTH_FAILED_NEW_KEY, + /** + * Authentication failed because a changed host key was detected and + * SSH is configured with strict host key checking enabled. + */ + ERR_AUTH_FAILED_DIFF_KEY, + /** + * The remote host closed the connection for unknown reasons. + */ + ERR_CLOSED_BY_REMOTE_HOST, + /** + * We have no idea what happened + */ + ERR_UNKNOWN, + /** + * The connect state machine entered an invalid state. + */ + ERR_INVALID_STATE, + ERR_MAX + }; + + /** + * Initialize a SSH process using the first SSH binary found in the PATH + */ + KSshProcess(); + + /** + * Initialize a SSH process using the specified SSH binary. + * @param pathToSsh The fully qualified path name of the ssh binary + * KSshProcess should use to setup a SSH connection. + */ + KSshProcess(QString pathToSsh); + ~KSshProcess(); + + /** + * Set the ssh binary KSshProcess should use. This will only affect the + * next ssh connection attempt using this instance. + * + * @param pathToSsh Full path to the ssh binary. + * + * @return True if the ssh binary is found and KSshProcess + * recognizes the version. + * + */ + bool setSshPath(QString pathToSsh); + + /** + * Get the ssh version. + * + * @return The ssh version or -1 if KSshProcess does not recognize + * the ssh version. The returned value corresponds to the + * member of the SshVersion enum. + */ + SshVersion version(); + + /** + * Get a string describing the ssh version + * + * @return A string describing the ssh version recognized by KSshProcess + */ + //QString versionStr(); + + /** + * Get the last error encountered by KSshProcess. + * + * @param msg Set to the error message, if any, outputted by ssh when it is run. + * + * @return The error number. See SshError for descriptions. + */ + int error(QString& msg); + + /** + * Get the last error encountered by KSshProcess. + * @return The error number. See SshError for descriptions. + */ + int error() { return mError; } + + QString errorMsg() { return mErrorMsg; } + + /** + * Send a signal to the ssh process. Do not use this to end the + * ssh connection as it will not correctly reset the internal + * state of the KSshProcess object. Use KSshProcess::disconnect() + * instead. + * + * @param signal The signal to send to the ssh process. See 'kill -l' + * for a list of possible signals. + * The default signal is SIGKILL which kills ssh. + * + */ + void kill(int signal = SIGKILL); + + /** + * The pid of the ssh process started by this instance of KSshProcess. + * Only valid if KSshProcess::running() returns true; + * + * @return The pid of the running ssh process. + */ + int pid() { return ssh.pid(); } + + /** + * Whether a ssh connection has been established with a + * remote host. A establish connection means ssh has successfully + * authenticated with the remote host and user data can be transfered + * between the local and remote host. This cannot return + * true unless the most recent call to KSshProccess::connect() returned true. + * + * @return True if a ssh connection has been established with a remote + * host. False otherwise. + */ + bool connected() { return mConnected; } + + /** + * Whether a ssh process is currently running. This only indicates + * if a ssh process has been started and is still running. It does not + * tell if authentication has been successful. This may return true + * even if the most recent call to KSshProcess::connect() returned false. + * + * @return True if a ssh process started by this instance of KSshProcess + * is running. False otherwise. + */ + bool running() { return mRunning; } + + /** + * Print the command line arguments ssh is run with using kdDebug. + */ + void printArgs(); + + /** + * Set the SSH options. + * This must be called before connect(). See SshOptType for a list of + * supported ssh options. The required options are SSH_USERNAME + * and SSH_HOST. + * + * To reset the saved options, just recall setOptions() again with + * a different options list. + * + * @param opts A list of SshOpt objects specifying the ssh options. + * + * @return True if all options are valid. False if unrecognized options + * or a required option is missing. Call error() + * for details. + * + */ + bool setOptions(const SshOptList& opts); + + /** + * Create a ssh connection based on the options provided by setOptions(). + * Sets one of the following error codes on failure: + * <ul> + * <li>ERR_NO_OPTIONS</li> + * <li>ERR_CANNOT_LAUNCH</li> + * <li>ERR_INVALID_STATE</li> + * <li>ERR_NEED_PASSWD</li> + * <li>ERR_AUTH_FAILED</li> + * <li>ERR_NEW_HOST_KEY</li> + * <li>ERR_KEY_ACCEPTED</li> + * <li>ERR_DIFF_HOST_KEY</li> + * <li>ERR_INTERNAL</li> + * <li>ERR_INTERACT</li> + * </ul> + * + * @param acceptHostKey When true KSshProcess will automatically accept + * unrecognized or changed host keys. + * + * @return True if the ssh connection is successful. False if the connection + * fails. Call error() to get the reason for the failure. + */ + bool connect(); + + + /** + * Disconnect ssh from the host. This kills the ssh process and + * resets the internal state of this KSshProcess object. After a + * disconnect, the same KSshProcess can be used to connect to a + * host. + */ + void disconnect(); + + /** + * Call to respond to a ERR_NEW_HOST_KEY or ERR_DIFF_HOST_KEY error. + * + * @param accept True to accept the host key, false to not accept the + * host key and kill ssh. + * + */ + void acceptHostKey(bool accept); + + /** + * Call to respond to a ERR_NEED_PASSWD or ERR_NEED_PASSPHRASE error. + * + * @param password The user password to give ssh. + */ + void setPassword(QString password); + + /** + * Access to standard in and out of the ssh process. + * + * @return The file description for stdin and stdout of the ssh process. + */ + int stdioFd() { return ssh.stdioFd(); } + + /** + * Access to standard error of the ssh process. + * + * @return The file descriptior for stderr of the ssh process. + */ + int stderrFd() { return ssh.stderrFd(); } + + /** + * Access the pty to which the ssh process is attached. + * + * @return The file descriptor of pty to which ssh is attached. + */ + int pty() { return ssh.fd(); } +private: + /** + * Path the the ssh binary. + */ + QString mSshPath; + + /** + * SSH version. This is an index into the supported SSH + * versions array, and the various messages arrays. + */ + SshVersion mVersion; + + /** + * User's password. Zero this out when it is no longer needed. + */ + QString mPassword; + + /** + * User's username. + */ + QString mUsername; + + /** + * Name of host we are connecting to. + */ + QString mHost; + + /** + * Accept new or changed host keys if true. + */ + bool mAcceptHostKey; + + /** + * Flag to tell use if we have an open, authenticated ssh + * session going. + */ + bool mConnected; + + /** + * Flag to tell us if we have started a ssh process, we use this + * to make sure we kill ssh before going away. + */ + bool mRunning; + + /** + * Save any key fingerprint msg from ssh so we can present + * it to the caller. + */ + QString mKeyFingerprint; + + /** + * The location of the known host key file. We grab this from + * any error messages ssh prints out. + */ + QString mKnownHostsFile; + + /** + * The state of our connect state machine. + */ + int mConnectState; + + /** + * Port on on which the target ssh server is listening. + */ + int mPort; + + /** + * The last error number encountered. This is only valid for the + * last error. + */ + SshError mError; + + /** + * An error message that corresponds to the error number set in + * mError. Optional. + */ + QString mErrorMsg; + + /** + * Interface to the SSH process we ceate. Handles communication + * to and from the SSH process using stdin, stdout, stderr, and + * pty. + */ + MyPtyProcess ssh; + + /** + * List of arguments we start SSH with. + */ + QCStringList mArgs; + void init(); + + /** + * Handler to clean up when ssh process terminates. + */ + static void SIGCHLD_handler(int signo); + void installSignalHandlers(); + void removeSignalHandlers(); + + QString getLine(); + + static QRegExp versionStrs[]; + static const char * const passwordPrompt[]; + static const char * const passphrasePrompt[]; + static const char * const authSuccessMsg[]; + static const char * const authFailedMsg[]; + static QRegExp hostKeyMissingMsg[]; + static const char * const hostKeyChangedMsg[]; + static const char * const continuePrompt[]; + static const char * const hostKeyAcceptedMsg[]; + static const char * const tryAgainMsg[]; + static QRegExp hostKeyVerifyFailedMsg[]; + static const char * const connectionClosedMsg[]; + static const char * const changeHostKeyOnDiskPrompt[]; + static QRegExp keyFingerprintMsg[]; + static QRegExp knownHostsFileMsg[]; +}; +#endif diff --git a/kioslave/sftp/ksshprocesstest.cpp b/kioslave/sftp/ksshprocesstest.cpp new file mode 100644 index 000000000..3a37be02c --- /dev/null +++ b/kioslave/sftp/ksshprocesstest.cpp @@ -0,0 +1,98 @@ +#include "ksshprocess.h" +#include <iostream> + +using namespace std; + +int main(int argc, char *argv[]) { + + if( argc < 5 ) { + cout << "Usage: " << argv[0] << + " <ssh path> <host> <username> <password>" << endl; + return 1; + } + + KSshProcess ssh(argv[1]); + cout << ssh.version() << endl; + + KSshProcess::SshOptList opts; + KSshProcess::SshOpt opt; + + opt.opt = KSshProcess::SSH_PORT; + opt.num = 22; + opts.append(opt); + + opt.opt = KSshProcess::SSH_HOST; + opt.str = QString(argv[2]); + opts.append(opt); + + opt.opt = KSshProcess::SSH_USERNAME; + opt.str = QString(argv[3]); + opts.append(opt); + +// opt.opt = KSshProcess::SSH_PASSWD; +// opt.str = QString(argv[4]); +// opts.append(opt); + + if( !ssh.setOptions(opts) ) { + cout << "ksshprocesstest: setOptions failed" << endl; + return -1; + } + + ssh.printArgs(); + + bool stop = false; + bool connected; + char buf[256]; + char c; + while( !stop && !(connected = ssh.connect()) ) { + cout << "ksshprocesstest: Error num - " << ssh.error() << endl; + cout << "ksshprocesstest: Error msg - " << ssh.errorMsg().latin1() << endl; + switch( ssh.error() ) { + case KSshProcess::ERR_NEED_PASSWD: + case KSshProcess::ERR_NEED_PASSPHRASE: + cout << "Password: "; + cin >> buf; + cout << "password is " << buf << endl; + ssh.setPassword(QString(buf)); + break; + case KSshProcess::ERR_NEW_HOST_KEY: + case KSshProcess::ERR_DIFF_HOST_KEY: + cout << "Accept host key? (y/n): "; + cin >> c; + cout << "Answered " << c << endl; + ssh.acceptHostKey(c == 'y' ? true : false); + break; + case KSshProcess::ERR_AUTH_FAILED: + cout << "ksshprocesstest: auth failed." << endl; + stop = true; + break; + case KSshProcess::ERR_AUTH_FAILED_NEW_KEY: + cout << "ksshprocesstest: auth failed because of new key." << endl; + stop = true; + break; + case KSshProcess::ERR_AUTH_FAILED_DIFF_KEY: + cout << "ksshprocesstest: auth failed because of changed key." << endl; + stop = true; + break; + + case KSshProcess::ERR_INTERACT: + case KSshProcess::ERR_INTERNAL: + case KSshProcess::ERR_UNKNOWN: + case KSshProcess::ERR_INVALID_STATE: + case KSshProcess::ERR_CANNOT_LAUNCH: + case KSshProcess::ERR_HOST_KEY_REJECTED: + cout << "ksshprocesstest: FATAL ERROR" << endl; + stop = true; + break; + + } + } + + if( connected ) { + cout << "ksshprocesstest: Successfully connected to " << argv[2] << endl; + } + else { + cout << "ksshprocesstest: Connect to " << argv[2] << " failed." << endl; + } + +} diff --git a/kioslave/sftp/process.cpp b/kioslave/sftp/process.cpp new file mode 100644 index 000000000..fcf012514 --- /dev/null +++ b/kioslave/sftp/process.cpp @@ -0,0 +1,493 @@ +/* vi: ts=8 sts=4 sw=4 + * + * + * This file is part of the KDE project, module kdesu. + * Copyright (C) 1999,2000 Geert Jansen <jansen@kde.org> + * + * This file contains code from TEShell.C of the KDE konsole. + * Copyright (c) 1997,1998 by Lars Doelle <lars.doelle@on-line.de> + * + * This is free software; you can use this library under the GNU Library + * General Public License, version 2. See the file "COPYING.LIB" for the + * exact licensing terms. + * + * process.cpp: Functionality to build a front end to password asking + * terminal programs. + */ + +#include <config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <fcntl.h> +#include <signal.h> +#include <errno.h> +#include <string.h> +#include <termios.h> +#include <signal.h> + +#include <sys/types.h> +#include <sys/wait.h> +#include <sys/stat.h> +#include <sys/time.h> +#include <sys/resource.h> +#include <sys/socket.h> + +#if defined(__SVR4) && defined(sun) +#include <stropts.h> +#include <sys/stream.h> +#endif + +#ifdef HAVE_SYS_SELECT_H +#include <sys/select.h> // Needed on some systems. +#endif + +#include <qglobal.h> +#include <qcstring.h> +#include <qfile.h> + +#include <kdebug.h> +#include <kstandarddirs.h> + +#include "process.h" +#include <kdesu/kdesu_pty.h> +#include <kdesu/kcookie.h> + + +MyPtyProcess::MyPtyProcess() +{ + m_bTerminal = false; + m_bErase = false; + m_pPTY = 0L; + m_Pid = -1; + m_Fd = -1; +} + + +int MyPtyProcess::init() +{ + delete m_pPTY; + m_pPTY = new PTY(); + m_Fd = m_pPTY->getpt(); + if (m_Fd < 0) + return -1; + if ((m_pPTY->grantpt() < 0) || (m_pPTY->unlockpt() < 0)) + { + kdError(PTYPROC) << k_lineinfo << "Master setup failed.\n" << endl; + m_Fd = -1; + return -1; + } + m_TTY = m_pPTY->ptsname(); + m_stdoutBuf.resize(0); + m_stderrBuf.resize(0); + m_ptyBuf.resize(0); + return 0; +} + + +MyPtyProcess::~MyPtyProcess() +{ + delete m_pPTY; +} + + +/* + * Read one line of input. The terminal is in canonical mode, so you always + * read a line at at time, but it's possible to receive multiple lines in + * one time. + */ + + +QCString MyPtyProcess::readLineFrom(int fd, QCString& inbuf, bool block) +{ + int pos; + QCString ret; + + if (!inbuf.isEmpty()) + { + + pos = inbuf.find('\n'); + + if (pos == -1) + { + ret = inbuf; + inbuf.resize(0); + } else + { + ret = inbuf.left(pos); + inbuf = inbuf.mid(pos+1); + } + return ret; + + } + + int flags = fcntl(fd, F_GETFL); + if (flags < 0) + { + kdError(PTYPROC) << k_lineinfo << "fcntl(F_GETFL): " << perror << "\n"; + return ret; + } + if (block) + flags &= ~O_NONBLOCK; + else + flags |= O_NONBLOCK; + if (fcntl(fd, F_SETFL, flags) < 0) + { + kdError(PTYPROC) << k_lineinfo << "fcntl(F_SETFL): " << perror << "\n"; + return ret; + } + + int nbytes; + char buf[256]; + while (1) + { + nbytes = read(fd, buf, 255); + if (nbytes == -1) + { + if (errno == EINTR) + continue; + else break; + } + if (nbytes == 0) + break; // eof + + buf[nbytes] = '\000'; + inbuf += buf; + + pos = inbuf.find('\n'); + if (pos == -1) + { + ret = inbuf; + inbuf.resize(0); + } else + { + ret = inbuf.left(pos); + inbuf = inbuf.mid(pos+1); + } + break; + + } + + return ret; +} + +void MyPtyProcess::writeLine(QCString line, bool addnl) +{ + if (!line.isEmpty()) + write(m_Fd, line, line.length()); + if (addnl) + write(m_Fd, "\n", 1); +} + +void MyPtyProcess::unreadLineFrom(QCString inbuf, QCString line, bool addnl) +{ + if (addnl) + line += '\n'; + if (!line.isEmpty()) + inbuf.prepend(line); +} + + +/* + * Fork and execute the command. This returns in the parent. + */ + +int MyPtyProcess::exec(QCString command, QCStringList args) +{ + kdDebug(PTYPROC) << "MyPtyProcess::exec(): " << command << endl;// << ", args = " << args << endl; + + if (init() < 0) + return -1; + + // Open the pty slave before forking. See SetupTTY() + int slave = open(m_TTY, O_RDWR); + if (slave < 0) + { + kdError(PTYPROC) << k_lineinfo << "Could not open slave pty.\n"; + return -1; + } + + // Also create a socket pair to connect to standard in/out. + // This will allow use to bypass the terminal. + int inout[2]; + int err[2]; + int ok = 1; + ok &= socketpair(AF_UNIX, SOCK_STREAM, 0, inout) >= 0; + ok &= socketpair(AF_UNIX, SOCK_STREAM, 0, err ) >= 0; + if( !ok ) { + kdDebug(PTYPROC) << "Could not create socket" << endl; + return -1; + } + m_stdinout = inout[0]; + m_err = err[0]; + + if ((m_Pid = fork()) == -1) + { + kdError(PTYPROC) << k_lineinfo << "fork(): " << perror << "\n"; + return -1; + } + + // Parent + if (m_Pid) + { + close(slave); + close(inout[1]); + close(err[1]); + return 0; + } + + // Child + + ok = 1; + ok &= dup2(inout[1], STDIN_FILENO) >= 0; + ok &= dup2(inout[1], STDOUT_FILENO) >= 0; + ok &= dup2(err[1], STDERR_FILENO) >= 0; + + if( !ok ) + { + kdError(PTYPROC) << "dup of socket descriptor failed" << endl; + _exit(1); + } + + close(inout[1]); + close(inout[0]); + close(err[1]); + close(err[0]); + + if (SetupTTY(slave) < 0) + _exit(1); + + // From now on, terminal output goes through the tty. + QCString path; + if (command.contains('/')) + path = command; + else + { + QString file = KStandardDirs::findExe(command); + if (file.isEmpty()) + { + kdError(PTYPROC) << k_lineinfo << command << " not found\n"; + _exit(1); + } + path = QFile::encodeName(file); + } + + int i; + const char * argp[32]; + argp[0] = path; + QCStringList::Iterator it; + for (i=1, it=args.begin(); it!=args.end() && i<31; it++) { + argp[i++] = *it; + kdDebug(PTYPROC) << *it << endl; + } + argp[i] = 0L; + execv(path, (char * const *)argp); + kdError(PTYPROC) << k_lineinfo << "execv(\"" << path << "\"): " << perror << "\n"; + _exit(1); + return -1; // Shut up compiler. Never reached. +} + +/* + * Wait until the terminal is set into no echo mode. At least one su + * (RH6 w/ Linux-PAM patches) sets noecho mode AFTER writing the Password: + * prompt, using TCSAFLUSH. This flushes the terminal I/O queues, possibly + * taking the password with it. So we wait until no echo mode is set + * before writing the password. + * Note that this is done on the slave fd. While Linux allows tcgetattr() on + * the master side, Solaris doesn't. + */ + +int MyPtyProcess::WaitSlave() +{ + int slave = open(m_TTY, O_RDWR); + if (slave < 0) + { + kdError(PTYPROC) << k_lineinfo << "Could not open slave tty.\n"; + return -1; + } + + struct termios tio; + struct timeval tv; + while (1) + { + if (tcgetattr(slave, &tio) < 0) + { + kdError(PTYPROC) << k_lineinfo << "tcgetattr(): " << perror << "\n"; + close(slave); + return -1; + } + if (tio.c_lflag & ECHO) + { + kdDebug(PTYPROC) << k_lineinfo << "Echo mode still on." << endl; + // sleep 1/10 sec + tv.tv_sec = 0; tv.tv_usec = 100000; + select(slave, 0L, 0L, 0L, &tv); + continue; + } + break; + } + close(slave); + return 0; +} + + +int MyPtyProcess::enableLocalEcho(bool enable) +{ + int slave = open(m_TTY, O_RDWR); + if (slave < 0) + { + kdError(PTYPROC) << k_lineinfo << "Could not open slave tty.\n"; + return -1; + } + struct termios tio; + if (tcgetattr(slave, &tio) < 0) + { + kdError(PTYPROC) << k_lineinfo << "tcgetattr(): " << perror << "\n"; + close(slave); return -1; + } + if (enable) + tio.c_lflag |= ECHO; + else + tio.c_lflag &= ~ECHO; + if (tcsetattr(slave, TCSANOW, &tio) < 0) + { + kdError(PTYPROC) << k_lineinfo << "tcsetattr(): " << perror << "\n"; + close(slave); return -1; + } + close(slave); + return 0; +} + + +/* + * Copy output to stdout until the child process exists, or a line of output + * matches `m_Exit'. + * We have to use waitpid() to test for exit. Merely waiting for EOF on the + * pty does not work, because the target process may have children still + * attached to the terminal. + */ + +int MyPtyProcess::waitForChild() +{ + int ret, state, retval = 1; + struct timeval tv; + + fd_set fds; + FD_ZERO(&fds); + + while (1) + { + tv.tv_sec = 1; tv.tv_usec = 0; + FD_SET(m_Fd, &fds); + ret = select(m_Fd+1, &fds, 0L, 0L, &tv); + if (ret == -1) + { + if (errno == EINTR) continue; + else + { + kdError(PTYPROC) << k_lineinfo << "select(): " << perror << "\n"; + return -1; + } + } + + if (ret) + { + QCString line = readLine(false); + while (!line.isNull()) + { + if (!m_Exit.isEmpty() && !qstrnicmp(line, m_Exit, m_Exit.length())) + kill(m_Pid, SIGTERM); + if (m_bTerminal) + { + fputs(line, stdout); + fputc('\n', stdout); + } + line = readLine(false); + } + } + + // Check if the process is still alive + ret = waitpid(m_Pid, &state, WNOHANG); + if (ret < 0) + { + if (errno == ECHILD) + retval = 0; + else + kdError(PTYPROC) << k_lineinfo << "waitpid(): " << perror << "\n"; + break; + } + if (ret == m_Pid) + { + if (WIFEXITED(state)) + retval = WEXITSTATUS(state); + break; + } + } + + return -retval; +} + +/* + * SetupTTY: Creates a new session. The filedescriptor "fd" should be + * connected to the tty. It is closed after the tty is reopened to make it + * our controlling terminal. This way the tty is always opened at least once + * so we'll never get EIO when reading from it. + */ + +int MyPtyProcess::SetupTTY(int fd) +{ + // Reset signal handlers + for (int sig = 1; sig < NSIG; sig++) + signal(sig, SIG_DFL); + signal(SIGHUP, SIG_IGN); + + // Close all file handles +// struct rlimit rlp; +// getrlimit(RLIMIT_NOFILE, &rlp); +// for (int i = 0; i < (int)rlp.rlim_cur; i++) +// if (i != fd) close(i); + + // Create a new session. + setsid(); + + // Open slave. This will make it our controlling terminal + int slave = open(m_TTY, O_RDWR); + if (slave < 0) + { + kdError(PTYPROC) << k_lineinfo << "Could not open slave side: " << perror << "\n"; + return -1; + } + close(fd); + +#if defined(__SVR4) && defined(sun) + + // Solaris STREAMS environment. + // Push these modules to make the stream look like a terminal. + ioctl(slave, I_PUSH, "ptem"); + ioctl(slave, I_PUSH, "ldterm"); + +#endif + + // Connect stdin, stdout and stderr +// dup2(slave, 0); dup2(slave, 1); dup2(slave, 2); +// if (slave > 2) +// close(slave); + + // Disable OPOST processing. Otherwise, '\n' are (on Linux at least) + // translated to '\r\n'. + struct termios tio; + if (tcgetattr(slave, &tio) < 0) + { + kdError(PTYPROC) << k_lineinfo << "tcgetattr(): " << perror << "\n"; + return -1; + } + tio.c_oflag &= ~OPOST; + if (tcsetattr(slave, TCSANOW, &tio) < 0) + { + kdError(PTYPROC) << k_lineinfo << "tcsetattr(): " << perror << "\n"; + return -1; + } + + return 0; +} diff --git a/kioslave/sftp/process.h b/kioslave/sftp/process.h new file mode 100644 index 000000000..64dcfb973 --- /dev/null +++ b/kioslave/sftp/process.h @@ -0,0 +1,148 @@ +/* vi: ts=8 sts=4 sw=4 + * + * + * This file is part of the KDE project, module kdesu. + * Copyright (C) 1999,2000 Geert Jansen <jansen@kde.org> + * + * This is free software; you can use this library under the GNU Library + * General Public License, version 2. See the file "COPYING.LIB" for the + * exact licensing terms. + */ + +#ifndef __Process_h_Included__ +#define __Process_h_Included__ + +#include <qcstring.h> +#include <qstring.h> +#include <qstringlist.h> +#include <qvaluelist.h> + +#define PTYPROC 7120 + +class PTY; +typedef QValueList<QCString> QCStringList; + +/** + * Synchronous communication with tty programs. + * + * PtyProcess provides synchronous communication with tty based programs. + * The communications channel used is a pseudo tty (as opposed to a pipe) + * This means that programs which require a terminal will work. + */ + +class MyPtyProcess +{ +public: + MyPtyProcess(); + virtual ~MyPtyProcess(); + + /** + * Fork off and execute a command. The command's standard in and output + * are connected to the pseudo tty. They are accessible with @ref #readLine + * and @ref #writeLine. + * @param command The command to execute. + * @param args The arguments to the command. + */ + int exec(QCString command, QCStringList args); + + /** + * Read a line from the program's standard out. Depending on the @em block + * parameter, this call blocks until a single, full line is read. + * @param block Block until a full line is read? + * @return The output string. + */ + QCString readLine(bool block = true) + { return readLineFrom(m_Fd, m_ptyBuf, block); } + + QCString readLineFromPty(bool block = true) + { return readLineFrom(m_Fd, m_ptyBuf, block); } + + QCString readLineFromStdout(bool block = true) + { return readLineFrom(m_stdinout, m_stdoutBuf, block); } + + QCString readLineFromStderr(bool block = true) + { return readLineFrom(m_err, m_stderrBuf, block); } + + /** + * Write a line of text to the program's standard in. + * @param line The text to write. + * @param addNewline Adds a '\n' to the line. + */ + void writeLine(QCString line, bool addNewline=true); + + /** + * Put back a line of input. + * @param line The line to put back. + * @param addNewline Adds a '\n' to the line. + */ + + void unreadLine(QCString line, bool addNewline = true) + { unreadLineFrom(m_ptyBuf, line, addNewline); } + + void unreadLineFromPty(QCString line, bool addNewline = true) + { unreadLineFrom(m_ptyBuf, line, addNewline); } + + void unreadLineFromStderr(QCString line, bool addNewline = true) + { unreadLineFrom(m_stderrBuf, line, addNewline); } + + void unreadLineFromStdout(QCString line, bool addNewline = true) + { unreadLineFrom(m_stdoutBuf, line, addNewline); } + + /** + * Set exit string. If a line of program output matches this, + * @ref #waitForChild() will terminate the program and return. + */ + void setExitString(QCString exit) { m_Exit = exit; } + + /** + * Wait for the child to exit. See also @ref #setExitString. + */ + int waitForChild(); + + /** + * Wait until the pty has cleared the ECHO flag. This is useful + * when programs write a password prompt before they disable ECHO. + * Disabling it might flush any input that was written. + */ + int WaitSlave(); + + /** Enables/disables local echo on the pseudo tty. */ + int enableLocalEcho(bool enable=true); + + /** Enable/disable terminal output. Relevant only to some subclasses. */ + void setTerminal(bool terminal) { m_bTerminal = terminal; } + + /** Overwritte the password as soon as it is used. Relevant only to + * some subclasses. */ + void setErase(bool erase) { m_bErase = erase; } + + /** Return the filedescriptor of the process. */ + int fd() {return m_Fd;} + + /** Return the pid of the process. */ + int pid() {return m_Pid;} + + int stdioFd() {return m_stdinout;} + + int stderrFd() {return m_err;} + +protected: + bool m_bErase, m_bTerminal; + int m_Pid, m_Fd, m_stdinout, m_err; + QCString m_Command, m_Exit; + +private: + int init(); + int SetupTTY(int fd); + + PTY *m_pPTY; + QCString m_TTY; + QCString m_ptyBuf, m_stderrBuf, m_stdoutBuf; + + QCString readLineFrom(int fd, QCString& inbuf, bool block); + void unreadLineFrom(QCString inbuf, QCString line, bool addnl); + class PtyProcessPrivate; + PtyProcessPrivate *d; +}; + +#endif diff --git a/kioslave/sftp/sftp.h b/kioslave/sftp/sftp.h new file mode 100644 index 000000000..95518130d --- /dev/null +++ b/kioslave/sftp/sftp.h @@ -0,0 +1,91 @@ +/* $OpenBSD: sftp.h,v 1.3 2001/03/07 10:11:23 djm Exp $ */ + +/* + * Copyright (c) 2001 Markus Friedl. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * draft-ietf-secsh-filexfer-01.txt + */ + +/* version */ +#define SSH2_FILEXFER_VERSION 3 + +/* client to server */ +#define SSH2_FXP_INIT 1 +#define SSH2_FXP_OPEN 3 +#define SSH2_FXP_CLOSE 4 +#define SSH2_FXP_READ 5 +#define SSH2_FXP_WRITE 6 +#define SSH2_FXP_LSTAT 7 +#define SSH2_FXP_FSTAT 8 +#define SSH2_FXP_SETSTAT 9 +#define SSH2_FXP_FSETSTAT 10 +#define SSH2_FXP_OPENDIR 11 +#define SSH2_FXP_READDIR 12 +#define SSH2_FXP_REMOVE 13 +#define SSH2_FXP_MKDIR 14 +#define SSH2_FXP_RMDIR 15 +#define SSH2_FXP_REALPATH 16 +#define SSH2_FXP_STAT 17 +#define SSH2_FXP_RENAME 18 +#define SSH2_FXP_READLINK 19 +#define SSH2_FXP_SYMLINK 20 + +/* server to client */ +#define SSH2_FXP_VERSION 2 +#define SSH2_FXP_STATUS 101 +#define SSH2_FXP_HANDLE 102 +#define SSH2_FXP_DATA 103 +#define SSH2_FXP_NAME 104 +#define SSH2_FXP_ATTRS 105 + +#define SSH2_FXP_EXTENDED 200 +#define SSH2_FXP_EXTENDED_REPLY 201 + +/* attributes */ +#define SSH2_FILEXFER_ATTR_SIZE 0x00000001 +#define SSH2_FILEXFER_ATTR_UIDGID 0x00000002 +#define SSH2_FILEXFER_ATTR_PERMISSIONS 0x00000004 +#define SSH2_FILEXFER_ATTR_ACMODTIME 0x00000008 +#define SSH2_FILEXFER_ATTR_EXTENDED 0x80000000 + +/* portable open modes */ +#define SSH2_FXF_READ 0x00000001 +#define SSH2_FXF_WRITE 0x00000002 +#define SSH2_FXF_APPEND 0x00000004 +#define SSH2_FXF_CREAT 0x00000008 +#define SSH2_FXF_TRUNC 0x00000010 +#define SSH2_FXF_EXCL 0x00000020 + +/* status messages */ +#define SSH2_FX_OK 0 +#define SSH2_FX_EOF 1 +#define SSH2_FX_NO_SUCH_FILE 2 +#define SSH2_FX_PERMISSION_DENIED 3 +#define SSH2_FX_FAILURE 4 +#define SSH2_FX_BAD_MESSAGE 5 +#define SSH2_FX_NO_CONNECTION 6 +#define SSH2_FX_CONNECTION_LOST 7 +#define SSH2_FX_OP_UNSUPPORTED 8 +#define SSH2_FX_MAX 8 diff --git a/kioslave/sftp/sftp.protocol b/kioslave/sftp/sftp.protocol new file mode 100644 index 000000000..177cd86e9 --- /dev/null +++ b/kioslave/sftp/sftp.protocol @@ -0,0 +1,84 @@ +[Protocol] +exec=kio_sftp +protocol=sftp +input=none +listing=Name,Type,Size,Date,Access,Owner,Group,Link +output=filesystem +copyToFile=true +copyFromFile=true +reading=true +writing=true +makedir=true +deleting=true +moving=true +Icon=ftp +Description=A kioslave for sftp +Description[af]='n Kioslave vir sftp +Description[be]=Kioslave для sftp +Description[bg]=kioslave за sftp +Description[bn]=এস.এফ.টি.পি-র জন্য একটি kioslave +Description[br]=Ur kioslave evit sftp +Description[bs]=kioslave za SFTP +Description[ca]=Un kioslave per a sftp +Description[cs]=Protokol KDE pro sftp +Description[csb]=Plugins dlô procedurë òbsłużënkù sftp +Description[da]=En kioslave for sftp +Description[de]=Ein-/Ausgabemodul für das sftp-Protokoll +Description[el]=Ένας kioslave για sftp +Description[eo]=kenelsklavo por sftp +Description[es]=Un kioslave para sftp +Description[et]=SFTP IO-moodul +Description[eu]=Sftp-rako kioslave bat +Description[fa]=یک kioslave برای sftp +Description[fi]=Sftp:n liitin +Description[fr]=Un module d'entrées / sorties pour le sftp +Description[fy]=Een kioslave Foar sftp +Description[ga]=kioslave le haghaidh sftp +Description[gl]=Un kioslave para sftp +Description[he]=ממשק kioslave עבור sftp +Description[hi]=एसएफटीपी के लिए केआईओस्लेव +Description[hr]=Kioslave za SFTP +Description[hu]=KDE-protokoll az sftp-hez +Description[is]=kioslave fyrir sftp +Description[it]=Un kioslave per sftp +Description[ja]=sftp のための kioslave +Description[kk]=sftp-ке арналған енгізу-шығару модулі +Description[km]=kioslave របស់ sftp +Description[ko]=SFTP KIO 슬레이브 +Description[lt]=Kiovergas sftp protokolui +Description[lv]=KIO vergs priekš sftp +Description[mk]=kio-служител за sftp +Description[ms]=Kioslave untuk sftp +Description[nb]=En kioslave for sftp +Description[nds]=En In-/Utgaavdeenst för sftpl +Description[ne]=sftp का लागि एउटा किओस्लेभ +Description[nl]=Een kioslave voor sftp +Description[nn]=Ein kioslave for sftp +Description[pa]=sftp ਲਈ kioslave +Description[pl]=Wtyczka protokołu sftp +Description[pt]=Um 'kioslave' para sftp +Description[pt_BR]=Uma implementação para o sftp +Description[ro]=Un dispozitiv de I/E pentru SFTP +Description[ru]=Модуль ввода-вывода для sftp +Description[rw]=kioslave ya sftp +Description[se]=ŠO-šláva sftp-protokolla várás +Description[sk]=kioslave pre sftp +Description[sl]=kioslave za sftp +Description[sr]=Kioslave за SFTP +Description[sr@Latn]=Kioslave za SFTP +Description[sv]=En I/O-slav för SFTP +Description[ta]=sftpக்கான ஒரு க்யோஸ்லேவ் +Description[te]=ఎస్ ఎఫ్ టి పి కొరకు ఐఒ బానిస +Description[th]=ตัวนำข้อมูลเข้า-ออกสำหรับ sftp +Description[tr]=Sftp için kioslave +Description[tt]=sftp öçen kioslave +Description[uk]=Підлеглий В/В для sftp +Description[uz]=SFTP uchun KCH-sleyv +Description[uz@cyrillic]=SFTP учун КЧ-слейв +Description[vi]=A kioslave (đày tớ vào ra KDE) cho sftp +Description[wa]=On kioslave po sftp +Description[zh_CN]=sftp 的 kioslave +Description[zh_TW]=sftp 的 kioslave +DocPath=kioslave/sftp.html +Icon=ftp +Class=:internet diff --git a/kioslave/sftp/sftpfileattr.cpp b/kioslave/sftp/sftpfileattr.cpp new file mode 100644 index 000000000..07aada36d --- /dev/null +++ b/kioslave/sftp/sftpfileattr.cpp @@ -0,0 +1,346 @@ +/*************************************************************************** + sftpfileattr.cpp - description + ------------------- + begin : Sat Jun 30 2001 + copyright : (C) 2001 by Lucas Fisher + email : ljfisher@iastate.edu + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "sftpfileattr.h" + +#include <ctype.h> +#include <sys/types.h> +#include <sys/stat.h> + +#include <qstring.h> +#include <qdatastream.h> + +#include <kio/global.h> +#include <kremoteencoding.h> + +using namespace KIO; + +sftpFileAttr::sftpFileAttr(){ + clear(); + mDirAttrs = false; +} + +sftpFileAttr::sftpFileAttr(KRemoteEncoding* encoding){ + clear(); + mDirAttrs = false; + mEncoding = encoding; +} + + +/** Constructor to initialize the file attributes on declaration. */ +sftpFileAttr::sftpFileAttr(Q_ULLONG size, uid_t uid, gid_t gid, + mode_t permissions, time_t atime, + time_t mtime, Q_UINT32 extendedCount) { + clear(); + mDirAttrs = false; + mSize = size; + mUid = uid; + mGid = gid; + mAtime = atime; + mMtime = mtime; + mPermissions = permissions; + mExtendedCount = extendedCount; +} + +sftpFileAttr::~sftpFileAttr(){ +} + +/** Returns a UDSEntry describing the file. +The UDSEntry is generated from the sftp file attributes. */ +UDSEntry sftpFileAttr::entry() { + UDSEntry entry; + UDSAtom atom; + + atom.m_uds = UDS_NAME; + atom.m_str = mFilename; + entry.append(atom); + + if( mFlags & SSH2_FILEXFER_ATTR_SIZE ) { + atom.m_uds = UDS_SIZE; + atom.m_long = mSize; + entry.append(atom); + } + + if( mFlags & SSH2_FILEXFER_ATTR_ACMODTIME ) { + atom.m_uds = UDS_ACCESS_TIME; + atom.m_long = mAtime; + entry.append(atom); + + atom.m_uds = UDS_MODIFICATION_TIME; + atom.m_long = mMtime; + entry.append(atom); + } + + if( mFlags & SSH2_FILEXFER_ATTR_UIDGID ) { + if( mUserName.isEmpty() || mGroupName.isEmpty() ) + getUserGroupNames(); + + atom.m_uds = UDS_USER; + atom.m_str = mUserName; + entry.append(atom); + + atom.m_uds = UDS_GROUP; + atom.m_str = mGroupName; + entry.append(atom); + } + + if( mFlags & SSH2_FILEXFER_ATTR_PERMISSIONS ) { + atom.m_uds = UDS_ACCESS; + atom.m_long = mPermissions; + entry.append(atom); + + mode_t type = fileType(); + + // Set the type if we know what it is + if( type != 0 ) { + atom.m_uds = UDS_FILE_TYPE; + atom.m_long = (mLinkType ? mLinkType:type); + entry.append(atom); + } + + if( S_ISLNK(type) ) { + atom.m_uds = UDS_LINK_DEST; + atom.m_str = mLinkDestination; + entry.append(atom); + } + } + + return entry; +} + +/** Use to output the file attributes to a sftp packet */ +QDataStream& operator<< (QDataStream& s, const sftpFileAttr& fa) { + s << (Q_UINT32)fa.mFlags; + + if( fa.mFlags & SSH2_FILEXFER_ATTR_SIZE ) + { s << (Q_ULLONG)fa.mSize; } + + if( fa.mFlags & SSH2_FILEXFER_ATTR_UIDGID ) + { s << (Q_UINT32)fa.mUid << (Q_UINT32)fa.mGid; } + + if( fa.mFlags & SSH2_FILEXFER_ATTR_PERMISSIONS ) + { s << (Q_UINT32)fa.mPermissions; } + + if( fa.mFlags & SSH2_FILEXFER_ATTR_ACMODTIME ) + { s << (Q_UINT32)fa.mAtime << (Q_UINT32)fa.mMtime; } + + if( fa.mFlags & SSH2_FILEXFER_ATTR_EXTENDED ) { + s << (Q_UINT32)fa.mExtendedCount; + // XXX: Write extensions to data stream here + // s.writeBytes(extendedtype).writeBytes(extendeddata); + } + return s; +} + + +/** Use to read a file attribute from a sftp packet */ +QDataStream& operator>> (QDataStream& s, sftpFileAttr& fa) { + + // XXX Add some error checking in here in case + // we get a bad sftp packet. + + fa.clear(); + + if( fa.mDirAttrs ) { + QCString fn; + s >> fn; + fn.truncate( fn.size() ); + + fa.mFilename = fa.mEncoding->decode( fn ); + + s >> fa.mLongname; + fa.mLongname.truncate( fa.mLongname.size() ); + //kdDebug() << ">>: ftpfileattr long filename (" << fa.mLongname.size() << ")= " << fa.mLongname << endl; + } + + s >> fa.mFlags; // get flags + + if( fa.mFlags & SSH2_FILEXFER_ATTR_SIZE ) { + Q_ULLONG fileSize; + s >> fileSize; + fa.setFileSize(fileSize); + } + + Q_UINT32 x; + + if( fa.mFlags & SSH2_FILEXFER_ATTR_UIDGID ) { + s >> x; fa.setUid(x); + s >> x; fa.setGid(x); + } + + if( fa.mFlags & SSH2_FILEXFER_ATTR_PERMISSIONS ) { + s >> x; fa.setPermissions(x); + } + + if( fa.mFlags & SSH2_FILEXFER_ATTR_ACMODTIME ) { + s >> x; fa.setAtime(x); + s >> x; fa.setMtime(x); + } + + if( fa.mFlags & SSH2_FILEXFER_ATTR_EXTENDED ) { + s >> x; fa.setExtendedCount(x); + // XXX: Read in extensions from data stream here + // s.readBytes(extendedtype).readBytes(extendeddata); + } + + fa.getUserGroupNames(); + return s; +} +/** Parse longname for the owner and group names. */ +void sftpFileAttr::getUserGroupNames(){ + // Get the name of the owner and group of the file from longname. + QString user, group; + if( mLongname.isEmpty() ) { + // do not have the user name so use the user id instead + user.setNum(mUid); + group.setNum(mGid); + } + else { + int field = 0; + int i = 0; + int l = mLongname.length(); + + QString longName = mEncoding->decode( mLongname ); + + kdDebug(7120) << "Decoded: " << longName << endl; + + // Find the beginning of the third field which contains the user name. + while( field != 2 ) { + if( longName[i].isSpace() ) { + field++; i++; + while( i < l && longName[i].isSpace() ) { i++; } + } + else { i++; } + } + // i is the index of the first character of the third field. + while( i < l && !longName[i].isSpace() ) { + user.append(longName[i]); + i++; + } + + // i is the first character of the space between fields 3 and 4 + // user contains the owner's user name + while( i < l && longName[i].isSpace() ) { + i++; + } + + // i is the first character of the fourth field + while( i < l && !longName[i].isSpace() ) { + group.append(longName[i]); + i++; + } + // group contains the name of the group. + } + + mUserName = user; + mGroupName = group; +} + +/** No descriptions */ +kdbgstream& operator<< (kdbgstream& s, sftpFileAttr& a) { + s << "Filename: " << a.mFilename + << ", Uid: " << a.mUid + << ", Gid: " << a.mGid + << ", Username: " << a.mUserName + << ", GroupName: " << a.mGroupName + << ", Permissions: " << a.mPermissions + << ", size: " << a.mSize + << ", atime: " << a.mAtime + << ", mtime: " << a.mMtime + << ", extended cnt: " << a.mExtendedCount; + + if (S_ISLNK(a.mLinkType)) { + s << ", Link Type: " << a.mLinkType; + s << ", Link Destination: " << a.mLinkDestination; + } + + return s; +} + +/** Make sure it builds with NDEBUG */ +kndbgstream& operator<< (kndbgstream& s, sftpFileAttr& ) { + return s; +} + +/** Clear all attributes and flags. */ +void sftpFileAttr::clear(){ + clearAtime(); + clearMtime(); + clearGid(); + clearUid(); + clearFileSize(); + clearPermissions(); + clearExtensions(); + mFilename = QString::null; + mGroupName = QString::null; + mUserName = QString::null; + mLinkDestination = QString::null; + mFlags = 0; + mLongname = "\0"; + mLinkType = 0; +} + +/** Return the size of the sftp attribute. */ +Q_UINT32 sftpFileAttr::size() const{ + Q_UINT32 size = 4; // for the attr flag + if( mFlags & SSH2_FILEXFER_ATTR_SIZE ) + size += 8; + + if( mFlags & SSH2_FILEXFER_ATTR_UIDGID ) + size += 8; + + if( mFlags & SSH2_FILEXFER_ATTR_PERMISSIONS ) + size += 4; + + if( mFlags & SSH2_FILEXFER_ATTR_ACMODTIME ) + size += 8; + + if( mFlags & SSH2_FILEXFER_ATTR_EXTENDED ) { + size += 4; + // add size of extensions + } + return size; +} + +/** Returns the file type as determined from the file permissions */ +mode_t sftpFileAttr::fileType() const{ + mode_t type = 0; + + if( S_ISLNK(mPermissions) ) + type |= S_IFLNK; + + if( S_ISREG(mPermissions) ) + type |= S_IFREG; + else if( S_ISDIR(mPermissions) ) + type |= S_IFDIR; + else if( S_ISCHR(mPermissions) ) + type |= S_IFCHR; + else if( S_ISBLK(mPermissions) ) + type |= S_IFBLK; + else if( S_ISFIFO(mPermissions) ) + type |= S_IFIFO; + else if( S_ISSOCK(mPermissions) ) + type |= S_IFSOCK; + + return type; +} + +void sftpFileAttr::setEncoding( KRemoteEncoding* encoding ) +{ + mEncoding = encoding; +} +// vim:ts=4:sw=4 diff --git a/kioslave/sftp/sftpfileattr.h b/kioslave/sftp/sftpfileattr.h new file mode 100644 index 000000000..28743504b --- /dev/null +++ b/kioslave/sftp/sftpfileattr.h @@ -0,0 +1,261 @@ +/*************************************************************************** + sftpfileattr.h - description + ------------------- + begin : Sat Jun 30 2001 + copyright : (C) 2001 by Lucas Fisher + email : ljfisher@iastate.edu + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef SFTPFILEATTR_H +#define SFTPFILEATTR_H + +#include <sys/types.h> + +#include <qglobal.h> +#include <qstring.h> +#include <qdatastream.h> + +#include <kio/global.h> +#include <kdebug.h> + +#include "sftp.h" + +/** + *@author Lucas Fisher + */ + +class KRemoteEncoding; + +class sftpFileAttr { + +private: // Private attributes + /** Name of file. */ + QString mFilename; + + /** Specifies which fields of the file attribute are available. */ + Q_UINT32 mFlags; + + /** Size of the file in bytes. Should be 64 bit safe. */ + Q_ULLONG mSize; + + /** User id of the owner of the file. */ + uid_t mUid; + + /** Group id of the group to which the file belongs. */ + gid_t mGid; + + /** POSIX permissions of the file. */ + mode_t mPermissions; + + /** Last access time of the file in seconds from Jan 1, 1970. */ + time_t mAtime; + + /** Last modification time of file in seconds since Jan. 1, 1970. */ + time_t mMtime; + + /** Number of file attribute extensions. + Not currently implemented */ + Q_UINT32 mExtendedCount; + + /** Longname of the file as found in a SSH_FXP_NAME sftp packet. + These contents are parse to return the file's owner name and + gr oup name. */ + QCString mLongname; + + QString mUserName; + QString mGroupName; + + /** If file is a link, contains the destination of the link */ + QString mLinkDestination; + + /** If resource is a link, contains the type the link,e.g. file,dir... */ + mode_t mLinkType; + + /** Whether >> operator should read filename and longname from the stream. */ + bool mDirAttrs; + + /** Holds the encoding of the remote host */ + KRemoteEncoding* mEncoding; + +public: + sftpFileAttr(); + + sftpFileAttr(KRemoteEncoding* encoding); + + ~sftpFileAttr(); + + /** Constructor to initialize the file attributes on declaration. */ + sftpFileAttr(Q_ULLONG size_, uid_t uid_, gid_t gid_, mode_t permissions_, + time_t atime_, time_t mtime_, Q_UINT32 extendedCount_ = 0); + + /** Return the size of the sftp attribute not including filename or longname*/ + Q_UINT32 size() const; + + /** Clear all attributes and flags. */ + void clear(); + + /** Set the size of the file. */ + void setFileSize(Q_ULLONG s) + { mSize = s; mFlags |= SSH2_FILEXFER_ATTR_SIZE; } + + /** The size file attribute will not be included in the UDSEntry + or when the file attribute is written to the sftp packet. */ + void clearFileSize() + { mSize = 0; mFlags &= ~SSH2_FILEXFER_ATTR_SIZE; } + + /** Returns the size of the file. */ + Q_ULLONG fileSize() const { return mSize; } + + /** Sets the POSIX permissions of the file. */ + void setPermissions(mode_t p) + { mPermissions = p; mFlags |= SSH2_FILEXFER_ATTR_PERMISSIONS; } + + /** The permissions file attribute will not be included in the UDSEntry + or when the file attribute is written to the sftp packet. */ + void clearPermissions() + { mPermissions = 0; mFlags &= ~SSH2_FILEXFER_ATTR_PERMISSIONS; } + + /** Returns the POSIX permissons of the file. */ + mode_t permissions() const { return mPermissions; } + + /** Sets the group id of the file. */ + void setGid(gid_t id) + { mGid = id; mFlags |= SSH2_FILEXFER_ATTR_UIDGID; } + + /** Neither the gid or uid file attributes will not be included in the UDSEntry + or when the file attribute is written to the sftp packet. This is + equivalent to clearUid() */ + void clearGid() + { mGid = 0; mFlags &= SSH2_FILEXFER_ATTR_UIDGID; } + + /** Returns the group id of the file. */ + gid_t gid() const { return mGid; } + + /** Sets the uid of the file. */ + void setUid(uid_t id) + { mUid = id; mFlags |= SSH2_FILEXFER_ATTR_UIDGID; } + + /** Neither the gid or uid file attributes will not be included in the UDSEntry + or when the file attribute is written to the sftp packet. This is + equivalent to clearGid() */ + void clearUid() + { mUid = 0; mFlags &= SSH2_FILEXFER_ATTR_UIDGID; } + + /** Returns the user id of the file. */ + gid_t uid() const { return mUid; } + + /** Set the modificatoin time of the file in seconds since Jan. 1, 1970. */ + void setMtime(time_t t) + { mMtime = t; mFlags |= SSH2_FILEXFER_ATTR_ACMODTIME; } + + /** Neither the mtime or atime file attributes will not be included in the UDSEntry + or when the file attribute is written to the sftp packet. This is + equivalent to clearAtime() */ + void clearMtime() + { mMtime = 0; mFlags &= SSH2_FILEXFER_ATTR_ACMODTIME; } + + /** Returns the modification time of the file in seconds since Jan. 1, 1970. */ + time_t mtime() const { return mMtime; } + + /** Sets the access time of the file in seconds since Jan. 1, 1970. */ + void setAtime(time_t t) + { mAtime = t; mFlags |= SSH2_FILEXFER_ATTR_ACMODTIME; } + + /** Neither the atime or mtime file attributes will not be included in the UDSEntry + or when the file attribute is written to the sftp packet. This is + equivalent to clearMtime() */ + void clearAtime() + { mAtime = 0; mFlags &= SSH2_FILEXFER_ATTR_ACMODTIME; } + + /** Returns the last access time of the file in seconds since Jan. 1, 1970. */ + time_t atime() const { return mAtime; } + + /** Sets the number of file attribute extensions. */ + void setExtendedCount(unsigned int c) + { mExtendedCount = c; mFlags |= SSH2_FILEXFER_ATTR_EXTENDED; } + + /** No extensions will be included when the file attribute is written + to a sftp packet. */ + void clearExtensions() + { mExtendedCount = 0; mFlags &= ~SSH2_FILEXFER_ATTR_EXTENDED; } + + /** Returns the number of file attribute extentsions. */ + unsigned int extendedCount() const { return mExtendedCount; } + + /** Returns the flags for the sftp file attributes. */ + unsigned int flags() const { return mFlags; } + + /** Sets file's longname. See sftpFileAttr::longname. */ + void setLongname(QString l) { mLongname = l.latin1(); } + + /** Returns a string describing the file attributes. The format is specific + to the implementation of the sftp server. In most cases (ie OpenSSH) + this is similar to the long output of 'ls'. */ + QString longname() const { return mLongname; } + + void setLinkDestination(const QString& target) + { mLinkDestination = target; } + + QString linkDestination() + { return mLinkDestination; } + + /** Sets the actual type a symbolic link points to. */ + void setLinkType (mode_t type) { mLinkType = type; } + + mode_t linkType() const { return mLinkType; } + + /** No descriptions */ + void setFilename(const QString& fn) + { mFilename = fn; } + + QString filename() const + { return mFilename; } + + /** Returns a UDSEntry describing the file. + The UDSEntry is generated from the sftp file attributes. */ + KIO::UDSEntry entry(); + + /** Use to output the file attributes to a sftp packet + This will only write the sftp ATTR structure to the stream. + It will never write the filename and longname because the client + never sends those to the server. */ + friend QDataStream& operator<< (QDataStream&, const sftpFileAttr&); + + /** Use to read a file attribute from a sftp packet. + Read this carefully! If the DirAttrs flag is true, this will + read the filename, longname, and file attributes from the stream. + This is for use with listing directories. + If the DirAttrs flag is false, this will only read file attributes + from the stream. + BY DEFAULT, A NEW INSTANCE HAS DirAttrs == false */ + friend QDataStream& operator>> (QDataStream&, sftpFileAttr&); + + /** Parse longname for the owner and group names. */ + void getUserGroupNames(); + + /** Sets the DirAttrs flag. This flag affects how the >> operator works on data streams. */ + void setDirAttrsFlag(bool flag){ mDirAttrs = flag; } + + /** Gets the DirAttrs flag. */ + bool getDirAttrsFlag() const { return mDirAttrs; } + + friend kdbgstream& operator<< (kdbgstream& s, sftpFileAttr& a); + friend kndbgstream& operator<< (kndbgstream& s, sftpFileAttr& a); + + /** Returns the file type as determined from the file permissions */ + mode_t fileType() const; + + /** Set the encoding of the remote file system */ + void setEncoding( KRemoteEncoding* encoding ); +}; + +#endif |