Heavily overhauled client command handling and changed motd to use multi-line text.

This commit is contained in:
2024-12-15 11:02:13 -06:00
parent 28efbfb185
commit f37999498c
3 changed files with 207 additions and 187 deletions

View File

@@ -39,10 +39,6 @@ public:
submit(230, conf_format(server_name, (std::map<std::string, std::string>){{std::string(APPNAME_VAR),std::string(APPNAME)},{std::string(VERSION_VAR),std::string(VERSION)}}));
}
void addOption(std::string name, bool toggle) {
this->options[name] = toggle;
}
int receive(std::string cmd, std::string argstr) {
if (control_sock <= 0) return 1;
@@ -69,97 +65,127 @@ public:
return 0;
}
if (cmd == "AUTH") {
if (cmd == "USER") {
if (state == FTP_STATE_GUEST) {
if (argstr.length() >= sizeof(auth_data->username))
throw std::runtime_error("Username too long");
std::strncpy(auth_data->username, argstr.c_str(), sizeof(auth_data->username) - 1);
auth_data->username[sizeof(auth_data->username) - 1] = '\0';
if (auth->isPasswordRequired())
submit(331, "Password required");
else authedInit();
} else submit(503, "Already logged in");
} else if (cmd == "PASS") {
if (state == FTP_STATE_GUEST) {
if (strlen(auth_data->username) > 0) {
std::strncpy(auth_data->password, argstr.c_str(), sizeof(auth_data->password) - 1);
auth_data->password[sizeof(auth_data->password) - 1] = '\0';
if (auth->authenticate(auth_data))
authedInit();
else {
auth_data->username[0] = {};
submit(530, "Invalid Credentials.");
}
} else submit(503, "Bad sequence of commands");
} else submit(503, "Already logged in");
} else if (cmd == "REIN") {
if (state == FTP_STATE_ONDATA) data_close();
auth_data = new ClientAuthDetails{};
filer = default_filer_factory();
// Reset features
for (const auto &x : features)
features[x.first] = "OFF";
state = FTP_STATE_GUEST;
submit(220, "Service ready for user");
} else if (cmd == "AUTH") {
if (argstr == "TLS" || argstr == "SSL") {
if (setupControlSSL()) {
submit(234, "AUTH TLS successful");
is_secure = true;
return 0;
} else {
submit(431, "AUTH TLS failed");
}
} else {
submit(504, "AUTH type not supported");
}
return 0;
} else submit(431, "AUTH TLS failed");
} else submit(504, "AUTH type not supported");
} else if (cmd == "PBSZ") {
if (!is_secure) {
submit(503, "PBSZ not allowed on insecure control connection");
} else {
submit(200, "PBSZ=0");
}
if (!is_secure) submit(503, "PBSZ not allowed on insecure control connection");
else submit(200, "PBSZ=0");
return 0;
} else if (cmd == "PROT") {
if (!is_secure) {
submit(503, "PROT not allowed on insecure control connection");
} else if (argstr == "P") {
if (!is_secure) submit(503, "PROT not allowed on insecure control connection");
else if (argstr == "P") {
protect_data = true;
submit(200, "Protection level set to Private");
} else if (argstr == "C") {
protect_data = false;
submit(200, "Protection level set to Clear");
} else {
submit(504, "PROT level not supported");
}
return 0;
} else if (cmd == "QUIT") {
state = FTP_STATE_CLOSE;
submit(250, "Goodbye!");
shutdown(control_sock, SHUT_RDWR); // Add immediate shutdown
return 1; // Signal thread to terminate
} else if (state >= FTP_STATE_AUTHED) {
if (cmd == "SYST") {
submit(215, "UNIX Type: L8");
} else if (cmd == "TYPE") {
} else submit(504, "PROT level not supported");
} else if (cmd == "FEAT") {
submit("211-Extensions supported:");
for (const auto &s : global_features)
submit(" "+toUpper(s));
submit(211, "END");
} else if (cmd == "OPTS") {
std::vector<std::string> args = parseArgs(argstr);
if (args.size() > 0) {
std::string name = toUpper(args[0]);
if (args.size() > 1) {
if (features.find(name) == features.end())
features[name] = toUpper(args[1]);
else submit(451, "Unknown feature "+name);
} else {
if (features.find(name) == features.end())
submit(200, name+" is set to "+features[name]);
else submit(501, "Unknown feature "+name);
}
} else submit(550, "Malformed Request");
} else if (cmd == "NOOP") {
submit(226, "OK");
} else if (cmd == "SYST") {
submit(215, "UNIX Type: L8");
} else if (cmd == "TYPE") {
if (state >= FTP_STATE_AUTHED) {
char type = 0;
sscanf(argstr.c_str(), "%c", &(type));
filer->setTransferMode(type);
submit(226, "OK");
} else if (cmd == "OPTS") {
std::vector<std::string> args = parseArgs(argstr);
if (args.size() > 0) {
std::string name = toUpper(args[0]);
int rc = false;
if (args.size() > 1) rc = toggleOption(name, toUpper(args[1])=="ON");
else rc = toggleOption(name, true);
if (rc == 0) submit(200, "OK");
else if (rc == 1) submit(451, "Option not enabled or not recognized");
else submit(550, "Unknown error");
} else submit(550, "Malformed Request");
} else if (cmd == "PWD") {
} else submit(530, "Not logged in");
} else if (cmd == "PWD") {
if (state >= FTP_STATE_AUTHED) {
submit(257, "'"+filer->getCWD().string()+"'");
} else if (cmd == "CWD") {
} else submit(530, "Not logged in");
} else if (cmd == "CWD") {
if (state >= FTP_STATE_AUTHED) {
struct file_data fd = filer->traverse(argstr);
if (fd.error.code == 0) submit(250, "OK");
else submit(431, std::string(fd.error.msg));
} else if (cmd == "CDUP") {
} else submit(530, "Not logged in");
} else if (cmd == "CDUP") {
if (state >= FTP_STATE_AUTHED) {
struct file_data fd = filer->traverse("..");
if (fd.error.code == 0) submit(250, "OK");
else submit(431, std::string(fd.error.msg));
} else if (cmd == "MKD") {
} else submit(530, "Not logged in");
} else if (cmd == "MKD") {
if (state >= FTP_STATE_AUTHED) {
struct file_data fd = filer->createDirectory(argstr);
if (fd.error.code == 0) submit(257, "\""+std::string(fd.path)+"\" directory created");
else if (fd.error.code == FilerStatusCodes::FileExists) submit(521, std::string(fd.error.msg));
else submit(550, std::string(fd.error.msg));
} else if (cmd == "SIZE") {
} else submit(530, "Not logged in");
} else if (cmd == "SIZE") {
if (state >= FTP_STATE_AUTHED) {
struct file_data fd = filer->fileSize(argstr);
if (fd.error.code == 0) submit(213, std::to_string(fd.size));
else submit(550, std::string(fd.error.msg));
} else if (cmd == "FEAT") {
submit("211-Extensions supported:");
for (const auto &opt : options) {
submit(" "+toUpper(opt.first));
}
submit(211, "END");
} else if (cmd == "NOOP") {
submit(226, "OK");
} else if (cmd == "DELE" || cmd == "RMD") {
} else submit(530, "Not logged in");
} else if (cmd == "DELE" || cmd == "RMD") {
if (state >= FTP_STATE_AUTHED) {
struct file_data fd = filer->deleteFile(argstr);
if (fd.error.code == 0) submit(250, "OK");
else submit(550, std::string(fd.error.msg));
} else if (cmd == "PASV") {
} else submit(530, "Not logged in");
} else if (cmd == "PASV") {
if (state >= FTP_STATE_AUTHED) {
if (state == FTP_STATE_ONDATA) data_close();
if ((data_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("pasv socket() failed");
return 1;
@@ -233,9 +259,11 @@ public:
submit(425, "Can't open data connection");
data_close();
}
} else if (cmd == "RNFR") {
} else submit(530, "Not logged in");
} else if (cmd == "RNFR") {
if (state >= FTP_STATE_AUTHED) {
if (argstr.empty()) {
submit(501, "Syntax error in parameters or arguments.");
submit(501, "Syntax error in parameters or arguments");
return 0;
}
@@ -250,15 +278,17 @@ public:
// Store the source path and mark rename as pending
rename_from = argstr;
rename_pending = true;
submit(350, "Ready for RNTO.");
} else if (cmd == "RNTO") {
submit(350, "Ready for RNTO");
} else submit(530, "Not logged in");
} else if (cmd == "RNTO") {
if (state >= FTP_STATE_AUTHED) {
if (!rename_pending) {
submit(503, "RNFR required first.");
submit(503, "RNFR required first");
return 0;
}
if (argstr.empty()) {
submit(501, "Syntax error in parameters or arguments.");
submit(501, "Syntax error in parameters or arguments");
rename_pending = false;
return 0;
}
@@ -267,136 +297,97 @@ public:
struct file_data fd = filer->renameFile(rename_from, argstr);
rename_pending = false; // Reset rename state
if (fd.error.code == 0) {
submit(250, "Rename successful.");
} else {
submit(550, std::string(fd.error.msg));
}
} else if (state == FTP_STATE_ONDATA) {
if (data_sock <= 0) return 1;
if (cmd == "LIST" || cmd == "NLST") {
if (fd.error.code == 0) submit(250, "Rename successful");
else submit(550, std::string(fd.error.msg));
} else submit(530, "Not logged in");
} else if (cmd == "LIST" || cmd == "NLST") {
if (state == FTP_STATE_ONDATA && data_sock > 0) {
submit(150, "Transferring");
std::string dirname = "";
if (argstr.find_first_of('/') != std::string::npos)
dirname = argstr.substr(argstr.find_first_of('/'));
struct file_data fd = filer->list(dirname);
data_submit(fd.bin, fd.size);
submit(226, "OK");
data_close();
} else submit(425, "No data connection");
} else if (cmd == "RETR") {
if (state == FTP_STATE_ONDATA && data_sock > 0) {
struct file_data fd = filer->readFile(argstr);
if (fd.error.code == 0 && fd.stream && fd.stream->is_open()) {
submit(150, "Transferring");
std::string dirname = "";
if (argstr.find_first_of('/') != std::string::npos)
dirname = argstr.substr(argstr.find_first_of('/'));
struct file_data fd = filer->list(dirname);
data_submit(fd.bin, fd.size);
char buffer[8192];
bool transfer_ok = true;
submit(226, "OK");
} else if (cmd == "RETR") {
struct file_data fd = filer->readFile(argstr);
if (fd.error.code == 0 && fd.stream && fd.stream->is_open()) {
submit(150, "Transferring");
while (!fd.stream->eof()) {
fd.stream->read(buffer, sizeof(buffer));
size_t bytes_read = fd.stream->gcount();
char buffer[8192];
bool transfer_ok = true;
while (!fd.stream->eof()) {
fd.stream->read(buffer, sizeof(buffer));
size_t bytes_read = fd.stream->gcount();
if (bytes_read > 0) {
if (data_submit(buffer, bytes_read) != 0) {
submit(426, "Transfer failed");
transfer_ok = false;
break;
}
}
if (fd.stream->fail() && !fd.stream->eof()) {
submit(426, "Read error");
if (bytes_read > 0) {
if (data_submit(buffer, bytes_read) != 0) {
submit(426, "Transfer failed");
transfer_ok = false;
break;
}
}
if (transfer_ok) {
submit(226, "OK");
}
} else {
submit(550, std::string(fd.error.msg));
}
data_close();
} else if (cmd == "STOR") {
unsigned char inbuf[BUFFERSIZE] = {0};
submit(150, "Transferring");
int psize;
struct file_data fd = filer->writeFile(argstr, inbuf, 0);
while (true) {
if (is_secure && protect_data && data_ssl) {
psize = SSL_read(data_ssl, inbuf, sizeof(inbuf));
if (psize <= 0) {
int err = SSL_get_error(data_ssl, psize);
if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) {
continue;
}
break;
}
} else {
psize = recv(data_sock, inbuf, sizeof(inbuf), 0);
if (psize <= 0) break;
}
fd = filer->writeFile(argstr, inbuf, psize, true);
if (fd.error.code != 0) {
submit(550, "Access Denied: "+std::string(fd.error.msg));
data_close();
if (fd.stream->fail() && !fd.stream->eof()) {
submit(426, "Read error");
transfer_ok = false;
break;
}
memset(inbuf, 0, BUFFERSIZE);
}
submit(226, "OK");
} else {
submit(502, "Command not implemented");
}
if (transfer_ok) submit(226, "OK");
} else submit(550, std::string(fd.error.msg));
data_close();
} else {
submit(502, "Command not implemented");
}
} else {
state = FTP_STATE_GUEST; // If we reach here, force state to zero just in case.
if (cmd == "USER") {
if (argstr.length() >= sizeof(auth_data->username)) {
throw std::runtime_error("Username too long");
} else submit(425, "No data connection");
} else if (cmd == "STOR") {
if (state == FTP_STATE_ONDATA && data_sock > 0) {
unsigned char inbuf[BUFFERSIZE] = {0};
submit(150, "Transferring");
int psize;
struct file_data fd = filer->writeFile(argstr, inbuf, 0);
while (true) {
if (is_secure && protect_data && data_ssl) {
psize = SSL_read(data_ssl, inbuf, sizeof(inbuf));
if (psize <= 0) {
int err = SSL_get_error(data_ssl, psize);
if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) {
continue;
}
break;
}
} else {
psize = recv(data_sock, inbuf, sizeof(inbuf), 0);
if (psize <= 0) break;
}
fd = filer->writeFile(argstr, inbuf, psize, true);
if (fd.error.code != 0) {
submit(550, "Access Denied: "+std::string(fd.error.msg));
data_close();
break;
}
memset(inbuf, 0, BUFFERSIZE);
}
std::strncpy(auth_data->username, argstr.c_str(), sizeof(auth_data->username) - 1);
auth_data->username[sizeof(auth_data->username) - 1] = '\0';
if (auth->isPasswordRequired())
submit(331, "Password required");
else authedInit();
} else if (cmd == "PASS") {
std::strncpy(auth_data->password, argstr.c_str(), sizeof(auth_data->password) - 1);
auth_data->password[sizeof(auth_data->password) - 1] = '\0';
if (auth->authenticate(auth_data)) authedInit();
else {
auth_data->username[0] = {};
submit(530, "Invalid Credentials.");
}
} else {
submit(332, "Not Logged In!");
return 1;
}
}
submit(226, "OK");
data_close();
} else submit(425, "No data connection");
} else if (cmd == "QUIT") {
state = FTP_STATE_CLOSE;
submit(250, "Goodbye!");
shutdown(control_sock, SHUT_RDWR); // Add immediate shutdown
return 1; // Signal thread to terminate
} else submit(502, "Command not implemented");
return 0;
}
int toggleOption(std::string name, bool toggle) {
auto feat_it = options.find(name);
if (feat_it != options.end()) {
feat_it->second = toggle;
return 0;
}
return 1;
}
bool getOption(std::string name) {
auto feat_it = options.find(name);
if (feat_it != options.end()) {
return feat_it->second;
}
return false;
void addFeature(std::string name, std::string value) {
this->features[name] = value.empty()?"OFF":value;
}
int submit(std::string msg) {
@@ -445,6 +436,15 @@ public:
logger->print(LOGLEVEL_DEBUG, "C(%i) << %d %s", control_sock, code, msg.c_str());
}
void submit(int code, const std::vector<std::string>& msgs) {
for(int i = 0; i < msgs.size(); i++){
if (i+1 == msgs.size())
submit(std::to_string(code) + " " + msgs[i]);
else
submit(std::to_string(code) + "-" + msgs[i]);
}
}
int data_submit(char* out, size_t size) {
size_t total_sent = 0;
while (total_sent < size) {
@@ -514,7 +514,7 @@ public:
}
private:
std::map<std::string, bool> options;
std::map<std::string, std::string> features;
ClientAuthDetails* auth_data = new ClientAuthDetails{};
Filer* filer = {};
const int opt = 1;
@@ -634,6 +634,7 @@ private:
struct file_data fd;
std::string root;
std::string home = "/";
std::vector<std::string> auth_response;
if (auth->isChroot()) {
root = std::string(this->auth_data->home_dir);
logger->print(LOGLEVEL_INFO, "C(%i) Set chrooted root of '%s' to '%s'", control_sock, auth_data->username, root.c_str());
@@ -647,15 +648,16 @@ private:
((struct file_data)filer->setCWD(home)).error.code == 0
) {
state = FTP_STATE_AUTHED;
submit(230, "Login OK");
auth_response.push_back("Login OK");
std::string user_motd = getMotD();
if (user_motd.size() > 0) {
std::stringstream motd_stream(user_motd);
std::string line;
while(std::getline(motd_stream, line)) {
submit(230, line);
auth_response.push_back(line);
}
}
submit(230, auth_response);
return;
}
submit(530, "An error occured setting root and/or cwd");

View File

@@ -248,7 +248,9 @@ int main(int argc , char *argv[]) {
logger->openFileOnLevel(LOGLEVEL_ERROR, config->getValue("logging", "error", mainLogFile).c_str());
logger->openFileOnLevel(LOGLEVEL_CRITICAL, config->getValue("logging", "critical", mainLogFile).c_str());
if (config->getBool("net", "ssl", false)) {
bool ssl_enable = config->getBool("net", "ssl", true);
bool ssl_flags = SSL_OP_NO_TICKET;
if (ssl_enable) {
std::string cert_file = config->getValue("ssl", "certificate", "cert.pem");
if (cert_file[0] != '/')
cert_file = concatPath(std::string(CONFIG_DIR), cert_file);
@@ -264,7 +266,6 @@ int main(int argc , char *argv[]) {
// set configured ciphers
SSLManager::getInstance().setCiphers(config->getValue("ssl", "ciphers", "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH").c_str());
// handle configured flags
bool ssl_flags = SSL_OP_NO_TICKET;
if (!config->getBool("ssl", "ssl_v2", false))
ssl_flags+=SSL_OP_NO_SSLv2;
if (!config->getBool("ssl", "ssl_v3", false))
@@ -278,6 +279,15 @@ int main(int argc , char *argv[]) {
if (!config->getBool("ssl", "tls_v1_3", true))
ssl_flags+=SSL_OP_NO_TLSv1_3;
if (!(ssl_flags & SSL_OP_NO_SSLv2) || !(ssl_flags & SSL_OP_NO_SSLv3))
global_features.push_back("AUTH SSL");
if (!(ssl_flags & SSL_OP_NO_TLSv1) || !(ssl_flags & SSL_OP_NO_TLSv1_1) ||
!(ssl_flags & SSL_OP_NO_TLSv1_2) || !(ssl_flags & SSL_OP_NO_TLSv1_3))
global_features.push_back("AUTH TLS");
global_features.push_back("PBSZ");
global_features.push_back("PROT");
if ((ssl_flags & SSL_OP_NO_SSLv2) && (ssl_flags & SSL_OP_NO_SSLv3) &&
(ssl_flags & SSL_OP_NO_TLSv1) && (ssl_flags & SSL_OP_NO_TLSv1_1) &&
(ssl_flags & SSL_OP_NO_TLSv1_2) && (ssl_flags & SSL_OP_NO_TLSv1_3))
@@ -413,8 +423,8 @@ int main(int argc , char *argv[]) {
fds[slot].revents = 0;
fdc[slot].client = new Client(newsock);
fdc[slot].client->addOption("SIZE", true);
fdc[slot].client->addOption("UTF8", config->getBool("features", "utf8", true));
for (const auto &o : client_options)
fdc[slot].client->addFeature(o, "");
fdc[slot].thread = new std::thread(runClient, &fdc[slot]);
fdc[slot].close = false;

View File

@@ -25,5 +25,13 @@ Logger* logger;
static typename PluginTraits<Filer>::CreateFunc default_filer_factory = nullptr;
bool runServer;
bool runCompression;
std::vector<std::string> global_features = {
"SIZE",
"UTF8",
"PASV"
};
std::vector<std::string> client_options = {
"UTF8"
};
#endif