Commit 543d4451d2a619415d0cdd962ddb161a8a84baf9

Authored by Wiebe Cazemier
1 parent 1b9f0978

Auth plugin init works

It can be defined in the config file, along with options.
CMakeLists.txt
... ... @@ -23,6 +23,7 @@ add_executable(FlashMQ
23 23 cirbuf.cpp
24 24 logger.cpp
25 25 authplugin.cpp
  26 + configfileparser.cpp
26 27 )
27 28  
28 29 target_link_libraries(FlashMQ pthread dl)
... ...
authplugin.cpp
... ... @@ -7,10 +7,6 @@
7 7  
8 8 #include "exceptions.h"
9 9  
10   -// TODO: error handling on all the calls to the plugin. Exceptions? Passing to the caller?
11   -// TODO: where to do the conditionals about whether the plugin is loaded, what to do on error, etc?
12   -// -> Perhaps merely log the error (and return 'denied'?)?
13   -
14 10 void mosquitto_log_printf(int level, const char *fmt, ...)
15 11 {
16 12 Logger *logger = Logger::getInstance();
... ... @@ -21,12 +17,18 @@ void mosquitto_log_printf(int level, const char *fmt, ...)
21 17 }
22 18  
23 19  
24   -AuthPlugin::AuthPlugin() // our configuration object as param
  20 +AuthPlugin::AuthPlugin(ConfigFileParser &confFileParser) :
  21 + confFileParser(confFileParser)
25 22 {
26 23 logger = Logger::getInstance();
27 24 }
28 25  
29   -void *AuthPlugin::loadSymbol(void *handle, const char *symbol)
  26 +AuthPlugin::~AuthPlugin()
  27 +{
  28 + cleanup();
  29 +}
  30 +
  31 +void *AuthPlugin::loadSymbol(void *handle, const char *symbol) const
