diff --git a/CMakeLists.txt b/CMakeLists.txt index 864dde8..c6aab64 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,10 +1,12 @@ -cmake_minimum_required(VERSION 3.26) -PROJECT(worlioftp) +CMAKE_MINIMUM_REQUIRED(VERSION 3.26) +PROJECT(digftp) SET(CMAKE_CXX_STANDARD 20) - -#SET(CMAKE_VERBOSE_MAKEFILE ON) - -#ADD_DEFINITIONS(-Wall -O2) +SET(CMAKE_CXX_STANDARD_REQUIRED True) +SET(CMAKE_CXX_FLAGS "-O3") +SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) +INCLUDE_DIRECTORIES(${CMAKE_BINARY_DIR}) ADD_SUBDIRECTORY(src) +FILE(COPY conf DESTINATION ${CMAKE_BINARY_DIR}) +INSTALL(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION bin) \ No newline at end of file diff --git a/conf/ftp.conf b/conf/ftp.conf new file mode 100644 index 0000000..85ccd42 --- /dev/null +++ b/conf/ftp.conf @@ -0,0 +1,15 @@ +[main] +server_name=My FTP Server +motd_file=motd +auth_engine=noauth +[net] +listen_address=127.0.0.1 +control_port=21 +max_clients=255 +[noauth] +user_root=/home/%u/ +chroot=1 +[passdb] +file=conf/passdb +user_root=/home/%u/ +case_sensitive=1 \ No newline at end of file diff --git a/conf/motd b/conf/motd new file mode 100644 index 0000000..17e36db --- /dev/null +++ b/conf/motd @@ -0,0 +1 @@ +Welcome to my FTP server. \ No newline at end of file diff --git a/conf/passdb b/conf/passdb new file mode 100644 index 0000000..bd9aed6 --- /dev/null +++ b/conf/passdb @@ -0,0 +1 @@ +anonymous:anonymous@ \ No newline at end of file diff --git a/conf/worlio-ftp.conf b/conf/worlio-ftp.conf deleted file mode 100644 index 397a0cf..0000000 --- a/conf/worlio-ftp.conf +++ /dev/null @@ -1,14 +0,0 @@ -[general] -name=Worlio FTP -contact=admin@localhost -location=US -motd=Welcome! -version=0.0 -path=/data/ftp-test - -[network] -address=0.0.0.0 -port=21 -data-port=20 -active=true -passive=true \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 91cb6c5..2a42749 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,4 +1,4 @@ add_executable( - worlioftp + ${PROJECT_NAME} main.cpp ) diff --git a/src/auth.h b/src/auth.h new file mode 100644 index 0000000..c2f7485 --- /dev/null +++ b/src/auth.h @@ -0,0 +1,27 @@ +#ifndef AUTHMETHODS_H +#define AUTHMETHODS_H +#include + +class Auth { +public: + Auth() {}; + + virtual void setOptions(ConfigSection* options) {}; + + virtual bool isChroot() { return false; }; + + virtual bool check(std::string username, std::string password) { return false; }; + + virtual std::string getUserDirectory(std::string username) { return ""; }; +}; + +#include "auth/noauth.h" +#include "auth/passdb.h" + +Auth* getAuthByName(std::string name) { + if (name == "noauth") return new NoAuth(); + if (name == "passdb") return new PassdbAuth(); + return new Auth(); +} + +#endif \ No newline at end of file diff --git a/src/auth/noauth.h b/src/auth/noauth.h new file mode 100644 index 0000000..3db77ca --- /dev/null +++ b/src/auth/noauth.h @@ -0,0 +1,32 @@ +#ifndef _AUTH_NOAUTH +#define _AUTH_NOAUTH +#include "../util.h" + +class NoAuth : public Auth { +public: + NoAuth() {}; + + void setOptions(ConfigSection* options) { + this->options = options; + }; + + ConfigSection* getOptions() { + return options; + } + + bool isChroot() { + return options->getInt("chroot", 1) == 1; + }; + + bool check(std::string username, std::string password) { return true; }; + + std::string getUserDirectory(std::string username) { + std::string authDir = options->getValue("user_root", "/home/%u/"); + authDir = replace(authDir, "%u", username); + return authDir; + }; +private: + ConfigSection* options; +}; + +#endif \ No newline at end of file diff --git a/src/auth/passdb.h b/src/auth/passdb.h new file mode 100644 index 0000000..464b035 --- /dev/null +++ b/src/auth/passdb.h @@ -0,0 +1,43 @@ +#ifndef _AUTH_PASSDB +#define _AUTH_PASSDB +#include "../util.h" + +class PassdbAuth : public Auth { +public: + PassdbAuth() {}; + + void setOptions(ConfigSection* options) { + std::ifstream file(options->getValue("file", "passdb")); + std::string line; + while (std::getline(file, line)) { + if (line.length() == 0) continue; + else if (line.rfind("#", 0) == 0) continue; + else { + std::string username = line.substr(0, line.find(":")); + if (options->getInt("case_sensitive", 1) != 0) + username = toLower(username); + userlist[username] = line.substr(line.find(":")+1, line.length()); + } + } + userRoot = options->getValue("user_root", "/home/%u/"); + }; + + bool isChroot() { return true; }; + + bool check(std::string username, std::string password) { + if (userlist.find(username) == userlist.end()) return false; + if (userlist[username] != password) return false; + return true; + }; + + std::string getUserDirectory(std::string username) { + std::string authDir = userRoot; + authDir = replace(authDir, "%u", username); + return authDir; + }; +private: + std::map userlist; + std::string userRoot; +}; + +#endif \ No newline at end of file diff --git a/src/client.cpp b/src/client.cpp index 53fd0c1..f741f70 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -10,6 +10,7 @@ #include #include +#include "main.h" #include "server.h" #include "filer.cpp" #include "util.h" @@ -30,7 +31,7 @@ public: Client(int &_sock) { control_sock = _sock; - submit(230, "Init"); + submit(230, config->getValue("main", "server_name", "digFTP Server")); }; int receive(std::string cmd, std::string argstr) { @@ -44,29 +45,17 @@ public: submit(257, "'"+filer->cwd.string()+"'"); } else if (cmd == "CWD") { int rc = filer->traverse(argstr); - if (rc == 0) { - submit(250, "OK"); - } else if (rc == -1) { - submit(500, "Not found!"); - } else { - submit(500, std::to_string(rc)); - } + if (rc == 0) submit(250, "OK"); + else submit(431, "No such directory"); } else if (cmd == "CDUP") { int rc = filer->traverse(".."); - if (rc == 0) { - submit(250, "OK"); - } else if (rc == -1) { - submit(500, "Not found!"); - } else { - submit(500, std::to_string(rc)); - } + if (rc == 0) submit(250, "OK"); + else submit(431, "No such directory"); } else if (cmd == "MKD") { int rc = filer->createDirectory(argstr); - if (rc == 0) { - submit(250, "OK"); - } else { - submit(500, std::to_string(rc)); - } + if (rc == 0) submit(257, "\""+filer->relPath(argstr).string()+"\" directory created"); + else if (rc == -1) submit(521, "Directory already exists"); + else submit(550, "Access Denied"); } else if (cmd == "TYPE") { sscanf(argstr.c_str(), "%c", &(filer->type)); submit(226, "OK"); @@ -104,10 +93,7 @@ public: } if (getsockname(data_fd, (struct sockaddr *)&data_address, &datalen) == 0) { - //memcpy(&netaddr[0], &data_address.sin_addr, 4); - netaddr[0] = 127; - netaddr[1] = netaddr[2] = 0; - netaddr[3] = 1; + sscanf(config->getValue("net", "listen_address", "127.0.0.1").c_str(), "%d.%d.%d.%d", &netaddr[0], &netaddr[1], &netaddr[2], &netaddr[3]); dataport = ntohs(data_address.sin_port); memcpy(&netport[0], &dataport, 2); printf("[i] D(%i) PASV initialized: %u.%u.%u.%u:%u\n", data_fd, netaddr[0], netaddr[1], netaddr[2], netaddr[3], dataport); @@ -120,7 +106,7 @@ public: char* pasvok; asprintf( &pasvok, - "Entering Passive Mode (%u,%u,%u,%u,%u,%u)", + "Entering Passive Mode (%u,%u,%u,%u,%u,%u).", netaddr[0], netaddr[1], netaddr[2], @@ -135,7 +121,6 @@ public: if ((data_sock = accept(data_fd, NULL, NULL)) >= 0) { printf("[i] D(%i) PASV accepted: %i\n", data_fd, data_sock); state = 2; - //submit(200, "OK"); } else { perror("accept() failed"); submit(425, "Unknown Error"); @@ -177,7 +162,7 @@ public: } else if (cmd == "DELE" || cmd == "RMD") { int ret = filer->deleteFile(argstr); if (ret == 0) { - submit(226, "OK"); + submit(250, "OK"); } else { submit(550, "Access Denied "+std::to_string(ret)); } @@ -197,13 +182,12 @@ public: if (fd.ecode == 0) { submit(150, "Transferring"); data_submit(fd.data, fd.size); + free(fd.data); + submit(226, "OK"); } else { if (fd.ecode == -3) submit(550, "I refuse to transmit in ASCII mode!"); else submit(550, "Access Denied"); } - // Segfault on freeing ASCII data, memory leak on binary. - if (filer->type != 'A') free(fd.data); - submit(226, "OK"); } else if (cmd == "STOR") { unsigned char inbuf[BUFFERSIZE] = {0}; submit(150, "Transferring"); @@ -232,20 +216,21 @@ public: name = argstr; submit(331, "Password required"); } else if (cmd == "PASS") { - if (argstr == "123") { + if (auth->check(name, argstr)) { printf("[d] (%i) logged in as '%s'\n", control_sock, name.c_str()); // We can now set the root safely (I hope). - filer->setRoot("/data/ftp-test/"+name); + filer->setRoot(auth->getUserDirectory(name)); state = 1; - submit(230, "Login successful"); + submit(230, "Login OK"); } else { - submit(530, "Authentication refused."); - return -1; + name = ""; + submit(530, "Invalid Credentials."); } } else if (cmd == "AUTH") { submit(502, "Command not implemented"); } else { - submit(332, "Login Required"); + submit(332, "Not Logged In!"); + return -1; } } return 0; @@ -291,4 +276,4 @@ private: int data_fd; int data_sock; struct sockaddr_in data_address; -}; \ No newline at end of file +}; diff --git a/src/conf.h b/src/conf.h new file mode 100644 index 0000000..2e32dff --- /dev/null +++ b/src/conf.h @@ -0,0 +1,116 @@ +#ifndef CONF_H +#define CONF_H +#include +#include +#include +#include + +class ConfigSection { +public: + ConfigSection() {}; + ConfigSection(std::map options) { + this->options = options; + } + + std::map get() { + return this->options; + } + + std::vector getKeyValues() { + std::vector values; + for (auto const& c : this->options) { + values.push_back(c.first); + } + return values; + } + + std::string getValue(std::string name, std::string def) { + if (this->options.find(name) != this->options.end()) return this->options[name]; + else return def; + } + + int getInt(std::string name, int def) { + if (this->options.find(name) != this->options.end()) return std::stoi(this->options[name]); + else return def; + } + + void setValue(std::string name, std::string value) { + this->options[name] = value; + } + + void setInt(std::string name, int value) { + this->options[name] = std::to_string(value); + } +private: + std::map options; +}; + +class ConfigFile { +public: + ConfigFile() {}; + ConfigFile(std::string path) { + std::ifstream file(path); + ConfigSection* section; + std::string line; + while (std::getline(file, line)) { + if (line.length() == 0) continue; + else if (line.rfind("#", 0) == 0) continue; + else if (line.rfind("[", 0) == 0) { + section = new ConfigSection(); + this->sections[line.substr(1, line.find("]")-1)] = section; + } else if (line.find("=") != std::string::npos) { + section->setValue(line.substr(0, line.find("=")), line.substr(line.find("=")+1, line.length())); + } + } + printf("Configuration loaded: \"%s\"\n", path.c_str()); + } + + std::map get() { + return this->sections; + } + + ConfigSection* get(std::string section) { + return this->sections[section]; + } + + std::vector getKeyValues() { + std::vector values; + for (auto const& c : this->sections) { + values.push_back(c.first); + } + return values; + } + + std::string getValue(std::string section, std::string name, std::string def) { + if (this->sections[section]->get().find(name) != this->sections[section]->get().end()) return this->sections[section]->getValue(name, def); + else return def; + } + + int getInt(std::string section, std::string name, int def) { + if (this->sections[section]->get().find(name) != this->sections[section]->get().end()) return this->sections[section]->getInt(name, def); + else return def; + } + + void setValue(std::string section, std::string name, std::string value) { + this->sections[section]->setValue(name, value); + } + + void setInt(std::string section, std::string name, int value) { + this->sections[section]->setInt(name, value); + } + + void write(std::string path) { + std::ofstream outfile(path); + for (std::map::iterator si = this->sections.begin(); si != this->sections.end(); ++si) { + outfile << "[" << si->first << "]" << std::endl; + for (std::map::iterator it = si->second->get().begin(); it != si->second->get().end(); ++it) { + outfile << it->first << "=" << it->second << std::endl; + } + } + outfile.close(); + } +private: + std::map sections; +}; + +#endif \ No newline at end of file diff --git a/src/filer.cpp b/src/filer.cpp index 01cf5e4..f6039cd 100644 --- a/src/filer.cpp +++ b/src/filer.cpp @@ -78,6 +78,14 @@ public: return fs::weakly_canonical(root.string()+"/"+(cwd / in).string()); } + fs::path relPath() { + return fs::weakly_canonical("/"+cwd.string()); + } + + fs::path relPath(std::string in) { + return fs::weakly_canonical("/"+(cwd / in).string()); + } + int fileSize(std::string name) { fs::path nfile = fs::weakly_canonical(cwd / name); fs::path ffile = fullPath(name); @@ -114,19 +122,7 @@ public: printf("[i] Retreiving file: %s\n", nfile.c_str()); if (ffile.string().rfind(root.string(), 0) != 0) fd.ecode = -2; else if (!fs::exists(ffile)) fd.ecode = -1; - else if (type == 'A') { - std::ifstream infile(ffile, std::ios::in|std::ios::ate); - if (infile.is_open()) { - std::string filedata; - for (std::string line; getline(infile, line);) { - filedata+=(line+"\r\n"); - } - fd.size = filedata.size(); - strncpy(fd.data, filedata.c_str(), fd.size); - infile.close(); - fd.ecode = 0; - } else fd.ecode = -2; - } else { + else if (type != 'A') { std::ifstream infile(ffile, std::ios::in|std::ios::binary|std::ios::ate); if (infile.is_open()) { fd.size = infile.tellg(); @@ -136,7 +132,7 @@ public: infile.close(); fd.ecode = 0; } else fd.ecode = -2; - } + } else fd.ecode = -3; return fd; } @@ -153,7 +149,6 @@ public: outfile.write((char *)data, size); outfile.close(); return size; - } return 0; } @@ -167,7 +162,6 @@ public: outfile.write((char *)data, size); outfile.close(); return size; - } return 0; } diff --git a/src/main.cpp b/src/main.cpp index d15cbda..b78f16e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -15,67 +15,73 @@ #include #include +#include "main.h" #include "server.h" #include "client.cpp" #include "util.h" using namespace std::chrono_literals; -// I stole the majority* of this multi-client sock code from IBM -// of all places. Had to still modify it for my own uses but -// laziness wins again! +const int max_clients = config->getInt("net", "max_clients", 255); +const int cport = config->getInt("net", "control_port", 21); -struct pollfd fds[MAXCLIENTS]; +struct pollfd fds[65535]; struct clientfd { Client* client; bool close = false; -} fdc[MAXCLIENTS]; +} fdc[65535]; bool run = true, compress_array = false; void runClient(struct clientfd* cfd) { char inbuf[BUFFERSIZE]; - printf("[d] C(%i) Initialized\n", cfd->client->control_sock); + //printf("[d] C(%i) Initialized\n", cfd->client->control_sock); // Loop as long as it is a valid file descriptor. try { - while (fcntl(cfd->client->control_sock, F_GETFD) != -1) { - printf("[d] C(%i) Attempting read...\n", cfd->client->control_sock); - int rc = recv(cfd->client->control_sock, inbuf, sizeof(inbuf), 0); - if (rc < 0) { - if (errno != EWOULDBLOCK) { - perror("recv() failed"); + //while (fcntl(cfd->client->control_sock, F_GETFD) != -1) { + while (true) { + if (cfd->client == nullptr) { break; } + printf("[d] C(%i) Attempting read...\n", cfd->client->control_sock); + int rc = recv(cfd->client->control_sock, inbuf, sizeof(inbuf), 0); + if (rc < 0) { + printf("[d] C(%i) Recieved empty packet\n", cfd->client->control_sock); + if (errno != EWOULDBLOCK) { + perror("recv() failed"); + break; + } + continue; + } + + if (rc == 0 || cfd->client == nullptr) { + //printf("[d] C(%i) closed\n", cfd->client->control_sock); break; } - continue; - } - - if (rc == 0 || cfd->client == nullptr) { - printf("[d] C(%i) closed\n", cfd->client->control_sock); - break; - } - - std::string lin(inbuf); - int len = lin.find("\r\n", 0); - int cmdend = lin.find(" ", 0); - if (cmdend >= len || cmdend == std::string::npos) cmdend = len; - std::string cmd = toUpper(lin.substr(0, cmdend)); - std::string args = ""; - if (len > cmdend) args = lin.substr(cmdend+1, len-cmdend-1); - printf("[d] C(%i) >> '%s' '%s'\n", cfd->client->control_sock, cmd.c_str(), args.c_str()); - - if (cfd->client->receive(cmd, args) < 0) break; - inbuf[0] = '\0'; - } + std::string lin(inbuf); + int len = lin.find("\r\n", 0); + int cmdend = lin.find(" ", 0); + if (cmdend >= len || cmdend == std::string::npos) cmdend = len; + std::string cmd = toUpper(lin.substr(0, cmdend)); + std::string args = ""; + if (len > cmdend) args = lin.substr(cmdend+1, len-cmdend-1); + + printf("[d] C(%i) >> '%s' '%s'\n", cfd->client->control_sock, cmd.c_str(), args.c_str()); + + if (cfd->client->receive(cmd, args) < 0) break; + inbuf[0] = '\0'; + } } catch (...) { printf("[!] C(%i) Caught error!\n", cfd->client->control_sock); } - cfd->client->thread.detach(); - printf("[d] C(%i) Marking for deletion...\n", cfd->client->control_sock); + //printf("[d] C(%i) Marking for deletion...\n", cfd->client->control_sock); cfd->close = true; } int main(int argc , char *argv[]) { + std::string authType = config->getValue("main", "auth_engine", "plain"); + auth = getAuthByName(authType); + auth->setOptions(config->get(authType)); + int opt = 1, master_socket = -1, newsock = -1, @@ -103,7 +109,7 @@ int main(int argc , char *argv[]) { ctrl_address.sin_family = AF_INET; ctrl_address.sin_addr.s_addr = INADDR_ANY; - ctrl_address.sin_port = htons(CONTROL_PORT); + ctrl_address.sin_port = htons(cport); if (bind(master_socket, (struct sockaddr *)&ctrl_address, sizeof(ctrl_address))<0 < 0) { perror("bind() failed"); @@ -117,8 +123,8 @@ int main(int argc , char *argv[]) { exit(-1); } - memset(fds, 0 , sizeof(fds)); - memset(fdc, 0 , sizeof(fdc)); + memset(fds, 0, sizeof(fds)); + memset(fdc, 0, sizeof(fdc)); fds[0].fd = master_socket; fds[0].events = POLLIN; @@ -146,7 +152,6 @@ int main(int argc , char *argv[]) { printf("[!] C(%i) Error! revents = %d\n", fds[i].fd, fds[i].revents); goto conn_close; } - if (fds[i].fd == master_socket) { do { newsock = accept(master_socket, NULL, NULL); @@ -177,10 +182,11 @@ int main(int argc , char *argv[]) { fdc[i].client = nullptr; fdc[i].close = false; compress_array = true; + } else { + fds[i].revents = 0; } } } - if (compress_array) { compress_array = false; for (int i = 0; i < nfds; i++) { @@ -189,7 +195,13 @@ int main(int argc , char *argv[]) { if (fds[j].fd == -1) { printf("[d] Compressing: %i(fd:%i) <= %i(fd:%i)\n", j, fds[j].fd, j+1, fds[j+1].fd); fds[j].fd = fds[j+1].fd; + fds[j].revents = fds[j+1].revents; + fds[j+1].fd = -1; + printf("[d] Reinitialized fds of %i\n", j+1); fdc[j].client = fdc[j+1].client; + fdc[j].close = fdc[j+1].close; + fdc[j+1] = {}; + printf("[d] Reinitialized fdc of %i\n", j+1); } } i--; diff --git a/src/main.h b/src/main.h new file mode 100644 index 0000000..f31a0a1 --- /dev/null +++ b/src/main.h @@ -0,0 +1,10 @@ +#ifndef MAIN_H +#define MAIN_H +#include "conf.h" +#include "auth.h" +#include "auth/noauth.h" + +ConfigFile* config = new ConfigFile("conf/ftp.conf"); +Auth* auth; + +#endif \ No newline at end of file diff --git a/src/server.h b/src/server.h index a64dafe..949cb53 100644 --- a/src/server.h +++ b/src/server.h @@ -1,13 +1,8 @@ #ifndef SERVER_H #define SERVER_H -// BUNCH OF BULLSHIT -#define APPNAME "tunnelFTP (Worlio Bridge)" +#define APPNAME "digFTP" #define APPVER "0.0" -#define CONTROL_PORT 21 -#define DATA_PORT 20 #define BUFFERSIZE 1024*512 -#define MAXCLIENTS 256 -#define BASEDIR "/data/ftp-test" #endif \ No newline at end of file diff --git a/src/util.h b/src/util.h index cfa04b6..678e40c 100644 --- a/src/util.h +++ b/src/util.h @@ -9,6 +9,15 @@ #include #include +std::string replace(std::string subject, const std::string& search, const std::string& replace) { + size_t pos = 0; + while ((pos = subject.find(search, pos)) != std::string::npos) { + subject.replace(pos, search.length(), replace); + pos += replace.length(); + } + return subject; +} + template void split(const std::string &s, char delim, Out result, int limit) { int it = 0;