From add93a178202d4011ecacec29b8d90dd0523b5b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Ludwig?= Date: Wed, 10 Apr 2013 20:03:35 +0200 Subject: [PATCH] Implement automatic help screen functionality, adjust code style and move privilege lowering code to vibe.core.core. Privilege lowering was broken as it happened before any calls to listenTCP() or similar functions. It is now executed explicitly using lowerPrivileges(). Also processCommandLineArgs() is now fixed to at least perform in a reasonably similar way to how it used to do so that compatibility is not silently broken. See also #211. --- source/vibe/appmain.d | 13 ++- source/vibe/core/args.d | 200 +++++++++++++++++++++++++------------- source/vibe/core/core.d | 123 +++++++++++++++++++++-- source/vibe/core/log.d | 14 ++- source/vibe/core/setuid.d | 105 -------------------- source/vibe/http/server.d | 7 +- source/vibe/vibe.d | 1 - 7 files changed, 269 insertions(+), 194 deletions(-) delete mode 100644 source/vibe/core/setuid.d diff --git a/source/vibe/appmain.d b/source/vibe/appmain.d index f6f6b8c548..e28e67c4af 100644 --- a/source/vibe/appmain.d +++ b/source/vibe/appmain.d @@ -19,8 +19,8 @@ */ module vibe.appmain; -import vibe.core.args : finalizeCommandLineArgs; -import vibe.core.core : runEventLoop; +import vibe.core.args : finalizeCommandLineOptions; +import vibe.core.core : runEventLoop, lowerPrivileges; import vibe.core.log; // only include main if VibeCustomMain is not set @@ -38,7 +38,14 @@ int main() logInfo("All unit tests were successful."); return 0; } else { - finalizeCommandLineArgs(); + try if (!finalizeCommandLineOptions()) return 0; + catch (Exception e) { + logDiagnostic("Error processing command line: %s", e.msg); + return 1; + } + + lowerPrivileges(); + logInfo("Running event loop..."); debug { return runEventLoop(); diff --git a/source/vibe/core/args.d b/source/vibe/core/args.d index a89df84393..aa9604fbc2 100644 --- a/source/vibe/core/args.d +++ b/source/vibe/core/args.d @@ -16,6 +16,8 @@ module vibe.core.args; import vibe.core.log; import vibe.data.json; +import std.algorithm : any, array, map, sort; +import std.array : replicate; import std.exception; import std.file; import std.getopt; @@ -24,69 +26,143 @@ import std.string; import core.runtime; -/// Deprecated. Currently does nothing - Vibe will parse arguments -/// automatically on startup. Call $(D finalizeCommandLineArgs) from your -/// $(D main()) if you use a custom one, to check for unrecognized options. + +/** + Deprecated. Removes any recognized arguments from args leaving any unrecognized options. + + Note that vibe.d parses all options on start up and calling this function is not necessary. + It is recommended to use + Currently does nothing - Vibe will parse arguments + automatically on startup. Call $(D finalizeCommandLineArgs) from your + $(D main()) if you use a custom one, to check for unrecognized options. +*/ deprecated void processCommandLineArgs(ref string[] args) { - finalizeCommandLineArgs(); + args = g_args.dup; } + /** Finds and reads an option from the configuration file or command line. - Command line options take precedence. + + Command line options take precedence over configuration file entries. Params: - names = Option names. Separate multiple name variants with $(D |), - as with $(D std.getopt). + names = Option names. Separate multiple name variants with "|", + as for $(D std.getopt). pvalue = Pointer to store the value. Unchanged if value was not found. Returns: $(D true) if the value was found, $(D false) otherwise. */ -bool getOption(T)(string names, T* pvalue) +bool getOption(T)(string names, T* pvalue, string help_text) { - if (!args) // May happen due to http://d.puremagic.com/issues/show_bug.cgi?id=9881 - init(); - - auto oldLength = args.length; - getopt(args, getoptConfig, names, pvalue); - if (oldLength != args.length) // getopt found it - { - static void removeArg(string names) - { - T v; - getopt(args, getoptConfig, names, &v); - } - argRemovers[names] = &removeArg; - return true; - } + // May happen due to http://d.puremagic.com/issues/show_bug.cgi?id=9881 + if (!g_args) init(); + + OptionInfo info; + info.names = names.split("|").sort!((a, b) => a.length < b.length)().array(); + info.hasValue = !is(T == bool); + info.helpText = help_text; + assert(!g_options.any!(o => o.names == info.names)(), "getOption() may only be called once per option name."); + g_options ~= info; - if (haveConfig) - foreach (name; names.split("|")) - if (auto pv = name in config) - { + getopt(g_args, getoptConfig, names, pvalue); + + if (g_haveConfig) { + foreach (name; info.names) + if (auto pv = name in g_config) { *pvalue = pv.get!T; return true; } + } return false; } -/// Checks for unrecognized options. -/// Called automatically from $(D vibe.appmain). -void finalizeCommandLineArgs() + +/** + Prints a help screen consisting of all options encountered in getOption calls. +*/ +void printCommandLineHelp() +{ + enum dcolumn = 20; + enum ncolumns = 80; + + logInfo("Usage: %s \n", g_args[0]); + foreach (opt; g_options) { + string shortopt; + string[] longopts; + if (opt.names[0].length == 1 && !opt.hasValue) { + shortopt = "-"~opt.names[0]; + longopts = opt.names[1 .. $]; + } else { + shortopt = " "; + longopts = opt.names; + } + + string optionString(string name) + { + if (name.length == 1) return "-"~name~(opt.hasValue ? " " : ""); + else return "--"~name~(opt.hasValue ? "=" : ""); + } + + auto optstr = format(" %s %s", shortopt, longopts.map!optionString().join(", ")); + if (optstr.length < dcolumn) optstr ~= replicate(" ", dcolumn - optstr.length); + + auto indent = replicate(" ", dcolumn+1); + auto desc = wrap(opt.helpText, ncolumns - dcolumn - 2, optstr.length > dcolumn ? indent : "", indent).stripRight(); + + if (optstr.length > dcolumn) + logInfo("%s\n%s", optstr, desc); + else logInfo("%s %s", optstr, desc); + } +} + + +/** + Checks for unrecognized command line options and display a help screen. + + This function is called automatically from vibe.appmain to check for + correct command line usage. It will print a help screen in case of + unrecognized options. + + Returns: + If "--help" was passed, the function returns false. In all other + cases either true is returned or an exception is thrown. +*/ +bool finalizeCommandLineOptions() { - foreach (names, fn; argRemovers) - fn(names); - enforce(args.length<=1, "Unrecognized command-line parameter: " ~ args[1]); + if (g_args.length > 1) { + logError("Unrecognized command line option: %s\n", g_args[1]); + printCommandLineHelp(); + throw new Exception("Unrecognized command line option."); + } + + if (g_help) { + printCommandLineHelp(); + return false; + } + + return true; } -private: -enum configName = "vibe.conf"; +private struct OptionInfo { + string[] names; + bool hasValue; + string helpText; +} + +private { + __gshared string[] g_args; + __gshared bool g_haveConfig; + __gshared Json g_config; + __gshared OptionInfo[] g_options; + __gshared bool g_help; +} -string[] getConfigPaths() +private string[] getConfigPaths() { string[] result = [""]; import std.process : environment; @@ -97,42 +173,32 @@ string[] getConfigPaths() return result; } -shared static this() +// this is invoked by the first getOption call (at least vibe.core will porform one) +private void init() { - if (!args) - init(); -} - -void init() -{ - args = Runtime.args; - - auto searchPaths = getConfigPaths(); - foreach (searchPath; searchPaths) - { - auto configPath = buildPath(searchPath, configName); - if (configPath.exists) - { - scope(failure) logError("Failed to parse config file %s:", configPath); - auto configText = configPath.readText(); - config = configText.parseJson(); - haveConfig = true; + g_args = Runtime.args.dup; + + // TODO: let different config files override induvidual fields + auto searchpaths = getConfigPaths(); + foreach (spath; searchpaths) { + auto cpath = buildPath(spath, configName); + if (cpath.exists) { + scope(failure) logError("Failed to parse config file %s.", cpath); + auto text = cpath.readText(); + g_config = text.parseJson(); + g_haveConfig = true; + break; } } - if (!haveConfig) - logDebug("No config file found in %s", searchPaths); + if (!g_haveConfig) + logDiagnostic("No config file found in %s", searchpaths); + + getOption("h|help", &g_help, "Prints this help screen."); } -template ValueTuple(T...) { alias T ValueTuple; } -alias getoptConfig = ValueTuple!( - std.getopt.config.passThrough, - std.getopt.config.bundling, -); +private enum configName = "vibe.conf"; -__gshared: +private template ValueTuple(T...) { alias T ValueTuple; } -string[] args; -bool haveConfig; -Json config; -void function(string)[string] argRemovers; +private alias getoptConfig = ValueTuple!(std.getopt.config.passThrough, std.getopt.config.bundling); diff --git a/source/vibe/core/core.d b/source/vibe/core/core.d index fb995206f3..33dc6918c2 100644 --- a/source/vibe/core/core.d +++ b/source/vibe/core/core.d @@ -9,6 +9,7 @@ module vibe.core.core; public import vibe.core.driver; +import vibe.core.args; import vibe.core.concurrency; import vibe.core.log; import vibe.utils.array; @@ -29,10 +30,30 @@ import vibe.core.drivers.libevent2; import vibe.core.drivers.win32; import vibe.core.drivers.winrt; -version(Posix){ +version(Posix) +{ import core.sys.posix.signal; + import core.sys.posix.unistd; + import core.sys.posix.pwd; + + static if (__traits(compiles, {import core.sys.posix.grp; getgrgid(0);})) { + import core.sys.posix.grp; + } else { + extern (C) { + struct group { + char* gr_name; + char* gr_passwd; + gid_t gr_gid; + char** gr_mem; + } + group* getgrgid(gid_t); + group* getgrnam(in char*); + } + } } -version(Windows){ + +version (Windows) +{ import core.stdc.signal; } @@ -354,6 +375,30 @@ void enableWorkerThreads() } } + +/** + Sets the effective user and group ID to the ones configured for privilege lowering. + + This function is useful for services run as root to give up on the privileges that + they only need for initialization (such as listening on ports <= 1024 or opening + system log files). +*/ +void lowerPrivileges() +{ + auto uname = s_privilegeLoweringUserName; + auto gname = s_privilegeLoweringGroupName; + if (uname || gname) { + static bool tryParse(T)(string s, out T n) { import std.conv; n = parse!T(s); return s.length==0; } + int uid = -1, gid = -1; + if (uname && !tryParse(uname, uid)) uid = getUID(uname); + if (gname && !tryParse(gname, gid)) gid = getGID(gname); + setUID(uid, gid); + } else if (isRoot()) { + logWarn("Vibe was run as root, and no user/group has been specified for privilege lowering."); + } +} + + /** A version string representing the current vibe version */ @@ -565,6 +610,9 @@ private { CoreTask[] s_availableFibers; size_t s_availableFibersCount; size_t s_fiberCount; + + string s_privilegeLoweringUserName; + string s_privilegeLoweringGroupName; } // per process setup @@ -613,6 +661,9 @@ shared static this() logTrace("setup gc"); s_core.setupGcTimer(); } + + getOption("uid|user", &s_privilegeLoweringUserName, "Sets the user name or id used for privilege lowering."); + getOption("gid|group", &s_privilegeLoweringGroupName, "Sets the group name or id used for privilege lowering."); } shared static ~this() @@ -708,22 +759,74 @@ private void handleWorkerTasks() logDebug("worker task exit"); } -private extern(C) nothrow + +private extern(C) void extrap() +nothrow { + logTrace("exception trap"); +} + +private extern(C) void onSignal(int signal) +nothrow { + logInfo("Received signal %d. Shutting down.", signal); + + if( s_eventLoopRunning ) try exitEventLoop(); catch(Exception e) {} + else exit(1); +} + +private extern(C) void onBrokenPipe(int signal) +nothrow { + logTrace("Broken pipe."); +} + +version(Posix) { - void extrap() + private bool isRoot() { return geteuid() == 0; } + + private void setUID(int uid, int gid) { - logTrace("exception trap"); + logInfo("Lowering privileges to uid=%d, gid=%d...", uid, gid); + if (gid >= 0) { + enforce(getgrgid(gid) !is null, "Invalid group id!"); + enforce(setegid(gid) == 0, "Error setting group id!"); + } + //if( initgroups(const char *user, gid_t group); + if (uid >= 0) { + enforce(getpwuid(uid) !is null, "Invalid user id!"); + enforce(seteuid(uid) == 0, "Error setting user id!"); + } } - nothrow void onSignal(int signal) + private int getUID(string name) { - logInfo("Received signal %d. Shutting down.", signal); + auto pw = getpwnam(name.toStringz()); + enforce(pw !is null, "Unknown user name: "~name); + return pw.pw_uid; + } - if( s_eventLoopRunning ) try exitEventLoop(); catch(Exception e) {} - else exit(1); + private int getGID(string name) + { + auto gr = getgrnam(name.toStringz()); + enforce(gr !is null, "Unknown group name: "~name); + return gr.gr_gid; + } +} else version(Windows){ + private bool isRoot() { return false; } + + private void setUID(int uid, int gid) + { + enforce(false, "UID/GID not supported on Windows."); } - void onBrokenPipe(int signal) + private int getUID(string name) { + enforce(false, "Privilege lowering not supported on Windows."); + assert(false); + } + + private int getGID(string name) + { + enforce(false, "Privilege lowering not supported on Windows."); + assert(false); } } + diff --git a/source/vibe/core/log.d b/source/vibe/core/log.d index de72966c33..04d6b9a189 100644 --- a/source/vibe/core/log.d +++ b/source/vibe/core/log.d @@ -113,7 +113,11 @@ nothrow { ll.log(msg); } } - } catch(Exception) assert(false); + } catch (Exception e) { + try writefln("Error during logging: %s", e.toString()); + catch(Exception) {} + assert(false, "Exception during logging: "~e.msg); + } } /// Specifies the log level for a particular log message. @@ -396,10 +400,10 @@ package void initializeLogModule() ss_loggers ~= ss_stdoutLogger; bool[4] verbose; - getOption("verbose|v" , &verbose[0]); - getOption("vverbose|vv", &verbose[1]); - getOption("vvv" , &verbose[2]); - getOption("vvvv" , &verbose[3]); + getOption("verbose|v" , &verbose[0], "Enables diagnostic messages (verbosity level 1)."); + getOption("vverbose|vv", &verbose[1], "Enables debugging output (verbosity level 2)."); + getOption("vvv" , &verbose[2], "Enables high frequency debugging output (verbosity level 3)."); + getOption("vvvv" , &verbose[3], "Enables high frequency trace output (verbosity level 4)."); foreach_reverse (i, v; verbose) if (v) { diff --git a/source/vibe/core/setuid.d b/source/vibe/core/setuid.d deleted file mode 100644 index 5ec1288e04..0000000000 --- a/source/vibe/core/setuid.d +++ /dev/null @@ -1,105 +0,0 @@ -/** - Handles and applies the uid/gid/user/group configuration settings. - - Copyright: © 2012 RejectedSoftware e.K. - License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. - Authors: Sönke Ludwig -*/ -module vibe.core.setuid; - -import vibe.core.log; -import vibe.core.args; - -import std.exception; - -version(Posix) -{ - import core.sys.posix.unistd; - import core.sys.posix.pwd; - - static if( __traits(compiles, {import core.sys.posix.grp; getgrgid(0);}) ){ - import core.sys.posix.grp; - } else { - extern(C){ - struct group - { - char* gr_name; - char* gr_passwd; - gid_t gr_gid; - char** gr_mem; - } - group* getgrgid(gid_t); - group* getgrnam(in char*); - } - } - - private bool isRoot() { return geteuid()==0; } - - private void setUID(int uid, int gid) - { - logInfo("Lowering privileges to uid=%d, gid=%d...", uid, gid); - if( gid >= 0 ){ - enforce(getgrgid(gid) !is null, "Invalid group id!"); - enforce(setegid(gid) == 0, "Error setting group id!"); - } - //if( initgroups(const char *user, gid_t group); - if( uid >= 0 ){ - enforce(getpwuid(uid) !is null, "Invalid user id!"); - enforce(seteuid(uid) == 0, "Error setting user id!"); - } - } - - private int getUID(string name) - { - auto pw = getpwnam(name.toStringz()); - enforce(pw !is null, "Unknown user name: "~name); - return pw.pw_uid; - } - - private int getGID(string name) - { - auto gr = getgrnam(name.toStringz()); - enforce(gr !is null, "Unknown group name: "~name); - return gr.gr_gid; - } -} else version(Windows){ - private bool isRoot() { return false; } - - private void setUID(int uid, int gid) - { - enforce(false, "UID/GID not supported on Windows."); - } - - private int getUID(string name) - { - enforce(false, "Privilege lowering not supported on Windows."); - assert(false); - } - - private int getGID(string name) - { - enforce(false, "Privilege lowering not supported on Windows."); - assert(false); - } -} - -shared static this() -{ - string uname, gname; - getOption("uid|user" , &uname); - getOption("gid|group", &gname); - - if (uname || gname) - { - static bool tryParse(T)(string s, out T n) { import std.conv; n = parse!T(s); return s.length==0; } - int uid = -1, gid = -1; - if (uname && !tryParse(uname, uid)) uid = getUID(uname); - if (gname && !tryParse(gname, gid)) gid = getGID(gname); - setUID(uid, gid); - } - else - { - if (isRoot()) - logWarn("Vibe was run as root, and no user/group has been specified for privilege lowering."); - } -} diff --git a/source/vibe/http/server.d b/source/vibe/http/server.d index ccd91f1433..44a5472f6d 100644 --- a/source/vibe/http/server.d +++ b/source/vibe/http/server.d @@ -1374,8 +1374,9 @@ private string formatAlloc(ARGS...)(Allocator alloc, string fmt, ARGS args) shared static this() { - string disthost=s_distHost; ushort distport = s_distPort; - getOption("disthost|d", &disthost); - getOption("distport" , &distport); + string disthost = s_distHost; + ushort distport = s_distPort; + getOption("disthost|d", &disthost, "Sets the name of a vibedist server to use for load balancing."); + getOption("distport", &distport, "Sets the port used for load balancing."); setVibeDistHost(disthost, distport); } diff --git a/source/vibe/vibe.d b/source/vibe/vibe.d index 0feb809dab..d8c8582859 100644 --- a/source/vibe/vibe.d +++ b/source/vibe/vibe.d @@ -17,7 +17,6 @@ public import vibe.core.core; public import vibe.core.file; public import vibe.core.log; public import vibe.core.net; -public import vibe.core.setuid; public import vibe.core.sync; public import vibe.crypto.passwordhash; public import vibe.data.bson;