# -*- Perl -*- # # File: PTools/Proc/Daemonize.pm # Desc: Turn a script into a daemon process # Date: Mon Oct 25 08:44:09 2004 # Stat: Production # # Abstract: # Simplifies the tasks necessary to turn a Perl script into a # daemon process. Tasks performed can include the following. # . verifying the current Uid and/or Gid # . changing the current working directory # . cleaning/untainting the runtime environment # . redirecting standard IO file descriptors # . detaching process from a terminal # . writing the new PID to a log file # This class also includes a method to untaint a text string # for convienience when running scripts with the "-T" switch. # # See POD after the "__END__" of this module for all the details. # package PTools::Proc::Daemonize; use strict; use warnings; our $PACK = __PACKAGE__; our $VERSION = '0.10'; our @ISA = qw(); use POSIX qw( errno_h ); use Fcntl qw( :DEFAULT ); my $DefaultPath = "/usr/bin:/usr/sbin"; my $EtcPath = ""; my $LoadEtcPath = 0; sub new { bless {}, ref($_[0])||$_[0] } # $self is a simple hash ref. sub set { $_[0]->{$_[1]}=$_[2] } # Note that the 'param' method sub get { return( $_[0]->{$_[1]}||"" ) } # combines 'set' and 'get' sub param { $_[2] ? $_[0]->{$_[1]}=$_[2] : return( $_[0]->{$_[1]}||"" ) } sub setErr { return( $_[0]->{STATUS}=$_[1]||0, $_[0]->{ERROR}=$_[2]||"" ) } sub status { return( $_[0]->{STATUS}||0, $_[0]->{ERROR}||"" ) } sub stat { ( wantarray ? ($_[0]->{ERROR}||"") : ($_[0]->{STATUS} ||0) ) } sub err { return($_[0]->{ERROR}||"") } sub setIn { $_[0]->{in} = $_[1] } sub setOut { $_[0]->{out} = $_[1] } sub setError { $_[0]->{err} = $_[1] } sub oldIn { return( $_[0]->{in} ) } sub oldOut { return( $_[0]->{out} ) } sub oldError { return( $_[0]->{err} ) } sub setPid { $_[0]->{pid} = $_[1] } sub pid { return($_[0]->{pid}||"") } sub defaultPath { $DefaultPath } sub etcPath { $EtcPath } *runAs = \&run; sub run { my($self,$debug,$uid,$gid,$newDir,$umask,$evList,$newPath,$outFile,$pidFile) = @_; ref $self or $self = new $PACK; $self->setErr( 0,"" ); $self->verifyProcess( $debug, $pidFile ) if $pidFile; return $self if $self->stat(); $self->verifyUserGroup( $debug, $uid, $gid ); $self->changeWorkingDir( $debug, $newDir ) if $newDir; $self->setUmask( $debug, $umask ); $self->untaintEnv( $debug, $evList ); $self->resetPath( $debug, $newPath ); $self->redirectIO( $debug, $outFile ); $self->detachSession( $debug ); # PRIOR to 'writePidToFile' $self->writePidToFile( $debug, $pidFile ); # AFTER 'detachSession' return $self; } sub import { my($class,@args) = @_; # Usage: use PTools::Proc::Daemonize qw( /etc/path ); # or use PTools::Proc::Daemonize qw( LoadEtcPath ); return unless (@args); $LoadEtcPath = 1 if ($args[0] =~ m#^((Load|/)?Etc(/)?Path)$#i); return; } sub verifyUserGroup { my($self,$debug, $uid,$gid) = @_; return unless (defined($uid) and length($uid)); return unless (defined($gid) and length($gid)); $debug and print "DEBUG: $PACK: verify running as uid='$uid, gid='$gid'.\n"; my $err; if (length($uid) and $uid != $<) { $err = "user: ". getpwuid( $uid ); } if (length($gid) and $gid != $() { $err .= ", " if $err; $err .= "group: ". getgrgid( $gid ); } $err and die "\nError: expected to be run by $err\n"; # Set Effective Uid/Gid to match Real, if they don't already if (($> != $<) or ($) != $()) { $debug and print "DEBUG: $PACK: set Effective uid/gid to match Real.\n"; $) = $gid; $> = $uid; } return; } sub changeWorkingDir { my($self,$debug, $newDir) = @_; $newDir ||= "/"; $debug and print "DEBUG: $PACK: changing working directory to '$newDir'.\n"; chdir("$newDir") || die "$PACK: Can't chdir to '$newDir': $!"; return; } sub redirectIO { my($self,$debug, $outFile) = @_; if ($debug) { ## Allow for a "Debug" flag here. print "DEBUG: $PACK: skipping 'IO redirection': 'Debug' is set.\n"; return; } # Since a daemon detaches from terminal I/O we need to make sure # STDIN, STDOUT and STDERR are all redirected to a file handle. # Don't simply close the original file handles, as this strategy # may confise subprocesses that expect the standard file handles. if ($outFile) { $self->createFile( $outFile ); } else { $outFile ||= "/dev/null"; } if ( -f $outFile ) { open(STDOUT, ">>$outFile") || die "$PACK: Can't redirect 'STDOUT' to '$outFile': $!"; } else { open(STDOUT, "> $outFile") || die "$PACK: Can't redirect 'STDOUT' to '$outFile': $!"; } open(STDIN, "&STDOUT") || die "$PACK: Can't redirect 'STDERR' (dup to STDOUT): $!"; select STDERR; $| = 1; # unbuffer STDERR select STDOUT; $| = 1; # unbuffer STDOUT return; } sub setUmask { my($self,$debug, $umask) = @_; $debug and print "DEBUG: $PACK: setting umask.\n"; #______________________________________________________ # Set the file/directory creation permissions mask $umask ||= 022; umask( $umask ); return; } sub untaintEnv { my($self,$debug, $evList) = @_; $debug and print "DEBUG: $PACK: cleaning environment vars.\n"; #______________________________________________________ # This will 'UnTaint' the environment via the $ENV{} hash foreach my $key (keys %ENV) { if ( (ref($evList) eq "ARRAY") and (grep(/^$key$/, @$evList)) ) { $debug and print "DEBUG: -- no reset for '$key'\n"; next; } delete $ENV{$key}; } return; } #----------------------------------------------------------------------- # Include this for convenience, since we're thinking about 'untainting' # Usage: # $text = $daemon->untaintString( $text [, $allowedCharList ] ); # # Any character not in the "$allowedCharList" becomes an underscore ("_"). # The default "$allowedCharList" includes those characters identified in # "The WWW Security FAQ" with the addition of the space (" ") character. # An expanded set of allowed characters is available for use when the # situation dictates. Use with care! (See also RFC1738.) my $AllowedChars = '- a-zA-Z0-9_.@'; # default allowed chars my $DangerousChars= $AllowedChars .'~":;?!@#$%^&*()+=,<>{}[]|\\t\\n\\'. "`'"; *allChars = \&dangerousChars; *untaintChars = \&allowedChars; *untaintText = \&untaintString; sub allowedChars { return $AllowedChars } # default allowed chars sub dangerousChars { return $DangerousChars } # non-ctrl chars, tab, nl sub untaintString { my($class, $text, $allowedChars) = @_; $allowedChars ||= $AllowedChars; # default allowed chars $text =~ s/[^$allowedChars]/_/go; # replace disallowed chars $text =~ m/(.*)/; # untaint using a match return $1; # return untainted match } #----------------------------------------------------------------------- my $EtcPathLoaded = 0; sub resetPath { my($self,$debug, $newPath) = @_; $debug and print "DEBUG: $PACK: resetting PATH var.\n"; #______________________________________________________ # The default is an 'UnTainted' $PATH $newPath ||= $DefaultPath; $newPath ||= "/usr/bin:/usr/ccs/bin:/usr/contrib/bin"; if ($LoadEtcPath and ! $EtcPathLoaded) { local(*IN); if (open(IN,"; if ($EtcPath =~ /^(.*)$/) { $EtcPath = $1 } close(IN) || die "$PACK: Can't close '/etc/PATH': $!"; } $EtcPathLoaded = 1; # Emulate "/etc/profile" to ensure PATH includs "/usr/bin" # If /usr/bin is present in /etc/PATH then $DefaultPath is set # to the contents of /etc/PATH. Otherwise, add the contents of # /etc/PATH to the end of the $DefaultPath definition above. if ($EtcPath =~ m#(^|:)/usr/bin(:|$)#) { $DefaultPath = "$EtcPath"; } elsif ($EtcPath) { $DefaultPath .= ":$EtcPath"; } } # And again ... ensure PATH includs "/usr/bin" if ($LoadEtcPath) { $ENV{PATH} = $DefaultPath; } elsif ($newPath =~ m#(^|:)/usr/bin(:|$)#) { $ENV{PATH} = $newPath; } elsif ($DefaultPath =~ m#(^|:)/usr/bin(:|$)#) { $debug and print "DEBUG: $PACK: NOTE that PATH is reset to default.\n"; $ENV{PATH} = "$newPath:$DefaultPath"; } else { $ENV{PATH} = "/usr/bin:$newPath:$DefaultPath"; } return; } sub detachSession { my($self, $debug) = @_; if ($debug) { ## Allow for a "Debug" flag here. print "DEBUG: $PACK: skipping 'session detach': 'Debug' is set.\n"; return; } my $pid = fork; defined $pid || die "$PACK: Fork failed: $!"; $pid > 0 and exit(0); ## Parent process bails out here. $pid == 0 || die "$PACK: Could not fork daemon process."; ## Child process continues here. $self->setPid( $$ ); # For systems that don't support the "setsid()" call, see # the "Proc::Daemon" CPAN module. POSIX::setsid(); sub TIOCNOTTY { return 0x20007471 } local(*TTY); if (open (TTY, " || die "$PACK: Can't read '$pidFile': $!"; close(IN) || die "$PACK: Can't close '$pidFile': $!"; chomp( $processId ); $processId = $self->untaintString( $processId ); ## $debug and print "DEBUG: $PACK: found: pid='$processId'\n"; return unless ($processId and $processId =~ /^\d+$/); $debug and print "DEBUG: $PACK: check for running proc: pid='$processId'\n"; my $procRunning = CORE::kill( 0, $processId ); my($err); if ($! == EPERM) { # Not owner: expected error ## $debug and print "DEBUG: $PACK: kill 0 $processId = 'EPERM'\n"; $procRunning = 1; # (allows running as non-root user) } elsif ($! == ESRCH) { # No process: expected error ## $debug and print "DEBUG: $PACK: kill 0 $processId = 'ESRCH'\n"; } elsif ($! == EBADF) { # Bad filenum: possible error (why?) ## $debug and print "DEBUG: $PACK: kill 0 $processId = 'EBADF'\n"; } elsif ($! == ECHILD) { # No child procs: possible error (why?) ## $debug and print "DEBUG: $PACK: kill 0 $processId = 'ECHLD'\n"; } elsif ( $! ) { # Other: unexpected error ## $debug and print "DEBUG: $PACK: kill 0 $processId = '$!'\n"; $err = sprintf( "kill 0 failed for pid='%d': %s (err:%d)\n", $processId, $!, $! ); } if ($procRunning) { ## $debug and print "DEBUG: $PACK: $processId 'IS RUNNING'\n"; $err ||= "daemon process already running (pid $processId)"; return $self->setErr(-1, $err); } else { ## $debug and print "DEBUG: $PACK: $processId 'NOT running'\n"; } return; } sub writePidToFile { my($self, $debug, $pidFile ) = @_; return unless $pidFile; $debug and print "DEBUG: $PACK: saving PID to '$pidFile'\n"; $self->createFile( $pidFile ); local(*OUT); open(OUT, ">$pidFile") || die "$PACK: Can't open '$pidFile': $!"; print OUT "$$\n" || die "$PACK: Can't write '$pidFile': $!"; close(OUT) || die "$PACK: Can't close '$pidFile': $!"| return; } sub createFile { my($self,$fileName,$umask) = @_; my $oldUmask; $oldUmask = umask( 0 ) unless $umask; # save current umask; reset to 0 if (! -f $fileName) { my $newUmask = $umask || '644'; local(*FH); sysopen(FH, $fileName, O_WRONLY|O_CREAT, oct($newUmask)) || die "$PACK: Can't sysopen '$fileName': $!"; close(FH) || die "$PACK: Can't close '$fileName': $!"; } umask( (umask() & 0) | $oldUmask ) if $oldUmask; # restore prior umask # my($mode,$uid,$gid) = ( 0664, $<, $(, ); # chmod( $mode, $fileName) || die "Can't chmod($mode, $fileName): $!"; # chown($uid,$gid,$fileName) || die "Can't chown($uid,$gid, $fileName): $!"; # umask( (umask() & 0) | $oldUmask ); # restore prior umask return; } #_________________________ 1; # Required by require() __END__ =head1 NAME PTools::Proc::Daemonize - Turn a script into a daemon process =head1 VERSION This document describes version 0.09, released April, 2006. =head1 SYNOPSIS use PTools::Proc::Daemonize; or use PTools::Proc::Daemonize qw( LoadEtcPath ); run PTools::Proc::Daemonize(); or run PTools::Proc::Daemonize( Debug, UID, GID, WorkingDir, Umask, EvListRef, PATH, OutFile, PidFile ); or $daemon = new PTools::Proc::Daemonize; $daemon->verifyProcess( Debug, PidFile ); die $daemon->err() if $daemon->stat(); # Exit if a daemon is running $daemon->verifyUserGroup( Debug, UID, GID ); $daemon->changeWorkingDir( Debug, WorkingDir ); $daemon->setUmask( Debug, Umask ); $daemon->untaintEnv( Debug, EvListRef ); $daemon->resetPath( Debug, PATH ); $daemon->redirectIO( Debug, OutFile ); # Skipped when Debug true $daemon->detachSession( Debug ); # Skipped when Debug true $daemon->writePidToFile( Debug, PidFile ); The following methods are provided for convenience when running Perl scripts that use the '-T' switch (highly recommended for scripts that will become daemon processes). $allow = $daemon->allowedChars(); $text = $daemon->untaintString( $text, $allow ); =head1 DESCRIPTION This class simplifies all of the tasks necessary to turn a Perl script into a daemon process. Tasks performed can include any or all of the following. . verifying the current Uid and/or Gid . changing the current working directory . setting a new file/dir creation Umask value . cleaning/untainting the runtime environment . resetting the PATH environment variable . redirecting standard IO file descriptors . detaching process from a terminal . writing the new PID to a log file When used with a 'B' parameter, the 'B' method will process the '/etc/PATH' file as done in '/etc/profile'. =head2 Constructor =over 4 =item run ( Debug, UID, GID, WorkingDir, Umask, EvListRef, PATH, OutFile, PidFile ) The B method can be used to accomplish any or all of the necessary tasks described in the L section, above. As an alternative, each of the specific tasks can be accomplished separately, by making explicit calls to each of the various methods described below. Each of the arguments described here are the same for the various methods. Note that 'B' is a synonymous name for this method, and all arguments to the methods are optional. Also note that when using the B method there is no default for the B argument. However, when explicitly calling the B method, described below, the B (system root) directory is the default. And, when using a B for a given daemon, the B method will verify the contents of the named file does not match an active process ID (if so, an error condition is set). =over 4 =item Debug Any non-zero value enables B mode. When set, the current standard IO is not redirected, and the session is not detached from a controlling terminal. =item UID A Unix account B, expected to be an integer between 0 and 65535. =item GID A Unix account B, expected to be an integer between 0 and 65535. =item WorkingDir A path that will become the script's new B. Default is '/'. =item Umask A numeric value that will become the script's new file creation B value. Default is '0'. =item EvListRef This parameter must be a reference to a list of environment variable names. Any listed EV names will B be removed from the script's environment. =item Path A colon separated string of directory paths that will become the script's new B. Default is '/usr/bin:/usr/sbin'. =item Outfile A path that will become the script's new STDOUT and STDERR output file. Default is '/dev/null'. =item PidFile A path to which the script's new will be written. Default is to not save the new PID. =back =back =head2 Methods =over 4 =item verifyProcess ( [ Debug ], PidFile ) Verify that the process identification number contained in the named B does B match a currently active process. If the 'pid' B match, an error condition is set. Argument values are the same as described for the L method, above. =item verifyUserGroup ( [ Debug ] [, UID ] [, GID ] ) Verify that the script is currently running as one or both of the specified B or B. Argument values are the same as described for the L method, above. =item changeWorkingDir ( [ Debug [, WorkingDir ] ) Change the script's current working directory to a new B path. Argument values are the same as described for the L method, above. =item setUmask ( [ Debug ] [, Umask] ) Set a new file and directory creation B value Argument values are the same as described for the L method, above. =item untaintEnv ( [ Debug ] [, EvListRef ]) Clean up the script's current environment settings. An optional B argument can be passed to retain named Environment Variables. This argument is expected to be a reference to an array of variable names. Argument values are the same as described for the L method, above. =item resetPath ( [ Debug ] [, PATH ] ) Set a new value for the B environment variable. When this class is used with the 'B' parameter, this method will process the 'etc/PATH' file as done in '/etc/profile'. Argument values are the same as described for the L method, above. =item redirectIO ( [ Debug ] [, OutFile ] ) Redirect the script's standard IO, including STDIN, STDOUT and STDERR to a new B. Redirection is skipped when B true. Argument values are the same as described for the L method, above. =item detachSession ( [ Debug ] ) The script is not detached from the session when when B true. Argument value is the same as described for the L method, above. =item writePidToFile ( [ Debug ] [, PidFile ] ) After the current process is I into a new sesison, write the new Process ID into the specified B. Argument values are the same as described for the L method, above. =item untaintString ( Text [, AllowedChars ] ) =item untaintText ( Text [, AllowedChars ] ) This method is provided for convenience when running a Perl script with the '-T' switch. It is highly recommended that a script which will become a daemon process be run in this manner. This method simplifies the process of 'untainting' Perl variables when necessary. =over 4 =item Text This parameter is a 'tainted' Perl string. =item AllowedChars This optional parameter is a string of allowed characters that are retained in the B string during the 'untainting' process. This defaults to the value returned by the B> method. =back Example: $text = $daemon->untaintString( $text ); =item allowedChars () =item untaintChars () This method returns the default string of characters that are allowed (i.e., 'retained') in strings that are 'untainted' using the B> method. This list includes those characters identified in I with the addition of the space character (http://www.w3.org/Security/Faq/www-security-faq.html). - a-zA-Z0-9_.@ This includes the characters dash, space, alpha-numerics, underscore, dot or period, and the commercial 'at' symbol. Example: $allow = $daemon->allowedChars(); $text = $daemon->untaintString( $text, $allow ); =item allChars =item dangerousChars This method returns all typable 'non-control' characters including tab and newline. The result can then be used with the B> method, above, to untaint a text (scalar) variable that came from a 'reasonably secure' source. For example, loading configuration or template data from an external file into a 'tainted' variable. - a-zA-Z0-9_.@~`'":;?!@#$%^&*()+=,<>{}[]|\\t\\n\\ B Example: $danger = $daemon->dangerousChars(); $text = $daemon->untaintString( $text, $danger ); =back =head1 INHERITANCE None currently. =head1 SEE ALSO See L and L. =head1 AUTHOR Chris Cobb [no dot spam at ccobb dot net] =head1 COPYRIGHT Copyright (c) 2004-2007 by Chris Cobb. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut