Add MotD support.
Fixed config up. Better handling of config variables
This commit is contained in:
@@ -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
1
conf/ftp.motd
Normal file
@@ -0,0 +1 @@
|
||||
Welcome %u!
|
||||
18
src/auth.h
18
src/auth.h
@@ -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);
|
||||
|
||||
111
src/client.cpp
111
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, 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
26
src/main.cpp
26
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<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(),
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
typedef int (*GetAPIVersionFunc)();
|
||||
typedef void (*SetLoggerFunc)(Logger*);
|
||||
typedef const char* (*GetPluginTypeFunc)();
|
||||
|
||||
class IPlugin {
|
||||
public:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
66
src/util.h
66
src/util.h
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user