1# $OpenBSD: Syslogd.pm,v 1.25 2020/07/24 22:12:00 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 chdir $self->{chdir} 188 or die ref($self), " chdir '$self->{chdir}' failed: $!" 189 if $self->{chdir}; 190 191 my @libevent; 192 foreach (qw(EVENT_NOKQUEUE EVENT_NOPOLL EVENT_NOSELECT)) { 193 push @libevent, "$_=1" if delete $ENV{$_}; 194 } 195 push @libevent, "EVENT_SHOW_METHOD=1" if @libevent; 196 my @ktrace; 197 @ktrace = ($self->{ktraceexec}, "-i", "-f", $self->{ktracefile}) 198 if $self->{ktraceexec}; 199 my @cmd = (@sudo, @libevent, @ktrace, $self->{execfile}, 200 "-f", $self->{conffile}); 201 push @cmd, "-d" if !$self->{foreground} && !$self->{daemon}; 202 push @cmd, "-F" if $self->{foreground}; 203 push @cmd, "-V" unless $self->{cacrt}; 204 push @cmd, "-C", $self->{cacrt} 205 if $self->{cacrt} && $self->{cacrt} ne "default"; 206 push @cmd, "-s", $self->{ctlsock} if $self->{ctlsock}; 207 push @cmd, @{$self->{options}} if $self->{options}; 208 print STDERR "execute: @cmd\n"; 209 exec @cmd; 210 die ref($self), " exec '@cmd' failed: $!"; 211} 212 213sub up { 214 my $self = Proc::up(shift, @_); 215 my $timeout = shift || 10; 216 217 my $end = time() + $timeout; 218 219 while ($self->{fstat}) { 220 $self->fstat(); 221 last unless $self->{foreground} || $self->{daemon}; 222 223 # in foreground mode and as daemon we have no debug output 224 # check fstat kqueue entry to detect statup 225 open(my $fh, '<', $self->{fstatfile}) or die ref($self), 226 " open $self->{fstatfile} for reading failed: $!"; 227 last if grep { /kqueue .* state: W/ } <$fh>; 228 time() < $end 229 or croak ref($self), " no 'kqueue' in $self->{fstatfile} ". 230 "after $timeout seconds"; 231 sleep .1; 232 } 233 234 return $self; 235} 236 237sub down { 238 my $self = shift; 239 240 if (my $dir = $self->{tempdir}) { 241 # keep all logs in single directory for easy debugging 242 copy($_, ".") foreach glob("$dir/*"); 243 } 244 245 return Proc::down($self, @_) unless $self->{daemon}; 246 247 my $timeout = $_[0] || 10; 248 my $end = time() + $timeout; 249 250 my @sudo = $ENV{SUDO} ? $ENV{SUDO} : "env"; 251 my @pkill = (@sudo, "pkill", "-TERM", "-x", "syslogd"); 252 my @pgrep = ("pgrep", "-x", "syslogd"); 253 system(@pkill) && $? != 256 254 and die ref($self), " system '@pkill' failed: $?"; 255 do { 256 sleep .1; 257 system(@pgrep) && $? != 256 258 and die ref($self), " system '@pgrep' failed: $?"; 259 return Proc::down($self, @_) if $? == 256; 260 print STDERR "syslogd still running\n"; 261 } while (time() < $end); 262 263 return; 264} 265 266sub fstat { 267 my $self = shift; 268 269 open(my $fh, '>', $self->{fstatfile}) or die ref($self), 270 " open $self->{fstatfile} for writing failed: $!"; 271 my @cmd = ("fstat"); 272 open(my $fs, '-|', @cmd) 273 or die ref($self), " open pipe from '@cmd' failed: $!"; 274 print $fh grep { /^\w+ *syslogd *\d+/ } <$fs>; 275 close($fs) or die ref($self), $! ? 276 " close pipe from '@cmd' failed: $!" : 277 " command '@cmd' failed: $?"; 278 close($fh) 279 or die ref($self), " close $self->{fstatfile} failed: $!"; 280} 281 282sub _make_abspath { 283 my $file = ref($_[0]) ? ${$_[0]} : $_[0]; 284 if (substr($file, 0, 1) ne "/") { 285 $file = getcwd(). "/". $file; 286 ${$_[0]} = $file if ref($_[0]); 287 } 288 return $file; 289} 290 291sub kill_privsep { 292 return Proc::kill(@_); 293} 294 295sub kill_syslogd { 296 my $self = shift; 297 my $sig = shift // 'TERM'; 298 my $ppid = shift // $self->{pid}; 299 300 # find syslogd child of privsep parent 301 my @cmd = ("ps", "-ww", "-p", $ppid, "-U", "_syslogd", 302 "-o", "pid,ppid,comm", ); 303 open(my $ps, '-|', @cmd) 304 or die ref($self), " open pipe from '@cmd' failed: $!"; 305 my @pslist; 306 my @pshead = split(' ', scalar <$ps>); 307 while (<$ps>) { 308 s/\s+$//; 309 my %h; 310 @h{@pshead} = split(' ', $_, scalar @pshead); 311 push @pslist, \%h; 312 } 313 close($ps) or die ref($self), $! ? 314 " close pipe from '@cmd' failed: $!" : 315 " command '@cmd' failed: $?"; 316 my @pschild = 317 grep { $_->{PPID} == $ppid && $_->{COMMAND} eq "syslogd" } @pslist; 318 @pschild == 1 319 or die ref($self), " not one privsep child: ", 320 join(" ", map { $_->{PID} } @pschild); 321 322 return Proc::kill($self, $sig, $pschild[0]{PID}); 323} 324 325my $rotate_num = 0; 326sub rotate { 327 my $self = shift; 328 329 $self->loggrep("bytes transferred", 1) or sleep 1; 330 foreach my $name (qw(file pipe)) { 331 my $file = $self->{"out$name"}; 332 for (my $i = $rotate_num; $i >= 0; $i--) { 333 my $new = $file. ".$i"; 334 my $old = $file. ($i > 0 ? ".".($i-1) : ""); 335 336 rename($old, $new) or die ref($self), 337 " rename from '$old' to '$new' failed: $!"; 338 } 339 } 340 $rotate_num++; 341 return $self->create_out(); 342}; 343 3441; 345