1# $OpenBSD: funcs.pl,v 1.40 2022/03/25 14:15:10 bluhm Exp $ 2 3# Copyright (c) 2010-2021 Alexander Bluhm <bluhm@openbsd.org> 4# 5# Permission to use, copy, modify, and distribute this software for any 6# purpose with or without fee is hereby granted, provided that the above 7# copyright notice and this permission notice appear in all copies. 8# 9# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 17use strict; 18use warnings; 19no warnings 'experimental::smartmatch'; 20use feature 'switch'; 21use Errno; 22use List::Util qw(first); 23use Socket; 24use Socket6; 25use Sys::Syslog qw(:standard :extended :macros); 26use Time::HiRes 'sleep'; 27use IO::Socket; 28use IO::Socket::SSL; 29 30my $firstlog = "syslogd regress test first message"; 31my $secondlog = "syslogd regress test second message"; 32my $thirdlog = "syslogd regress test third message"; 33my $testlog = "syslogd regress test log message"; 34my $downlog = "syslogd regress client shutdown"; 35my $charlog = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; 36 37sub find_ports { 38 my %args = @_; 39 my $num = delete $args{num} // 1; 40 my $domain = delete $args{domain} // AF_INET; 41 my $addr = delete $args{addr} // "127.0.0.1"; 42 my $proto = delete $args{proto} // "udp"; 43 $proto = "tcp" if $proto eq "tls"; 44 45 my @sockets = (1..$num); 46 foreach my $s (@sockets) { 47 $s = IO::Socket::IP->new( 48 Domain => $domain, 49 LocalAddr => $addr, 50 Proto => $proto, 51 ) or die "find_ports: create and bind socket failed: $!"; 52 } 53 my @ports = map { $_->sockport() } @sockets; 54 55 return wantarray ? @ports : $ports[0]; 56} 57 58######################################################################## 59# Client funcs 60######################################################################## 61 62sub write_log { 63 my $self = shift; 64 65 write_message($self, $testlog); 66 IO::Handle::flush(\*STDOUT); 67 ${$self->{syslogd}}->loggrep($testlog, 2); 68 write_shutdown($self); 69} 70 71sub write_between2logs { 72 my $self = shift; 73 my $func = shift; 74 75 write_message($self, $firstlog); 76 $func->($self, @_); 77 write_message($self, $testlog); 78 IO::Handle::flush(\*STDOUT); 79 ${$self->{syslogd}}->loggrep($testlog, 2); 80 write_shutdown($self); 81} 82 83sub write_message { 84 my $self = shift; 85 86 if (defined($self->{connectdomain})) { 87 my $msg = join("", @_); 88 if ($self->{connectdomain} eq "sendsyslog") { 89 my $flags = $self->{connect}{flags} || 0; 90 sendsyslog($msg, $flags) 91 or die ref($self), " sendsyslog failed: $!"; 92 } elsif ($self->{connectproto} eq "udp") { 93 # writing UDP packets works only with syswrite() 94 defined(my $n = syswrite(STDOUT, $msg)) 95 or die ref($self), " write log line failed: $!"; 96 $n == length($msg) 97 or die ref($self), " short UDP write"; 98 } else { 99 print $msg; 100 print "\n" if $self->{connectproto} =~ /^(tcp|tls)$/; 101 } 102 print STDERR "<<< $msg\n"; 103 } else { 104 syslog(LOG_INFO, @_); 105 } 106} 107 108sub sendsyslog { 109 my $msg = shift; 110 my $flags = shift; 111 require 'sys/syscall.ph'; 112 return syscall(&SYS_sendsyslog, $msg, length($msg), $flags) != -1; 113} 114 115sub write_shutdown { 116 my $self = shift; 117 118 setlogsock("native") 119 or die ref($self), " setlogsock native failed: $!"; 120 syslog(LOG_NOTICE, $downlog); 121 122 if (defined($self->{connectdomain}) && 123 $self->{connectproto} eq "tls" && 124 $self->{exit}) { 125 # Due to missing handshakes TLS 1.3 cannot detect all 126 # connection errors while writing. Try to read. 127 defined(read(STDIN, my $buf, 1)) 128 or die ref($self), " error after shutdown: $!,$SSL_ERROR"; 129 } 130} 131 132sub write_lines { 133 my $self = shift; 134 my ($lines, $length) = @_; 135 136 foreach (1..$lines) { 137 write_chars($self, $length, " $_"); 138 } 139} 140 141sub write_lengths { 142 my $self = shift; 143 my ($lengths, $tail) = ref $_[0] ? @_ : [@_]; 144 145 write_chars($self, $lengths, $tail); 146} 147 148sub generate_chars { 149 my ($len) = @_; 150 151 my $msg = ""; 152 my $char = '0'; 153 for (my $i = 0; $i < $len; $i++) { 154 $msg .= $char; 155 given ($char) { 156 when(/9/) { $char = 'A' } 157 when(/Z/) { $char = 'a' } 158 when(/z/) { $char = '0' } 159 default { $char++ } 160 } 161 } 162 return $msg; 163} 164 165sub write_chars { 166 my $self = shift; 167 my ($length, $tail) = @_; 168 169 foreach my $len (ref $length ? @$length : $length) { 170 my $t = $tail // ""; 171 substr($t, 0, length($t) - $len, "") 172 if length($t) && length($t) > $len; 173 my $msg = generate_chars($len - length($t)); 174 $msg .= $t if length($t); 175 write_message($self, $msg); 176 # if client is sending too fast, syslogd will not see everything 177 sleep .01; 178 } 179} 180 181sub write_unix { 182 my $self = shift; 183 my $path = shift || "/dev/log"; 184 my $id = shift // $path; 185 186 my $u = IO::Socket::UNIX->new( 187 Type => SOCK_DGRAM, 188 Peer => $path, 189 ) or die ref($self), " connect to $path unix socket failed: $!"; 190 my $msg = "id $id unix socket: $testlog"; 191 print $u $msg; 192 print STDERR "<<< $msg\n"; 193} 194 195sub write_tcp { 196 my $self = shift; 197 my $fh = shift || \*STDOUT; 198 my $id = shift // $fh; 199 200 my $msg = "id $id tcp socket: $testlog"; 201 print $fh "$msg\n"; 202 print STDERR "<<< $msg\n"; 203} 204 205sub redo_connect { 206 my $self = shift; 207 my $func = shift; 208 209 $func->($self, @_); 210 if ($self->{cs}) { 211 # wait for possible icmp errors, port is open 212 sleep .1; 213 close(delete $self->{cs}) 214 or die ref($self), " close failed: $!"; 215 delete $self->{ts}; 216 } 217 if (my $redo = shift @{$self->{redo}}) { 218 if (my $connect = $redo->{connect}) { 219 delete $self->{logsock}; 220 $self->{connectdomain} = $connect->{domain}; 221 $self->{connectaddr} = $connect->{addr}; 222 $self->{connectproto} = $connect->{proto}; 223 $self->{connectport} = $connect->{port}; 224 } elsif (my $logsock = $redo->{logsock}) { 225 delete $self->{connectdomain}; 226 delete $self->{connectaddr}; 227 delete $self->{connectproto}; 228 delete $self->{connectport}; 229 $self->{logsock} = $logsock; 230 } else { 231 die ref($self), " no connect or logsock in redo"; 232 } 233 } else { 234 delete $self->{connectdomain}; 235 delete $self->{connectaddr}; 236 delete $self->{connectproto}; 237 delete $self->{connectport}; 238 $self->{logsock} = { type => "native" }; 239 setlogsock($self->{logsock}) 240 or die ref($self), " setlogsock failed: $!"; 241 sleep .1; 242 write_log($self); 243 undef $self->{redo}; 244 } 245} 246 247######################################################################## 248# Server funcs 249######################################################################## 250 251sub read_log { 252 my $self = shift; 253 254 read_message($self, $downlog); 255} 256 257sub read_between2logs { 258 my $self = shift; 259 my $func = shift; 260 261 read_message($self, $firstlog); 262 $func->($self, @_); 263 read_message($self, $testlog); 264 read_message($self, $downlog); 265} 266 267sub accept_between2logs { 268 my $self = shift; 269 my $func = shift; 270 271 unless ($self->{redo}) { 272 read_message($self, $firstlog); 273 $func->($self, @_); 274 $self->{redo} = 1; 275 } else { 276 $self->{redo} = 0; 277 read_message($self, $testlog); 278 read_message($self, $downlog); 279 } 280} 281 282sub read_message { 283 my $self = shift; 284 my $regex = shift; 285 286 local $_; 287 for (;;) { 288 if ($self->{listenproto} eq "udp") { 289 # reading UDP packets works only with sysread() 290 defined(my $n = sysread(STDIN, $_, 8194)) 291 or die ref($self), " read log line failed: $!"; 292 last if $n == 0; 293 } else { 294 defined($_ = <STDIN>) 295 or last; 296 } 297 chomp; 298 print STDERR ">>> $_\n"; 299 last if /$regex/; 300 } 301} 302 303######################################################################## 304# Script funcs 305######################################################################## 306 307sub get_testlog { 308 return $testlog; 309} 310 311sub get_testgrep { 312 return qr/$testlog\r*$/; 313} 314 315sub get_firstlog { 316 return $firstlog; 317} 318 319sub get_secondlog { 320 return $secondlog; 321} 322 323sub get_thirdlog { 324 return $thirdlog; 325} 326 327sub get_charlog { 328 # add a space so that we match at the beginning of the message 329 return " $charlog"; 330} 331 332sub get_between2loggrep { 333 return ( 334 qr/$firstlog/ => 1, 335 qr/$testlog/ => 1, 336 ); 337} 338 339sub get_downlog { 340 return $downlog; 341} 342 343sub selector2config { 344 my %s2m = @_; 345 my $conf = ""; 346 my $i = 0; 347 foreach my $sel (sort keys %s2m) { 348 $conf .= "$sel\t\$objdir/file-$i.log\n"; 349 $i++; 350 } 351 return $conf; 352} 353 354sub selector2loggrep { 355 my %s2m = @_; 356 my %allmsg; 357 @allmsg{map { @$_} values %s2m} = (); 358 my @loggrep; 359 foreach my $sel (sort keys %s2m) { 360 my @m = @{$s2m{$sel}}; 361 my %msg; 362 $msg{$_}++ foreach (@m); 363 my %nomsg = %allmsg; 364 delete @nomsg{@m}; 365 push @loggrep, { 366 (map { qr/: $_$/ => $msg{$_} } sort keys %msg), 367 (map { qr/: $_$/ => 0 } sort keys %nomsg), 368 }; 369 } 370 return @loggrep; 371} 372 373sub check_logs { 374 my ($c, $r, $s, $m, %args) = @_; 375 376 return if $args{nocheck}; 377 378 check_log($c, $r, $s, @$m); 379 check_out($r, %args); 380 check_fstat($c, $r, $s); 381 check_ktrace($c, $r, $s); 382 if (my $file = $s->{"outfile"}) { 383 my $pattern = $s->{filegrep} || get_testgrep(); 384 check_pattern(ref $s, $file, $pattern, \&filegrep); 385 } 386 check_multifile(@{$args{multifile} || []}); 387} 388 389sub compare($$) { 390 local $_ = $_[1]; 391 if (/^\d+/) { 392 return $_[0] == $_; 393 } elsif (/^==(\d+)/) { 394 return $_[0] == $1; 395 } elsif (/^!=(\d+)/) { 396 return $_[0] != $1; 397 } elsif (/^>=(\d+)/) { 398 return $_[0] >= $1; 399 } elsif (/^<=(\d+)/) { 400 return $_[0] <= $1; 401 } elsif (/^~(\d+)/) { 402 return $1 * 0.8 <= $_[0] && $_[0] <= $1 * 1.2; 403 } 404 die "bad compare operator: $_"; 405} 406 407sub check_pattern { 408 my ($name, $proc, $pattern, $func) = @_; 409 410 $pattern = [ $pattern ] unless ref($pattern) eq 'ARRAY'; 411 foreach my $pat (@$pattern) { 412 if (ref($pat) eq 'HASH') { 413 foreach my $re (sort keys %$pat) { 414 my $num = $pat->{$re}; 415 my @matches = $func->($proc, $re); 416 compare(@matches, $num) 417 or die "$name matches '@matches': ", 418 "'$re' => $num"; 419 } 420 } else { 421 $func->($proc, $pat) 422 or die "$name log missing pattern: $pat"; 423 } 424 } 425} 426 427sub check_log { 428 foreach my $proc (@_) { 429 next unless $proc && !$proc->{nocheck}; 430 my $pattern = $proc->{loggrep} || get_testgrep(); 431 check_pattern(ref $proc, $proc, $pattern, \&loggrep); 432 } 433} 434 435sub loggrep { 436 my ($proc, $pattern) = @_; 437 438 return $proc->loggrep($pattern); 439} 440 441sub check_out { 442 my ($r, %args) = @_; 443 444 unless ($args{pipe}{nocheck}) { 445 $r->loggrep("bytes transferred", 1) or sleep 1; 446 } 447 foreach my $dev (qw(console user)) { 448 $args{$dev}{nocheck} ||= $args{tty}{nocheck}; 449 $args{$dev}{loggrep} ||= $args{tty}{loggrep}; 450 next if $args{$dev}{nocheck}; 451 my $ctl = $r->{"ctl$dev"}; 452 close($ctl); 453 my $file = $r->{"out$dev"}; 454 open(my $fh, '<', $file) 455 or die "Open file $file for reading failed: $!"; 456 grep { /^logout/ or /^console .* off/ } <$fh> or sleep 1; 457 close($fh); 458 } 459 460 foreach my $name (qw(file pipe console user)) { 461 next if $args{$name}{nocheck}; 462 my $file = $r->{"out$name"} or die; 463 my $pattern = $args{$name}{loggrep} || get_testgrep(); 464 check_pattern($name, $file, $pattern, \&filegrep); 465 } 466} 467 468sub check_fstat { 469 foreach my $proc (@_) { 470 my $pattern = $proc && $proc->{fstat} or next; 471 my $file = $proc->{fstatfile} or die; 472 check_pattern("fstat", $file, $pattern, \&filegrep); 473 } 474} 475 476sub filegrep { 477 my ($file, $pattern) = @_; 478 479 open(my $fh, '<', $file) 480 or die "Open file $file for reading failed: $!"; 481 return wantarray ? 482 grep { /$pattern/ } <$fh> : first { /$pattern/ } <$fh>; 483} 484 485sub check_ktrace { 486 foreach my $proc (@_) { 487 my $pattern = $proc && $proc->{ktrace} or next; 488 my $file = $proc->{ktracefile} or die; 489 check_pattern("ktrace", $file, $pattern, \&kdumpgrep); 490 } 491} 492 493sub kdumpgrep { 494 my ($file, $pattern) = @_; 495 496 my @sudo = ! -r $file && $ENV{SUDO} ? $ENV{SUDO} : (); 497 my @cmd = (@sudo, "kdump", "-f", $file); 498 open(my $fh, '-|', @cmd) 499 or die "Open pipe from '@cmd' failed: $!"; 500 my @matches = grep { /$pattern/ } <$fh>; 501 close($fh) or die $! ? 502 "Close pipe from '@cmd' failed: $!" : 503 "Command '@cmd' failed: $?"; 504 return wantarray ? @matches : $matches[0]; 505} 506 507sub create_multifile { 508 for (my $i = 0; $i < @_; $i++) { 509 my $file = "file-$i.log"; 510 open(my $fh, '>', $file) 511 or die "Create $file failed: $!"; 512 } 513} 514 515sub check_multifile { 516 for (my $i = 0; $i < @_; $i++) { 517 my $file = "file-$i.log"; 518 my $pattern = $_[$i]{loggrep} or die; 519 check_pattern("multifile $i", $file, $pattern, \&filegrep); 520 } 521} 522 5231; 524