NAWA 0.9
Web Application Framework for C++
nawarun.cpp
Go to the documentation of this file.
1/*
2 * Copyright (C) 2019-2022 Tobias Flaig.
3 *
4 * This file is part of nawa.
5 *
6 * nawa is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Lesser General Public License,
8 * version 3, as published by the Free Software Foundation.
9 *
10 * nawa is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Lesser General Public License for more details.
14 *
15 * You should have received a copy of the GNU Lesser General Public License
16 * along with nawa. If not, see <https://www.gnu.org/licenses/>.
17 */
18
24#include <atomic>
25#include <csignal>
26#include <dlfcn.h>
27#include <grp.h>
28#include <nawa/Exception.h>
30#include <nawa/application.h>
31#include <nawa/config/Config.h>
32#include <nawa/logging/Log.h>
33#include <nawa/oss.h>
34#include <nawa/util/utils.h>
35#include <nawarun/nawarun.h>
36#include <pwd.h>
37#include <stdexcept>
38#include <thread>
39
40using namespace nawa;
41using namespace nawarun;
42using namespace std;
43
44namespace {
45 unique_ptr<RequestHandler> requestHandlerPtr;
46 optional<string> configFile;
47 atomic<bool> readyToReconfigure(false);
48 Log logger;
49 unsigned int terminationTimeout = 10;
50
51 // signal handler for SIGINT, SIGTERM, and SIGUSR1
52 void shutdown(int signum) {
53 NLOG_INFO(logger, "Terminating on signal " << signum)
54
55 // terminate worker threads
56 if (requestHandlerPtr) {
57 // should stop
58 requestHandlerPtr->stop();
59
60 // if this did not work, try harder after 10 seconds
61 sleep(terminationTimeout);
62 if (requestHandlerPtr && signum != SIGUSR1) {
63 NLOG_INFO(logger, "Enforcing termination now, ignoring pending requests.")
64 requestHandlerPtr->terminate();
65 }
66 } else {
67 exit(0);
68 }
69 }
70
71 // load a symbol from an app .so file
72 void* loadAppSymbol(void* appOpen, char const* symbolName, string const& error) {
73 void* symbol = dlsym(appOpen, symbolName);
74 auto dlsymError = dlerror();
75 if (dlsymError) {
76 throw Exception(__FUNCTION__, 11, error, dlsymError);
77 }
78 return symbol;
79 }
80
81 // free memory of an open app
82 void closeApp(void* appOpen) {
83 dlclose(appOpen);
84 }
85
90 void setTerminationTimeout(Config const& config) {
91 string terminationTimeoutStr = config[{"system", "termination_timeout"}];
92 if (!terminationTimeoutStr.empty()) {
93 try {
94 auto newTerminationTimeout = stoul(terminationTimeoutStr);
95 terminationTimeout = newTerminationTimeout;
96 } catch (invalid_argument& e) {
97 NLOG_WARNING(logger, "WARNING: Invalid termination timeout given in configuration, default value "
98 "or previous value will be used.")
99 }
100 }
101 }
102
107 optional<Config> loadConfig(bool reload = false) {
108 if (configFile) {
109 try {
110 return Config(*configFile);
111 } catch (Exception const& e) {
112 if (reload) {
113 NLOG_ERROR(logger, "ERROR: Could not reload config: " << e.getMessage())
114 NLOG_WARNING(logger, "WARNING: App will not be reloaded as well")
115 } else {
116 NLOG_ERROR(logger, "Fatal Error: Could not load config: " << e.getMessage())
117 }
118 NLOG_DEBUG(logger, "Debug info: " << e.getDebugMessage())
119 return nullopt;
120 }
121 }
122 return Config();
123 }
124
129 void doPrivilegeDowngrade(PrivilegeDowngradeData const& data) {
130 auto const& [uid, gid, supplementaryGroups] = data;
131 if (uid != 0 && gid != 0 && setgroups(supplementaryGroups.size(), &supplementaryGroups[0]) != 0) {
132 NLOG_ERROR(logger, "Fatal Error: Could not set supplementary groups during privilege downgrade.")
133 exit(1);
134 }
135 if (setgid(gid) != 0 || setuid(uid) != 0) {
136 NLOG_ERROR(logger, "Fatal Error: Could not set privileges during privilege downgrade.")
137 exit(1);
138 }
139 }
140
141 void printHelpAndExit() {
142 cout << "nawarun is the runner for NAWA web applications.\n\n"
143 "Usage: nawarun [<overrides>] [<config-file> | --no-config-file]\n\n"
144 "Format for configuration overrides: --<category>:<key>=<value>\n\n"
145 "If no config file is given, nawarun will try to use config.ini from the current\n"
146 "working directory, unless the --no-config-file option is given. The config file\n"
147 "as well as --no-config-file are only accepted as the last command line argument\n"
148 "after the overrides.\n\n"
149 "nawarun version "
150 << nawa_version_major << '.' << nawa_version_minor << '\n';
151 exit(0);
152 }
153
154 void printVersionAndExit() {
155 cout << nawa_version_major << '.' << nawa_version_minor << '\n';
156 exit(0);
157 }
158
159 void setUpSignalHandlers() {
160 signal(SIGINT, shutdown);
161 signal(SIGTERM, shutdown);
162 signal(SIGUSR1, shutdown);
163 signal(SIGHUP, reload);
164 }
165
166 void setUpLogging(Config const& config) {
167 auto configuredLogLevel = config[{"logging", "level"}];
168 if (configuredLogLevel == "off") {
169 Log::setOutputLevel(Log::Level::OFF);
170 } else if (configuredLogLevel == "error") {
171 Log::setOutputLevel(Log::Level::ERROR);
172 } else if (configuredLogLevel == "warning") {
173 Log::setOutputLevel(Log::Level::WARNING);
174 } else if (configuredLogLevel == "debug") {
175 Log::setOutputLevel(Log::Level::DEBUG);
176 }
177 if (config[{"logging", "extended"}] == "on") {
178 Log::setExtendedFormat(true);
179 }
180 Log::lockStream();
181 }
182}// namespace
183
184unsigned int nawarun::getConcurrency(Config const& config) {
185 double cReal;
186 try {
187 cReal = config.isSet({"system", "threads"})
188 ? stod(config[{"system", "threads"}])
189 : 1.0;
190 } catch (invalid_argument& e) {
191 NLOG_WARNING(logger, "WARNING: Invalid value given for system/concurrency given in the config file.")
192 cReal = 1.0;
193 }
194 if (config[{"system", "concurrency"}] == "hardware") {
195 cReal = max(1.0, thread::hardware_concurrency() * cReal);
196 }
197 return static_cast<unsigned int>(cReal);
198}
199
200pair<init_t*, shared_ptr<HandleRequestFunctionWrapper>> nawarun::loadAppFunctions(Config const& config) {
201 // load application init function
202 string appPath = config[{"application", "path"}];
203 if (appPath.empty()) {
204 throw Exception(__FUNCTION__, 1, "Application path not set in config file.");
205 }
206 void* appOpen = dlopen(appPath.c_str(), RTLD_LAZY);
207 if (!appOpen) {
208 throw Exception(__FUNCTION__, 2, "Application file could not be loaded.", dlerror());
209 }
210
211 // reset dl errors
212 dlerror();
213
214 // load symbols and check for errors
215 // first load nawa_version_major (defined in Application.h, included in Connection.h)
216 // the version the app has been compiled against should match the version of this nawarun
217 string appVersionError = "Could not read nawa version from application.";
218 auto appNawaVersionMajor = (int*) loadAppSymbol(appOpen, "nawa_version_major", appVersionError);
219 auto appNawaVersionMinor = (int*) loadAppSymbol(appOpen, "nawa_version_minor", appVersionError);
220 if (*appNawaVersionMajor != nawa_version_major || *appNawaVersionMinor != nawa_version_minor) {
221 throw Exception(__FUNCTION__, 3, "App has been compiled against another version of NAWA.");
222 }
223 auto appInit = (init_t*) loadAppSymbol(appOpen, "init", "Could not load init function from application.");
224 auto appHandleRequest = (handleRequest_t*) loadAppSymbol(appOpen, "handleRequest",
225 "Could not load handleRequest function from application.");
226 return {appInit, make_shared<HandleRequestFunctionWrapper>(appHandleRequest, appOpen, closeApp)};
227}
228
229void nawarun::reload(int signum) {
230 if (!configFile) {
231 NLOG_WARNING(logger, "WARNING: Reloading is not supported without config file and will therefore not "
232 "happen.")
233 return;
234 }
235
236 if (requestHandlerPtr && readyToReconfigure) {
237 NLOG_INFO(logger, "Reloading config and app on signal " << signum)
238 readyToReconfigure = false;
239
240 optional<Config> config = loadConfig(true);
241 if (!config) {
242 readyToReconfigure = true;
243 return;
244 }
245
246 // set new termination timeout, if given
247 setTerminationTimeout(*config);
248
249 init_t* appInit;
250 shared_ptr<HandleRequestFunctionWrapper> appHandleRequest;
251 try {
252 tie(appInit, appHandleRequest) = loadAppFunctions(*config);
253 } catch (Exception const& e) {
254 NLOG_ERROR(logger, "ERROR: Could not reload app: " << e.getMessage())
255 NLOG_DEBUG(logger, "Debug info: " << e.getDebugMessage())
256 NLOG_WARNING(logger, "WARNING: Configuration will be reloaded anyway")
257
258 // just reload config, not app
259 requestHandlerPtr->reconfigure(nullopt, nullopt, config);
260 readyToReconfigure = true;
261 return;
262 }
263
264 {
265 AppInit appInitStruct(*config, getConcurrency(*config));
266 auto initReturn = appInit(appInitStruct);
267
268 // init function of the app should return 0 on success, otherwise we will not reload
269 if (initReturn != 0) {
270 NLOG_ERROR(logger,
271 "ERROR: App init function returned " << initReturn << " -- cancelling reload of app.")
272 NLOG_WARNING(logger, "WARNING: Configuration will be reloaded anyway")
273
274 // just reload config, not app
275 requestHandlerPtr->reconfigure(nullopt, nullopt, config);
276 readyToReconfigure = true;
277 return;
278 }
279
280 // reconfigure everything
281 requestHandlerPtr->reconfigure(appHandleRequest, appInitStruct.accessFilters(), appInitStruct.config());
282 readyToReconfigure = true;
283 }
284 }
285}
286
287optional<PrivilegeDowngradeData> nawarun::preparePrivilegeDowngrade(Config const& config) {
288 auto initialUID = getuid();
289 uid_t privUID = -1;
290 gid_t privGID = -1;
291 vector<gid_t> supplementaryGroups;
292
293 if (initialUID == 0) {
294 if (!config.isSet({"privileges", "user"}) || !config.isSet({"privileges", "group"})) {
295 throw Exception(__PRETTY_FUNCTION__, 1,
296 "Running as root and user or group for privilege downgrade is not set in the configuration.");
297 }
298 string username = config[{"privileges", "user"}];
299 string groupname = config[{"privileges", "group"}];
300 passwd* privUser;
301 group* privGroup;
302 privUser = getpwnam(username.c_str());
303 privGroup = getgrnam(groupname.c_str());
304 if (privUser == nullptr || privGroup == nullptr) {
305 throw Exception(__PRETTY_FUNCTION__, 2,
306 "The user or group name for privilege downgrade given in the configuration is invalid.");
307 }
308 privUID = privUser->pw_uid;
309 privGID = privGroup->gr_gid;
310 if (privUID == 0 || privGID == 0) {
311 NLOG_WARNING(logger, "WARNING: nawarun will be running as user or group root. Security risk!")
312 } else {
313 // get supplementary groups for non-root user
314 int n = 0;
315 getgrouplist(username.c_str(), privGID, nullptr, &n);
316 supplementaryGroups.resize(n, 0);
317 if (getgrouplist(username.c_str(), privGID, oss::getGIDPtrForGetgrouplist(&supplementaryGroups[0]), &n) != n) {
318 NLOG_WARNING(logger, "WARNING: Could not get supplementary groups for user " << username)
319 supplementaryGroups = {privGID};
320 }
321 }
322 return make_tuple(privUID, privGID, supplementaryGroups);
323 }
324
325 NLOG_WARNING(logger, "WARNING: Not starting as root, cannot set privileges.")
326 return nullopt;
327}
328
329void nawarun::replaceLogger(Log const& log) {
330 logger = log;
331}
332
340Parameters nawarun::parseCommandLine(int argc, char** argv) {
341 // start from arg 1 (as 0 is the program), iterate through all arguments and add valid options in the format
342 // --category:key=value to overrides
343 optional<string> configPath;
344 vector<pair<pair<string, string>, string>> overrides;
345 bool noConfigFile = false;
346 for (size_t i = 1; i < argc; ++i) {
347 string currentArg(argv[i]);
348
349 if (i == 1 && (currentArg == "--help" || currentArg == "-h")) {
350 printHelpAndExit();
351 }
352
353 if (i == 1 && (currentArg == "--version" || currentArg == "-v")) {
354 printVersionAndExit();
355 }
356
357 if (currentArg.substr(0, 2) == "--") {
358 auto idAndVal = utils::splitString(currentArg.substr(2), '=', true);
359 if (idAndVal.size() == 2) {
360 auto categoryAndKey = utils::splitString(idAndVal.at(0), ':', true);
361 string const& value = idAndVal.at(1);
362 if (categoryAndKey.size() == 2) {
363 string const& category = categoryAndKey.at(0);
364 string const& key = categoryAndKey.at(1);
365 overrides.push_back({{category, key}, value});
366 continue;
367 }
368 }
369 }
370
371 // last argument is interpreted as config file if it does not match the pattern
372 // if "--no-config-file" is given as the last argument, no config file is used
373 if (i == argc - 1) {
374 if (currentArg != "--no-config-file") {
375 configPath = currentArg;
376 } else {
377 noConfigFile = true;
378 }
379 } else {
380 NLOG_WARNING(logger, "WARNING: Invalid command line argument \"" << currentArg << "\" will be ignored")
381 }
382 }
383
384 // use config.ini in current directory if no config file was given and --no-config-file option is not set
385 if (!configPath && !noConfigFile) {
386 configPath = "config.ini";
387 }
388
389 return {configPath, overrides};
390}
391
392int nawarun::run(Parameters const& parameters) {
393 setUpSignalHandlers();
394
395 configFile = parameters.configFile;
396 optional<Config> config = loadConfig();
397 if (!config)
398 return 1;
399 config->override(parameters.configOverrides);
400
401 setTerminationTimeout(*config);
402 setUpLogging(*config);
403
404 // prepare privilege downgrade and check for errors (downgrade will happen after socket setup)
405 optional<PrivilegeDowngradeData> privilegeDowngradeData;
406 try {
407 privilegeDowngradeData = preparePrivilegeDowngrade(*config);
408 } catch (Exception const& e) {
409 NLOG_ERROR(logger, "Fatal Error: " << e.getMessage())
410 NLOG_DEBUG(logger, "Debug info: " << e.getDebugMessage())
411 return 1;
412 }
413
414 // load init and handleRequest symbols from app
415 init_t* appInit;
416 shared_ptr<HandleRequestFunctionWrapper> appHandleRequest;
417 try {
418 tie(appInit, appHandleRequest) = loadAppFunctions(*config);
419 } catch (Exception const& e) {
420 NLOG_ERROR(logger, "Fatal Error: " << e.getMessage())
421 NLOG_DEBUG(logger, "Debug info: " << e.getDebugMessage())
422 return 1;
423 }
424
425 // pass config, app function, and concurrency to RequestHandler
426 // already here to make (socket) preparation possible before privilege downgrade
427 auto concurrency = getConcurrency(*config);
428 try {
429 requestHandlerPtr = RequestHandler::newRequestHandler(appHandleRequest, *config, concurrency);
430 } catch (Exception const& e) {
431 NLOG_ERROR(logger, "Fatal Error: " << e.getMessage())
432 NLOG_DEBUG(logger, "Debug info: " << e.getDebugMessage())
433 return 1;
434 }
435
436 // do privilege downgrade if possible
437 if (privilegeDowngradeData) {
438 doPrivilegeDowngrade(*privilegeDowngradeData);
439 }
440
441 // before request handling starts, init app
442 {
443 AppInit appInitStruct(*config, concurrency);
444 auto initReturn = appInit(appInitStruct);
445
446 // init function of the app should return 0 on success
447 if (initReturn != 0) {
448 NLOG_ERROR(logger, "Fatal Error: App init function returned " << initReturn << " -- exiting.")
449 return 1;
450 }
451
452 // reconfigure request handler using access filters and (potentially altered by app init) config
453 requestHandlerPtr->reconfigure(nullopt, appInitStruct.accessFilters(), appInitStruct.config());
454 }
455
456 try {
457 requestHandlerPtr->start();
458 readyToReconfigure = true;
459 } catch (const Exception& e) {
460 NLOG_ERROR(logger, "Fatal Error: " << e.getMessage())
461 NLOG_DEBUG(logger, "Debug info: " << e.getDebugMessage())
462 }
463
464 requestHandlerPtr->join();
465
466 // the request handler has to be destroyed before unloading the app (using dlclose)
467 requestHandlerPtr.reset(nullptr);
468
469 return 0;
470}
Reader for config files and accessor to config values.
Exception class that can be used by apps to catch errors resulting from nawa function calls.
Simple class for (not (yet) thread-safe) logging to stderr or to any other output stream.
#define NLOG_WARNING(Logger, Message)
Definition: Log.h:189
#define NLOG_INFO(Logger, Message)
Definition: Log.h:195
#define NLOG_DEBUG(Logger, Message)
Definition: Log.h:201
#define NLOG_ERROR(Logger, Message)
Definition: Log.h:183
Handles and serves incoming requests via the NAWA app.
This file will be configured by CMake and contains the necessary properties to ensure that a loaded a...
const int nawa_version_major
Definition: application.h:33
const int nawa_version_minor
Definition: application.h:34
AccessFilterList & accessFilters()
Definition: AppInit.cpp:47
Config & config()
Definition: AppInit.cpp:43
bool isSet(std::pair< std::string, std::string > const &key) const
Definition: Config.cpp:81
virtual std::string getMessage() const noexcept
Definition: Exception.h:71
virtual std::string getDebugMessage() const noexcept
Definition: Exception.h:79
Definition: Log.h:38
Config loadConfig()
Definition: main.cpp:34
int * getGIDPtrForGetgrouplist(gid_t *in)
Definition: oss.h:65
std::vector< std::string > splitString(std::string str, char delimiter, bool ignoreEmpty=false)
Definition: utils.cpp:448
Definition: AppInit.h:31
std::vector< ConfigOverride > configOverrides
Definition: nawarun.h:42
Parameters parseCommandLine(int argc, char **argv)
Definition: nawarun.cpp:340
std::optional< std::string > configFile
Definition: nawarun.h:41
int(nawa::Connection &) handleRequest_t
Definition: nawarun.h:47
void reload(int signum)
Definition: nawarun.cpp:229
int(nawa::AppInit &) init_t
Definition: nawarun.h:46
unsigned int getConcurrency(nawa::Config const &config)
Definition: nawarun.cpp:184
int run(Parameters const &parameters)
Definition: nawarun.cpp:392
std::optional< PrivilegeDowngradeData > preparePrivilegeDowngrade(nawa::Config const &config)
Definition: nawarun.cpp:287
std::pair< init_t *, std::shared_ptr< nawa::HandleRequestFunctionWrapper > > loadAppFunctions(nawa::Config const &config)
Definition: nawarun.cpp:200
void replaceLogger(nawa::Log const &log)
Definition: nawarun.cpp:329
std::tuple< uid_t, gid_t, std::vector< gid_t > > PrivilegeDowngradeData
Definition: nawarun.h:36
Definitions for the nawarun implementation.
This file contains helpers for operating-system specific stuff.
Contains useful functions that improve the readability and facilitate maintenance of the NAWA code.