xref: /openbsd/regress/usr.sbin/relayd/funcs.pl (revision 09467b48)
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