#include "ftpcontrolconnection.h"
#include "ftplistcommand.h"
#include "ftpretrcommand.h"
#include "ftpstorcommand.h"
#include "dataconnection.h"
#include <QFileInfo>
#include <QDateTime>
#include <QDir>
#include <QStringList>
#include <QEventLoop>
#include <QDebug>
#include <QTimer>
#include <QTcpSocket>
#include <QHostAddress>
FtpControlConnection::FtpControlConnection(QObject *parent, QTcpSocket *socket, const QString &rootPath, const QString &userName, const QString &password, bool readOnly) :
QObject(parent)
{
this->socket = socket;
this->userName = userName;
this->password = password;
this->rootPath = rootPath;
this->readOnly = readOnly;
isLoggedIn = false;
encryptDataConnection = false;
socket->setParent(this);
connect(socket, SIGNAL(readyRead()), this, SLOT(acceptNewData()));
connect(socket, SIGNAL(disconnected()), this, SLOT(deleteLater()));
currentDirectory = "/";
dataConnection = new DataConnection(this);
reply("220 Welcome to QFtpServer.");
}
FtpControlConnection::~FtpControlConnection()
{
}
void FtpControlConnection::acceptNewData()
{
if (!socket->canReadLine()) {
return;
}
// Note how we execute only one line, and use QTimer::singleShot, instead
// of using a for-loop until no more lines are available. This is done
// so we don't block the event loop for a long time.
processCommand(QString::fromUtf8(socket->readLine()).trimmed());
QTimer::singleShot(0, this, SLOT(acceptNewData()));
}
void FtpControlConnection::disconnectFromHost()
{
socket->disconnectFromHost();
}
bool FtpControlConnection::verifyAuthentication(const QString &command)
{
if (isLoggedIn) {
return true;
}
const char *commandsRequiringAuth[] = {
"PWD", "CWD", "TYPE", "PORT", "PASV", "LIST", "RETR", "REST",
"NLST", "SIZE", "SYST", "PROT", "CDUP", "OPTS", "PBSZ", "NOOP",
"STOR", "MKD", "RMD", "DELE", "RNFR", "RNTO", "APPE"
};
for (size_t ii = 0; ii < sizeof(commandsRequiringAuth)/sizeof(commandsRequiringAuth[0]); ++ii) {
if (command == commandsRequiringAuth[ii]) {
reply("530 You must log in first.");
return false;
}
}
return true;
}
bool FtpControlConnection::verifyWritePermission(const QString &command)
{
if (!readOnly) {
return true;
}
const char *commandsRequiringWritePermission[] = {
"STOR", "MKD", "RMD", "DELE", "RNFR", "RNTO", "APPE"
};
for (size_t ii = 0; ii < sizeof(commandsRequiringWritePermission)/sizeof(commandsRequiringWritePermission[0]); ++ii) {
if (command == commandsRequiringWritePermission[ii]) {
reply("550 Can't do that in read-only mode.");
return false;
}
}
return true;
}
QString FtpControlConnection::stripFlagL(const QString &fileName)
{
QString a = fileName.toUpper();
if (a == "-L") {
return "";
}
if (a.startsWith("-L ")) {
return fileName.mid(3);
}
return fileName;
}
void FtpControlConnection::parseCommand(const QString &entireCommand, QString *command, QString *commandParameters)
{
// Split parameters and command.
int pos = entireCommand.indexOf(' ');
if (-1 != pos) {
*command = entireCommand.left(pos).trimmed().toUpper();
*commandParameters = entireCommand.mid(pos+1).trimmed();
} else {
*command = entireCommand.trimmed().toUpper();
}
}
QString FtpControlConnection::toLocalPath(const QString &fileName) const
{
QString localPath = fileName;
// Some FTP clients send backslashes.
localPath.replace('\\', '/');
// If this is a relative path, we prepend the current directory.
if (!localPath.startsWith('/')) {
localPath = currentDirectory + '/' + localPath;
}
// Evaluate all the ".." and ".", "/path/././to/dir/../.." becomes "/path".
// Note we do this **before** prepending the root path, in order to avoid
// "jailbreaking" out of the "chroot".
QStringList components;
foreach (const QString &component, localPath.split('/', QString::SkipEmptyParts)) {
if (component == "..") {
if (!components.isEmpty()) {
components.pop_back();
}
} else if (component != ".") {
components += component;
}
}
// Prepend the root path.
localPath = QDir::cleanPath(rootPath + '/' + components.join("/"));
qDebug() << "to local path" << fileName << "->" << localPath;
return localPath;
}
void FtpControlConnection::reply(const QString &replyCode)
{
qDebug() << "reply" << replyCode;
socket->write((replyCode + "\r\n").toUtf8());
}
void FtpControlConnection::processCommand(const QString &entireCommand)
{
qDebug() << "command" << entireCommand;
QString command;
QString commandParameters;
parseCommand(entireCommand, &command, &commandParameters);
if (!verifyAuthentication(command)) {
return;
}
if (!verifyWritePermission(command)) {
return;
}
if ("USER" == command) {
reply("331 User name OK, need password.");
} else if ("PASS" == command) {
pass(commandParameters);
} else if ("QUIT" == command) {
quit();
} else if ("AUTH" == command && "TLS" == commandParameters.toUpper()) {
auth();
} else if ("FEAT" == command) {
feat();
} else if ("PWD" == command) {
reply(QString("257 \"%1\"").arg(currentDirectory));
} else if ("CWD" == command) {
cwd(commandParameters);
} else if ("TYPE" == command) {
reply("200 Command okay.");
} else if ("PORT" == command) {
port(commandParameters);
} else if ("PASV" == command) {
pasv();
} else if ("LIST" == command) {
list(toLocalPath(stripFlagL(commandParameters)), false);
} else if ("RETR" == command) {
retr(toLocalPath(commandParameters));
} else if ("REST" == command) {
reply("350 Requested file action pending further information.");
} else if ("NLST" == command) {
list(toLocalPath(stripFlagL(commandParameters)), true);
} else if ("SIZE" == command) {
size(toLocalPath(commandParameters));
} else if ("SYST" == command) {
reply("215 UNIX");
} else if ("PROT" == command) {
prot(commandParameters.toUpper());
} else if ("CDUP" == command) {
cdup();
} else if ("OPTS" == command && "UTF8 ON" == commandParameters.toUpper()) {
reply("200 Command okay.");
} else if ("PBSZ" == command && "0" == commandParameters.toUpper()) {
reply("200 Command okay.");
} else if ("NOOP" == command) {
reply("200 Command okay.");
} else if ("STOR" == command) {
stor(toLocalPath(commandParameters));
} else if ("MKD" == command) {
mkd(toLocalPath(commandParameters));
} else if ("RMD" == command) {
rmd(toLocalPath(commandParameters));
} else if ("DELE" == command) {
dele(toLocalPath(commandParameters));
} else if ("RNFR" == command) {
reply("350 Requested file action pending further information.");
} else if ("RNTO" == command) {
rnto(toLocalPath(commandParameters));
} else if ("APPE" == command) {
stor(toLocalPath(commandParameters), true);
} else {
reply("502 Command not implemented.");
}
lastProcessedCommand = entireCommand;
}
void FtpControlConnection::startOrScheduleCommand(FtpCommand *ftpCommand)
{
connect(ftpCommand, SIGNAL(reply(QString)), this, SLOT(reply(QString)));
if (!dataConnection->setFtpCommand(ftpCommand)) {
delete ftpCommand;
reply("425 Can't open data connection.");
re