diff options
Diffstat (limited to 'kftpgrabber/src/engine/ftpsocket.cpp')
-rw-r--r-- | kftpgrabber/src/engine/ftpsocket.cpp | 2749 |
1 files changed, 2749 insertions, 0 deletions
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); +} + +} |