diff --git a/conf/ftp.conf b/conf/ftp.conf index 0f34f3f..1c1a096 100644 --- a/conf/ftp.conf +++ b/conf/ftp.conf @@ -3,14 +3,19 @@ ## This is the primary configuration file. Here is where you'll find all the ## necessary options to configure your FTP server. -## == String Formatting +#### Cheat Sheet +### String Variables ## These may be used in certain options to be runtime replaced. +## %a App Name ## %u Username -## %p Password ## %v Version ## %h Hostname - -## == Booleans +## %d Date +## %t Time +## Example: %a %v: Hi %u. It is %t on %d. +## digFTP v1.0.0: Hi user on example.com, your password is h4ckm3. +## +### Booleans ## true | false ## ----- | ----- ## 1 | 0 @@ -18,41 +23,38 @@ ## yes | no ## Anything unrecognized will be treated as 'false'. -## CORE OPTIONS -## These affect the very core of the server. [core] ## The name of the server it sends to the client. ## Syntax: server_name= -server_name=digFTP %v +## Default: %a %v +server_name=%a %v -## MotD command to post the output to clients after log in. -## WARNING: This method can be insecure. Use with caution. -## Syntax: motd_command= -#motd_command=cowsay -r Welcome %u. - -## MotD file to post to clients after log in. Overrides `motd_command`. -## Syntax: motd_file= -#motd_file=motd - -## MotD text to post to clients after log in. Overrides `motd_command` and -## `motd_file`. -## Syntax: motd_text= -#motd_text= +## Message of the Day to post to clients upon log in. +## Syntax: motd= +## motd=file: +## motd=cmd: +## WARNING: Commands could be dangerous. Use with caution. +#motd=cmd:cowsay -r Welcome %u. +#motd=file:ftp.motd +#motd=Welcome. ## Path to digFTP plugins ## Syntax: plugin_path= +## Default: /usr/lib/digftp/plugins plugin_path=/usr/lib/digftp/plugins -## Authentication Engine to use for logging in. -## Syntax: auth_engine= +[engines] +## These specify the interface to use for functions. +## Engines are provided through plugins. +## Syntax: = -auth_engine=local - -## Filer engine to use for the clients file system. -## Syntax: file_engine= -## Possible values are: local -filer_engine=local +## Engine used for authenticating users. +## Default: pam +auth=pam +## Engine used for handling the filesystem. +## Default: local +filer=local [net] ## Network address and port to listen on. @@ -60,6 +62,8 @@ filer_engine=local listen=127.0.0.1:21 ## Whether to support SSL. Server must be compiled with WITH_SSL=ON. +## This is highly recommended as it allows clients to secure their login +## credentials. SSL support is provided as explicit (AUTH command). ## Syntax: ssl= ssl=on @@ -67,9 +71,13 @@ ssl=on utf8=off [ssl] +## Configuration for FTPS (Explicit SSL) + certificate=cert.pem private_key=key.pem +## Ciphers to use for SSL. +## Default: ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH ciphers=ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH ssl_v2=no @@ -81,9 +89,10 @@ tls_v1_3=yes compression=yes prefer_server_ciphers=yes +#### LOGGING +## Filenames for loglevels. [logging] -## Log messages to file. Logging has multiple options pertaining to each -## loglevel. All values are paths. +## Log server output files. ## Syntax: [console|critical|error|warning|info|debug|all]= info=digftp.info.log error=digftp.error.log @@ -96,4 +105,7 @@ file=passdb ## Root of logged in user. Can use string formatting. ## Syntax: home_path= -home_path=/home/%u/ \ No newline at end of file +home_path=/home/%u/ + +[pam] +chroot=yes \ No newline at end of file diff --git a/conf/ftp.motd b/conf/ftp.motd new file mode 100644 index 0000000..18f2615 --- /dev/null +++ b/conf/ftp.motd @@ -0,0 +1 @@ +Welcome %u! \ No newline at end of file diff --git a/conf/motd b/conf/motd deleted file mode 100644 index 17e36db..0000000 --- a/conf/motd +++ /dev/null @@ -1 +0,0 @@ -Welcome to my FTP server. \ No newline at end of file diff --git a/src/auth.h b/src/auth.h index 8240a09..92a5f1a 100644 --- a/src/auth.h +++ b/src/auth.h @@ -7,6 +7,7 @@ #include #include "globals.h" +#include "build.h" #include "logger.h" #include "util.h" @@ -40,14 +41,15 @@ public: virtual bool authenticate(ClientAuthDetails* details) { return false; } virtual void setUserDirectory(ClientAuthDetails* details) { - std::string userdir = this->user_directory; - // Replace escaped var symbol with a dummy character - userdir = replace(userdir, std::string(CNF_PERCENTSYM_VAR), std::string(1, 0xFE)); - userdir = replace(userdir, std::string(CNF_USERNAME_VAR), std::string(details->username)); - userdir = replace(userdir, std::string(CNF_PASSWORD_VAR), std::string(details->password)); - userdir = replace(userdir, std::string(CNF_HOSTNAME_VAR), std::string(details->hostname)); - // Replace dummy character and return - userdir = replace(userdir, std::string(1, 0xFE), "%"); + std::map vars = { + {std::string(APPNAME_VAR), APPNAME}, + {std::string(VERSION_VAR), VERSION}, + {std::string(USERNAME_VAR), std::string(details->username)}, + {std::string(DATE_VAR), getCurrentUTCTime()}, + {std::string(TIME_VAR), getCurrentUTCDate()} + }; + + std::string userdir = conf_format(this->user_directory, vars); // Store the computed path in the details structure strncpy(details->home_dir, userdir.c_str(), sizeof(details->home_dir) - 1); diff --git a/src/client.cpp b/src/client.cpp index 91f9f6f..4b46169 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -36,7 +36,7 @@ public: return; } filer = default_filer_factory(); - submit(230, replace(server_name, CNF_VERSION_VAR, VERSION)); + submit(230, conf_format(server_name, (std::map){{std::string(APPNAME_VAR),std::string(APPNAME)},{std::string(VERSION_VAR),std::string(VERSION)}})); } void addOption(std::string name, bool toggle) { @@ -521,41 +521,16 @@ private: uint8_t flags = {}; int state = FTP_STATE_GUEST; + // Data int data_fd; int data_sock; struct sockaddr_in data_address; + // Rename (RNFR RNTO) std::string rename_from; bool rename_pending = false; - void authedInit() { - logger->print(LOGLEVEL_INFO, "C(%i) logging in as '%s'", control_sock, auth_data->username); - struct file_data fd; - if (auth->isChroot()) { - if ( - ((struct file_data)filer->setRoot(std::string(this->auth_data->home_dir))).error.code == 0 && - ((struct file_data)filer->setCWD("/")).error.code == 0 - ) { - state = FTP_STATE_AUTHED; - submit(230, "Login OK"); - logger->print(LOGLEVEL_INFO, "C(%i) Set chrooted root of '%s' to '%s'", control_sock, auth_data->username, filer->getRoot().c_str()); - return; - } - } else { - if ( - ((struct file_data)filer->setRoot("/")).error.code == 0 && - ((struct file_data)filer->setCWD(std::string(this->auth_data->home_dir))).error.code == 0 - ) { - state = FTP_STATE_AUTHED; - submit(230, "Login OK"); - logger->print(LOGLEVEL_INFO, "C(%i) Set home of '%s' to '%s'", control_sock, auth_data->username, filer->getCWD().c_str()); - return; - } - } - submit(530, "An error occured setting root and/or cwd"); - return; - } - + // SSL SSL* control_ssl; SSL* data_ssl; SSL_SESSION* cached_session = nullptr; @@ -653,4 +628,82 @@ private: return true; } + + void authedInit() { + logger->print(LOGLEVEL_INFO, "C(%i) logging in as '%s'", control_sock, auth_data->username); + struct file_data fd; + std::string root; + std::string home = "/"; + 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()); + } else { + root = "/"; + home = std::string(this->auth_data->home_dir); + logger->print(LOGLEVEL_INFO, "C(%i) Set home of '%s' to '%s'", control_sock, auth_data->username, home.c_str()); + } + if ( + ((struct file_data)filer->setRoot(root)).error.code == 0 && + ((struct file_data)filer->setCWD(home)).error.code == 0 + ) { + state = FTP_STATE_AUTHED; + submit(230, "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); + } + } + return; + } + submit(530, "An error occured setting root and/or cwd"); + return; + } + + std::string getMotD() { + std::string new_motd; + + std::map vars = { + {std::string(APPNAME_VAR), APPNAME}, + {std::string(VERSION_VAR), VERSION}, + {std::string(USERNAME_VAR), auth_data ? auth_data->username : ""}, + {std::string(DATE_VAR), getCurrentUTCTime()}, + {std::string(TIME_VAR), getCurrentUTCDate()} + }; + + if (motd.rfind("cmd:", 0) == 0) { + std::string cmd = conf_format(motd.substr(4), vars); + + FILE* pipe = popen(cmd.c_str(), "r"); + if (!pipe) { + logger->print(LOGLEVEL_ERROR, "Failed to execute MOTD command: %s", cmd.c_str()); + return ""; + } + + char buffer[128]; + std::string result; + while (fgets(buffer, sizeof(buffer), pipe) != nullptr) { + result += buffer; + } + + pclose(pipe); + return result; + } else if (motd.rfind("file:", 0) == 0) { + std::string path = motd.substr(5); + + std::ifstream file(path); + if (!file) { + logger->print(LOGLEVEL_ERROR, "Failed to open MOTD file: %s", path.c_str()); + return ""; + } + + std::stringstream buffer; + buffer << file.rdbuf(); + return conf_format(buffer.str(), vars); + } else { + return conf_format(new_motd, vars); + } + } }; diff --git a/src/filer.h b/src/filer.h index 36a67b2..216def6 100644 --- a/src/filer.h +++ b/src/filer.h @@ -36,6 +36,8 @@ class Filer { public: virtual ~Filer() {}; + virtual bool initialize(const std::map& config) { return true; } + virtual file_data setRoot(std::string _root) = 0; virtual file_data setCWD(std::string _cwd) = 0; virtual std::filesystem::path getRoot() = 0; diff --git a/src/globals.h b/src/globals.h index 9a2e7bb..8c9c4a2 100644 --- a/src/globals.h +++ b/src/globals.h @@ -1,10 +1,12 @@ #ifndef GLOBALS_H #define GLOBALS_H -#define CNF_PERCENTSYM_VAR "%%" -#define CNF_VERSION_VAR "%v" -#define CNF_USERNAME_VAR "%u" -#define CNF_PASSWORD_VAR "%p" -#define CNF_HOSTNAME_VAR "%h" +#define APPNAME_VAR "a" +#define VERSION_VAR "v" +#define USERNAME_VAR "u" +#define PASSWORD_VAR "p" +#define HOSTNAME_VAR "h" +#define DATE_VAR "d" +#define TIME_VAR "t" #endif \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index c91e5a1..a08cced 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -183,29 +183,24 @@ void runClient(struct ftpconn* cfd) { void initializePlugins() { std::string plugin_dir = config->getValue("core", "plugin_path", PLUGIN_DIR); - // Load auth plugins auto& auth_manager = PluginManager::getInstance(); - auth_manager.setLogger(logger); - - // Load filer plugins auto& filer_manager = PluginManager::getInstance(); + + auth_manager.setLogger(logger); filer_manager.setLogger(logger); + // Try loading all plugins into both managers for (const auto& entry : std::filesystem::directory_iterator(plugin_dir)) { if (entry.path().extension() == ".so") { - const std::string& filename = entry.path().filename().string(); - if (filename.find(PluginTraits::pluginPrefix()) == 0) { - auth_manager.loadPlugin(entry.path().string()); - } - else if (filename.find(PluginTraits::pluginPrefix()) == 0) { - filer_manager.loadPlugin(entry.path().string()); - } + logger->print(LOGLEVEL_DEBUG, "Loading plugin: %s", entry.path().c_str()); + auth_manager.loadPlugin(entry.path().string()); + filer_manager.loadPlugin(entry.path().string()); } } // Initialize auth - std::string auth_type = config->getValue("core", "auth_engine", "pam"); - auth = auth_manager.createPlugin(auth_type); + std::string auth_type = config->getValue("engines", "auth", "pam"); + auth = auth_manager.createPlugin(auth_type, config->get(auth_type)->get()); if (!auth) { logger->print(LOGLEVEL_CRITICAL, "Failed to create auth engine: %s", auth_type.c_str()); @@ -213,7 +208,7 @@ void initializePlugins() { } // Initialize filer - std::string filer_type = config->getValue("core", "filer_engine", "local"); + std::string filer_type = config->getValue("engines", "filer", "local"); default_filer_factory = filer_manager.getFactory(filer_type); if (!default_filer_factory) { @@ -231,7 +226,8 @@ int main(int argc , char *argv[]) { signal(SIGPIPE, SIG_IGN); config = new ConfigFile(concatPath(std::string(CONFIG_DIR), "ftp.conf")); - server_name = config->getValue("core", "server_name", "digFTP %v"); + server_name = config->getValue("core", "server_name", "%a %v"); + motd = config->getValue("core", "motd", "cmd:cowsay -r Welcome %u!"); sscanf( config->getValue("net", "listen", "127.0.0.1:21").c_str(), diff --git a/src/plugin.h b/src/plugin.h index 787cbf9..28edb01 100644 --- a/src/plugin.h +++ b/src/plugin.h @@ -6,6 +6,7 @@ typedef int (*GetAPIVersionFunc)(); typedef void (*SetLoggerFunc)(Logger*); +typedef const char* (*GetPluginTypeFunc)(); class IPlugin { public: diff --git a/src/plugin_manager.h b/src/plugin_manager.h index 8cdc7a3..0915aaf 100644 --- a/src/plugin_manager.h +++ b/src/plugin_manager.h @@ -1,43 +1,49 @@ #ifndef PLUGIN_MANAGER_H #define PLUGIN_MANAGER_H - #include #include #include #include "plugin.h" +class IPluginManager { +public: + virtual ~IPluginManager() = default; + virtual void setLogger(Logger* log) = 0; + virtual bool loadPlugin(const std::string& path) = 0; +}; + template -class PluginManager { +class PluginManager : public IPluginManager { public: static PluginManager& getInstance() { static PluginManager instance; return instance; } - void setLogger(Logger* log) { - logger = log; + void setLogger(Logger* log) override { IPlugin::setLogger(log); + logger = log; } - T* createPlugin(const std::string& type) { + T* createPlugin(const std::string& type, const std::map& config) { auto it = plugins.find(type); if (it != plugins.end()) { - return it->second.create(); + T* plugin = it->second.create(); + if (plugin) plugin->initialize(config); + return plugin; } if (logger) logger->print(LOGLEVEL_ERROR, "Plugin type '%s' not found", type.c_str()); return nullptr; } bool loadPlugin(const std::string& path) { - if (logger) logger->print(LOGLEVEL_DEBUG, "Loading plugin: %s", path.c_str()); - void* handle = dlopen(path.c_str(), RTLD_LAZY); if (!handle) { if (logger) logger->print(LOGLEVEL_ERROR, "Failed to load plugin %s: %s", path.c_str(), dlerror()); return false; } - + // Load common functions auto create = (typename PluginTraits::CreateFunc)dlsym(handle, PluginTraits::createFuncName()); auto destroy = (typename PluginTraits::DestroyFunc)dlsym(handle, PluginTraits::destroyFuncName()); @@ -46,32 +52,44 @@ public: auto get_name = (const char* (*)())dlsym(handle, "getPluginName"); auto get_desc = (const char* (*)())dlsym(handle, "getPluginDescription"); auto get_ver = (const char* (*)())dlsym(handle, "getPluginVersion"); - - if (!checkRequiredFunctions(create, destroy, get_api_version, setLogger, get_name, get_desc, get_ver)) { + + // If create function isn't found, this plugin doesn't implement our interface + if (!create) { + dlclose(handle); + return true; // Not an error, just not our type + } + + // Check all required functions are present + if (!checkRequiredFunctions(create, destroy, get_api_version, setLogger, + get_name, get_desc, get_ver)) { dlclose(handle); return false; } - + + // Check API version compatibility if (!checkAPIVersion(get_api_version(), path)) { dlclose(handle); return false; } - + std::string plugin_name = get_name(); setLogger(logger); - + + // Check if already loaded if (plugins.find(plugin_name) != plugins.end()) { if (logger) logger->print(LOGLEVEL_DEBUG, "Plugin %s is already loaded", plugin_name.c_str()); dlclose(handle); return true; } - + + // Create and store plugin info auto plugin_info = createPluginInfo(plugin_name, get_desc(), get_ver(), create, destroy, - get_api_version, setLogger, handle); + get_api_version, setLogger, handle); plugins[plugin_name] = plugin_info; - if (logger) logger->print(LOGLEVEL_INFO, "Loaded plugin: %s v%s", - plugin_name.c_str(), plugin_info.version.c_str()); + if (logger) logger->print(LOGLEVEL_INFO, "Loaded plugin: %s v%s (%s interface)", + plugin_name.c_str(), plugin_info.version.c_str(), + PluginTraits::interfaceName()); return true; } diff --git a/src/plugin_traits.h b/src/plugin_traits.h index 054096b..aae916a 100644 --- a/src/plugin_traits.h +++ b/src/plugin_traits.h @@ -30,7 +30,7 @@ struct PluginTraits { static const char* createFuncName() { return "createAuthPlugin"; } static const char* destroyFuncName() { return "destroyAuthPlugin"; } - static const char* pluginPrefix() { return "libauth_"; } + static const char* interfaceName() { return "Auth"; } }; template<> @@ -42,7 +42,7 @@ struct PluginTraits { static const char* createFuncName() { return "createFilerPlugin"; } static const char* destroyFuncName() { return "destroyFilerPlugin"; } - static const char* pluginPrefix() { return "libfiler_"; } + static const char* interfaceName() { return "Filer"; } }; #endif \ No newline at end of file diff --git a/src/util.h b/src/util.h index 512eff8..057aff6 100644 --- a/src/util.h +++ b/src/util.h @@ -9,6 +9,7 @@ #include #include #include +#include std::string replace(std::string subject, const std::string& search, const std::string& replace) { size_t pos = 0; @@ -19,6 +20,55 @@ std::string replace(std::string subject, const std::string& search, const std::s return subject; } +std::string conf_format(const std::string& fmt, const std::map& vars) { + std::string result; + result.reserve(fmt.length()); + + for (size_t i = 0; i < fmt.length(); ++i) { + if (fmt[i] != '%') { + result += fmt[i]; + continue; + } + + // Handle %% -> % + if (i + 1 < fmt.length() && fmt[i + 1] == '%') { + result += '%'; + i++; + continue; + } + + // Look for variable name + if (i + 1 < fmt.length()) { + size_t start = i + 1; + size_t end = start; + + // Find the end of the variable name + while (end < fmt.length() && + (isalnum(fmt[end]) || fmt[end] == '_')) { + end++; + } + + if (end > start) { + std::string var = fmt.substr(start, end - start); + auto it = vars.find(var); + if (it != vars.end()) { + result += it->second; + } else { + // Keep the original %var if not found + result += fmt.substr(i, end - i); + } + i = end - 1; + continue; + } + } + + // If we get here, it's a single % with no variable + result += '%'; + } + + return result; +} + template void split(const std::string &s, char delim, Out result, int limit) { int it = 0; @@ -133,6 +183,22 @@ std::string concatPath(std::string dir1, std::string dir2) { return std::filesystem::weakly_canonical(dir1+"/"+dir2).string(); } +std::string getCurrentUTCTime() { + auto now = std::chrono::system_clock::now(); + auto time = std::chrono::system_clock::to_time_t(now); + std::string result(9, '\0'); + strftime(&result[0], result.size(), "%H:%M:%S", gmtime(&time)); + return result; +} + +std::string getCurrentUTCDate() { + auto now = std::chrono::system_clock::now(); + auto time = std::chrono::system_clock::to_time_t(now); + std::string result(11, '\0'); + strftime(&result[0], result.size(), "%Y-%m-%d", gmtime(&time)); + return result; +} + static char* trim(char *str) { char *end; while(isspace(*str))