1# $OpenBSD: funcs.pl,v 1.23 2017/07/14 14:41:03 bluhm Exp $ 2 3# Copyright (c) 2010-2017 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 Digest::MD5; 23use Socket; 24use Socket6; 25use IO::Socket; 26use IO::Socket::INET6; 27 28sub find_ports { 29 my %args = @_; 30 my $num = delete $args{num} // 1; 31 my $domain = delete $args{domain} // AF_INET; 32 my $addr = delete $args{addr} // "127.0.0.1"; 33 34 my @sockets = (1..$num); 35 foreach my $s (@sockets) { 36 $s = IO::Socket::INET6->new( 37 Proto => "tcp", 38 Domain => $domain, 39 $addr ? (LocalAddr => $addr) : (), 40 ) or die "find_ports: create and bind socket failed: $!"; 41 } 42 my @ports = map { $_->sockport() } @sockets; 43 44 return @ports; 45} 46 47######################################################################## 48# Client funcs 49######################################################################## 50 51sub write_syswrite { 52 my $self = shift; 53 my $buf = shift; 54 55 IO::Handle::flush(\*STDOUT); 56 my $size = length($buf); 57 my $len = 0; 58 while ($len < $size) { 59 my $n = syswrite(STDOUT, $buf, $size, $len); 60 if (!defined($n)) { 61 $!{EWOULDBLOCK} 62 or die ref($self), " syswrite failed: $!"; 63 print STDERR "blocked write at $len of $size: $!\n"; 64 next; 65 } 66 if ($len + $n != $size) { 67 print STDERR "short write $n at $len of $size\n"; 68 } 69 $len += $n; 70 } 71 return $len; 72} 73 74sub write_block { 75 my $self = shift; 76 my $len = shift; 77 78 my $data; 79 my $outb = 0; 80 my $blocks = int($len / 1000); 81 my $rest = $len % 1000; 82 83 for (my $i = 1; $i <= 100 ; $i++) { 84 $data .= "012345678\n"; 85 } 86 87 my $opct = 0; 88 for (my $i = 1; $i <= $blocks; $i++) { 89 $outb += write_syswrite($self, $data); 90 my $pct = ($outb / $len) * 100.0; 91 if ($pct >= $opct + 1) { 92 printf(STDERR "%.2f%% $outb/$len\n", $pct); 93 $opct = $pct; 94 } 95 } 96 97 if ($rest>0) { 98 for (my $i = 1; $i < $rest-1 ; $i++) { 99 $outb += write_syswrite($self, 'r'); 100 my $pct = ($outb / $len) * 100.0; 101 if ($pct >= $opct + 1) { 102 printf(STDERR "%.2f%% $outb/$len\n", $pct); 103 $opct = $pct; 104 } 105 } 106 } 107 $outb += write_syswrite($self, "\n\n"); 108 IO::Handle::flush(\*STDOUT); 109 print STDERR "LEN: ", $outb, "\n"; 110} 111 112sub write_char { 113 my $self = shift; 114 my $len = shift // $self->{len} // 251; 115 my $sleep = $self->{sleep}; 116 117 if ($self->{fast}) { 118 write_block($self, $len); 119 return; 120 } 121 122 my $ctx = Digest::MD5->new(); 123 my $char = '0'; 124 for (my $i = 1; $i < $len; $i++) { 125 $ctx->add($char); 126 print $char 127 or die ref($self), " print failed: $!"; 128 given ($char) { 129 when(/9/) { $char = 'A' } 130 when(/Z/) { $char = 'a' } 131 when(/z/) { $char = "\n" } 132 when(/\n/) { print STDERR "."; $char = '0' } 133 default { $char++ } 134 } 135 if ($self->{sleep}) { 136 IO::Handle::flush(\*STDOUT); 137 sleep $self->{sleep}; 138 } 139 } 140 if ($len) { 141 $char = "\n"; 142 $ctx->add($char); 143 print $char 144 or die ref($self), " print failed: $!"; 145 print STDERR ".\n"; 146 } 147 IO::Handle::flush(\*STDOUT); 148 149 print STDERR "LEN: ", $len, "\n"; 150 print STDERR "MD5: ", $ctx->hexdigest, "\n"; 151} 152 153sub http_client { 154 my $self = shift; 155 156 unless ($self->{lengths}) { 157 # only a single http request 158 my $len = shift // $self->{len} // 251; 159 my $cookie = $self->{cookie}; 160 http_request($self, $len, "1.0", $cookie); 161 http_response($self, $len); 162 return; 163 } 164 165 $self->{http_vers} ||= ["1.1", "1.0"]; 166 my $vers = $self->{http_vers}[0]; 167 my @lengths = @{$self->{redo}{lengths} || $self->{lengths}}; 168 my @cookies = @{$self->{redo}{cookies} || $self->{cookies} || []}; 169 while (defined (my $len = shift @lengths)) { 170 my $cookie = shift @cookies || $self->{cookie}; 171 eval { 172 http_request($self, $len, $vers, $cookie); 173 http_response($self, $len); 174 }; 175 warn $@ if $@; 176 if (@lengths && ($@ || $vers eq "1.0")) { 177 # reconnect and redo the outstanding requests 178 $self->{redo} = { 179 lengths => \@lengths, 180 cookies => \@cookies, 181 }; 182 return; 183 } 184 } 185 delete $self->{redo}; 186 shift @{$self->{http_vers}}; 187 if (@{$self->{http_vers}}) { 188 # run the tests again with other persistence 189 $self->{redo} = { 190 lengths => [@{$self->{lengths}}], 191 cookies => [@{$self->{cookies} || []}], 192 }; 193 } 194} 195 196sub http_request { 197 my ($self, $len, $vers, $cookie) = @_; 198 my $method = $self->{method} || "GET"; 199 my %header = %{$self->{header} || {}}; 200 201 # encode the requested length or chunks into the url 202 my $path = ref($len) eq 'ARRAY' ? join("/", @$len) : $len; 203 # overwrite path with custom path 204 if (defined($self->{path})) { 205 $path = $self->{path}; 206 } 207 my @request = ("$method /$path HTTP/$vers"); 208 push @request, "Host: foo.bar" unless defined $header{Host}; 209 if ($vers eq "1.1" && $method eq "PUT") { 210 if (ref($len) eq 'ARRAY') { 211 push @request, "Transfer-Encoding: chunked" 212 if !defined $header{'Transfer-Encoding'}; 213 } else { 214 push @request, "Content-Length: $len" 215 if !defined $header{'Content-Length'}; 216 } 217 } 218 foreach my $key (sort keys %header) { 219 my $val = $header{$key}; 220 if (ref($val) eq 'ARRAY') { 221 push @request, "$key: $_" 222 foreach @{$val}; 223 } else { 224 push @request, "$key: $val"; 225 } 226 } 227 push @request, "Cookie: $cookie" if $cookie; 228 push @request, ""; 229 print STDERR map { ">>> $_\n" } @request; 230 print map { "$_\r\n" } @request; 231 if ($method eq "PUT") { 232 if (ref($len) eq 'ARRAY') { 233 if ($vers eq "1.1") { 234 write_chunked($self, @$len); 235 } else { 236 write_char($self, $_) foreach (@$len); 237 } 238 } else { 239 write_char($self, $len); 240 } 241 } 242 IO::Handle::flush(\*STDOUT); 243 # XXX client shutdown seems to be broken in relayd 244 #shutdown(\*STDOUT, SHUT_WR) 245 # or die ref($self), " shutdown write failed: $!" 246 # if $vers ne "1.1"; 247} 248 249sub http_response { 250 my ($self, $len) = @_; 251 my $method = $self->{method} || "GET"; 252 253 my $vers; 254 my $chunked = 0; 255 { 256 local $/ = "\r\n"; 257 local $_ = <STDIN>; 258 defined 259 or die ref($self), " missing http $len response"; 260 chomp; 261 print STDERR "<<< $_\n"; 262 m{^HTTP/(\d\.\d) 200 OK$} 263 or die ref($self), " http response not ok" 264 unless $self->{httpnok}; 265 $vers = $1; 266 while (<STDIN>) { 267 chomp; 268 print STDERR "<<< $_\n"; 269 last if /^$/; 270 if (/^Content-Length: (.*)/) { 271 if ($self->{httpnok}) { 272 $len = $1; 273 } else { 274 $1 == $len or die ref($self), 275 " bad content length $1"; 276 } 277 } 278 if (/^Transfer-Encoding: chunked$/) { 279 $chunked = 1; 280 } 281 } 282 } 283 if ($chunked) { 284 read_chunked($self); 285 } else { 286 undef $len unless defined($vers) && $vers eq "1.1"; 287 read_char($self, $len) 288 if $method eq "GET"; 289 } 290} 291 292sub read_chunked { 293 my $self = shift; 294 295 for (;;) { 296 my $len; 297 { 298 local $/ = "\r\n"; 299 local $_ = <STDIN>; 300 defined or die ref($self), " missing chunk size"; 301 chomp; 302 print STDERR "<<< $_\n"; 303 /^[[:xdigit:]]+$/ 304 or die ref($self), " chunk size not hex: $_"; 305 $len = hex; 306 } 307 last unless $len > 0; 308 read_char($self, $len); 309 { 310 local $/ = "\r\n"; 311 local $_ = <STDIN>; 312 defined or die ref($self), " missing chunk data end"; 313 chomp; 314 print STDERR "<<< $_\n"; 315 /^$/ or die ref($self), " no chunk data end: $_"; 316 } 317 } 318 { 319 local $/ = "\r\n"; 320 while (<STDIN>) { 321 chomp; 322 print STDERR "<<< $_\n"; 323 last if /^$/; 324 } 325 defined or die ref($self), " missing chunk trailer"; 326 } 327} 328 329sub errignore { 330 $SIG{PIPE} = 'IGNORE'; 331 $SIG{__DIE__} = sub { 332 die @_ if $^S; 333 warn "Error ignored"; 334 warn @_; 335 IO::Handle::flush(\*STDERR); 336 POSIX::_exit(0); 337 }; 338} 339 340######################################################################## 341# Common funcs 342######################################################################## 343 344sub read_char { 345 my $self = shift; 346 my $max = shift // $self->{max}; 347 348 if ($self->{fast}) { 349 read_block($self, $max); 350 return; 351 } 352 353 my $ctx = Digest::MD5->new(); 354 my $len = 0; 355 if (defined($max) && $max == 0) { 356 print STDERR "Max\n"; 357 } else { 358 while (<STDIN>) { 359 $len += length($_); 360 $ctx->add($_); 361 print STDERR "."; 362 if (defined($max) && $len >= $max) { 363 print STDERR "\nMax"; 364 last; 365 } 366 } 367 print STDERR "\n"; 368 } 369 370 print STDERR "LEN: ", $len, "\n"; 371 print STDERR "MD5: ", $ctx->hexdigest, "\n"; 372} 373 374sub read_block { 375 my $self = shift; 376 my $max = shift // $self->{max}; 377 378 my $opct = 0; 379 my $ctx = Digest::MD5->new(); 380 my $len = 0; 381 for (;;) { 382 if (defined($max) && $len >= $max) { 383 print STDERR "Max\n"; 384 last; 385 } 386 my $rlen = POSIX::BUFSIZ; 387 if (defined($max) && $rlen > $max - $len) { 388 $rlen = $max - $len; 389 } 390 defined(my $n = read(STDIN, my $buf, $rlen)) 391 or die ref($self), " read failed: $!"; 392 $n or last; 393 $len += $n; 394 $ctx->add($buf); 395 my $pct = ($len / $max) * 100.0; 396 if ($pct >= $opct + 1) { 397 printf(STDERR "%.2f%% $len/$max\n", $pct); 398 $opct = $pct; 399 } 400 } 401 402 print STDERR "LEN: ", $len, "\n"; 403 print STDERR "MD5: ", $ctx->hexdigest, "\n"; 404} 405 406######################################################################## 407# Server funcs 408######################################################################## 409 410sub http_server { 411 my $self = shift; 412 my %header = %{$self->{header} || { Server => "Perl/".$^V }}; 413 my $cookie = $self->{cookie} || ""; 414 415 my($method, $url, $vers); 416 do { 417 my $len; 418 { 419 local $/ = "\r\n"; 420 local $_ = <STDIN>; 421 return unless defined $_; 422 chomp; 423 print STDERR "<<< $_\n"; 424 ($method, $url, $vers) = m{^(\w+) (.*) HTTP/(1\.[01])$} 425 or die ref($self), " http request not ok"; 426 $method =~ /^(GET|PUT)$/ 427 or die ref($self), " unknown method: $method"; 428 ($len, my @chunks) = $url =~ /(\d+)/g; 429 $len = [ $len, @chunks ] if @chunks; 430 while (<STDIN>) { 431 chomp; 432 print STDERR "<<< $_\n"; 433 last if /^$/; 434 if ($method eq "PUT" && 435 /^Content-Length: (.*)/) { 436 $1 == $len or die ref($self), 437 " bad content length $1"; 438 } 439 $cookie ||= $1 if /^Cookie: (.*)/; 440 } 441 } 442 if ($method eq "PUT" ) { 443 if (ref($len) eq 'ARRAY') { 444 read_chunked($self); 445 } else { 446 read_char($self, $len); 447 } 448 } 449 450 my @response = ("HTTP/$vers 200 OK"); 451 $len = defined($len) ? $len : scalar(split /|/,$url); 452 if ($vers eq "1.1" && $method eq "GET") { 453 if (ref($len) eq 'ARRAY') { 454 push @response, "Transfer-Encoding: chunked"; 455 } else { 456 push @response, "Content-Length: $len"; 457 } 458 } 459 foreach my $key (sort keys %header) { 460 my $val = $header{$key}; 461 if (ref($val) eq 'ARRAY') { 462 push @response, "$key: $_" 463 foreach @{$val}; 464 } else { 465 push @response, "$key: $val"; 466 } 467 } 468 push @response, "Set-Cookie: $cookie" if $cookie; 469 push @response, ""; 470 471 print STDERR map { ">>> $_\n" } @response; 472 print map { "$_\r\n" } @response; 473 474 if ($method eq "GET") { 475 if (ref($len) eq 'ARRAY') { 476 if ($vers eq "1.1") { 477 write_chunked($self, @$len); 478 } else { 479 write_char($self, $_) foreach (@$len); 480 } 481 } else { 482 write_char($self, $len); 483 } 484 } 485 IO::Handle::flush(\*STDOUT); 486 } while ($vers eq "1.1"); 487 $self->{redo}-- if $self->{redo}; 488} 489 490sub write_chunked { 491 my $self = shift; 492 my @chunks = @_; 493 494 foreach my $len (@chunks) { 495 printf STDERR ">>> %x\n", $len; 496 printf "%x\r\n", $len; 497 write_char($self, $len); 498 printf STDERR ">>> \n"; 499 print "\r\n"; 500 } 501 my @trailer = ("0", "X-Chunk-Trailer: @chunks", ""); 502 print STDERR map { ">>> $_\n" } @trailer; 503 print map { "$_\r\n" } @trailer; 504} 505 506######################################################################## 507# Script funcs 508######################################################################## 509 510sub check_logs { 511 my ($c, $r, $s, %args) = @_; 512 513 return if $args{nocheck}; 514 515 check_len($c, $r, $s, %args); 516 check_md5($c, $r, $s, %args); 517 check_loggrep($c, $r, $s, %args); 518 $r->loggrep("lost child") 519 and die "relayd lost child"; 520} 521 522sub check_len { 523 my ($c, $r, $s, %args) = @_; 524 525 $args{len} ||= 251 unless $args{lengths}; 526 527 my (@clen, @slen); 528 @clen = $c->loggrep(qr/^LEN: /) or die "no client len" 529 unless $args{client}{nocheck}; 530 @slen = $s->loggrep(qr/^LEN: /) or die "no server len" 531 unless $args{server}{nocheck}; 532 !@clen || !@slen || @clen ~~ @slen 533 or die "client: @clen", "server: @slen", "len mismatch"; 534 !defined($args{len}) || !$clen[0] || $clen[0] eq "LEN: $args{len}\n" 535 or die "client: $clen[0]", "len $args{len} expected"; 536 !defined($args{len}) || !$slen[0] || $slen[0] eq "LEN: $args{len}\n" 537 or die "server: $slen[0]", "len $args{len} expected"; 538 my @lengths = map { ref eq 'ARRAY' ? @$_ : $_ } 539 @{$args{lengths} || []}; 540 foreach my $len (@lengths) { 541 unless ($args{client}{nocheck}) { 542 my $clen = shift @clen; 543 $clen eq "LEN: $len\n" 544 or die "client: $clen", "len $len expected"; 545 } 546 unless ($args{server}{nocheck}) { 547 my $slen = shift @slen; 548 $slen eq "LEN: $len\n" 549 or die "server: $slen", "len $len expected"; 550 } 551 } 552} 553 554sub check_md5 { 555 my ($c, $r, $s, %args) = @_; 556 557 my (@cmd5, @smd5); 558 @cmd5 = $c->loggrep(qr/^MD5: /) unless $args{client}{nocheck}; 559 @smd5 = $s->loggrep(qr/^MD5: /) unless $args{server}{nocheck}; 560 !@cmd5 || !@smd5 || $cmd5[0] eq $smd5[0] 561 or die "client: $cmd5[0]", "server: $smd5[0]", "md5 mismatch"; 562 563 my @md5 = ref($args{md5}) eq 'ARRAY' ? @{$args{md5}} : $args{md5} || () 564 or return; 565 foreach my $md5 (@md5) { 566 unless ($args{client}{nocheck}) { 567 my $cmd5 = shift @cmd5 568 or die "too few md5 in client log"; 569 $cmd5 =~ /^MD5: ($md5)$/ 570 or die "client: $cmd5", "md5 $md5 expected"; 571 } 572 unless ($args{server}{nocheck}) { 573 my $smd5 = shift @smd5 574 or die "too few md5 in server log"; 575 $smd5 =~ /^MD5: ($md5)$/ 576 or die "server: $smd5", "md5 $md5 expected"; 577 } 578 } 579 @cmd5 && ref($args{md5}) eq 'ARRAY' 580 and die "too many md5 in client log"; 581 @smd5 && ref($args{md5}) eq 'ARRAY' 582 and die "too many md5 in server log"; 583} 584 585sub check_loggrep { 586 my ($c, $r, $s, %args) = @_; 587 588 my %name2proc = (client => $c, relayd => $r, server => $s); 589 foreach my $name (qw(client relayd server)) { 590 my $p = $name2proc{$name} or next; 591 my $pattern = $args{$name}{loggrep} or next; 592 $pattern = [ $pattern ] unless ref($pattern) eq 'ARRAY'; 593 foreach my $pat (@$pattern) { 594 if (ref($pat) eq 'HASH') { 595 while (my($re, $num) = each %$pat) { 596 my @matches = $p->loggrep($re); 597 @matches == $num 598 or die "$name matches '@matches': ", 599 "'$re' => $num"; 600 } 601 } else { 602 $p->loggrep($pat) 603 or die "$name log missing pattern: '$pat'"; 604 } 605 } 606 } 607} 608 6091; 610