30 32 {
31 33 void *r = dlsym(handle, symbol);
32 34  
... ... @@ -41,8 +43,14 @@ void *AuthPlugin::loadSymbol(void *handle, const char *symbol)
41 43  
42 44 void AuthPlugin::loadPlugin(const std::string &pathToSoFile)
43 45 {
  46 + if (pathToSoFile.empty())
  47 + return;
  48 +
44 49 logger->logf(LOG_INFO, "Loading auth plugin %s", pathToSoFile.c_str());
45 50  
  51 + initialized = false;
  52 + wanted = true;
  53 +
46 54 if (access(pathToSoFile.c_str(), R_OK) != 0)
47 55 {
48 56 std::ostringstream oss;
... ... @@ -72,47 +80,105 @@ void AuthPlugin::loadPlugin(const std::string &pathToSoFile)
72 80 acl_check_v2 = (F_auth_plugin_acl_check_v2)loadSymbol(r, "mosquitto_auth_acl_check");
73 81 unpwd_check_v2 = (F_auth_plugin_unpwd_check_v2)loadSymbol(r, "mosquitto_auth_unpwd_check");
74 82 psk_key_get_v2 = (F_auth_plugin_psk_key_get_v2)loadSymbol(r, "mosquitto_auth_psk_key_get");
  83 +
  84 + initialized = true;
75 85 }
76 86  
77   -int AuthPlugin::init()
  87 +void AuthPlugin::init()
78 88 {
79   - struct mosquitto_auth_opt auth_opts[2]; // TODO: get auth opts from central config object
80   - std::memset(&auth_opts, 0, sizeof(struct mosquitto_auth_opt) * 2);
81   - int result = init_v2(&pluginData, auth_opts, 2);
82   - return result;
  89 + if (!wanted)
  90 + return;
  91 +
  92 + AuthOptCompatWrap &authOpts = confFileParser.getAuthOptsCompat();
  93 + int result = init_v2(&pluginData, authOpts.head(), authOpts.size());
  94 + if (result != 0)
  95 + throw FatalError("Error initialising auth plugin.");
83 96 }
84 97  
85   -int AuthPlugin::cleanup()
  98 +void AuthPlugin::cleanup()
86 99 {
87   - struct mosquitto_auth_opt auth_opts[2]; // TODO: get auth opts from central config object
88   - std::memset(&auth_opts, 0, sizeof(struct mosquitto_auth_opt) * 2);
89   - return cleanup_v2(pluginData, auth_opts, 2);
  100 + if (!cleanup_v2)
  101 + return;
  102 +
  103 + securityCleanup(false);
  104 +
  105 + AuthOptCompatWrap &authOpts = confFileParser.getAuthOptsCompat();
  106 + int result = cleanup_v2(pluginData, authOpts.head(), authOpts.size());
  107 + if (result != 0)
  108 + logger->logf(LOG_ERR, "Error cleaning up auth plugin"); // Not doing exception, because we're shutting down anyway.
90 109 }
91 110  
92   -int AuthPlugin::securityInit(bool reloading)
  111 +void AuthPlugin::securityInit(bool reloading)
93 112 {
94   - struct mosquitto_auth_opt auth_opts[2]; // TODO: get auth opts from central config object
95   - std::memset(&auth_opts, 0, sizeof(struct mosquitto_auth_opt) * 2);
96   - return security_init_v2(pluginData, auth_opts, 2, reloading);
  113 + if (!wanted)
  114 + return;
  115 +
  116 + AuthOptCompatWrap &authOpts = confFileParser.getAuthOptsCompat();
  117 + int result = security_init_v2(pluginData, authOpts.head(), authOpts.size(), reloading);
  118 + if (result != 0)
  119 + {
  120 + throw AuthPluginException("Plugin function mosquitto_auth_security_init returned an error. If it didn't log anything, we don't know what it was.");
  121 + }
  122 + initialized = true;
97 123 }
98 124  
99   -int AuthPlugin::securityCleanup(bool reloading)
  125 +void AuthPlugin::securityCleanup(bool reloading)
100 126 {
101   - struct mosquitto_auth_opt auth_opts[2]; // TODO: get auth opts from central config object
102   - std::memset(&auth_opts, 0, sizeof(struct mosquitto_auth_opt) * 2);
103   - return security_cleanup_v2(pluginData, auth_opts, 2, reloading);
  127 + if (!wanted)
  128 + return;
  129 +
  130 + initialized = false;
  131 + AuthOptCompatWrap &authOpts = confFileParser.getAuthOptsCompat();
  132 + int result = security_cleanup_v2(pluginData, authOpts.head(), authOpts.size(), reloading);
  133 +
  134 + if (result != 0)
  135 + {
  136 + throw AuthPluginException("Plugin function mosquitto_auth_security_cleanup returned an error. If it didn't log anything, we don't know what it was.");
  137 + }
104 138 }
105 139  
106 140 AuthResult AuthPlugin::aclCheck(const std::string &clientid, const std::string &username, const std::string &topic, AclAccess access)
107 141 {
  142 + if (!wanted)
  143 + return AuthResult::success;
  144 +
  145 + if (!initialized)
  146 + {
  147 + logger->logf(LOG_ERR, "ACL check wanted, but initialization failed. Can't perform check.");
  148 + return AuthResult::error;
  149 + }
  150 +
108 151 int result = acl_check_v2(pluginData, clientid.c_str(), username.c_str(), topic.c_str(), static_cast<int>(access));
109   - return static_cast<AuthResult>(result);
  152 + AuthResult result_ = static_cast<AuthResult>(result);
  153 +
  154 + if (result_ == AuthResult::error)
  155 + {
  156 + logger->logf(LOG_ERR, "ACL check by plugin returned error for topic '%s'. If it didn't log anything, we don't know what it was.", topic.c_str());
  157 + }
  158 +
  159 + return result_;
110 160 }
111 161  
112 162 AuthResult AuthPlugin::unPwdCheck(const std::string &username, const std::string &password)
113 163 {
  164 + if (!wanted)
  165 + return AuthResult::success;
  166 +
  167 + if (!initialized)
  168 + {
  169 + logger->logf(LOG_ERR, "Username+password check wanted, but initialization failed. Can't perform check.");
  170 + return AuthResult::error;
  171 + }
  172 +
114 173 int result = unpwd_check_v2(pluginData, username.c_str(), password.c_str());
115   - return static_cast<AuthResult>(result);
  174 + AuthResult r = static_cast<AuthResult>(result);
  175 +
  176 + if (r == AuthResult::error)
  177 + {
  178 + logger->logf(LOG_ERR, "Username+password check by plugin returned error for user '%s'. If it didn't log anything, we don't know what it was.", username.c_str());
  179 + }
  180 +
  181 + return r;
116 182 }
117 183  
118 184  
... ...
authplugin.h
... ... @@ -5,6 +5,7 @@
5 5 #include <cstring>
6 6  
7 7 #include "logger.h"
  8 +#include "configfileparser.h"
8 9  
9 10 // Compatible with Mosquitto
10 11 enum class AclAccess
... ... @@ -23,11 +24,6 @@ enum class AuthResult
23 24 error = 13
24 25 };
25 26  
26   -struct mosquitto_auth_opt {
27   - char *key;
28   - char *value;
29   -};
30   -
31 27 typedef int (*F_auth_plugin_version)(void);
32 28  
33 29 typedef int (*F_auth_plugin_init_v2)(void **, struct mosquitto_auth_opt *, int);
... ... @@ -55,18 +51,25 @@ class AuthPlugin
55 51 F_auth_plugin_unpwd_check_v2 unpwd_check_v2 = nullptr;
56 52 F_auth_plugin_psk_key_get_v2 psk_key_get_v2 = nullptr;
57 53  
  54 + ConfigFileParser &confFileParser;
  55 +
58 56 void *pluginData = nullptr;
59 57 Logger *logger = nullptr;
  58 + bool initialized = false;
  59 + bool wanted = false;
60 60  
61   - void *loadSymbol(void *handle, const char *symbol);
  61 + void *loadSymbol(void *handle, const char *symbol) const;
62 62 public:
63   - AuthPlugin();
  63 + AuthPlugin(ConfigFileParser &confFileParser);
  64 + AuthPlugin(const AuthPlugin &other) = delete;
  65 + AuthPlugin(AuthPlugin &&other) = delete;
  66 + ~AuthPlugin();
64 67  
65 68 void loadPlugin(const std::string &pathToSoFile);
66   - int init();
67   - int cleanup();
68   - int securityInit(bool reloading);
69   - int securityCleanup(bool reloading);
  69 + void init();
  70 + void cleanup();
  71 + void securityInit(bool reloading);
  72 + void securityCleanup(bool reloading);
70 73 AuthResult aclCheck(const std::string &clientid, const std::string &username, const std::string &topic, AclAccess access);
71 74 AuthResult unPwdCheck(const std::string &username, const std::string &password);
72 75  
... ...
configfileparser.cpp 0 โ†’ 100644
  1 +#include "configfileparser.h"
  2 +
  3 +#include <fcntl.h>
  4 +#include <unistd.h>
  5 +#include <sstream>
  6 +#include "fstream"
  7 +
  8 +#include "exceptions.h"
  9 +#include "utils.h"
  10 +#include <regex>
  11 +
  12 +
  13 +mosquitto_auth_opt::mosquitto_auth_opt(const std::string &key, const std::string &value)
  14 +{
  15 + this->key = strdup(key.c_str());
  16 + this->value = strdup(value.c_str());
  17 +}
  18 +
  19 +mosquitto_auth_opt::mosquitto_auth_opt(mosquitto_auth_opt &&other)
  20 +{
  21 + this->key = other.key;
  22 + this->value = other.value;
  23 + other.key = nullptr;
  24 + other.value = nullptr;
  25 +}
  26 +
  27 +mosquitto_auth_opt::~mosquitto_auth_opt()
  28 +{
  29 + if (key)
  30 + delete key;
  31 + if (value)
  32 + delete value;
  33 +}
  34 +
  35 +AuthOptCompatWrap::AuthOptCompatWrap(const std::unordered_map<std::string, std::string> &authOpts)
  36 +{
  37 + for(auto &pair : authOpts)
  38 + {
  39 + mosquitto_auth_opt opt(pair.first, pair.second);
  40 + optArray.push_back(std::move(opt));
  41 + }
  42 +}
  43 +
  44 +ConfigFileParser::ConfigFileParser(const std::string &path) :
  45 + path(path)
  46 +{
  47 + validKeys.insert("auth_plugin");
  48 +}
  49 +
  50 +void ConfigFileParser::loadFile()
  51 +{
  52 + if (access(path.c_str(), R_OK) != 0)
  53 + {
  54 + std::ostringstream oss;
  55 + oss << "Error: " << path << " is not there or not readable";
  56 + throw ConfigFileException(oss.str());
  57 + }
  58 +
  59 + std::ifstream infile(path, std::ios::in);
  60 +
  61 + if (!infile.is_open())
  62 + {
  63 + std::ostringstream oss;
  64 + oss << "Error loading " << path;
  65 + throw ConfigFileException(oss.str());
  66 + }
  67 +
  68 + std::list<std::string> lines;
  69 +
  70 + const std::regex r("^([a-zA-Z0-9_\\-]+) +([a-zA-Z0-9_\\-/\\.]+)$");
  71 +
  72 + // First parse the file and keep the valid lines.
  73 + for(std::string line; getline(infile, line ); )
  74 + {
  75 + trim(line);
  76 +
  77 + if (startsWith(line, "#"))
  78 + continue;
  79 +
  80 + if (line.empty())
  81 + continue;
  82 +
  83 + std::smatch matches;
  84 +
  85 + if (!std::regex_search(line, matches, r) || matches.size() != 3)
  86 + {
  87 + std::ostringstream oss;
  88 + oss << "Line '" << line << "' not in 'key value' format";
  89 + throw ConfigFileException(oss.str());
  90 + }
  91 +
  92 + lines.push_back(line);
  93 + }
  94 +
  95 + authOpts.clear();
  96 + authOptCompatWrap.reset();
  97 +
  98 + // Then once we know the config file is valid, process it.
  99 + for (std::string &line : lines)
  100 + {
  101 + std::smatch matches;
  102 +
  103 + if (!std::regex_search(line, matches, r) || matches.size() != 3)
  104 + {
  105 + throw ConfigFileException("Config parse error at a point that should not be possible.");
  106 + }
  107 +
  108 + std::string key = matches[1].str();
  109 + const std::string value = matches[2].str();
  110 +
  111 + const std::string auth_opt_ = "auth_opt_";
  112 + if (startsWith(key, auth_opt_))
  113 + {
  114 + key.replace(0, auth_opt_.length(), "");
  115 + authOpts[key] = value;
  116 + }
  117 + else
  118 + {
  119 + auto valid_key_it = validKeys.find(key);
  120 + if (valid_key_it == validKeys.end())
  121 + {
  122 + std::ostringstream oss;
  123 + oss << "Config key '" << key << "' is not valid";
  124 + throw ConfigFileException(oss.str());
  125 + }
  126 +
  127 + if (key == "auth_plugin")
  128 + {
  129 + this->authPluginPath = value;
  130 + }
  131 + }
  132 + }
  133 +
  134 + authOptCompatWrap.reset(new AuthOptCompatWrap(authOpts));
  135 +}
  136 +
  137 +AuthOptCompatWrap &ConfigFileParser::getAuthOptsCompat()
  138 +{
  139 + return *authOptCompatWrap.get();
  140 +}
  141 +
  142 +
  143 +
... ...
configfileparser.h 0 โ†’ 100644
  1 +#ifndef CONFIGFILEPARSER_H
  2 +#define CONFIGFILEPARSER_H
  3 +
  4 +#include <string>
  5 +#include <set>
  6 +#include <unordered_map>
  7 +#include <vector>
  8 +#include <memory>
  9 +
  10 +struct mosquitto_auth_opt
  11 +{
  12 + char *key = nullptr;
  13 + char *value = nullptr;
  14 +
  15 + mosquitto_auth_opt(const std::string &key, const std::string &value);
  16 + mosquitto_auth_opt(mosquitto_auth_opt &&other);
  17 + mosquitto_auth_opt(const mosquitto_auth_opt &other) = delete;
  18 + ~mosquitto_auth_opt();
  19 +};
  20 +
  21 +struct AuthOptCompatWrap
  22 +{
  23 + std::vector<struct mosquitto_auth_opt> optArray;
  24 +
  25 + AuthOptCompatWrap(const std::unordered_map<std::string, std::string> &authOpts);
  26 + AuthOptCompatWrap(const AuthOptCompatWrap &other) = delete;
  27 + AuthOptCompatWrap(AuthOptCompatWrap &&other) = delete;
  28 +
  29 + struct mosquitto_auth_opt *head() { return &optArray[0]; }
  30 + int size() { return optArray.size(); }
  31 +};
  32 +
  33 +class ConfigFileParser
  34 +{
  35 + const std::string path;
  36 + std::set<std::string> validKeys;
  37 + std::unordered_map<std::string, std::string> authOpts;
  38 + std::unique_ptr<AuthOptCompatWrap> authOptCompatWrap;
  39 + std::string authPluginPath;
  40 +
  41 +public:
  42 + ConfigFileParser(const std::string &path);
  43 + void loadFile();
  44 + AuthOptCompatWrap &getAuthOptsCompat();
  45 +
  46 + std::string getAuthPluginPath() { return authPluginPath; }
  47 +};
  48 +
  49 +#endif // CONFIGFILEPARSER_H
... ...
exceptions.h
... ... @@ -22,4 +22,16 @@ public:
22 22 FatalError(const std::string &msg) : std::runtime_error(msg) {}
23 23 };
24 24  
  25 +class ConfigFileException : public std::runtime_error
  26 +{
  27 +public:
  28 + ConfigFileException(const std::string &msg) : std::runtime_error(msg) {}
  29 +};
  30 +
  31 +class AuthPluginException : public std::runtime_error
  32 +{
  33 +public:
  34 + AuthPluginException(const std::string &msg) : std::runtime_error(msg) {}
  35 +};
  36 +
