/* * This file is part of the KFTPGrabber project * * Copyright (C) 2003-2006 by the KFTPGrabber developers * Copyright (C) 2003-2006 Jernej Kos * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and * NON-INFRINGEMENT. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston, * MA 02110-1301, USA. * * In addition, as a special exception, the copyright holders give * permission to link the code of portions of this program with the * OpenSSL library under certain conditions as described in each * individual source file, and distribute linked combinations * including the two. * * You must obey the GNU General Public License in all respects * for all of the code used other than OpenSSL. If you modify * file(s) with this exception, you may extend this exception to your * version of the file(s), but you are not obligated to do so. If you * do not wish to do so, delete this exception statement from your * version. If you delete this exception statement from all source * files in the program, then also delete it here. */ #include "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 #include #include #include #include #include #include #include #include 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(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(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(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(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(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); } }