NAWA 0.8
Web Application Framework for C++
nawarun.cpp
Go to the documentation of this file.
1
6/*
7 * Copyright (C) 2019-2021 Tobias Flaig.
8 *
9 * This file is part of nawa.
10 *
11 * nawa is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License,
13 * version 3, as published by the Free Software Foundation.
14 *
15 * nawa is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU Lesser General Public License for more details.
19 *
20 * You should have received a copy of the GNU Lesser General Public License
21 * along with nawa. If not, see <https://www.gnu.org/licenses/>.
22 */
23
24#include <atomic>
25#include <csignal>
26#include <dlfcn.h>
27#include <grp.h>
28#include <nawa/Application.h>
29#include <nawa/Exception.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";
149 exit(0);
150 }
151
152 void setUpSignalHandlers() {
153 signal(SIGINT, shutdown);
154 signal(SIGTERM, shutdown);
155 signal(SIGUSR1, shutdown);
156 signal(SIGHUP, reload);
157 }
158
159 void setUpLogging(Config const& config) {
160 auto configuredLogLevel = config[{"logging", "level"}];
161 if (configuredLogLevel == "off") {
162 Log::setOutputLevel(Log::Level::OFF);
163 } else if (configuredLogLevel == "error") {
164 Log::setOutputLevel(Log::Level::ERROR);
165 } else if (configuredLogLevel == "warning") {
166 Log::setOutputLevel(Log::Level::WARNING);
167 } else if (configuredLogLevel == "debug") {
168 Log::setOutputLevel(Log::Level::DEBUG);
169 }
170 if (config[{"logging", "extended"}] == "on") {
171 Log::setExtendedFormat(true);
172 }
173 Log::lockStream();
174 }
175}// namespace
176
177unsigned int nawarun::getConcurrency(Config const& config) {
178 double cReal;
179 try {
180 cReal = config.isSet({"system", "threads"})
181 ? stod(config[{"system", "threads"}])
182 : 1.0;
183 } catch (invalid_argument& e) {
184 NLOG_WARNING(logger, "WARNING: Invalid value given for system/concurrency given in the config file.")
185 cReal = 1.0;
186 }
187 if (config[{"system", "concurrency"}] == "hardware") {
188 cReal = max(1.0, thread::hardware_concurrency() * cReal);
189 }
190 return static_cast<unsigned int>(cReal);
191}
192
193pair<init_t*, shared_ptr<HandleRequestFunctionWrapper>> nawarun::loadAppFunctions(Config const& config) {
194 // load application init function
195 string appPath = config[{"application", "path"}];
196 if (appPath.empty()) {
197 throw Exception(__FUNCTION__, 1, "Application path not set in config file.");
198 }
199 void* appOpen = dlopen(appPath.c_str(), RTLD_LAZY);
200 if (!appOpen) {
201 throw Exception(__FUNCTION__, 2, "Application file could not be loaded.", dlerror());
202 }
203
204 // reset dl errors
205 dlerror();
206
207 // load symbols and check for errors
208 // first load nawa_version_major (defined in Application.h, included in Connection.h)
209 // the version the app has been compiled against should match the version of this nawarun
210 string appVersionError = "Could not read nawa version from application.";
211 auto appNawaVersionMajor = (int*) loadAppSymbol(appOpen, "nawa_version_major", appVersionError);
212 auto appNawaVersionMinor = (int*) loadAppSymbol(appOpen, "nawa_version_minor", appVersionError);
213 if (*appNawaVersionMajor != nawa_version_major || *appNawaVersionMinor != nawa_version_minor) {
214 throw Exception(__FUNCTION__, 3, "App has been compiled against another version of NAWA.");
215 }
216 auto appInit = (init_t*) loadAppSymbol(appOpen, "init", "Could not load init function from application.");
217 auto appHandleRequest = (handleRequest_t*) loadAppSymbol(appOpen, "handleRequest",
218 "Could not load handleRequest function from application.");
219 return {appInit, make_shared<HandleRequestFunctionWrapper>(appHandleRequest, appOpen, closeApp)};
220}
221
222void nawarun::reload(int signum) {
223 if (!configFile) {
224 NLOG_WARNING(logger, "WARNING: Reloading is not supported without config file and will therefore not "
225 "happen.")
226 return;
227 }
228
229 if (requestHandlerPtr && readyToReconfigure) {
230 NLOG_INFO(logger, "Reloading config and app on signal " << signum)
231 readyToReconfigure = false;
232
233 optional<Config> config = loadConfig(true);
234 if (!config) {
235 readyToReconfigure = true;
236 return;
237 }
238
239 // set new termination timeout, if given
240 setTerminationTimeout(*config);
241
242 init_t* appInit;
243 shared_ptr<HandleRequestFunctionWrapper> appHandleRequest;
244 try {
245 tie(appInit, appHandleRequest) = loadAppFunctions(*config);
246 } catch (Exception const& e) {
247 NLOG_ERROR(logger, "ERROR: Could not reload app: " << e.getMessage())
248 NLOG_DEBUG(logger, "Debug info: " << e.getDebugMessage())
249 NLOG_WARNING(logger, "WARNING: Configuration will be reloaded anyway")
250
251 // just reload config, not app
252 requestHandlerPtr->reconfigure(nullopt, nullopt, config);
253 readyToReconfigure = true;
254 return;
255 }
256
257 {
258 AppInit appInitStruct(*config, getConcurrency(*config));
259 auto initReturn = appInit(appInitStruct);
260
261 // init function of the app should return 0 on success, otherwise we will not reload
262 if (initReturn != 0) {
263 NLOG_ERROR(logger,
264 "ERROR: App init function returned " << initReturn << " -- cancelling reload of app.")
265 NLOG_WARNING(logger, "WARNING: Configuration will be reloaded anyway")
266
267 // just reload config, not app
268 requestHandlerPtr->reconfigure(nullopt, nullopt, config);
269 readyToReconfigure = true;
270 return;
271 }
272
273 // reconfigure everything
274 requestHandlerPtr->reconfigure(appHandleRequest, appInitStruct.accessFilters(), appInitStruct.config());
275 readyToReconfigure = true;
276 }
277 }
278}
279
280optional<PrivilegeDowngradeData> nawarun::preparePrivilegeDowngrade(Config const& config) {
281 auto initialUID = getuid();
282 uid_t privUID = -1;
283 gid_t privGID = -1;
284 vector<gid_t> supplementaryGroups;
285
286 if (initialUID == 0) {
287 if (!config.isSet({"privileges", "user"}) || !config.isSet({"privileges", "group"})) {
288 throw Exception(__PRETTY_FUNCTION__, 1,
289 "Running as root and user or group for privilege downgrade is not set in the configuration.");
290 }
291 string username = config[{"privileges", "user"}];
292 string groupname = config[{"privileges", "group"}];
293 passwd* privUser;
294 group* privGroup;
295 privUser = getpwnam(username.c_str());
296 privGroup = getgrnam(groupname.c_str());
297 if (privUser == nullptr || privGroup == nullptr) {
298 throw Exception(__PRETTY_FUNCTION__, 2,
299 "The user or group name for privilege downgrade given in the configuration is invalid.");
300 }
301 privUID = privUser->pw_uid;
302 privGID = privGroup->gr_gid;
303 if (privUID == 0 || privGID == 0) {
304 NLOG_WARNING(logger, "WARNING: nawarun will be running as user or group root. Security risk!")
305 } else {
306 // get supplementary groups for non-root user
307 int n = 0;
308 getgrouplist(username.c_str(), privGID, nullptr, &n);
309 supplementaryGroups.resize(n, 0);
310 if (getgrouplist(username.c_str(), privGID, oss::getGIDPtrForGetgrouplist(&supplementaryGroups[0]), &n) != n) {
311 NLOG_WARNING(logger, "WARNING: Could not get supplementary groups for user " << username)
312 supplementaryGroups = {privGID};
313 }
314 }
315 return make_tuple(privUID, privGID, supplementaryGroups);
316 }
317
318 NLOG_WARNING(logger, "WARNING: Not starting as root, cannot set privileges.")
319 return nullopt;
320}
321
322void nawarun::replaceLogger(Log const& log) {
323 logger = log;
324}
325
333Parameters nawarun::parseCommandLine(int argc, char** argv) {
334 // start from arg 1 (as 0 is the program), iterate through all arguments and add valid options in the format
335 // --category:key=value to overrides
336 optional<string> configPath;
337 vector<pair<pair<string, string>, string>> overrides;
338 bool noConfigFile = false;
339 for (size_t i = 1; i < argc; ++i) {
340 string currentArg(argv[i]);
341
342 if (i == 1 && (currentArg == "--help" || currentArg == "-h")) {
343 printHelpAndExit();
344 }
345
346 if (currentArg.substr(0, 2) == "--") {
347 auto idAndVal = utils::splitString(currentArg.substr(2), '=', true);
348 if (idAndVal.size() == 2) {
349 auto categoryAndKey = utils::splitString(idAndVal.at(0), ':', true);
350 string const& value = idAndVal.at(1);
351 if (categoryAndKey.size() == 2) {
352 string const& category = categoryAndKey.at(0);
353 string const& key = categoryAndKey.at(1);
354 overrides.push_back({{category, key}, value});
355 continue;
356 }
357 }
358 }
359
360 // last argument is interpreted as config file if it does not match the pattern
361 // if "--no-config-file" is given as the last argument, no config file is used
362 if (i == argc - 1) {
363 if (currentArg != "--no-config-file") {
364 configPath = currentArg;
365 } else {
366 noConfigFile = true;
367 }
368 } else {
369 NLOG_WARNING(logger, "WARNING: Invalid command line argument \"" << currentArg << "\" will be ignored")
370 }
371 }
372
373 // use config.ini in current directory if no config file was given and --no-config-file option is not set
374 if (!configPath && !noConfigFile) {
375 configPath = "config.ini";
376 }
377
378 return {configPath, overrides};
379}
380
381int nawarun::run(Parameters const& parameters) {
382 setUpSignalHandlers();
383
384 configFile = parameters.configFile;
385 optional<Config> config = loadConfig();
386 if (!config)
387 return 1;
388 config->override(parameters.configOverrides);
389
390 setTerminationTimeout(*config);
391 setUpLogging(*config);
392
393 // prepare privilege downgrade and check for errors (downgrade will happen after socket setup)
394 optional<PrivilegeDowngradeData> privilegeDowngradeData;
395 try {
396 privilegeDowngradeData = preparePrivilegeDowngrade(*config);
397 } catch (Exception const& e) {
398 NLOG_ERROR(logger, "Fatal Error: " << e.getMessage())
399 NLOG_DEBUG(logger, "Debug info: " << e.getDebugMessage())
400 return 1;
401 }
402
403 // load init and handleRequest symbols from app
404 init_t* appInit;
405 shared_ptr<HandleRequestFunctionWrapper> appHandleRequest;
406 try {
407 tie(appInit, appHandleRequest) = loadAppFunctions(*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 // pass config, app function, and concurrency to RequestHandler
415 // already here to make (socket) preparation possible before privilege downgrade
416 auto concurrency = getConcurrency(*config);
417 try {
418 requestHandlerPtr = RequestHandler::newRequestHandler(appHandleRequest, *config, concurrency);
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 // do privilege downgrade if possible
426 if (privilegeDowngradeData) {
427 doPrivilegeDowngrade(*privilegeDowngradeData);
428 }
429
430 // before request handling starts, init app
431 {
432 AppInit appInitStruct(*config, concurrency);
433 auto initReturn = appInit(appInitStruct);
434
435 // init function of the app should return 0 on success
436 if (initReturn != 0) {
437 NLOG_ERROR(logger, "Fatal Error: App init function returned " << initReturn << " -- exiting.")
438 return 1;
439 }
440
441 // reconfigure request handler using access filters and (potentially altered by app init) config
442 requestHandlerPtr->reconfigure(nullopt, appInitStruct.accessFilters(), appInitStruct.config());
443 }
444
445 try {
446 requestHandlerPtr->start();
447 readyToReconfigure = true;
448 } catch (const Exception& e) {
449 NLOG_ERROR(logger, "Fatal Error: " << e.getMessage())
450 NLOG_DEBUG(logger, "Debug info: " << e.getDebugMessage())
451 }
452
453 requestHandlerPtr->join();
454
455 // the request handler has to be destroyed before unloading the app (using dlclose)
456 requestHandlerPtr.reset(nullptr);
457
458 return 0;
459}
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
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.
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
std::vector< std::string > splitString(std::string str, char delimiter, bool ignoreEmpty=false)
Definition: utils.cpp:418
Definition: AppInit.h:31
Contains useful functions that improve the readability and facilitate maintenance of the NAWA code.