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