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