Refactoring: split loading service from directory into seperate class
[oweals/dinit.git] / src / load_service.cc
1 #include <algorithm>
2 #include <string>
3 #include <fstream>
4 #include <locale>
5 #include <iostream>
6 #include <limits>
7
8 #include <sys/stat.h>
9 #include <sys/types.h>
10 #include <pwd.h>
11 #include <grp.h>
12
13 #include "service.h"
14
15 typedef std::string string;
16 typedef std::string::iterator string_iterator;
17
18 // Utility function to skip white space. Returns an iterator at the
19 // first non-white-space position (or at end).
20 static string_iterator skipws(string_iterator i, string_iterator end)
21 {
22     using std::locale;
23     using std::isspace;
24     
25     while (i != end) {
26       if (! isspace(*i, locale::classic())) {
27         break;
28       }
29       ++i;
30     }
31     return i;
32 }
33
34 // Read a setting name.
35 static string read_setting_name(string_iterator & i, string_iterator end)
36 {
37     using std::locale;
38     using std::ctype;
39     using std::use_facet;
40     
41     const ctype<char> & facet = use_facet<ctype<char> >(locale::classic());
42
43     string rval;
44     // Allow alphabetical characters, and dash (-) in setting name
45     while (i != end && (*i == '-' || facet.is(ctype<char>::alpha, *i))) {
46         rval += *i;
47         ++i;
48     }
49     return rval;
50 }
51
52 namespace {
53     class SettingException
54     {
55         std::string info;
56         
57         public:
58         SettingException(const std::string &&exc_info) : info(std::move(exc_info))
59         {
60         }
61         
62         std::string &getInfo()
63         {
64             return info;
65         }
66     };
67 }
68
69
70 // Read a setting value
71 //
72 // In general a setting value is a single-line string. It may contain multiple parts
73 // separated by white space (which is normally collapsed). A hash mark - # - denotes
74 // the end of the value and the beginning of a comment (it should be preceded by
75 // whitespace).
76 //
77 // Part of a value may be quoted using double quote marks, which prevents collapse
78 // of whitespace and interpretation of most special characters (the quote marks will
79 // not be considered part of the value). A backslash can precede a character (such
80 // as '#' or '"' or another backslash) to remove its special meaning. Newline
81 // characters are not allowed in values and cannot be quoted.
82 //
83 // This function expects the string to be in an ASCII-compatible, single byte
84 // encoding (the "classic" locale).
85 //
86 // Params:
87 //    service_name - the name of the service to which the setting applies
88 //    i  -  reference to string iterator through the line
89 //    end -   iterator at end of line
90 //    part_positions -  list of <int,int> to which the position of each setting value
91 //                      part will be added as [start,end). May be null.
92 static string read_setting_value(string_iterator & i, string_iterator end,
93         std::list<std::pair<unsigned,unsigned>> * part_positions = nullptr)
94 {
95     using std::locale;
96     using std::isspace;
97
98     i = skipws(i, end);
99     
100     string rval;
101     bool new_part = true;
102     int part_start;
103     
104     while (i != end) {
105         char c = *i;
106         if (c == '\"') {
107             if (new_part) {
108                 part_start = rval.length();
109                 new_part = false;
110             }
111             // quoted string
112             ++i;
113             while (i != end) {
114                 c = *i;
115                 if (c == '\"') break;
116                 if (c == '\n') {
117                     throw SettingException("Line end inside quoted string");
118                 }
119                 else if (c == '\\') {
120                     // A backslash escapes the following character.
121                     ++i;
122                     if (i != end) {
123                         c = *i;
124                         if (c == '\n') {
125                             throw SettingException("Line end follows backslash escape character (`\\')");
126                         }
127                         rval += c;
128                     }
129                 }
130                 else {
131                     rval += c;
132                 }
133                 ++i;
134             }
135             if (i == end) {
136                 // String wasn't terminated
137                 throw SettingException("Unterminated quoted string");
138             }
139         }
140         else if (c == '\\') {
141             if (new_part) {
142                 part_start = rval.length();
143                 new_part = false;
144             }
145             // A backslash escapes the next character
146             ++i;
147             if (i != end) {
148                 rval += *i;
149             }
150             else {
151                 throw SettingException("Backslash escape (`\\') not followed by character");
152             }
153         }
154         else if (isspace(c, locale::classic())) {
155             if (! new_part && part_positions != nullptr) {
156                 part_positions->emplace_back(part_start, rval.length());
157                 new_part = true;
158             }
159             i = skipws(i, end);
160             if (i == end) break;
161             if (*i == '#') break; // comment
162             rval += ' ';  // collapse ws to a single space
163             continue;
164         }
165         else if (c == '#') {
166             // Possibly intended a comment; we require leading whitespace to reduce occurrence of accidental
167             // comments in setting values.
168             throw SettingException("hashmark (`#') comment must be separated from setting value by whitespace");
169         }
170         else {
171             if (new_part) {
172                 part_start = rval.length();
173                 new_part = false;
174             }
175             rval += c;
176         }
177         ++i;
178     }
179
180     // Got to end:
181     if (part_positions != nullptr) {
182         part_positions->emplace_back(part_start, rval.length());
183     }
184
185     return rval;
186 }
187
188 static int signalNameToNumber(std::string &signame)
189 {
190     if (signame == "HUP") return SIGHUP;
191     if (signame == "INT") return SIGINT;
192     if (signame == "QUIT") return SIGQUIT;
193     if (signame == "USR1") return SIGUSR1;
194     if (signame == "USR2") return SIGUSR2;
195     return -1;
196 }
197
198 static const char * uid_err_msg = "Specified user id contains invalid numeric characters or is outside allowed range.";
199
200 // Parse a userid parameter which may be a numeric user ID or a username. If a name, the
201 // userid is looked up via the system user database (getpwnam() function). In this case,
202 // the associated group is stored in the location specified by the group_p parameter iff
203 // it is not null and iff it contains the value -1.
204 static uid_t parse_uid_param(const std::string &param, const std::string &service_name, gid_t *group_p)
205 {
206     // Could be a name or a numeric id. But we should assume numeric first, just in case
207     // a user manages to give themselves a username that parses as a number.
208     std::size_t ind = 0;
209     try {
210         // POSIX does not specify whether uid_t is an signed or unsigned, but regardless
211         // is is probably safe to assume that valid values are positive. We'll also assert
212         // that the value range fits within "unsigned long long" since it seems unlikely
213         // that would ever not be the case.
214         static_assert((uintmax_t)std::numeric_limits<uid_t>::max() <= (uintmax_t)std::numeric_limits<unsigned long long>::max(), "uid_t is too large");
215         unsigned long long v = std::stoull(param, &ind, 0);
216         if (v > static_cast<unsigned long long>(std::numeric_limits<uid_t>::max()) || ind != param.length()) {
217             throw service_description_exc(service_name, uid_err_msg);
218         }
219         return v;
220     }
221     catch (std::out_of_range &exc) {
222         throw service_description_exc(service_name, uid_err_msg);
223     }
224     catch (std::invalid_argument &exc) {
225         // Ok, so it doesn't look like a number: proceed...
226     }
227
228     errno = 0;
229     struct passwd * pwent = getpwnam(param.c_str());
230     if (pwent == nullptr) {
231         // Maybe an error, maybe just no entry.
232         if (errno == 0) {
233             throw service_description_exc(service_name, "Specified user \"" + param + "\" does not exist in system database.");
234         }
235         else {
236             throw service_description_exc(service_name, std::string("Error accessing user database: ") + strerror(errno));
237         }
238     }
239     
240     if (group_p && *group_p != (gid_t)-1) {
241         *group_p = pwent->pw_gid;
242     }
243     
244     return pwent->pw_uid;
245 }
246
247 static const char * num_err_msg = "Specified value contains invalid numeric characters or is outside allowed range.";
248
249 // Parse an unsigned numeric parameter value
250 static unsigned long long parse_unum_param(const std::string &param, const std::string &service_name,
251         unsigned long long max = std::numeric_limits<unsigned long long>::max())
252 {
253     std::size_t ind = 0;
254     try {
255         unsigned long long v = std::stoull(param, &ind, 0);
256         if (v > max || ind != param.length()) {
257             throw service_description_exc(service_name, num_err_msg);
258         }
259         return v;
260     }
261     catch (std::out_of_range &exc) {
262         throw service_description_exc(service_name, num_err_msg);
263     }
264     catch (std::invalid_argument &exc) {
265         throw service_description_exc(service_name, num_err_msg);
266     }
267 }
268
269 static const char * gid_err_msg = "Specified group id contains invalid numeric characters or is outside allowed range.";
270
271 static gid_t parse_gid_param(const std::string &param, const std::string &service_name)
272 {
273     // Could be a name or a numeric id. But we should assume numeric first, just in case
274     // a user manages to give themselves a username that parses as a number.
275     std::size_t ind = 0;
276     try {
277         // POSIX does not specify whether uid_t is an signed or unsigned, but regardless
278         // is is probably safe to assume that valid values are positive. We'll also assume
279         // that the value range fits with "unsigned long long" since it seems unlikely
280         // that would ever not be the case.
281         //
282         // TODO perhaps write a number parser, since even the unsigned variants of the C/C++
283         //      functions accept a leading minus sign...
284         unsigned long long v = std::stoull(param, &ind, 0);
285         if (v > static_cast<unsigned long long>(std::numeric_limits<gid_t>::max()) || ind != param.length()) {
286             throw service_description_exc(service_name, gid_err_msg);
287         }
288         return v;
289     }
290     catch (std::out_of_range &exc) {
291         throw service_description_exc(service_name, gid_err_msg);
292     }
293     catch (std::invalid_argument &exc) {
294         // Ok, so it doesn't look like a number: proceed...
295     }
296
297     errno = 0;
298     struct group * grent = getgrnam(param.c_str());
299     if (grent == nullptr) {
300         // Maybe an error, maybe just no entry.
301         if (errno == 0) {
302             throw service_description_exc(service_name, "Specified group \"" + param + "\" does not exist in system database.");
303         }
304         else {
305             throw service_description_exc(service_name, std::string("Error accessing group database: ") + strerror(errno));
306         }
307     }
308     
309     return grent->gr_gid;
310 }
311
312 static void parse_timespec(const std::string &paramval, const std::string &servicename,
313         const char * paramname, timespec &ts)
314 {
315     decltype(ts.tv_sec) isec = 0;
316     decltype(ts.tv_nsec) insec = 0;
317     auto max_secs = std::numeric_limits<decltype(isec)>::max() / 10;
318     auto len = paramval.length();
319     decltype(len) i;
320     for (i = 0; i < len; i++) {
321         char ch = paramval[i];
322         if (ch == '.') {
323             i++;
324             break;
325         }
326         if (ch < '0' || ch > '9') {
327             throw service_description_exc(servicename, std::string("Bad value for ") + paramname);
328         }
329         // check for overflow
330         if (isec >= max_secs) {
331            throw service_description_exc(servicename, std::string("Too-large value for ") + paramname);
332         }
333         isec *= 10;
334         isec += ch - '0';
335     }
336     decltype(insec) insec_m = 100000000; // 10^8
337     for ( ; i < len; i++) {
338         char ch = paramval[i];
339         if (ch < '0' || ch > '9') {
340             throw service_description_exc(servicename, std::string("Bad value for ") + paramname);
341         }
342         insec += (ch - '0') * insec_m;
343         insec_m /= 10;
344     }
345     ts.tv_sec = isec;
346     ts.tv_nsec = insec;
347 }
348
349 // Find a service record, or load it from file. If the service has
350 // dependencies, load those also.
351 //
352 // Might throw a ServiceLoadExc exception if a dependency cycle is found or if another
353 // problem occurs (I/O error, service description not found etc). Throws std::bad_alloc
354 // if a memory allocation failure occurs.
355 service_record * dirload_service_set::load_service(const char * name)
356 {
357     using std::string;
358     using std::ifstream;
359     using std::ios;
360     using std::ios_base;
361     using std::locale;
362     using std::isspace;
363     
364     using std::list;
365     using std::pair;
366     
367     // First try and find an existing record...
368     service_record * rval = find_service(string(name));
369     if (rval != 0) {
370         if (rval->isDummy()) {
371             throw service_cyclic_dependency(name);
372         }
373         return rval;
374     }
375
376     // Couldn't find one. Have to load it.    
377     string service_filename = service_dir;
378     if (*(service_filename.rbegin()) != '/') {
379         service_filename += '/';
380     }
381     service_filename += name;
382     
383     string command;
384     list<pair<unsigned,unsigned>> command_offsets;
385     string stop_command;
386     list<pair<unsigned,unsigned>> stop_command_offsets;
387     string pid_file;
388
389     service_type service_type = service_type::PROCESS;
390     std::list<service_record *> depends_on;
391     std::list<service_record *> depends_soft;
392     string logfile;
393     onstart_flags_t onstart_flags;
394     int term_signal = -1;  // additional termination signal
395     bool auto_restart = false;
396     bool smooth_recovery = false;
397     string socket_path;
398     int socket_perms = 0666;
399     // Note: Posix allows that uid_t and gid_t may be unsigned types, but eg chown uses -1 as an
400     // invalid value, so it's safe to assume that we can do the same:
401     uid_t socket_uid = -1;
402     gid_t socket_gid = -1;
403     // Restart limit interval / count; default is 10 seconds, 3 restarts:
404     timespec restart_interval = { .tv_sec = 10, .tv_nsec = 0 };
405     int max_restarts = 3;
406     timespec restart_delay = { .tv_sec = 0, .tv_nsec = 200000000 };
407     
408     string line;
409     ifstream service_file;
410     service_file.exceptions(ios::badbit | ios::failbit);
411     
412     try {
413         service_file.open(service_filename.c_str(), ios::in);
414     }
415     catch (std::ios_base::failure &exc) {
416         throw service_not_found(name);
417     }
418     
419     // Add a dummy service record now to prevent infinite recursion in case of cyclic dependency
420     rval = new service_record(this, string(name));
421     add_service(rval);
422     
423     try {
424         // getline can set failbit if it reaches end-of-file, we don't want an exception in that case:
425         service_file.exceptions(ios::badbit);
426         
427         while (! (service_file.rdstate() & ios::eofbit)) {
428             getline(service_file, line);
429             string::iterator i = line.begin();
430             string::iterator end = line.end();
431           
432             i = skipws(i, end);
433             if (i != end) {
434                 if (*i == '#') {
435                     continue;  // comment line
436                 }
437                 string setting = read_setting_name(i, end);
438                 i = skipws(i, end);
439                 if (i == end || (*i != '=' && *i != ':')) {
440                     throw service_description_exc(name, "Badly formed line.");
441                 }
442                 i = skipws(++i, end);
443                 
444                 if (setting == "command") {
445                     command = read_setting_value(i, end, &command_offsets);
446                 }
447                 else if (setting == "socket-listen") {
448                     socket_path = read_setting_value(i, end, nullptr);
449                 }
450                 else if (setting == "socket-permissions") {
451                     string sock_perm_str = read_setting_value(i, end, nullptr);
452                     std::size_t ind = 0;
453                     try {
454                         socket_perms = std::stoi(sock_perm_str, &ind, 8);
455                         if (ind != sock_perm_str.length()) {
456                             throw std::logic_error("");
457                         }
458                     }
459                     catch (std::logic_error &exc) {
460                         throw service_description_exc(name, "socket-permissions: Badly-formed or out-of-range numeric value");
461                     }
462                 }
463                 else if (setting == "socket-uid") {
464                     string sock_uid_s = read_setting_value(i, end, nullptr);
465                     socket_uid = parse_uid_param(sock_uid_s, name, &socket_gid);
466                 }
467                 else if (setting == "socket-gid") {
468                     string sock_gid_s = read_setting_value(i, end, nullptr);
469                     socket_gid = parse_gid_param(sock_gid_s, name);
470                 }
471                 else if (setting == "stop-command") {
472                     stop_command = read_setting_value(i, end, &stop_command_offsets);
473                 }
474                 else if (setting == "pid-file") {
475                     pid_file = read_setting_value(i, end);
476                 }
477                 else if (setting == "depends-on") {
478                     string dependency_name = read_setting_value(i, end);
479                     depends_on.push_back(load_service(dependency_name.c_str()));
480                 }
481                 else if (setting == "waits-for") {
482                     string dependency_name = read_setting_value(i, end);
483                     depends_soft.push_back(load_service(dependency_name.c_str()));
484                 }
485                 else if (setting == "logfile") {
486                     logfile = read_setting_value(i, end);
487                 }
488                 else if (setting == "restart") {
489                     string restart = read_setting_value(i, end);
490                     auto_restart = (restart == "yes" || restart == "true");
491                 }
492                 else if (setting == "smooth-recovery") {
493                     string recovery = read_setting_value(i, end);
494                     smooth_recovery = (recovery == "yes" || recovery == "true");
495                 }
496                 else if (setting == "type") {
497                     string type_str = read_setting_value(i, end);
498                     if (type_str == "scripted") {
499                         service_type = service_type::SCRIPTED;
500                     }
501                     else if (type_str == "process") {
502                         service_type = service_type::PROCESS;
503                     }
504                     else if (type_str == "bgprocess") {
505                         service_type = service_type::BGPROCESS;
506                     }
507                     else if (type_str == "internal") {
508                         service_type = service_type::INTERNAL;
509                     }
510                     else {
511                         throw service_description_exc(name, "Service type must be one of: \"scripted\","
512                             " \"process\", \"bgprocess\" or \"internal\"");
513                     }
514                 }
515                 else if (setting == "options") {
516                     std::list<std::pair<unsigned,unsigned>> indices;
517                     string onstart_cmds = read_setting_value(i, end, &indices);
518                     for (auto indexpair : indices) {
519                         string option_txt = onstart_cmds.substr(indexpair.first, indexpair.second - indexpair.first);
520                         if (option_txt == "starts-rwfs") {
521                             onstart_flags.rw_ready = true;
522                         }
523                         else if (option_txt == "starts-log") {
524                             onstart_flags.log_ready = true;
525                         }
526                         else if (option_txt == "no-sigterm") {
527                             onstart_flags.no_sigterm = true;
528                         }
529                         else if (option_txt == "runs-on-console") {
530                             onstart_flags.runs_on_console = true;
531                             // A service that runs on the console necessarily starts on console:
532                             onstart_flags.starts_on_console = true;
533                         }
534                         else if (option_txt == "starts-on-console") {
535                             onstart_flags.starts_on_console = true;
536                         }
537                         else if (option_txt == "pass-cs-fd") {
538                             onstart_flags.pass_cs_fd = true;
539                         }
540                         else {
541                             throw service_description_exc(name, "Unknown option: " + option_txt);
542                         }
543                     }
544                 }
545                 else if (setting == "termsignal") {
546                     string signame = read_setting_value(i, end, nullptr);
547                     int signo = signalNameToNumber(signame);
548                     if (signo == -1) {
549                         throw service_description_exc(name, "Unknown/unsupported termination signal: " + signame);
550                     }
551                     else {
552                         term_signal = signo;
553                     }
554                 }
555                 else if (setting == "restart-limit-interval") {
556                     string interval_str = read_setting_value(i, end, nullptr);
557                     parse_timespec(interval_str, name, "restart-limit-interval", restart_interval);
558                 }
559                 else if (setting == "restart-delay") {
560                     string rsdelay_str = read_setting_value(i, end, nullptr);
561                     parse_timespec(rsdelay_str, name, "restart-delay", restart_delay);
562                 }
563                 else if (setting == "restart-limit-count") {
564                     string limit_str = read_setting_value(i, end, nullptr);
565                     max_restarts = parse_unum_param(limit_str, name, std::numeric_limits<int>::max());
566                 }
567                 else {
568                     throw service_description_exc(name, "Unknown setting: " + setting);
569                 }
570             }
571         }
572         
573         service_file.close();
574         
575         if (service_type == service_type::PROCESS || service_type == service_type::BGPROCESS || service_type == service_type::SCRIPTED) {
576             if (command.length() == 0) {
577                 throw service_description_exc(name, "Service command not specified");
578             }
579         }
580         
581         // Now replace the dummy service record with a real record:
582         for (auto iter = records.begin(); iter != records.end(); iter++) {
583             if (*iter == rval) {
584                 // We've found the dummy record
585                 delete rval;
586                 if (service_type == service_type::PROCESS) {
587                     auto rvalps = new process_service(this, string(name), std::move(command),
588                         command_offsets, &depends_on, &depends_soft);
589                     rvalps->set_restart_interval(restart_interval, max_restarts);
590                     rvalps->set_restart_delay(restart_delay);
591                     rval = rvalps;
592                 }
593                 else if (service_type == service_type::BGPROCESS) {
594                     auto rvalps = new bgproc_service(this, string(name), std::move(command),
595                         command_offsets, &depends_on, &depends_soft);
596                     rvalps->set_pid_file(std::move(pid_file));
597                     rvalps->set_restart_interval(restart_interval, max_restarts);
598                     rvalps->set_restart_delay(restart_delay);
599                     rval = rvalps;
600                 }
601                 else if (service_type == service_type::SCRIPTED) {
602                     rval = new scripted_service(this, string(name), std::move(command),
603                                                 command_offsets, &depends_on, &depends_soft);
604                     rval->setStopCommand(stop_command, stop_command_offsets);
605                 }
606                 else {
607                     rval = new service_record(this, string(name), service_type, std::move(command), command_offsets,
608                             &depends_on, &depends_soft);
609                 }
610                 rval->setLogfile(logfile);
611                 rval->setAutoRestart(auto_restart);
612                 rval->setSmoothRecovery(smooth_recovery);
613                 rval->setOnstartFlags(onstart_flags);
614                 rval->setExtraTerminationSignal(term_signal);
615                 rval->set_socket_details(std::move(socket_path), socket_perms, socket_uid, socket_gid);
616                 *iter = rval;
617                 break;
618             }
619         }
620         
621         return rval;
622     }
623     catch (SettingException &setting_exc)
624     {
625         // Must remove the dummy service record.
626         std::remove(records.begin(), records.end(), rval);
627         delete rval;
628         throw service_description_exc(name, std::move(setting_exc.getInfo()));
629     }
630     catch (...) {
631         // Must remove the dummy service record.
632         std::remove(records.begin(), records.end(), rval);
633         delete rval;
634         throw;
635     }
636 }