Add MotD support.

Fixed config up.
Better handling of config variables
This commit is contained in:
2024-12-15 08:14:48 -06:00
parent bc88a30c0c
commit 28efbfb185
12 changed files with 261 additions and 109 deletions

View File

@@ -3,14 +3,19 @@
## This is the primary configuration file. Here is where you'll find all the ## This is the primary configuration file. Here is where you'll find all the
## necessary options to configure your FTP server. ## necessary options to configure your FTP server.
## == String Formatting #### Cheat Sheet
### String Variables
## These may be used in certain options to be runtime replaced. ## These may be used in certain options to be runtime replaced.
## %a App Name
## %u Username ## %u Username
## %p Password
## %v Version ## %v Version
## %h Hostname ## %h Hostname
## %d Date
## == Booleans ## %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 ## true | false
## ----- | ----- ## ----- | -----
## 1 | 0 ## 1 | 0
@@ -18,41 +23,38 @@
## yes | no ## yes | no
## Anything unrecognized will be treated as 'false'. ## Anything unrecognized will be treated as 'false'.
## CORE OPTIONS
## These affect the very core of the server.
[core] [core]
## The name of the server it sends to the client. ## The name of the server it sends to the client.
## Syntax: server_name=<string> ## Syntax: server_name=<string>
server_name=digFTP %v ## Default: %a %v
server_name=%a %v
## MotD command to post the output to clients after log in. ## Message of the Day to post to clients upon log in.
## WARNING: This method can be insecure. Use with caution. ## Syntax: motd=<string>
## Syntax: motd_command=<command> ## motd=file:<path>
#motd_command=cowsay -r Welcome %u. ## motd=cmd:<command>
## WARNING: Commands could be dangerous. Use with caution.
## MotD file to post to clients after log in. Overrides `motd_command`. #motd=cmd:cowsay -r Welcome %u.
## Syntax: motd_file=<path> #motd=file:ftp.motd
#motd_file=motd #motd=Welcome.
## MotD text to post to clients after log in. Overrides `motd_command` and
## `motd_file`.
## Syntax: motd_text=<string>
#motd_text=
## Path to digFTP plugins ## Path to digFTP plugins
## Syntax: plugin_path=<path> ## Syntax: plugin_path=<path>
## Default: /usr/lib/digftp/plugins
plugin_path=/usr/lib/digftp/plugins plugin_path=/usr/lib/digftp/plugins
## Authentication Engine to use for logging in. [engines]
## Syntax: auth_engine=<name> ## These specify the interface to use for functions.
## Engines are provided through plugins.
## Syntax: <interface>=<plugin name>
auth_engine=local ## Engine used for authenticating users.
## Default: pam
## Filer engine to use for the clients file system. auth=pam
## Syntax: file_engine=<name>
## Possible values are: local
filer_engine=local
## Engine used for handling the filesystem.
## Default: local
filer=local
[net] [net]
## Network address and port to listen on. ## Network address and port to listen on.
@@ -60,6 +62,8 @@ filer_engine=local
listen=127.0.0.1:21 listen=127.0.0.1:21
## Whether to support SSL. Server must be compiled with WITH_SSL=ON. ## 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=<bool> ## Syntax: ssl=<bool>
ssl=on ssl=on
@@ -67,9 +71,13 @@ ssl=on
utf8=off utf8=off
[ssl] [ssl]
## Configuration for FTPS (Explicit SSL)
certificate=cert.pem certificate=cert.pem
private_key=key.pem private_key=key.pem
## Ciphers to use for SSL.
## Default: ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH
ciphers=ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH ciphers=ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH
ssl_v2=no ssl_v2=no
@@ -81,9 +89,10 @@ tls_v1_3=yes
compression=yes compression=yes
prefer_server_ciphers=yes prefer_server_ciphers=yes
#### LOGGING
## Filenames for loglevels.
[logging] [logging]
## Log messages to file. Logging has multiple options pertaining to each ## Log server output files.
## loglevel. All values are paths.
## Syntax: [console|critical|error|warning|info|debug|all]=<path> ## Syntax: [console|critical|error|warning|info|debug|all]=<path>
info=digftp.info.log info=digftp.info.log
error=digftp.error.log error=digftp.error.log
@@ -97,3 +106,6 @@ file=passdb
## Root of logged in user. Can use string formatting. ## Root of logged in user. Can use string formatting.
## Syntax: home_path=<path> ## Syntax: home_path=<path>
home_path=/home/%u/ home_path=/home/%u/
[pam]
chroot=yes

