1# $OpenBSD: Syslogd.pm,v 1.26 2021/03/09 15:16:28 bluhm Exp $ 2 3# Copyright (c) 2010-2020 Alexander Bluhm <bluhm@openbsd.org> 4# Copyright (c) 2014 Florian Riehm <mail@friehm.de> 5# 6# Permission to use, copy, modify, and distribute this software for any 7# purpose with or without fee is hereby granted, provided that the above 8# copyright notice and this permission notice appear in all copies. 9# 10# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 18use strict; 19use warnings; 20 21package Syslogd; 22use parent 'Proc'; 23use Carp; 24use Cwd; 25use File::Basename; 26use File::Copy; 27use File::Temp qw(tempfile tempdir); 28use Sys::Hostname; 29use Time::HiRes qw(time alarm sleep); 30 31sub new { 32 my $class = shift; 33 my %args = @_; 34 $args{ktraceexec} = "ktrace" if $args{ktrace}; 35 $args{ktraceexec} = $ENV{KTRACE} if $ENV{KTRACE}; 36 $args{ktracefile} ||= "syslogd.ktrace"; 37 $args{fstatfile} ||= "syslogd.fstat"; 38 $args{logfile} ||= "syslogd.log"; 39 $args{up} ||= "syslogd: started"; 40 $args{down} ||= "syslogd: exited"; 41 $args{up} = $args{down} = "execute:" 42 if $args{foreground} || $args{daemon}; 43 $args{foreground} && $args{daemon} 44 and croak "$class cannot run in foreground and as daemon"; 45 $args{func} = sub { Carp::confess "$class func may not be called" }; 46 $args{execfile} ||= $ENV{SYSLOGD} ? $ENV{SYSLOGD} : "syslogd"; 47 $args{conffile} ||= "syslogd.conf"; 48 $args{outfile} ||= "file.log"; 49 unless ($args{outpipe}) { 50 my $dir = tempdir("syslogd-regress-XXXXXXXXXX", 51 CLEANUP => 1, TMPDIR => 1); 52 chmod(0755, $dir) 53 or die "$class chmod directory $dir failed: $!"; 54 $args{tempdir} = $dir; 55 $args{outpipe} = "$dir/pipe.log"; 56 } 57 $args{outconsole} ||= "console.log"; 58 $args{outuser} ||= "user.log"; 59 if ($args{memory}) { 60 $args{memory} = {} unless ref $args{memory}; 61 $args{memory}{name} ||= "memory"; 62 $args{memory}{size} //= 1; 63 } 64 my $self = Proc::new($class, %args); 65 $self->{connectaddr} 66 or croak "$class connect addr not given"; 67 68 _make_abspath(\$self->{$_}) foreach (qw(conffile outfile outpipe)); 69 _make_abspath(\$self->{ktracefile}) if $self->{chdir}; 70 71 # substitute variables in config file 72 my $curdir = dirname($0) || "."; 73 my $objdir = getcwd(); 74 my $hostname = hostname(); 75 (my $host = $hostname) =~ s/\..*//; 76 my $connectdomain = $self->{connectdomain}; 77 my $connectaddr = $self->{connectaddr}; 78 my $connectproto = $self->{connectproto}; 79 my $connectport = $self->{connectport}; 80 81 open(my $fh, '>', $self->{conffile}) 82 or die ref($self), " create conf file $self->{conffile} failed: $!"; 83 print $fh "*.*\t$self->{outfile}\n"; 84 print $fh "*.*\t|dd of=$self->{outpipe}\n" unless $self->{nopipe}; 85 print $fh "*.*\t/dev/console\n" unless $self->{noconsole}; 86 print $fh "*.*\tsyslogd-regress\n" unless $self->{nouser}; 87 my $memory = $self->{memory}; 88 print $fh "*.*\t:$memory->{size}:$memory->{name}\n" if $memory; 89 my $loghost = $self->{loghost}; 90 unless ($loghost) { 91 $loghost = '@$connectaddr'; 92 $loghost .= ':$connectport' if $connectport; 93 } 94 my $config = "*.*\t$loghost\n"; 95 $config .= $self->{conf} if $self->{conf}; 96 $config =~ s/(\$[a-z]+)/$1/eeg; 97 print $fh $config; 98 close $fh; 99 100 return $self->create_out(); 101} 102 103sub create_out { 104 my $self = shift; 105 my $timeout = shift || 10; 106 my @sudo = $ENV{SUDO} ? $ENV{SUDO} : (); 107 108 my $end = time() + $timeout; 109 110 open(my $fh, '>', $self->{outfile}) 111 or die ref($self), " create log file $self->{outfile} failed: $!"; 112 close $fh; 113 114 open($fh, '>', $self->{outpipe}) 115 or die ref($self), " create pipe file $self->{outpipe} failed: $!"; 116 chmod(0644, $self->{outpipe}) 117 or die ref($self), " chmod pipe file $self->{outpipe} failed: $!"; 118 my @cmd = (@sudo, "chown", "_syslogd", $self->{outpipe}); 119 system(@cmd) 120 and die ref($self), " chown pipe file $self->{outpipe} failed: $!"; 121 close $fh; 122 123 foreach my $dev (qw(console user)) { 124 my $file = $self->{"out$dev"}; 125 unlink($file); 126 open($fh, '>', $file) 127 or die ref($self), " create $dev file $file failed: $!"; 128 close $fh; 129 my $user = $dev eq "console" ? 130 "/dev/console" : "syslogd-regress"; 131 my @cmd = (@sudo, "./ttylog", $user, $file); 132 $self->{"pid$dev"} = open(my $ctl, '|-', @cmd) 133 or die ref($self), " pipe to @cmd failed: $!"; 134 # remember until object is destroyed, autoclose will send EOF 135 $self->{"ctl$dev"} = $ctl; 136 } 137 138 foreach my $dev (qw(console user)) { 139 my $file = $self->{"out$dev"}; 140 while ($self->{"ctl$dev"}) { 141 open(my $fh, '<', $file) or die ref($self), 142 " open $file for reading failed: $!"; 143 last if grep { /ttylog: started/ } <$fh>; 144 time() < $end 145 or croak ref($self), " no 'started' in $file ". 146 "after $timeout seconds"; 147 sleep .1; 148 } 149 } 150 151 return $self; 152} 153 154sub ttykill { 155 my $self = shift; 156 my $dev = shift; 157 my $sig = shift; 158 my $pid = $self->{"pid$dev"} 159 or die ref($self), " no tty log pid$dev"; 160 161 if (kill($sig => $pid) != 1) { 162 my $sudo = $ENV{SUDO}; 163 $sudo && $!{EPERM} 164 or die ref($self), " kill $pid failed: $!"; 165 my @cmd = ($sudo, '/bin/kill', "-$sig", $pid); 166 system(@cmd) 167 and die ref($self), " sudo kill $pid failed: $?"; 168 } 169 return $self; 170} 171 172sub child { 173 my $self = shift; 174 my @sudo = $ENV{SUDO} ? $ENV{SUDO} : "env"; 175 176 my @pkill = (@sudo, "pkill", "-KILL", "-x", "syslogd"); 177 my @pgrep = ("pgrep", "-x", "syslogd"); 178 system(@pkill) && $? != 256 179 and die ref($self), " system '@pkill' failed: $?"; 180 while ($? == 0) { 181 print STDERR "syslogd still running\n"; 182 system(@pgrep) && $? != 256 183 and die ref($self), " system '@pgrep' failed: $?"; 184 } 185 print STDERR "syslogd not running\n"; 186 187 unless (${$self->{client}}->{early}) { 188 my @flush = (@sudo, "./logflush"); 189 system(@flush) 190 and die "Command '@flush' failed: $?"; 191 } 192 193 chdir $self->{chdir} 194 or die ref($self), " chdir '$self->{chdir}' failed: $!" 195 if $self->{chdir}; 196 197 my @libevent; 198 foreach (qw(EVENT_NOKQUEUE EVENT_NOPOLL EVENT_NOSELECT)) { 199 push @libevent, "$_=1" if delete $ENV{$_}; 200 } 201 push @libevent, "EVENT_SHOW_METHOD=1" if @libevent; 202 my @ktrace; 203 @ktrace = ($self->{ktraceexec}, "-i", "-f", $self->{ktracefile}) 204 if $self->{ktraceexec}; 205 my @cmd = (@sudo, @libevent, @ktrace, $self->{execfile}, 206 "-f", $self->{conffile}); 207 push @cmd, "-d" if !$self->{foreground} && !$self->{daemon}; 208 push @cmd, "-F" if $self->{foreground}; 209 push @cmd, "-V" unless $self->{cacrt}; 210 push @cmd, "-C", $self->{cacrt} 211 if $self->{cacrt} && $self->{cacrt} ne "default"; 212 push @cmd, "-s", $self->{ctlsock} if $self->{ctlsock}; 213 push @cmd, @{$self->{options}} if $self->{options}; 214 print STDERR "execute: @cmd\n"; 215 exec @cmd; 216 die ref($self), " exec '@cmd' failed: $!"; 217} 218 219sub up { 220 my $self = Proc::up(shift, @_); 221 my $timeout = shift || 10; 222 223 my $end = time() + $timeout; 224 225 while ($self->{fstat}) { 226 $self->fstat(); 227 last unless $self->{foreground} || $self->{daemon}; 228 229 # in foreground mode and as daemon we have no debug output 230 # check fstat kqueue entry to detect statup 231 open(my $fh, '<', $self->{fstatfile}) or die ref($self), 232 " open $self->{fstatfile} for reading failed: $!"; 233 last if grep { /kqueue .* state: W/ } <$fh>; 234 time() < $end 235 or croak ref($self), " no 'kqueue' in $self->{fstatfile} ". 236 "after $timeout seconds"; 237 sleep .1; 238 } 239 240 return $self; 241} 242 243sub down { 244 my $self = shift; 245 246 if (my $dir = $self->{tempdir}) { 247 # keep all logs in single directory for easy debugging 248 copy($_, ".") foreach glob("$dir/*"); 249 } 250 251 return Proc::down($self, @_) unless $self->{daemon}; 252 253 my $timeout = $_[0] || 10; 254 my $end = time() + $timeout; 255 256 my @sudo = $ENV{SUDO} ? $ENV{SUDO} : "env"; 257 my @pkill = (@sudo, "pkill", "-TERM", "-x", "syslogd"); 258 my @pgrep = ("pgrep", "-x", "syslogd"); 259 system(@pkill) && $? != 256 260 and die ref($self), " system '@pkill' failed: $?"; 261 do { 262 sleep .1; 263 system(@pgrep) && $? != 256 264 and die ref($self), " system '@pgrep' failed: $?"; 265 return Proc::down($self, @_) if $? == 256; 266 print STDERR "syslogd still running\n"; 267 } while (time() < $end); 268 269 return; 270} 271 272sub fstat { 273 my $self = shift; 274 275 open(my $fh, '>', $self->{fstatfile}) or die ref($self), 276 " open $self->{fstatfile} for writing failed: $!"; 277 my @cmd = ("fstat"); 278 open(my $fs, '-|', @cmd) 279 or die ref($self), " open pipe from '@cmd' failed: $!"; 280 print $fh grep { /^\w+ *syslogd *\d+/ } <$fs>; 281 close($fs) or die ref($self), $! ? 282 " close pipe from '@cmd' failed: $!" : 283 " command '@cmd' failed: $?"; 284 close($fh) 285 or die ref($self), " close $self->{fstatfile} failed: $!"; 286} 287 288sub _make_abspath { 289 my $file = ref($_[0]) ? ${$_[0]} : $_[0]; 290 if (substr($file, 0, 1) ne "/") { 291 $file = getcwd(). "/". $file; 292 ${$_[0]} = $file if ref($_[0]); 293 } 294 return $file; 295} 296 297sub kill_privsep { 298 return Proc::kill(@_); 299} 300 301sub kill_syslogd { 302 my $self = shift; 303 my $sig = shift // 'TERM'; 304 my $ppid = shift // $self->{pid}; 305 306 # find syslogd child of privsep parent 307 my @cmd = ("ps", "-ww", "-p", $ppid, "-U", "_syslogd", 308 "-o", "pid,ppid,comm", ); 309 open(my $ps, '-|', @cmd) 310 or die ref($self), " open pipe from '@cmd' failed: $!"; 311 my @pslist; 312 my @pshead = split(' ', scalar <$ps>); 313 while (<$ps>) { 314 s/\s+$//; 315 my %h; 316 @h{@pshead} = split(' ', $_, scalar @pshead); 317 push @pslist, \%h; 318 } 319 close($ps) or die ref($self), $! ? 320 " close pipe from '@cmd' failed: $!" : 321 " command '@cmd' failed: $?"; 322 my @pschild = 323 grep { $_->{PPID} == $ppid && $_->{COMMAND} eq "syslogd" } @pslist; 324 @pschild == 1 325 or die ref($self), " not one privsep child: ", 326 join(" ", map { $_->{PID} } @pschild); 327 328 return Proc::kill($self, $sig, $pschild[0]{PID}); 329} 330 331my $rotate_num = 0; 332sub rotate { 333 my $self = shift; 334 335 $self->loggrep("bytes transferred", 1) or sleep 1; 336 foreach my $name (qw(file pipe)) { 337 my $file = $self->{"out$name"}; 338 for (my $i = $rotate_num; $i >= 0; $i--) { 339 my $new = $file. ".$i"; 340 my $old = $file. ($i > 0 ? ".".($i-1) : ""); 341 342 rename($old, $new) or die ref($self), 343 " rename from '$old' to '$new' failed: $!"; 344 } 345 } 346 $rotate_num++; 347 return $self->create_out(); 348}; 349 3501; 351