Merge branch 'daemon-ext'
[libi2ncommon] / src / daemonfunc.cpp
1 /*
2 The software in this package is distributed under the GNU General
3 Public License version 2 (with a special exception described below).
4
5 A copy of GNU General Public License (GPL) is included in this distribution,
6 in the file COPYING.GPL.
7
8 As a special exception, if other files instantiate templates or use macros
9 or inline functions from this file, or you compile this file and link it
10 with other works to produce a work based on this file, this file
11 does not by itself cause the resulting work to be covered
12 by the GNU General Public License.
13
14 However the source code for this file must still be made available
15 in accordance with section (3) of the GNU General Public License.
16
17 This exception does not invalidate any other reasons why a work based
18 on this file might be covered by the GNU General Public License.
19 */
20 /***************************************************************************
21  *   Copyright (C) 2008 by Intra2net AG - Thomas Jarosch                   *
22  ***************************************************************************/
23 #include <sys/types.h>
24 #include <unistd.h>
25 #include <stdlib.h>
26 #include <pwd.h>
27 #include <grp.h>
28 #include <errno.h>
29 #include <string.h>
30 #include <fcntl.h>
31
32 #include <string>
33 #include <stdexcept>
34 #include "daemonfunc.hpp"
35 #include "stringfunc.hxx"
36 #include "filefunc.hxx"
37
38
39 namespace I2n
40 {
41 namespace Daemon
42 {
43
44 using namespace std;
45
46 /**
47  * Fork into the background.
48  */
49 void daemonize()
50 {
51    int pid=fork();
52
53    if (pid < 0)
54    {
55       throw runtime_error("fork() failed");
56    }
57    if (pid > 0)
58    {
59       // parent process
60       exit (0);
61    }
62    // pid==0 -> child process: continue
63 }
64
65 /**
66  * Drop root privileges
67  * @param username User to become. Don't change user if empty
68  * @param group Group to become. Don't change group if empty
69  * @param get_group_from_user Get group GID from user information if group is empty.
70  * @return true if all is fine, false otherwise
71  */
72 bool drop_root_privileges(const std::string &username,
73                           const std::string &group, bool get_group_from_user)
74 {
75    if (!group.empty())
76    {
77       Group my_group(group);
78       if (!my_group.is_valid())
79          return false;
80
81       if (setgid((my_group.Gid)))
82          return false;
83    }
84
85    if (!username.empty())
86    {
87       User my_user(username);
88       if (!my_user.is_valid())
89          return false;
90
91       if (get_group_from_user && group.empty())
92       {
93          if (setgid((my_user.Gid)))
94             return false;
95       }
96
97       // Initialize additional groups the user is a member of
98       if (initgroups(username.c_str(), getgid()) == -1)
99          return false;
100
101       if (setuid(my_user.Uid))
102          return false;
103    }
104
105     return true;
106 }
107
108 static const char *const EXE_DELETED_MARKER = " (deleted)";
109
110 /** @brief      Remove the deletion marker from a binary name, if applicable.
111  *
112  * @param exe   Binary name as obtained from ``/proc/$PID/exe``.
113  *
114  * @return      The input binary name with a trailing `` (deleted)`` removed.
115  *
116  * From proc(5):
117  *
118  *     /proc/[pid]/exe
119  *            […] If the pathname has been unlinked, the symbolic link will
120  *            contain the string '(deleted)' appended to the original pathname.
121  *
122  * Which introduces an ambiguity if the path of the actual binary ends in
123  * `` (deleted)``.
124  */
125 static inline std::string
126 strip_deleted_marker (const std::string &exe)
127 {
128     if (exe.size () <= sizeof (*EXE_DELETED_MARKER)) {
129         /* This binary name can’t possibly contain a deleted marker. */
130         return exe;
131     }
132
133     const size_t found = exe.rfind (EXE_DELETED_MARKER);
134     if (found == std::string::npos) {
135         /* Input does not contain the deleted marker. */
136         return exe;
137     }
138
139     return exe.substr (0, found);
140 }
141
142 /**
143  * @brief determine the pids for a given program
144  * @param[in] name name (or full path) of the binary
145  * @param[out] result the pids associated with the name.
146  * @return @a true if the function performed without errors.
147  *
148  * Walk though the /proc/\<pid\>'s and search for the name.
149  *
150  * @note Since this function uses /proc, it's system specific. Currently:
151  * Linux only!
152  *
153  * @todo check cmdline and stat in /proc/\<pid\> dir for the searched name.
154  */
155 bool pid_of(const std::string& name, std::vector< pid_t >& result)
156 {
157    std::vector< std::string > entries;
158    std::vector< pid_t > fuzz1_result;
159    std::vector< pid_t > fuzz2_result;
160    result.clear();
161
162    if (name.empty()) return false;
163    if (!get_dir("/proc", entries)) return false;
164
165    for (std::vector< std::string >::const_iterator it= entries.begin();
166          it != entries.end();
167          ++it)
168    {
169       pid_t pid;
170       if (! string_to<pid_t>(*it, pid)) continue;
171       std::string base_path= std::string("/proc/") + *it;
172       std::string exe_path= base_path + "/exe";
173       I2n::Stat stat(exe_path, false);
174       if (not stat or not stat.is_link()) continue;
175       std::string real_exe= read_link(exe_path);
176       if (!real_exe.empty())
177       {
178          // we got the path of the exe
179          if (real_exe == name || strip_deleted_marker (real_exe) == name)
180          {
181                  result.push_back( pid );
182                  continue;
183          }
184          
185          std::string proc_stat= read_file( base_path + "/stat");
186          if (proc_stat.empty()) continue; // process vanished
187          
188          //TODO some more fuzz tests here?! (cmdline, stat(us))
189          
190          real_exe = basename (real_exe);
191          if (real_exe == name || strip_deleted_marker (real_exe) == name)
192          {
193                  fuzz2_result.push_back(pid);
194                  continue;
195          }
196       }
197       else
198       {
199          // we haven't got the path of the exe
200          // this can happen e.g. with processes owned by other users
201          // -> parse the commandline instead
202          std::string cmdline = read_file(base_path + "/cmdline");
203          if (cmdline.empty()) continue;
204
205          // in /proc/*/cmdline, the parameters are split by nullbytes, we only care for the first parameter
206          if (cmdline.find('\0') != string::npos)
207             cmdline.erase(cmdline.find('\0'));
208
209          if (cmdline == name)
210          {
211                result.push_back( pid );
212                continue;
213          }
214
215          if (basename(cmdline) == name)
216          {
217                fuzz2_result.push_back(pid);
218                continue;
219          }
220       }
221    }
222    if (result.empty())
223    {
224       result.swap(fuzz1_result);
225    }
226    if (result.empty())
227    {
228       result.swap(fuzz2_result);
229    }
230    return true;
231 } // eo pidOf(const std::string&,std::vector< pid_t >&)
232
233 /**
234  * @brief   establish a new session for the current process.
235  *
236  * @return  the id of the new session.
237  *
238  * This wraps setsid(2); to be called from a child process after forking.
239  * Raises ``runtime_error`` if the call fails.
240  */
241 pid_t create_session (void)
242 {
243     pid_t sid;
244
245     errno = 0;
246     if ((sid = setsid ()) == -1)
247     {
248         throw std::runtime_error
249             ((std::string)
250              "create_session: setsid() returned an error ("
251              + I2n::to_string (errno)
252              + "): " + strerror (errno));
253     }
254
255     return sid;
256 }
257
258 /**
259  * @brief   reopen standard file descriptors as ``/dev/null``.
260  *
261  * Disable stdin, stdout and stderr to ensure no output can be read from or
262  * written to those descriptors. This assumes the process will interact with
263  * the outside world through other means like syslog and sockets.
264  *
265  * Raises ``runtime_error`` in case of errors.
266  */
267 void null_fds () {
268     int devnull;
269
270     errno = 0;
271     if ((devnull = open ("/dev/null", O_RDWR)) == -1)
272     {
273         throw std::runtime_error
274             ((std::string)
275              "null_fds: open(/dev/null) returned an error ("
276              + I2n::to_string (errno)
277              + "): " + strerror (errno));
278     }
279
280     for (int fd = 0; fd != 3; ++fd)
281     {
282         errno = 0;
283         if (dup2 (devnull, fd) == -1)
284         {
285             throw std::runtime_error
286                 ((std::string)
287                  "null_fds: dup2(/dev/null, "
288                  + I2n::to_string (fd)
289                  + ") returned an error ("
290                  + I2n::to_string (errno)
291                  + "): " + strerror (errno));
292         }
293     }
294
295     errno = 0;
296     if (close (devnull) == -1)
297     {
298         throw std::runtime_error
299             ((std::string)
300              "null_fds: close(/dev/null) returned an error ("
301              + I2n::to_string (errno)
302              + "): " + strerror (errno));
303     }
304 }
305
306 /**
307  * @brief   convert the current process into a background process.
308  *
309  * This convenience wrapper combines forking, creation of a new session and
310  * disabling stdio.
311  *
312  * Raises ``runtime_error`` in case of errors.
313  */
314 void daemonize_full (void)
315 {
316     daemonize ();
317     (void)create_session ();
318     null_fds ();
319 }
320
321 }
322 }