/* * This file is part of the KFTPGrabber project * * Copyright (C) 2003-2006 by the KFTPGrabber developers * Copyright (C) 2003-2006 Jernej Kos * * 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/kftpconfig.h" #include namespace KFTPEngine { Socket::Socket(Thread *thread, const TQString &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 TQString ¶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; } TQValueList 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 TQString ¶m1, const TQString ¶m2) { TQValueList 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) { TQValueList 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) { TQValueList params; params.append(EventParameter(param1)); // Dispatch the event via socket thread m_thread->event(type, params); } void Socket::emitEvent(Event::Type type, void *param1) { TQValueList params; params.append(EventParameter(param1)); // Dispatch the event via socket thread m_thread->event(type, params); } void Socket::changeEncoding(const TQString &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 = TQTime(); } } 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) { TQPtrList::iterator chainEnd = m_commandChain.end(); for (TQPtrList::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()) { tqDebug("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()) { tqDebug("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 TQValueList list = socket()->getLastDirectoryListing().list(); TQValueList::iterator listEnd = list.end(); for (TQValueList::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()) { TQValueList list = cached.list(); TQValueList::iterator listEnd = list.end(); for (TQValueList::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) TQValueList currentList; TQValueList::const_iterator currentEntry; TQString 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) TQValueList currentList; TQValueList::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) TQValueList currentList; TQValueList::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); } } }