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
## 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=<string>
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=<command>
#motd_command=cowsay -r Welcome %u.
## MotD file to post to clients after log in. Overrides `motd_command`.
## Syntax: motd_file=<path>
#motd_file=motd
## MotD text to post to clients after log in. Overrides `motd_command` and
## `motd_file`.
## Syntax: motd_text=<string>
#motd_text=
## Message of the Day to post to clients upon log in.
## Syntax: motd=<string>
## motd=file:<path>
## motd=cmd:<command>
## 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=<path>
## Default: /usr/lib/digftp/plugins
plugin_path=/usr/lib/digftp/plugins
## Authentication Engine to use for logging in.
## Syntax: auth_engine=<name>
[engines]
## These specify the interface to use for functions.
## Engines are provided through plugins.
## Syntax: <interface>=<plugin name>
auth_engine=local
## Filer engine to use for the clients file system.
## Syntax: file_engine=<name>
## 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=<bool>
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]=<path>
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=<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 "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<std::string, std::string> 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);

View File

@@ -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, std::string>){{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<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:
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 setCWD(std::string _cwd) = 0;
virtual std::filesystem::path getRoot() = 0;

View File

@@ -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

View File

@@ -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<Auth>::getInstance();
auth_manager.setLogger(logger);
// Load filer plugins
auto& filer_manager = PluginManager<Filer>::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<Auth>::pluginPrefix()) == 0) {
auth_manager.loadPlugin(entry.path().string());
}
else if (filename.find(PluginTraits<Filer>::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(),

View File

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

View File

@@ -1,43 +1,49 @@
#ifndef PLUGIN_MANAGER_H
#define PLUGIN_MANAGER_H
#include <map>
#include <dlfcn.h>
#include <filesystem>
#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>
class PluginManager {
class PluginManager : public IPluginManager {
public:
static PluginManager<T>& getInstance() {
static PluginManager<T> 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<std::string, std::string>& 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<T>::CreateFunc)dlsym(handle, PluginTraits<T>::createFuncName());
auto destroy = (typename PluginTraits<T>::DestroyFunc)dlsym(handle, PluginTraits<T>::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<T>::interfaceName());
return true;
}

View File

@@ -30,7 +30,7 @@ struct PluginTraits<Auth> {
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<Filer> {
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

View File

@@ -9,6 +9,7 @@
#include <iterator>
#include <algorithm>
#include <filesystem>
#include <chrono>
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<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>
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))