1
conf/ftp.motd Normal file
View File

@@ -0,0 +1 @@
Welcome %u!

View File

@@ -1 +0,0 @@
Welcome to my FTP server.

View File

@@ -7,6 +7,7 @@
#include <map> #include <map>
#include "globals.h" #include "globals.h"
#include "build.h"
#include "logger.h" #include "logger.h"
#include "util.h" #include "util.h"
@@ -40,14 +41,15 @@ public:
virtual bool authenticate(ClientAuthDetails* details) { return false; } virtual bool authenticate(ClientAuthDetails* details) { return false; }
virtual void setUserDirectory(ClientAuthDetails* details) { virtual void setUserDirectory(ClientAuthDetails* details) {
std::string userdir = this->user_directory; std::map<std::string, std::string> vars = {
// Replace escaped var symbol with a dummy character {std::string(APPNAME_VAR), APPNAME},
userdir = replace(userdir, std::string(CNF_PERCENTSYM_VAR), std::string(1, 0xFE)); {std::string(VERSION_VAR), VERSION},
userdir = replace(userdir, std::string(CNF_USERNAME_VAR), std::string(details->username)); {std::string(USERNAME_VAR), std::string(details->username)},
userdir = replace(userdir, std::string(CNF_PASSWORD_VAR), std::string(details->password)); {std::string(DATE_VAR), getCurrentUTCTime()},
userdir = replace(userdir, std::string(CNF_HOSTNAME_VAR), std::string(details->hostname)); {std::string(TIME_VAR), getCurrentUTCDate()}
// Replace dummy character and return };
userdir = replace(userdir, std::string(1, 0xFE), "%");
std::string userdir = conf_format(this->user_directory, vars);
// Store the computed path in the details structure // Store the computed path in the details structure
strncpy(details->home_dir, userdir.c_str(), sizeof(details->home_dir) - 1); strncpy(details->home_dir, userdir.c_str(), sizeof(details->home_dir) - 1);

View File

@@ -36,7 +36,7 @@ public:
return; return;
} }
filer = default_filer_factory(); filer = default_filer_factory();
submit(230, replace(server_name, CNF_VERSION_VAR, VERSION)); 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) { void addOption(std::string name, bool toggle) {
@@ -521,41 +521,16 @@ private:
uint8_t flags = {}; uint8_t flags = {};
int state = FTP_STATE_GUEST; int state = FTP_STATE_GUEST;
// Data
int data_fd; int data_fd;
int data_sock; int data_sock;
struct sockaddr_in data_address; struct sockaddr_in data_address;
// Rename (RNFR RNTO)
std::string rename_from; std::string rename_from;
bool rename_pending = false; bool rename_pending = false;
void authedInit() { // SSL
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* control_ssl; SSL* control_ssl;
SSL* data_ssl; SSL* data_ssl;
SSL_SESSION* cached_session = nullptr; SSL_SESSION* cached_session = nullptr;
@@ -653,4 +628,82 @@ private:
return true; 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<std::string, std::string> 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);
}
}
}; };

View File