25 37 #endif // EXCEPTIONS_H
... ...
main.cpp
... ... @@ -6,7 +6,7 @@
6 6  
7 7 #include "mainapp.h"
8 8  
9   -MainApp *mainApp = MainApp::getMainApp();
  9 +MainApp *mainApp = nullptr;
10 10  
11 11 static void signal_handler(int signal)
12 12 {
... ... @@ -65,6 +65,7 @@ int main()
65 65 {
66 66 try
67 67 {
  68 + mainApp = MainApp::getMainApp();
68 69 check<std::runtime_error>(register_signal_handers());
69 70 mainApp->start();
70 71 }
... ...
mainapp.cpp
... ... @@ -111,7 +111,8 @@ void do_thread_work(ThreadData *threadData)
111 111 MainApp::MainApp() :
112 112 subscriptionStore(new SubscriptionStore())
113 113 {
114   -
  114 + confFileParser.reset(new ConfigFileParser("/home/halfgaar/Projects/FlashMQThings/config.txt")); // TODO: from argv
  115 + confFileParser->loadFile();
115 116 }
116 117  
117 118 MainApp *MainApp::getMainApp()
... ... @@ -154,7 +155,7 @@ void MainApp::start()
154 155  
155 156 for (int i = 0; i < NR_OF_THREADS; i++)
156 157 {
157   - std::shared_ptr<ThreadData> t(new ThreadData(i, subscriptionStore));
  158 + std::shared_ptr<ThreadData> t(new ThreadData(i, subscriptionStore, *confFileParser.get()));
158 159 std::thread thread(do_thread_work, t.get());
159 160 t->moveThreadHere(std::move(thread));
160 161 threads.push_back(t);
... ...
mainapp.h
... ... @@ -16,6 +16,7 @@
16 16 #include "client.h"
17 17 #include "mqttpacket.h"
18 18 #include "subscriptionstore.h"
  19 +#include "configfileparser.h"
19 20  
20 21 class MainApp
21 22 {
... ... @@ -25,6 +26,7 @@ class MainApp
25 26 bool running = true;
26 27 std::vector<std::shared_ptr<ThreadData>> threads;
27 28 std::shared_ptr<SubscriptionStore> subscriptionStore;
  29 + std::unique_ptr<ConfigFileParser> confFileParser;
28 30  
29 31 MainApp();
30 32 public:
... ...
threaddata.cpp
... ... @@ -2,11 +2,19 @@
2 2 #include <string>
3 3 #include <sstream>
4 4  
5   -ThreadData::ThreadData(int threadnr, std::shared_ptr<SubscriptionStore> &subscriptionStore) :
  5 +ThreadData::ThreadData(int threadnr, std::shared_ptr<SubscriptionStore> &subscriptionStore, ConfigFileParser &confFileParser) :
6 6 subscriptionStore(subscriptionStore),
  7 + confFileParser(confFileParser),
  8 + authPlugin(confFileParser),
7 9 threadnr(threadnr)
8 10 {
  11 + logger = Logger::getInstance();
  12 +
9 13 epollfd = check<std::runtime_error>(epoll_create(999));
  14 +
  15 + authPlugin.loadPlugin(confFileParser.getAuthPluginPath());
  16 + authPlugin.init();
  17 + authPlugin.securityInit(false);
10 18 }
11 19  
12 20 void ThreadData::moveThreadHere(std::thread &&thread)
... ... @@ -92,5 +100,18 @@ bool ThreadData::doKeepAliveCheck()
92 100 return true;
93 101 }
94 102  
  103 +void ThreadData::reload()
  104 +{
  105 + try
  106 + {
  107 + authPlugin.securityCleanup(true);
  108 + authPlugin.securityInit(true);
  109 + }
  110 + catch (AuthPluginException &ex)
  111 + {
  112 + logger->logf(LOG_ERR, "Error reloading auth plugin: %s. Security checks will now fail, because we don't know the status of the plugin anymore.", ex.what());
  113 + }
  114 +}
  115 +
95 116  
96 117  
... ...
threaddata.h
... ... @@ -16,14 +16,18 @@
16 16 #include "client.h"
17 17 #include "subscriptionstore.h"
18 18 #include "utils.h"
19   -
20   -
  19 +#include "configfileparser.h"
  20 +#include "authplugin.h"
  21 +#include "logger.h"
21 22  
22 23 class ThreadData
23 24 {
24 25 std::unordered_map<int, Client_p> clients_by_fd;
25 26 std::mutex clients_by_fd_mutex;
26 27 std::shared_ptr<SubscriptionStore> subscriptionStore;
  28 + ConfigFileParser &confFileParser;
  29 + AuthPlugin authPlugin;
  30 + Logger *logger;
27 31  
28 32 public:
29 33 bool running = true;
... ... @@ -31,7 +35,9 @@ public:
31 35 int threadnr = 0;
32 36 int epollfd = 0;
33 37  
34   - ThreadData(int threadnr, std::shared_ptr<SubscriptionStore> &subscriptionStore);
  38 + ThreadData(int threadnr, std::shared_ptr<SubscriptionStore> &subscriptionStore, ConfigFileParser &confFileParser);
  39 + ThreadData(const ThreadData &other) = delete;
  40 + ThreadData(ThreadData &&other) = delete;
35 41  
36 42 void moveThreadHere(std::thread &&thread);
37 43 void quit();
... ... @@ -42,6 +48,7 @@ public:
42 48 std::shared_ptr<SubscriptionStore> &getSubscriptionStore();
43 49  
44 50 bool doKeepAliveCheck();
  51 + void reload();
45 52 };
46 53  
47 54 #endif // THREADDATA_H
... ...
utils.cpp
1 1 #include "utils.h"
2 2  
3   -
  3 +#include <algorithm>
4 4  
5 5 std::list<std::__cxx11::string> split(const std::string &input, const char sep, size_t max, bool keep_empty_parts)
6 6 {
... ... @@ -143,3 +143,28 @@ std::vector&lt;std::string&gt; splitToVector(const std::string &amp;input, const char sep,
143 143 list.push_back(input.substr(start, std::string::npos));
144 144 return list;
145 145 }
  146 +
  147 +void ltrim(std::string &s)
  148 +{
  149 + s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
  150 + return !std::isspace(ch);
  151 + }));
  152 +}
  153 +
  154 +void rtrim(std::string &s)
  155 +{
  156 + s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) {
  157 + return !std::isspace(ch);
  158 + }).base(), s.end());
  159 +}
  160 +
  161 +void trim(std::string &s)
  162 +{
  163 + ltrim(s);
  164 + rtrim(s);
  165 +}
  166 +
  167 +bool startsWith(const std::string &s, const std::string &needle)
  168 +{
  169 + return s.find(needle) == 0;
  170 +}
... ...
... ... @@ -7,6 +7,7 @@
7 7 #include <list>
8 8 #include <limits>
9 9 #include <vector>
  10 +#include <algorithm>
10 11  
11 12 template<typename T> int check(int rc)
12 13 {
... ... @@ -31,4 +32,10 @@ bool strContains(const std::string &amp;s, const std::string &amp;needle);
31 32  
32 33 bool isValidPublishPath(const std::string &s);
33 34  
  35 +void ltrim(std::string &s);
  36 +void rtrim(std::string &s);
  37 +void trim(std::string &s);
  38 +bool startsWith(const std::string &s, const std::string &needle);
  39 +
  40 +
34 41 #endif // UTILS_H
... ...