diff options
Diffstat (limited to 'kftpgrabber/src/engine')
25 files changed, 9435 insertions, 0 deletions
diff --git a/kftpgrabber/src/engine/Makefile.am b/kftpgrabber/src/engine/Makefile.am new file mode 100644 index 0000000..ad6adb1 --- /dev/null +++ b/kftpgrabber/src/engine/Makefile.am @@ -0,0 +1,11 @@ +INCLUDES = -I.. -I$(srcdir)/.. \ + -I../misc -I$(srcdir)/../misc \ + $(all_includes) +METASOURCES = AUTO +noinst_LIBRARIES = libengine.a +noinst_HEADERS = socket.h thread.h directorylisting.h commands.h event.h \ + ftpsocket.h ftpdirectoryparser.h cache.h sftpsocket.h connectionretry.h \ + speedlimiter.h ssl.h +libengine_a_SOURCES = socket.cpp thread.cpp directorylisting.cpp commands.cpp \ + event.cpp ftpsocket.cpp ftpdirectoryparser.cpp cache.cpp sftpsocket.cpp \ + connectionretry.cpp speedlimiter.cpp ssl.cpp diff --git a/kftpgrabber/src/engine/cache.cpp b/kftpgrabber/src/engine/cache.cpp new file mode 100644 index 0000000..1b28dd6 --- /dev/null +++ b/kftpgrabber/src/engine/cache.cpp @@ -0,0 +1,175 @@ +/* + * This file is part of the KFTPGrabber project + * + * Copyright (C) 2003-2006 by the KFTPGrabber developers + * Copyright (C) 2003-2006 Jernej Kos <kostko@jweb-network.net> + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and + * NON-INFRINGEMENT. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations + * including the two. + * + * You must obey the GNU General Public License in all respects + * for all of the code used other than OpenSSL. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you + * do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source + * files in the program, then also delete it here. + */ + +#include "cache.h" +#include "socket.h" + +#include <kstaticdeleter.h> + +namespace KFTPEngine { + +Cache *Cache::m_self = 0; +static KStaticDeleter<Cache> staticCacheDeleter; + +Cache *Cache::self() +{ + if (!m_self) { + staticCacheDeleter.setObject(m_self, new Cache()); + } + + return m_self; +} + +Cache::Cache() +{ +} + +Cache::~Cache() +{ + if (m_self == this) + staticCacheDeleter.setObject(m_self, 0, false); +} + +void Cache::addDirectory(KURL &url, DirectoryListing listing) +{ + url.adjustPath(-1); + m_listingCache[url] = listing; +} + +void Cache::addDirectory(Socket *socket, DirectoryListing listing) +{ + KURL url = socket->getCurrentUrl(); + url.setPath(socket->getCurrentDirectory()); + + addDirectory(url, listing); +} + +void Cache::updateDirectoryEntry(Socket *socket, KURL &path, filesize_t filesize) +{ + KURL url = socket->getCurrentUrl(); + url.setPath(path.directory()); + url.adjustPath(-1); + + if (m_listingCache.contains(url)) { + DirectoryListing listing = m_listingCache[url]; + listing.updateEntry(path.fileName(), filesize); + + m_listingCache.replace(url, listing); + } +} + +void Cache::addPath(KURL &url, const QString &target) +{ + url.adjustPath(-1); + m_pathCache[url] = target; +} + +void Cache::addPath(Socket *socket, const QString &target) +{ + KURL url = socket->getCurrentUrl(); + url.setPath(socket->getCurrentDirectory()); + + addPath(url, target); +} + +void Cache::invalidateEntry(KURL &url) +{ + url.adjustPath(-1); + m_listingCache.remove(url); +} + +void Cache::invalidateEntry(Socket *socket, const QString &path) +{ + KURL url = socket->getCurrentUrl(); + url.setPath(path); + + invalidateEntry(url); +} + +void Cache::invalidatePath(KURL &url) +{ + url.adjustPath(-1); + m_pathCache.remove(url); +} + +void Cache::invalidatePath(Socket *socket, const QString &path) +{ + KURL url = socket->getCurrentUrl(); + url.setPath(path); + + invalidatePath(url); +} + +DirectoryListing Cache::findCached(KURL &url) +{ + url.adjustPath(-1); + + if (m_listingCache.contains(url)) + return m_listingCache[url]; + + DirectoryListing invalid; + invalid.setValid(false); + + return invalid; +} + +DirectoryListing Cache::findCached(Socket *socket, const QString &path) +{ + KURL url = socket->getCurrentUrl(); + url.setPath(path); + + return findCached(url); +} + +QString Cache::findCachedPath(KURL &url) +{ + url.adjustPath(-1); + + if (m_pathCache.contains(url)) + return m_pathCache[url]; + + return QString::null; +} + +QString Cache::findCachedPath(Socket *socket, const QString &path) +{ + KURL url = socket->getCurrentUrl(); + url.setPath(path); + + return findCachedPath(url); +} + +} diff --git a/kftpgrabber/src/engine/cache.h b/kftpgrabber/src/engine/cache.h new file mode 100644 index 0000000..7b6cf21 --- /dev/null +++ b/kftpgrabber/src/engine/cache.h @@ -0,0 +1,176 @@ +/* + * This file is part of the KFTPGrabber project + * + * Copyright (C) 2003-2006 by the KFTPGrabber developers + * Copyright (C) 2003-2006 Jernej Kos <kostko@jweb-network.net> + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and + * NON-INFRINGEMENT. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations + * including the two. + * + * You must obey the GNU General Public License in all respects + * for all of the code used other than OpenSSL. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you + * do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source + * files in the program, then also delete it here. + */ + +#ifndef KFTPENGINECACHE_H +#define KFTPENGINECACHE_H + +#include <qmap.h> +#include <kurl.h> + +#include "directorylisting.h" + +namespace KFTPEngine { + +class Socket; + +/** + * This class provides a cache of paths and directory listings to be used for + * faster operations. + * + * @author Jernej Kos <kostko@jweb-network.net> + */ +class Cache { +public: + static Cache *self(); + ~Cache(); + + /** + * Cache a directory listing. + * + * @param url The listing url (including host information) + * @param listing The directory listing to cache + */ + void addDirectory(KURL &url, DirectoryListing listing); + + /** + * Cache a directory listing, extracting the host information from the + * socket and using the current directory path. + * + * @param socket The socket to extract the host info from + * @param listing The directory listing to cache + */ + void addDirectory(Socket *socket, DirectoryListing listing); + + /** + * Updates a single directory entry. + * + * @param socket The socket to extract the host info from + * @param path Entry location + * @param filesize New file size + */ + void updateDirectoryEntry(Socket *socket, KURL &path, filesize_t filesize); + + /** + * Cache path information. + * + * @param url The url (including host information) + * @param target Actual target directory + */ + void addPath(KURL &url, const QString &target); + + /** + * Cache path information, extracting the host information from the + * socket and using the current directory path. + * + * @param socket The socket to extract the host info from + * @param target Actual target directory + */ + void addPath(Socket *socket, const QString &target); + + /** + * Invalidate a cached entry. + * + * @param url Url of the entry + */ + void invalidateEntry(KURL &url); + + /** + * Invalidate a cached entry. + * + * @param socket The socket to extract the host info from + * @param path Path of the entry + */ + void invalidateEntry(Socket *socket, const QString &path); + + /** + * Invalidate a cached path. + * + * @param url Url of the entry + */ + void invalidatePath(KURL &url); + + /** + * Invalidate a cached path. + * + * @param socket The socket to extract the host info from + * @param path Path of the entry + */ + void invalidatePath(Socket *socket, const QString &path); + + /** + * Retrieve a cached directory listing. + * + * @param url Url of the entry + * @return A valid DirectoryListing if found, an empty DirectoryListing otherwise + */ + DirectoryListing findCached(KURL &url); + + /** + * Retrieve a cached directory listing. + * + * @param socket The socket to extract the host info from + * @param path Path of the entry + * @return A valid DirectoryListing if found, an empty DirectoryListing otherwise + */ + DirectoryListing findCached(Socket *socket, const QString &path); + + /** + * Retrieve a cached path. + * + * @param url Url of the entry + * @return A target path if found, QString::null otherwise + */ + QString findCachedPath(KURL &url); + + /** + * Retrieve a cached path. + * + * @param socket The socket to extract the host info from + * @param path Path of the entry + * @return A target path if found, QString::null otherwise + */ + QString findCachedPath(Socket *socket, const QString &path); +protected: + Cache(); + static Cache *m_self; +private: + QMap<KURL, DirectoryListing> m_listingCache; + QMap<KURL, QString> m_pathCache; +}; + +} + +#endif diff --git a/kftpgrabber/src/engine/commands.cpp b/kftpgrabber/src/engine/commands.cpp new file mode 100644 index 0000000..5e0569f --- /dev/null +++ b/kftpgrabber/src/engine/commands.cpp @@ -0,0 +1,78 @@ +/* + * This file is part of the KFTPGrabber project + * + * Copyright (C) 2003-2006 by the KFTPGrabber developers + * Copyright (C) 2003-2006 Jernej Kos <kostko@jweb-network.net> + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and + * NON-INFRINGEMENT. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations + * including the two. + * + * You must obey the GNU General Public License in all respects + * for all of the code used other than OpenSSL. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you + * do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source + * files in the program, then also delete it here. + */ + +#include "commands.h" + +namespace KFTPEngine { + +namespace Commands { + +Base::Base(Socket *socket, Type command) + : m_command(command), + m_socket(socket), + m_wakeupEvent(0), + m_processing(false), + m_autoDestruct(false), + m_clean(false) +{ +} + +void Base::setProcessing(bool value) +{ + if (value) + m_processing++; + else + m_processing--; +} + +void Base::autoDestruct(ResetCode code) +{ + m_autoDestruct = true; + m_resetCode = code; +} + +void Base::wakeup(WakeupEvent *event) +{ + // The default implementation just calls process() + m_wakeupEvent = event; + process(); + m_wakeupEvent = 0; +} + +} + +} + diff --git a/kftpgrabber/src/engine/commands.h b/kftpgrabber/src/engine/commands.h new file mode 100644 index 0000000..679c673 --- /dev/null +++ b/kftpgrabber/src/engine/commands.h @@ -0,0 +1,136 @@ +/* + * This file is part of the KFTPGrabber project + * + * Copyright (C) 2003-2006 by the KFTPGrabber developers + * Copyright (C) 2003-2006 Jernej Kos <kostko@jweb-network.net> + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and + * NON-INFRINGEMENT. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations + * including the two. + * + * You must obey the GNU General Public License in all respects + * for all of the code used other than OpenSSL. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you + * do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source + * files in the program, then also delete it here. + */ + +#ifndef COMMANDS_H +#define COMMANDS_H + +#include "event.h" + +#define ENGINE_STANDARD_COMMAND_CONSTRUCTOR(class, type, cmd) public: \ + class(type *socket) : Commands::Base(socket, Commands::cmd), currentState(None) {} \ + private: \ + State currentState; \ + \ + type *socket() {\ + return static_cast<type*>(m_socket);\ + }\ + public: + +#define ENGINE_CANCELLATION_POINT { if (isDestructable()) \ + return; } + +#define setupCommandClass(class) if (m_cmdData) \ + delete m_cmdData; \ + m_cmdData = new class(this); + +#define chainCommandClass(class) Commands::Base *_cmd = new class(socket()); \ + socket()->addToCommandChain(_cmd); \ + socket()->nextCommand(); \ + return; + +#define activateCommandClass(class) if (m_cmdData) { \ + Commands::Base *_cmd = new class(this); \ + addToCommandChain(_cmd); \ + nextCommand(); \ + } else { \ + m_cmdData = new class(this); \ + m_cmdData->process(); \ + } + +namespace KFTPEngine { + +class Socket; + +namespace Commands { + +enum Type { + CmdNone, + CmdNext, + + // Actual commands + CmdConnect, + CmdConnectRetry, + CmdDisconnect, + CmdList, + CmdScan, + CmdGet, + CmdPut, + CmdDelete, + CmdRename, + CmdMkdir, + CmdChmod, + CmdRaw, + CmdFxp, + CmdKeepAlive +}; + +class Base { +public: + Base(Socket *socket, Type type); + + void setProcessing(bool value); + bool isProcessing() { return m_processing > 0; } + + void autoDestruct(ResetCode code); + bool isDestructable() { return m_autoDestruct && !isProcessing(); } + ResetCode resetCode() { return m_resetCode; } + + bool isClean() { return m_clean; } + + Type command() { return m_command; } + + bool isWakeup() { return m_wakeupEvent != 0; } + virtual void wakeup(WakeupEvent *event); + virtual void process() = 0; + virtual void cleanup() {} +protected: + void markClean() { m_clean = true; } +protected: + Type m_command; + Socket *m_socket; + WakeupEvent *m_wakeupEvent; + + int m_processing; + bool m_autoDestruct; + ResetCode m_resetCode; + bool m_clean; +}; + +} + +} + +#endif diff --git a/kftpgrabber/src/engine/connectionretry.cpp b/kftpgrabber/src/engine/connectionretry.cpp new file mode 100644 index 0000000..e93a1b9 --- /dev/null +++ b/kftpgrabber/src/engine/connectionretry.cpp @@ -0,0 +1,112 @@ +/* + * This file is part of the KFTPGrabber project + * + * Copyright (C) 2003-2006 by the KFTPGrabber developers + * Copyright (C) 2003-2006 Jernej Kos <kostko@jweb-network.net> + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and + * NON-INFRINGEMENT. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations + * including the two. + * + * You must obey the GNU General Public License in all respects + * for all of the code used other than OpenSSL. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you + * do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source + * files in the program, then also delete it here. + */ +#include "connectionretry.h" +#include "socket.h" +#include "thread.h" +#include "event.h" + +#include <klocale.h> + +namespace KFTPEngine { + +ConnectionRetry::ConnectionRetry(Socket *socket) + : QObject(), + m_socket(socket), + m_delay(socket->getConfigInt("retry_delay")), + m_max(socket->getConfigInt("max_retries")), + m_iteration(0) +{ + m_timer = new QTimer(this); + + connect(m_timer, SIGNAL(timeout()), this, SLOT(slotShouldRetry())); + connect(m_socket->thread()->eventHandler(), SIGNAL(engineEvent(KFTPEngine::Event*)), this, SLOT(slotEngineEvent(KFTPEngine::Event*))); +} + +void ConnectionRetry::startRetry() +{ + if ((m_iteration++ >= m_max && m_max != 0) || m_delay < 1) { + abortRetry(); + return; + } + + m_socket->setCurrentCommand(Commands::CmdConnectRetry); + m_socket->emitEvent(Event::EventMessage, i18n("Waiting %1 seconds before reconnect...").arg(m_delay)); + m_socket->emitEvent(Event::EventState, i18n("Waiting...")); + + m_timer->start(1000 * m_delay, true); +} + +void ConnectionRetry::slotShouldRetry() +{ + m_socket->setCurrentCommand(Commands::CmdNone); + if (m_max > 0) + m_socket->emitEvent(Event::EventMessage, i18n("Retrying connection (%1/%2)...").arg(m_iteration).arg(m_max)); + else + m_socket->emitEvent(Event::EventMessage, i18n("Retrying connection...").arg(m_iteration).arg(m_max)); + + // Reconnect + Thread *thread = m_socket->thread(); + thread->connect(m_socket->getCurrentUrl()); +} + +void ConnectionRetry::abortRetry() +{ + m_timer->stop(); + + // Disable retry so we avoid infinite loops + m_socket->setConfig("retry", 0); + + m_socket->setCurrentCommand(Commands::CmdNone); + m_socket->emitEvent(Event::EventMessage, i18n("Retry aborted.")); + m_socket->emitEvent(Event::EventState, i18n("Idle.")); + m_socket->emitEvent(Event::EventReady); + m_socket->emitError(ConnectFailed); + + // This object should be automagicly removed + QObject::deleteLater(); +} + +void ConnectionRetry::slotEngineEvent(KFTPEngine::Event *event) +{ + if (event->type() == Event::EventConnect) { + m_socket->emitEvent(Event::EventRetrySuccess); + + // This object should be automagicly removed + QObject::deleteLater(); + } +} + +} diff --git a/kftpgrabber/src/engine/connectionretry.h b/kftpgrabber/src/engine/connectionretry.h new file mode 100644 index 0000000..59a351d --- /dev/null +++ b/kftpgrabber/src/engine/connectionretry.h @@ -0,0 +1,86 @@ +/* + * This file is part of the KFTPGrabber project + * + * Copyright (C) 2003-2006 by the KFTPGrabber developers + * Copyright (C) 2003-2006 Jernej Kos <kostko@jweb-network.net> + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and + * NON-INFRINGEMENT. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations + * including the two. + * + * You must obey the GNU General Public License in all respects + * for all of the code used other than OpenSSL. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you + * do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source + * files in the program, then also delete it here. + */ +#ifndef KFTPENGINECONNECTIONRETRY_H +#define KFTPENGINECONNECTIONRETRY_H + +#include <qobject.h> +#include <qtimer.h> + +namespace KFTPEngine { + +class Socket; +class Event; + +/** + * This class will retry to reconnect to the currently set URL for the + * socket specified in constructor. + * + * @author Jernej Kos <kostko@jweb-network.net> + */ +class ConnectionRetry : public QObject +{ +Q_OBJECT +public: + /** + * Constructs a new ConnectionRetry class instance. + */ + ConnectionRetry(Socket *socket); + + /** + * Start the reconnect cycle. + */ + void startRetry(); + + /** + * Abort the running reconnect cycle and schedule this object's + * destruction. + */ + void abortRetry(); +private: + Socket *m_socket; + int m_delay; + int m_max; + int m_iteration; + + QTimer *m_timer; +private slots: + void slotShouldRetry(); + void slotEngineEvent(KFTPEngine::Event *event); +}; + +} + +#endif diff --git a/kftpgrabber/src/engine/directorylisting.cpp b/kftpgrabber/src/engine/directorylisting.cpp new file mode 100644 index 0000000..1647bb2 --- /dev/null +++ b/kftpgrabber/src/engine/directorylisting.cpp @@ -0,0 +1,188 @@ +/* + * This file is part of the KFTPGrabber project + * + * Copyright (C) 2003-2006 by the KFTPGrabber developers + * Copyright (C) 2003-2006 Jernej Kos <kostko@jweb-network.net> + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and + * NON-INFRINGEMENT. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations + * including the two. + * + * You must obey the GNU General Public License in all respects + * for all of the code used other than OpenSSL. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you + * do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source + * files in the program, then also delete it here. + */ + +#include "directorylisting.h" +#include "misc/filter.h" + +#include <qdatetime.h> + +#include <klocale.h> +#include <kglobal.h> +#include <kmimetype.h> + +#include <sys/stat.h> + +using namespace KFTPCore::Filter; +using namespace KIO; + +namespace KFTPEngine { + +DirectoryEntry::DirectoryEntry() +{ +} + +KIO::UDSEntry DirectoryEntry::toUdsEntry() const +{ + bool directory = m_type == 'd'; + UDSAtom atom; + UDSEntry entry; + + atom.m_uds = UDS_NAME; + atom.m_str = m_filename; + entry.append(atom); + + atom.m_uds = UDS_SIZE; + atom.m_long = m_size; + entry.append(atom); + + atom.m_uds = UDS_MODIFICATION_TIME; + atom.m_long = m_time; + entry.append(atom); + + atom.m_uds = UDS_USER; + atom.m_str = m_owner; + entry.append(atom); + + atom.m_uds = UDS_GROUP; + atom.m_str = m_group; + entry.append(atom); + + atom.m_uds = UDS_ACCESS; + atom.m_long = m_permissions; + entry.append(atom); + + if (!m_link.isEmpty()) { + atom.m_uds = UDS_LINK_DEST; + atom.m_str = m_link; + entry.append(atom); + + KMimeType::Ptr mime = KMimeType::findByURL(KURL("ftp://host/" + m_filename)); + if (mime->name() == KMimeType::defaultMimeType()) { + atom.m_uds = UDS_GUESSED_MIME_TYPE; + atom.m_str = "inode/directory"; + entry.append(atom); + + directory = true; + } + } + + atom.m_uds = UDS_FILE_TYPE; + atom.m_long = directory ? S_IFDIR : S_IFREG; + entry.append(atom); + + return entry; +} + +QString DirectoryEntry::timeAsString() +{ + QDateTime dt; + dt.setTime_t(time()); + + return KGlobal::locale()->formatDateTime(dt); +} + +bool DirectoryEntry::operator<(const DirectoryEntry &entry) const +{ + const Action *firstAction = Filters::self()->process(*this, Action::Priority); + const Action *secondAction = Filters::self()->process(entry, Action::Priority); + + int priorityFirst = firstAction ? firstAction->value().toInt() : 0; + int prioritySecond = secondAction ? secondAction->value().toInt() : 0; + + if (priorityFirst == prioritySecond) { + if (isDirectory() != entry.isDirectory()) + return isDirectory(); + + return m_filename < entry.m_filename; + } + + return priorityFirst > prioritySecond; +} + +DirectoryTree::DirectoryTree(DirectoryEntry entry) + : m_entry(entry) +{ + m_directories.setAutoDelete(true); +} + +void DirectoryTree::addFile(DirectoryEntry entry) +{ + m_files.append(entry); +} + +DirectoryTree *DirectoryTree::addDirectory(DirectoryEntry entry) +{ + DirectoryTree *tree = new DirectoryTree(entry); + m_directories.append(tree); + + return tree; +} + +DirectoryListing::DirectoryListing(const KURL &path) + : m_valid(true), + m_path(path) +{ +} + +DirectoryListing::~DirectoryListing() +{ + m_list.clear(); +} + +void DirectoryListing::addEntry(DirectoryEntry entry) +{ + m_list.append(entry); +} + +void DirectoryListing::updateEntry(const QString &filename, ::filesize_t size) +{ + QValueList<DirectoryEntry>::iterator listEnd = m_list.end(); + for (QValueList<DirectoryEntry>::iterator i = m_list.begin(); i != listEnd; i++) { + if ((*i).filename() == filename) { + (*i).setSize(size); + return; + } + } + + // Entry not found, add one + DirectoryEntry entry; + entry.setFilename(filename); + entry.setSize(size); + + addEntry(entry); +} + +} diff --git a/kftpgrabber/src/engine/directorylisting.h b/kftpgrabber/src/engine/directorylisting.h new file mode 100644 index 0000000..b332d37 --- /dev/null +++ b/kftpgrabber/src/engine/directorylisting.h @@ -0,0 +1,141 @@ +/* + * This file is part of the KFTPGrabber project + * + * Copyright (C) 2003-2006 by the KFTPGrabber developers + * Copyright (C) 2003-2006 Jernej Kos <kostko@jweb-network.net> + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and + * NON-INFRINGEMENT. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations + * including the two. + * + * You must obey the GNU General Public License in all respects + * for all of the code used other than OpenSSL. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you + * do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source + * files in the program, then also delete it here. + */ + +#ifndef KFTPNETWORKDIRECTORYLISTING_H +#define KFTPNETWORKDIRECTORYLISTING_H + +#include <kio/global.h> +#include <kurl.h> + +#include <qvaluelist.h> +#include <qptrlist.h> + +#include <time.h> +#include <sys/time.h> + +typedef unsigned long long int filesize_t; + +namespace KFTPEngine { + +class DirectoryEntry { +public: + DirectoryEntry(); + + void setFilename(const QString &filename) { m_filename = filename; } + void setOwner(const QString &owner) { m_owner = owner; } + void setGroup(const QString &group) { m_group = group; } + void setLink(const QString &link) { m_link = link; } + void setPermissions(int permissions) { m_permissions = permissions; } + void setSize(filesize_t size) { m_size = size; } + void setType(char type) { m_type = type; } + void setTime(time_t time) { m_time = time; } + + QString filename() const { return m_filename; } + QString owner() const { return m_owner; } + QString group() const { return m_group; } + QString link() const { return m_link; } + int permissions() const { return m_permissions; } + filesize_t size() const { return m_size; } + char type() const { return m_type; } + time_t time() const { return m_time; } + QString timeAsString(); + + bool isDirectory() const { return m_type == 'd'; } + bool isFile() const { return m_type == 'f'; } + bool isDevice() const { return m_type == 'c' || m_type == 'b'; } + bool isSymlink() const { return !m_link.isEmpty(); } + + KIO::UDSEntry toUdsEntry() const; + + struct tm timeStruct; + + bool operator<(const DirectoryEntry &entry) const; +private: + QString m_filename; + QString m_owner; + QString m_group; + QString m_link; + + int m_permissions; + filesize_t m_size; + char m_type; + time_t m_time; +}; + +class DirectoryTree { +public: + typedef QValueList<DirectoryEntry>::ConstIterator FileIterator; + typedef QPtrList<DirectoryTree>::ConstIterator DirIterator; + + DirectoryTree() {} + DirectoryTree(DirectoryEntry entry); + + void addFile(DirectoryEntry entry); + DirectoryTree *addDirectory(DirectoryEntry entry); + + DirectoryEntry info() { return m_entry; } + + QValueList<DirectoryEntry> *files() { return &m_files; } + QPtrList<DirectoryTree> *directories() { return &m_directories; } +private: + DirectoryEntry m_entry; + QValueList<DirectoryEntry> m_files; + QPtrList<DirectoryTree> m_directories; +}; + +/** + * @author Jernej Kos <kostko@jweb-network.net> + */ +class DirectoryListing { +public: + DirectoryListing(const KURL &path = KURL()); + ~DirectoryListing(); + + void addEntry(DirectoryEntry entry); + void updateEntry(const QString &filename, filesize_t size); + QValueList<DirectoryEntry> list() { return m_list; } + + void setValid(bool value) { m_valid = value; } + bool isValid() { return m_valid; } +private: + bool m_valid; + KURL m_path; + QValueList<DirectoryEntry> m_list; +}; + +} + +#endif diff --git a/kftpgrabber/src/engine/event.cpp b/kftpgrabber/src/engine/event.cpp new file mode 100644 index 0000000..bfbf6f6 --- /dev/null +++ b/kftpgrabber/src/engine/event.cpp @@ -0,0 +1,166 @@ +/* + * This file is part of the KFTPGrabber project + * + * Copyright (C) 2003-2006 by the KFTPGrabber developers + * Copyright (C) 2003-2006 Jernej Kos <kostko@jweb-network.net> + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and + * NON-INFRINGEMENT. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations + * including the two. + * + * You must obey the GNU General Public License in all respects + * for all of the code used other than OpenSSL. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you + * do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source + * files in the program, then also delete it here. + */ + +#include "event.h" +#include "thread.h" + +namespace KFTPEngine { + +EventParameter::EventParameter() +{ +} + +EventParameter::EventParameter(const QString &string) +{ + m_type = ParamString; + m_string = string; +} + +EventParameter::EventParameter(const KURL &url) +{ + m_type = ParamUrl; + m_url = url; +} + +EventParameter::EventParameter(DirectoryListing listing) +{ + m_type = ParamDirListing; + m_directoryListing = listing; +} + +EventParameter::EventParameter(DirectoryTree tree) +{ + m_type = ParamDirTree; + m_directoryTree = tree; +} + +EventParameter::EventParameter(ErrorCode error) +{ + m_type = ParamErrorCode; + m_errorCode = error; +} + +EventParameter::EventParameter(filesize_t size) +{ + m_type = ParamSize; + m_fileSize = size; +} + +EventParameter::EventParameter(void *data) +{ + m_type = ParamData; + m_data = data; +} + +QString EventParameter::asString() const +{ + return m_string; +} + +KURL EventParameter::asUrl() const +{ + return m_url; +} + +DirectoryListing EventParameter::asDirectoryListing() const +{ + return m_directoryListing; +} + +DirectoryTree EventParameter::asDirectoryTree() const +{ + return m_directoryTree; +} + +ErrorCode EventParameter::asErrorCode() const +{ + return m_errorCode; +} + +filesize_t EventParameter::asFileSize() const +{ + return m_fileSize; +} + +bool EventParameter::asBoolean() const +{ + return (bool) m_fileSize; +} + +void *EventParameter::asData() const +{ + return m_data; +} + +Event::Event(Type type, QValueList<EventParameter> params) + : QCustomEvent(65123), + m_type(type), + m_params(params) +{ +} + +Event::~Event() +{ +} + +EventHandler::EventHandler(Thread *thread) + : QObject(), + m_thread(thread) +{ +} + +void EventHandler::customEvent(QCustomEvent *e) +{ + if (e->type() == 65123) { + Event *ev = static_cast<Event*>(e); + + emit engineEvent(ev); + + switch (ev->type()) { + case Event::EventConnect: emit connected(); break; + case Event::EventDisconnect: emit disconnected(); break; + case Event::EventResponse: + case Event::EventMultiline: { + emit gotResponse(ev->getParameter(0).asString()); + break; + } + case Event::EventRaw: emit gotRawResponse(ev->getParameter(0).asString()); break; + default: break; + } + } +} + +} diff --git a/kftpgrabber/src/engine/event.h b/kftpgrabber/src/engine/event.h new file mode 100644 index 0000000..e21a45e --- /dev/null +++ b/kftpgrabber/src/engine/event.h @@ -0,0 +1,372 @@ +/* + * This file is part of the KFTPGrabber project + * + * Copyright (C) 2003-2006 by the KFTPGrabber developers + * Copyright (C) 2003-2006 Jernej Kos <kostko@jweb-network.net> + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and + * NON-INFRINGEMENT. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations + * including the two. + * + * You must obey the GNU General Public License in all respects + * for all of the code used other than OpenSSL. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you + * do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source + * files in the program, then also delete it here. + */ + +#ifndef KFTPNETWORKEVENT_H +#define KFTPNETWORKEVENT_H + +#include <qobject.h> +#include <qevent.h> +#include <qshared.h> + +#include "directorylisting.h" + +namespace KFTPEngine { + +/** + * Engine reset codes. TODO description of each reset code. + */ +enum ResetCode { + Ok, + UserAbort, + Failed, + FailedSilently +}; + +/** + * Engine error codes. TODO: description of each error code. + */ +enum ErrorCode { + ConnectFailed, + LoginFailed, + PermissionDenied, + FileNotFound, + OperationFailed, + ListFailed, + FileOpenFailed +}; + +/** + * This class is used for event parameter passing between the socket + * thread and the main thread. It supports multiple parameter types. + * + * @author Jernej Kos <kostko@jweb-network.net> + */ +class EventParameter { +public: + /** + * Parameter type enum. + */ + enum Type { + ParamString, + ParamUrl, + ParamDirListing, + ParamDirTree, + ParamErrorCode, + ParamSize, + ParamData + }; + + EventParameter(); + + /** + * Constructs a new string parameter. + * + * @param string The QString value + */ + EventParameter(const QString &string); + + /** + * Construct a new url parameter. + * + * @param url The KURL value + */ + EventParameter(const KURL &url); + + /** + * Construct a new directory listing parameter. + * + * @param listing The DirectoryListing value + */ + EventParameter(DirectoryListing listing); + + /** + * Construct a new directory tree parameter. + * + * @param tree The DirectoryTree value + */ + EventParameter(DirectoryTree tree); + + /** + * Construct a new error code parameter. + * + * @param error The ErrorCode value + */ + EventParameter(ErrorCode error); + + /** + * Construct a new filesize parameter. + * + * @param size The filesize_t value + */ + EventParameter(filesize_t size); + + /** + * Constructs a new data parameter. + * + * @param data A pointer to some data. + */ + EventParameter(void *data); + + /** + * Returns the parameter's string value. + * + * @return Parameter's string value + */ + QString asString() const; + + /** + * Returns the parameter's url value. + * + * @return Parameter's url value + */ + KURL asUrl() const; + + /** + * Returns the parameter's directory listing value. + * + * @return Parameter's directory listing value + */ + DirectoryListing asDirectoryListing() const; + + /** + * Returns the parameter's directory tree value. + * + * @return Parameter's directory tree value. + */ + DirectoryTree asDirectoryTree() const; + + /** + * Returns the parameter's error code value. + * + * @return Parameter's error code value + */ + ErrorCode asErrorCode() const; + + /** + * Returns the parameter's filesize value. + * + * @return Parameter's filesize value + */ + filesize_t asFileSize() const; + + /** + * Returns the parameter's boolean value. + * + * @return Parameter's boolean value + */ + bool asBoolean() const; + + /** + * Returns raw parameter data pointer. + * + * @return Raw parameter data pointer + */ + void *asData() const; +private: + Type m_type; + + QString m_string; + KURL m_url; + DirectoryListing m_directoryListing; + DirectoryTree m_directoryTree; + ErrorCode m_errorCode; + filesize_t m_fileSize; + void *m_data; +}; + +/** + * A wakeup event is a special type event used to transfer some response from + * the GUI to the engine that has been temporarly suspended. After receiving + * this event, the current command handler's wakeup() method will be called + * with this event as a parameter. + * + * @author Jernej Kos <kostko@jweb-network.net> + */ +class WakeupEvent { +public: + /** + * Possible wakeup event types. Each type should subclass this class to + * provide any custom methods needed. + */ + enum Type { + WakeupFileExists, + WakeupPubkey + }; + + /** + * Constructs a new wakeup event of specified type. + * + * @param type Event type + */ + WakeupEvent(Type type) : m_type(type) {} +private: + Type m_type; +}; + +/** + * A file exists wakeup event that is used to continue pending transfers. + * + * @author Jernej Kos <kostko@jweb-network.net> + */ +class FileExistsWakeupEvent : public WakeupEvent { +public: + /** + * Possible actions the engine can take. + */ + enum Action { + Overwrite, + Rename, + Resume, + Skip + }; + + /** + * Constructs a new file exists wakeup event with Skip action as default. + */ + FileExistsWakeupEvent() : WakeupEvent(WakeupFileExists), action(Skip) {} + + Action action; + QString newFileName; +}; + +/** + * A public key password request event for SFTP connections. + * + * @author Jernej Kos <kostko@jweb-network.net> + */ +class PubkeyWakeupEvent : public WakeupEvent { +public: + /** + * Constructs a new public key wakeup event. + */ + PubkeyWakeupEvent() : WakeupEvent(WakeupPubkey) {} + + QString password; +}; + +/** + * This class represents an event that is passed to the EventHandler for + * processing. It can have multiple EventParameters. + * + * @author Jernej Kos <kostko@jweb-network.net> + */ +class Event : public QCustomEvent { +public: + enum Type { + EventMessage, + EventCommand, + EventResponse, + EventMultiline, + EventRaw, + EventDirectoryListing, + EventDisconnect, + EventError, + EventConnect, + EventReady, + EventState, + EventScanComplete, + EventRetrySuccess, + EventReloadNeeded, + + // Transfer events + EventTransferComplete, + EventResumeOffset, + + // Events that require wakeup events + EventFileExists, + EventPubkeyPassword + }; + + /** + * Construct a new event with a parameter list. + * + * @param params Parameter list + */ + Event(Type type, QValueList<EventParameter> params); + ~Event(); + + /** + * Return the event's type. + * + * @return Event's type + */ + Type type() { return m_type; } + + /** + * Returns the parameter with a specific index. + * + * @param index Parameter's index + * @return An EventParameter object + */ + EventParameter getParameter(int index) { return m_params[index]; } +protected: + Type m_type; + QValueList<EventParameter> m_params; +}; + +class Thread; + +/** + * This class handles events receieved from the thread and passes them + * on to the GUI as normal Qt signals. + * + * @author Jernej Kos <kostko@jweb-network.net> + */ +class EventHandler : public QObject { +Q_OBJECT +public: + /** + * Construct a new event handler. + * + * @param thread The thread this event handler belongs to + */ + EventHandler(Thread *thread); +protected: + void customEvent(QCustomEvent *e); +protected: + Thread *m_thread; +signals: + void engineEvent(KFTPEngine::Event *event); + + void connected(); + void disconnected(); + void gotResponse(const QString &text); + void gotRawResponse(const QString &text); +}; + +} + +#endif diff --git a/kftpgrabber/src/engine/ftpdirectoryparser.cpp b/kftpgrabber/src/engine/ftpdirectoryparser.cpp new file mode 100644 index 0000000..074328c --- /dev/null +++ b/kftpgrabber/src/engine/ftpdirectoryparser.cpp @@ -0,0 +1,1144 @@ +/* + * This file is part of the KFTPGrabber project + * + * Copyright (C) 2003-2004 by the KFTPGrabber developers + * Copyright (C) 2003-2004 Jernej Kos <kostko@jweb-network.net> + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and + * NON-INFRINGEMENT. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations + * including the two. + * + * You must obey the GNU General Public License in all respects + * for all of the code used other than OpenSSL. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you + * do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source + * files in the program, then also delete it here. + */ +#include "ftpdirectoryparser.h" +#include "ftpsocket.h" + +#include <qvaluevector.h> +#include <qstringlist.h> +#include <qdatetime.h> + +#include <time.h> +#include <sys/stat.h> + +namespace KFTPEngine { + +class DToken { +public: + enum TokenTypeInfo { + Unknown, + Yes, + No + }; + + DToken() + : m_token(QString::null), + m_valid(false) + { + } + + DToken(const QString &token, int start = 0) + : m_token(token), + m_length(token.length()), + m_start(start), + m_valid(true), + m_numeric(Unknown), + m_leftNumeric(Unknown), + m_rightNumeric(Unknown) + { + } + + int getStart() + { + return m_start; + } + + QString getToken() + { + return m_token; + } + + int getLength() + { + return m_length; + } + + QString getString(int type = 0) + { + switch (type) { + case 0: return m_token; break; + case 1: { + if (!isRightNumeric() || isNumeric()) + return QString::null; + + int pos = m_length - 1; + while (m_token[pos] >= '0' && m_token[pos] <= '9') + pos--; + + return m_token.mid(0, pos + 1); + break; + } + case 2: { + if (!isLeftNumeric() || isNumeric()) + return QString::null; + + int len = 0; + while (m_token[len] >= '0' && m_token[len] <= '9') + len++; + + return m_token.mid(0, len); + break; + } + } + + return QString::null; + } + + int find(const char *chr, unsigned int start = 0) const + { + if (!chr) + return -1; + + for (unsigned int i = start; i < m_length; i++) { + for (int c = 0; chr[c]; c++) { + if (m_token[i] == chr[c]) + return i; + } + } + + return -1; + } + + unsigned long long getInteger() + { + return m_token.toULongLong(); + } + + unsigned long long getInteger(unsigned int start, int len) + { + return m_token.mid(start, len).toULongLong(); + } + + bool isValid() + { + return m_valid; + } + + bool isNumeric() + { + if (m_numeric == Unknown) { + bool ok; + (void) m_token.toInt(&ok); + + m_numeric = ok ? Yes : No; + } + + return m_numeric == Yes; + } + + + bool isNumeric(unsigned int start, unsigned int len) + { + len = start + len < m_length ? start + len : m_length; + + for (unsigned int i = start; i < len; i++) { + if (m_token[i] < '0' || m_token[i] > '9') + return false; + } + + return true; + } + + bool isLeftNumeric() + { + if (m_leftNumeric == Unknown) { + if (m_length < 2) + m_leftNumeric = No; + else if (m_token[0] < '0' || m_token[0] > '9') + m_leftNumeric = No; + else + m_leftNumeric = Yes; + } + + return m_leftNumeric == Yes; + } + + bool isRightNumeric() + { + if (m_rightNumeric == Unknown) { + if (m_length < 2) + m_rightNumeric = No; + else if (m_token[m_length - 1] < '0' || m_token[m_length - 1] > '9') + m_rightNumeric = No; + else + m_rightNumeric = Yes; + } + + return m_rightNumeric == Yes; + } + + QChar operator[](unsigned int n) const + { + return m_token[n]; + } +private: + QString m_token; + unsigned int m_length; + int m_start; + bool m_valid; + + TokenTypeInfo m_numeric; + TokenTypeInfo m_leftNumeric; + TokenTypeInfo m_rightNumeric; +}; + +class DLine { +public: + DLine(const QString &line) + : m_line(line.stripWhiteSpace()), + m_parsePos(0) + { + } + + bool getToken(unsigned int index, DToken &token, bool toEnd = false) + { + if (!toEnd) { + if (m_tokens.count() > index) { + token = m_tokens[index]; + return true; + } + + unsigned int start = m_parsePos; + while (m_parsePos < m_line.length()) { + if (m_line[m_parsePos] == ' ') { + m_tokens.append(DToken(m_line.mid(start, m_parsePos - start), start)); + + while (m_line[m_parsePos] == ' ' && m_parsePos < m_line.length()) + m_parsePos++; + + if (m_tokens.count() > index) { + token = m_tokens[index]; + return true; + } + + start = m_parsePos; + } + + m_parsePos++; + } + + if (m_parsePos != start) { + m_tokens.append(DToken(m_line.mid(start, m_parsePos - start), start)); + } + + if (m_tokens.count() > index) { + token = m_tokens[index]; + return true; + } + + return false; + } else { + if (m_endLineTokens.count() > index) { + token = m_endLineTokens[index]; + return true; + } + + if (m_tokens.count() <= index && !getToken(index, token)) + return false; + + for (unsigned int i = m_endLineTokens.count(); i <= index; i++) { + m_endLineTokens.append(DToken(m_line.mid(m_tokens[i].getStart()))); + } + + token = m_endLineTokens[index]; + return true; + } + } +private: + QStringList m_stringList; + QValueVector<DToken> m_tokens; + QValueVector<DToken> m_endLineTokens; + QString m_line; + unsigned int m_parsePos; +}; + +FtpDirectoryParser::FtpDirectoryParser(FtpSocket *socket) + : m_socket(socket), + m_listing(DirectoryListing(socket->getCurrentDirectory())) +{ + // Populate month names as they appear in the listing + m_monthNameMap["jan"] = 1; + m_monthNameMap["feb"] = 2; + m_monthNameMap["mar"] = 3; + m_monthNameMap["apr"] = 4; + m_monthNameMap["may"] = 5; + m_monthNameMap["jun"] = 6; + m_monthNameMap["june"] = 6; + m_monthNameMap["jul"] = 7; + m_monthNameMap["july"] = 7; + m_monthNameMap["aug"] = 8; + m_monthNameMap["sep"] = 9; + m_monthNameMap["sept"] = 9; + m_monthNameMap["oct"] = 10; + m_monthNameMap["nov"] = 11; + m_monthNameMap["dec"] = 12; + + m_monthNameMap["1"] = 1; + m_monthNameMap["01"] = 1; + m_monthNameMap["2"] = 2; + m_monthNameMap["02"] = 2; + m_monthNameMap["3"] = 3; + m_monthNameMap["03"] = 3; + m_monthNameMap["4"] = 4; + m_monthNameMap["04"] = 4; + m_monthNameMap["5"] = 5; + m_monthNameMap["05"] = 5; + m_monthNameMap["6"] = 6; + m_monthNameMap["06"] = 6; + m_monthNameMap["7"] = 7; + m_monthNameMap["07"] = 7; + m_monthNameMap["8"] = 8; + m_monthNameMap["08"] = 8; + m_monthNameMap["9"] = 9; + m_monthNameMap["09"] = 9; + m_monthNameMap["10"] = 10; + m_monthNameMap["11"] = 11; + m_monthNameMap["12"] = 12; +} + +void FtpDirectoryParser::addDataLine(const QString &line) +{ + QString tmp(line); + tmp.append("\n"); + addData(tmp.ascii(), tmp.length()); +} + +void FtpDirectoryParser::addData(const char *data, int len) +{ + // Append new data to the buffer and check for any new lines + m_buffer.append(QString::fromAscii(data, len)); + + int pos; + while ((pos = m_buffer.find('\n')) > -1) { + DirectoryEntry entry; + QString line = m_buffer.mid(0, pos).stripWhiteSpace(); + line = m_socket->remoteEncoding()->decode(QCString(line.ascii())); + + if (parseLine(line, entry) && !entry.filename().isEmpty()) { + if (entry.type() == '-') + entry.setType('f'); + + m_listing.addEntry(entry); + } + + // Remove what we just parsed + m_buffer.remove(0, pos + 1); + } +} + +bool FtpDirectoryParser::parseMlsd(const QString &line, DirectoryEntry &entry) +{ + QStringList facts = QStringList::split(';', line); + QStringList::Iterator end = facts.end(); + + for (QStringList::Iterator i = facts.begin(); i != end; ++i) { + if ((*i).contains('=')) { + QString key = (*i).section('=', 0, 0).lower(); + QString value = (*i).section('=', 1, 1); + + if (key == "type") { + if (value == "file") + entry.setType('f'); + else if (value == "dir") + entry.setType('d'); + } else if (key == "size") { + entry.setSize(value.toULongLong()); + } else if (key == "modify") { + struct tm dt; + + dt.tm_year = value.left(4).toInt() - 1900; + dt.tm_mon = value.mid(4, 2).toInt() - 1; + dt.tm_mday = value.mid(6, 2).toInt(); + dt.tm_hour = value.mid(8, 2).toInt(); + dt.tm_min = value.mid(10, 2).toInt(); + dt.tm_sec = value.mid(12, 2).toInt(); + entry.setTime(mktime(&dt)); + } else if (key == "unix.mode") { + entry.setPermissions(value.toInt(0, 8)); + } else if (key == "unix.uid") { + entry.setOwner(value); + } else if (key == "unix.gid") { + entry.setGroup(value); + } + } else { + entry.setFilename((*i).stripWhiteSpace()); + } + } + + return true; +} + +bool FtpDirectoryParser::parseUnixPermissions(const QString &permissions, DirectoryEntry &entry) +{ + int p = 0; + + if (permissions[1] == 'r') p |= S_IRUSR; + if (permissions[2] == 'w') p |= S_IWUSR; + if (permissions[3] == 'x' || permissions[3] == 's') p |= S_IXUSR; + + if (permissions[4] == 'r') p |= S_IRGRP; + if (permissions[5] == 'w') p |= S_IWGRP; + if (permissions[6] == 'x' || permissions[6] == 's') p |= S_IXGRP; + + if (permissions[7] == 'r') p |= S_IROTH; + if (permissions[8] == 'w') p |= S_IWOTH; + if (permissions[9] == 'x' || permissions[9] == 't') p |= S_IXOTH; + + if (permissions[3] == 's' || permissions[3] == 'S') p |= S_ISUID; + if (permissions[6] == 's' || permissions[6] == 'S') p |= S_ISGID; + if (permissions[9] == 't' || permissions[9] == 'T') p |= S_ISVTX; + + entry.setPermissions(p); +} + +bool FtpDirectoryParser::parseLine(const QString &line, DirectoryEntry &entry) +{ + DLine *tLine = new DLine(line); + bool done = false; + + // Invalidate timestamp + entry.setTime(-1); + entry.timeStruct.tm_year = 0; + entry.timeStruct.tm_mon = 0; + entry.timeStruct.tm_hour = 0; + entry.timeStruct.tm_mday = 0; + entry.timeStruct.tm_min = 0; + entry.timeStruct.tm_sec = 0; + entry.timeStruct.tm_wday = 0; + entry.timeStruct.tm_yday = 0; + entry.timeStruct.tm_isdst = 0; + + // Attempt machine friendly format first, when socket supports MLSD + if (m_socket->getConfigInt("feat.mlsd")) + done = parseMlsd(line, entry); + + if (!done) + done = parseUnix(tLine, entry); + if (!done) + done = parseDos(tLine, entry); + if (!done) + done = parseVms(tLine, entry); + + if (done) { + // Convert datetime to UNIX epoch + if (entry.time() == -1) { + // Correct format for mktime + entry.timeStruct.tm_year -= 1900; + entry.timeStruct.tm_mon -= 1; + entry.setTime(mktime(&entry.timeStruct)); + } + + // Add symlink if any + if (entry.filename().contains(" -> ") > 0) { + int pos = entry.filename().findRev(" -> "); + + entry.setLink(entry.filename().mid(pos + 4)); + entry.setFilename(entry.filename().mid(0, pos)); + } + + // Parse owner into group/owner + if (entry.owner().contains(" ") > 0) { + int pos = entry.owner().find(" "); + + entry.setGroup(entry.owner().mid(pos + 1)); + entry.setOwner(entry.owner().mid(0, pos)); + } + + // Remove unwanted names + if (entry.filename() == "." || entry.filename() == "..") { + entry.setFilename(QString::null); + } + } + + delete tLine; + return done; +} + +bool FtpDirectoryParser::parseUnix(DLine *line, DirectoryEntry &entry) +{ + int index = 0; + DToken token; + + if (!line->getToken(index, token)) + return false; + + + char chr = token[0]; + if (chr != 'b' && + chr != 'c' && + chr != 'd' && + chr != 'l' && + chr != 'p' && + chr != 's' && + chr != '-') + return false; + + QString permissions = token.getString(); + entry.setType(chr); + + // Check for netware servers, which split the permissions into two parts + bool netware = false; + if (token.getLength() == 1) { + if (!line->getToken(++index, token)) + return false; + + permissions += " " + token.getString(); + netware = true; + } + + parseUnixPermissions(permissions, entry); + + int numOwnerGroup = 3; + if (!netware) { + // Filter out groupid, we don't need it + if (!line->getToken(++index, token)) + return false; + + if (!token.isNumeric()) + index--; + } + + // Repeat until numOwnerGroup is 0 since not all servers send every possible field + int startindex = index; + do { + // Reset index + index = startindex; + + entry.setOwner(QString::null); + for (int i = 0; i < numOwnerGroup; i++) { + if (!line->getToken(++index, token)) + return false; + + if (i) + entry.setOwner(entry.owner() + " "); + + entry.setOwner(entry.owner() + token.getString()); + } + + if (!line->getToken(++index, token)) + return false; + + + // Check for concatenated groupname and size fields + filesize_t size; + if (!parseComplexFileSize(token, size)) { + if (!token.isRightNumeric()) + continue; + + entry.setSize(token.getInteger()); + } else { + entry.setSize(size); + } + + // Append missing group to ownerGroup + if (!token.isNumeric() && token.isRightNumeric()) { + if (!entry.owner().isEmpty()) + entry.setOwner(entry.owner() + " "); + + entry.setOwner(entry.owner() + token.getString(1)); + } + + if (!parseUnixDateTime(line, index, entry)) + continue; + + // Get the filename + if (!line->getToken(++index, token, true)) + continue; + + entry.setFilename(token.getString()); + + // Filter out cpecial chars at the end of the filenames + chr = token[token.getLength() - 1]; + if (chr == '/' || + chr == '|' || + chr == '*') + entry.setFilename(entry.filename().mid(0, entry.filename().length() - 1)); + + return true; + } while (--numOwnerGroup); + + return false; +} + +bool FtpDirectoryParser::parseUnixDateTime(DLine *line, int &index, DirectoryEntry &entry) +{ + DToken token; + + // Get the month date field + QString dateMonth; + if (!line->getToken(++index, token)) + return false; + + // Some servers use the following date formats: + // 26-05 2002, 2002-10-14, 01-jun-99 + // slashes instead of dashes are also possible + int pos = token.find("-/"); + + if (pos != -1) { + int pos2 = token.find("-/", pos + 1); + + if (pos2 == -1) { + // something like 26-05 2002 + int day = token.getInteger(pos + 1, token.getLength() - pos - 1); + + if (day < 1 || day > 31) + return false; + + entry.timeStruct.tm_mday = day; + dateMonth = token.getString().left(pos); + } else if (!parseShortDate(token, entry)) { + return false; + } + } else { + dateMonth = token.getString(); + } + + bool bHasYearAndTime = false; + if (!entry.timeStruct.tm_mday) { + // Get day field + if (!line->getToken(++index, token)) + return false; + + int dateDay; + + // Check for non-numeric day + if (!token.isNumeric() && !token.isLeftNumeric()) { + if (dateMonth.right(1) == ".") + dateMonth.remove(dateMonth.length() - 1, 1); + + bool tmp; + dateDay = dateMonth.toInt(&tmp); + if (!tmp) + return false; + + dateMonth = token.getString(); + } else { + dateDay = token.getInteger(); + + if (token[token.getLength() - 1] == ',') + bHasYearAndTime = true; + } + + if (dateDay < 1 || dateDay > 31) + return false; + + entry.timeStruct.tm_mday = dateDay; + } + + if (!entry.timeStruct.tm_mon) { + // Check month name + if (dateMonth.right(1) == "," || dateMonth.right(1) == ".") + dateMonth.remove(dateMonth.length() - 1, 1); + + dateMonth = dateMonth.lower(); + + QMap<QString, int>::iterator iter = m_monthNameMap.find(dateMonth); + if (iter == m_monthNameMap.end()) + return false; + + entry.timeStruct.tm_mon = iter.data(); + } + + // Get time/year field + if (!line->getToken(++index, token)) + return false; + + pos = token.find(":.-"); + if (pos != -1) { + // token is a time + if (!pos || static_cast<size_t>(pos) == (token.getLength() - 1)) + return false; + + QString str = token.getString(); + bool tmp; + int hour = str.left(pos).toInt(&tmp); + if (!tmp) + return false; + + int minute = str.mid(pos + 1).toInt(&tmp); + if (!tmp) + return false; + + if (hour < 0 || hour > 23) + return false; + + if (minute < 0 || minute > 59) + return false; + + entry.timeStruct.tm_hour = hour; + entry.timeStruct.tm_min = minute; + + // Some servers use times only for files nweer than 6 months, + int year = QDate::currentDate().year(); + int now = QDate::currentDate().day() + 31 * QDate::currentDate().month(); + int file = entry.timeStruct.tm_mon * 31 + entry.timeStruct.tm_mday; + + if (now >= file) + entry.timeStruct.tm_year = year; + else + entry.timeStruct.tm_year = year - 1; + } else if (!entry.timeStruct.tm_year) { + // token is a year + if (!token.isNumeric() && !token.isLeftNumeric()) + return false; + + int year = token.getInteger(); + if (year > 3000) + return false; + + if (year < 1000) + year += 1900; + + entry.timeStruct.tm_year = year; + + if (bHasYearAndTime) { + if (!line->getToken(++index, token)) + return false; + + if (token.find(":") == 2 && token.getLength() == 5 && token.isLeftNumeric() && token.isRightNumeric()) { + int pos = token.find(":"); + + // token is a time + if (!pos || static_cast<size_t>(pos) == (token.getLength() - 1)) + return false; + + QString str = token.getString(); + bool tmp; + long hour = str.left(pos).toInt(&tmp); + if (!tmp) + return false; + + long minute = str.mid(pos + 1).toInt(&tmp); + if (!tmp) + return false; + + if (hour < 0 || hour > 23) + return false; + + if (minute < 0 || minute > 59) + return false; + + entry.timeStruct.tm_hour = hour; + entry.timeStruct.tm_min = minute; + } else { + index--; + } + } + } else { + index--; + } + + return true; +} + +bool FtpDirectoryParser::parseShortDate(DToken &token, DirectoryEntry &entry) +{ + if (token.getLength() < 1) + return false; + + bool gotYear = false; + bool gotMonth = false; + bool gotDay = false; + bool gotMonthName = false; + + int value = 0; + + int pos = token.find("-./"); + if (pos < 1) + return false; + + if (!token.isNumeric(0, pos)) { + // Seems to be monthname-dd-yy + + // Check month name + QString dateMonth = token.getString().mid(0, pos); + dateMonth = dateMonth.lower(); + + QMap<QString, int>::iterator iter = m_monthNameMap.find(dateMonth); + if (iter == m_monthNameMap.end()) + return false; + + entry.timeStruct.tm_mon = iter.data(); + gotMonth = true; + gotMonthName = true; + } else if (pos == 4) { + // Seems to be yyyy-mm-dd + int year = token.getInteger(0, pos); + + if (year < 1900 || year > 3000) + return false; + + entry.timeStruct.tm_year = year; + gotYear = true; + } else if (pos <= 2) { + int value = token.getInteger(0, pos); + + if (token[pos] == '.') { + // Maybe dd.mm.yyyy + if (value < 1900 || value > 3000) + return false; + + entry.timeStruct.tm_mday = value; + gotDay = true; + } else { + // Detect mm-dd-yyyy or mm/dd/yyyy and + // dd-mm-yyyy or dd/mm/yyyy + if (value < 1) + return false; + + if (value > 12) { + if (value > 31) + return false; + + entry.timeStruct.tm_mday = value; + gotDay = true; + } else { + entry.timeStruct.tm_mon = value; + gotMonth = true; + } + } + } else { + return false; + } + + + int pos2 = token.find("-./", pos + 1); + + if (pos2 == -1 || (pos2 - pos) == 1) + return false; + + if (static_cast<size_t>(pos2) == (token.getLength() - 1)) + return false; + + // If we already got the month and the second field is not numeric, + // change old month into day and use new token as month + if (!token.isNumeric(pos + 1, pos2 - pos - 1) && gotMonth) { + if (gotMonthName) + return false; + + if (gotDay) + return false; + + gotDay = true; + gotMonth = false; + entry.timeStruct.tm_mday = entry.timeStruct.tm_mon; + } + + if (gotYear || gotDay) { + // Month field in yyyy-mm-dd or dd-mm-yyyy + // Check month name + QString dateMonth = token.getString().mid(pos + 1, pos2 - pos - 1); + dateMonth = dateMonth.lower(); + + QMap<QString, int>::iterator iter = m_monthNameMap.find(dateMonth); + if (iter == m_monthNameMap.end()) + return false; + + entry.timeStruct.tm_mon = iter.data(); + gotMonth = true; + } else { + int value = token.getInteger(pos + 1, pos2 - pos - 1); + + // Day field in mm-dd-yyyy + if (value < 1 || value > 31) + return false; + + entry.timeStruct.tm_mday = value; + gotDay = true; + } + + value = token.getInteger(pos2 + 1, token.getLength() - pos2 - 1); + if (gotYear) { + // Day field in yyy-mm-dd + if (!value || value > 31) + return false; + + entry.timeStruct.tm_mday = value; + gotDay = true; + } else { + if (value < 0) + return false; + + if (value < 50) { + value += 2000; + } else if (value < 1000) { + value += 1900; + } + + entry.timeStruct.tm_year = value; + gotYear = true; + } + + if (!gotMonth || !gotDay || !gotYear) + return false; + + return true; +} + +bool FtpDirectoryParser::parseDos(DLine *line, DirectoryEntry &entry) +{ + int index = 0; + DToken token; + + // Get first token, has to be a valid date + if (!line->getToken(index, token)) + return false; + + if (!parseShortDate(token, entry)) + return false; + + // Extract time + if (!line->getToken(++index, token)) + return false; + + if (!parseTime(token, entry)) + return false; + + // If next token is <DIR>, entry is a directory + // else, it should be the filesize. + if (!line->getToken(++index, token)) + return false; + + if (token.getString() == "<DIR>") { + entry.setType('d'); + entry.setSize(0); + } else if (token.isNumeric() || token.isLeftNumeric()) { + // Convert size, filter out separators + unsigned long size = 0; + int len = token.getLength(); + + for (int i = 0; i < len; i++) { + char chr = token[i]; + + if (chr == ',' || chr == '.') + continue; + + if (chr < '0' || chr > '9') + return false; + + size *= 10; + size += chr - '0'; + } + + entry.setSize(size); + entry.setType('f'); + } else { + return false; + } + + // Extract filename + if (!line->getToken(++index, token, true)) + return false; + + entry.setFilename(token.getString()); + + return true; +} + + +bool FtpDirectoryParser::parseTime(DToken &token, DirectoryEntry &entry) +{ + int pos = token.find(":"); + if (pos < 1 || static_cast<unsigned int>(pos) >= (token.getLength() - 1)) + return false; + + int hour = token.getInteger(0, pos); + if (hour < 0 || hour > 23) + return false; + + int minute = token.getInteger(pos + 1, 2); + if (minute < 0 || minute > 59) + return false; + + // Convert to 24h format + if (!token.isRightNumeric()) { + if (token[token.getLength() - 2] == 'P') { + if (hour < 12) { + hour += 12; + } + } else if (hour == 12) { + hour = 0; + } + } + + entry.timeStruct.tm_hour = hour; + entry.timeStruct.tm_min = minute; + + return true; +} + +bool FtpDirectoryParser::parseVms(DLine *line, DirectoryEntry &entry) +{ + DToken token; + int index = 0; + + if (!line->getToken(index, token)) + return false; + + int pos = token.find(";"); + + if (pos == -1) + return false; + + if (pos > 4 && token.getString().mid(pos - 4, 4) == ".DIR") { + entry.setType('d'); + entry.setFilename(token.getString().left(pos - 4) + token.getString().mid(pos)); + } else { + entry.setType('f'); + entry.setFilename(token.getString()); + } + + // Get size + if (!line->getToken(++index, token)) + return false; + + if (!token.isNumeric() && !token.isLeftNumeric()) + return false; + + entry.setSize(token.getInteger()); + + // Get date + if (!line->getToken(++index, token)) + return false; + + if (!parseShortDate(token, entry)) + return false; + + // Get time + if (!line->getToken(++index, token)) + return true; + + if (!parseTime(token, entry)) { + int len = token.getLength(); + + if (token[0] == '[' && token[len] != ']') + return false; + if (token[0] == '(' && token[len] != ')') + return false; + if (token[0] != '[' && token[len] == ']') + return false; + if (token[0] != '(' && token[len] == ')') + return false; + + index--; + } + + // Owner / group + while (line->getToken(++index, token)) { + int len = token.getLength(); + + if (len > 2 && token[0] == '(' && token[len - 1] == ')') + entry.setPermissions(0); + else if (len > 2 && token[0] == '[' && token[len - 1] == ']') + entry.setOwner(token.getString().mid(1, len - 2)); + else + entry.setPermissions(0); + } + + return true; +} + +bool FtpDirectoryParser::parseComplexFileSize(DToken &token, filesize_t &size) +{ + if (token.isNumeric()) { + size = token.getInteger(); + return true; + } + + int len = token.getLength() - 1; + + char last = token[len]; + if (last == 'B' || last == 'b') { + char c = token[len]; + + if (c < '0' || c > '9') { + last = token[len]; + len--; + } + } + + size = 0; + + int dot = -1; + for (int i = 0; i < len; i++) { + char c = token[i]; + + if (c >= '0' && c <= '9') { + size *= 10; + size += c - '0'; + } else if (c == '.') { + if (dot != -1) + return false; + + dot = len - i - 1; + } else { + return false; + } + } + + switch (last) { + case 'k': + case 'K': { + size *= 1000; + break; + } + case 'm': + case 'M': { + size *= 1000 * 1000; + break; + } + case 'g': + case 'G': { + size *= 1000 * 1000 * 1000; + break; + } + case 't': + case 'T': { + size *= 1000 * 1000; + size *= 1000 * 1000; + break; + } + case 'b': + case 'B': break; + default: return false; + } + + while (dot-- > 0) + size /= 10; + + return true; +} + +} diff --git a/kftpgrabber/src/engine/ftpdirectoryparser.h b/kftpgrabber/src/engine/ftpdirectoryparser.h new file mode 100644 index 0000000..1b56831 --- /dev/null +++ b/kftpgrabber/src/engine/ftpdirectoryparser.h @@ -0,0 +1,92 @@ +/* + * This file is part of the KFTPGrabber project + * + * Copyright (C) 2003-2004 by the KFTPGrabber developers + * Copyright (C) 2003-2004 Jernej Kos <kostko@jweb-network.net> + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and + * NON-INFRINGEMENT. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations + * including the two. + * + * You must obey the GNU General Public License in all respects + * for all of the code used other than OpenSSL. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you + * do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source + * files in the program, then also delete it here. + */ + +#ifndef KFTPENGINEFTPDIRECTORYPARSER_H +#define KFTPENGINEFTPDIRECTORYPARSER_H + +#include <qmap.h> + +#include "directorylisting.h" + +namespace KFTPEngine { + +class FtpSocket; + +class DToken; +class DLine; + +/** + * This class can parse multiple directory formats. Some code portions have + * been taken from a windows FTP client "FileZilla by Tim Kosse" - the + * logic is mostly the same, the code has just been ported so it is more Qt + * and so it integrates nicely with the rest of the engine. + * + * @author Jernej Kos <kostko@jweb-network.net> + * @author Tim Kosse <tim.kosse@gmx.de> + */ +class FtpDirectoryParser { +public: + FtpDirectoryParser(FtpSocket *socket); + + void addData(const char *data, int len); + void addDataLine(const QString &line); + + bool parseLine(const QString &line, DirectoryEntry &entry); + DirectoryListing getListing() { return m_listing; } +private: + FtpSocket *m_socket; + QString m_buffer; + DirectoryListing m_listing; + + QMap<QString, int> m_monthNameMap; + + bool parseMlsd(const QString &line, DirectoryEntry &entry); + bool parseUnix(DLine *line, DirectoryEntry &entry); + bool parseDos(DLine *line, DirectoryEntry &entry); + bool parseVms(DLine *line, DirectoryEntry &entry); + + bool parseUnixDateTime(DLine *line, int &index, DirectoryEntry &entry); + bool parseShortDate(DToken &token, DirectoryEntry &entry); + bool parseTime(DToken &token, DirectoryEntry &entry); + + bool parseComplexFileSize(DToken &token, filesize_t &size); + + bool parseUnixPermissions(const QString &permissions, DirectoryEntry &entry); +}; + +} + +#endif diff --git a/kftpgrabber/src/engine/ftpsocket.cpp b/kftpgrabber/src/engine/ftpsocket.cpp new file mode 100644 index 0000000..2741f4d --- /dev/null +++ b/kftpgrabber/src/engine/ftpsocket.cpp @@ -0,0 +1,2749 @@ +/* + * This file is part of the KFTPGrabber project + * + * Copyright (C) 2003-2006 by the KFTPGrabber developers + * Copyright (C) 2003-2006 Jernej Kos <kostko@jweb-network.net> + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and + * NON-INFRINGEMENT. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations + * including the two. + * + * You must obey the GNU General Public License in all respects + * for all of the code used other than OpenSSL. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you + * do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source + * files in the program, then also delete it here. + */ + +#include "ftpsocket.h" +#include "thread.h" +#include "ftpdirectoryparser.h" +#include "cache.h" +#include "speedlimiter.h" +#include "ssl.h" + +#include "misc/kftpotpgenerator.h" +#include "misc/config.h" + +#include <qdir.h> + +#include <klocale.h> +#include <kstandarddirs.h> +#include <ksocketdevice.h> + +#include <utime.h> +#include <stdlib.h> +#include <errno.h> +#include <sys/socket.h> +#include <netinet/in.h> + +namespace KFTPEngine { + +FtpSocket::FtpSocket(Thread *thread) + : KNetwork::KStreamSocket(), + Socket(thread, "ftp"), + SpeedLimiterItem(), + m_login(false), + m_transferSocket(0), + m_directoryParser(0), + m_controlConnecting(false), + m_controlSsl(0), + m_dataSsl(0), + m_clientCert(0) +{ + enableRead(false); + setBlocking(false); +} + +FtpSocket::~FtpSocket() +{ + protoDisconnect(); +} + +void FtpSocket::poll() +{ + if (m_controlConnecting) { + if (isFatalError(error())) { + slotError(); + resetError(); + m_controlConnecting = false; + return; + } + + if (state() == Connected) { + m_controlConnecting = false; + slotConnected(); + } + + return; + } + + slotControlTryRead(); + + if (!m_buffer.isEmpty()) + processBuffer(); + + if (m_transferSocket) { + if (m_transferConnecting && m_transferSocket->state() == Connected) { + m_transferConnecting = false; + slotDataConnected(); + } else if (!m_transferConnecting) { + if (getCurrentCommand() == Commands::CmdPut) { + if (m_transferStart >= 2) + slotDataTryWrite(); + } else { + bool input; + m_transferSocket->socketDevice()->poll(&input, 0, 0, 0); + + if (input) + slotDataTryRead(); + } + } + } else if (m_serverSocket) { + bool input; + m_serverSocket->socketDevice()->poll(&input, 0, 0, 0); + + if (input) { + KNetwork::KActiveSocketBase *socket = m_serverSocket->accept(); + + if (socket) { + slotDataAccept(static_cast<KNetwork::KStreamSocket*>(socket)); + m_transferConnecting = false; + } + } + } + + // Check for timeouts + // NOTE This should be moved to a QTimer's slot when ported to Qt 4 + timeoutCheck(); + keepaliveCheck(); +} + +void FtpSocket::slotControlTryRead() +{ + QString tmpStr; + Q_LONG size = 0; + + // Read what we can + if (getConfigInt("ssl") && m_controlSsl) { + size = m_controlSsl->read(m_controlBuffer, sizeof(m_controlBuffer) - 1); + + if (size == -1) { + protoDisconnect(); + return; + } + } else + size = readBlock(m_controlBuffer, sizeof(m_controlBuffer) - 1); + + if (error() != NoError) { + // Have we been disconnected ? + if (error() != WouldBlock) + protoDisconnect(); + + return; + } + + if (size == 0) + return; + + for (int i = 0; i < size; i++) + if (m_controlBuffer[i] == 0) + m_controlBuffer[i] = '!'; + + memset(m_controlBuffer + size, 0, sizeof(m_controlBuffer) - size); + m_buffer.append(m_controlBuffer); +} + +void FtpSocket::processBuffer() +{ + // Parse any lines we might have + int pos; + while ((pos = m_buffer.find('\n')) > -1) { + QString line = m_buffer.mid(0, pos); + line = m_remoteEncoding->decode(QCString(line.ascii())); + parseLine(line); + + // Remove what we just parsed + m_buffer.remove(0, pos + 1); + } +} + +void FtpSocket::parseLine(const QString &line) +{ + // Is this the end of multiline response ? + if (!m_multiLineCode.isEmpty() && line.left(4) == m_multiLineCode) { + m_multiLineCode = ""; + emitEvent(Event::EventResponse, line); + } else if (line[3] == '-' && m_multiLineCode.isEmpty()) { + m_multiLineCode = line.left(3) + " "; + emitEvent(Event::EventMultiline, line); + } else if (!m_multiLineCode.isEmpty()) { + emitEvent(Event::EventMultiline, line); + } else { + // Normal response + emitEvent(Event::EventResponse, line); + } + + timeoutWait(false); + + // Parse our response + m_response = line; + nextCommand(); +} + +bool FtpSocket::isResponse(const QString &code) +{ + QString ref; + + if (isMultiline()) + ref = m_multiLineCode; + else + ref = m_response; + + return ref.left(code.length()) == code; +} + +void FtpSocket::sendCommand(const QString &command) +{ + emitEvent(Event::EventCommand, command); + QCString buffer(m_remoteEncoding->encode(command) + "\r\n"); + + if (getConfigInt("ssl") && m_controlSsl) + m_controlSsl->write(buffer.data(), buffer.length()); + else + writeBlock(buffer.data(), buffer.length()); + + timeoutWait(true); +} + +void FtpSocket::resetCommandClass(ResetCode code) +{ + timeoutWait(false); + + if (m_transferSocket && code != Ok) { + // Invalidate the socket + closeDataTransferSocket(); + + // Close the file that failed transfer + if (getTransferFile()->isOpen()) { + getTransferFile()->close(); + + if (getCurrentCommand() == Commands::CmdGet && getTransferFile()->size() == 0) + getTransferFile()->remove(); + } + } + + if (m_serverSocket && code != Ok) + delete m_serverSocket; + + Socket::resetCommandClass(code); +} + +// ******************************************************************************************* +// ***************************************** CONNECT ***************************************** +// ******************************************************************************************* + +class FtpCommandConnect : public Commands::Base { +public: + enum State { + None, + SentAuthTls, + SentUser, + SentPass, + SentPbsz, + SentProt, + DoingSyst, + DoingFeat, + SentPwd + }; + + ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandConnect, FtpSocket, CmdNone) + + void process() + { + switch (currentState) { + case None: { + if (!socket()->isMultiline()) { + if (socket()->isResponse("2")) { + // Negotiate a SSL connection if configured + if (socket()->getConfigInt("ssl.use_tls")) { + currentState = SentAuthTls; + socket()->sendCommand("AUTH TLS"); + } else { + // Send username + currentState = SentUser; + socket()->sendCommand("USER " + socket()->getCurrentUrl().user()); + } + } else { + socket()->emitEvent(Event::EventMessage, i18n("Connection has failed.")); + + socket()->protoAbort(); + socket()->emitError(ConnectFailed); + } + } + break; + } + case SentAuthTls: { + if (socket()->isResponse("2")) { + socket()->m_controlSsl = new Ssl(socket()); + + // Setup client certificate if one was provided + if (socket()->m_clientCert) + socket()->m_controlSsl->setClientCertificate(socket()->m_clientCert); + + if (socket()->m_controlSsl->connect()) { + socket()->emitEvent(Event::EventMessage, i18n("SSL negotiation successful. Connection is secured with %1 bit cipher %2.").arg(socket()->m_controlSsl->connectionInfo().getCipherUsedBits()).arg(socket()->m_controlSsl->connectionInfo().getCipher())); + socket()->setConfig("ssl", 1); + + // Now send the username + currentState = SentUser; + socket()->sendCommand("USER " + socket()->getCurrentUrl().user()); + } else { + delete socket()->m_controlSsl; + socket()->m_controlSsl = 0; + + socket()->emitEvent(Event::EventMessage, i18n("SSL negotiation failed. Login aborted.")); + socket()->resetCommandClass(Failed); + + socket()->protoAbort(); + } + } else { + socket()->emitEvent(Event::EventMessage, i18n("SSL negotiation request failed. Login aborted.")); + socket()->resetCommandClass(Failed); + + socket()->protoAbort(); + } + break; + } + case SentUser: { + if (socket()->isResponse("331")) { + // Send password + if (socket()->isResponse("331 Response to otp-") || + socket()->isResponse("331 Response to s/key")) { + // OTP: 331 Response to otp-md5 41 or4828 ext required for foo. + QString tmp = socket()->getResponse(); + tmp = tmp.section(' ', 3, 5); + + KFTPOTPGenerator otp(tmp, socket()->getCurrentUrl().pass()); + currentState = SentPass; + socket()->sendCommand("PASS " + otp.generateOTP()); + } else { + socket()->sendCommand("PASS " + socket()->getCurrentUrl().pass()); + currentState = SentPass; + } + } else if (socket()->isResponse("230")) { + // Some servers imediately send the 230 response for anonymous accounts + if (!socket()->isMultiline()) { + if (socket()->getConfigInt("ssl")) { + currentState = SentPbsz; + socket()->sendCommand("PBSZ 0"); + } else { + // Do SYST + socket()->sendCommand("SYST"); + currentState = DoingSyst; + } + } + } else { + socket()->emitEvent(Event::EventMessage, i18n("Login has failed.")); + + socket()->protoAbort(); + socket()->emitError(LoginFailed); + } + break; + } + case SentPass: { + if (socket()->isResponse("230")) { + if (!socket()->isMultiline()) { + if (socket()->getConfigInt("ssl")) { + currentState = SentPbsz; + socket()->sendCommand("PBSZ 0"); + } else { + // Do SYST + socket()->sendCommand("SYST"); + currentState = DoingSyst; + } + } + } else { + socket()->emitEvent(Event::EventMessage, i18n("Login has failed.")); + + socket()->protoAbort(); + socket()->emitError(LoginFailed); + } + break; + } + case SentPbsz: { + currentState = SentProt; + QString prot = "PROT "; + + if (socket()->getConfigInt("ssl.prot_mode") == 0) + prot.append('P'); + else + prot.append('C'); + + socket()->sendCommand(prot); + break; + } + case SentProt: { + if (socket()->isResponse("5")) { + // Fallback to unencrypted data channel + socket()->setConfig("ssl.prot_mode", 2); + } + + currentState = DoingSyst; + socket()->sendCommand("SYST"); + break; + } + case DoingSyst: { + socket()->sendCommand("FEAT"); + currentState = DoingFeat; + break; + } + case DoingFeat: { + if (socket()->isMultiline()) { + parseFeat(); + } else { + socket()->sendCommand("PWD"); + currentState = SentPwd; + } + break; + } + case SentPwd: { + // Parse the current working directory + if (socket()->isResponse("2")) { + // 257 "/home/default/path" + QString tmp = socket()->getResponse(); + int first = tmp.find('"') + 1; + tmp = tmp.mid(first, tmp.findRev('"') - first); + + socket()->setDefaultDirectory(tmp); + socket()->setCurrentDirectory(tmp); + } + + // Enable transmission of keepalive events + socket()->keepaliveStart(); + + currentState = None; + socket()->emitEvent(Event::EventMessage, i18n("Connected.")); + socket()->emitEvent(Event::EventConnect); + socket()->m_login = true; + socket()->resetCommandClass(); + break; + } + } + } + + void parseFeat() + { + QString feat = socket()->getResponse().stripWhiteSpace().upper(); + + if (feat.left(3).toInt() > 0 && feat[3] == '-') + feat.remove(0, 4); + + if (feat.left(4) == "MDTM") { + // Server has MDTM (MoDification TiMe) support + socket()->setConfig("feat.mdtm", 1); + } else if (feat.left(4) == "PRET") { + // Server is a distributed ftp server and requires PRET for transfers + socket()->setConfig("feat.pret", 1); + } else if (feat.left(4) == "MLSD") { + // Server supports machine-friendly directory listings + socket()->setConfig("feat.mlsd", 1); + } else if (feat.left(4) == "REST") { + // Server supports resume operations + socket()->setConfig("feat.rest", 1); + } else if (feat.left(4) == "SSCN") { + // Server supports SSCN for secure site-to-site transfers + socket()->setConfig("feat.sscn", 1); + socket()->setConfig("feat.cpsv", 0); + } else if (feat.left(4) == "CPSV" && !socket()->getConfigInt("feat.sscn")) { + // Server supports CPSV for secure site-to-site transfers + socket()->setConfig("feat.cpsv", 1); + } + } +}; + +void FtpSocket::protoConnect(const KURL &url) +{ + emitEvent(Event::EventState, i18n("Connecting...")); + emitEvent(Event::EventMessage, i18n("Connecting to %1:%2...").arg(url.host()).arg(url.port())); + + if (!getConfig("encoding").isEmpty()) + changeEncoding(getConfig("encoding")); + + // Start the connect procedure + m_controlConnecting = true; + setCurrentUrl(url); + KNetwork::KStreamSocket::connect(url.host(), QString::number(url.port())); +} + +void FtpSocket::slotConnected() +{ + if (getConfigInt("ssl.use_implicit")) { + m_controlSsl = new Ssl(this); + + // Setup client certificate if one was provided + if (m_clientCert) + m_controlSsl->setClientCertificate(m_clientCert); + + if (m_controlSsl->connect()) { + emitEvent(Event::EventMessage, i18n("SSL negotiation successful. Connection is secured with %1 bit cipher %2.").arg(m_controlSsl->connectionInfo().getCipherUsedBits()).arg(m_controlSsl->connectionInfo().getCipher())); + setConfig("ssl", 1); + } else { + delete m_controlSsl; + m_controlSsl = 0; + + emitEvent(Event::EventMessage, i18n("SSL negotiation failed. Connect aborted.")); + resetCommandClass(Failed); + + protoAbort(); + } + } + + timeoutWait(true); + + emitEvent(Event::EventState, i18n("Logging in...")); + emitEvent(Event::EventMessage, i18n("Connected with server, waiting for welcome message...")); + setupCommandClass(FtpCommandConnect); +} + +void FtpSocket::slotError() +{ + if (isFatalError(error())) { + emitEvent(Event::EventMessage, i18n("Failed to connect (%1)").arg(errorString(error()))); + emitError(ConnectFailed); + + resetCommandClass(FailedSilently); + } +} + +// ******************************************************************************************* +// **************************************** DISCONNECT *************************************** +// ******************************************************************************************* + +void FtpSocket::protoDisconnect() +{ + Socket::protoDisconnect(); + + // Close SSL + if (getConfigInt("ssl") && m_controlSsl) { + m_controlSsl->close(); + delete m_controlSsl; + m_controlSsl = 0; + + if (m_clientCert) { + delete m_clientCert; + m_clientCert = 0; + } + } + + // Terminate the connection + m_login = false; + KNetwork::KStreamSocket::close(); +} + +void FtpSocket::protoAbort() +{ + Socket::protoAbort(); + + if (getCurrentCommand() != Commands::CmdNone) { + // Abort current command + if (getCurrentCommand() == Commands::CmdConnect) + protoDisconnect(); + + if (m_cmdData) + resetCommandClass(UserAbort); + + emitEvent(Event::EventMessage, i18n("Aborted.")); + } +} + +// ******************************************************************************************* +// ********************************* NEGOTIATE DATA CONNECTION ******************************* +// ******************************************************************************************* + +class FtpCommandNegotiateData : public Commands::Base { +public: + enum State { + None, + SentSscnOff, + SentType, + SentProt, + SentPret, + NegotiateActive, + NegotiatePasv, + NegotiateEpsv, + HaveConnection, + SentRest, + SentDataCmd, + WaitTransfer + }; + + ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandNegotiateData, FtpSocket, CmdNone) + + void process() + { + switch (currentState) { + case None: { + if (socket()->getConfigInt("sscn.activated")) { + // First disable SSCN + currentState = SentSscnOff; + socket()->sendCommand("SSCN OFF"); + return; + } + } + case SentSscnOff: { + if (currentState == SentSscnOff) + socket()->setConfig("sscn.activated", 0); + + // Change type + currentState = SentType; + socket()->resetTransferStart(); + + QString type = "TYPE "; + type.append(socket()->getConfigInt("params.data_type")); + socket()->sendCommand(type); + break; + } + case SentType: { + if (socket()->getConfigInt("ssl") && socket()->getConfigInt("ssl.prot_mode") == 1) { + currentState = SentProt; + + if (socket()->getPreviousCommand() == Commands::CmdList) + socket()->sendCommand("PROT P"); + else + socket()->sendCommand("PROT C"); + } else if (socket()->getConfigInt("feat.pret")) { + currentState = SentPret; + socket()->sendCommand("PRET " + socket()->getConfig("params.data_command")); + } else { + negotiateDataConnection(); + } + break; + } + case SentProt: { + if (socket()->getConfigInt("feat.pret")) { + currentState = SentPret; + socket()->sendCommand("PRET " + socket()->getConfig("params.data_command")); + } else { + negotiateDataConnection(); + } + break; + } + case SentPret: { + // PRET failed because of filesystem problems, abort right away! + if (socket()->isResponse("530")) { + socket()->emitError(PermissionDenied); + socket()->resetCommandClass(Failed); + return; + } else if (socket()->isResponse("550")) { + socket()->emitError(FileNotFound); + socket()->resetCommandClass(Failed); + return; + } else if (socket()->isResponse("5")) { + // PRET is not supported, disable for future use + socket()->setConfig("feat.pret", 0); + } + + negotiateDataConnection(); + break; + } + case NegotiateActive: negotiateActive(); break; + case NegotiateEpsv: negotiateEpsv(); break; + case NegotiatePasv: negotiatePasv(); break; + case HaveConnection: { + // We have the connection + if (socket()->getConfigInt("params.data_rest_do")) { + currentState = SentRest; + socket()->sendCommand("REST " + QString::number(socket()->getConfigFs("params.data_rest"))); + } else { + currentState = SentDataCmd; + socket()->sendCommand(socket()->getConfig("params.data_command")); + } + break; + } + case SentRest: { + if (!socket()->isResponse("2") && !socket()->isResponse("3")) { + socket()->setConfig("feat.rest", 0); + socket()->getTransferFile()->close(); + + bool ok; + + if (socket()->getPreviousCommand() == Commands::CmdGet) + ok = socket()->getTransferFile()->open(IO_WriteOnly | IO_Truncate); + else + ok = socket()->getTransferFile()->open(IO_ReadOnly); + + // Check if there was a problem opening the file + if (!ok) { + socket()->emitError(FileOpenFailed); + socket()->resetCommandClass(Failed); + return; + } + } + + // We have sent REST, now send the data command + currentState = SentDataCmd; + socket()->sendCommand(socket()->getConfig("params.data_command")); + break; + } + case SentDataCmd: { + if (!socket()->isResponse("1")) { + // Some problems while executing the data command + socket()->resetCommandClass(Failed); + return; + } + + if (!socket()->isMultiline()) { + socket()->checkTransferStart(); + currentState = WaitTransfer; + } + break; + } + case WaitTransfer: { + if (!socket()->isResponse("2")) { + // Transfer has failed + socket()->resetCommandClass(Failed); + return; + } + + if (!socket()->isMultiline()) { + // Transfer has been completed + socket()->checkTransferEnd(); + } + break; + } + } + } + + void negotiateDataConnection() + { + if (socket()->getConfigInt("feat.epsv")) { + negotiateEpsv(); + } else if (socket()->getConfigInt("feat.pasv")) { + negotiatePasv(); + } else { + negotiateActive(); + } + } + + void negotiateEpsv() + { + if (currentState == NegotiateEpsv) { + if (!socket()->isResponse("2")) { + // Negotiation failed + socket()->setConfig("feat.epsv", "0"); + + // Try the next thing + negotiateDataConnection(); + return; + } + + // 229 Entering Extended Passive Mode (|||55016|) + char *begin = strchr(socket()->getResponse().ascii(), '('); + int port; + + if (!begin || sscanf(begin, "(|||%d|)", &port) != 1) { + // Unable to parse, try the next thing + socket()->setConfig("feat.epsv", "0"); + negotiateDataConnection(); + return; + } + + // We have the address, let's setup the transfer socket and then + // we are done. + currentState = HaveConnection; + socket()->setupPassiveTransferSocket(QString::null, port); + } else { + // Just send the EPSV command + currentState = NegotiateEpsv; + socket()->sendCommand("EPSV"); + } + } + + void negotiatePasv() + { + if (currentState == NegotiatePasv) { + if (!socket()->isResponse("2")) { + // Negotiation failed + socket()->setConfig("feat.pasv", "0"); + + // Try the next thing + negotiateDataConnection(); + return; + } + + // Ok PASV command successfull - let's parse the result + int ip[6]; + char *begin = strchr(socket()->getResponse().ascii(), '('); + + // Some stinky servers don't respect RFC and do it on their own + if (!begin) + begin = strchr(socket()->getResponse().ascii(), '='); + + if (!begin || (sscanf(begin, "(%d,%d,%d,%d,%d,%d)",&ip[0], &ip[1], &ip[2], &ip[3], &ip[4], &ip[5]) != 6 && + sscanf(begin, "=%d,%d,%d,%d,%d,%d",&ip[0], &ip[1], &ip[2], &ip[3], &ip[4], &ip[5]) != 6)) { + // Unable to parse, try the next thing + socket()->setConfig("feat.pasv", "0"); + negotiateDataConnection(); + return; + } + + // Convert to string + QString host; + int port; + + host.sprintf("%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); + port = ip[4] << 8 | ip[5]; + + // If the reported IP address is from a private IP range, this might be because the + // remote server is not properly configured. So we just use the server's real IP instead + // of the one we got (if the host is really local, then this should work as well). + if (!socket()->getConfigInt("feat.pret")) { + if (host.startsWith("192.168.") || host.startsWith("10.") || host.startsWith("172.16.")) + host = socket()->peerAddress().nodeName(); + } + + // We have the address, let's setup the transfer socket and then + // we are done. + currentState = HaveConnection; + socket()->setupPassiveTransferSocket(host, port); + } else { + // Just send the PASV command + currentState = NegotiatePasv; + socket()->sendCommand("PASV"); + } + } + + void negotiateActive() + { + if (currentState == NegotiateActive) { + if (!socket()->isResponse("2")) { + if (socket()->getConfigInt("feat.eprt")) { + socket()->setConfig("feat.eprt", 0); + } else { + // Negotiation failed, reset since active is the last fallback + socket()->resetCommandClass(Failed); + return; + } + } else { + currentState = HaveConnection; + socket()->nextCommandAsync(); + return; + } + } + + // Setup the socket and set the apropriate port command + currentState = NegotiateActive; + + KNetwork::KSocketAddress address = socket()->setupActiveTransferSocket(); + if (address.address()) { + if (socket()->getConfigInt("feat.eprt")) { + QString ianaFamily = QString::number(address.ianaFamily()); + + socket()->sendCommand("EPRT |" + ianaFamily + "|" + address.nodeName() + "|" + address.serviceName() + "|"); + } else if (address.ianaFamily() == 1) { + QString format = address.nodeName().replace(".", ","); + + format.append(","); + format.append(QString::number((unsigned char) address.address()->sa_data[0])); + format.append(","); + format.append(QString::number((unsigned char) address.address()->sa_data[1])); + + socket()->sendCommand("PORT " + format); + } else { + socket()->emitEvent(Event::EventMessage, i18n("Incompatible address family for PORT, but EPRT not supported, aborting!")); + socket()->resetCommandClass(Failed); + } + } + } +}; + +void FtpSocket::initializeTransferSocket() +{ + m_transferConnecting = true; + m_transferEnd = 0; + m_transferBytes = 0; + m_transferBufferSize = 4096; + m_transferBuffer = (char*) malloc(m_transferBufferSize); + + m_speedLastTime = time(0); + m_speedLastBytes = 0; + + // Setup the speed limiter + switch (getPreviousCommand()) { + case Commands::CmdGet: SpeedLimiter::self()->append(this, SpeedLimiter::Download); break; + case Commands::CmdPut: SpeedLimiter::self()->append(this, SpeedLimiter::Upload); break; + default: break; + } + + m_transferSocket->enableRead(false); + m_transferSocket->setBlocking(false); + m_transferSocket->setAddressReuseable(true); +} + +void FtpSocket::setupPassiveTransferSocket(const QString &host, int port) +{ + // Use the host from control connection if empty + QString realHost = host; + if (host.isEmpty() || getConfigInt("pasv.use_site_ip")) + realHost = peerAddress().nodeName(); + + // Let's connect + emitEvent(Event::EventMessage, i18n("Establishing data connection with %1:%2...").arg(realHost).arg(port)); + + if (!m_transferSocket) + m_transferSocket = new KNetwork::KStreamSocket(); + + initializeTransferSocket(); + m_transferSocket->connect(realHost, QString::number(port)); +} + +KNetwork::KSocketAddress FtpSocket::setupActiveTransferSocket() +{ + if (!m_serverSocket) + m_serverSocket = new KNetwork::KServerSocket(); + + m_serverSocket->setAcceptBuffered(false); + m_serverSocket->setFamily(KNetwork::KResolver::InetFamily); + + if (KFTPCore::Config::activeForcePort()) { + // Bind only to ports in a specified portrange + bool found = false; + unsigned int max = KFTPCore::Config::activeMaxPort(); + unsigned int min = KFTPCore::Config::activeMinPort(); + + for (unsigned int port = min + rand() % (max - min + 1); port <= max; port++) { + m_serverSocket->setAddress(QString::number(port)); + bool success = m_serverSocket->listen(); + + if (found = (success && m_serverSocket->error() == KSocketBase::NoError)) + break; + + m_serverSocket->close(); + } + + if (!found) { + emitEvent(Event::EventMessage, i18n("Unable to establish a listening socket.")); + resetCommandClass(Failed); + return KNetwork::KSocketAddress(); + } + } else { + m_serverSocket->setAddress("0"); + + if (!m_serverSocket->listen()) { + emitEvent(Event::EventMessage, i18n("Unable to establish a listening socket.")); + resetCommandClass(Failed); + return KNetwork::KSocketAddress(); + } + } + + KNetwork::KSocketAddress serverAddr = m_serverSocket->localAddress(); + KNetwork::KSocketAddress controlAddr = localAddress(); + KNetwork::KSocketAddress request; + + if (KFTPCore::Config::portForceIp() && !getConfigInt("active.no_force_ip")) { + QString remoteIp = peerAddress().nodeName(); + + if (KFTPCore::Config::ignoreExternalIpForLan() && + (remoteIp.startsWith("192.168.") || remoteIp.startsWith("10.") || remoteIp.startsWith("172.16."))) { + request = controlAddr; + } else { + // Force a specified IP/hostname to be used in PORT + KNetwork::KResolverResults resolverResults; + + resolverResults = KNetwork::KResolver::resolve(KFTPCore::Config::portIp(), "21"); + if (resolverResults.error() < 0) { + // Well, we are unable to resolve the name, so we should use what we got + // from control socket + request = controlAddr; + } else { + // The name has been resolved and we have the address, so we should + // use it + request = resolverResults[0].address(); + } + } + } else { + // Just use our IP we bound to when connecting to the remote server + request = controlAddr; + } + + // Set the proper port + request.address()->sa_data[0] = serverAddr.address()->sa_data[0]; + request.address()->sa_data[1] = serverAddr.address()->sa_data[1]; + + emitEvent(Event::EventMessage, i18n("Waiting for data connection on port %1...").arg(serverAddr.serviceName())); + + return request; +} + +void FtpSocket::slotDataAccept(KNetwork::KStreamSocket *socket) +{ + m_transferSocket = socket; + initializeTransferSocket(); + + // Socket has been accepted so the server is not needed anymore + delete m_serverSocket; + + emitEvent(Event::EventMessage, i18n("Data connection established.")); + checkTransferStart(); +} + +void FtpSocket::closeDataTransferSocket() +{ + if (m_dataSsl) { + m_dataSsl->close(); + delete m_dataSsl; + m_dataSsl = 0; + } + + // Free the buffer and invalidate the socket + free(m_transferBuffer); + + m_transferSocket->close(); + delete m_transferSocket; + m_transferBytes = 0; + + SpeedLimiter::self()->remove(this); +} + +void FtpSocket::transferCompleted() +{ + // Transfer has been completed, cleanup + closeDataTransferSocket(); + checkTransferEnd(); +} + +void FtpSocket::checkTransferStart() +{ + if (++m_transferStart >= 2) { + // Setup SSL data connection + if (getConfigInt("ssl") && (getConfigInt("ssl.prot_mode") == 0 || + (getConfigInt("ssl.prot_mode") == 1 && getToplevelCommand() == Commands::CmdList)) && !m_dataSsl) { + m_dataSsl = new Ssl(m_transferSocket); + + if (m_dataSsl->connect()) { + emitEvent(Event::EventMessage, i18n("Data channel secured with %1 bit SSL.").arg(m_dataSsl->connectionInfo().getCipherUsedBits())); + } else { + emitEvent(Event::EventMessage, i18n("SSL negotiation for the data channel has failed. Aborting transfer.")); + resetCommandClass(Failed); + return; + } + } + } +} + +void FtpSocket::checkTransferEnd() +{ + if (++m_transferEnd >= 2) { + emitEvent(Event::EventMessage, i18n("Transfer completed.")); + resetCommandClass(); + } +} + +void FtpSocket::slotDataConnected() +{ + emitEvent(Event::EventMessage, i18n("Data connection established.")); + + checkTransferStart(); + nextCommand(); +} + +void FtpSocket::variableBufferUpdate(Q_LONG size) +{ + if (size > m_transferBufferSize - 64) { + if (m_transferBufferSize + 512 <= 32768) { + m_transferBufferSize += 512; + m_transferBuffer = (char*) realloc(m_transferBuffer, m_transferBufferSize); + } + } else if (size < m_transferBufferSize - 65) { + if (m_transferBufferSize - 512 >= 4096) { + m_transferBufferSize -= 512; + m_transferBuffer = (char*) realloc(m_transferBuffer, m_transferBufferSize); + } + } +} + +void FtpSocket::slotDataTryWrite() +{ + bool updateVariableBuffer = true; + + // Enforce speed limits + if (allowedBytes() > -1) { + m_transferBufferSize = allowedBytes(); + + if (m_transferBufferSize > 32768) + m_transferBufferSize = 32768; + else if (m_transferBufferSize == 0) + return; + + m_transferBuffer = (char*) realloc(m_transferBuffer, m_transferBufferSize); + updateVariableBuffer = false; + } else if (m_transferBufferSize == 0) { + m_transferBufferSize = 4096; + m_transferBuffer = (char*) realloc(m_transferBuffer, m_transferBufferSize); + } + + if (!getTransferFile()->isOpen()) + return; + + // If there is nothing to upload, just close the connection right away + if (getTransferFile()->size() == 0) { + transferCompleted(); + return; + } + + QFile::Offset tmpOffset = getTransferFile()->at(); + Q_LONG readSize = getTransferFile()->readBlock(m_transferBuffer, m_transferBufferSize); + + Q_LONG size = 0; + + if (m_dataSsl) + size = m_dataSsl->write(m_transferBuffer, readSize); + else + size = m_transferSocket->writeBlock(m_transferBuffer, readSize); + + if (size < 0) { + getTransferFile()->at(tmpOffset); + return; + } else if (size < readSize) + getTransferFile()->at(tmpOffset + size); + + m_transferBytes += size; + updateUsage(size); + timeoutPing(); + + if (getTransferFile()->atEnd()) { + // We have reached the end of file, so we should terminate the connection + transferCompleted(); + return; + } + + if (updateVariableBuffer) + variableBufferUpdate(size); +} + +void FtpSocket::slotDataTryRead() +{ + bool updateVariableBuffer = true; + + // Enforce speed limits + if (allowedBytes() > -1) { + m_transferBufferSize = allowedBytes(); + + if (m_transferBufferSize > 32768) + m_transferBufferSize = 32768; + else if (m_transferBufferSize == 0) + return; + + m_transferBuffer = (char*) realloc(m_transferBuffer, m_transferBufferSize); + updateVariableBuffer = false; + } else if (m_transferBufferSize == 0) { + m_transferBufferSize = 4096; + m_transferBuffer = (char*) realloc(m_transferBuffer, m_transferBufferSize); + } + + Q_LONG size = 0; + + if (m_dataSsl) { + size = m_dataSsl->read(m_transferBuffer, m_transferBufferSize); + + if (size == -1) { + transferCompleted(); + return; + } + } else { + size = m_transferSocket->readBlock(m_transferBuffer, m_transferBufferSize); + + // Check if the connection has been closed + if (m_transferSocket->error() != NoError) { + if (m_transferSocket->error() != WouldBlock) { + transferCompleted(); + return; + } + } + } + + if (size <= 0) { + if (!m_dataSsl) + transferCompleted(); + + return; + } + + updateUsage(size); + timeoutPing(); + + switch (getPreviousCommand()) { + case Commands::CmdList: { + // Feed the data to the directory listing parser + if (m_directoryParser) + m_directoryParser->addData(m_transferBuffer, size); + break; + } + case Commands::CmdGet: { + // Write to file + getTransferFile()->writeBlock(m_transferBuffer, size); + m_transferBytes += size; + break; + } + default: { + qDebug("WARNING: slotDataReadActivity called for an invalid command!"); + return; + } + } + + if (updateVariableBuffer) + variableBufferUpdate(size); +} + +// ******************************************************************************************* +// ******************************************* LIST ****************************************** +// ******************************************************************************************* + +class FtpCommandList : public Commands::Base { +public: + enum State { + None, + SentCwd, + SentStat, + WaitList + }; + + ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandList, FtpSocket, CmdList) + + QString path; + + void process() + { + switch (currentState) { + case None: { + path = socket()->getConfig("params.list.path"); + + if (socket()->isChained()) + socket()->m_lastDirectoryListing = DirectoryListing(); + + // Change working directory + currentState = SentCwd; + socket()->changeWorkingDirectory(path); + break; + } + case SentCwd: { + if (!socket()->getConfigInt("status.cwd")) { + // Change directory has failed and we should be silent (=error reporting is off) + socket()->resetCommandClass(); + return; + } + + // Check the directory listing cache + DirectoryListing cached = Cache::self()->findCached(socket(), socket()->getCurrentDirectory()); + if (cached.isValid()) { + socket()->emitEvent(Event::EventMessage, i18n("Using cached directory listing.")); + + if (socket()->isChained()) { + // We don't emit an event, because this list has been called from another + // command. Just save the listing. + socket()->m_lastDirectoryListing = cached; + } else + socket()->emitEvent(Event::EventDirectoryListing, cached); + + socket()->resetCommandClass(); + return; + } + + socket()->m_directoryParser = new FtpDirectoryParser(socket()); + + // Support for faster stat directory listings over the control connection + if (socket()->getConfigInt("stat_listings")) { + currentState = SentStat; + socket()->sendCommand("STAT ."); + return; + } + + // First we have to initialize the data connection, another class will + // do this for us, so we just add it to the command chain + socket()->setConfig("params.data_rest_do", 0); + socket()->setConfig("params.data_type", 'A'); + + if (socket()->getConfigInt("feat.mlsd")) + socket()->setConfig("params.data_command", "MLSD"); + else + socket()->setConfig("params.data_command", "LIST -a"); + + currentState = WaitList; + chainCommandClass(FtpCommandNegotiateData); + break; + } + case SentStat: { + if (!socket()->isResponse("2")) { + // The server doesn't support STAT, disable it and fallback + socket()->setConfig("stat_listings", 0); + + socket()->setConfig("params.data_rest_do", 0); + socket()->setConfig("params.data_type", 'A'); + + if (socket()->getConfigInt("feat.mlsd")) + socket()->setConfig("params.data_command", "MLSD"); + else + socket()->setConfig("params.data_command", "LIST -a"); + + currentState = WaitList; + chainCommandClass(FtpCommandNegotiateData); + return; + } else if (socket()->isMultiline()) { + // Some servers put the response code into the multiline reply + QString response = socket()->getResponse(); + if (response.left(3) == "211") + response = response.mid(4); + + socket()->m_directoryParser->addDataLine(response); + return; + } + + // If we are done, just go on and emit the listing + } + case WaitList: { + // List has been received + if (socket()->isChained()) { + // We don't emit an event, because this list has been called from another + // command. Just save the listing. + socket()->m_lastDirectoryListing = socket()->m_directoryParser->getListing(); + } else + socket()->emitEvent(Event::EventDirectoryListing, socket()->m_directoryParser->getListing()); + + // Cache the directory listing + Cache::self()->addDirectory(socket(), socket()->m_directoryParser->getListing()); + + delete socket()->m_directoryParser; + socket()->m_directoryParser = 0; + + socket()->resetCommandClass(); + break; + } + } + } +}; + +void FtpSocket::protoList(const KURL &path) +{ + emitEvent(Event::EventState, i18n("Fetching directory listing...")); + emitEvent(Event::EventMessage, i18n("Fetching directory listing...")); + + // Set the directory that should be listed + setConfig("params.list.path", path.path()); + + activateCommandClass(FtpCommandList); +} + +// ******************************************************************************************* +// ******************************************* GET ******************************************* +// ******************************************************************************************* + +class FtpCommandGet : public Commands::Base { +public: + enum State { + None, + SentCwd, + SentMdtm, + StatDone, + DestChecked, + WaitTransfer + }; + + ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandGet, FtpSocket, CmdGet) + + KURL sourceFile; + KURL destinationFile; + time_t modificationTime; + + void process() + { + switch (currentState) { + case None: { + modificationTime = 0; + sourceFile.setPath(socket()->getConfig("params.get.source")); + destinationFile.setPath(socket()->getConfig("params.get.destination")); + + // Attempt to CWD to the parent directory + currentState = SentCwd; + socket()->changeWorkingDirectory(sourceFile.directory()); + break; + } + case SentCwd: { + // Send MDTM + if (socket()->getConfigInt("feat.mdtm")) { + currentState = SentMdtm; + socket()->sendCommand("MDTM " + sourceFile.path()); + break; + } else { + // Don't break so we will get on to checking for file existance + } + } + case SentMdtm: { + if (currentState == SentMdtm) { + if (socket()->isResponse("550")) { + // The file probably doesn't exist, just ignore it + } else if (!socket()->isResponse("213")) { + socket()->setConfig("feat.mdtm", 0); + } else { + // Parse MDTM response + struct tm dt = {0,0,0,0,0,0,0,0,0,0,0}; + QString tmp(socket()->getResponse()); + + tmp.remove(0, 4); + dt.tm_year = tmp.left(4).toInt() - 1900; + dt.tm_mon = tmp.mid(4, 2).toInt() - 1; + dt.tm_mday = tmp.mid(6, 2).toInt(); + dt.tm_hour = tmp.mid(8, 2).toInt(); + dt.tm_min = tmp.mid(10, 2).toInt(); + dt.tm_sec = tmp.mid(12, 2).toInt(); + modificationTime = mktime(&dt); + } + } + + // Check if the local file exists and stat the remote file if so + if (QDir::root().exists(destinationFile.path())) { + socket()->protoStat(sourceFile); + currentState = StatDone; + return; + } else { + KStandardDirs::makeDir(destinationFile.directory()); + + // Don't break so we will get on to initiating the data connection + } + } + case StatDone: { + if (currentState == StatDone) { + DirectoryListing list; + list.addEntry(socket()->getStatResponse()); + + currentState = DestChecked; + socket()->emitEvent(Event::EventFileExists, list); + return; + } + } + case DestChecked: { + socket()->setConfig("params.data_rest_do", 0); + + if (isWakeup()) { + // We have been waken up because a decision has been made + FileExistsWakeupEvent *event = static_cast<FileExistsWakeupEvent*>(m_wakeupEvent); + + if (!socket()->getConfigInt("feat.rest") && event->action == FileExistsWakeupEvent::Resume) + event->action = FileExistsWakeupEvent::Overwrite; + + switch (event->action) { + case FileExistsWakeupEvent::Rename: { + // Change the destination filename, otherwise it is the same as overwrite + destinationFile.setPath(event->newFileName); + } + case FileExistsWakeupEvent::Overwrite: { + socket()->getTransferFile()->setName(destinationFile.path()); + socket()->getTransferFile()->open(IO_WriteOnly | IO_Truncate); + + if (socket()->getConfigInt("feat.rest")) { + socket()->setConfig("params.data_rest_do", 1); + socket()->setConfig("params.data_rest", 0); + } + break; + } + case FileExistsWakeupEvent::Resume: { + socket()->getTransferFile()->setName(destinationFile.path()); + socket()->getTransferFile()->open(IO_WriteOnly | IO_Append); + + // Signal resume + socket()->emitEvent(Event::EventResumeOffset, socket()->getTransferFile()->size()); + + socket()->setConfig("params.data_rest_do", 1); + socket()->setConfig("params.data_rest", (filesize_t) socket()->getTransferFile()->size()); + break; + } + case FileExistsWakeupEvent::Skip: { + // Transfer should be aborted + socket()->emitEvent(Event::EventTransferComplete); + socket()->resetCommandClass(); + return; + } + } + } else { + // The file doesn't exist so we are free to overwrite + socket()->getTransferFile()->setName(destinationFile.path()); + socket()->getTransferFile()->open(IO_WriteOnly | IO_Truncate); + } + + // Check if there was a problem opening the file + if (!socket()->getTransferFile()->isOpen()) { + socket()->emitError(FileOpenFailed); + socket()->resetCommandClass(Failed); + return; + } + + // First we have to initialize the data connection, another class will + // do this for us, so we just add it to the command chain + socket()->setConfig("params.data_type", KFTPCore::Config::self()->ftpMode(sourceFile.path())); + socket()->setConfig("params.data_command", "RETR " + sourceFile.filename()); + + currentState = WaitTransfer; + chainCommandClass(FtpCommandNegotiateData); + break; + } + case WaitTransfer: { + // Transfer has been completed + socket()->getTransferFile()->close(); + + if (modificationTime != 0) { + // Use the modification time we got from MDTM + utimbuf tmp; + tmp.actime = time(0); + tmp.modtime = modificationTime; + utime(destinationFile.path().latin1(), &tmp); + } + + socket()->emitEvent(Event::EventTransferComplete); + socket()->emitEvent(Event::EventReloadNeeded); + socket()->resetCommandClass(); + break; + } + } + } +}; + +void FtpSocket::protoGet(const KURL &source, const KURL &destination) +{ + emitEvent(Event::EventState, i18n("Transfering...")); + emitEvent(Event::EventMessage, i18n("Downloading file '%1'...").arg(source.fileName())); + + // Set the source and destination + setConfig("params.get.source", source.path()); + setConfig("params.get.destination", destination.path()); + + activateCommandClass(FtpCommandGet); +} + +// ******************************************************************************************* +// ******************************************* CWD ******************************************* +// ******************************************************************************************* + +class FtpCommandCwd : public Commands::Base { +public: + enum State { + None, + SentCwd, + SentPwd, + SentMkd, + SentCwdEnd + }; + + ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandCwd, FtpSocket, CmdNone) + + QString targetDirectory; + QString currentPathPart; + QString cached; + int currentPart; + int numParts; + bool shouldCreate; + + void process() + { + switch (currentState) { + case None: { + targetDirectory = socket()->getConfig("params.cwd.path"); + socket()->setConfig("status.cwd", 1); + + // If we are already there, no need to CWD + if (socket()->getCurrentDirectory() == targetDirectory) { + socket()->resetCommandClass(); + return; + } + + cached = Cache::self()->findCachedPath(socket(), targetDirectory); + if (!cached.isEmpty()) { + if (socket()->getCurrentDirectory() == cached) { + // We are already there + socket()->resetCommandClass(); + return; + } + } + + // First check the toplevel directory and if it exists we are done + currentState = SentCwd; + currentPart = 0; + numParts = targetDirectory.contains('/'); + shouldCreate = socket()->getConfigInt("params.cwd.create"); + + socket()->sendCommand("CWD " + targetDirectory); + break; + } + case SentCwd: { + if (socket()->isMultiline()) + return; + + if (socket()->isResponse("250") && currentPart == 0) { + if (!cached.isEmpty()) { + socket()->setCurrentDirectory(cached); + socket()->resetCommandClass(); + } else { + // Directory exists, check where we are + currentState = SentPwd; + socket()->sendCommand("PWD"); + } + } else { + // Changing the working directory has failed + if (shouldCreate) { + currentPathPart = targetDirectory.section('/', 0, ++currentPart); + currentState = SentMkd; + socket()->sendCommand("MKD " + currentPathPart); + } else if (socket()->errorReporting()) { + socket()->emitError(socket()->getPreviousCommand() == Commands::CmdList ? ListFailed : FileNotFound); + socket()->resetCommandClass(Failed); + } else { + socket()->setConfig("status.cwd", 0); + socket()->resetCommandClass(); + } + } + break; + } + case SentPwd: { + // Parse the current working directory + if (socket()->isResponse("2")) { + QString tmp = socket()->getResponse(); + int first = tmp.find('"') + 1; + tmp = tmp.mid(first, tmp.findRev('"') - first); + + // Set the current directory and cache it + socket()->setCurrentDirectory(tmp); + Cache::self()->addPath(socket(), tmp); + + socket()->resetCommandClass(); + } else if (socket()->errorReporting()) { + socket()->emitError(socket()->getPreviousCommand() == Commands::CmdList ? ListFailed : FileNotFound); + socket()->resetCommandClass(Failed); + } else { + socket()->setConfig("status.cwd", 0); + socket()->resetCommandClass(); + } + break; + } + case SentMkd: { + // Invalidate parent cache + if (socket()->isResponse("2")) { + Cache::self()->invalidateEntry(socket(), KURL(currentPathPart).directory()); + } + + if (currentPart == numParts) { + // We are done, since all directories have been created + currentState = SentCwdEnd; + socket()->sendCommand("CWD " + targetDirectory); + } else { + currentPathPart = targetDirectory.section('/', 0, ++currentPart); + currentState = SentMkd; + socket()->sendCommand("MKD " + currentPathPart); + } + break; + } + case SentCwdEnd: { + if (socket()->isMultiline()) + return; + + // See where we are and set current working directory + currentState = SentPwd; + socket()->sendCommand("PWD"); + break; + } + } + } +}; + +void FtpSocket::changeWorkingDirectory(const QString &path, bool shouldCreate) +{ + // Set the path to cwd to + setConfig("params.cwd.path", path); + setConfig("params.cwd.create", shouldCreate); + + activateCommandClass(FtpCommandCwd); +} + +// ******************************************************************************************* +// ******************************************* PUT ******************************************* +// ******************************************************************************************* + +class FtpCommandPut : public Commands::Base { +public: + enum State { + None, + WaitCwd, + SentSize, + StatDone, + DestChecked, + WaitTransfer + }; + + ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandPut, FtpSocket, CmdPut) + + KURL sourceFile; + KURL destinationFile; + + bool fetchedSize; + filesize_t destinationSize; + + void cleanup() + { + // Unclean upload termination, be sure to erase the cached stat infos + Cache::self()->invalidateEntry(socket(), destinationFile.directory()); + } + + void process() + { + switch (currentState) { + case None: { + sourceFile.setPath(socket()->getConfig("params.get.source")); + destinationFile.setPath(socket()->getConfig("params.get.destination")); + fetchedSize = false; + + // Check if the local file exists + if (!QDir::root().exists(sourceFile.path())) { + socket()->emitError(FileNotFound); + socket()->resetCommandClass(Failed); + return; + } + + // Change to the current working directory, creating any directories that are + // still missing + currentState = WaitCwd; + socket()->changeWorkingDirectory(destinationFile.directory(), true); + break; + } + case WaitCwd: { + // Check if the remote file exists + if (socket()->getConfigInt("feat.size")) { + currentState = SentSize; + socket()->sendCommand("SIZE " + destinationFile.path()); + } else { + // SIZE is not available, try stat directly + currentState = StatDone; + socket()->protoStat(destinationFile); + } + break; + } + case SentSize: { + if (socket()->isResponse("213")) { + destinationSize = socket()->getResponse().mid(4).toULongLong(); + fetchedSize = true; + + // File exists, we have to stat to get more data + currentState = StatDone; + socket()->protoStat(destinationFile); + } else if (socket()->isResponse("500") || socket()->getResponse().contains("Operation not permitted", false)) { + // Yes, some servers don't support the SIZE command :/ + socket()->setConfig("feat.size", 0); + + currentState = StatDone; + socket()->protoStat(destinationFile); + } else { + currentState = DestChecked; + process(); + } + break; + } + case StatDone: { + if (!socket()->getStatResponse().filename().isEmpty()) { + if (fetchedSize) { + if (socket()->getStatResponse().size() != destinationSize) { + // It would seem that the size has changed, cached data is invalid + Cache::self()->invalidateEntry(socket(), destinationFile.directory()); + + currentState = StatDone; + socket()->protoStat(destinationFile); + return; + } + } + + // Remote file exists, emit a request for action + DirectoryListing list; + list.addEntry(socket()->getStatResponse()); + + currentState = DestChecked; + socket()->emitEvent(Event::EventFileExists, list); + return; + } + + // Don't break here + } + case DestChecked: { + socket()->setConfig("params.data_rest_do", 0); + + if (isWakeup()) { + // We have been waken up because a decision has been made + FileExistsWakeupEvent *event = static_cast<FileExistsWakeupEvent*>(m_wakeupEvent); + + if (!socket()->getConfigInt("feat.rest") && event->action == FileExistsWakeupEvent::Resume) + event->action = FileExistsWakeupEvent::Overwrite; + + switch (event->action) { + case FileExistsWakeupEvent::Rename: { + // Change the destination filename, otherwise it is the same as overwrite + destinationFile.setPath(event->newFileName); + } + case FileExistsWakeupEvent::Overwrite: { + socket()->getTransferFile()->setName(sourceFile.path()); + socket()->getTransferFile()->open(IO_ReadOnly); + + if (socket()->getConfigInt("feat.rest")) { + socket()->setConfig("params.data_rest_do", 1); + socket()->setConfig("params.data_rest", 0); + } + break; + } + case FileExistsWakeupEvent::Resume: { + socket()->getTransferFile()->setName(sourceFile.path()); + socket()->getTransferFile()->open(IO_ReadOnly); + socket()->getTransferFile()->at(socket()->getStatResponse().size()); + + // Signal resume + socket()->emitEvent(Event::EventResumeOffset, socket()->getStatResponse().size()); + + socket()->setConfig("params.data_rest_do", 1); + socket()->setConfig("params.data_rest", (filesize_t) socket()->getStatResponse().size()); + break; + } + case FileExistsWakeupEvent::Skip: { + // Transfer should be aborted + markClean(); + + socket()->resetCommandClass(UserAbort); + socket()->emitEvent(Event::EventTransferComplete); + return; + } + } + } else { + // The file doesn't exist so we are free to overwrite + socket()->getTransferFile()->setName(sourceFile.path()); + socket()->getTransferFile()->open(IO_ReadOnly); + } + + // Check if there was a problem opening the file + if (!socket()->getTransferFile()->isOpen()) { + socket()->emitError(FileOpenFailed); + socket()->resetCommandClass(Failed); + return; + } + + // First we have to initialize the data connection, another class will + // do this for us, so we just add it to the command chain + socket()->setConfig("params.data_type", KFTPCore::Config::self()->ftpMode(destinationFile.path())); + socket()->setConfig("params.data_command", "STOR " + destinationFile.filename()); + + currentState = WaitTransfer; + chainCommandClass(FtpCommandNegotiateData); + break; + } + case WaitTransfer: { + // Transfer has been completed + Cache::self()->updateDirectoryEntry(socket(), destinationFile, socket()->getTransferFile()->size()); + socket()->getTransferFile()->close(); + markClean(); + + socket()->emitEvent(Event::EventTransferComplete); + socket()->emitEvent(Event::EventReloadNeeded); + socket()->resetCommandClass(); + break; + } + } + } +}; + +void FtpSocket::protoPut(const KURL &source, const KURL &destination) +{ + emitEvent(Event::EventState, i18n("Transfering...")); + emitEvent(Event::EventMessage, i18n("Uploading file '%1'...").arg(source.fileName())); + + // Set the source and destination + setConfig("params.get.source", source.path()); + setConfig("params.get.destination", destination.path()); + + activateCommandClass(FtpCommandPut); +} + +// ******************************************************************************************* +// **************************************** REMOVE ******************************************* +// ******************************************************************************************* + +class FtpCommandRemove : public Commands::Base { +public: + enum State { + None, + SentCwd, + SentRemove + }; + + ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandRemove, FtpSocket, CmdNone) + + QString destinationPath; + QString parentDirectory; + + void process() + { + switch (currentState) { + case None: { + destinationPath = socket()->getConfig("params.remove.path"); + parentDirectory = socket()->getConfig("params.remove.parent"); + + currentState = SentRemove; + + if (socket()->getConfigInt("params.remove.directory")) { + if (socket()->getCurrentDirectory() != parentDirectory) { + // We should change working directory to parent directory before removing + currentState = SentCwd; + socket()->sendCommand("CWD " + parentDirectory); + } else { + socket()->sendCommand("RMD " + destinationPath); + } + } else { + socket()->sendCommand("DELE " + destinationPath); + } + break; + } + case SentCwd: { + if (socket()->isMultiline()) + return; + + if (socket()->isResponse("2")) { + // CWD was successful + socket()->setCurrentDirectory(parentDirectory); + } + + currentState = SentRemove; + socket()->sendCommand("RMD " + destinationPath); + break; + } + case SentRemove: { + if (socket()->isMultiline()) + return; + + if (!socket()->isResponse("2")) { + socket()->resetCommandClass(Failed); + } else { + // Invalidate cached parent entry (if any) + Cache::self()->invalidateEntry(socket(), parentDirectory); + Cache::self()->invalidatePath(socket(), destinationPath); + + if (!socket()->isChained()) + socket()->emitEvent(Event::EventReloadNeeded); + socket()->resetCommandClass(); + } + break; + } + } + } +}; + +void FtpSocket::protoRemove(const KURL &path) +{ + emitEvent(Event::EventState, i18n("Removing...")); + + // Set the file to remove + setConfig("params.remove.parent", path.directory()); + setConfig("params.remove.path", path.path()); + + activateCommandClass(FtpCommandRemove); +} + +// ******************************************************************************************* +// **************************************** RENAME ******************************************* +// ******************************************************************************************* + +class FtpCommandRename : public Commands::Base { +public: + enum State { + None, + SentRnfr, + SentRnto + }; + + ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandRename, FtpSocket, CmdRename) + + QString sourcePath; + QString destinationPath; + + void process() + { + switch (currentState) { + case None: { + sourcePath = socket()->getConfig("params.rename.source"); + destinationPath = socket()->getConfig("params.rename.destination"); + + currentState = SentRnfr; + socket()->sendCommand("RNFR " + sourcePath); + break; + } + case SentRnfr: { + if (socket()->isResponse("3")) { + currentState = SentRnto; + socket()->sendCommand("RNTO " + destinationPath); + } else + socket()->resetCommandClass(Failed); + break; + } + case SentRnto: { + if (socket()->isResponse("2")) { + // Invalidate cached parent entry (if any) + Cache::self()->invalidateEntry(socket(), KURL(sourcePath).directory()); + Cache::self()->invalidateEntry(socket(), KURL(destinationPath).directory()); + + Cache::self()->invalidatePath(socket(), sourcePath); + Cache::self()->invalidatePath(socket(), destinationPath); + + socket()->emitEvent(Event::EventReloadNeeded); + socket()->resetCommandClass(); + } else + socket()->resetCommandClass(Failed); + break; + } + } + } +}; + +void FtpSocket::protoRename(const KURL &source, const KURL &destination) +{ + emitEvent(Event::EventState, i18n("Renaming...")); + + // Set rename options + setConfig("params.rename.source", source.path()); + setConfig("params.rename.destination", destination.path()); + + activateCommandClass(FtpCommandRename); +} + +// ******************************************************************************************* +// **************************************** CHMOD ******************************************** +// ******************************************************************************************* + +class FtpCommandChmod : public Commands::Base { +public: + enum State { + None, + SentChmod + }; + + ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandChmod, FtpSocket, CmdChmod) + + void process() + { + switch (currentState) { + case None: { + currentState = SentChmod; + + QString chmod; + chmod.sprintf("SITE CHMOD %.3d %s", socket()->getConfigInt("params.chmod.mode"), + socket()->getConfig("params.chmod.path").ascii()); + socket()->sendCommand(chmod); + break; + } + case SentChmod: { + if (!socket()->isResponse("2")) + socket()->resetCommandClass(Failed); + else { + // Invalidate cached parent entry (if any) + Cache::self()->invalidateEntry(socket(), KURL(socket()->getConfig("params.chmod.path")).directory()); + + socket()->emitEvent(Event::EventReloadNeeded); + socket()->resetCommandClass(); + } + break; + } + } + } +}; + +void FtpSocket::protoChmodSingle(const KURL &path, int mode) +{ + emitEvent(Event::EventState, i18n("Changing mode...")); + + // Set chmod options + setConfig("params.chmod.path", path.path()); + setConfig("params.chmod.mode", mode); + + activateCommandClass(FtpCommandChmod); +} + +// ******************************************************************************************* +// **************************************** MKDIR ******************************************** +// ******************************************************************************************* + +class FtpCommandMkdir : public Commands::Base { +public: + enum State { + None, + SentMkdir + }; + + ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandMkdir, FtpSocket, CmdMkdir) + + void process() + { + switch (currentState) { + case None: { + currentState = SentMkdir; + socket()->changeWorkingDirectory(socket()->getConfig("params.mkdir.path"), true); + break; + } + case SentMkdir: { + // Invalidate cached parent entry (if any) + Cache::self()->invalidateEntry(socket(), KURL(socket()->getCurrentDirectory()).directory()); + + socket()->emitEvent(Event::EventReloadNeeded); + socket()->resetCommandClass(); + break; + } + } + } +}; + +void FtpSocket::protoMkdir(const KURL &path) +{ + emitEvent(Event::EventState, i18n("Making directory...")); + + setConfig("params.mkdir.path", path.path()); + activateCommandClass(FtpCommandMkdir); +} + +// ******************************************************************************************* +// ******************************************* RAW ******************************************* +// ******************************************************************************************* + +class FtpCommandRaw : public Commands::Base { +public: + enum State { + None, + SentRaw + }; + + ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandRaw, FtpSocket, CmdRaw) + + QString response; + + void process() + { + switch (currentState) { + case None: { + currentState = SentRaw; + socket()->sendCommand(socket()->getConfig("params.raw.command")); + break; + } + case SentRaw: { + response.append(socket()->getResponse()); + + if (!socket()->isMultiline()) { + socket()->emitEvent(Event::EventRaw, response); + socket()->resetCommandClass(); + } + break; + } + } + } +}; + +void FtpSocket::protoRaw(const QString &raw) +{ + setConfig("params.raw.command", raw); + activateCommandClass(FtpCommandRaw); +} + +// ******************************************************************************************* +// ******************************************* FXP ******************************************* +// ******************************************************************************************* + +class FtpCommandFxp : public Commands::Base { +public: + enum State { + None, + + // Source socket + SourceSentCwd, + SourceSentStat, + SourceDestVerified, + SourceSentType, + SourceSentSscn, + SourceSentProt, + SourceWaitType, + SourceSentPret, + SourceSentPasv, + SourceDoRest, + SourceSentRest, + SourceDoRetr, + SourceSentRetr, + SourceWaitTransfer, + SourceResetProt, + + // Destination socket + DestSentStat, + DestWaitCwd, + DestDoType, + DestSentType, + DestSentSscn, + DestSentProt, + DestDoPort, + DestSentPort, + DestSentRest, + DestDoStor, + DestSentStor, + DestWaitTransfer, + DestResetProt + }; + + enum ProtectionMode { + ProtClear = 0, + ProtPrivate = 1, + ProtSSCN = 2 + }; + + enum TransferMode { + TransferPASV = 0, + TransferCPSV = 1 + }; + + ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandFxp, FtpSocket, CmdFxp) + + FtpSocket *companion; + + KURL sourceFile; + KURL destinationFile; + filesize_t resumeOffset; + + void cleanup() + { + // We have been interrupted, so we have to abort the companion as well + if (!socket()->getConfigInt("params.fxp.abort")) { + companion->setConfig("params.fxp.abort", 1); + companion->protoAbort(); + } + + // Unclean upload termination, be sure to erase the cached stat infos + if (!socket()->getConfigInt("params.fxp.keep_cache")) + Cache::self()->invalidateEntry(socket(), destinationFile.directory()); + } + + void process() + { + switch (currentState) { + case None: { + sourceFile.setPath(socket()->getConfig("params.fxp.source")); + destinationFile.setPath(socket()->getConfig("params.fxp.destination")); + socket()->setConfig("params.fxp.keep_cache", 0); + + // Who are we ? Where shall we begin ? + if (socket()->getConfigInt("params.fxp.companion")) { + // We are the companion, so we should check the destination + socket()->setConfig("params.fxp.companion", 0); + + currentState = DestSentStat; + socket()->protoStat(destinationFile); + return; + } else { + socket()->setConfig("params.transfer.mode", TransferPASV); + + if (socket()->getCurrentDirectory() != sourceFile.directory()) { + // Attempt to CWD to the parent directory + currentState = SourceSentCwd; + socket()->sendCommand("CWD " + sourceFile.directory()); + return; + } + } + } + + // *************************************************************************** + // ***************************** Source socket ******************************* + // *************************************************************************** + case SourceSentCwd: { + if (currentState == SourceSentCwd) { + if (!socket()->isResponse("250")) { + socket()->emitError(FileNotFound); + socket()->resetCommandClass(Failed); + return; + } + + if (socket()->isMultiline()) + return; + else + socket()->setCurrentDirectory(sourceFile.directory()); + } + + // We are the source socket, let's stat + currentState = SourceSentStat; + socket()->protoStat(sourceFile); + break; + } + case SourceSentStat: { + if (socket()->getStatResponse().filename().isEmpty()) { + socket()->emitError(FileNotFound); + socket()->resetCommandClass(Failed); + } else { + // File exists, invoke the companion + companion->setConfig("params.fxp.companion", 1); + companion->thread()->siteToSite(socket()->thread(), sourceFile, destinationFile); + currentState = SourceDestVerified; + } + break; + } + case SourceDestVerified: { + if (isWakeup()) { + // We have been waken up because a decision has been made + FileExistsWakeupEvent *event = static_cast<FileExistsWakeupEvent*>(m_wakeupEvent); + + if (!socket()->getConfigInt("feat.rest") && event->action == FileExistsWakeupEvent::Resume) + event->action = FileExistsWakeupEvent::Overwrite; + + switch (event->action) { + case FileExistsWakeupEvent::Rename: { + // Change the destination filename, otherwise it is the same as overwrite + destinationFile.setPath(event->newFileName); + } + case FileExistsWakeupEvent::Overwrite: { + companion->setConfig("params.fxp.rest", 0); + resumeOffset = 0; + break; + } + case FileExistsWakeupEvent::Resume: { + companion->setConfig("params.fxp.rest", companion->getStatResponse().size()); + resumeOffset = companion->getStatResponse().size(); + break; + } + case FileExistsWakeupEvent::Skip: { + // Transfer should be aborted + companion->setConfig("params.fxp.keep_cache", 1); + socket()->setConfig("params.fxp.keep_cache", 1); + + socket()->resetCommandClass(UserAbort); + socket()->emitEvent(Event::EventTransferComplete); + return; + } + } + } else { + companion->setConfig("params.fxp.rest", 0); + resumeOffset = 0; + } + + // Change type + currentState = SourceSentType; + + QString type = "TYPE "; + type.append(KFTPCore::Config::self()->ftpMode(sourceFile.path())); + socket()->sendCommand(type); + break; + } + case SourceSentType: { + if (socket()->getConfigInt("ssl") && socket()->getConfigInt("ssl.prot_mode") != 2 && !socket()->getConfigInt("sscn.activated")) { + if (socket()->getConfigInt("ssl.prot_mode") == 0) { + if (socket()->getConfigInt("feat.sscn")) { + // We support SSCN + currentState = SourceSentSscn; + socket()->sendCommand("SSCN ON"); + companion->setConfig("params.ssl.mode", ProtPrivate); + } else if (companion->getConfigInt("feat.sscn")) { + // Companion supports SSCN + currentState = SourceWaitType; + companion->setConfig("params.ssl.mode", ProtSSCN); + companion->nextCommandAsync(); + } else if (socket()->getConfigInt("feat.cpsv")) { + // We support CPSV + currentState = SourceWaitType; + socket()->setConfig("params.transfer.mode", TransferCPSV); + companion->setConfig("params.ssl.mode", ProtPrivate); + companion->nextCommandAsync(); + } else { + // Neither support SSCN, can't do SSL transfer + socket()->emitEvent(Event::EventMessage, i18n("Neither server supports SSCN/CPSV but SSL data connection requested, aborting transfer!")); + socket()->resetCommandClass(Failed); + return; + } + } else { + currentState = SourceSentProt; + socket()->sendCommand("PROT C"); + companion->setConfig("params.ssl.mode", ProtClear); + } + } else { + currentState = SourceWaitType; + companion->nextCommandAsync(); + } + break; + } + case SourceSentSscn: { + if (!socket()->isResponse("2")) { + socket()->resetCommandClass(Failed); + } else { + socket()->setConfig("sscn.activated", 1); + socket()->setConfig("params.fxp.changed_prot", 0); + + currentState = SourceWaitType; + companion->nextCommandAsync(); + } + break; + } + case SourceSentProt: { + if (!socket()->isResponse("2")) { + socket()->resetCommandClass(Failed); + } else { + socket()->setConfig("params.fxp.changed_prot", 1); + + currentState = SourceWaitType; + companion->nextCommandAsync(); + } + break; + } + case SourceWaitType: { + // We are ready to invoke file transfer, do PASV + if (socket()->getConfigInt("feat.pret")) { + currentState = SourceSentPret; + socket()->sendCommand("PRET RETR " + sourceFile.filename()); + } else { + currentState = SourceSentPasv; + + switch (socket()->getConfigInt("params.transfer.mode")) { + case TransferPASV: socket()->sendCommand("PASV"); break; + case TransferCPSV: socket()->sendCommand("CPSV"); break; + } + } + break; + } + case SourceSentPret: { + if (!socket()->isResponse("2")) { + if (socket()->isResponse("550")) { + socket()->emitError(PermissionDenied); + socket()->resetCommandClass(Failed); + return; + } else if (socket()->isResponse("530")) { + socket()->emitError(FileNotFound); + socket()->resetCommandClass(Failed); + } + + socket()->setConfig("feat.pret", 0); + } + + currentState = SourceSentPasv; + + switch (socket()->getConfigInt("params.transfer.mode")) { + case TransferPASV: socket()->sendCommand("PASV"); break; + case TransferCPSV: socket()->sendCommand("CPSV"); break; + } + break; + } + case SourceSentPasv: { + // Parse the PASV response and get it to the companion to issue PORT + if (!socket()->isResponse("2")) { + socket()->resetCommandClass(Failed); + } else { + QString tmp = socket()->getResponse(); + int pos = tmp.find('(') + 1; + tmp = tmp.mid(pos, tmp.find(')') - pos); + + currentState = SourceDoRest; + companion->setConfig("params.fxp.ip", tmp); + companion->nextCommandAsync(); + } + break; + } + case SourceDoRest: { + currentState = SourceSentRest; + socket()->sendCommand("REST " + QString::number(resumeOffset)); + break; + } + case SourceSentRest: { + if (!socket()->isResponse("2") && !socket()->isResponse("3")) { + socket()->setConfig("feat.rest", 0); + companion->setConfig("params.fxp.rest", 0); + } else { + // Signal resume + socket()->emitEvent(Event::EventResumeOffset, resumeOffset); + } + + currentState = SourceDoRetr; + companion->nextCommandAsync(); + break; + } + case SourceDoRetr: { + currentState = SourceSentRetr; + socket()->sendCommand("RETR " + sourceFile.filename()); + break; + } + case SourceSentRetr: { + if (!socket()->isResponse("1")) { + socket()->resetCommandClass(Failed); + } else { + currentState = SourceWaitTransfer; + } + break; + } + case SourceWaitTransfer: { + if (!socket()->isMultiline()) { + // Transfer has been completed + if (socket()->getConfigInt("params.fxp.changed_prot")) { + currentState = SourceResetProt; + + QString prot = "PROT "; + + if (socket()->getConfigInt("ssl.prot_mode") == 0) + prot.append('P'); + else + prot.append('C'); + + socket()->sendCommand(prot); + } else { + markClean(); + + socket()->emitEvent(Event::EventMessage, i18n("Transfer completed.")); + socket()->emitEvent(Event::EventTransferComplete); + socket()->resetCommandClass(); + } + } + break; + } + case SourceResetProt: { + markClean(); + + socket()->emitEvent(Event::EventMessage, i18n("Transfer completed.")); + socket()->emitEvent(Event::EventTransferComplete); + socket()->resetCommandClass(); + break; + } + + // *************************************************************************** + // *************************** Destination socket **************************** + // *************************************************************************** + case DestSentStat: { + if (socket()->getStatResponse().filename().isEmpty()) { + // Change the working directory + currentState = DestWaitCwd; + socket()->changeWorkingDirectory(destinationFile.directory(), true); + } else { + // The file already exists, request action + DirectoryListing list; + list.addEntry(companion->getStatResponse()); + list.addEntry(socket()->getStatResponse()); + + currentState = DestDoType; + socket()->emitEvent(Event::EventFileExists, list); + } + break; + } + case DestWaitCwd: { + // Directory has been changed/created, call back the companion + currentState = DestDoType; + companion->nextCommandAsync(); + break; + } + case DestDoType: { + currentState = DestSentType; + + QString type = "TYPE "; + type.append(KFTPCore::Config::self()->ftpMode(sourceFile.path())); + socket()->sendCommand(type); + break; + } + case DestSentType: { + if (socket()->getConfigInt("ssl")) { + // Check what the source socket has instructed us to do + switch (socket()->getConfigInt("params.ssl.mode")) { + case ProtClear: { + // We should use cleartext data channel + if (socket()->getConfigInt("ssl.prot_mode") != 2) { + currentState = DestSentProt; + socket()->sendCommand("PROT C"); + } else { + currentState = DestDoPort; + companion->nextCommandAsync(); + } + break; + } + case ProtPrivate: { + // We should use private data channel + if (socket()->getConfigInt("ssl.prot_mode") != 0) { + currentState = DestSentProt; + socket()->sendCommand("PROT P"); + } else { + currentState = DestDoPort; + companion->nextCommandAsync(); + } + break; + } + case ProtSSCN: { + // We should initialize SSCN mode + if (!socket()->getConfigInt("sscn.activated")) { + currentState = DestSentSscn; + socket()->sendCommand("SSCN ON"); + } else { + currentState = DestDoPort; + companion->nextCommandAsync(); + } + break; + } + } + } else { + currentState = DestDoPort; + companion->nextCommandAsync(); + } + break; + } + case DestSentSscn: { + if (!socket()->isResponse("2")) { + socket()->resetCommandClass(Failed); + } else { + socket()->setConfig("sscn.activated", 1); + socket()->setConfig("params.fxp.changed_prot", 0); + + currentState = DestDoPort; + companion->nextCommandAsync(); + } + break; + } + case DestSentProt: { + if (!socket()->isResponse("2")) { + socket()->resetCommandClass(Failed); + } else { + socket()->setConfig("params.fxp.changed_prot", 1); + + currentState = DestDoPort; + companion->nextCommandAsync(); + } + break; + } + case DestDoPort: { + currentState = DestSentPort; + socket()->sendCommand("PORT " + socket()->getConfig("params.fxp.ip")); + break; + } + case DestSentPort: { + if (!socket()->isResponse("2")) { + socket()->resetCommandClass(Failed); + } else { + currentState = DestSentRest; + socket()->sendCommand("REST " + socket()->getConfig("params.fxp.rest")); + } + break; + } + case DestSentRest: { + // We are ready for file transfer + currentState = DestDoStor; + companion->nextCommandAsync(); + break; + } + case DestDoStor: { + currentState = DestSentStor; + socket()->sendCommand("STOR " + destinationFile.filename()); + break; + } + case DestSentStor: { + if (!socket()->isResponse("1")) { + socket()->resetCommandClass(Failed); + } else { + currentState = DestWaitTransfer; + companion->nextCommandAsync(); + } + break; + } + case DestWaitTransfer: { + if (!socket()->isMultiline()) { + // Transfer has been completed + if (socket()->getConfigInt("params.fxp.changed_prot")) { + currentState = DestResetProt; + + QString prot = "PROT "; + + if (socket()->getConfigInt("ssl.prot_mode") == 0) + prot.append('P'); + else + prot.append('C'); + + socket()->sendCommand(prot); + } else { + markClean(); + + socket()->emitEvent(Event::EventMessage, i18n("Transfer completed.")); + socket()->emitEvent(Event::EventReloadNeeded); + socket()->resetCommandClass(); + } + } + break; + } + case DestResetProt: { + markClean(); + + socket()->emitEvent(Event::EventMessage, i18n("Transfer completed.")); + socket()->emitEvent(Event::EventReloadNeeded); + socket()->resetCommandClass(); + break; + } + } + } +}; + +void FtpSocket::protoSiteToSite(Socket *socket, const KURL &source, const KURL &destination) +{ + emitEvent(Event::EventState, i18n("Transfering...")); + emitEvent(Event::EventMessage, i18n("Transfering file '%1'...").arg(source.fileName())); + + // Set the source and destination + setConfig("params.fxp.abort", 0); + setConfig("params.fxp.source", source.path()); + setConfig("params.fxp.destination", destination.path()); + + FtpCommandFxp *fxp = new FtpCommandFxp(this); + fxp->companion = static_cast<FtpSocket*>(socket); + m_cmdData = fxp; + m_cmdData->process(); +} + +// ******************************************************************************************* +// ******************************************* NOOP ****************************************** +// ******************************************************************************************* + +class FtpCommandKeepAlive : public Commands::Base { +public: + enum State { + None, + SentNoop + }; + + ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandKeepAlive, FtpSocket, CmdKeepAlive) + + void process() + { + switch (currentState) { + case None: { + currentState = SentNoop; + socket()->sendCommand("NOOP"); + break; + } + case SentNoop: { + socket()->resetCommandClass(); + break; + } + } + } +}; + +void FtpSocket::protoKeepAlive() +{ + emitEvent(Event::EventState, i18n("Transmitting keep-alive...")); + setCurrentCommand(Commands::CmdKeepAlive); + activateCommandClass(FtpCommandKeepAlive); +} + +} diff --git a/kftpgrabber/src/engine/ftpsocket.h b/kftpgrabber/src/engine/ftpsocket.h new file mode 100644 index 0000000..1762bfd --- /dev/null +++ b/kftpgrabber/src/engine/ftpsocket.h @@ -0,0 +1,154 @@ +/* + * This file is part of the KFTPGrabber project + * + * Copyright (C) 2003-2006 by the KFTPGrabber developers + * Copyright (C) 2003-2006 Jernej Kos <kostko@jweb-network.net> + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and + * NON-INFRINGEMENT. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations + * including the two. + * + * You must obey the GNU General Public License in all respects + * for all of the code used other than OpenSSL. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you + * do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source + * files in the program, then also delete it here. + */ + +#ifndef KFTPENGINEFTPSOCKET_H +#define KFTPENGINEFTPSOCKET_H + +#include <kstreamsocket.h> +#include <kserversocket.h> +#include <kssl.h> + +#include <qguardedptr.h> +#include <qfile.h> + +#include "speedlimiter.h" +#include "socket.h" + +namespace KFTPEngine { + +class FtpDirectoryParser; +class Ssl; + +/** + * @author Jernej Kos <kostko@jweb-network.net> + */ +class FtpSocket : public KNetwork::KStreamSocket, public Socket, public SpeedLimiterItem +{ +Q_OBJECT +friend class Commands::Base; +friend class FtpCommandConnect; +friend class FtpCommandNegotiateData; +friend class FtpCommandList; +public: + FtpSocket(Thread *thread); + ~FtpSocket(); + + void protoConnect(const KURL &url); + void protoDisconnect(); + void protoAbort(); + void protoGet(const KURL &source, const KURL &destination); + void protoPut(const KURL &source, const KURL &destination); + void protoRemove(const KURL &path); + void protoRename(const KURL &source, const KURL &destination); + void protoChmodSingle(const KURL &path, int mode); + void protoMkdir(const KURL &path); + void protoList(const KURL &path); + void protoRaw(const QString &raw); + void protoSiteToSite(Socket *socket, const KURL &source, const KURL &destination); + void protoKeepAlive(); + + void changeWorkingDirectory(const QString &path, bool shouldCreate = false); + + void poll(); + + int features() { return SF_FXP_TRANSFER | SF_RAW_COMMAND; } + + bool isConnected() { return m_login; } + bool isEncrypted() { return isConnected() && getConfigInt("ssl"); } + + void setSslClientCertificate(KSSLPKCS12 *certificate) { m_clientCert = certificate; } + + bool isResponse(const QString &code); + QString getResponse() { return m_response; } + bool isMultiline() { return !m_multiLineCode.isEmpty(); } + + void sendCommand(const QString &command); + void resetCommandClass(ResetCode code = Ok); + + void setupPassiveTransferSocket(const QString &host, int port); + KNetwork::KSocketAddress setupActiveTransferSocket(); + + QFile *getTransferFile() { return &m_transferFile; } + + void checkTransferEnd(); + void checkTransferStart(); + void resetTransferStart() { m_transferStart = 0; } +protected: + void processBuffer(); + void parseLine(const QString &line); + void variableBufferUpdate(Q_LONG size); + void closeDataTransferSocket(); + void initializeTransferSocket(); + void transferCompleted(); +private: + bool m_login; + + QString m_buffer; + QString m_multiLineCode; + QString m_response; + + QGuardedPtr<KNetwork::KStreamSocket> m_transferSocket; + QGuardedPtr<KNetwork::KServerSocket> m_serverSocket; + FtpDirectoryParser *m_directoryParser; + + char m_controlBuffer[1024]; + + QFile m_transferFile; + char *m_transferBuffer; + int m_transferBufferSize; + int m_transferStart; + int m_transferEnd; + + bool m_transferConnecting; + bool m_controlConnecting; + + Ssl *m_controlSsl; + Ssl *m_dataSsl; + KSSLPKCS12 *m_clientCert; +protected slots: + void slotConnected(); + void slotControlTryRead(); + void slotError(); + + void slotDataAccept(KNetwork::KStreamSocket *socket); + void slotDataConnected(); + void slotDataTryRead(); + void slotDataTryWrite(); +}; + +} + +#endif diff --git a/kftpgrabber/src/engine/sftpsocket.cpp b/kftpgrabber/src/engine/sftpsocket.cpp new file mode 100644 index 0000000..a683723 --- /dev/null +++ b/kftpgrabber/src/engine/sftpsocket.cpp @@ -0,0 +1,775 @@ +/* + * This file is part of the KFTPGrabber project + * + * Copyright (C) 2003-2006 by the KFTPGrabber developers + * Copyright (C) 2003-2006 Jernej Kos <kostko@jweb-network.net> + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and + * NON-INFRINGEMENT. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations + * including the two. + * + * You must obey the GNU General Public License in all respects + * for all of the code used other than OpenSSL. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you + * do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source + * files in the program, then also delete it here. + */ + +#include "sftpsocket.h" +#include "cache.h" +#include "misc/config.h" + +#include <qdir.h> + +#include <klocale.h> +#include <kstandarddirs.h> +#include <kio/job.h> +#include <kio/renamedlg.h> + +#include <sys/stat.h> +#include <fcntl.h> + +namespace KFTPEngine { + +SftpSocket::SftpSocket(Thread *thread) + : Socket(thread, "sftp"), + m_login(false) +{ +} + +SftpSocket::~SftpSocket() +{ +} + +int addPermInt(int &x, int n, int add) +{ + if (x >= n) { + x -= n; + return add; + } else { + return 0; + } +} + +int SftpSocket::intToPosix(int permissions) +{ + int posix = 0; + QString str = QString::number(permissions); + + int user = str.mid(0, 1).toInt(); + int group = str.mid(1, 1).toInt(); + int other = str.mid(2, 1).toInt(); + + posix |= addPermInt(user, 4, S_IRUSR); + posix |= addPermInt(user, 2, S_IWUSR); + posix |= addPermInt(user, 1, S_IXUSR); + + posix |= addPermInt(group, 4, S_IRGRP); + posix |= addPermInt(group, 2, S_IWGRP); + posix |= addPermInt(group, 1, S_IXGRP); + + posix |= addPermInt(other, 4, S_IROTH); + posix |= addPermInt(other, 2, S_IWOTH); + posix |= addPermInt(other, 1, S_IXOTH); + + return posix; +} + + +// ******************************************************************************************* +// ***************************************** CONNECT ***************************************** +// ******************************************************************************************* + +class SftpCommandConnect : public Commands::Base { +public: + enum State { + None, + ConnectComplete, + LoginComplete + }; + + ENGINE_STANDARD_COMMAND_CONSTRUCTOR(SftpCommandConnect, SftpSocket, CmdConnect) + + void process() + { + KURL url = socket()->getCurrentUrl(); + + switch (currentState) { + case None: { + // Set connection info + SSH_OPTIONS *sshOptions = options_new(); + options_set_username(sshOptions, (char*) url.user().ascii()); + options_set_host(sshOptions, url.host().ascii()); + options_set_port(sshOptions, url.port()); + options_set_timeout(sshOptions, 10, 0); + + socket()->m_sftpSession = 0; + socket()->m_sshSession = ssh_connect(sshOptions); + + if (!socket()->sshSession()) { + socket()->emitEvent(Event::EventMessage, i18n("Unable to establish SSH connection (%1)").arg(ssh_get_error(0))); + socket()->emitError(ConnectFailed); + return; + } + + socket()->emitEvent(Event::EventState, i18n("Logging in...")); + socket()->emitEvent(Event::EventMessage, i18n("Connected with server, attempting to login...")); + + currentState = ConnectComplete; + } + case ConnectComplete: { + SSH_SESSION *sshSession = socket()->sshSession(); + QString password; + + // Check if a public key password was supplied using the wakeup event + if (isWakeup()) { + PubkeyWakeupEvent *event = static_cast<PubkeyWakeupEvent*>(m_wakeupEvent); + password = event->password; + } + + // Try the public key auth with the set password (if any) + int pkey_ret = ssh_userauth_autopubkey(sshSession, (char*) password.ascii()); + if (pkey_ret == -666) { + // Make a password request + socket()->emitEvent(Event::EventPubkeyPassword); + return; + } else if (pkey_ret != SSH_AUTH_SUCCESS) { + // First let's try the keyboard-interactive authentification + if (keyboardInteractiveLogin() != SSH_AUTH_SUCCESS) { + // If this fails, let's try the password authentification + if (ssh_userauth_password(sshSession, NULL, (char*) url.pass().ascii()) != SSH_AUTH_SUCCESS) { + socket()->emitEvent(Event::EventMessage, i18n("Login has failed.")); + socket()->emitError(LoginFailed); + + socket()->protoAbort(); + return; + } + } else { + socket()->emitEvent(Event::EventMessage, i18n("Keyboard-interactive authentication succeeded.")); + } + } else { + socket()->emitEvent(Event::EventMessage, i18n("Public key authentication succeeded.")); + } + + currentState = LoginComplete; + } + case LoginComplete: { + socket()->m_sftpSession = sftp_new(socket()->sshSession()); + + if (!socket()->sftpSession()) { + socket()->emitEvent(Event::EventMessage, i18n("Unable to initialize SFTP channel.")); + socket()->emitError(LoginFailed); + + socket()->protoAbort(); + return; + } + + if (sftp_init(socket()->sftpSession())) { + socket()->emitEvent(Event::EventMessage, i18n("Unable to initialize SFTP.")); + socket()->emitError(LoginFailed); + + socket()->protoAbort(); + return; + } + + // Get the current directory + char *cwd = sftp_canonicalize_path(socket()->sftpSession(), "./"); + socket()->setDefaultDirectory(socket()->remoteEncoding()->decode(cwd)); + socket()->setCurrentDirectory(socket()->remoteEncoding()->decode(cwd)); + delete cwd; + + socket()->emitEvent(Event::EventMessage, i18n("Connected.")); + socket()->emitEvent(Event::EventConnect); + socket()->m_login = true; + + socket()->resetCommandClass(); + break; + } + } + } + + int keyboardInteractiveLogin() + { + int err = ssh_userauth_kbdint(socket()->sshSession(), NULL, NULL); + char *name, *instruction, *prompt; + int i, n; + char echo; + + while (err == SSH_AUTH_INFO) { + name = ssh_userauth_kbdint_getname(socket()->sshSession()); + instruction = ssh_userauth_kbdint_getinstruction(socket()->sshSession()); + n = ssh_userauth_kbdint_getnprompts(socket()->sshSession()); + + // FIXME Name and instruction are currently ignored. The libssh API reference + // suggests displaying an interactive dialog box for the user to supply the + // information requested from the server. + + for(i = 0; i < n; ++i) { + prompt = ssh_userauth_kbdint_getprompt(socket()->sshSession(), i, &echo); + + if (!echo) { + // We should send the password (since only the password should be masked) + ssh_userauth_kbdint_setanswer(socket()->sshSession(), i, (char*) socket()->getCurrentUrl().pass().ascii()); + } else { + // FIXME Server requests something else ? + } + } + + err = ssh_userauth_kbdint(socket()->sshSession(), NULL, NULL); + } + + return err; + } +}; + +void SftpSocket::protoConnect(const KURL &url) +{ + emitEvent(Event::EventState, i18n("Connecting...")); + emitEvent(Event::EventMessage, i18n("Connecting to %1:%2...").arg(url.host()).arg(url.port())); + + if (!getConfig("encoding").isEmpty()) + changeEncoding(getConfig("encoding")); + + // Connect to the remote host + setCurrentUrl(url); + activateCommandClass(SftpCommandConnect); +} + +// ******************************************************************************************* +// **************************************** DISCONNECT *************************************** +// ******************************************************************************************* + +void SftpSocket::protoDisconnect() +{ + Socket::protoDisconnect(); + + if (m_sftpSession) + sftp_free(m_sftpSession); + + ssh_disconnect(m_sshSession); + m_sshSession = 0; + + m_login = false; +} + +void SftpSocket::protoAbort() +{ + Socket::protoAbort(); + + if (getCurrentCommand() == Commands::CmdGet || getCurrentCommand() == Commands::CmdPut) { + // Abort current command + resetCommandClass(UserAbort); + emitEvent(Event::EventMessage, i18n("Aborted.")); + } +} + +// ******************************************************************************************* +// ******************************************* LIST ****************************************** +// ******************************************************************************************* + +class SftpCommandList : public Commands::Base { +public: + enum State { + None + }; + + ENGINE_STANDARD_COMMAND_CONSTRUCTOR(SftpCommandList, SftpSocket, CmdList) + + void process() + { + // Check the directory listing cache + DirectoryListing cached = Cache::self()->findCached(socket(), socket()->getCurrentDirectory()); + if (cached.isValid()) { + socket()->emitEvent(Event::EventMessage, i18n("Using cached directory listing.")); + + if (socket()->isChained()) { + // We don't emit an event, because this list has been called from another + // command. Just save the listing. + socket()->m_lastDirectoryListing = cached; + } else + socket()->emitEvent(Event::EventDirectoryListing, cached); + + socket()->resetCommandClass(); + return; + } + + socket()->m_lastDirectoryListing = DirectoryListing(socket()->getCurrentDirectory()); + + SFTP_DIR *m_dir = sftp_opendir(socket()->sftpSession(), socket()->remoteEncoding()->encode(socket()->getCurrentDirectory()).data()); + if (!m_dir) { + if (socket()->errorReporting()) { + socket()->emitError(ListFailed); + socket()->resetCommandClass(Failed); + } else + socket()->resetCommandClass(); + return; + } + + // Read the specified directory + SFTP_ATTRIBUTES *file; + DirectoryEntry entry; + + while ((file = sftp_readdir(socket()->sftpSession(), m_dir))) { + entry.setFilename(file->name); + + if (entry.filename() != "." && entry.filename() != "..") { + entry.setFilename(socket()->remoteEncoding()->decode(entry.filename().ascii())); + entry.setOwner(file->owner); + entry.setGroup(file->group); + entry.setTime(file->mtime); + entry.setSize(file->size); + entry.setPermissions(file->permissions); + + if (file->permissions & S_IFDIR) + entry.setType('d'); + else + entry.setType('f'); + + socket()->m_lastDirectoryListing.addEntry(entry); + } + + sftp_attributes_free(file); + } + + sftp_dir_close(m_dir); + + // Cache the directory listing + Cache::self()->addDirectory(socket(), socket()->m_lastDirectoryListing); + + if (!socket()->isChained()) + socket()->emitEvent(Event::EventDirectoryListing, socket()->m_lastDirectoryListing); + socket()->resetCommandClass(); + } +}; + +void SftpSocket::protoList(const KURL &path) +{ + emitEvent(Event::EventState, i18n("Fetching directory listing...")); + emitEvent(Event::EventMessage, i18n("Fetching directory listing...")); + + // Set the directory that should be listed + setCurrentDirectory(path.path()); + + activateCommandClass(SftpCommandList); +} + +// ******************************************************************************************* +// ******************************************* GET ******************************************* +// ******************************************************************************************* + +class SftpCommandGet : public Commands::Base { +public: + enum State { + None, + WaitStat, + DestChecked + }; + + ENGINE_STANDARD_COMMAND_CONSTRUCTOR(SftpCommandGet, SftpSocket, CmdGet) + + KURL sourceFile; + KURL destinationFile; + filesize_t resumeOffset; + + void process() + { + switch (currentState) { + case None: { + // Stat source file + resumeOffset = 0; + sourceFile.setPath(socket()->getConfig("params.get.source")); + destinationFile.setPath(socket()->getConfig("params.get.destination")); + + currentState = WaitStat; + socket()->protoStat(sourceFile); + break; + } + case WaitStat: { + socket()->emitEvent(Event::EventState, i18n("Transfering...")); + + if (socket()->getStatResponse().filename().isEmpty()) { + socket()->emitError(FileNotFound); + socket()->resetCommandClass(Failed); + return; + } + + if (QDir::root().exists(destinationFile.path())) { + DirectoryListing list; + list.addEntry(socket()->getStatResponse()); + + currentState = DestChecked; + socket()->emitEvent(Event::EventFileExists, list); + return; + } else + KStandardDirs::makeDir(destinationFile.directory()); + } + case DestChecked: { + QFile file; + + if (isWakeup()) { + // We have been waken up because a decision has been made + FileExistsWakeupEvent *event = static_cast<FileExistsWakeupEvent*>(m_wakeupEvent); + + switch (event->action) { + case FileExistsWakeupEvent::Rename: { + // Change the destination filename, otherwise it is the same as overwrite + destinationFile.setPath(event->newFileName); + } + case FileExistsWakeupEvent::Overwrite: { + file.setName(destinationFile.path()); + file.open(IO_WriteOnly | IO_Truncate); + break; + } + case FileExistsWakeupEvent::Resume: { + file.setName(destinationFile.path()); + file.open(IO_WriteOnly | IO_Append); + + // Signal resume + resumeOffset = file.size(); + socket()->emitEvent(Event::EventResumeOffset, resumeOffset); + break; + } + case FileExistsWakeupEvent::Skip: { + // Transfer should be aborted + socket()->emitEvent(Event::EventTransferComplete); + socket()->resetCommandClass(); + return; + } + } + } else { + // The file doesn't exist so we are free to overwrite + file.setName(destinationFile.path()); + file.open(IO_WriteOnly | IO_Truncate); + } + + // Download the file + SFTP_FILE *rfile = sftp_open(socket()->sftpSession(), socket()->remoteEncoding()->encode(sourceFile.path()).data(), O_RDONLY, 0); + if (!rfile) { + file.close(); + socket()->resetCommandClass(Failed); + return; + } + + if (resumeOffset > 0) + sftp_seek(rfile, resumeOffset); + + char buffer[16384]; + int size; + + do { + size = sftp_read(rfile, buffer, sizeof(buffer)); + + if (size > 0) { + file.writeBlock(buffer, size); + socket()->m_transferBytes += size; + } + + if (socket()->shouldAbort()) + break; + } while (size); + + sftp_file_close(rfile); + file.close(); + + socket()->emitEvent(Event::EventTransferComplete); + socket()->resetCommandClass(); + break; + } + } + } +}; + +void SftpSocket::protoGet(const KURL &source, const KURL &destination) +{ + emitEvent(Event::EventState, i18n("Transfering...")); + emitEvent(Event::EventMessage, i18n("Downloading file '%1'...").arg(source.fileName())); + + // Set the source and destination + setConfig("params.get.source", source.path()); + setConfig("params.get.destination", destination.path()); + + m_transferBytes = 0; + + m_speedLastTime = time(0); + m_speedLastBytes = 0; + + activateCommandClass(SftpCommandGet); +} + +// ******************************************************************************************* +// ******************************************* PUT ******************************************* +// ******************************************************************************************* + +class SftpCommandPut : public Commands::Base { +public: + enum State { + None, + WaitStat, + DestChecked + }; + + ENGINE_STANDARD_COMMAND_CONSTRUCTOR(SftpCommandPut, SftpSocket, CmdPut) + + KURL sourceFile; + KURL destinationFile; + filesize_t resumeOffset; + + void process() + { + switch (currentState) { + case None: { + // Stat source file + resumeOffset = 0; + sourceFile.setPath(socket()->getConfig("params.get.source")); + destinationFile.setPath(socket()->getConfig("params.get.destination")); + + if (!QDir::root().exists(sourceFile.path())) { + socket()->emitError(FileNotFound); + socket()->resetCommandClass(Failed); + return; + } + + currentState = WaitStat; + socket()->protoStat(destinationFile); + break; + } + case WaitStat: { + socket()->emitEvent(Event::EventState, i18n("Transfering...")); + + if (!socket()->getStatResponse().filename().isEmpty()) { + DirectoryListing list; + list.addEntry(socket()->getStatResponse()); + + currentState = DestChecked; + socket()->emitEvent(Event::EventFileExists, list); + return; + } else { + // Create destination directories + socket()->setErrorReporting(false); + + QString destinationDir = destinationFile.directory(); + QString fullPath; + + for (register int i = 1; i <= destinationDir.contains('/'); i++) { + fullPath += "/" + destinationDir.section('/', i, i); + + // Create the directory + socket()->protoMkdir(fullPath); + } + } + } + case DestChecked: { + QFile file; + + if (isWakeup()) { + // We have been waken up because a decision has been made + FileExistsWakeupEvent *event = static_cast<FileExistsWakeupEvent*>(m_wakeupEvent); + + switch (event->action) { + case FileExistsWakeupEvent::Rename: { + // Change the destination filename, otherwise it is the same as overwrite + destinationFile.setPath(event->newFileName); + } + case FileExistsWakeupEvent::Overwrite: { + file.setName(sourceFile.path()); + file.open(IO_ReadOnly); + break; + } + case FileExistsWakeupEvent::Resume: { + resumeOffset = socket()->getStatResponse().size(); + + file.setName(sourceFile.path()); + file.open(IO_ReadOnly); + file.at(resumeOffset); + + // Signal resume + socket()->emitEvent(Event::EventResumeOffset, resumeOffset); + break; + } + case FileExistsWakeupEvent::Skip: { + // Transfer should be aborted + socket()->emitEvent(Event::EventTransferComplete); + socket()->resetCommandClass(); + return; + } + } + } else { + // The file doesn't exist so we are free to overwrite + file.setName(sourceFile.path()); + file.open(IO_ReadOnly); + } + + // Download the file + SFTP_FILE *rfile; + + if (resumeOffset > 0) { + rfile = sftp_open(socket()->sftpSession(), socket()->remoteEncoding()->encode(destinationFile.path()).data(), O_WRONLY | O_APPEND, 0); + sftp_seek(rfile, resumeOffset); + } else + rfile = sftp_open(socket()->sftpSession(), socket()->remoteEncoding()->encode(destinationFile.path()).data(), O_WRONLY | O_CREAT, 0); + + if (!rfile) { + file.close(); + socket()->resetCommandClass(Failed); + return; + } + + char buffer[16384]; + int size; + + do { + size = file.readBlock(buffer, sizeof(buffer)); + + if (size > 0) { + sftp_write(rfile, buffer, size); + socket()->m_transferBytes += size; + } + + if (socket()->shouldAbort()) + break; + } while (size); + + sftp_file_close(rfile); + file.close(); + + socket()->emitEvent(Event::EventTransferComplete); + socket()->resetCommandClass(); + break; + } + } + } +}; + +void SftpSocket::protoPut(const KURL &source, const KURL &destination) +{ + emitEvent(Event::EventState, i18n("Transfering...")); + emitEvent(Event::EventMessage, i18n("Uploading file '%1'...").arg(source.fileName())); + + // Set the source and destination + setConfig("params.get.source", source.path()); + setConfig("params.get.destination", destination.path()); + + m_transferBytes = 0; + + m_speedLastTime = time(0); + m_speedLastBytes = 0; + + activateCommandClass(SftpCommandPut); +} + +// ******************************************************************************************* +// **************************************** REMOVE ******************************************* +// ******************************************************************************************* + +void SftpSocket::protoRemove(const KURL &path) +{ + emitEvent(Event::EventState, i18n("Removing...")); + + // Remove a file or directory + int result = 0; + + if (getConfigInt("params.remove.directory")) + result = sftp_rmdir(m_sftpSession, remoteEncoding()->encode(path.path()).data()); + else + result = sftp_rm(m_sftpSession, remoteEncoding()->encode(path.path()).data()); + + if (result < 0) { + resetCommandClass(Failed); + } else { + // Invalidate cached parent entry (if any) + Cache::self()->invalidateEntry(this, path.directory()); + + emitEvent(Event::EventReloadNeeded); + resetCommandClass(); + } +} + +// ******************************************************************************************* +// **************************************** RENAME ******************************************* +// ******************************************************************************************* + +void SftpSocket::protoRename(const KURL &source, const KURL &destination) +{ + emitEvent(Event::EventState, i18n("Renaming...")); + + if (sftp_rename(m_sftpSession, remoteEncoding()->encode(source.path()).data(), remoteEncoding()->encode(destination.path()).data()) < 0) { + resetCommandClass(Failed); + } else { + // Invalidate cached parent entry (if any) + Cache::self()->invalidateEntry(this, source.directory()); + Cache::self()->invalidateEntry(this, destination.directory()); + + emitEvent(Event::EventReloadNeeded); + resetCommandClass(); + } +} + +// ******************************************************************************************* +// **************************************** CHMOD ******************************************** +// ******************************************************************************************* + +void SftpSocket::protoChmodSingle(const KURL &path, int mode) +{ + emitEvent(Event::EventState, i18n("Changing mode...")); + + SFTP_ATTRIBUTES *attrs = static_cast<SFTP_ATTRIBUTES*>(new SFTP_ATTRIBUTES); + memset(attrs, 0, sizeof(*attrs)); + + attrs->permissions = intToPosix(mode); + attrs->flags = SSH_FILEXFER_ATTR_PERMISSIONS; + + sftp_setstat(m_sftpSession, remoteEncoding()->encode(path.path()).data(), attrs); + sftp_attributes_free(attrs); + + // Invalidate cached parent entry (if any) + Cache::self()->invalidateEntry(this, path.directory()); + + emitEvent(Event::EventReloadNeeded); + resetCommandClass(); +} + +// ******************************************************************************************* +// **************************************** MKDIR ******************************************** +// ******************************************************************************************* + +void SftpSocket::protoMkdir(const KURL &path) +{ + SFTP_ATTRIBUTES *attrs = static_cast<SFTP_ATTRIBUTES*>(new SFTP_ATTRIBUTES); + memset(attrs, 0, sizeof(*attrs)); + + if (sftp_mkdir(m_sftpSession, remoteEncoding()->encode(path.path()).data(), attrs) < 0) { + if (errorReporting()) + resetCommandClass(Failed); + } else { + // Invalidate cached parent entry (if any) + Cache::self()->invalidateEntry(this, path.directory()); + + if (errorReporting()) { + emitEvent(Event::EventReloadNeeded); + resetCommandClass(); + } + } + + delete attrs; +} + +} diff --git a/kftpgrabber/src/engine/sftpsocket.h b/kftpgrabber/src/engine/sftpsocket.h new file mode 100644 index 0000000..f34a896 --- /dev/null +++ b/kftpgrabber/src/engine/sftpsocket.h @@ -0,0 +1,92 @@ +/* + * This file is part of the KFTPGrabber project + * + * Copyright (C) 2003-2006 by the KFTPGrabber developers + * Copyright (C) 2003-2006 Jernej Kos <kostko@jweb-network.net> + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and + * NON-INFRINGEMENT. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations + * including the two. + * + * You must obey the GNU General Public License in all respects + * for all of the code used other than OpenSSL. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you + * do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source + * files in the program, then also delete it here. + */ + +#ifndef KFTPENGINESFTPSOCKET_H +#define KFTPENGINESFTPSOCKET_H + +// LibSSH includes +#include "misc/libs/ssh/libssh.h" +#include "misc/libs/ssh/sftp.h" + +#include "socket.h" + +namespace KFTPEngine { + +/** + * @author Jernej Kos <kostko@jweb-network.net> + */ +class SftpSocket : public Socket { +friend class SftpCommandConnect; +friend class SftpCommandList; +friend class SftpCommandGet; +friend class SftpCommandPut; +public: + SftpSocket(Thread *thread); + ~SftpSocket(); + + void protoConnect(const KURL &url); + void protoDisconnect(); + void protoAbort(); + void protoGet(const KURL &source, const KURL &destination); + void protoPut(const KURL &source, const KURL &destination); + void protoRemove(const KURL &path); + void protoRename(const KURL &source, const KURL &destination); + void protoChmodSingle(const KURL &path, int mode); + void protoMkdir(const KURL &path); + void protoList(const KURL &path); + + void poll() {}; + + int features() { return 0; } + + bool isConnected() { return m_login; } + bool isEncrypted() { return true; } + + SSH_SESSION *sshSession() { return m_sshSession; } + SFTP_SESSION *sftpSession() { return m_sftpSession; } +private: + QString posixToString(int permissions); + int intToPosix(int permissions); +private: + SSH_SESSION *m_sshSession; + SFTP_SESSION *m_sftpSession; + + bool m_login; +}; + +} + +#endif diff --git a/kftpgrabber/src/engine/socket.cpp b/kftpgrabber/src/engine/socket.cpp new file mode 100644 index 0000000..370de1b --- /dev/null +++ b/kftpgrabber/src/engine/socket.cpp @@ -0,0 +1,866 @@ +/* + * This file is part of the KFTPGrabber project + * + * Copyright (C) 2003-2006 by the KFTPGrabber developers + * Copyright (C) 2003-2006 Jernej Kos <kostko@jweb-network.net> + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and + * NON-INFRINGEMENT. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations + * including the two. + * + * You must obey the GNU General Public License in all respects + * for all of the code used other than OpenSSL. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you + * do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source + * files in the program, then also delete it here. + */ +#include "socket.h" +#include "thread.h" +#include "connectionretry.h" +#include "speedlimiter.h" +#include "cache.h" + +#include "misc/config.h" + +#include <klocale.h> + +namespace KFTPEngine { + +Socket::Socket(Thread *thread, const QString &protocol) + : m_remoteEncoding(new KRemoteEncoding()), + m_cmdData(0), + m_thread(thread), + m_transferBytes(0), + m_speedLastTime(0), + m_speedLastBytes(0), + m_protocol(protocol), + m_currentCommand(Commands::CmdNone), + m_errorReporting(true), + m_shouldAbort(false) +{ + m_commandChain.setAutoDelete(true); +} + +Socket::~Socket() +{ + delete m_remoteEncoding; + + if (m_connectionRetry) + delete m_connectionRetry; +} + +void Socket::initConfig() +{ + m_config.clear(); + + // Fill in some default values + setConfig("feat.epsv", 1); + setConfig("feat.eprt", 1); + setConfig("feat.pasv", 1); + setConfig("feat.size", 1); + setConfig("ssl.prot_mode", 2); + setConfig("keepalive.enabled", 1); + setConfig("keepalive.timeout", 60); +} + +void Socket::emitError(ErrorCode code, const QString ¶m1) +{ + // Intercept connect and login errors and pass them on to the ConnectionRetry class (if enabled) + if (getConfigInt("retry") && (code == ConnectFailed || code == LoginFailed)) { + if (!m_connectionRetry) + m_connectionRetry = new ConnectionRetry(this); + + m_connectionRetry->startRetry(); + return; + } + + QValueList<EventParameter> params; + params.append(EventParameter(code)); + params.append(EventParameter(param1)); + + // Dispatch the event via socket thread + m_thread->event(Event::EventError, params); +} + +void Socket::emitEvent(Event::Type type, const QString ¶m1, const QString ¶m2) +{ + QValueList<EventParameter> params; + params.append(EventParameter(param1)); + params.append(EventParameter(param2)); + + // Dispatch the event via socket thread + m_thread->event(type, params); +} + +void Socket::emitEvent(Event::Type type, DirectoryListing param1) +{ + QValueList<EventParameter> params; + params.append(EventParameter(param1)); + + // Dispatch the event via socket thread + m_thread->event(type, params); +} + +void Socket::emitEvent(Event::Type type, filesize_t param1) +{ + QValueList<EventParameter> params; + params.append(EventParameter(param1)); + + // Dispatch the event via socket thread + m_thread->event(type, params); +} + +void Socket::emitEvent(Event::Type type, void *param1) +{ + QValueList<EventParameter> params; + params.append(EventParameter(param1)); + + // Dispatch the event via socket thread + m_thread->event(type, params); +} + +void Socket::changeEncoding(const QString &encoding) +{ + // Alter encoding and change socket config + m_remoteEncoding->setEncoding(encoding.ascii()); + setConfig("encoding", encoding); +} + +void Socket::protoDisconnect() +{ + resetCommandClass(UserAbort); + + emitEvent(Event::EventMessage, i18n("Disconnected.")); + emitEvent(Event::EventDisconnect); +} + +void Socket::timeoutWait(bool start) +{ + if (start) { + m_timeoutCounter.start(); + } else { + m_timeoutCounter = QTime(); + } +} + +void Socket::timeoutPing() +{ + m_timeoutCounter.restart(); +} + +void Socket::timeoutCheck() +{ + if (!isConnected()) + return; + + if (!m_timeoutCounter.isNull()) { + Commands::Type command = getCurrentCommand(); + int timeout = 0; + + // Ignore timeouts for FXP transfers, since there is no way to do pings + if (command == Commands::CmdFxp) + return; + + if (command == Commands::CmdGet || command == Commands::CmdPut) + timeout = KFTPCore::Config::dataTimeout(); + else + timeout = KFTPCore::Config::controlTimeout(); + + if (timeout > 0 && m_timeoutCounter.elapsed() > (timeout * 1000)) { + timeoutWait(false); + + // We have a timeout, let's abort + emitEvent(Event::EventMessage, i18n("Connection timed out.")); + protoDisconnect(); + } + } +} + +void Socket::keepaliveStart() +{ + m_keepaliveCounter.start(); +} + +void Socket::keepaliveCheck() +{ + // Ignore keepalive if the socket is busy + if (isBusy() || !isConnected()) { + m_keepaliveCounter.restart(); + return; + } + + if (getConfigInt("keepalive.enabled") && m_keepaliveCounter.elapsed() > getConfigInt("keepalive.timeout") * 1000) { + protoKeepAlive(); + + // Reset the counter + m_keepaliveCounter.restart(); + } +} + +Commands::Type Socket::getCurrentCommand() +{ + if (m_commandChain.count() > 0) { + QPtrList<Commands::Base>::iterator chainEnd = m_commandChain.end(); + for (QPtrList<Commands::Base>::iterator i = m_commandChain.begin(); i != chainEnd; i++) { + if ((*i)->command() != Commands::CmdNone) + return (*i)->command(); + } + } + + return m_currentCommand; +} + +Commands::Type Socket::getToplevelCommand() +{ + return m_currentCommand; +} + +Commands::Type Socket::getPreviousCommand() +{ + if (!isChained()) + return Commands::CmdNone; + + if (m_commandChain.count() > 1) { + Commands::Base *previous = m_commandChain.prev(); + m_commandChain.next(); + + return previous->command(); + } else { + return m_currentCommand; + } +} + +void Socket::resetCommandClass(ResetCode code) +{ + if (m_commandChain.count() > 0) { + Commands::Base *current = m_commandChain.current(); + + if (current->isProcessing()) { + current->autoDestruct(code); + return; + } else { + if (!current->isClean()) + current->cleanup(); + + m_commandChain.remove(); + } + + if (code == Ok) { + nextCommandAsync(); + } else { + // Command has completed with an error code. We should abort the + // complete chain. + resetCommandClass(code); + } + } else { + if (m_cmdData) { + if (m_cmdData->isProcessing()) { + m_cmdData->autoDestruct(code); + return; + } else { + if (!m_cmdData->isClean()) + m_cmdData->cleanup(); + + delete m_cmdData; + m_cmdData = 0; + } + } + + if (code == Failed) + emitError(OperationFailed); + + // Reset current command and emit a ready event + if (getCurrentCommand() != Commands::CmdConnectRetry) { + setCurrentCommand(Commands::CmdNone); + emitEvent(Event::EventReady); + emitEvent(Event::EventState, i18n("Idle.")); + } + + setErrorReporting(true); + m_shouldAbort = false; + } +} + +void Socket::nextCommand() +{ + if (m_commandChain.count() > 0) { + Commands::Base *current = m_commandChain.current(); + + current->setProcessing(true); + current->process(); + current->setProcessing(false); + + if (current->isDestructable()) + resetCommandClass(current->resetCode()); + } else if (m_cmdData) { + m_cmdData->setProcessing(true); + m_cmdData->process(); + m_cmdData->setProcessing(false); + + if (m_cmdData->isDestructable()) + resetCommandClass(m_cmdData->resetCode()); + } +} + +void Socket::nextCommandAsync() +{ + m_thread->m_commandQueue.append(Commands::CmdNext); +} + +void Socket::wakeup(WakeupEvent *event) +{ + if (m_commandChain.count() > 0) { + Commands::Base *current = m_commandChain.current(); + + if (current->isProcessing()) { + qDebug("WARNING: Attempted to wakeup a processing socket!"); + return; + } + + current->setProcessing(true); + current->wakeup(event); + current->setProcessing(false); + + if (current->isDestructable()) + resetCommandClass(current->resetCode()); + } else if (m_cmdData) { + if (m_cmdData->isProcessing()) { + qDebug("WARNING: Attempted to wakeup a processing socket!"); + return; + } + + m_cmdData->setProcessing(true); + m_cmdData->wakeup(event); + m_cmdData->setProcessing(false); + + if (m_cmdData->isDestructable()) + resetCommandClass(m_cmdData->resetCode()); + } +} + +filesize_t Socket::getTransferSpeed() +{ + time_t timeDelta = time(0) - m_speedLastTime; + + if (timeDelta == 0) + return 0; + + if (m_speedLastBytes > m_transferBytes) + m_speedLastBytes = 0; + + filesize_t speed = (m_transferBytes - m_speedLastBytes)/(time(0) - m_speedLastTime); + + m_speedLastBytes = m_transferBytes; + m_speedLastTime = time(0); + + return speed; +} + +void Socket::protoAbort() +{ + m_shouldAbort = true; + + if (m_connectionRetry && !m_cmdData) + m_connectionRetry->abortRetry(); +} + +// ******************************************************************************************* +// ******************************************* STAT ****************************************** +// ******************************************************************************************* + +class FtpCommandStat : public Commands::Base { +public: + enum State { + None, + WaitList + }; + + ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandStat, Socket, CmdNone) + + KURL path; + + void process() + { + switch (currentState) { + case None: { + // Issue a list of the parent directory + currentState = WaitList; + socket()->setErrorReporting(false); + socket()->protoList(path.directory()); + break; + } + case WaitList: { + // Now just extract what we need + QValueList<DirectoryEntry> list = socket()->getLastDirectoryListing().list(); + QValueList<DirectoryEntry>::iterator listEnd = list.end(); + for (QValueList<DirectoryEntry>::iterator i = list.begin(); i != listEnd; i++) { + if ((*i).filename() == path.fileName()) { + socket()->m_lastStatResponse = *i; + socket()->resetCommandClass(); + return; + } + } + + // We found no such file + socket()->m_lastStatResponse = DirectoryEntry(); + socket()->resetCommandClass(); + break; + } + } + } +}; + +void Socket::protoStat(const KURL &path) +{ + // Lookup the cache first and don't even try to list if cached + DirectoryListing cached = Cache::self()->findCached(this, path.directory()); + if (cached.isValid()) { + QValueList<DirectoryEntry> list = cached.list(); + QValueList<DirectoryEntry>::iterator listEnd = list.end(); + for (QValueList<DirectoryEntry>::iterator i = list.begin(); i != listEnd; i++) { + if ((*i).filename() == path.fileName()) { + m_lastStatResponse = *i; + nextCommandAsync(); + return; + } + } + + // Cached is valid but file can't be found + m_lastStatResponse = DirectoryEntry(); + nextCommandAsync(); + return; + } + + // Not cached, let's do a real listing + FtpCommandStat *stat = new FtpCommandStat(this); + stat->path = path; + addToCommandChain(stat); + nextCommand(); +} + +// ******************************************************************************************* +// ****************************************** SCAN ******************************************* +// ******************************************************************************************* + +class FtpCommandScan : public Commands::Base { +public: + enum State { + None, + SentList, + ProcessList, + ScannedDir + }; + + ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandScan, Socket, CmdNone) + + QValueList<DirectoryEntry> currentList; + QValueList<DirectoryEntry>::const_iterator currentEntry; + + QString currentDirectory; + DirectoryTree *currentTree; + + void cleanup() + { + // We didn't emit the tree, so we should free it + if (!socket()->isChained()) + delete currentTree; + } + + void process() + { + // NOTE: The missing breaks are mising for a purpuse! Do not dare to add them ;) + switch (currentState) { + case None: { + // We would like to disable error reporting + socket()->setErrorReporting(false); + + // Issue a directory listing on the given URL + currentState = SentList; + socket()->protoList(currentDirectory); + break; + } + case SentList: { + currentList = socket()->getLastDirectoryListing().list(); + qHeapSort(currentList); + + currentEntry = currentList.begin(); + currentState = ProcessList; + + // Empty listing, we are done + if (currentEntry == currentList.end()) { + if (socket()->isChained()) { + socket()->resetCommandClass(); + } else { + // We are the toplevel scan command + markClean(); + + socket()->emitEvent(Event::EventScanComplete, currentTree); + socket()->emitEvent(Event::EventMessage, i18n("Scan complete.")); + socket()->resetCommandClass(); + } + + return; + } + } + case ProcessList: { + if ((*currentEntry).isDirectory()) { + // A directory entry + DirectoryTree *tree = currentTree->addDirectory(*currentEntry); + currentState = ScannedDir; + + FtpCommandScan *scan = new FtpCommandScan(socket()); + scan->currentDirectory = currentDirectory + "/" + (*currentEntry).filename(); + scan->currentTree = tree; + socket()->addToCommandChain(scan); + socket()->nextCommandAsync(); + return; + } else { + // A file entry + currentTree->addFile(*currentEntry); + } + } + case ScannedDir: { + currentState = ProcessList; + + if (++currentEntry == currentList.end()) { + // We are done + if (socket()->isChained()) { + socket()->resetCommandClass(); + } else { + // We are the toplevel scan command + markClean(); + + socket()->emitEvent(Event::EventScanComplete, currentTree); + socket()->emitEvent(Event::EventMessage, i18n("Scan complete.")); + socket()->resetCommandClass(); + } + } else { + socket()->nextCommandAsync(); + } + break; + } + } + } +}; + +void Socket::protoScan(const KURL &path) +{ + emitEvent(Event::EventMessage, i18n("Starting recursive directory scan...")); + + // We have to create a new command class manually, since we need to set the + // currentTree parameter + FtpCommandScan *scan = new FtpCommandScan(this); + scan->currentDirectory = path.path(); + scan->currentTree = new DirectoryTree(DirectoryEntry()); + m_cmdData = scan; + m_cmdData->process(); +} + +// ******************************************************************************************* +// ***************************************** DELETE ****************************************** +// ******************************************************************************************* + +class FtpCommandDelete : public Commands::Base { +public: + enum State { + None, + VerifyDir, + SimpleRemove, + SentList, + ProcessList, + DeletedDir, + DeletedFile + }; + + ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandDelete, Socket, CmdDelete) + + QValueList<DirectoryEntry> currentList; + QValueList<DirectoryEntry>::const_iterator currentEntry; + + KURL destinationPath; + + void process() + { + switch (currentState) { + case None: { + // We have to determine if the destination is a file or a directory + // TODO use cached information + if (socket()->isChained()) { + // We know that it is a directory + currentState = SentList; + socket()->protoList(destinationPath); + } else { + currentState = VerifyDir; + socket()->protoStat(destinationPath); + } + break; + } + case VerifyDir: { + DirectoryEntry entry = socket()->getStatResponse(); + + if (entry.filename().isEmpty()) { + // The file doesn't exist, abort + socket()->resetCommandClass(Failed); + } else { + if (entry.isDirectory()) { + // It is a directory, remove recursively + currentState = SentList; + socket()->protoList(destinationPath); + } else { + // A single file, a simple remove + currentState = SimpleRemove; + socket()->setConfig("params.remove.directory", 0); + socket()->protoRemove(destinationPath); + } + } + break; + } + case SimpleRemove: { + if (!socket()->isChained()) + socket()->emitEvent(Event::EventReloadNeeded); + socket()->resetCommandClass(); + break; + } + case SentList: { + currentList = socket()->getLastDirectoryListing().list(); + currentEntry = currentList.begin(); + currentState = ProcessList; + + // Empty listing, we are done + if (currentEntry == currentList.end()) { + if (socket()->isChained()) + socket()->resetCommandClass(); + else { + // We are the top level command class, remove the destination dir + currentState = SimpleRemove; + socket()->setConfig("params.remove.directory", 1); + socket()->protoRemove(destinationPath); + } + + return; + } + } + case ProcessList: { + KURL childPath = destinationPath; + childPath.addPath((*currentEntry).filename()); + + if ((*currentEntry).isDirectory()) { + // A directory, chain another delete command + currentState = DeletedDir; + + // Chain manually, since we need to set some parameters + FtpCommandDelete *del = new FtpCommandDelete(socket()); + del->destinationPath = childPath; + socket()->addToCommandChain(del); + socket()->nextCommand(); + } else { + // A file entry - remove + currentState = DeletedFile; + socket()->setConfig("params.remove.directory", 0); + socket()->protoRemove(childPath); + } + break; + } + case DeletedDir: { + // We have to remove the empty directory + KURL childPath = destinationPath; + childPath.addPath((*currentEntry).filename()); + + currentState = DeletedFile; + socket()->setConfig("params.remove.directory", 1); + socket()->protoRemove(childPath); + break; + } + case DeletedFile: { + currentState = ProcessList; + + if (++currentEntry == currentList.end()) { + if (socket()->isChained()) + socket()->resetCommandClass(); + else { + // We are the top level command class, remove the destination dir + currentState = SimpleRemove; + socket()->setConfig("params.remove.directory", 1); + socket()->protoRemove(destinationPath); + } + } else + socket()->nextCommand(); + break; + } + } + } +}; + +void Socket::protoDelete(const KURL &path) +{ + // We have to create a new command class manually to set some parameter + FtpCommandDelete *del = new FtpCommandDelete(this); + del->destinationPath = path; + m_cmdData = del; + m_cmdData->process(); +} + +// ******************************************************************************************* +// ***************************************** CHMOD ******************************************* +// ******************************************************************************************* + +class FtpCommandRecursiveChmod : public Commands::Base { +public: + enum State { + None, + VerifyDir, + SimpleChmod, + SentList, + ProcessList, + ChmodedDir, + ChmodedFile + }; + + ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandRecursiveChmod, Socket, CmdChmod) + + QValueList<DirectoryEntry> currentList; + QValueList<DirectoryEntry>::const_iterator currentEntry; + + KURL destinationPath; + int mode; + + void process() + { + switch (currentState) { + case None: { + // We have to determine if the destination is a file or a directory + if (socket()->isChained()) { + // We know that it is a directory + currentState = SentList; + socket()->protoList(destinationPath); + } else { + currentState = VerifyDir; + socket()->protoStat(destinationPath); + } + break; + } + case VerifyDir: { + DirectoryEntry entry = socket()->getStatResponse(); + + if (entry.filename().isEmpty()) { + // The file doesn't exist, abort + socket()->resetCommandClass(Failed); + } else { + if (entry.isDirectory()) { + // It is a directory, chmod recursively + currentState = SentList; + socket()->protoList(destinationPath); + } else { + // A single file, a simple chmod + currentState = SimpleChmod; + socket()->protoChmodSingle(destinationPath, mode); + } + } + break; + } + case SimpleChmod: { + socket()->resetCommandClass(); + break; + } + case SentList: { + currentList = socket()->getLastDirectoryListing().list(); + currentEntry = currentList.begin(); + currentState = ProcessList; + + // Empty listing, we are done + if (currentEntry == currentList.end()) { + if (socket()->isChained()) + socket()->resetCommandClass(); + else { + // We are the top level command class, chmod the destination dir + currentState = SimpleChmod; + socket()->protoChmodSingle(destinationPath, mode); + } + + return; + } + } + case ProcessList: { + KURL childPath = destinationPath; + childPath.addPath((*currentEntry).filename()); + + if ((*currentEntry).isDirectory()) { + // A directory, chain another recursive chmod command + currentState = ChmodedDir; + + // Chain manually, since we need to set some parameters + FtpCommandRecursiveChmod *cm = new FtpCommandRecursiveChmod(socket()); + cm->destinationPath = childPath; + cm->mode = mode; + socket()->addToCommandChain(cm); + socket()->nextCommand(); + } else { + // A file entry - remove + currentState = ChmodedFile; + socket()->protoChmodSingle(childPath, mode); + } + break; + } + case ChmodedDir: { + // We have to chmod the directory + KURL childPath = destinationPath; + childPath.addPath((*currentEntry).filename()); + + currentState = ChmodedFile; + socket()->protoChmodSingle(childPath, mode); + break; + } + case ChmodedFile: { + currentState = ProcessList; + + if (++currentEntry == currentList.end()) { + if (socket()->isChained()) + socket()->resetCommandClass(); + else { + // We are the top level command class, chmod the destination dir + currentState = SimpleChmod; + socket()->protoChmodSingle(destinationPath, mode); + } + } else + socket()->nextCommand(); + break; + } + } + } +}; + +void Socket::protoChmod(const KURL &path, int mode, bool recursive) +{ + if (recursive) { + // We have to create a new command class manually to set some parameters + FtpCommandRecursiveChmod *cm = new FtpCommandRecursiveChmod(this); + cm->destinationPath = path; + cm->mode = mode; + m_cmdData = cm; + m_cmdData->process(); + } else { + // No recursive, just chmod a single file + protoChmodSingle(path, mode); + } +} + +} diff --git a/kftpgrabber/src/engine/socket.h b/kftpgrabber/src/engine/socket.h new file mode 100644 index 0000000..3c2296a --- /dev/null +++ b/kftpgrabber/src/engine/socket.h @@ -0,0 +1,605 @@ +/* + * This file is part of the KFTPGrabber project + * + * Copyright (C) 2003-2006 by the KFTPGrabber developers + * Copyright (C) 2003-2006 Jernej Kos <kostko@jweb-network.net> + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and + * NON-INFRINGEMENT. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations + * including the two. + * + * You must obey the GNU General Public License in all respects + * for all of the code used other than OpenSSL. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you + * do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source + * files in the program, then also delete it here. + */ +#ifndef KFTPENGINESOCKET_H +#define KFTPENGINESOCKET_H + +#include <kurl.h> +#include <kremoteencoding.h> + +#include <qptrlist.h> +#include <qguardedptr.h> +#include <qdatetime.h> + +#include "commands.h" + +namespace KFTPEngine { + +class ConnectionRetry; + +enum SocketFeatures { + SF_FXP_TRANSFER = 1, + SF_RAW_COMMAND = 2 +}; + +/** + * The socket class provides an abstract class for all implemented protocols. It + * provides basic methods and also some remote operations (recursive scan, + * recursive removal). + * + * @author Jernej Kos <kostko@jweb-network.net> + */ +class Socket +{ +friend class Thread; +friend class FtpCommandStat; +public: + /** + * Constructs a new socket. + * + * @param thread The thread that created this socket + * @param protocol The protocol name + */ + Socket(Thread *thread, const QString &protocol); + ~Socket(); + + /** + * Set an internal config value. + * + * @param key Key + * @param value Value + */ + void setConfig(const QString &key, const QString &value) { m_config[key] = value; } + + /** + * Set an internal config value. + * + * @param key Key + * @param value Value + */ + void setConfig(const QString &key, int value) { m_config[key] = QString::number(value); } + + /** + * Set an internal config value. + * + * @param key Key + * @param value Value + */ + void setConfig(const QString &key, filesize_t value) { m_config[key] = QString::number(value); } + + /** + * Get an internal config value as string. + * + * @param key Key + * @return The key's value or an empty string if the key doesn't exist + */ + QString getConfig(const QString &key) { return m_config[key]; } + + /** + * Get an internal config value as an integer. + * + * @param key Key + * @return The key's value or 0 if the key doesn't exist + */ + int getConfigInt(const QString &key) { return m_config[key].toInt(); } + + /** + * Get an internal config value as filesize. + * + * @param key Key + * @return The key's value or 0 if the key doesn't exist + */ + filesize_t getConfigFs(const QString &key) { return m_config[key].toULongLong(); } + + /** + * This method should initialize the configuration map. + */ + virtual void initConfig(); + + /** + * This method should trigger the connection process. + * + * @param url Remote url to connect to + */ + virtual void protoConnect(const KURL &url) = 0; + + /** + * This method should disconnect from the remote host. + */ + virtual void protoDisconnect(); + + /** + * This method should abort any ongoing action. + */ + virtual void protoAbort(); + + /** + * This method should download a remote file and save it localy. + * + * @param source The source url + * @param destination The destination url + */ + virtual void protoGet(const KURL &source, const KURL &destination) = 0; + + /** + * This method should upload a local file and save it remotely. + * + * @param source The source url + * @param destination The destination url + */ + virtual void protoPut(const KURL &source, const KURL &destination) = 0; + + /** + * Each protocol should implement this method. It should remove just one + * single entry. A config variable "params.remove.directory" will be set + * to 1 if the entry to remove is a directory and to 0 if it should expect + * a file. + * + * @warning You should NOT use this method directly! Use @ref protoDelete + * instead! + * @param path The path to the entry to remove + */ + virtual void protoRemove(const KURL &path) = 0; + + /** + * This method should rename/move a remote file. + * + * @param source The source file path + * @param destination The destination file path + */ + virtual void protoRename(const KURL &source, const KURL &destination) = 0; + + /** + * This method should change file's mode. + * + * * @warning You should NOT use this method directly! Use @ref protoChmod + * instead! + * @param path The file's path + * @param mode The new file mode + */ + virtual void protoChmodSingle(const KURL &path, int mode) = 0; + + /** + * This method should create a new remote directory. + * + * @param path Path of the newly created remote directory + */ + virtual void protoMkdir(const KURL &path) = 0; + + /** + * This method should fetch the remote directory listing for a specified + * directory. Note that this method could be called as a chained command, + * so it MUST NOT emit an EventDirectoryListing event if isChained returns + * true! In this case it should save the directory listing to the + * m_lastDirectoryListing member variable. + * + * @param path The path to list + */ + virtual void protoList(const KURL &path) = 0; + + /** + * This method should fetch the information about the given path. It is + * usualy called as a chained command. + * + * @param path The path to stat + */ + virtual void protoStat(const KURL &path); + + /** + * This method should send a raw command in case the protocol supports it + * (the SF_RAW_COMMAND is among features). + * + * @param command The command to send + */ + virtual void protoRaw(const QString&) {} + + /** + * This method should initiate a site to site transfer in case the protocol + * supports it (the SF_FXP_TRANSFER is among features). + * + * @param socket The destination socket + * @param source The source url + * @param destination The destination url + */ + virtual void protoSiteToSite(Socket*, const KURL&, const KURL&) {} + + /** + * Send a packet to keep the connection alive. + */ + virtual void protoKeepAlive() {} + + /** + * Recursively scan a directory and emit a DirectoryTree that can be used to + * create new transfers for addition to the queue. + * + * @param path The path to recursively scan + */ + void protoScan(const KURL &path); + + /** + * Identify if the remote path is a file or a directory and recursively remove + * it if so. The difference between this command and @ref protoRemove is, that + * protoRemove removes just one entry, and doesn't identify file type. + * + * @param path The path to remove + */ + void protoDelete(const KURL &path); + + /** + * Change file or directory mode. Also supports recursive mode changes. + * + * @param path The file's path + * @param mode The new file mode + * @param recursive Should the mode be recursively changed + */ + void protoChmod(const KURL &path, int mode, bool recursive); + + /** + * Returns this socket's parent thread. + * + * @return Socket's parent thread + */ + Thread *thread() { return m_thread; } + + /** + * Returns the protocol name of this socket. + * + * @return This socket's protocol name + */ + QString protocolName() { return m_protocol; } + + /** + * This method should return the socket's features by or-ing the values in + * SocketFeatures enum. + * + * @return Socket's features + */ + virtual int features() = 0; + + /** + * This method should return true if this socket is connected. + * + * @return True if the socket has successfully connected + */ + virtual bool isConnected() = 0; + + /** + * This method should return true if the connection is encrypted by some method. + * + * @return True if the connection is encrypted + */ + virtual bool isEncrypted() = 0; + + /** + * Returns true if the socket is currently busy performing an action. + * + * @return True if the socket is busy + */ + virtual bool isBusy() { return m_currentCommand != Commands::CmdNone; } + + /** + * Emit an engine error code. + * + * @param code The error code + * @param param1 Optional string parameter + */ + void emitError(ErrorCode code, const QString ¶m1 = 0); + + /** + * Emit an engine event. + * + * @param type Event type + * @param param1 Optional string parameter + * @param param2 Optional string parameter + */ + void emitEvent(Event::Type type, const QString ¶m1 = 0, const QString ¶m2 = 0); + + /** + * Emit an engine event containing a directory listing. + * + * @param type Event type + * @param param1 The DirectoryListing parameter + */ + void emitEvent(Event::Type type, DirectoryListing param1); + + /** + * Emit an engine event containing a filesize. + * + * @param type Event type + * @param param1 The filesize parameter + */ + void emitEvent(Event::Type type, filesize_t param1); + + /** + * Emit an engine event containing a custom pointer. + * + * @param type Event type + * @param param1 The custom pointer parameter + */ + void emitEvent(Event::Type type, void *param1); + + /** + * This method will set the socket's remote encoding which will be used when + * converting filenames into UTF-8 and back. + * + * @param encoding A valid encoding name + */ + virtual void changeEncoding(const QString &encoding); + + /** + * Retrieve the KRemoteEncoding object for this socket set to the appropriate + * encoding. + * + * @return The KRemoteEncoding object + */ + KRemoteEncoding *remoteEncoding() { return m_remoteEncoding; } + + /** + * Sets the current directory path. + * + * @param path The current directory path + */ + void setCurrentDirectory(const QString &path) { m_currentDirectory = path; } + + /** + * Get the current directory path. + * + * @return The current directory path. + */ + virtual QString getCurrentDirectory() { return m_currentDirectory; } + + /** + * Sets the default directory path (like a remote home directory). + * + * @param path The default directory path + */ + void setDefaultDirectory(const QString &path) { m_defaultDirectory = path; } + + /** + * Get the default directory path. + * + * @return The default directory path + */ + virtual QString getDefaultDirectory() { return m_defaultDirectory; } + + /** + * Sets the url this socket is connected to. + * + * @param url The url this socket is connected to + */ + void setCurrentUrl(const KURL &url) { m_currentUrl = url; } + + /** + * Get the url this socket is connected to. + * + * @return The url this socket is currently connected to + */ + KURL getCurrentUrl() { return m_currentUrl; } + + /** + * Sets the command the socket is currently executing. + * + * @param type Command type + */ + void setCurrentCommand(Commands::Type type) { m_currentCommand = type; } + + /** + * Get the current socket command. + * + * @return The current socket command + */ + Commands::Type getCurrentCommand(); + + /** + * Get the toplevel socket command in the command chain. + * + * @return The toplevel socket command + */ + Commands::Type getToplevelCommand(); + + /** + * Get the command that executed the current command. Note that this + * is valid only if the current command is chained. Otherwise this + * method returns Commands::CmdNone. + * + * @return The previous command + */ + Commands::Type getPreviousCommand(); + + /** + * Get the last directory listing made by protoList. + * + * @return The last directory listing + */ + DirectoryListing getLastDirectoryListing() { return m_lastDirectoryListing; } + + /** + * Get the last stat response made by protoStat. + * + * @return The last stat response + */ + DirectoryEntry getStatResponse() { return m_lastStatResponse; } + + /** + * Get the number of bytes transfered from the beginning of the transfer. + * + * @return The number of bytes transfered + */ + filesize_t getTransferBytes() { return m_transferBytes; } + + /** + * Get the current transfer speed. + * + * @return The current transfer speed. + */ + filesize_t getTransferSpeed(); + + /** + * This method will be called every cycle. It should be usually used to + * poll the data transfer socket. + */ + virtual void poll() = 0; + + /** + * Wakeup the last command processor with a specific wakeup event. This + * is used for async two-way communication between the engine and the + * GUI (wakeup event is a reply from the GUI). + * + * By default this method just passes the event to the currently active + * command processor. + * + * @param event The wakeup event that should be passed to the command class + */ + virtual void wakeup(WakeupEvent *event); + + /** + * Reset the current command class, possibly invoking the calling chained + * command class or completing the operation. + * + * @param code The result code + */ + virtual void resetCommandClass(ResetCode code = Ok); + + /** + * Add a command class to the command chain so that it will be executed next. + * + * @param cmd The command class to add + */ + void addToCommandChain(Commands::Base *cmd) { m_commandChain.append(cmd); } + + /** + * Execute the next command. + */ + void nextCommand(); + + /** + * Schedule the execution of the next command in the next thread loop. + */ + void nextCommandAsync(); + + /** + * Returns true if the current command has been chained from another command class. + * + * @return True if the current command has been chained + */ + bool isChained() { return m_commandChain.count() > 0; } + + /** + * Set the error reporting on or off. This variable is then used by some + * command classes do determine if they should emit errors and reset with + * failure or if they should just silently ignore the error and reset + * the command class with an Ok code. + * + * @param value Error reporting value + */ + void setErrorReporting(bool value) { m_errorReporting = value; } + + /** + * Get the current error reporting setting. This only makes sense if the + * class is chained - otherwise this allways returns true. + * + * @return The current error reporting setting + */ + bool errorReporting() { return m_errorReporting || !isChained(); } + + /** + * Returns true if the current operation should abort. This method should be + * used when the underlying socket implementation is doing blocking operations. + * + * @return True if the operation should be aborted + */ + bool shouldAbort() { return m_shouldAbort; } +protected: + /** + * Call this method when a long wait period has started or ended. If the wait + * isn't nulled before the timeout is reached the current action will be aborted + * and the socket will be disconnected. + * + * @param start True if the wait period should start, false if it should end + */ + void timeoutWait(bool start); + + /** + * Reset the timeout counter. Call this once in a while during long wait periods + * to notify the engine that the socket is still responsive. + */ + void timeoutPing(); + + /** + * Check if we should timeout. This method might cause a disconnect if the timeout + * value is reached. + */ + void timeoutCheck(); + + /** + * Enable the issue of keepalive packets. + */ + void keepaliveStart(); + + /** + * Check if we should transmit a new keepalive packet. + */ + void keepaliveCheck(); +protected: + KRemoteEncoding *m_remoteEncoding; + + Commands::Base *m_cmdData; + QPtrList<Commands::Base> m_commandChain; + + Thread *m_thread; + DirectoryListing m_lastDirectoryListing; + DirectoryEntry m_lastStatResponse; + + filesize_t m_transferBytes; + time_t m_speedLastTime; + filesize_t m_speedLastBytes; + + QTime m_timeoutCounter; + QTime m_keepaliveCounter; +private: + QMap<QString, QString> m_config; + QString m_currentDirectory; + QString m_defaultDirectory; + KURL m_currentUrl; + QString m_protocol; + Commands::Type m_currentCommand; + bool m_errorReporting; + bool m_shouldAbort; + QGuardedPtr<ConnectionRetry> m_connectionRetry; +}; + +} + +#endif diff --git a/kftpgrabber/src/engine/speedlimiter.cpp b/kftpgrabber/src/engine/speedlimiter.cpp new file mode 100644 index 0000000..85f2b72 --- /dev/null +++ b/kftpgrabber/src/engine/speedlimiter.cpp @@ -0,0 +1,240 @@ +/* + * This file is part of the KFTPGrabber project + * + * Copyright (C) 2003-2007 by the KFTPGrabber developers + * Copyright (C) 2003-2007 Jernej Kos <kostko@jweb-network.net> + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and + * NON-INFRINGEMENT. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations + * including the two. + * + * You must obey the GNU General Public License in all respects + * for all of the code used other than OpenSSL. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you + * do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source + * files in the program, then also delete it here. + */ +#include "speedlimiter.h" +#include "misc/config.h" + +#include <kstaticdeleter.h> + +using namespace KFTPCore; + +namespace KFTPEngine { + +static const int tickDelay = 250; +static int bucketSize = 1000 / tickDelay; + +SpeedLimiter *SpeedLimiter::m_self = 0; +static KStaticDeleter<SpeedLimiter> staticSpeedLimiterDeleter; + +SpeedLimiter *SpeedLimiter::self() +{ + if (!m_self) { + staticSpeedLimiterDeleter.setObject(m_self, new SpeedLimiter()); + } + + return m_self; +} + +SpeedLimiter::SpeedLimiter() + : m_timer(false) +{ + // Reset limits and token debts + m_limits[0] = 0; + m_limits[1] = 0; + + m_tokenDebt[0] = 0; + m_tokenDebt[1] = 0; + + // Subscribe to config updates and update the limits + connect(Config::self(), SIGNAL(configChanged()), this, SLOT(updateLimits())); + updateLimits(); +} + +SpeedLimiter::~SpeedLimiter() +{ + if (m_self == this) + staticSpeedLimiterDeleter.setObject(m_self, 0, false); +} + +void SpeedLimiter::updateLimits() +{ + setLimit(SpeedLimiter::Download, Config::downloadSpeedLimit() * 1024); + setLimit(SpeedLimiter::Upload, Config::uploadSpeedLimit() * 1024); +} + +void SpeedLimiter::setLimit(Type type, int limit) +{ + m_limits[type] = limit; +} + +void SpeedLimiter::append(SpeedLimiterItem *item, Type type) +{ + m_objects[type].append(item); + + int limit = m_limits[type]; + if (limit > 0) { + int tokens = limit * tickDelay / 1000; + tokens /= m_objects[type].count(); + + if (m_tokenDebt[type] > 0) { + if (tokens >= m_tokenDebt[type]) { + tokens -= m_tokenDebt[type]; + m_tokenDebt[type] = 0; + } else { + tokens = 0; + } + } + + item->m_availableBytes = tokens; + } else { + item->m_availableBytes = -1; + } + + // Fire the timer if not running + if (!m_timer) { + startTimer(tickDelay); + m_timer = true; + } +} + +void SpeedLimiter::remove(SpeedLimiterItem *item) +{ + remove(item, Download); + remove(item, Upload); +} + +void SpeedLimiter::remove(SpeedLimiterItem *item, Type type) +{ + if (m_objects[type].containsRef(item)) { + int tokens = m_limits[type] * tickDelay / 1000; + tokens /= m_objects[type].count(); + + if (item->m_availableBytes < tokens) + m_tokenDebt[type] += tokens - item->m_availableBytes; + + m_objects[type].removeRef(item); + } + + item->m_availableBytes = -1; +} + +void SpeedLimiter::timerEvent(QTimerEvent*) +{ + QPtrList<SpeedLimiterItem> pendingWakeup; + + for (int i = 0; i < 2; i++) { + m_tokenDebt[i] = 0; + + int limit = m_limits[i]; + if (!limit) { + // There is no limit, reset all items + for (SpeedLimiterItem *item = m_objects[i].first(); item; item = m_objects[i].next()) { + item->m_availableBytes = -1; + } + + continue; + } + + // If there are no objects, just skip it + if (m_objects[i].isEmpty()) + continue; + + int tokens = limit * tickDelay / 1000; + if (!tokens) + tokens = 1; + + int maxTokens = tokens * bucketSize; + + // Get amount of tokens for each object + int tokensPerObject = tokens / m_objects[i].count(); + if (!tokensPerObject) + tokensPerObject = 1; + + tokens = 0; + + QPtrList<SpeedLimiterItem> unsaturatedObjects; + + for (SpeedLimiterItem *item = m_objects[i].first(); item; item = m_objects[i].next()) { + if (item->m_availableBytes == -1) { + item->m_availableBytes = tokensPerObject; + unsaturatedObjects.append(item); + } else { + item->m_availableBytes += tokensPerObject; + + if (item->m_availableBytes > maxTokens) { + tokens += item->m_availableBytes - maxTokens; + item->m_availableBytes = maxTokens; + } else { + unsaturatedObjects.append(item); + } + } + } + + // Assign any left-overs to unsaturated sources + while (tokens && !unsaturatedObjects.isEmpty()) { + tokensPerObject = tokens / unsaturatedObjects.count(); + if (!tokensPerObject) + break; + + tokens = 0; + + for (SpeedLimiterItem *item = unsaturatedObjects.first(); item; item = unsaturatedObjects.next()) { + item->m_availableBytes += tokensPerObject; + + if (item->m_availableBytes > maxTokens) { + tokens += item->m_availableBytes - maxTokens; + item->m_availableBytes = maxTokens; + unsaturatedObjects.removeRef(item); + } + } + } + } + + if (m_objects[0].isEmpty() && m_objects[1].isEmpty()) { + killTimers(); + m_timer = false; + } +} + +SpeedLimiterItem::SpeedLimiterItem() + : m_availableBytes(-1) +{ +} + +void SpeedLimiterItem::updateUsage(int bytes) +{ + // Ignore if there are no limits + if (m_availableBytes == -1) + return; + + if (bytes > m_availableBytes) + m_availableBytes = 0; + else + m_availableBytes -= bytes; +} + +} + +#include "speedlimiter.moc" diff --git a/kftpgrabber/src/engine/speedlimiter.h b/kftpgrabber/src/engine/speedlimiter.h new file mode 100644 index 0000000..789cb19 --- /dev/null +++ b/kftpgrabber/src/engine/speedlimiter.h @@ -0,0 +1,158 @@ +/* + * This file is part of the KFTPGrabber project + * + * Copyright (C) 2003-2007 by the KFTPGrabber developers + * Copyright (C) 2003-2007 Jernej Kos <kostko@jweb-network.net> + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and + * NON-INFRINGEMENT. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations + * including the two. + * + * You must obey the GNU General Public License in all respects + * for all of the code used other than OpenSSL. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you + * do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source + * files in the program, then also delete it here. + */ +#ifndef KFTPENGINESPEEDLIMITER_H +#define KFTPENGINESPEEDLIMITER_H + +#include <qobject.h> +#include <qptrlist.h> + +namespace KFTPEngine { + +class SpeedLimiterItem; + +/** + * This class is used by Socket implementations to enforce speed limits for + * uploads or downloads. It implements a variant of Token Bucket algorithm. + * + * @author Jernej Kos <kostko@unimatrix-one.org> + */ +class SpeedLimiter : public QObject { +Q_OBJECT +public: + /** + * Possible limit types. + */ + enum Type { + Download = 0, + Upload = 1 + }; + + /** + * Returns the global speed limiter instance. + */ + static SpeedLimiter *self(); + + /** + * Class destructor. + */ + ~SpeedLimiter(); + + /** + * Set a limit rate. + * + * @param type Limit type + * @param limit Rate + */ + void setLimit(Type type, int limit); + + /** + * Appends an item to be managed by the speed limiter. + * + * @param item Item instance + * @param type Limit type + */ + void append(SpeedLimiterItem *item, Type type); + + /** + * Removes an item from the speed limiter. + * + * @param item Item instance + */ + void remove(SpeedLimiterItem *item); + + /** + * Removes an item from the speed limiter. + * + * @param item Item instance + * @param type Limit type + */ + void remove(SpeedLimiterItem *item, Type type); +protected: + /** + * Static class instance. + */ + static SpeedLimiter *m_self; + + /** + * Class constructor. + */ + SpeedLimiter(); + + /** + * Timer event. + */ + void timerEvent(QTimerEvent*); +private: + bool m_timer; + int m_limits[2]; + + QPtrList<SpeedLimiterItem> m_objects[2]; + + int m_tokenDebt[2]; +private slots: + void updateLimits(); +}; + +/** + * This class represents an item managed by the speed limiter. This is + * usually a socket. + * + * @author Jernej Kos <kostko@unimatrix-one.org> + */ +class SpeedLimiterItem { +friend class SpeedLimiter; +public: + /** + * Class constructor. + */ + SpeedLimiterItem(); + + /** + * Returns the number of bytes allowed for consumption. + */ + int allowedBytes() const { return m_availableBytes; } +protected: + /** + * Updates object's byte usage. + */ + void updateUsage(int bytes); +private: + int m_availableBytes; +}; + +} + +#endif diff --git a/kftpgrabber/src/engine/ssl.cpp b/kftpgrabber/src/engine/ssl.cpp new file mode 100644 index 0000000..92418bb --- /dev/null +++ b/kftpgrabber/src/engine/ssl.cpp @@ -0,0 +1,264 @@ +/* + * This file is part of the KFTPGrabber project + * + * Copyright (C) 2003-2006 by the KFTPGrabber developers + * Copyright (C) 2003-2006 Jernej Kos <kostko@jweb-network.net> + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and + * NON-INFRINGEMENT. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations + * including the two. + * + * You must obey the GNU General Public License in all respects + * for all of the code used other than OpenSSL. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you + * do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source + * files in the program, then also delete it here. + */ +#include "ssl.h" + +#include <ksocketdevice.h> +#include <kmdcodec.h> +#include <ksslx509v3.h> + +#include <openssl/ssl.h> +#include <openssl/x509.h> + +#include <unistd.h> + +namespace KFTPEngine { + +class Ssl::Private { +public: + Private() + : ssl(0), sslCtx(0), bio(0) + { + } + + bool initialized; + + SSL *ssl; + SSL_CTX *sslCtx; + BIO *bio; + X509 *certificate; +}; + +Ssl::Ssl(KNetwork::KStreamSocket *socket) + : d(new Ssl::Private()), + m_socket(socket) +{ + d->ssl = 0; + d->sslCtx = 0; + d->bio = 0; + d->certificate = 0; + d->initialized = false; + + initialize(); +} + +Ssl::~Ssl() +{ + close(); + delete d; +} + +void Ssl::initialize() +{ + if (!d->ssl) { + SSL_library_init(); + + d->sslCtx = SSL_CTX_new(SSLv23_client_method()); + d->ssl = SSL_new(d->sslCtx); + + SSL_CTX_set_options(d->sslCtx, SSL_OP_ALL); + + // Initialize the socket BIO + d->bio = BIO_new_socket(m_socket->socketDevice()->socket(), BIO_NOCLOSE); + SSL_set_bio(d->ssl, d->bio, d->bio); + } + + d->initialized = true; +} + +bool Ssl::connect() +{ + if (!d->initialized) + return false; + +retry_connect: + int ret = SSL_connect(d->ssl); + if (ret == 1) { + // Connection established + setConnectionInfo(); + return true; + } else { + int err = SSL_get_error(d->ssl, ret); + + if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) { +retry_poll: + bool input; + m_socket->socketDevice()->poll(&input, 0, 0, 0); + + if (input) + goto retry_connect; + else { + ::usleep(20000); + goto retry_poll; + } + } else { + return false; + } + } + + return true; +} + +bool Ssl::setClientCertificate(KSSLPKCS12 *pkcs) +{ + if (!pkcs || !pkcs->getCertificate()) + return false; + + int ret; + X509 *x; + EVP_PKEY *k = pkcs->getPrivateKey(); + QCString cert = QCString(pkcs->getCertificate()->toString().ascii()); + + QByteArray qba, qbb = cert.copy(); + KCodecs::base64Decode(qbb, qba); +#if OPENSSL_VERSION_NUMBER > 0x009070afL + const unsigned char *qbap = reinterpret_cast<unsigned char *>(qba.data()); +#else + unsigned char *qbap = reinterpret_cast<unsigned char *>(qba.data()); +#endif + x = d2i_X509(NULL, &qbap, qba.size()); + + if (!x || !k) + return false; + + if (!pkcs->getCertificate()->x509V3Extensions().certTypeSSLClient()) + return false; + + ret = SSL_CTX_use_certificate(d->sslCtx, x); + if (ret <= 0) + return false; + + ret = SSL_CTX_use_PrivateKey(d->sslCtx, k); + if (ret <= 0) + return false; + + return true; +} + +void Ssl::setConnectionInfo() +{ + SSL_CIPHER *cipher; + char buffer[1024]; + + buffer[0] = 0; + cipher = SSL_get_current_cipher(d->ssl); + + if (!cipher) + return; + + m_connectionInfo.m_cipherUsedBits = SSL_CIPHER_get_bits(cipher, &(m_connectionInfo.m_cipherBits)); + m_connectionInfo.m_cipherVersion = SSL_CIPHER_get_version(cipher); + m_connectionInfo.m_cipherName = SSL_CIPHER_get_name(cipher); + m_connectionInfo.m_cipherDescription = SSL_CIPHER_description(cipher, buffer, 1023); +} + +SslConnectionInfo &Ssl::connectionInfo() +{ + return m_connectionInfo; +} + +void Ssl::close() +{ + if (!d->initialized) + return; + + if (d->certificate) { + X509_free(d->certificate); + d->certificate = 0; + } + + if (d->ssl) { + SSL_shutdown(d->ssl); + SSL_free(d->ssl); + SSL_CTX_free(d->sslCtx); + + d->ssl = 0; + d->sslCtx = 0; + d->bio = 0; + } +} + +int Ssl::read(void *buffer, int bytes) +{ + if (!d->initialized) + return -1; + + int ret = SSL_read(d->ssl, buffer, bytes); + + if (ret <= 0) { + int err = SSL_get_error(d->ssl, ret); + + if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) + return 0; + else + return -1; + } + + return ret; +} + +int Ssl::write(void *buffer, int bytes) +{ + if (!d->initialized) + return -1; + +retry_write: + int ret = SSL_write(d->ssl, buffer, bytes); + + if (ret <= 0) { + int err = SSL_get_error(d->ssl, ret); + + if (err == SSL_ERROR_WANT_READ) { +retry_poll: + bool input; + m_socket->socketDevice()->poll(&input, 0, 0, 0); + + if (input) + goto retry_write; + else { + ::usleep(20000); + goto retry_poll; + } + } else if (err == SSL_ERROR_WANT_WRITE) { + return -1; + } else { + return -1; + } + } + + return ret; +} + +} diff --git a/kftpgrabber/src/engine/ssl.h b/kftpgrabber/src/engine/ssl.h new file mode 100644 index 0000000..e0933ed --- /dev/null +++ b/kftpgrabber/src/engine/ssl.h @@ -0,0 +1,176 @@ +/* + * This file is part of the KFTPGrabber project + * + * Copyright (C) 2003-2006 by the KFTPGrabber developers + * Copyright (C) 2003-2006 Jernej Kos <kostko@jweb-network.net> + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and + * NON-INFRINGEMENT. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations + * including the two. + * + * You must obey the GNU General Public License in all respects + * for all of the code used other than OpenSSL. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you + * do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source + * files in the program, then also delete it here. + */ +#ifndef KFTPENGINESSL_H +#define KFTPENGINESSL_H + +#include <kstreamsocket.h> +#include <ksslcertificate.h> +#include <ksslpkcs12.h> + +namespace KFTPEngine { + +/** + * This class contains information about the currently established SSL + * connection. + * + * @author Jernej Kos + */ +class SslConnectionInfo { +friend class Ssl; +public: + /** + * Get the cipher in use. + */ + const QString &getCipher() const { return m_cipherName; } + + /** + * Describe the cipher in use. + */ + const QString &getCipherDescription() const { return m_cipherDescription; } + + /** + * Get the version of the cipher in use. + */ + const QString &getCipherVersion() const { return m_cipherVersion; } + + /** + * Get the number of bits of the cipher that are actually used. + */ + int getCipherUsedBits() const { return m_cipherUsedBits; } + + /** + * Get bit-size of the cipher. + */ + int getCipherBits() const { return m_cipherBits; } +protected: + /** + * Class constructor. + */ + SslConnectionInfo() {} + + int m_cipherUsedBits; + int m_cipherBits; + + QString m_cipherName; + QString m_cipherDescription; + QString m_cipherVersion; +}; + +/** + * A class that properly handles asynchronious SSL connections. + * + * @author Jernej Kos + */ +class Ssl { +public: + /** + * Class constructor. + * + * @param socket The socket to use as transport + */ + Ssl(KNetwork::KStreamSocket *socket); + + /** + * Class destructor. + */ + ~Ssl(); + + /** + * Start the SSL handshake. This method will block until the + * handshake is completed. + * + * @return True if the handshake was successful, false otherwise + */ + bool connect(); + + /** + * Close the SSL connection and deallocate resources. + */ + void close(); + + /** + * Read from the underlying socket. + * + * @param buffer The tarrget buffer + * @param bytes Maximum number of bytes to read + * @return Number of bytes actually read or -1 in case of an error + */ + int read(void *buffer, int bytes); + + /** + * Write to the underlying socket. + * + * @param buffer The source buffer + * @param bytes Number of bytes to write + * @return Number of bytes actually written or -1 in case of an error + */ + int write(void *buffer, int bytes); + + /** + * Obtain a reference to the connection information. + * + * @return A reference ot the connection information, valid after connected + */ + SslConnectionInfo &connectionInfo(); + + /** + * Set the client certificate to use. + * + * @return True if the certificate was successfuly set + */ + bool setClientCertificate(KSSLPKCS12 *pkcs); +private: + class Private; + Private *d; + + KNetwork::KStreamSocket *m_socket; + SslConnectionInfo m_connectionInfo; +protected: + /** + * Initialize the SSL session for operation. + */ + void initialize(); + + /** + * Populate the connection info object with data retrieved from the SSL + * socket. Note that the socket has to be connected! + */ + void setConnectionInfo(); +}; + +} + +#endif diff --git a/kftpgrabber/src/engine/thread.cpp b/kftpgrabber/src/engine/thread.cpp new file mode 100644 index 0000000..3e151b5 --- /dev/null +++ b/kftpgrabber/src/engine/thread.cpp @@ -0,0 +1,346 @@ +/* + * This file is part of the KFTPGrabber project + * + * Copyright (C) 2003-2006 by the KFTPGrabber developers + * Copyright (C) 2003-2006 Jernej Kos <kostko@jweb-network.net> + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and + * NON-INFRINGEMENT. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations + * including the two. + * + * You must obey the GNU General Public License in all respects + * for all of the code used other than OpenSSL. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you + * do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source + * files in the program, then also delete it here. + */ +#include "thread.h" +#include "ftpsocket.h" +#include "sftpsocket.h" + +#include <qapplication.h> + +namespace KFTPEngine { + +Thread::Thread() + : QThread(), + m_eventHandler(new EventHandler(this)), + m_socket(0), + m_wakeupEvent(0), + m_abortLoop(false), + m_wakeUpPending(false) +{ + m_protocolMap.insert("ftp", new FtpSocket(this)); + m_protocolMap.insert("sftp", new SftpSocket(this)); + + // FTP is the default protocol + m_socket = m_protocolMap["ftp"]; + + // Auto start the thread + start(); +} + +Thread::~Thread() +{ + m_abortLoop = true; + + if (!wait(1000)) + terminate(); + + // Destroy all protocol sockets + delete static_cast<FtpSocket*>(m_protocolMap["ftp"]); + delete static_cast<SftpSocket*>(m_protocolMap["sftp"]); + + m_protocolMap.clear(); +} + +void Thread::run() +{ + while (!m_abortLoop) { + QThread::usleep(100); + + // "Poll" the socket + m_socket->poll(); + + // Transmit wakeup events if any + if (m_wakeUpPending && m_socket->isBusy()) { + m_wakeupMutex.lock(); + m_socket->wakeup(m_wakeupEvent); + + delete m_wakeupEvent; + m_wakeupEvent = 0; + m_wakeUpPending = false; + m_wakeupMutex.unlock(); + } + + // Execute any pending commands if the socket isn't busy + if (!m_commandQueue.empty()) { + m_queueMutex.lock(); + + QValueList<Commands::Type>::iterator queueEnd = m_commandQueue.end(); + for (QValueList<Commands::Type>::iterator i = m_commandQueue.begin(); i != queueEnd; ++i) { + Commands::Type cmdType = *i; + + // Execute the command + if (cmdType == Commands::CmdNext) { + m_commandQueue.remove(i--); + m_socket->nextCommand(); + } else if (!m_socket->isBusy()) { + m_commandQueue.remove(i--); + m_socket->setCurrentCommand(cmdType); + + switch (cmdType) { + case Commands::CmdConnect: { + m_socket->protoConnect(nextCommandParameter().asUrl()); + break; + } + case Commands::CmdDisconnect: { + m_socket->protoDisconnect(); + break; + } + case Commands::CmdList: { + m_socket->protoList(nextCommandParameter().asUrl()); + break; + } + case Commands::CmdScan: { + m_socket->protoScan(nextCommandParameter().asUrl()); + break; + } + case Commands::CmdGet: { + m_socket->protoGet(nextCommandParameter().asUrl(), + nextCommandParameter().asUrl()); + break; + } + case Commands::CmdPut: { + m_socket->protoPut(nextCommandParameter().asUrl(), + nextCommandParameter().asUrl()); + break; + } + case Commands::CmdDelete: { + m_socket->protoDelete(nextCommandParameter().asUrl()); + break; + } + case Commands::CmdRename: { + m_socket->protoRename(nextCommandParameter().asUrl(), + nextCommandParameter().asUrl()); + break; + } + case Commands::CmdChmod: { + m_socket->protoChmod(nextCommandParameter().asUrl(), + nextCommandParameter().asFileSize(), + nextCommandParameter().asBoolean()); + break; + } + case Commands::CmdMkdir: { + m_socket->protoMkdir(nextCommandParameter().asUrl()); + break; + } + case Commands::CmdRaw: { + m_socket->protoRaw(nextCommandParameter().asString()); + break; + } + case Commands::CmdFxp: { + m_socket->protoSiteToSite(static_cast<Socket*>(nextCommandParameter().asData()), + nextCommandParameter().asUrl(), + nextCommandParameter().asUrl()); + break; + } + default: { + // Just ignore unknown commands for now + break; + } + } + } + } + + m_queueMutex.unlock(); + } + } +} + +void Thread::wakeup(WakeupEvent *event) +{ + QMutexLocker locker(&m_wakeupMutex); + + m_wakeupEvent = event; + m_wakeUpPending = true; +} + +void Thread::abort() +{ + // Clear any pending wakeup events + if (m_wakeUpPending) { + QMutexLocker locker(&m_wakeupMutex); + + m_wakeupEvent = 0; + m_wakeUpPending = false; + } + + m_socket->protoAbort(); +} + +void Thread::event(Event::Type type, QValueList<EventParameter> params) +{ + if (m_eventHandler) { + Event *e = new Event(type, params); + qApp->postEvent(m_eventHandler, e); + } +} + +void Thread::selectSocketForProtocol(const KURL &url) +{ + if (url.protocol() == m_socket->protocolName()) + return; + + // Change the socket if one exists + Socket *socket = m_protocolMap.find(url.protocol()); + if (socket) + m_socket = socket; +} + +EventParameter Thread::nextCommandParameter() +{ + QMutexLocker locker(&m_paramsMutex); + EventParameter param = m_commandParams.front(); + m_commandParams.pop_front(); + + return param; +} + +void Thread::connect(const KURL &url) +{ + QMutexLocker locker(&m_paramsMutex); + QMutexLocker lockerq(&m_queueMutex); + + // Setup the correct socket to use for connection + selectSocketForProtocol(url); + + m_commandQueue.append(Commands::CmdConnect); + m_commandParams.append(EventParameter(url)); +} + +void Thread::disconnect() +{ + QMutexLocker locker(&m_paramsMutex); + QMutexLocker lockerq(&m_queueMutex); + + m_commandQueue.append(Commands::CmdDisconnect); +} + +void Thread::list(const KURL &url) +{ + QMutexLocker locker(&m_paramsMutex); + QMutexLocker lockerq(&m_queueMutex); + + m_commandQueue.append(Commands::CmdList); + m_commandParams.append(EventParameter(url)); +} + +void Thread::scan(const KURL &url) +{ + QMutexLocker locker(&m_paramsMutex); + QMutexLocker lockerq(&m_queueMutex); + + m_commandQueue.append(Commands::CmdScan); + m_commandParams.append(EventParameter(url)); +} + +void Thread::get(const KURL &source, const KURL &destination) +{ + QMutexLocker locker(&m_paramsMutex); + QMutexLocker lockerq(&m_queueMutex); + + m_commandQueue.append(Commands::CmdGet); + m_commandParams.append(EventParameter(destination)); + m_commandParams.append(EventParameter(source)); +} + +void Thread::put(const KURL &source, const KURL &destination) +{ + QMutexLocker locker(&m_paramsMutex); + QMutexLocker lockerq(&m_queueMutex); + + m_commandQueue.append(Commands::CmdPut); + m_commandParams.append(EventParameter(destination)); + m_commandParams.append(EventParameter(source)); +} + +void Thread::remove(const KURL &url) +{ + QMutexLocker locker(&m_paramsMutex); + QMutexLocker lockerq(&m_queueMutex); + + m_commandQueue.append(Commands::CmdDelete); + m_commandParams.append(EventParameter(url)); +} + +void Thread::rename(const KURL &source, const KURL &destination) +{ + QMutexLocker locker(&m_paramsMutex); + QMutexLocker lockerq(&m_queueMutex); + + m_commandQueue.append(Commands::CmdRename); + m_commandParams.append(EventParameter(destination)); + m_commandParams.append(EventParameter(source)); +} + +void Thread::chmod(const KURL &url, int mode, bool recursive) +{ + QMutexLocker locker(&m_paramsMutex); + QMutexLocker lockerq(&m_queueMutex); + + m_commandQueue.append(Commands::CmdChmod); + m_commandParams.append(EventParameter(recursive)); + m_commandParams.append(EventParameter(mode)); + m_commandParams.append(EventParameter(url)); +} + +void Thread::mkdir(const KURL &url) +{ + QMutexLocker locker(&m_paramsMutex); + QMutexLocker lockerq(&m_queueMutex); + + m_commandQueue.append(Commands::CmdMkdir); + m_commandParams.append(EventParameter(url)); +} + +void Thread::raw(const QString &raw) +{ + QMutexLocker locker(&m_paramsMutex); + QMutexLocker lockerq(&m_queueMutex); + + m_commandQueue.append(Commands::CmdRaw); + m_commandParams.append(EventParameter(raw)); +} + +void Thread::siteToSite(Thread *thread, const KURL &source, const KURL &destination) +{ + QMutexLocker locker(&m_paramsMutex); + QMutexLocker lockerq(&m_queueMutex); + + m_commandQueue.append(Commands::CmdFxp); + m_commandParams.append(EventParameter(destination)); + m_commandParams.append(EventParameter(source)); + m_commandParams.append(EventParameter(thread->socket())); +} + +} diff --git a/kftpgrabber/src/engine/thread.h b/kftpgrabber/src/engine/thread.h new file mode 100644 index 0000000..62a36c4 --- /dev/null +++ b/kftpgrabber/src/engine/thread.h @@ -0,0 +1,133 @@ +/* + * This file is part of the KFTPGrabber project + * + * Copyright (C) 2003-2006 by the KFTPGrabber developers + * Copyright (C) 2003-2006 Jernej Kos <kostko@jweb-network.net> + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and + * NON-INFRINGEMENT. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations + * including the two. + * + * You must obey the GNU General Public License in all respects + * for all of the code used other than OpenSSL. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you + * do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source + * files in the program, then also delete it here. + */ +#ifndef KFTPENGINETHREAD_H +#define KFTPENGINETHREAD_H + +#include <qthread.h> +#include <qmutex.h> +#include <qvaluelist.h> +#include <qdict.h> + +#include "event.h" +#include "directorylisting.h" +#include "commands.h" +#include "socket.h" + +namespace KFTPEngine { + +/** + * This class represents a socket thread. It serves as a command queue to + * the underlying socket implementation and also as an abstraction layer + * to support multiple protocols. + * + * @author Jernej Kos <kostko@jweb-network.net> + */ +class Thread : public QThread +{ +friend class EventHandler; +friend class Socket; +public: + Thread(); + ~Thread(); + + /** + * Returns the event handler for this thread. Should be used to connect + * to any signals this thread may emit. + * + * @return A pointer to the EventHandler object + */ + EventHandler *eventHandler() { return m_eventHandler; } + + /** + * Returns the underlying socket object. + * + * @return A pointer to the Socket object + */ + Socket *socket() { return m_socket; } + + /** + * Prepare the apropriate socket for use. + * + * @param url The url that should be used to identify the protocol + */ + void selectSocketForProtocol(const KURL &url); + + /** + * Schedules a wakeup event to be passed on to the underlying socket. + * + * @param event The wakeup event to pass on + */ + void wakeup(WakeupEvent *event); + + void abort(); + void connect(const KURL &url); + void disconnect(); + void list(const KURL &url); + void scan(const KURL &url); + void get(const KURL &source, const KURL &destination); + void put(const KURL &source, const KURL &destination); + void remove(const KURL &url); + void rename(const KURL &source, const KURL &destination); + void chmod(const KURL &url, int mode, bool recursive = false); + void mkdir(const KURL &url); + void raw(const QString &raw); + void siteToSite(Thread *thread, const KURL &source, const KURL &destination); +protected: + void run(); + void event(Event::Type type, QValueList<EventParameter> params); + + EventParameter nextCommandParameter(); +protected: + EventHandler *m_eventHandler; + Socket *m_socket; + + QMutex m_eventMutex; + QMutex m_wakeupMutex; + QMutex m_paramsMutex; + QMutex m_queueMutex; + + QDict<Socket> m_protocolMap; + QValueList<Commands::Type> m_commandQueue; + QValueList<EventParameter> m_commandParams; + WakeupEvent *m_wakeupEvent; + + bool m_abortLoop; + bool m_wakeUpPending; +}; + +} + +#endif |