1# $OpenBSD: funcs.pl,v 1.10 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 Digest::MD5; 21use POSIX; 22use Socket; 23use Socket6; 24use IO::Socket; 25 26sub find_ports { 27 my %args = @_; 28 my $num = delete $args{num} // 1; 29 my $domain = delete $args{domain} // AF_INET; 30 my $addr = delete $args{addr} // "127.0.0.1"; 31 32 my @sockets = (1..$num); 33 foreach my $s (@sockets) { 34 $s = IO::Socket::IP->new( 35 Proto => "tcp", 36 Domain => $domain, 37 $addr ? (LocalAddr => $addr) : (), 38 ) or die "find_ports: create and bind socket failed: $!"; 39 } 40 my @ports = map { $_->sockport() } @sockets; 41 42 return @ports; 43} 44 45sub path_md5 { 46 my $name = shift; 47 my $val = `cat md5-$name`; 48} 49 50######################################################################## 51# Client funcs 52######################################################################## 53 54sub write_char { 55 my $self = shift; 56 my $len = shift // $self->{len} // 512; 57 my $sleep = $self->{sleep}; 58 59 my $ctx = Digest::MD5->new(); 60 my $char = '0'; 61 for (my $i = 1; $i < $len; $i++) { 62 $ctx->add($char); 63 print $char 64 or die ref($self), " print failed: $!"; 65 if ($char =~ /9/) { $char = 'A' } 66 elsif ($char =~ /Z/) { $char = 'a' } 67 elsif ($char =~ /z/) { $char = "\n" } 68 elsif ($char =~ /\n/) { print STDERR "."; $char = '0' } 69 else { $char++ } 70 if ($self->{sleep}) { 71 IO::Handle::flush(\*STDOUT); 72 sleep $self->{sleep}; 73 } 74 } 75 if ($len) { 76 $char = "\n"; 77 $ctx->add($char); 78 print $char 79 or die ref($self), " print failed: $!"; 80 print STDERR ".\n"; 81 } 82 IO::Handle::flush(\*STDOUT); 83 84 print STDERR "LEN: ", $len, "\n"; 85 print STDERR "MD5: ", $ctx->hexdigest, "\n"; 86} 87 88sub http_client { 89 my $self = shift; 90 91 unless ($self->{lengths}) { 92 # only a single http request 93 my $len = shift // $self->{len} // 512; 94 my $cookie = $self->{cookie}; 95 http_request($self, $len, "1.0", $cookie); 96 http_response($self, $len); 97 return; 98 } 99 100 $self->{http_vers} ||= ["1.1", "1.0"]; 101 my $vers = $self->{http_vers}[0]; 102 my @lengths = @{$self->{redo}{lengths} || $self->{lengths}}; 103 my @cookies = @{$self->{redo}{cookies} || $self->{cookies} || []}; 104 while (defined (my $len = shift @lengths)) { 105 my $cookie = shift @cookies || $self->{cookie}; 106 eval { 107 http_request($self, $len, $vers, $cookie); 108 http_response($self, $len); 109 }; 110 warn $@ if $@; 111 if (@lengths && ($@ || $vers eq "1.0")) { 112 # reconnect and redo the outstanding requests 113 $self->{redo} = { 114 lengths => \@lengths, 115 cookies => \@cookies, 116 }; 117 return; 118 } 119 } 120 delete $self->{redo}; 121 shift @{$self->{http_vers}}; 122 if (@{$self->{http_vers}}) { 123 # run the tests again with other persistence 124 $self->{redo} = { 125 lengths => [@{$self->{lengths}}], 126 cookies => [@{$self->{cookies} || []}], 127 }; 128 } 129} 130 131sub http_request { 132 my ($self, $len, $vers, $cookie) = @_; 133 my $method = $self->{method} || "GET"; 134 my %header = %{$self->{header} || {}}; 135 136 # encode the requested length or chunks into the url 137 my $path = ref($len) eq 'ARRAY' ? join("/", @$len) : $len; 138 # overwrite path with custom path 139 if (defined($self->{path})) { 140 $path = $self->{path}; 141 } 142 my @request = ("$method /$path HTTP/$vers"); 143 push @request, "Host: foo.bar" unless defined $header{Host}; 144 if ($vers eq "1.1" && $method eq "PUT") { 145 if (ref($len) eq 'ARRAY') { 146 push @request, "Transfer-Encoding: chunked" 147 if !defined $header{'Transfer-Encoding'}; 148 } else { 149 push @request, "Content-Length: $len" 150 if !defined $header{'Content-Length'}; 151 } 152 } 153 foreach my $key (sort keys %header) { 154 my $val = $header{$key}; 155 if (ref($val) eq 'ARRAY') { 156 push @request, "$key: $_" 157 foreach @{$val}; 158 } else { 159 push @request, "$key: $val"; 160 } 161 } 162 push @request, "Cookie: $cookie" if $cookie; 163 push @request, ""; 164 print STDERR map { ">>> $_\n" } @request; 165 print map { "$_\r\n" } @request; 166 if ($method eq "PUT") { 167 if (ref($len) eq 'ARRAY') { 168 if ($vers eq "1.1") { 169 write_chunked($self, @$len); 170 } else { 171 write_char($self, $_) foreach (@$len); 172 } 173 } else { 174 write_char($self, $len); 175 } 176 } 177 IO::Handle::flush(\*STDOUT); 178 # XXX client shutdown seems to be broken in httpd 179 #shutdown(\*STDOUT, SHUT_WR) 180 # or die ref($self), " shutdown write failed: $!" 181 # if $vers ne "1.1"; 182} 183 184sub http_response { 185 my ($self, $len) = @_; 186 my $method = $self->{method} || "GET"; 187 my $code = $self->{code} || "200 OK"; 188 189 my $vers; 190 my $chunked = 0; 191 my $multipart = 0; 192 my $boundary; 193 { 194 local $/ = "\r\n"; 195 local $_ = <STDIN>; 196 defined 197 or die ref($self), " missing http $len response"; 198 chomp; 199 print STDERR "<<< $_\n"; 200 m{^HTTP/(\d\.\d) $code$} 201 or die ref($self), " http response not $code" 202 unless $self->{httpnok}; 203 $vers = $1; 204 while (<STDIN>) { 205 chomp; 206 print STDERR "<<< $_\n"; 207 last if /^$/; 208 if (/^Content-Length: (.*)/) { 209 if ($self->{httpnok} or $self->{multipart}) { 210 $len = $1; 211 } else { 212 $1 == $len or die ref($self), 213 " bad content length $1"; 214 } 215 } 216 if (/^Transfer-Encoding: chunked$/) { 217 $chunked = 1; 218 } 219 if (/^Content-Type: multipart\/byteranges; boundary=(.*)$/) { 220 $multipart = 1; 221 $boundary = $1; 222 } 223 } 224 } 225 die ref($self), " no multipart response" 226 if ($self->{multipart} && $multipart == 0); 227 228 if ($multipart) { 229 read_multipart($self, $boundary); 230 } elsif ($chunked) { 231 read_chunked($self); 232 } else { 233 read_char($self, $len) 234 if $method eq "GET"; 235 } 236} 237 238sub read_chunked { 239 my $self = shift; 240 241 for (;;) { 242 my $len; 243 { 244 local $/ = "\r\n"; 245 local $_ = <STDIN>; 246 defined or die ref($self), " missing chunk size"; 247 chomp; 248 print STDERR "<<< $_\n"; 249 /^[[:xdigit:]]+$/ 250 or die ref($self), " chunk size not hex: $_"; 251 $len = hex; 252 } 253 last unless $len > 0; 254 read_char($self, $len); 255 { 256 local $/ = "\r\n"; 257 local $_ = <STDIN>; 258 defined or die ref($self), " missing chunk data end"; 259 chomp; 260 print STDERR "<<< $_\n"; 261 /^$/ or die ref($self), " no chunk data end: $_"; 262 } 263 } 264 { 265 local $/ = "\r\n"; 266 while (<STDIN>) { 267 chomp; 268 print STDERR "<<< $_\n"; 269 last if /^$/; 270 } 271 defined or die ref($self), " missing chunk trailer"; 272 } 273} 274 275sub read_multipart { 276 my $self = shift; 277 my $boundary = shift; 278 my $ctx = Digest::MD5->new(); 279 my $len = 0; 280 281 for (;;) { 282 my $part = 0; 283 { 284 local $/ = "\r\n"; 285 local $_ = <STDIN>; 286 local $_ = <STDIN>; 287 defined or die ref($self), " missing boundary"; 288 chomp; 289 print STDERR "<<< $_\n"; 290 /^--$boundary(--)?$/ 291 or die ref($self), " boundary not found: $_"; 292 if (not $1) { 293 while (<STDIN>) { 294 chomp; 295 if (/^Content-Length: (.*)/) { 296 $part = $1; 297 } 298 if (/^Content-Range: bytes (\d+)-(\d+)\/(\d+)$/) { 299 $part = $2 - $1 + 1; 300 } 301 print STDERR "<<< $_\n"; 302 last if /^$/; 303 } 304 } 305 } 306 last unless $part > 0; 307 308 $len += read_part($self, $ctx, $part); 309 } 310 311 print STDERR "LEN: ", $len, "\n"; 312 print STDERR "MD5: ", $ctx->hexdigest, "\n"; 313} 314 315sub errignore { 316 $SIG{PIPE} = 'IGNORE'; 317 $SIG{__DIE__} = sub { 318 die @_ if $^S; 319 warn "Error ignored"; 320 warn @_; 321 IO::Handle::flush(\*STDERR); 322 POSIX::_exit(0); 323 }; 324} 325 326######################################################################## 327# Common funcs 328######################################################################## 329 330sub read_char { 331 my $self = shift; 332 my $max = shift // $self->{max}; 333 334 my $ctx = Digest::MD5->new(); 335 my $len = read_part($self, $ctx, $max); 336 337 print STDERR "LEN: ", $len, "\n"; 338 print STDERR "MD5: ", $ctx->hexdigest, "\n"; 339} 340 341sub read_part { 342 my $self = shift; 343 my ($ctx, $max) = @_; 344 345 my $opct = 0; 346 my $len = 0; 347 for (;;) { 348 if (defined($max) && $len >= $max) { 349 print STDERR "Max\n"; 350 last; 351 } 352 my $rlen = POSIX::BUFSIZ; 353 if (defined($max) && $rlen > $max - $len) { 354 $rlen = $max - $len; 355 } 356 defined(my $n = read(STDIN, my $buf, $rlen)) 357 or die ref($self), " read failed: $!"; 358 $n or last; 359 $len += $n; 360 $ctx->add($buf); 361 my $pct = ($len / $max) * 100.0; 362 if ($pct >= $opct + 1) { 363 printf(STDERR "%.2f%% $len/$max\n", $pct); 364 $opct = $pct; 365 } 366 } 367 return $len; 368} 369 370sub write_chunked { 371 my $self = shift; 372 my @chunks = @_; 373 374 foreach my $len (@chunks) { 375 printf STDERR ">>> %x\n", $len; 376 printf "%x\r\n", $len; 377 write_char($self, $len); 378 printf STDERR ">>> \n"; 379 print "\r\n"; 380 } 381 my @trailer = ("0", "X-Chunk-Trailer: @chunks", ""); 382 print STDERR map { ">>> $_\n" } @trailer; 383 print map { "$_\r\n" } @trailer; 384} 385 386######################################################################## 387# Script funcs 388######################################################################## 389 390sub check_logs { 391 my ($c, $r, %args) = @_; 392 393 return if $args{nocheck}; 394 395 check_len($c, $r, %args); 396 check_md5($c, $r, %args); 397 check_loggrep($c, $r, %args); 398 $r->loggrep("lost child") 399 and die "httpd lost child"; 400} 401 402sub check_len { 403 my ($c, $r, %args) = @_; 404 405 $args{len} ||= 512 unless $args{lengths}; 406 407 my @clen; 408 @clen = $c->loggrep(qr/^LEN: /) or die "no client len" 409 unless $args{client}{nocheck}; 410# !@clen 411# or die "client: @clen", "len mismatch"; 412 !defined($args{len}) || !$clen[0] || $clen[0] eq "LEN: $args{len}\n" 413 or die "client: $clen[0]", "len $args{len} expected"; 414 my @lengths = map { ref eq 'ARRAY' ? @$_ : $_ } 415 @{$args{lengths} || []}; 416 foreach my $len (@lengths) { 417 unless ($args{client}{nocheck}) { 418 my $clen = shift @clen; 419 $clen eq "LEN: $len\n" 420 or die "client: $clen", "len $len expected"; 421 } 422 } 423} 424 425sub check_md5 { 426 my ($c, $r, %args) = @_; 427 428 my @cmd5; 429 @cmd5 = $c->loggrep(qr/^MD5: /) unless $args{client}{nocheck}; 430 my @md5 = ref($args{md5}) eq 'ARRAY' ? @{$args{md5}} : $args{md5} || () 431 or return; 432 foreach my $md5 (@md5) { 433 unless ($args{client}{nocheck}) { 434 my $cmd5 = shift @cmd5 435 or die "too few md5 in client log"; 436 $cmd5 =~ /^MD5: ($md5)$/ 437 or die "client: $cmd5", "md5 $md5 expected"; 438 } 439 } 440 @cmd5 && ref($args{md5}) eq 'ARRAY' 441 and die "too many md5 in client log"; 442} 443 444sub check_loggrep { 445 my ($c, $r, %args) = @_; 446 447 my %name2proc = (client => $c, httpd => $r); 448 foreach my $name (qw(client httpd)) { 449 my $p = $name2proc{$name} or next; 450 my $pattern = $args{$name}{loggrep} or next; 451 $pattern = [ $pattern ] unless ref($pattern) eq 'ARRAY'; 452 foreach my $pat (@$pattern) { 453 if (ref($pat) eq 'HASH') { 454 while (my($re, $num) = each %$pat) { 455 my @matches = $p->loggrep($re); 456 @matches == $num 457 or die "$name matches '@matches': ", 458 "'$re' => $num"; 459 } 460 } else { 461 $p->loggrep($pat) 462 or die "$name log missing pattern: '$pat'"; 463 } 464 } 465 } 466} 467 4681; 469