Factor out service directory option processing
[oweals/dinit.git] / src / includes / load-service.h
1 #include <iostream>
2 #include <list>
3 #include <limits>
4 #include <csignal>
5 #include <cstring>
6
7 #include <sys/types.h>
8 #include <sys/time.h>
9 #include <sys/resource.h>
10 #include <grp.h>
11 #include <pwd.h>
12
13 struct service_flags_t
14 {
15     // on-start flags:
16     bool rw_ready : 1;  // file system should be writable once this service starts
17     bool log_ready : 1; // syslog should be available once this service starts
18
19     // Other service options flags:
20     bool no_sigterm : 1;  // do not send SIGTERM
21     bool runs_on_console : 1;  // run "in the foreground"
22     bool starts_on_console : 1; // starts in the foreground
23     bool shares_console : 1;    // run on console, but not exclusively
24     bool pass_cs_fd : 1;  // pass this service a control socket connection via fd
25     bool start_interruptible : 1; // the startup of this service process is ok to interrupt with SIGINT
26     bool skippable : 1;   // if interrupted the service is skipped (scripted services)
27     bool signal_process_only : 1;  // signal the session process, not the whole group
28
29     service_flags_t() noexcept : rw_ready(false), log_ready(false), no_sigterm(false),
30             runs_on_console(false), starts_on_console(false), shares_console(false),
31             pass_cs_fd(false), start_interruptible(false), skippable(false), signal_process_only(false)
32     {
33     }
34 };
35
36 // Resource limits for a particular service & particular resource
37 struct service_rlimits
38 {
39     int resource_id; // RLIMIT_xxx identifying resource
40     bool soft_set : 1;
41     bool hard_set : 1;
42     struct rlimit limits;
43
44     service_rlimits(int id) : resource_id(id), soft_set(0), hard_set(0), limits({0,0}) { }
45 };
46
47 // Exception while loading a service
48 class service_load_exc
49 {
50     public:
51     std::string service_name;
52     std::string exc_description;
53
54     protected:
55     service_load_exc(const std::string &serviceName, std::string &&desc) noexcept
56         : service_name(serviceName), exc_description(std::move(desc))
57     {
58     }
59 };
60
61 class service_not_found : public service_load_exc
62 {
63     public:
64     service_not_found(const std::string &serviceName) noexcept
65         : service_load_exc(serviceName, "Service description not found.")
66     {
67     }
68 };
69
70 class service_cyclic_dependency : public service_load_exc
71 {
72     public:
73     service_cyclic_dependency(const std::string &serviceName) noexcept
74         : service_load_exc(serviceName, "Has cyclic dependency.")
75     {
76     }
77 };
78
79 class service_description_exc : public service_load_exc
80 {
81     public:
82     service_description_exc(const std::string &serviceName, std::string &&extraInfo) noexcept
83         : service_load_exc(serviceName, std::move(extraInfo))
84     {
85     }
86 };
87
88 namespace dinit_load {
89
90 using string = std::string;
91 using string_iterator = std::string::iterator;
92
93 // exception thrown when encountering a syntax issue when reading a setting value
94 class setting_exception
95 {
96     std::string info;
97
98     public:
99     setting_exception(const std::string &&exc_info) : info(std::move(exc_info))
100     {
101     }
102
103     std::string &get_info()
104     {
105         return info;
106     }
107 };
108
109
110 // Utility function to skip white space. Returns an iterator at the
111 // first non-white-space position (or at end).
112 inline string_iterator skipws(string_iterator i, string_iterator end)
113 {
114     using std::locale;
115     using std::isspace;
116
117     while (i != end) {
118       if (! isspace(*i, locale::classic())) {
119         break;
120       }
121       ++i;
122     }
123     return i;
124 }
125
126 // Convert a signal name to the corresponding signal number
127 inline int signal_name_to_number(std::string &signame)
128 {
129     if (signame == "HUP") return SIGHUP;
130     if (signame == "INT") return SIGINT;
131     if (signame == "QUIT") return SIGQUIT;
132     if (signame == "USR1") return SIGUSR1;
133     if (signame == "USR2") return SIGUSR2;
134     if (signame == "KILL") return SIGKILL;
135     return -1;
136 }
137
138 // Read a setting name.
139 inline string read_setting_name(string_iterator & i, string_iterator end)
140 {
141     using std::locale;
142     using std::ctype;
143     using std::use_facet;
144
145     const ctype<char> & facet = use_facet<ctype<char> >(locale::classic());
146
147     string rval;
148     // Allow alphabetical characters, and dash (-) in setting name
149     while (i != end && (*i == '-' || *i == '.' || facet.is(ctype<char>::alpha, *i))) {
150         rval += *i;
151         ++i;
152     }
153     return rval;
154 }
155
156 // Read a setting value.
157 //
158 // In general a setting value is a single-line string. It may contain multiple parts
159 // separated by white space (which is normally collapsed). A hash mark - # - denotes
160 // the end of the value and the beginning of a comment (it should be preceded by
161 // whitespace).
162 //
163 // Part of a value may be quoted using double quote marks, which prevents collapse
164 // of whitespace and interpretation of most special characters (the quote marks will
165 // not be considered part of the value). A backslash can precede a character (such
166 // as '#' or '"' or another backslash) to remove its special meaning. Newline
167 // characters are not allowed in values and cannot be quoted.
168 //
169 // This function expects the string to be in an ASCII-compatible encoding (the "classic" locale).
170 //
171 // Throws setting_exception on error.
172 //
173 // Params:
174 //    service_name - the name of the service to which the setting applies
175 //    i  -  reference to string iterator through the line
176 //    end -   iterator at end of line (not including newline character if any)
177 //    part_positions -  list of <int,int> to which the position of each setting value
178 //                      part will be added as [start,end). May be null.
179 inline string read_setting_value(string_iterator & i, string_iterator end,
180         std::list<std::pair<unsigned,unsigned>> * part_positions = nullptr)
181 {
182     using std::locale;
183     using std::isspace;
184
185     i = skipws(i, end);
186
187     string rval;
188     bool new_part = true;
189     int part_start;
190
191     while (i != end) {
192         char c = *i;
193         if (c == '\"') {
194             if (new_part) {
195                 part_start = rval.length();
196                 new_part = false;
197             }
198             // quoted string
199             ++i;
200             while (i != end) {
201                 c = *i;
202                 if (c == '\"') break;
203                 else if (c == '\\') {
204                     // A backslash escapes the following character.
205                     ++i;
206                     if (i != end) {
207                         c = *i;
208                         rval += c;
209                     }
210                     else {
211                         throw setting_exception("Line end follows backslash escape character (`\\')");
212                     }
213                 }
214                 else {
215                     rval += c;
216                 }
217                 ++i;
218             }
219             if (i == end) {
220                 // String wasn't terminated
221                 throw setting_exception("Unterminated quoted string");
222             }
223         }
224         else if (c == '\\') {
225             if (new_part) {
226                 part_start = rval.length();
227                 new_part = false;
228             }
229             // A backslash escapes the next character
230             ++i;
231             if (i != end) {
232                 rval += *i;
233             }
234             else {
235                 throw setting_exception("Backslash escape (`\\') not followed by character");
236             }
237         }
238         else if (isspace(c, locale::classic())) {
239             if (! new_part && part_positions != nullptr) {
240                 part_positions->emplace_back(part_start, rval.length());
241                 new_part = true;
242             }
243             i = skipws(i, end);
244             if (i == end) break;
245             if (*i == '#') break; // comment
246             rval += ' ';  // collapse ws to a single space
247             continue;
248         }
249         else if (c == '#') {
250             // Possibly intended a comment; we require leading whitespace to reduce occurrence of accidental
251             // comments in setting values.
252             throw setting_exception("hashmark (`#') comment must be separated from setting value by whitespace");
253         }
254         else {
255             if (new_part) {
256                 part_start = rval.length();
257                 new_part = false;
258             }
259             rval += c;
260         }
261         ++i;
262     }
263
264     // Got to end:
265     if (part_positions != nullptr) {
266         part_positions->emplace_back(part_start, rval.length());
267     }
268
269     return rval;
270 }
271
272 // Parse a userid parameter which may be a numeric user ID or a username. If a name, the
273 // userid is looked up via the system user database (getpwnam() function). In this case,
274 // the associated group is stored in the location specified by the group_p parameter iff
275 // it is not null and iff it contains the value -1.
276 inline uid_t parse_uid_param(const std::string &param, const std::string &service_name, gid_t *group_p)
277 {
278     const char * uid_err_msg = "Specified user id contains invalid numeric characters "
279             "or is outside allowed range.";
280
281     // Could be a name or a numeric id. But we should assume numeric first, just in case
282     // a user manages to give themselves a username that parses as a number.
283     std::size_t ind = 0;
284     try {
285         // POSIX does not specify whether uid_t is an signed or unsigned, but regardless
286         // is is probably safe to assume that valid values are positive. We'll also assert
287         // that the value range fits within "unsigned long long" since it seems unlikely
288         // that would ever not be the case.
289         static_assert((uintmax_t)std::numeric_limits<uid_t>::max()
290                 <= (uintmax_t)std::numeric_limits<unsigned long long>::max(), "uid_t is too large");
291         unsigned long long v = std::stoull(param, &ind, 0);
292         if (v > static_cast<unsigned long long>(std::numeric_limits<uid_t>::max())
293                 || ind != param.length()) {
294             throw service_description_exc(service_name, uid_err_msg);
295         }
296         return v;
297     }
298     catch (std::out_of_range &exc) {
299         throw service_description_exc(service_name, uid_err_msg);
300     }
301     catch (std::invalid_argument &exc) {
302         // Ok, so it doesn't look like a number: proceed...
303     }
304
305     errno = 0;
306     struct passwd * pwent = getpwnam(param.c_str());
307     if (pwent == nullptr) {
308         // Maybe an error, maybe just no entry.
309         if (errno == 0) {
310             throw service_description_exc(service_name, "Specified user \"" + param
311                     + "\" does not exist in system database.");
312         }
313         else {
314             throw service_description_exc(service_name, std::string("Error accessing user database: ")
315                     + strerror(errno));
316         }
317     }
318
319     if (group_p && *group_p != (gid_t)-1) {
320         *group_p = pwent->pw_gid;
321     }
322
323     return pwent->pw_uid;
324 }
325
326 inline gid_t parse_gid_param(const std::string &param, const std::string &service_name)
327 {
328     const char * gid_err_msg = "Specified group id contains invalid numeric characters or is "
329             "outside allowed range.";
330
331     // Could be a name or a numeric id. But we should assume numeric first, just in case
332     // a user manages to give themselves a username that parses as a number.
333     std::size_t ind = 0;
334     try {
335         // POSIX does not specify whether uid_t is an signed or unsigned, but regardless
336         // is is probably safe to assume that valid values are positive. We'll also assume
337         // that the value range fits with "unsigned long long" since it seems unlikely
338         // that would ever not be the case.
339         static_assert((uintmax_t)std::numeric_limits<gid_t>::max()
340                 <= (uintmax_t)std::numeric_limits<unsigned long long>::max(), "gid_t is too large");
341         unsigned long long v = std::stoull(param, &ind, 0);
342         if (v > static_cast<unsigned long long>(std::numeric_limits<gid_t>::max())
343                 || ind != param.length()) {
344             throw service_description_exc(service_name, gid_err_msg);
345         }
346         return v;
347     }
348     catch (std::out_of_range &exc) {
349         throw service_description_exc(service_name, gid_err_msg);
350     }
351     catch (std::invalid_argument &exc) {
352         // Ok, so it doesn't look like a number: proceed...
353     }
354
355     errno = 0;
356     struct group * grent = getgrnam(param.c_str());
357     if (grent == nullptr) {
358         // Maybe an error, maybe just no entry.
359         if (errno == 0) {
360             throw service_description_exc(service_name, "Specified group \"" + param
361                     + "\" does not exist in system database.");
362         }
363         else {
364             throw service_description_exc(service_name, std::string("Error accessing group database: ")
365                     + strerror(errno));
366         }
367     }
368
369     return grent->gr_gid;
370 }
371
372 // Parse a time, specified as a decimal number of seconds (with optional fractional component after decimal
373 // point or decimal comma).
374 inline void parse_timespec(const std::string &paramval, const std::string &servicename,
375         const char * paramname, timespec &ts)
376 {
377     decltype(ts.tv_sec) isec = 0;
378     decltype(ts.tv_nsec) insec = 0;
379     auto max_secs = std::numeric_limits<decltype(isec)>::max() / 10;
380     auto len = paramval.length();
381     decltype(len) i;
382     for (i = 0; i < len; i++) {
383         char ch = paramval[i];
384         if (ch == '.' || ch == ',') {
385             i++;
386             break;
387         }
388         if (ch < '0' || ch > '9') {
389             throw service_description_exc(servicename, std::string("Bad value for ") + paramname);
390         }
391         // check for overflow
392         if (isec >= max_secs) {
393            throw service_description_exc(servicename, std::string("Too-large value for ") + paramname);
394         }
395         isec *= 10;
396         isec += ch - '0';
397     }
398     decltype(insec) insec_m = 100000000; // 10^8
399     for ( ; i < len; i++) {
400         char ch = paramval[i];
401         if (ch < '0' || ch > '9') {
402             throw service_description_exc(servicename, std::string("Bad value for ") + paramname);
403         }
404         insec += (ch - '0') * insec_m;
405         insec_m /= 10;
406     }
407     ts.tv_sec = isec;
408     ts.tv_nsec = insec;
409 }
410
411 // Parse an unsigned numeric parameter value
412 inline unsigned long long parse_unum_param(const std::string &param, const std::string &service_name,
413         unsigned long long max = std::numeric_limits<unsigned long long>::max())
414 {
415     const char * num_err_msg = "Specified value contains invalid numeric characters or is outside "
416             "allowed range.";
417
418     std::size_t ind = 0;
419     try {
420         unsigned long long v = std::stoull(param, &ind, 0);
421         if (v > max || ind != param.length()) {
422             throw service_description_exc(service_name, num_err_msg);
423         }
424         return v;
425     }
426     catch (std::out_of_range &exc) {
427         throw service_description_exc(service_name, num_err_msg);
428     }
429     catch (std::invalid_argument &exc) {
430         throw service_description_exc(service_name, num_err_msg);
431     }
432 }
433
434 // In a vector, find or create rlimits for a particular resource type.
435 inline service_rlimits &find_rlimits(std::vector<service_rlimits> &all_rlimits, int resource_id)
436 {
437     for (service_rlimits &limits : all_rlimits) {
438         if (limits.resource_id == resource_id) {
439             return limits;
440         }
441     }
442
443     all_rlimits.emplace_back(resource_id);
444     return all_rlimits.back();
445 }
446
447 // Parse resource limits setting (can specify both hard and soft limit).
448 inline void parse_rlimit(const std::string &line, const std::string &service_name, const char *param_name,
449         service_rlimits &rlimit)
450 {
451     // Examples:
452     // 4:5 - soft:hard limits both set
453     // 4:-   soft set, hard set to unlimited
454     // 4:    soft set, hard limit unchanged
455     // 4     soft and hard limit set to same limit
456
457     if (line.empty()) {
458         throw service_description_exc(service_name, std::string("Bad value for ") + param_name);
459     }
460
461     const char *cline = line.c_str();
462     rlimit.hard_set = rlimit.soft_set = false;
463
464     try {
465         const char * index = cline;
466         errno = 0;
467         if (cline[0] != ':') {
468             rlimit.soft_set = true;
469             if (cline[0] == '-') {
470                 rlimit.limits.rlim_cur = RLIM_INFINITY;
471                 index = cline + 1;
472             }
473             else {
474                 char *nindex;
475                 unsigned long long limit = std::strtoull(cline, &nindex, 0);
476                 index = nindex;
477                 if (errno == ERANGE || limit > std::numeric_limits<rlim_t>::max()) throw std::out_of_range("");
478                 if (index == cline) throw std::invalid_argument("");
479                 rlimit.limits.rlim_cur = limit;
480             }
481
482             if (*index == 0) {
483                 rlimit.hard_set = true;
484                 rlimit.limits.rlim_max = rlimit.limits.rlim_cur;
485                 return;
486             }
487
488             if (*index != ':') {
489                 throw service_description_exc(service_name, std::string("Bad value for ") + param_name);
490             }
491         }
492
493         index++;
494         if (*index == 0) return;
495
496         if (*index == '-') {
497             rlimit.limits.rlim_max = RLIM_INFINITY;
498             if (index[1] != 0) {
499                 throw service_description_exc(service_name, std::string("Bad value for ") + param_name);
500             }
501         }
502         else {
503             const char *hard_start = index;
504             char *nindex;
505             unsigned long long limit = std::strtoull(cline, &nindex, 0);
506             index = nindex;
507             if (errno == ERANGE || limit > std::numeric_limits<rlim_t>::max()) throw std::out_of_range("");
508             if (index == hard_start) throw std::invalid_argument("");
509             rlimit.limits.rlim_max = limit;
510         }
511     }
512     catch (std::invalid_argument &exc) {
513         throw service_description_exc(service_name, std::string("Bad value for ") + param_name);
514     }
515     catch (std::out_of_range &exc) {
516         throw service_description_exc(service_name, std::string("Too-large value for ") + param_name);
517     }
518 }
519
520 // Process an opened service file, line by line.
521 //    name - the service name
522 //    service_file - the service file input stream
523 //    func - a function of the form:
524 //             void(string &line, string &setting, string_iterator i, string_iterator end)
525 //           Called with:
526 //               line - the complete line (excluding newline character)
527 //               setting - the setting name, from the beginning of the line
528 //               i - iterator at the beginning of the setting value
529 //               end - iterator marking the end of the line
530 //
531 // May throw service load exceptions or I/O exceptions if enabled on stream.
532 template <typename T>
533 void process_service_file(string name, std::istream &service_file, T func)
534 {
535     string line;
536
537     while (getline(service_file, line)) {
538         string::iterator i = line.begin();
539         string::iterator end = line.end();
540
541         i = skipws(i, end);
542         if (i != end) {
543             if (*i == '#') {
544                 continue;  // comment line
545             }
546             string setting = read_setting_name(i, end);
547             i = skipws(i, end);
548             if (i == end || (*i != '=' && *i != ':')) {
549                 throw service_description_exc(name, "Badly formed line.");
550             }
551             i = skipws(++i, end);
552
553             func(line, setting, i, end);
554         }
555     }
556 }
557
558 } // namespace dinit_load
559
560 using dinit_load::process_service_file;