Report (instead of silently ignoring) various errors when reading service descriptions.
[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(exc_info)
59         {
60         }
61         
62         const 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 assume
212         // that the value range fits with "unsigned long long" since it seems unlikely
213         // that would ever not be the case.
214         //
215         // TODO perhaps write a number parser, since even the unsigned variants of the C/C++
216         //      functions accept a leading minus sign...
217         static_assert((uintmax_t)std::numeric_limits<uid_t>::max() <= (uintmax_t)std::numeric_limits<unsigned long long>::max(), "uid_t is too large");
218         unsigned long long v = std::stoull(param, &ind, 0);
219         if (v > static_cast<unsigned long long>(std::numeric_limits<uid_t>::max()) || ind != param.length()) {
220             throw ServiceDescriptionExc(service_name, uid_err_msg);
221         }
222         return v;
223     }
224     catch (std::out_of_range &exc) {
225         throw ServiceDescriptionExc(service_name, uid_err_msg);
226     }
227     catch (std::invalid_argument &exc) {
228         // Ok, so it doesn't look like a number: proceed...
229     }
230
231     errno = 0;
232     struct passwd * pwent = getpwnam(param.c_str());
233     if (pwent == nullptr) {
234         // Maybe an error, maybe just no entry.
235         if (errno == 0) {
236             throw new ServiceDescriptionExc(service_name, "Specified user \"" + param + "\" does not exist in system database.");
237         }
238         else {
239             throw new ServiceDescriptionExc(service_name, std::string("Error accessing user database: ") + strerror(errno));
240         }
241     }
242     
243     if (group_p && *group_p != (gid_t)-1) {
244         *group_p = pwent->pw_gid;
245     }
246     
247     return pwent->pw_uid;
248 }
249
250 static const char * gid_err_msg = "Specified group id contains invalid numeric characters or is outside allowed range.";
251
252 static gid_t parse_gid_param(const std::string &param, const std::string &service_name)
253 {
254     // Could be a name or a numeric id. But we should assume numeric first, just in case
255     // a user manages to give themselves a username that parses as a number.
256     std::size_t ind = 0;
257     try {
258         // POSIX does not specify whether uid_t is an signed or unsigned, but regardless
259         // is is probably safe to assume that valid values are positive. We'll also assume
260         // that the value range fits with "unsigned long long" since it seems unlikely
261         // that would ever not be the case.
262         //
263         // TODO perhaps write a number parser, since even the unsigned variants of the C/C++
264         //      functions accept a leading minus sign...
265         unsigned long long v = std::stoull(param, &ind, 0);
266         if (v > static_cast<unsigned long long>(std::numeric_limits<gid_t>::max()) || ind != param.length()) {
267             throw ServiceDescriptionExc(service_name, gid_err_msg);
268         }
269         return v;
270     }
271     catch (std::out_of_range &exc) {
272         throw ServiceDescriptionExc(service_name, gid_err_msg);
273     }
274     catch (std::invalid_argument &exc) {
275         // Ok, so it doesn't look like a number: proceed...
276     }
277
278     errno = 0;
279     struct group * grent = getgrnam(param.c_str());
280     if (grent == nullptr) {
281         // Maybe an error, maybe just no entry.
282         if (errno == 0) {
283             throw new ServiceDescriptionExc(service_name, "Specified group \"" + param + "\" does not exist in system database.");
284         }
285         else {
286             throw new ServiceDescriptionExc(service_name, std::string("Error accessing group database: ") + strerror(errno));
287         }
288     }
289     
290     return grent->gr_gid;
291 }
292
293 // Find a service record, or load it from file. If the service has
294 // dependencies, load those also.
295 //
296 // Might throw a ServiceLoadExc exception if a dependency cycle is found or if another
297 // problem occurs (I/O error, service description not found etc). Throws std::bad_alloc
298 // if a memory allocation failure occurs.
299 ServiceRecord * ServiceSet::loadServiceRecord(const char * name)
300 {
301     using std::string;
302     using std::ifstream;
303     using std::ios;
304     using std::ios_base;
305     using std::locale;
306     using std::isspace;
307     
308     using std::list;
309     using std::pair;
310     
311     // First try and find an existing record...
312     ServiceRecord * rval = findService(string(name));
313     if (rval != 0) {
314         if (rval->isDummy()) {
315             throw ServiceCyclicDependency(name);
316         }
317         return rval;
318     }
319
320     // Couldn't find one. Have to load it.    
321     string service_filename = service_dir;
322     if (*(service_filename.rbegin()) != '/') {
323         service_filename += '/';
324     }
325     service_filename += name;
326     
327     string command;
328     list<pair<unsigned,unsigned>> command_offsets;
329     string stop_command;
330     list<pair<unsigned,unsigned>> stop_command_offsets;
331     string pid_file;
332
333     ServiceType service_type = ServiceType::PROCESS;
334     std::list<ServiceRecord *> depends_on;
335     std::list<ServiceRecord *> depends_soft;
336     string logfile;
337     OnstartFlags onstart_flags;
338     int term_signal = -1;  // additional termination signal
339     bool auto_restart = false;
340     bool smooth_recovery = false;
341     string socket_path;
342     int socket_perms = 0666;
343     // Note: Posix allows that uid_t and gid_t may be unsigned types, but eg chown uses -1 as an
344     // invalid value, so it's safe to assume that we can do the same:
345     uid_t socket_uid = -1;
346     gid_t socket_gid = -1;
347     
348     string line;
349     ifstream service_file;
350     service_file.exceptions(ios::badbit | ios::failbit);
351     
352     try {
353         service_file.open(service_filename.c_str(), ios::in);
354     }
355     catch (std::ios_base::failure &exc) {
356         throw ServiceNotFound(name);
357     }
358     
359     // Add a dummy service record now to prevent infinite recursion in case of cyclic dependency
360     rval = new ServiceRecord(this, string(name));
361     records.push_back(rval);
362     
363     try {
364         // getline can set failbit if it reaches end-of-file, we don't want an exception in that case:
365         service_file.exceptions(ios::badbit);
366         
367         while (! (service_file.rdstate() & ios::eofbit)) {
368             getline(service_file, line);
369             string::iterator i = line.begin();
370             string::iterator end = line.end();
371           
372             i = skipws(i, end);
373             if (i != end) {
374                 if (*i == '#') {
375                     continue;  // comment line
376                 }
377                 string setting = read_setting_name(i, end);
378                 i = skipws(i, end);
379                 if (i == end || (*i != '=' && *i != ':')) {
380                     throw ServiceDescriptionExc(name, "Badly formed line.");
381                 }
382                 i = skipws(++i, end);
383                 
384                 if (setting == "command") {
385                     command = read_setting_value(i, end, &command_offsets);
386                 }
387                 else if (setting == "socket-listen") {
388                     socket_path = read_setting_value(i, end, nullptr);
389                 }
390                 else if (setting == "socket-permissions") {
391                     string sock_perm_str = read_setting_value(i, end, nullptr);
392                     std::size_t ind = 0;
393                     try {
394                         socket_perms = std::stoi(sock_perm_str, &ind, 8);
395                         if (ind != sock_perm_str.length()) {
396                             throw std::logic_error("");
397                         }
398                     }
399                     catch (std::logic_error &exc) {
400                         throw ServiceDescriptionExc(name, "socket-permissions: Badly-formed or out-of-range numeric value");
401                     }
402                 }
403                 else if (setting == "socket-uid") {
404                     string sock_uid_s = read_setting_value(i, end, nullptr);
405                     socket_uid = parse_uid_param(sock_uid_s, name, &socket_gid);
406                 }
407                 else if (setting == "socket-gid") {
408                     string sock_gid_s = read_setting_value(i, end, nullptr);
409                     socket_gid = parse_gid_param(sock_gid_s, name);
410                 }
411                 else if (setting == "stop-command") {
412                     stop_command = read_setting_value(i, end, &stop_command_offsets);
413                 }
414                 else if (setting == "pid-file") {
415                     pid_file = read_setting_value(i, end);
416                 }
417                 else if (setting == "depends-on") {
418                     string dependency_name = read_setting_value(i, end);
419                     depends_on.push_back(loadServiceRecord(dependency_name.c_str()));
420                 }
421                 else if (setting == "waits-for") {
422                     string dependency_name = read_setting_value(i, end);
423                     depends_soft.push_back(loadServiceRecord(dependency_name.c_str()));
424                 }
425                 else if (setting == "logfile") {
426                     logfile = read_setting_value(i, end);
427                 }
428                 else if (setting == "restart") {
429                     string restart = read_setting_value(i, end);
430                     auto_restart = (restart == "yes" || restart == "true");
431                 }
432                 else if (setting == "smooth-recovery") {
433                     string recovery = read_setting_value(i, end);
434                     smooth_recovery = (recovery == "yes" || recovery == "true");
435                 }
436                 else if (setting == "type") {
437                     string type_str = read_setting_value(i, end);
438                     if (type_str == "scripted") {
439                         service_type = ServiceType::SCRIPTED;
440                     }
441                     else if (type_str == "process") {
442                         service_type = ServiceType::PROCESS;
443                     }
444                     else if (type_str == "bgprocess") {
445                         service_type = ServiceType::BGPROCESS;
446                     }
447                     else if (type_str == "internal") {
448                         service_type = ServiceType::INTERNAL;
449                     }
450                     else {
451                         throw ServiceDescriptionExc(name, "Service type must be one of: \"scripted\","
452                             " \"process\", \"bgprocess\" or \"internal\"");
453                     }
454                 }
455                 else if (setting == "options") {
456                     std::list<std::pair<unsigned,unsigned>> indices;
457                     string onstart_cmds = read_setting_value(i, end, &indices);
458                     for (auto indexpair : indices) {
459                         string option_txt = onstart_cmds.substr(indexpair.first, indexpair.second - indexpair.first);
460                         if (option_txt == "starts-rwfs") {
461                             onstart_flags.rw_ready = true;
462                         }
463                         else if (option_txt == "starts-log") {
464                             onstart_flags.log_ready = true;
465                         }
466                         else if (option_txt == "no-sigterm") {
467                             onstart_flags.no_sigterm = true;
468                         }
469                         else if (option_txt == "runs-on-console") {
470                             onstart_flags.runs_on_console = true;
471                         }
472                         else {
473                             throw new ServiceDescriptionExc(name, "Unknown option: " + option_txt);
474                         }
475                     }
476                 }
477                 else if (setting == "termsignal") {
478                     string signame = read_setting_value(i, end, nullptr);
479                     int signo = signalNameToNumber(signame);
480                     if (signo == -1) {
481                         throw new ServiceDescriptionExc(name, "Unknown/unsupported termination signal: " + signame);
482                     }
483                     else {
484                         term_signal = signo;
485                     }
486                 }
487                 else {
488                     throw ServiceDescriptionExc(name, "Unknown setting: " + setting);
489                 }
490             }
491         }
492         
493         service_file.close();
494         
495         if (service_type == ServiceType::PROCESS || service_type == ServiceType::BGPROCESS || service_type == ServiceType::SCRIPTED) {
496             if (command.length() == 0) {
497                 throw ServiceDescriptionExc(name, "Service command not specified");
498             }
499         }
500         
501         // Now replace the dummy service record with a real record:
502         for (auto iter = records.begin(); iter != records.end(); iter++) {
503             if (*iter == rval) {
504                 // We've found the dummy record
505                 delete rval;
506                 rval = new ServiceRecord(this, string(name), service_type, std::move(command), command_offsets,
507                         & depends_on, & depends_soft);
508                 rval->setStopCommand(stop_command, stop_command_offsets);
509                 rval->setLogfile(logfile);
510                 rval->setAutoRestart(auto_restart);
511                 rval->setSmoothRecovery(smooth_recovery);
512                 rval->setOnstartFlags(onstart_flags);
513                 rval->setExtraTerminationSignal(term_signal);
514                 rval->set_pid_file(std::move(pid_file));
515                 rval->set_socket_details(std::move(socket_path), socket_perms, socket_uid, socket_gid);
516                 *iter = rval;
517                 break;
518             }
519         }
520         
521         return rval;
522     }
523     catch (SettingException &setting_exc)
524     {
525         // Must remove the dummy service record.
526         std::remove(records.begin(), records.end(), rval);
527         delete rval;
528         throw ServiceDescriptionExc(name, setting_exc.getInfo());
529     }
530     catch (...) {
531         // Must remove the dummy service record.
532         std::remove(records.begin(), records.end(), rval);
533         delete rval;
534         throw;
535     }
536 }