@@ -36,6 +36,8 @@ class Filer {
public: public:
virtual ~Filer() {}; virtual ~Filer() {};
virtual bool initialize(const std::map<std::string, std::string>& config) { return true; }
virtual file_data setRoot(std::string _root) = 0; virtual file_data setRoot(std::string _root) = 0;
virtual file_data setCWD(std::string _cwd) = 0; virtual file_data setCWD(std::string _cwd) = 0;
virtual std::filesystem::path getRoot() = 0; virtual std::filesystem::path getRoot() = 0;

View File

@@ -1,10 +1,12 @@
#ifndef GLOBALS_H #ifndef GLOBALS_H
#define GLOBALS_H #define GLOBALS_H
#define CNF_PERCENTSYM_VAR "%%" #define APPNAME_VAR "a"
#define CNF_VERSION_VAR "%v" #define VERSION_VAR "v"
#define CNF_USERNAME_VAR "%u" #define USERNAME_VAR "u"
#define CNF_PASSWORD_VAR "%p" #define PASSWORD_VAR "p"
#define CNF_HOSTNAME_VAR "%h" #define HOSTNAME_VAR "h"
#define DATE_VAR "d"
#define TIME_VAR "t"
#endif #endif

View File

@@ -183,29 +183,24 @@ void runClient(struct ftpconn* cfd) {
void initializePlugins() { void initializePlugins() {
std::string plugin_dir = config->getValue("core", "plugin_path", PLUGIN_DIR); std::string plugin_dir = config->getValue("core", "plugin_path", PLUGIN_DIR);
// Load auth plugins
auto& auth_manager = PluginManager<Auth>::getInstance(); auto& auth_manager = PluginManager<Auth>::getInstance();
auth_manager.setLogger(logger);
// Load filer plugins
auto& filer_manager = PluginManager<Filer>::getInstance(); auto& filer_manager = PluginManager<Filer>::getInstance();
auth_manager.setLogger(logger);
filer_manager.setLogger(logger); filer_manager.setLogger(logger);
// Try loading all plugins into both managers
for (const auto& entry : std::filesystem::directory_iterator(plugin_dir)) { for (const auto& entry : std::filesystem::directory_iterator(plugin_dir)) {
if (entry.path().extension() == ".so") { if (entry.path().extension() == ".so") {
const std::string& filename = entry.path().filename().string(); logger->print(LOGLEVEL_DEBUG, "Loading plugin: %s", entry.path().c_str());
if (filename.find(PluginTraits<Auth>::pluginPrefix()) == 0) {
auth_manager.loadPlugin(entry.path().string()); auth_manager.loadPlugin(entry.path().string());
}
else if (filename.find(PluginTraits<Filer>::pluginPrefix()) == 0) {
filer_manager.loadPlugin(entry.path().string()); filer_manager.loadPlugin(entry.path().string());
} }
} }
}
// Initialize auth // Initialize auth
std::string auth_type = config->getValue("core", "auth_engine", "pam"); std::string auth_type = config->getValue("engines", "auth", "pam");
auth = auth_manager.createPlugin(auth_type); auth = auth_manager.createPlugin(auth_type, config->get(auth_type)->get());
if (!auth) { if (!auth) {
logger->print(LOGLEVEL_CRITICAL, "Failed to create auth engine: %s", auth_type.c_str()); logger->print(LOGLEVEL_CRITICAL, "Failed to create auth engine: %s", auth_type.c_str());
@@ -213,7 +208,7 @@ void initializePlugins() {
} }
// Initialize filer // 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); default_filer_factory = filer_manager.getFactory(filer_type);
if (!default_filer_factory) { if (!default_filer_factory) {
@@ -231,7 +226,8 @@ int main(int argc , char *argv[]) {
signal(SIGPIPE, SIG_IGN); signal(SIGPIPE, SIG_IGN);
config = new ConfigFile(concatPath(std::string(CONFIG_DIR), "ftp.conf")); 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( sscanf(
config->getValue("net", "listen", "127.0.0.1:21").c_str(), config->getValue("net", "listen", "127.0.0.1:21").c_str(),

View File

@@ -6,6 +6,7 @@
typedef int (*GetAPIVersionFunc)(); typedef int (*GetAPIVersionFunc)();
typedef void (*SetLoggerFunc)(Logger*); typedef void (*SetLoggerFunc)(Logger*);
typedef const char* (*GetPluginTypeFunc)();
class IPlugin { class IPlugin {
public: public:

View File

@@ -1,36 +1,42 @@
#ifndef PLUGIN_MANAGER_H #ifndef PLUGIN_MANAGER_H
#define PLUGIN_MANAGER_H #define PLUGIN_MANAGER_H
#include <map> #include <map>
#include <dlfcn.h> #include <dlfcn.h>
#include <filesystem> #include <filesystem>
#include "plugin.h" #include "plugin.h"
class IPluginManager {
public:
virtual ~IPluginManager() = default;
virtual void setLogger(Logger* log) = 0;
virtual bool loadPlugin(const std::string& path) = 0;
};
template<typename T> template<typename T>
class PluginManager { class PluginManager : public IPluginManager {
public: public:
static PluginManager<T>& getInstance() { static PluginManager<T>& getInstance() {
static PluginManager<T> instance; static PluginManager<T> instance;
return instance; return instance;
} }
void setLogger(Logger* log) { void setLogger(Logger* log) override {
logger = log;
IPlugin::setLogger(log); IPlugin::setLogger(log);
logger = log;
} }
T* createPlugin(const std::string& type) { T* createPlugin(const std::string& type, const std::map<std::string, std::string>& config) {
auto it = plugins.find(type); auto it = plugins.find(type);
if (it != plugins.end()) { 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()); if (logger) logger->print(LOGLEVEL_ERROR, "Plugin type '%s' not found", type.c_str());
return nullptr; return nullptr;
} }
bool loadPlugin(const std::string& path) { 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); void* handle = dlopen(path.c_str(), RTLD_LAZY);
if (!handle) { if (!handle) {
if (logger) logger->print(LOGLEVEL_ERROR, "Failed to load plugin %s: %s", if (logger) logger->print(LOGLEVEL_ERROR, "Failed to load plugin %s: %s",
@@ -47,11 +53,20 @@ public:
auto get_desc = (const char* (*)())dlsym(handle, "getPluginDescription"); auto get_desc = (const char* (*)())dlsym(handle, "getPluginDescription");
auto get_ver = (const char* (*)())dlsym(handle, "getPluginVersion"); 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); dlclose(handle);
return false; return false;
} }
// Check API version compatibility
if (!checkAPIVersion(get_api_version(), path)) { if (!checkAPIVersion(get_api_version(), path)) {
dlclose(handle); dlclose(handle);
return false; return false;
@@ -60,18 +75,21 @@ public:
std::string plugin_name = get_name(); std::string plugin_name = get_name();
setLogger(logger); setLogger(logger);
// Check if already loaded
if (plugins.find(plugin_name) != plugins.end()) { if (plugins.find(plugin_name) != plugins.end()) {
if (logger) logger->print(LOGLEVEL_DEBUG, "Plugin %s is already loaded", plugin_name.c_str()); if (logger) logger->print(LOGLEVEL_DEBUG, "Plugin %s is already loaded", plugin_name.c_str());
dlclose(handle); dlclose(handle);
return true; return true;
} }
// Create and store plugin info
auto plugin_info = createPluginInfo(plugin_name, get_desc(), get_ver(), create, destroy, 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; plugins[plugin_name] = plugin_info;
if (logger) logger->print(LOGLEVEL_INFO, "Loaded plugin: %s v%s", if (logger) logger->print(LOGLEVEL_INFO, "Loaded plugin: %s v%s (%s interface)",
plugin_name.c_str(), plugin_info.version.c_str()); plugin_name.c_str(), plugin_info.version.c_str(),
PluginTraits<T>::interfaceName());
return true; return true;
} }

View File

@@ -30,7 +30,7 @@ struct PluginTraits<Auth> {
static const char* createFuncName() { return "createAuthPlugin"; } static const char* createFuncName() { return "createAuthPlugin"; }
static const char* destroyFuncName() { return "destroyAuthPlugin"; } static const char* destroyFuncName() { return "destroyAuthPlugin"; }
static const char* pluginPrefix() { return "libauth_"; } static const char* interfaceName() { return "Auth"; }
}; };
template<> template<>
@@ -42,7 +42,7 @@ struct PluginTraits<Filer> {
static const char* createFuncName() { return "createFilerPlugin"; } static const char* createFuncName() { return "createFilerPlugin"; }
static const char* destroyFuncName() { return "destroyFilerPlugin"; } static const char* destroyFuncName() { return "destroyFilerPlugin"; }
static const char* pluginPrefix() { return "libfiler_"; } static const char* interfaceName() { return "Filer"; }
}; };
#endif #endif

View File

@@ -9,6 +9,7 @@
#include <iterator> #include <iterator>
#include <algorithm> #include <algorithm>
#include <filesystem> #include <filesystem>
#include <chrono>
std::string replace(std::string subject, const std::string& search, const std::string& replace) { std::string replace(std::string subject, const std::string& search, const std::string& replace) {
size_t pos = 0; size_t pos = 0;
@@ -19,6 +20,55 @@ std::string replace(std::string subject, const std::string& search, const std::s
return subject; return subject;
} }
std::string conf_format(const std::string& fmt, const std::map<std::string, std::string>& 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 <typename Out> template <typename Out>
void split(const std::string &s, char delim, Out result, int limit) { void split(const std::string &s, char delim, Out result, int limit) {
int it = 0; int it = 0;
@@ -133,6 +183,22 @@ std::string concatPath(std::string dir1, std::string dir2) {
return std::filesystem::weakly_canonical(dir1+"/"+dir2).string(); 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) { static char* trim(char *str) {
char *end; char *end;
while(isspace(*str)) while(isspace(*str))