Clean up some TODOs
[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 // Read a setting value
53 //
54 // In general a setting value is a single-line string. It may contain multiple parts
55 // separated by white space (which is normally collapsed). A hash mark - # - denotes
56 // the end of the value and the beginning of a comment (it should be preceded by
57 // whitespace).
58 //
59 // Part of a value may be quoted using double quote marks, which prevents collapse
60 // of whitespace and interpretation of most special characters (the quote marks will
61 // not be considered part of the value). A backslash can precede a character (such
62 // as '#' or '"' or another backslash) to remove its special meaning. Newline
63 // characters are not allowed in values and cannot be quoted.
64 //
65 // This function expects the string to be in an ASCII-compatible, single byte
66 // encoding (the "classic" locale).
67 //
68 // Params:
69 //    i  -  reference to string iterator through the line
70 //    end -   iterator at end of line
71 //    part_positions -  list of <int,int> to which the position of each setting value
72 //                      part will be added as [start,end). May be null.
73 static string read_setting_value(string_iterator & i, string_iterator end,
74         std::list<std::pair<unsigned,unsigned>> * part_positions = nullptr)
75 {
76     using std::locale;
77     using std::isspace;
78
79     i = skipws(i, end);
80     
81     string rval;
82     bool new_part = true;
83     int part_start;
84     
85     while (i != end) {
86         char c = *i;
87         if (c == '\"') {
88             if (new_part) {
89                 part_start = rval.length();
90                 new_part = false;
91             }
92             // quoted string
93             ++i;
94             while (i != end) {
95                 c = *i;
96                 if (c == '\"') break;
97                 if (c == '\n') {
98                     // TODO error here.
99                 }
100                 else if (c == '\\') {
101                     // A backslash escapes the following character.
102                     ++i;
103                     if (i != end) {
104                         c = *i;
105                         if (c == '\n') {
106                             // TODO error here.
107                         }
108                         rval += c;
109                     }
110                 }
111                 else {
112                     rval += c;
113                 }
114                 ++i;
115             }
116             if (i == end) {
117                 // String wasn't terminated
118                 // TODO error here
119                 break;
120             }
121         }
122         else if (c == '\\') {
123             if (new_part) {
124                 part_start = rval.length();
125                 new_part = false;
126             }
127             // A backslash escapes the next character
128             ++i;
129             if (i != end) {
130                 rval += *i;
131             }
132             else {
133                 // TODO error here
134             }
135         }
136         else if (isspace(c, locale::classic())) {
137             if (! new_part && part_positions != nullptr) {
138                 part_positions->emplace_back(part_start, rval.length());
139                 new_part = true;
140             }
141             i = skipws(i, end);
142             if (i == end) break;
143             if (*i == '#') break; // comment
144             rval += ' ';  // collapse ws to a single space
145             continue;
146         }
147         else if (c == '#') {
148             // hmm... comment? Probably, though they should have put a space
149             // before it really. TODO throw an exception, and document
150             // that '#' for comments must be preceded by space, and in values
151             // must be quoted.
152             break;
153         }
154         else {
155             if (new_part) {
156                 part_start = rval.length();
157                 new_part = false;
158             }
159             rval += c;
160         }
161         ++i;
162     }
163
164     // Got to end:
165     if (part_positions != nullptr) {
166         part_positions->emplace_back(part_start, rval.length());
167     }
168
169     return rval;
170 }
171
172 static int signalNameToNumber(std::string &signame)
173 {
174     if (signame == "HUP") return SIGHUP;
175     if (signame == "INT") return SIGINT;
176     if (signame == "QUIT") return SIGQUIT;
177     if (signame == "USR1") return SIGUSR1;
178     if (signame == "USR2") return SIGUSR2;
179     return -1;
180 }
181
182 static const char * uid_err_msg = "Specified user id contains invalid numeric characters or is outside allowed range.";
183
184 // Parse a userid parameter which may be a numeric user ID or a username. If a name, the
185 // userid is looked up via the system user database (getpwnam() function). In this case,
186 // the associated group is stored in the location specified by the group_p parameter iff
187 // it is not null and iff it contains the value -1.
188 static uid_t parse_uid_param(const std::string &param, const std::string &service_name, gid_t *group_p)
189 {
190     // Could be a name or a numeric id. But we should assume numeric first, just in case
191     // a user manages to give themselves a username that parses as a number.
192     std::size_t ind = 0;
193     try {
194         // POSIX does not specify whether uid_t is an signed or unsigned, but regardless
195         // is is probably safe to assume that valid values are positive. We'll also assume
196         // that the value range fits with "unsigned long long" since it seems unlikely
197         // that would ever not be the case.
198         //
199         // TODO perhaps write a number parser, since even the unsigned variants of the C/C++
200         //      functions accept a leading minus sign...
201         static_assert((uintmax_t)std::numeric_limits<uid_t>::max() <= (uintmax_t)std::numeric_limits<unsigned long long>::max(), "uid_t is too large");
202         unsigned long long v = std::stoull(param, &ind, 0);
203         if (v > static_cast<unsigned long long>(std::numeric_limits<uid_t>::max()) || ind != param.length()) {
204             throw ServiceDescriptionExc(service_name, uid_err_msg);
205         }
206         return v;
207     }
208     catch (std::out_of_range &exc) {
209         throw ServiceDescriptionExc(service_name, uid_err_msg);
210     }
211     catch (std::invalid_argument &exc) {
212         // Ok, so it doesn't look like a number: proceed...
213     }
214
215     errno = 0;
216     struct passwd * pwent = getpwnam(param.c_str());
217     if (pwent == nullptr) {
218         // Maybe an error, maybe just no entry.
219         if (errno == 0) {
220             throw new ServiceDescriptionExc(service_name, "Specified user \"" + param + "\" does not exist in system database.");
221         }
222         else {
223             throw new ServiceDescriptionExc(service_name, std::string("Error accessing user database: ") + strerror(errno));
224         }
225     }
226     
227     if (group_p && *group_p != (gid_t)-1) {
228         *group_p = pwent->pw_gid;
229     }
230     
231     return pwent->pw_uid;
232 }
233
234 static const char * gid_err_msg = "Specified group id contains invalid numeric characters or is outside allowed range.";
235
236 static gid_t parse_gid_param(const std::string &param, const std::string &service_name)
237 {
238     // Could be a name or a numeric id. But we should assume numeric first, just in case
239     // a user manages to give themselves a username that parses as a number.
240     std::size_t ind = 0;
241     try {
242         // POSIX does not specify whether uid_t is an signed or unsigned, but regardless
243         // is is probably safe to assume that valid values are positive. We'll also assume
244         // that the value range fits with "unsigned long long" since it seems unlikely
245         // that would ever not be the case.
246         //
247         // TODO perhaps write a number parser, since even the unsigned variants of the C/C++
248         //      functions accept a leading minus sign...
249         unsigned long long v = std::stoull(param, &ind, 0);
250         if (v > static_cast<unsigned long long>(std::numeric_limits<gid_t>::max()) || ind != param.length()) {
251             throw ServiceDescriptionExc(service_name, gid_err_msg);
252         }
253         return v;
254     }
255     catch (std::out_of_range &exc) {
256         throw ServiceDescriptionExc(service_name, gid_err_msg);
257     }
258     catch (std::invalid_argument &exc) {
259         // Ok, so it doesn't look like a number: proceed...
260     }
261
262     errno = 0;
263     struct group * grent = getgrnam(param.c_str());
264     if (grent == nullptr) {
265         // Maybe an error, maybe just no entry.
266         if (errno == 0) {
267             throw new ServiceDescriptionExc(service_name, "Specified group \"" + param + "\" does not exist in system database.");
268         }
269         else {
270             throw new ServiceDescriptionExc(service_name, std::string("Error accessing group database: ") + strerror(errno));
271         }
272     }
273     
274     return grent->gr_gid;
275 }
276
277 // Find a service record, or load it from file. If the service has
278 // dependencies, load those also.
279 //
280 // Might throw a ServiceLoadExc exception if a dependency cycle is found or if another
281 // problem occurs (I/O error, service description not found etc). Throws std::bad_alloc
282 // if a memory allocation failure occurs.
283 ServiceRecord * ServiceSet::loadServiceRecord(const char * name)
284 {
285     using std::string;
286     using std::ifstream;
287     using std::ios;
288     using std::ios_base;
289     using std::locale;
290     using std::isspace;
291     
292     using std::list;
293     using std::pair;
294     
295     // First try and find an existing record...
296     ServiceRecord * rval = findService(string(name));
297     if (rval != 0) {
298         if (rval->isDummy()) {
299             throw ServiceCyclicDependency(name);
300         }
301         return rval;
302     }
303
304     // Couldn't find one. Have to load it.    
305     string service_filename = service_dir;
306     if (*(service_filename.rbegin()) != '/') {
307         service_filename += '/';
308     }
309     service_filename += name;
310     
311     string command;
312     list<pair<unsigned,unsigned>> command_offsets;
313     string stop_command;
314     list<pair<unsigned,unsigned>> stop_command_offsets;
315     string pid_file;
316
317     ServiceType service_type = ServiceType::PROCESS;
318     std::list<ServiceRecord *> depends_on;
319     std::list<ServiceRecord *> depends_soft;
320     string logfile;
321     OnstartFlags onstart_flags;
322     int term_signal = -1;  // additional termination signal
323     bool auto_restart = false;
324     bool smooth_recovery = false;
325     string socket_path;
326     int socket_perms = 0666;
327     // Note: Posix allows that uid_t and gid_t may be unsigned types, but eg chown uses -1 as an
328     // invalid value, so it's safe to assume that we can do the same:
329     uid_t socket_uid = -1;
330     gid_t socket_gid = -1;
331     
332     string line;
333     ifstream service_file;
334     service_file.exceptions(ios::badbit | ios::failbit);
335     
336     try {
337         service_file.open(service_filename.c_str(), ios::in);
338     }
339     catch (std::ios_base::failure &exc) {
340         throw ServiceNotFound(name);
341     }
342     
343     // Add a dummy service record now to prevent infinite recursion in case of cyclic dependency
344     rval = new ServiceRecord(this, string(name));
345     records.push_back(rval);
346     
347     try {
348         // getline can set failbit if it reaches end-of-file, we don't want an exception in that case:
349         service_file.exceptions(ios::badbit);
350         
351         while (! (service_file.rdstate() & ios::eofbit)) {
352             getline(service_file, line);
353             string::iterator i = line.begin();
354             string::iterator end = line.end();
355           
356             i = skipws(i, end);
357             if (i != end) {
358                 if (*i == '#') {
359                     continue;  // comment line
360                 }
361                 string setting = read_setting_name(i, end);
362                 i = skipws(i, end);
363                 if (i == end || (*i != '=' && *i != ':')) {
364                     throw ServiceDescriptionExc(name, "Badly formed line.");
365                 }
366                 i = skipws(++i, end);
367                 
368                 if (setting == "command") {
369                     command = read_setting_value(i, end, &command_offsets);
370                 }
371                 else if (setting == "socket-listen") {
372                     socket_path = read_setting_value(i, end, nullptr);
373                 }
374                 else if (setting == "socket-permissions") {
375                     string sock_perm_str = read_setting_value(i, end, nullptr);
376                     std::size_t ind = 0;
377                     try {
378                         socket_perms = std::stoi(sock_perm_str, &ind, 8);
379                         if (ind != sock_perm_str.length()) {
380                             throw std::logic_error("");
381                         }
382                     }
383                     catch (std::logic_error &exc) {
384                         throw ServiceDescriptionExc(name, "socket-permissions: Badly-formed or out-of-range numeric value");
385                     }
386                 }
387                 else if (setting == "socket-uid") {
388                     string sock_uid_s = read_setting_value(i, end, nullptr);
389                     socket_uid = parse_uid_param(sock_uid_s, name, &socket_gid);
390                 }
391                 else if (setting == "socket-gid") {
392                     string sock_gid_s = read_setting_value(i, end, nullptr);
393                     socket_gid = parse_gid_param(sock_gid_s, name);
394                 }
395                 else if (setting == "stop-command") {
396                     stop_command = read_setting_value(i, end, &stop_command_offsets);
397                 }
398                 else if (setting == "pid-file") {
399                     pid_file = read_setting_value(i, end);
400                 }
401                 else if (setting == "depends-on") {
402                     string dependency_name = read_setting_value(i, end);
403                     depends_on.push_back(loadServiceRecord(dependency_name.c_str()));
404                 }
405                 else if (setting == "waits-for") {
406                     string dependency_name = read_setting_value(i, end);
407                     depends_soft.push_back(loadServiceRecord(dependency_name.c_str()));
408                 }
409                 else if (setting == "logfile") {
410                     logfile = read_setting_value(i, end);
411                 }
412                 else if (setting == "restart") {
413                     string restart = read_setting_value(i, end);
414                     auto_restart = (restart == "yes" || restart == "true");
415                 }
416                 else if (setting == "smooth-recovery") {
417                     string recovery = read_setting_value(i, end);
418                     smooth_recovery = (recovery == "yes" || recovery == "true");
419                 }
420                 else if (setting == "type") {
421                     string type_str = read_setting_value(i, end);
422                     if (type_str == "scripted") {
423                         service_type = ServiceType::SCRIPTED;
424                     }
425                     else if (type_str == "process") {
426                         service_type = ServiceType::PROCESS;
427                     }
428                     else if (type_str == "bgprocess") {
429                         service_type = ServiceType::BGPROCESS;
430                     }
431                     else if (type_str == "internal") {
432                         service_type = ServiceType::INTERNAL;
433                     }
434                     else {
435                         throw ServiceDescriptionExc(name, "Service type must be one of: \"scripted\","
436                             " \"process\", \"bgprocess\" or \"internal\"");
437                     }
438                 }
439                 else if (setting == "onstart") {
440                     // deprecated
441                     std::list<std::pair<unsigned,unsigned>> indices;
442                     string onstart_cmds = read_setting_value(i, end, &indices);
443                     for (auto indexpair : indices) {
444                         string onstart_cmd = onstart_cmds.substr(indexpair.first, indexpair.second - indexpair.first);
445                         if (onstart_cmd == "rw_ready") {
446                             onstart_flags.rw_ready = true;
447                         }
448                         else if (onstart_cmd == "log_ready") {
449                             onstart_flags.log_ready = true;
450                         }
451                         else {
452                             throw new ServiceDescriptionExc(name, "Unknown onstart command: " + onstart_cmd);
453                         }
454                     }
455                 }
456                 else if (setting == "options") {
457                     std::list<std::pair<unsigned,unsigned>> indices;
458                     string onstart_cmds = read_setting_value(i, end, &indices);
459                     for (auto indexpair : indices) {
460                         string option_txt = onstart_cmds.substr(indexpair.first, indexpair.second - indexpair.first);
461                         if (option_txt == "starts-rwfs") {
462                             onstart_flags.rw_ready = true;
463                         }
464                         else if (option_txt == "starts-log") {
465                             onstart_flags.log_ready = true;
466                         }
467                         else if (option_txt == "nosigterm") {
468                             onstart_flags.no_sigterm = true;
469                         }
470                         else if (option_txt == "runs-on-console") {
471                             onstart_flags.runs_on_console = true;
472                         }
473                         else {
474                             throw new ServiceDescriptionExc(name, "Unknown option: " + option_txt);
475                         }
476                     }
477                 
478                 }
479                 else if (setting == "termsignal") {
480                     string signame = read_setting_value(i, end, nullptr);
481                     int signo = signalNameToNumber(signame);
482                     if (signo == -1) {
483                         throw new ServiceDescriptionExc(name, "Unknown/unsupported termination signal: " + signame);
484                     }
485                     else {
486                         term_signal = signo;
487                     }
488                 }
489                 else if (setting == "nosigterm") {
490                     // deprecated
491                     string sigtermsetting = read_setting_value(i, end);
492                     onstart_flags.no_sigterm = (sigtermsetting == "yes" || sigtermsetting == "true");
493                 }
494                 else if (setting == "runs-on-console") {
495                     // deprecated
496                     string runconsolesetting = read_setting_value(i, end);
497                     onstart_flags.runs_on_console = (runconsolesetting == "yes" || runconsolesetting == "true");
498                 }
499                 else {
500                     throw ServiceDescriptionExc(name, "Unknown setting: " + setting);
501                 }
502             }
503         }
504         
505         service_file.close();
506         // TODO check we actually have all the settings - type, command
507         
508         // Now replace the dummy service record with a real record:
509         for (auto iter = records.begin(); iter != records.end(); iter++) {
510             if (*iter == rval) {
511                 // We've found the dummy record
512                 delete rval;
513                 rval = new ServiceRecord(this, string(name), service_type, std::move(command), command_offsets,
514                         & depends_on, & depends_soft);
515                 rval->setStopCommand(stop_command, stop_command_offsets);
516                 rval->setLogfile(logfile);
517                 rval->setAutoRestart(auto_restart);
518                 rval->setSmoothRecovery(smooth_recovery);
519                 rval->setOnstartFlags(onstart_flags);
520                 rval->setExtraTerminationSignal(term_signal);
521                 rval->set_pid_file(std::move(pid_file));
522                 rval->set_socket_details(std::move(socket_path), socket_perms, socket_uid, socket_gid);
523                 *iter = rval;
524                 break;
525             }
526         }
527         
528         return rval;
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 }