You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
kftpgrabber/kftpgrabber/src/engine/socket.cpp

867 lines
25 KiB

/*
* 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/kftpconfig.h"
#include <tdelocale.h>
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 &param1)
{
// 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<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 TQString &param1, const TQString &param2)
{
TQValueList<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)
{
TQValueList<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)
{
TQValueList<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)
{
TQValueList<EventParameter> 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<Commands::Base>::iterator chainEnd = m_commandChain.end();
for (TQPtrList<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()) {
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<DirectoryEntry> list = socket()->getLastDirectoryListing().list();
TQValueList<DirectoryEntry>::iterator listEnd = list.end();
for (TQValueList<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()) {
TQValueList<DirectoryEntry> list = cached.list();
TQValueList<DirectoryEntry>::iterator listEnd = list.end();
for (TQValueList<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)
TQValueList<DirectoryEntry> currentList;
TQValueList<DirectoryEntry>::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<DirectoryEntry> currentList;
TQValueList<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)
TQValueList<DirectoryEntry> currentList;
TQValueList<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);
}
}
}