Commit Diff


commit - b400ca1d900bb955d47442147287a7f0112ed698
commit + caed4502eeca392fd0b05d89d49209b1b751cdfd
blob - /dev/null
blob + 7aaa0401e2f36632a49315053e1bf9fdc219fad8 (mode 644)
--- /dev/null
+++ regress/Client.pm
@@ -0,0 +1,110 @@
+#	$OpenBSD: Client.pm,v 1.15 2024/10/28 19:57:02 tb Exp $
+
+# Copyright (c) 2010-2021 Alexander Bluhm <bluhm@openbsd.org>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+use strict;
+use warnings;
+
+package Client;
+use parent 'Proc';
+use Carp;
+use Socket qw(:DEFAULT IPPROTO_TCP TCP_NODELAY);
+use Socket6;
+use IO::Socket::IP;
+use IO::Socket::SSL;
+
+sub new {
+	my $class = shift;
+	my %args = @_;
+	$args{logfile} ||= "client.log";
+	$args{up} ||= "Connected";
+	$args{timefile} //= "time.log";
+	my $self = Proc::new($class, %args);
+	$self->{connectdomain}
+	    or croak "$class connect domain not given";
+	$self->{connectaddr}
+	    or croak "$class connect addr not given";
+	$self->{connectport}
+	    or croak "$class connect port not given";
+	return $self;
+}
+
+sub child {
+	my $self = shift;
+
+	# in case we redo the connect, shutdown the old one
+	shutdown(\*STDOUT, SHUT_WR);
+	delete $self->{cs};
+
+	$SSL_ERROR = "";
+	my $iosocket = $self->{ssl} ? "IO::Socket::SSL" : "IO::Socket::IP";
+	my $cs = $iosocket->new(
+	    Proto		=> "tcp",
+	    Domain		=> $self->{connectdomain},
+	    # IO::Socket::IP calls the domain family
+	    Family		=> $self->{connectdomain},
+	    PeerAddr		=> $self->{connectaddr},
+	    PeerPort		=> $self->{connectport},
+	    SSL_verify_mode	=> SSL_VERIFY_NONE,
+	    SSL_use_cert	=> $self->{offertlscert} ? 1 : 0,
+	    SSL_cert_file	=> $self->{offertlscert} ?
+	                               "client.crt" : "",
+	    SSL_key_file	=> $self->{offertlscert} ?
+	                               "client.key" : "",
+	) or die ref($self), " $iosocket socket connect failed: $!,$SSL_ERROR";
+	if ($self->{sndbuf}) {
+		setsockopt($cs, SOL_SOCKET, SO_SNDBUF,
+		    pack('i', $self->{sndbuf}))
+		    or die ref($self), " set SO_SNDBUF failed: $!";
+	}
+	if ($self->{rcvbuf}) {
+		setsockopt($cs, SOL_SOCKET, SO_RCVBUF,
+		    pack('i', $self->{rcvbuf}))
+		    or die ref($self), " set SO_SNDBUF failed: $!";
+	}
+	if ($self->{sndtimeo}) {
+		setsockopt($cs, SOL_SOCKET, SO_SNDTIMEO,
+		    pack('l!l!', $self->{sndtimeo}, 0))
+		    or die ref($self), " set SO_SNDTIMEO failed: $!";
+	}
+	if ($self->{rcvtimeo}) {
+		setsockopt($cs, SOL_SOCKET, SO_RCVTIMEO,
+		    pack('l!l!', $self->{rcvtimeo}, 0))
+		    or die ref($self), " set SO_RCVTIMEO failed: $!";
+	}
+	setsockopt($cs, IPPROTO_TCP, TCP_NODELAY, pack('i', 1))
+	    or die ref($self), " set TCP_NODELAY failed: $!";
+
+	print STDERR "connect sock: ",$cs->sockhost()," ",$cs->sockport(),"\n";
+	print STDERR "connect peer: ",$cs->peerhost()," ",$cs->peerport(),"\n";
+	if ($self->{ssl}) {
+		print STDERR "ssl version: ",$cs->get_sslversion(),"\n";
+		print STDERR "ssl cipher: ",$cs->get_cipher(),"\n";
+		print STDERR "ssl peer certificate:\n",
+		    $cs->dump_peer_certificate();
+
+		if ($self->{offertlscert}) {
+			print STDERR "ssl client certificate:\n";
+			print STDERR "Subject Name: ",
+				"${\$cs->sock_certificate('subject')}\n";
+			print STDERR "Issuer  Name: ",
+				"${\$cs->sock_certificate('issuer')}\n";
+		}
+	}
+
+	*STDIN = *STDOUT = $self->{cs} = $cs;
+}
+
+1;
blob - /dev/null
blob + 78d7a52f6fbc00f7a9c4ef1e02adbeeaf874dd72 (mode 644)
--- /dev/null
+++ regress/LICENSE
@@ -0,0 +1,14 @@
+# Copyright (c) 2010-2021 Alexander Bluhm <bluhm@openbsd.org>
+# Copyright (c) 2014 Reyk Floeter <reyk@openbsd.org>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
blob - /dev/null
blob + a199e9ab731c5e3f621d2a52a0993894c4ba453f (mode 644)
--- /dev/null
+++ regress/Makefile
@@ -0,0 +1,156 @@
+#	$OpenBSD: Makefile,v 1.23 2024/12/27 10:53:46 bluhm Exp $
+
+# The following ports must be installed for the regression tests:
+# p5-Socket6		Perl defines relating to AF_INET6 sockets
+# p5-IO-Socket-SSL	perl interface to SSL sockets
+#
+# Check wether all required perl packages are installed.  If some
+# are missing print a warning and skip the tests, but do not fail.
+
+PERL_REQUIRE !=	perl -Mstrict -Mwarnings -e ' \
+    eval { require Socket6 } or print $@; \
+    eval { require IO::Socket::SSL } or print $@; \
+'
+.if ! empty (PERL_REQUIRE)
+regress:
+	@echo "${PERL_REQUIRE}"
+	@echo 'run "pkg_add p5-Socket6 p5-IO-Socket-SSL"'
+	@echo SKIPPED
+.endif
+
+REGRESS_SETUP_ONCE +=	setup
+setup:
+.if empty (REMOTE_SSH)
+	${SUDO} true
+.else
+	ssh -t ${REMOTE_SSH} ${SUDO} true
+.endif
+
+# Fill out these variables if you want to test relayd with
+# the relayd process running on a remote machine.  You have to specify
+# a local and remote ip address for the tcp connections.  To control
+# the remote machine you need a hostname for ssh to log in.  All the
+# test files must be in the same directory local and remote.
+
+LOCAL_ADDR ?=
+REMOTE_ADDR ?=
+REMOTE_SSH ?=
+
+# Automatically generate regress targets from test cases in directory.
+# EC tests are handled separately to avoid overwriting the RSA cert.
+
+ARGS_EC !=		cd ${.CURDIR} && ls args-*-ec.pl
+ARGS !=			cd ${.CURDIR} && ls args-*.pl | grep -v -- -ec\.pl
+CLEANFILES +=		*.log relayd.conf ktrace.out stamp-*
+CLEANFILES +=		*.pem *.req *.crt *.key *.srl
+
+# Set variables so that make runs with and without obj directory.
+# Only do that if necessary to keep visible output short.
+
+.if ${.CURDIR} == ${.OBJDIR}
+PERLINC =	-I.
+PERLPATH =
+.else
+PERLINC =	-I${.CURDIR}
+PERLPATH =	${.CURDIR}/
+.endif
+
+# The arg tests take a perl hash with arguments controlling the
+# test parameters.  Generally they consist of client, relayd, server.
+
+.for a in ${ARGS}
+REGRESS_TARGETS +=	run-$a
+run-$a: $a
+.if empty (REMOTE_SSH)
+	time SUDO="${SUDO}" KTRACE=${KTRACE} RELAYD=${RELAYD} perl ${PERLINC} ${PERLPATH}relayd.pl copy ${PERLPATH}$a
+	time SUDO="${SUDO}" KTRACE=${KTRACE} RELAYD=${RELAYD} perl ${PERLINC} ${PERLPATH}relayd.pl splice ${PERLPATH}$a
+.else
+	time SUDO="${SUDO}" KTRACE=${KTRACE} RELAYD=${RELAYD} perl ${PERLINC} ${PERLPATH}remote.pl copy ${LOCAL_ADDR} ${REMOTE_ADDR} ${REMOTE_SSH} ${PERLPATH}$a
+	time SUDO="${SUDO}" KTRACE=${KTRACE} RELAYD=${RELAYD} perl ${PERLINC} ${PERLPATH}remote.pl splice ${LOCAL_ADDR} ${REMOTE_ADDR} ${REMOTE_SSH} ${PERLPATH}$a
+.endif
+.endfor
+
+# EC tests
+.for a in ${ARGS_EC}
+REGRESS_TARGETS +=	run-$a
+run-$a: $a server.crt client.crt 127.0.0.1-ec.crt
+.if empty (REMOTE_SSH)
+	${SUDO} cp 127.0.0.1-ec.crt /etc/ssl/127.0.0.1.crt
+	${SUDO} cp 127.0.0.1-ec.key /etc/ssl/private/127.0.0.1.key
+	time SUDO="${SUDO}" KTRACE=${KTRACE} RELAYD=${RELAYD} perl ${PERLINC} ${PERLPATH}relayd.pl copy ${PERLPATH}$a
+	time SUDO="${SUDO}" KTRACE=${KTRACE} RELAYD=${RELAYD} perl ${PERLINC} ${PERLPATH}relayd.pl splice ${PERLPATH}$a
+.else
+	scp ${REMOTE_ADDR}-ec.crt root@${REMOTE_SSH}:/etc/ssl/${REMOTE_ADDR}.crt
+	scp ${REMOTE_ADDR}-ec.key root@${REMOTE_SSH}:/etc/ssl/private/${REMOTE_ADDR}.key
+	time SUDO="${SUDO}" KTRACE=${KTRACE} RELAYD=${RELAYD} perl ${PERLINC} ${PERLPATH}remote.pl copy ${LOCAL_ADDR} ${REMOTE_ADDR} ${REMOTE_SSH} ${PERLPATH}$a
+	time SUDO="${SUDO}" KTRACE=${KTRACE} RELAYD=${RELAYD} perl ${PERLINC} ${PERLPATH}remote.pl splice ${LOCAL_ADDR} ${REMOTE_ADDR} ${REMOTE_SSH} ${PERLPATH}$a
+.endif
+.endfor
+
+# create certificates for TLS
+
+.for ip in ${REMOTE_ADDR} 127.0.0.1
+${ip}.crt: ca.crt client-ca.crt
+	openssl req -batch -new \
+	    -subj /L=OpenBSD/O=relayd-regress/OU=relayd/CN=${ip}/ \
+	    -nodes -newkey rsa -keyout ${ip}.key -x509 \
+	    -out $@
+.if empty (REMOTE_SSH)
+	${SUDO} cp 127.0.0.1.crt /etc/ssl/
+	${SUDO} cp 127.0.0.1.key /etc/ssl/private/
+.else
+	scp ${REMOTE_ADDR}.crt root@${REMOTE_SSH}:/etc/ssl/
+	scp ${REMOTE_ADDR}.key root@${REMOTE_SSH}:/etc/ssl/private/
+	scp ca.crt ca.key ${REMOTE_SSH}:
+	scp client-ca.crt client-ca.key ${REMOTE_SSH}:
+.endif
+
+${ip}-ec.crt:
+	openssl ecparam -name secp384r1 -genkey -noout \
+	    -out ${ip}-ec.key
+	openssl req -batch -new -x509 \
+	    -subj /L=OpenBSD/O=relayd-regress/OU=relayd/CN=${ip}/ \
+	    -key ${ip}-ec.key \
+	    -out $@
+.endfor
+
+ca.crt client-ca.crt:
+	openssl req -batch -new \
+	    -subj /L=OpenBSD/O=relayd-regress/OU=${@:R}/CN=root/ \
+	    -nodes -newkey rsa -keyout ${@:R}.key -x509 \
+	    -out $@
+
+server.req client.req:
+	openssl req -batch -new \
+	    -subj /L=OpenBSD/O=relayd-regress/OU=${@:R}/CN=localhost/ \
+	    -nodes -newkey rsa -keyout ${@:R}.key \
+	    -out $@
+
+server.crt: ca.crt server.req
+	openssl x509 -CAcreateserial -CAkey ca.key -CA ca.crt \
+	    -req -in server.req -out server.crt
+
+client.crt: client-ca.crt client.req
+	openssl x509 -CAcreateserial -CAkey client-ca.key -CA client-ca.crt \
+	    -req -in client.req -out client.crt
+
+${REGRESS_TARGETS:M*ssl*} ${REGRESS_TARGETS:M*https*}: server.crt client.crt
+.if empty (REMOTE_SSH)
+${REGRESS_TARGETS:M*ssl*} ${REGRESS_TARGETS:M*https*}: 127.0.0.1.crt
+.else
+${REGRESS_TARGETS:M*ssl*} ${REGRESS_TARGETS:M*https*}: ${REMOTE_ADDR}.crt
+.endif
+
+# make perl syntax check for all args files
+
+.PHONY: syntax
+
+syntax: stamp-syntax
+
+stamp-syntax: ${ARGS} ${ARGS_EC}
+.for a in ${ARGS} ${ARGS_EC}
+	@perl -c ${PERLPATH}$a
+.endfor
+	@date >$@
+
+.include <bsd.regress.mk>
blob - /dev/null
blob + 7ca27a57a6d3f9b505f8b72907b4f3b144578565 (mode 644)
--- /dev/null
+++ regress/Proc.pm
@@ -0,0 +1,205 @@
+#	$OpenBSD: Proc.pm,v 1.13 2021/10/12 05:42:39 anton Exp $
+
+# Copyright (c) 2010-2016 Alexander Bluhm <bluhm@openbsd.org>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+use strict;
+use warnings;
+
+package Proc;
+use Carp;
+use Errno;
+use File::Basename;
+use IO::File;
+use POSIX;
+use Time::HiRes qw(time alarm sleep);
+
+my %CHILDREN;
+
+sub kill_children {
+	my @pids = @_ ? @_ : keys %CHILDREN
+	    or return;
+	my @perms;
+	foreach my $pid (@pids) {
+		if (kill(TERM => $pid) != 1 and $!{EPERM}) {
+			push @perms, $pid;
+		}
+	}
+	if (my @sudo = split(' ', $ENV{SUDO}) and @perms) {
+		local $?;  # do not modify during END block
+		my @cmd = (@sudo, '/bin/kill', '-TERM', @perms);
+		system(@cmd);
+	}
+	delete @CHILDREN{@pids};
+}
+
+BEGIN {
+	$SIG{TERM} = $SIG{INT} = sub {
+		my $sig = shift;
+		kill_children();
+		$SIG{TERM} = $SIG{INT} = 'DEFAULT';
+		POSIX::raise($sig);
+	};
+}
+
+END {
+	kill_children();
+	$SIG{TERM} = $SIG{INT} = 'DEFAULT';
+}
+
+sub new {
+	my $class = shift;
+	my $self = { @_ };
+	$self->{down} ||= "Shutdown";
+	$self->{func} && ref($self->{func}) eq 'CODE'
+	    or croak "$class func not given";
+	$self->{logfile}
+	    or croak "$class log file not given";
+	open(my $fh, '>', $self->{logfile})
+	    or die "$class log file $self->{logfile} create failed: $!";
+	$fh->autoflush;
+	$self->{log} = $fh;
+	$self->{ppid} = $$;
+	return bless $self, $class;
+}
+
+sub run {
+	my $self = shift;
+
+	pipe(my $reader, my $writer)
+	    or die ref($self), " pipe to child failed: $!";
+	defined(my $pid = fork())
+	    or die ref($self), " fork child failed: $!";
+	if ($pid) {
+		$CHILDREN{$pid} = 1;
+		$self->{pid} = $pid;
+		close($reader);
+		$self->{pipe} = $writer;
+		return $self;
+	}
+	%CHILDREN = ();
+	$SIG{TERM} = $SIG{INT} = 'DEFAULT';
+	$SIG{__DIE__} = sub {
+		die @_ if $^S;
+		warn @_;
+		IO::Handle::flush(\*STDERR);
+		POSIX::_exit(255);
+	};
+	open(STDERR, '>&', $self->{log})
+	    or die ref($self), " dup STDERR failed: $!";
+	close($writer);
+	open(STDIN, '<&', $reader)
+	    or die ref($self), " dup STDIN failed: $!";
+	close($reader);
+
+	do {
+		$self->child();
+		print STDERR $self->{up}, "\n";
+		$self->{begin} = time();
+		$self->{func}->($self);
+	} while ($self->{redo});
+	$self->{end} = time();
+	print STDERR "Shutdown", "\n";
+	if ($self->{timefile}) {
+		open(my $fh, '>>', $self->{timefile})
+		    or die ref($self), " open $self->{timefile} failed: $!";
+		printf $fh "time='%s' duration='%.10g' ".
+		    "forward='%s' test='%s'\n",
+		    scalar(localtime(time())), $self->{end} - $self->{begin},
+		    $self->{forward}, basename($self->{testfile});
+	}
+
+	IO::Handle::flush(\*STDOUT);
+	IO::Handle::flush(\*STDERR);
+	POSIX::_exit(0);
+}
+
+sub wait {
+	my $self = shift;
+	my $flags = shift;
+
+	# if we a not the parent process, assume the child is still running
+	return 0 unless $self->{ppid} == $$;
+
+	my $pid = $self->{pid}
+	    or croak ref($self), " no child pid";
+	my $kid = waitpid($pid, $flags);
+	if ($kid > 0) {
+		my $status = $?;
+		my $code;
+		$code = "exit: ".   WEXITSTATUS($?) if WIFEXITED($?);
+		$code = "signal: ". WTERMSIG($?)    if WIFSIGNALED($?);
+		$code = "stop: ".   WSTOPSIG($?)    if WIFSTOPPED($?);
+		delete $CHILDREN{$pid} if WIFEXITED($?) || WIFSIGNALED($?);
+		return wantarray ? ($kid, $status, $code) : $kid;
+	}
+	return $kid;
+}
+
+sub loggrep {
+	my $self = shift;
+	my($regex, $timeout) = @_;
+
+	my $end;
+	$end = time() + $timeout if $timeout;
+
+	do {
+		my($kid, $status, $code) = $self->wait(WNOHANG);
+		if ($kid > 0 && $status != 0 && !$self->{dryrun}) {
+			# child terminated with failure
+			die ref($self), " child status: $status $code";
+		}
+		open(my $fh, '<', $self->{logfile})
+		    or die ref($self), " log file open failed: $!";
+		my @match = grep { /$regex/ } <$fh>;
+		return wantarray ? @match : $match[0] if @match;
+		close($fh);
+		# pattern not found
+		if ($kid == 0) {
+			# child still running, wait for log data
+			sleep .1;
+		} else {
+			# child terminated, no new log data possible
+			return;
+		}
+	} while ($timeout and time() < $end);
+
+	return;
+}
+
+sub up {
+	my $self = shift;
+	my $timeout = shift || 10;
+	$self->loggrep(qr/$self->{up}/, $timeout)
+	    or croak ref($self), " no '$self->{up}' in $self->{logfile} ".
+		"after $timeout seconds";
+	return $self;
+}
+
+sub down {
+	my $self = shift;
+	my $timeout = shift || 30;
+	$self->loggrep(qr/$self->{down}/, $timeout)
+	    or croak ref($self), " no '$self->{down}' in $self->{logfile} ".
+		"after $timeout seconds";
+	return $self;
+}
+
+sub kill_child {
+	my $self = shift;
+	kill_children($self->{pid});
+	return $self;
+}
+
+1;
blob - /dev/null
blob + 0af36294e788e90a67daec4d06968b7a2c87ca92 (mode 644)
--- /dev/null
+++ regress/README
@@ -0,0 +1,25 @@
+Run relayd regressions tests.  The framework runs a client, and a
+server, and a relayd.  Currently the tcp and http forwarding code
+path is covered.  Each test creates a special relayd.conf and starts
+those three processes.  All processes write log files that are
+checked for certain messages.  The test arguments are kept in the
+args-*.pl files.  To find socket splicing bugs, each test is run
+in both copy and splice mode.
+
+SUDO=doas
+As relayd needs root privileges either run the tests as root or set
+this variable and run make as a regular user.  Only the code that
+requires it, is run as root.
+
+KTRACE=ktrace
+Set this variable if you want a ktrace output from relayd.  Note that
+ktrace is invoked after sudo as sudo would disable it.
+
+RELAYD=/usr/src/usr.sbin/relayd/obj/relayd
+Start an alternative relayd program that is not in the path.
+
+LOCAL_ADDR, REMOTE_ADDR, REMOTE_SSH
+Set these to run the relayd on a remote machine.  As the client and
+server run locally, network timing may influence the test results.
+
+Changes here have to be discussed with bluhm@, reyk@ or benno@.
blob - /dev/null
blob + 2a6aa926d1654d8e33cfc6112895d10e26aaabb5 (mode 644)
--- /dev/null
+++ regress/Relayd.pm
@@ -0,0 +1,129 @@
+#	$OpenBSD: Relayd.pm,v 1.20 2024/10/28 19:57:02 tb Exp $
+
+# Copyright (c) 2010-2015 Alexander Bluhm <bluhm@openbsd.org>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+use strict;
+use warnings;
+
+package Relayd;
+use parent 'Proc';
+use Carp;
+use Cwd;
+use Sys::Hostname;
+use File::Basename;
+
+sub new {
+	my $class = shift;
+	my %args = @_;
+	$args{logfile} ||= "relayd.log";
+	$args{up} ||= $args{dryrun} || "relay_launch: ";
+	$args{down} ||= $args{dryrun} ? "relayd.conf:" : "parent terminating";
+	$args{func} = sub { Carp::confess "$class func may not be called" };
+	$args{conffile} ||= "relayd.conf";
+	$args{forward}
+	    or croak "$class forward not given";
+	my $self = Proc::new($class, %args);
+	ref($self->{protocol}) eq 'ARRAY'
+	    or $self->{protocol} = [ split("\n", $self->{protocol} || "") ];
+	ref($self->{relay}) eq 'ARRAY'
+	    or $self->{relay} = [ split("\n", $self->{relay} || "") ];
+	$self->{listenaddr}
+	    or croak "$class listen addr not given";
+	$self->{listenport}
+	    or croak "$class listen port not given";
+	$self->{connectaddr}
+	    or croak "$class connect addr not given";
+	$self->{connectport}
+	    or croak "$class connect port not given";
+
+	my $test = basename($self->{testfile} || "");
+	# tls does not allow a too long session id, so truncate it
+	substr($test, 25, length($test) - 25, "") if length($test) > 25;
+	open(my $fh, '>', $self->{conffile})
+	    or die ref($self), " conf file $self->{conffile} create failed: $!";
+	print $fh "log state changes\n";
+	print $fh "log host checks\n";
+	print $fh "log connection\n";
+	print $fh "prefork 1\n";  # only crashes of first child are observed
+	print $fh "table <table-$test> { $self->{connectaddr} }\n"
+	    if defined($self->{table});
+
+	# substitute variables in config file
+	my $curdir = dirname($0) || ".";
+	my $objdir = getcwd();
+	my $hostname = hostname();
+	(my $host = $hostname) =~ s/\..*//;
+	my $connectport = $self->{connectport};
+	my $connectaddr = $self->{connectaddr};
+	my $listenaddr = $self->{listenaddr};
+	my $listenport = $self->{listenport};
+
+	my @protocol = @{$self->{protocol}};
+	my $proto = shift @protocol;
+	$proto = defined($proto) ? "$proto " : "";
+	unshift @protocol,
+	    $self->{forward} eq "splice" ? "tcp splice" :
+	    $self->{forward} eq "copy"   ? "tcp no splice" :
+	    die ref($self), " invalid forward $self->{forward}"
+	    unless grep { /splice/ } @protocol;
+	push @protocol, "tcp nodelay";
+	print $fh "${proto}protocol proto-$test {";
+	if ($self->{inspectssl}) {
+		$self->{listenssl} = $self->{forwardssl} = 1;
+		print $fh "\n\ttls ca cert ca.crt";
+		print $fh "\n\ttls ca key ca.key password ''";
+	}
+	if ($self->{verifyclient}) {
+		print $fh "\n\ttls client ca client-ca.crt";
+	}
+	# substitute variables in config file
+	foreach (@protocol) {
+		s/(\$[a-z]+)/$1/eeg;
+	}
+	print $fh  map { "\n\t$_" } @protocol;
+	print $fh  "\n}\n";
+
+	my @relay = @{$self->{relay}};
+	print $fh  "relay relay-$test {";
+	print $fh  "\n\tprotocol proto-$test"
+	    unless grep { /^protocol / } @relay;
+	my $tls = $self->{listenssl} ? " tls" : "";
+	print $fh  "\n\tlisten on $self->{listenaddr} ".
+	    "port $self->{listenport}$tls" unless grep { /^listen / } @relay;
+	my $withtls = $self->{forwardssl} ? " with tls" : "";
+	print $fh  "\n\tforward$withtls to $self->{connectaddr} ".
+	    "port $self->{connectport}" unless grep { /^forward / } @relay;
+	# substitute variables in config file
+	foreach (@relay) {
+		s/(\$[a-z]+)/$1/eeg;
+	}
+	print $fh  map { "\n\t$_" } @relay;
+	print $fh  "\n}\n";
+
+	return $self;
+}
+
+sub child {
+	my $self = shift;
+	my @sudo = $ENV{SUDO} ? split(' ', $ENV{SUDO}) : ();
+	my @ktrace = $ENV{KTRACE} ? ($ENV{KTRACE}, "-i") : ();
+	my $relayd = $ENV{RELAYD} ? $ENV{RELAYD} : "relayd";
+	my @cmd = (@sudo, @ktrace, $relayd, "-dvv", "-f", $self->{conffile});
+	print STDERR "execute: @cmd\n";
+	exec @cmd;
+	die ref($self), " exec '@cmd' failed: $!";
+}
+
+1;
blob - /dev/null
blob + 552bb82c7bf8c4576ba36e85ae17128fbebd2460 (mode 644)
--- /dev/null
+++ regress/Remote.pm
@@ -0,0 +1,109 @@
+#	$OpenBSD: Remote.pm,v 1.7 2016/05/03 19:13:04 bluhm Exp $
+
+# Copyright (c) 2010-2014 Alexander Bluhm <bluhm@openbsd.org>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+use strict;
+use warnings;
+
+package Remote;
+use parent 'Proc';
+use Carp;
+use Cwd;
+use File::Basename;
+
+my %PIPES;
+
+sub close_pipes {
+	my @pipes = @_ ? @_ : keys %PIPES
+	    or return;
+	foreach (@pipes) {
+		# file descriptor cannot be a hash key, so use hash value
+		my $fh = $PIPES{$_};
+		# also print new line as close is delayed by forked processes
+		print $fh "close\n";
+		close($fh);
+	}
+	sleep 1;  # give other end a chance to finish process
+	delete @PIPES{@pipes};
+}
+
+END {
+	close_pipes();
+}
+
+sub new {
+	my $class = shift;
+	my %args = @_;
+	$args{logfile} ||= "remote.log";
+	$args{up} ||= "listen sock: ";
+	$args{down} ||= $args{dryrun} ? "relayd.conf" : "parent terminating";
+	$args{func} = sub { Carp::confess "$class func may not be called" };
+	$args{remotessh}
+	    or croak "$class remote ssh host not given";
+	$args{forward}
+	    or croak "$class forward not given";
+	my $self = Proc::new($class, %args);
+	$self->{listenaddr}
+	    or croak "$class listen addr not given";
+	$self->{connectaddr}
+	    or croak "$class connect addr not given";
+	$self->{connectport}
+	    or croak "$class connect port not given";
+	return $self;
+}
+
+sub run {
+	my $self = Proc::run(shift, @_);
+	$PIPES{$self->{pipe}} = $self->{pipe};
+	return $self;
+}
+
+sub up {
+	my $self = Proc::up(shift, @_);
+	my $lsock = $self->loggrep(qr/^listen sock: /)
+	    or croak ref($self), " no 'listen sock: ' in $self->{logfile}";
+	my($addr, $port) = $lsock =~ /: (\S+) (\S+)$/
+	    or croak ref($self), " no listen addr and port in $self->{logfile}";
+	$self->{listenaddr} = $addr;
+	$self->{listenport} = $port;
+	return $self;
+}
+
+sub child {
+	my $self = shift;
+
+	my @opts = $ENV{SSH_OPTIONS} ? split(' ', $ENV{SSH_OPTIONS}) : ();
+	my @sudo = $ENV{SUDO} ? "SUDO=$ENV{SUDO}" : ();
+	my @ktrace = $ENV{KTRACE} ? "KTRACE=$ENV{KTRACE}" : ();
+	my @relayd = $ENV{RELAYD} ? "RELAYD=$ENV{RELAYD}" : ();
+	my $dir = dirname($0);
+	$dir = getcwd() if ! $dir || $dir eq ".";
+	my @cmd = ("ssh", @opts, $self->{remotessh},
+	    @sudo, @ktrace, @relayd, "perl",
+	    "-I", $dir, "$dir/".basename($0), $self->{forward},
+	    $self->{listenaddr}, $self->{connectaddr}, $self->{connectport},
+	    ($self->{testfile} ? "$dir/".basename($self->{testfile}) : ()));
+	print STDERR "execute: @cmd\n";
+	exec @cmd;
+	die ref($self), " exec '@cmd' failed: $!";
+}
+
+sub close_child {
+	my $self = shift;
+	close_pipes(delete $self->{pipe});
+	return $self;
+}
+
+1;
blob - /dev/null
blob + 8f1d4f3ce88ae30d869c020ecb717db8309475d0 (mode 644)
--- /dev/null
+++ regress/Server.pm
@@ -0,0 +1,108 @@
+#	$OpenBSD: Server.pm,v 1.15 2021/12/22 11:50:28 bluhm Exp $
+
+# Copyright (c) 2010-2021 Alexander Bluhm <bluhm@openbsd.org>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+use strict;
+use warnings;
+
+package Server;
+use parent 'Proc';
+use Carp;
+use Config;
+use Socket qw(:DEFAULT IPPROTO_TCP TCP_NODELAY);
+use Socket6;
+use IO::Socket::IP;
+use IO::Socket::SSL;
+
+sub new {
+	my $class = shift;
+	my %args = @_;
+	$args{logfile} ||= "server.log";
+	$args{up} ||= "Accepted";
+	my $self = Proc::new($class, %args);
+	$self->{listendomain}
+	    or croak "$class listen domain not given";
+	$SSL_ERROR = "";
+	my $iosocket = $self->{ssl} ? "IO::Socket::SSL" : "IO::Socket::IP";
+	my $ls = $iosocket->new(
+	    Proto		=> "tcp",
+	    ReuseAddr		=> 1,
+	    Domain		=> $self->{listendomain},
+	    # IO::Socket::IP calls the domain family
+	    Family		=> $self->{listendomain},
+	    $self->{listenaddr} ? (LocalAddr => $self->{listenaddr}) : (),
+	    $self->{listenport} ? (LocalPort => $self->{listenport}) : (),
+	    SSL_server          => 1,
+	    SSL_key_file	=> "server.key",
+	    SSL_cert_file	=> "server.crt",
+	    SSL_verify_mode	=> SSL_VERIFY_NONE,
+	) or die ref($self), " $iosocket socket failed: $!,$SSL_ERROR";
+	if ($self->{sndbuf}) {
+		setsockopt($ls, SOL_SOCKET, SO_SNDBUF,
+		    pack('i', $self->{sndbuf}))
+		    or die ref($self), " set SO_SNDBUF failed: $!";
+	}
+	if ($self->{rcvbuf}) {
+		setsockopt($ls, SOL_SOCKET, SO_RCVBUF,
+		    pack('i', $self->{rcvbuf}))
+		    or die ref($self), " set SO_RCVBUF failed: $!";
+	}
+	my $packstr = $Config{longsize} == 8 ? 'ql!' :
+	    $Config{byteorder} == 1234 ? 'lxxxxl!xxxx' : 'xxxxll!';
+	if ($self->{sndtimeo}) {
+		setsockopt($ls, SOL_SOCKET, SO_SNDTIMEO,
+		    pack($packstr, $self->{sndtimeo}, 0))
+		    or die ref($self), " set SO_SNDTIMEO failed: $!";
+	}
+	if ($self->{rcvtimeo}) {
+		setsockopt($ls, SOL_SOCKET, SO_RCVTIMEO,
+		    pack($packstr, $self->{rcvtimeo}, 0))
+		    or die ref($self), " set SO_RCVTIMEO failed: $!";
+	}
+	setsockopt($ls, IPPROTO_TCP, TCP_NODELAY, pack('i', 1))
+	    or die ref($self), " set TCP_NODELAY failed: $!";
+	listen($ls, 1)
+	    or die ref($self), " socket listen failed: $!";
+	my $log = $self->{log};
+	print $log "listen sock: ",$ls->sockhost()," ",$ls->sockport(),"\n";
+	$self->{listenaddr} = $ls->sockhost() unless $self->{listenaddr};
+	$self->{listenport} = $ls->sockport() unless $self->{listenport};
+	$self->{ls} = $ls;
+	return $self;
+}
+
+sub child {
+	my $self = shift;
+
+	# in case we redo the accept, shutdown the old one
+	shutdown(\*STDOUT, SHUT_WR);
+	delete $self->{as};
+
+	my $as = $self->{ls}->accept()
+	    or die ref($self)," ",ref($self->{ls}),
+	    " socket accept failed: $!,$SSL_ERROR";
+	print STDERR "accept sock: ",$as->sockhost()," ",$as->sockport(),"\n";
+	print STDERR "accept peer: ",$as->peerhost()," ",$as->peerport(),"\n";
+	if ($self->{ssl}) {
+		print STDERR "ssl version: ",$as->get_sslversion(),"\n";
+		print STDERR "ssl cipher: ",$as->get_cipher(),"\n";
+		print STDERR "ssl peer certificate:\n",
+		    $as->dump_peer_certificate();
+	}
+
+	*STDIN = *STDOUT = $self->{as} = $as;
+}
+
+1;
blob - /dev/null
blob + 28e53a9212642ffef6336db5db6dc037a490f692 (mode 644)
--- /dev/null
+++ regress/args-default.pl
@@ -0,0 +1,11 @@
+# test default values
+
+use strict;
+use warnings;
+
+our %args = (
+    len => 251,
+    md5 => "bc3a3f39af35fe5b1687903da2b00c7f",
+);
+
+1;
blob - /dev/null
blob + 23b7c3faf73c7a8c60de5fc7fecadf667411eaf7 (mode 644)
--- /dev/null
+++ regress/args-dryrun.pl
@@ -0,0 +1,20 @@
+# test broken config
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	noclient => 1,
+    },
+    relayd => {
+	protocol => [ "foo" ],
+	dryrun => qr/invalid protocol type: foo/,
+    },
+    server => {
+	noserver => 1,
+    },
+    nocheck => 1,
+);
+
+1;
blob - /dev/null
blob + f5acbd1ecf9bb88305ff06ea808c775e1672c2b9 (mode 644)
--- /dev/null
+++ regress/args-http-append-header.pl
@@ -0,0 +1,40 @@
+# test appending headers, both directions
+
+use strict;
+use warnings;
+
+my %header_client = (
+	"X-Header-Client" => "ABC",
+);
+my %header_server = (
+	"X-Header-Server" => "XYZ",
+);
+our %args = (
+    client => {
+	func => \&http_client,
+	header => \%header_client,
+	loggrep => {
+	    "X-Header-Server: XYZ" => 1,
+	    "X-Header-Server: xyz" => 1,
+	},
+    },
+    relayd => {
+	protocol => [ "http",
+	    'match request header append "X-Header-Client" value "abc"',
+	    'match response header append "X-Header-Server" value "xyz"',
+	    'match request header log "X-Header*"',
+	    'match response header log "X-Header*"',
+	],
+	loggrep => { qr/ (?:done|last write \(done\)), \[X-Header-Client: ABC\]\ GET \{X-Header-Server: XYZ\};/ => 1 },
+    },
+    server => {
+	func => \&http_server,
+	header => \%header_server,
+	loggrep => {
+	    "X-Header-Client: ABC" => 1,
+	    "X-Header-Client: abc" => 1,
+	},
+    },
+);
+
+1;
blob - /dev/null
blob + db13a06ae3d9dd11d4aeb05fffb40e4ff81061df (mode 644)
--- /dev/null
+++ regress/args-http-append.pl
@@ -0,0 +1,27 @@
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&http_client,
+	loggrep => {
+	    'X-Server-Append: \d+\.\d+\.\d+\.\d+:\d+$' => 1,
+	    'Set-Cookie: a=b\;' => 1,
+	},
+    },
+    relayd => {
+	protocol => [ "http",
+	    'match request header append X-Client-Append value \
+		"$REMOTE_ADDR:$REMOTE_PORT"',
+	    'match response header append X-Server-Append value \
+		"$SERVER_ADDR:$SERVER_PORT" \
+		cookie set "a" value "b"',
+	],
+    },
+    server => {
+	func => \&http_server,
+	loggrep => { 'X-Client-Append: \d+\.\d+\.\d+\.\d+:\d+$' => 1 },
+    },
+);
+
+1;
blob - /dev/null
blob + 0339d897a232a01d84cd0fcc7c71c74b9b33408d (mode 644)
--- /dev/null
+++ regress/args-http-callback.pl
@@ -0,0 +1,54 @@
+# test http connection over http relay invoking the callback.
+# The client uses a bad method in the second request.
+# Check that the relay handles the input after the error correctly.
+
+use strict;
+use warnings;
+
+my @lengths = (4, 3);
+our %args = (
+    client => {
+	func => sub {
+	    my $self = shift;
+	    print <<'EOF';
+PUT /4 HTTP/1.1
+Host: foo.bar
+Content-Length: 4
+
+123
+XXX
+PUT /3 HTTP/1.1
+Host: foo.bar
+Content-Length: 3
+
+12
+EOF
+	    print STDERR "LEN: 4\n";
+	    print STDERR "LEN: 3\n";
+	    # relayd does not forward the first request if the second one
+	    # is invalid.  So do not expect any response.
+	    #http_response($self, "without len");
+	},
+	http_vers => ["1.1"],
+	lengths => \@lengths,
+	method => "PUT",
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request header log foo",
+	    "match response header log bar",
+	],
+	loggrep => {
+	    qr/, malformed, PUT/ => 1,
+	},
+    },
+    server => {
+	func => \&http_server,
+	# The server does not get any connection.
+	noserver => 1,
+	nocheck => 1,
+    },
+    lengths => \@lengths,
+);
+
+1;
blob - /dev/null
blob + c6808c9815954754f3c96d07a5e2378155322b8b (mode 644)
--- /dev/null
+++ regress/args-http-change-cookie.pl
@@ -0,0 +1,27 @@
+use strict;
+use warnings;
+
+my $name = "Set-Cookie";
+my %header = ("$name" => [ "test=a;", "test=b;" ]);
+our %args = (
+    client => {
+	func => \&http_client,
+	loggrep => {
+		qr/$name: test=c/ => 1,
+	}
+    },
+    relayd => {
+	protocol => [ "http",
+	    'match response header set "'.$name.'" value "test=c"',
+	],
+    },
+    server => {
+	func => \&http_server,
+	header => \%header,
+	loggrep => {
+		qr/$name: test=a/ => 1
+	},
+    },
+);
+
+1;
blob - /dev/null
blob + c4070b2a76b4c2827151c35216199f85f9274428 (mode 644)
--- /dev/null
+++ regress/args-http-change-path.pl
@@ -0,0 +1,27 @@
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&http_client,
+	loggrep => {
+		qr/GET \/251 HTTP\/1\.0/ => 1,
+	},
+    },
+    relayd => {
+	protocol => [ "http",
+	    'match request path set "*" value "/foopath" \
+		url log "*"',
+	],
+	loggrep => { qr/ (?:done|last write \(done\)), \[foo.bar\/foopath\]/ => 1 },
+    },
+    server => {
+	func => \&http_server,
+	loggrep => {
+		qr/GET \/foopath HTTP\/1\.0/ => 1,
+	},
+    },
+    len => 8,
+);
+
+1;
blob - /dev/null
blob + fb38379b5ad120e0be40024c444e856f82fbfb80 (mode 644)
--- /dev/null
+++ regress/args-http-change.pl
@@ -0,0 +1,29 @@
+use strict;
+use warnings;
+
+my %header = ("X-Test-Header" => "XOriginalValue");
+our %args = (
+    client => {
+	func => \&http_client,
+	loggrep => {
+		qr/X-Test-Header: XChangedValue/ => 1,
+		qr/Host: foo.bar/ => 1,
+	}
+    },
+    relayd => {
+	protocol => [ "http",
+	    'match request header set "Host" value "foobar.changed"',
+	    'match response header set "X-Test-Header" value "XChangedValue"',
+	],
+    },
+    server => {
+	func => \&http_server,
+	header => \%header,
+	loggrep => {
+		qr/X-Test-Header: XOriginalValue/ => 1,
+		qr/Host: foobar.changed/ => 1,
+	},
+    },
+);
+
+1;
blob - /dev/null
blob + 3cb248a73bfc9984dd20bf75e6a23790a698bb7c (mode 644)
--- /dev/null
+++ regress/args-http-chunked-callback.pl
@@ -0,0 +1,58 @@
+# test chunked http connection over http relay invoking the callback
+# The client writes a bad chunk length in the second chunk.
+# Check that the relay handles the input after the error correctly.
+
+use strict;
+use warnings;
+
+my @lengths = ([4, 3]);
+our %args = (
+    client => {
+	func => sub {
+	    my $self = shift;
+	    print <<'EOF';
+PUT /4/3 HTTP/1.1
+Host: foo.bar
+Transfer-Encoding: chunked
+
+EOF
+	    ${$self->{server}}->up;
+	    print <<'EOF';
+4
+123
+
+XXX
+3
+12
+
+0
+
+EOF
+	    print STDERR "LEN: 4\n";
+	    print STDERR "LEN: 3\n";
+	    # relayd does not forward the first chunk if the second one
+	    # is invalid.  So do not expect any response.
+	    #http_response($self, "without len");
+	},
+	http_vers => ["1.1"],
+	lengths => \@lengths,
+	method => "PUT",
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request header log foo",
+	    "match response header log bar",
+	],
+	loggrep => {
+	    qr/, invalid chunk size, PUT/ => 1,
+	},
+    },
+    server => {
+	down => "Server missing chunk size",
+	func => sub { errignore(@_); http_server(@_); },
+	nocheck => 1,
+    },
+    lengths => \@lengths,
+);
+
+1;
blob - /dev/null
blob + ffe0751ab46700c9656c94b423558ce0c7150b07 (mode 644)
--- /dev/null
+++ regress/args-http-chunked-invalid.pl
@@ -0,0 +1,52 @@
+# Test parsing of invalid chunk length values
+# We force multiple connections since relayd will abort the connection
+# when it encounters a bogus chunk size.
+# 
+
+use strict;
+use warnings;
+
+my @lengths = (7, 6, 5, 4, 3, 2);
+my @chunks = ("0x4", "+3", "-0", "foo", "dead beef", "Ff0");
+our %args = (
+    client => {
+	func => sub {
+	    my $self = shift;
+	    my $chunk = shift(@chunks);
+	    $self->{redo} = int(@chunks);
+	    print <<"EOF";
+PUT /4/3 HTTP/1.1
+Host: foo.bar
+Transfer-Encoding: chunked
+
+$chunk
+
+EOF
+	    foreach (@lengths) {
+		print STDERR "LEN: $_\n";
+	    }
+	    # relayd does not forward the first chunk if the second one
+	    # is invalid.  So do not expect any response.
+	    #http_response($self, "without len");
+	},
+	http_vers => ["1.1"],
+	lengths => \@lengths,
+	method => "PUT",
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request header log foo",
+	    "match response header log bar",
+	],
+	loggrep => {
+	    qr/, invalid chunk size, PUT/ => 5,
+	},
+    },
+    server => {
+	func => \&http_server,
+	nocheck => 1,
+    },
+    lengths => \@lengths,
+);
+
+1;
blob - /dev/null
blob + 9a7e36dc84ac8142ec8e9518f3dcfd5ff749634b (mode 644)
--- /dev/null
+++ regress/args-http-chunked-put.pl
@@ -0,0 +1,38 @@
+# test chunked http request over http relay
+
+use strict;
+use warnings;
+
+my @lengths = ([ 251, 10000, 10 ], 1, [2, 3]);
+our %args = (
+    client => {
+	func => \&http_client,
+	lengths => \@lengths,
+	http_vers => ["1.1"],
+	method => "PUT",
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request header log Transfer-Encoding",
+	    "match response header log bar",
+	],
+	loggrep => {
+		qr/\[Transfer-Encoding: chunked\]/ => 1,
+		qr/\[\(null\)\]/ => 0,
+	},
+    },
+    server => {
+	func => \&http_server,
+    },
+    lengths => \@lengths,
+    md5 => [
+	"bc3a3f39af35fe5b1687903da2b00c7f",
+	"fccd8d69acceb0cc35f2fd4e2f6938d3",
+	"c47658d102d5b989e0da09ce403f7463",
+	"68b329da9893e34099c7d8ad5cb9c940",
+	"897316929176464ebc9ad085f31e7284",
+	"0ade138937c4b9cb36a28e2edb6485fc",
+    ],
+);
+
+1;
blob - /dev/null
blob + 5b0abfe3f806351cb3bfcb67250b7ca9c12cdb34 (mode 644)
--- /dev/null
+++ regress/args-http-chunked.pl
@@ -0,0 +1,37 @@
+# test chunked http 1.1 connection over http relay
+
+use strict;
+use warnings;
+
+my @lengths = ([ 251, 10000, 10 ], 1, [2, 3]);
+our %args = (
+    client => {
+	func => \&http_client,
+	lengths => \@lengths,
+	http_vers => ["1.1"],
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request header log foo",
+	    "match response header log Transfer-Encoding",
+	],
+	loggrep => {
+		"{Transfer-Encoding: chunked}" => 1,
+		qr/\[\(null\)\]/ => 0,
+	},
+    },
+    server => {
+	func => \&http_server,
+    },
+    lengths => \@lengths,
+    md5 => [
+	"bc3a3f39af35fe5b1687903da2b00c7f",
+	"fccd8d69acceb0cc35f2fd4e2f6938d3",
+	"c47658d102d5b989e0da09ce403f7463",
+	"68b329da9893e34099c7d8ad5cb9c940",
+	"897316929176464ebc9ad085f31e7284",
+	"0ade138937c4b9cb36a28e2edb6485fc",
+    ],
+);
+
+1;
blob - /dev/null
blob + 01dfa221debdccc204a60a0b80de363345ece275 (mode 644)
--- /dev/null
+++ regress/args-http-contentlength-get.pl
@@ -0,0 +1,50 @@
+# Test to verify that relayd strips Content-Length and body
+# from GET requests.
+
+use strict;
+use warnings;
+
+my $payload_len = 64;
+our %args = (
+    client => {
+	func => sub {
+	    my $self = shift;
+	    my @request_stream = split("\n", <<"EOF", -1);
+GET http://foo.bar/$payload_len HTTP/1.1
+Content-Length: $payload_len
+
+foo=bar
+
+EOF
+	    pop @request_stream;
+	    print map { "$_\r\n" } @request_stream;
+	    print STDERR map { ">>> $_\n" } @request_stream;
+	    $self->{method} = 'GET';
+	    http_response($self, $payload_len);
+	},
+	loggrep => {
+	    qr/Content-Length: $payload_len/ => 2,
+	    qr/foo=bar/ => 1,
+	},
+	http_vers => ["1.1"],
+	nocheck => 1,
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request path log \"*\"",
+	],
+	loggrep => {
+	    qr/, done, \[http:\/\/foo.bar\/$payload_len\] GET/ => 1,
+	},
+    },
+    server => {
+	func => \&http_server,
+	loggrep => {
+	    qr/Content-Length: $payload_len/ => 1,
+	    qr/foo=bar/ => 0,
+	},
+	nocheck => 1,
+    },
+);
+
+1;
blob - /dev/null
blob + 5b7ad2d9e3bcf2283cb16eee11670e9e23479557 (mode 644)
--- /dev/null
+++ regress/args-http-contentlength-invalid.pl
@@ -0,0 +1,40 @@
+# Test that relayd aborts the connection if Content-Length is invalid
+# We test "+0" because it is accepted by strtol(), sscanf(), etc
+# but is not legal according to the RFC.
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => sub {
+	    my $self = shift;
+	    print <<"EOF";
+PUT /1 HTTP/1.1
+Host: www.foo.com
+Content-Length: +0
+
+EOF
+	    # no http_response($self, 1);
+	},
+	http_vers => ["1.1"],
+	nocheck => 1,
+	method => "PUT",
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request header log Host",
+	],
+	loggrep => {
+	    qr/, invalid$/ => 1,
+	    qr/\[Host: www.foo.com\] PUT/ => 0,
+	},
+    },
+    server => {
+	func => \&http_server,
+	nocheck => 1,
+	noserver => 1,
+    }
+);
+
+1;
blob - /dev/null
blob + 2362af457c50e3692e20df3d21e0e45ae553a26a (mode 644)
--- /dev/null
+++ regress/args-http-contentlength.pl
@@ -0,0 +1,37 @@
+# test persistent http 1.1 connection and grep for content length
+
+use strict;
+use warnings;
+
+my @lengths = (1, 2, 0, 3, 4);
+our %args = (
+    client => {
+	func => \&http_client,
+	lengths => \@lengths,
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request header log foo",
+	    "match response header log Content-Length",
+	],
+	loggrep => [ map { "Content-Length: $_" } @lengths ],
+    },
+    server => {
+	func => \&http_server,
+    },
+    lengths => \@lengths,
+    md5 => [
+	"68b329da9893e34099c7d8ad5cb9c940",
+	"897316929176464ebc9ad085f31e7284",
+	"d41d8cd98f00b204e9800998ecf8427e",
+	"0ade138937c4b9cb36a28e2edb6485fc",
+	"e686f5db1f8610b65f98f3718e1a5b72",
+	"68b329da9893e34099c7d8ad5cb9c940",
+	"897316929176464ebc9ad085f31e7284",
+	"d41d8cd98f00b204e9800998ecf8427e",
+	"0ade138937c4b9cb36a28e2edb6485fc",
+	"e686f5db1f8610b65f98f3718e1a5b72",
+    ],
+);
+
+1;
blob - /dev/null
blob + 710ed854c45f2074818f17b6473477d8fea9520c (mode 644)
--- /dev/null
+++ regress/args-http-expect.pl
@@ -0,0 +1,28 @@
+use strict;
+use warnings;
+
+my @lengths = (21);
+our %args = (
+    client => {
+	func => \&http_client,
+	lengths => \@lengths,
+	path => "query?foo=bar&ok=yes",
+    },
+    relayd => {
+	protocol => [ "http",
+	    'block request',
+	    'block request query log "ok"',
+	    'pass query log "foo" value "bar"',
+	],
+	loggrep => {
+		qr/\[foo: bar\]/ => 2,
+		qr/\[ok: yes\]/ => 0,
+	},
+    },
+    server => {
+	func => \&http_server,
+    },
+    lengths => \@lengths,
+);
+
+1;
blob - /dev/null
blob + 340853ea25ea466f3151363c47334872145cac17 (mode 644)
--- /dev/null
+++ regress/args-http-filter-block.pl
@@ -0,0 +1,25 @@
+# test http block
+
+use strict;
+use warnings;
+
+my @lengths = (1, 2, 0, 3);
+our %args = (
+    client => {
+	func => \&http_client,
+	loggrep => qr/Client missing http 3 response/,
+	lengths => \@lengths,
+    },
+    relayd => {
+	protocol => [ "http",
+	    'block request path "/3"',
+	],
+	loggrep => qr/Forbidden/,
+    },
+    server => {
+	func => \&http_server,
+    },
+    lengths => [1, 2, 0],
+);
+
+1;
blob - /dev/null
blob + d72f99ddb4d2df7655843940fc08997269aa563c (mode 644)
--- /dev/null
+++ regress/args-http-filter-contentlength.pl
@@ -0,0 +1,27 @@
+# test http connection with request filter and explicit content length 0
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&http_client,
+	header => {
+	    'Content-Length' => 0,
+	},
+	len => 1,
+    },
+    relayd => {
+	protocol => [ "http",
+	    'block request path "/2"',
+	],
+	loggrep => qr/done/,
+    },
+    server => {
+	func => \&http_server,
+    },
+    len => 1,
+    md5 => "68b329da9893e34099c7d8ad5cb9c940",
+);
+
+1;
blob - /dev/null
blob + 3ac271660bbb491a41734c386a9a8f06f8e5f51e (mode 644)
--- /dev/null
+++ regress/args-http-filter-cookie.pl
@@ -0,0 +1,32 @@
+# test http block cookies
+
+use strict;
+use warnings;
+
+my @lengths = (1, 2, 3, 4);
+my @cookies = ("med=thx; domain=.foo.bar; path=/; expires=Mon, 27-Oct-2014 04:11:56 GMT;", "", "", "");
+our %args = (
+    client => {
+	func => \&http_client,
+	loggrep => {
+	    qr/Client missing http 1 response/ => 2,
+	    qr/Set-Cookie: a\=b\;/ => 6,
+	},
+	cookies => \@cookies,
+	lengths => \@lengths,
+    },
+    relayd => {
+	protocol => [ "http",
+	    'block request cookie log "med" value "thx"',
+	    'match response cookie append "a" value "b" tag "cookie"',
+	    'pass tagged "cookie"',
+	],
+	loggrep => qr/Forbidden, \[Cookie: med=thx.*/,
+    },
+    server => {
+	func => \&http_server,
+    },
+    lengths => [2, 3, 4],
+);
+
+1;
blob - /dev/null
blob + e646178c149c75349a0beaa989d62bd840ba995a (mode 644)
--- /dev/null
+++ regress/args-http-filter-method.pl
@@ -0,0 +1,27 @@
+# test method filtering
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&http_client,
+	method => 'HEAD',
+	loggrep => qr/HTTP\/1\.0 403 Forbidden/,
+	httpnok => 1,
+	nocheck => 1,
+    },
+    relayd => {
+	protocol => [ "http",
+	    'return error',
+	    'block request method "HEAD" path log',
+	],
+	loggrep => qr/403 Forbidden/,
+    },
+    server => {
+	noserver => 1,
+	nocheck => 1,
+    },
+);
+
+1;
blob - /dev/null
blob + 3ef85eb455dca29c9873c31af24c93224cc7ad54 (mode 644)
--- /dev/null
+++ regress/args-http-filter-null-host.pl
@@ -0,0 +1,33 @@
+# test http request with null Host header
+
+use strict;
+use warnings;
+
+
+my %header_client = (
+    "Host" => "",
+);
+
+our %args = (
+    client => {
+	func => \&http_client,
+	header => \%header_client,
+	httpnok => 1,
+	nocheck => 1,
+    },
+    relayd => {
+	protocol => [ "http",
+	    'pass',
+	    'block url "Host"',
+	    'return error',
+	],
+#	loggrep => qr/Forbidden, \[Cookie: med=thx.*/,
+    },
+    server => {
+	func => \&http_server,
+	noserver => 1,
+	nocheck => 1,
+    },
+);
+
+1;
blob - /dev/null
blob + fe03666de897826d9b4dfd8c72c687a3761d380e (mode 644)
--- /dev/null
+++ regress/args-http-filter-persistent.pl
@@ -0,0 +1,41 @@
+# test persistent http connection with request filter
+
+use strict;
+use warnings;
+
+my @lengths = (251, 16384, 0, 1, 2, 3, 4, 5);
+our %args = (
+    client => {
+	func => \&http_client,
+	lengths => \@lengths,
+	loggrep => qr/Client missing http 2 response/,
+    },
+    relayd => {
+	protocol => [ "http",
+	    'block request path "/2"',
+	],
+	loggrep => qr/Forbidden/,
+    },
+    server => {
+	func => \&http_server,
+    },
+    lengths => [251, 16384, 0, 1, 3, 4, 5],
+    md5 => [
+	"bc3a3f39af35fe5b1687903da2b00c7f",
+	"52afece07e61264c3087ddf52f729376",
+	"d41d8cd98f00b204e9800998ecf8427e",
+	"68b329da9893e34099c7d8ad5cb9c940",
+	"0ade138937c4b9cb36a28e2edb6485fc",
+	"e686f5db1f8610b65f98f3718e1a5b72",
+	"e5870c1091c20ed693976546d23b4841",
+	"bc3a3f39af35fe5b1687903da2b00c7f",
+	"52afece07e61264c3087ddf52f729376",
+	"d41d8cd98f00b204e9800998ecf8427e",
+	"68b329da9893e34099c7d8ad5cb9c940",
+	"0ade138937c4b9cb36a28e2edb6485fc",
+	"e686f5db1f8610b65f98f3718e1a5b72",
+	"e5870c1091c20ed693976546d23b4841",
+    ],
+);
+
+1;
blob - /dev/null
blob + 691beb7d27f4d1e453db004672e093456dcd8c98 (mode 644)
--- /dev/null
+++ regress/args-http-filter-put-contentlength.pl
@@ -0,0 +1,28 @@
+# test http put with request filter and request contentlength
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&http_client,
+	header => {
+	    'Content-Length' => 1,
+	},
+	len => 1,
+	method => 'PUT',
+    },
+    relayd => {
+	protocol => [ "http",
+	    'match request path "/2"',
+	],
+	loggrep => qr/done/,
+    },
+    server => {
+	func => \&http_server,
+    },
+    len => 1,
+    md5 => "68b329da9893e34099c7d8ad5cb9c940",
+);
+
+1;
blob - /dev/null
blob + e1e57543b392ba470c20c5cef8fb51b92d3a9262 (mode 644)
--- /dev/null
+++ regress/args-http-filter-put.pl
@@ -0,0 +1,25 @@
+# test http put with request filter
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&http_client,
+	len => 1,
+	method => 'PUT',
+    },
+    relayd => {
+	protocol => [ "http",
+	    'block request path "/2"',
+	],
+	loggrep => qr/done/,
+    },
+    server => {
+	func => \&http_server,
+    },
+    len => 1,
+    md5 => "68b329da9893e34099c7d8ad5cb9c940",
+);
+
+1;
blob - /dev/null
blob + 64551ea3d9bf08293b091a9ee13f3491fc906f1f (mode 644)
--- /dev/null
+++ regress/args-http-filter-url-digest.pl
@@ -0,0 +1,32 @@
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&http_client,
+	path => "a/b/c/d/e/f/gindex.html",
+	loggrep => [
+	    qr/403 Forbidden/,
+	    qr/Server: OpenBSD relayd/,
+	    qr/Connection: close/,
+	],
+	nocheck => 1,
+	httpnok => 1,
+    },
+    relayd => {
+	protocol => [ "http",
+	    'return error',
+	    'block request url log digest 0ac8ccfc03317891ae2820de10ee2167d31ebd16',
+	],
+	loggrep => {
+	    qr/Forbidden \(403 Forbidden\)/ => 1,
+	    qr/\[0ac8ccfc03317891ae2820de10ee2167d31ebd16\]/ => 1,
+	},
+    },
+    server => {
+	noserver => 1,
+	nocheck => 1,
+    },
+);
+
+1;
blob - /dev/null
blob + 11aae0018687b40d05d9a639e70ba600e1715205 (mode 644)
--- /dev/null
+++ regress/args-http-filter-url-file.in
@@ -0,0 +1,2 @@
+foo.bar/3
+foo.bar/0
blob - /dev/null
blob + 06a7545c0f2338754904fcb52c576b82e54ccd8a (mode 644)
--- /dev/null
+++ regress/args-http-filter-url-file.pl
@@ -0,0 +1,31 @@
+use strict;
+use warnings;
+
+my @lengths = (1, 2, 4, 0, 3, 5);
+our %args = (
+    client => {
+	func => \&http_client,
+	lengths => \@lengths,
+	loggrep => {
+		qr/403 Forbidden/ => 4,
+	},
+    },
+    relayd => {
+	protocol => [ "http",
+	    'return error',
+	    'pass',
+	    'block request url log file "$curdir/args-http-filter-url-file.in" value "*" label "test_reject_label"',
+	],
+	loggrep => {
+		qr/Forbidden/ => 4,
+		qr/\[test_reject_label\, foo\.bar\/0\]/ => 2,
+		qr/\[test_reject_label\, foo\.bar\/3\]/ => 2,
+	},
+    },
+    server => {
+	func => \&http_server,
+    },
+    lengths => [1, 2, 4, 5],
+);
+
+1;
blob - /dev/null
blob + ec325f230b658369b4f7c91c8bb10f40fe3a7498 (mode 644)
--- /dev/null
+++ regress/args-http-filter-url.pl
@@ -0,0 +1,32 @@
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&http_client,
+	path => "a/b/c/d/e/f/gindex.html",
+	loggrep => [
+	    qr/403 Forbidden/,
+	    qr/Server: OpenBSD relayd/,
+	    qr/Connection: close/,
+	],
+	httpnok => 1,
+	nocheck => 1,
+    },
+    relayd => {
+	protocol => [ "http",
+	    'return error',
+	    'block request url log "foo.bar/a/b/"',
+	],
+	loggrep => {
+	    qr/Forbidden \(403 Forbidden\)/ => 1,
+	    qr/\[foo.bar\/a\/b\// => 1,
+	},
+    },
+    server => {
+	noserver => 1,
+	nocheck => 1,
+    },
+);
+
+1;
blob - /dev/null
blob + bf282f7b998cbc7d6ce607ef74b41ffe9af50b27 (mode 644)
--- /dev/null
+++ regress/args-http-filter.pl
@@ -0,0 +1,24 @@
+# test http connection with request filter, triggers lateconnect
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&http_client,
+	len => 1,
+    },
+    relayd => {
+	protocol => [ "http",
+	    'match request path "/2"',
+	],
+	loggrep => qr/done/,
+    },
+    server => {
+	func => \&http_server,
+    },
+    len => 1,
+    md5 => "68b329da9893e34099c7d8ad5cb9c940",
+);
+
+1;
blob - /dev/null
blob + 235b5693dc25b1c7cac4d98025105d64b3ba6088 (mode 644)
--- /dev/null
+++ regress/args-http-hash.pl
@@ -0,0 +1,26 @@
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&http_client,
+	path => "query?foobar",
+    },
+    relayd => {
+	table => 1,
+	protocol => [ "http",
+	    'match request path hash "/query"',
+	    'match request path log "/query"',
+	],
+	relay => 'forward to <table-$test> port $connectport',
+	loggrep => {
+		qr/ (?:done|last write \(done\)), \[\/query: foobar\]/ => 1,
+	},
+    },
+    server => {
+	func => \&http_server,
+    },
+    len => 13,
+);
+
+1;
blob - /dev/null
blob + 3176d05e33f87fb98cb6f56497938e1e231c1e7b (mode 644)
--- /dev/null
+++ regress/args-http-head-get.pl
@@ -0,0 +1,45 @@
+use strict;
+use warnings;
+
+my $payload_len = 64;
+our %args = (
+    client => {
+	func => sub {
+	    my $self = shift;
+	    my @request_stream = split("\n", <<"EOF", -1);
+HEAD http://foo.bar/$payload_len HTTP/1.1
+
+EOF
+	    pop @request_stream;
+	    print map { "$_\r\n" } @request_stream;
+	    print STDERR map { ">>> $_\n" } @request_stream;
+	    $self->{method} = 'HEAD';
+	    http_response($self, $payload_len);
+	    @request_stream = split("\n", <<"EOF", -1);
+GET http://foo.bar/$payload_len HTTP/1.1
+
+EOF
+	    pop @request_stream;
+	    print map { "$_\r\n" } @request_stream;
+	    print STDERR map { ">>> $_\n" } @request_stream;
+	    $self->{method} = 'GET';
+	    http_response($self, $payload_len);
+	},
+	http_vers => ["1.1"],
+	nocheck => 1,
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request path log \"*\"",
+	],
+	loggrep => {
+	    qr/, done, \[http:\/\/foo.bar\/$payload_len\] HEAD; \[http:\/\/foo.bar\/$payload_len\] GET/ => 1,
+	},
+    },
+    server => {
+	func => \&http_server,
+	nocheck => 1,
+    },
+);
+
+1;
blob - /dev/null
blob + 1597e2766f833463afa2123efa4085b48630477f (mode 644)
--- /dev/null
+++ regress/args-http-headerlength.pl
@@ -0,0 +1,26 @@
+use strict;
+use warnings;
+
+my %header = ( "Host" => "www.example.com", "Set-Cookie" => "a="."X"x8192 );
+our %args = (
+    client => {
+	func => \&http_client,
+	header => \%header,
+	httpnok => 1,
+	nocheck => 1,
+	loggrep => qr/HTTP\/1\.0 413 Payload Too Large/,
+    },
+    relayd => {
+	protocol => [ "http",
+	    'return error',
+	    'pass',
+	],
+	loggrep => qr/413 Payload Too Large/,
+    },
+    server => {
+	noserver => 1,
+	nocheck => 1,
+    },
+);
+
+1;
blob - /dev/null
blob + cfc27e341b23d111b36ae81042564d734de5fe34 (mode 644)
--- /dev/null
+++ regress/args-http-headline-callback.pl
@@ -0,0 +1,54 @@
+# test persistent http connection over http relay invoking the callback
+# The client writes a bad header line in the second request.
+# Check that the relay handles the input after the error correctly.
+
+use strict;
+use warnings;
+
+my @lengths = (4, 3);
+our %args = (
+    client => {
+	func => sub {
+	    my $self = shift;
+	    print <<'EOF';
+PUT /4 HTTP/1.1
+Host: foo.bar
+Content-Length: 4
+
+123
+PUT /3 HTTP/1.1
+XXX
+Host: foo.bar
+Content-Length: 3
+
+12
+EOF
+	    print STDERR "LEN: 4\n";
+	    print STDERR "LEN: 3\n";
+	    # relayd does not forward the first request if the second one
+	    # is invalid.  So do not expect any response.
+	    #http_response($self, "without len");
+	},
+	http_vers => ["1.1"],
+	lengths => \@lengths,
+	method => "PUT",
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request header log foo",
+	    "match response header log bar",
+	],
+	loggrep => {
+	    qr/, malformed, PUT/ => 1,
+	},
+    },
+    server => {
+	func => \&http_server,
+	# The server does not get any connection.
+	noserver => 1,
+	nocheck => 1,
+    },
+    lengths => \@lengths,
+);
+
+1;
blob - /dev/null
blob + 3e86e0c8e50fc826b82ddc5e8aa121cd73da6211 (mode 644)
--- /dev/null
+++ regress/args-http-headline-close.pl
@@ -0,0 +1,30 @@
+# test http connection over http relay
+# The client writes an incomplete header line and closes the connection.
+# Check that the relay establishes and also closes the session.
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => sub {
+	    print "GET ";  # missing new line
+	},
+	nocheck => 1,
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request header log foo",
+	    "match response header log bar",
+	],
+	loggrep => {
+	    qr/session 1 .*, done/ => 1,
+	},
+    },
+    server => {
+	noserver => 1,
+	nocheck => 1,
+    },
+);
+
+1;
blob - /dev/null
blob + a77e852ee083f1980b07d1f697d5915e7bffdca1 (mode 644)
--- /dev/null
+++ regress/args-http-host.pl
@@ -0,0 +1,37 @@
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => sub {
+	    my $self = shift;
+	    print <<'EOF';
+GET /1 HTTP/1.1
+Host: www.foo.com
+Host: www.bar.com
+
+EOF
+	    # no http_response($self, 1);
+	},
+	http_vers => ["1.1"],
+	nocheck => 1,
+	method => "GET",
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request header log Host",
+	],
+	loggrep => {
+	    qr/, malformed header$/ => 1,
+	    qr/\[Host: www.foo.com\] GET/ => 0,
+	    qr/\[Host: www.bar.com\] GET/ => 0,
+	},
+    },
+    server => {
+	func => \&http_server,
+	noserver => 1,
+	nocheck => 1,
+    }
+);
+
+1;
blob - /dev/null
blob + b34a580665fe7c35f0c07eadec4cefb5a494ee8d (mode 644)
--- /dev/null
+++ regress/args-http-host2.pl
@@ -0,0 +1,34 @@
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => sub {
+	    my $self = shift;
+	    print <<'EOF';
+GET http://www.foo.com/1 HTTP/1.1
+
+EOF
+	    http_response($self, 1);
+	},
+	http_vers => ["1.1"],
+	nocheck => 1,
+	method => "GET",
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request header log Host",
+	    "match request path log \"*\"",
+	],
+	loggrep => {
+	    qr/, malformed header$/ => 0,
+	    qr/\[http:\/\/www.foo.com\/1\] GET/ => 1,
+	},
+    },
+    server => {
+	func => \&http_server,
+	nocheck => 1,
+    }
+);
+
+1;
blob - /dev/null
blob + ab2201396ea666fb20d08cd7cc43b93ec5fd2913 (mode 644)
--- /dev/null
+++ regress/args-http-host3.pl
@@ -0,0 +1,36 @@
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => sub {
+	    my $self = shift;
+	    print <<'EOF';
+GET http://www.foo.com/1 HTTP/1.1
+Host: www.foo.com
+
+EOF
+	    http_response($self, 1);
+	},
+	http_vers => ["1.1"],
+	nocheck => 1,
+	method => "GET",
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request header log Host",
+	    "match request path log \"*\"",
+	],
+	loggrep => {
+	    qr/, malformed host$/ => 0,
+	    qr/\[http:\/\/www.foo.com\/1\] GET/ => 1,
+	    qr/\[Host: www.foo.com\]/ => 1,
+	},
+    },
+    server => {
+	func => \&http_server,
+	nocheck => 1
+    },
+);
+
+1;
blob - /dev/null
blob + 11787efe99000de9b719be1b95ca32b298ed8ba3 (mode 644)
--- /dev/null
+++ regress/args-http-host4.pl
@@ -0,0 +1,37 @@
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => sub {
+	    my $self = shift;
+	    print <<'EOF';
+GET http://www.foo.com/1 HTTP/1.1
+Host: www.bar.com
+
+EOF
+	    # no http_response($self, 1);
+	},
+	http_vers => ["1.1"],
+	nocheck => 1,
+	method => "GET",
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request header log Host",
+	    "match request path log \"*\"",
+	],
+	loggrep => {
+	    qr/, malformed host$/ => 1,
+	    qr/\[http:\/\/www.foo.com\/1\] GET/ => 0,
+	    qr/\[Host: www.bar.com\]/ => 0,
+	},
+    },
+    server => {
+	func => \&http_server,
+	noserver => 1,
+	nocheck => 1,
+    }
+);
+
+1;
blob - /dev/null
blob + c12f21d659d9d6f5e83d3c2e3405c37ce19514f4 (mode 644)
--- /dev/null
+++ regress/args-http-invalid-header1.pl
@@ -0,0 +1,38 @@
+# Test that relayd aborts the connection if a header name has invalid chars
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => sub {
+	    my $self = shift;
+	    print <<"EOF";
+GET /1 HTTP/1.1
+Host: www.foo.com
+X-Header Client: ABC
+
+EOF
+	    # no http_response($self, 1);
+	},
+	http_vers => ["1.1"],
+	nocheck => 1,
+	method => "GET",
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request header log Host",
+	],
+	loggrep => {
+	    qr/, malformed$/ => 1,
+	    qr/\[Host: www.foo.com\] GET/ => 0,
+	},
+    },
+    server => {
+	func => \&http_server,
+	nocheck => 1,
+	noserver => 1,
+    }
+);
+
+1;
blob - /dev/null
blob + cb9d8b3d7446344afa2e0dd7775e22bba8a6deb3 (mode 644)
--- /dev/null
+++ regress/args-http-invalid-header2.pl
@@ -0,0 +1,38 @@
+# Test that relayd aborts the connection if a header include a NUL byte
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => sub {
+	    my $self = shift;
+	    print <<"EOF";
+GET /1 HTTP/1.1
+Host: www.foo.com
+X-Header-Client: ABC\0D
+
+EOF
+	    # no http_response($self, 1);
+	},
+	http_vers => ["1.1"],
+	nocheck => 1,
+	method => "GET",
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request header log Host",
+	],
+	loggrep => {
+	    qr/, malformed$/ => 1,
+	    qr/\[Host: www.foo.com\] GET/ => 0,
+	},
+    },
+    server => {
+	func => \&http_server,
+	nocheck => 1,
+	noserver => 1,
+    }
+);
+
+1;
blob - /dev/null
blob + b103f402f4cf792664dab77b6211f3dd36a4e5c8 (mode 644)
--- /dev/null
+++ regress/args-http-label.pl
@@ -0,0 +1,29 @@
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&http_client,
+	loggrep => {
+	    qr/403 Forbidden/ => 1,
+	    qr/Content-Type: text\/html/ => 1
+	},
+	path => "query?foo=bar&ok=yes",
+	httpnok => 1,
+	nocheck => 1,
+    },
+    relayd => {
+	protocol => [ "http",
+	    'return error',
+	    'block',
+	    'match request query log "foo" value "bar" label "expect_foobar_label"',
+	],
+	loggrep => qr/Forbidden.*403 Forbidden.*expect_foobar_label.*foo: bar/,
+    },
+    server => {
+	noserver => 1,
+	nocheck => 1,
+    },
+);
+
+1;
blob - /dev/null
blob + 1e2602b1a6ad115ed83cb1b42002fdc284c6bdf8 (mode 644)
--- /dev/null
+++ regress/args-http-log.pl
@@ -0,0 +1,27 @@
+# test http connection over http relay
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&http_client,
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request header log Host",
+	    "match response header log Server",
+	],
+	loggrep => {
+	    qr/\[Host: foo.bar\]/ => 1,
+	    qr/\{Server: Perl\/[^\s]+\s*\};/ => 1,
+	},
+    },
+    server => {
+	func => \&http_server,
+    },
+    len => 251,
+    md5 => "bc3a3f39af35fe5b1687903da2b00c7f",
+);
+
+1;
blob - /dev/null
blob + 7beec7496e0d0e55410c4a94a67febd90a7b5a8a (mode 644)
--- /dev/null
+++ regress/args-http-mark-marked.pl
@@ -0,0 +1,22 @@
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&http_client,
+	len => 33,
+    },
+    relayd => {
+	protocol => [ "http",
+	    'match request path "/3*" value "*" tag RING0',
+	    'match request tagged RING0 tag RINGX',
+	],
+	loggrep => { ", RINGX,.*done" => 1 },
+    },
+    server => {
+	func => \&http_server,
+    },
+    len => 33,
+);
+
+1;
blob - /dev/null
blob + 2eb8b42e3b3fe3e5bb50b34e806709f7e5a179e5 (mode 644)
--- /dev/null
+++ regress/args-http-mark-marked2.pl
@@ -0,0 +1,43 @@
+# match and set header with tags
+
+use strict;
+use warnings;
+
+my %header_client = (
+    "User-Agent" => "Mozilla Bla",
+    "MyHeader" => "UnmatchableContent",
+);
+
+our %args = (
+    client => {
+	func => \&http_client,
+	header => \%header_client,
+	len => 33,
+    },
+    relayd => {
+	protocol => [ "http",
+	    # setting the User-Agent should succeed
+	    'match request header "User-Agent" value "Mozilla*" tag BORK',
+	    'match request header set "User-Agent" value "BORK" tagged BORK',
+	    'match request header log "User-Agent"',
+	    # setting MyHeader should not happen
+	    'match request header "MyHeader" value "SomethingDifferent" tag FOO',
+	    'match request header set "MyHeader" value "FOO" tagged FOO',
+	    'match request header log "MyHeader"',
+	],
+	loggrep => {
+	    '\[User-Agent: BORK\]' => 1,
+	    'MyHeader: FOO' => 0,
+	},
+    },
+    server => {
+	func => \&http_server,
+	loggrep => {
+	    "User-Agent: BORK" => 1,
+	    "MyHeader: FOO" => 0,
+	}
+    },
+    len => 33,
+);
+
+1;
blob - /dev/null
blob + dfaa5290947cc189132d765bd3699a66daf3cdc7 (mode 644)
--- /dev/null
+++ regress/args-http-mark.pl
@@ -0,0 +1,23 @@
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&http_client,
+	path => "foobar?path",
+    },
+    relayd => {
+	protocol => [ "http",
+	    'match request path "/foobar" value "*" tag RING0',
+	    'block request',
+	    'pass request quick tagged RING0',
+	],
+	loggrep => { ", RING0,.*done" => 1 },
+    },
+    server => {
+	func => \&http_server,
+    },
+    len => 12,
+);
+
+1;
blob - /dev/null
blob + 2cc16566451ce55ef13e1a489b9b7152942e8eb6 (mode 644)
--- /dev/null
+++ regress/args-http-multi.pl
@@ -0,0 +1,25 @@
+# test 50 http get with length 1 over http relay
+
+use strict;
+use warnings;
+
+my @lengths = map { 1 } (1..50);
+our %args = (
+    client => {
+	func => \&http_client,
+	lengths => \@lengths,
+	method => "GET",
+    },
+    relayd => {
+	protocol => [ "http" ],
+	loggrep => {
+	    qr/, (?:done|last write \(done\)), GET/ => (1 + @lengths),
+	},
+    },
+    server => {
+	func => \&http_server,
+    },
+    lengths => \@lengths,
+);
+
+1;
blob - /dev/null
blob + b9e052b8c998cf5113bac4c639e5a4c2a6b03f46 (mode 644)
--- /dev/null
+++ regress/args-http-persistent.pl
@@ -0,0 +1,45 @@
+# test persistent http 1.1 connection over http relay
+
+use strict;
+use warnings;
+
+my @lengths = (251, 16384, 0, 1, 2, 3, 4, 5);
+our %args = (
+    client => {
+	func => \&http_client,
+	lengths => \@lengths,
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request header log foo",
+	    "match response header log bar",
+	],
+	loggrep => {
+	    qr/, (?:done|last write \(done\))/ => (1 + @lengths),
+	}
+    },
+    server => {
+	func => \&http_server,
+    },
+    lengths => \@lengths,
+    md5 => [
+	"bc3a3f39af35fe5b1687903da2b00c7f",
+	"52afece07e61264c3087ddf52f729376",
+	"d41d8cd98f00b204e9800998ecf8427e",
+	"68b329da9893e34099c7d8ad5cb9c940",
+	"897316929176464ebc9ad085f31e7284",
+	"0ade138937c4b9cb36a28e2edb6485fc",
+	"e686f5db1f8610b65f98f3718e1a5b72",
+	"e5870c1091c20ed693976546d23b4841",
+	"bc3a3f39af35fe5b1687903da2b00c7f",
+	"52afece07e61264c3087ddf52f729376",
+	"d41d8cd98f00b204e9800998ecf8427e",
+	"68b329da9893e34099c7d8ad5cb9c940",
+	"897316929176464ebc9ad085f31e7284",
+	"0ade138937c4b9cb36a28e2edb6485fc",
+	"e686f5db1f8610b65f98f3718e1a5b72",
+	"e5870c1091c20ed693976546d23b4841",
+    ],
+);
+
+1;
blob - /dev/null
blob + 380d7166bdedefe3423681ed93177ce383c3756a (mode 644)
--- /dev/null
+++ regress/args-http-put-multi.pl
@@ -0,0 +1,25 @@
+# test 50 http put with length 1 over http relay
+
+use strict;
+use warnings;
+
+my @lengths = map { 1 } (1..50);
+our %args = (
+    client => {
+	func => \&http_client,
+	lengths => \@lengths,
+	method => "PUT",
+    },
+    relayd => {
+	protocol => [ "http" ],
+	loggrep => {
+	    qr/, (?:done|last write \(done\)), PUT/ => (1 + @lengths),
+	},
+    },
+    server => {
+	func => \&http_server,
+    },
+    lengths => \@lengths,
+);
+
+1;
blob - /dev/null
blob + 5fa727db7f53d7e293f6f0379c315036296287b8 (mode 644)
--- /dev/null
+++ regress/args-http-put.pl
@@ -0,0 +1,46 @@
+# test persistent http 1.1 put over http relay
+
+use strict;
+use warnings;
+
+my @lengths = (251, 16384, 0, 1, 2, 3, 4, 5);
+our %args = (
+    client => {
+	func => \&http_client,
+	lengths => \@lengths,
+	method => "PUT",
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request header log foo",
+	    "match response header log bar",
+	],
+	loggrep => {
+	    qr/, (?:done|last write \(done\))/ => (1 + @lengths),
+	}
+    },
+    server => {
+	func => \&http_server,
+    },
+    lengths => \@lengths,
+    md5 => [
+	"bc3a3f39af35fe5b1687903da2b00c7f",
+	"52afece07e61264c3087ddf52f729376",
+	"d41d8cd98f00b204e9800998ecf8427e",
+	"68b329da9893e34099c7d8ad5cb9c940",
+	"897316929176464ebc9ad085f31e7284",
+	"0ade138937c4b9cb36a28e2edb6485fc",
+	"e686f5db1f8610b65f98f3718e1a5b72",
+	"e5870c1091c20ed693976546d23b4841",
+	"bc3a3f39af35fe5b1687903da2b00c7f",
+	"52afece07e61264c3087ddf52f729376",
+	"d41d8cd98f00b204e9800998ecf8427e",
+	"68b329da9893e34099c7d8ad5cb9c940",
+	"897316929176464ebc9ad085f31e7284",
+	"0ade138937c4b9cb36a28e2edb6485fc",
+	"e686f5db1f8610b65f98f3718e1a5b72",
+	"e5870c1091c20ed693976546d23b4841",
+    ],
+);
+
+1;
blob - /dev/null
blob + 6a57f89bc4a3b6cc4ec59cda68d324485381525d (mode 644)
--- /dev/null
+++ regress/args-http-remove.pl
@@ -0,0 +1,33 @@
+use strict;
+use warnings;
+
+my %header = (
+	"X-Header-Foo" => "foo",
+	"X-Header-Bar" => "bar",
+);
+our %args = (
+    client => {
+	func => \&http_client,
+	loggrep => {
+	    "X-Header-Foo: foo" => 0,
+	    "X-Header-Bar: bar" => 1,
+	},
+    },
+    relayd => {
+	protocol => [ "http",
+	    'match response header remove X-Header-Foo',
+	    'match response header log "*Foo"',
+	],
+	loggrep => { qr/ (?:done|last write \(done\)), GET \{X-Header-Foo: foo \(removed\)\s*\};/ => 1 },
+    },
+    server => {
+	func => \&http_server,
+	header => \%header,
+	loggrep => {
+	    "X-Header-Foo: foo" => 1,
+	    "X-Header-Bar: bar" => 1,
+	},
+    },
+);
+
+1;
blob - /dev/null
blob + 686729f129159785c982b99f399fbd467d50be0b (mode 644)
--- /dev/null
+++ regress/args-http-return.pl
@@ -0,0 +1,27 @@
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&http_client,
+	# XXX add more paths to match a case where it pass
+	path => "query?foo=bar&ok=yes",
+	nocheck => 1,
+	httpnok => 1,
+    },
+    relayd => {
+	protocol => [ "http",
+	    'return error',
+	    'pass',
+	    'block request query log "foo" value "bar" label \
+		"expect_foobar_return_test"',
+	],
+	loggrep => { 'Forbidden \(403 Forbidden\), \[expect_foobar_return_test, foo: bar\]' => 1 },
+    },
+    server => {
+	noserver => 1,
+	nocheck => 1,
+    },
+);
+
+1;
blob - /dev/null
blob + e8e6680b35cada26662040d128f7cc00f6abadcc (mode 644)
--- /dev/null
+++ regress/args-http-slow-consumer.pl
@@ -0,0 +1,48 @@
+# test that a slow (in this case sleeping) client causes relayd to slow down
+# reading from the server (instead of balooning its buffers)
+
+use strict;
+use warnings;
+use Errno ':POSIX';
+
+my @errors = (EWOULDBLOCK);
+my $errors = "(". join("|", map { $! = $_ } @errors). ")";
+
+my $size = 2**21;
+
+our %args = (
+    client => {
+	fast => 1,
+	max => 100,
+	func => sub {
+	    my $self = shift;
+	    http_request($self , $size, "1.0", "");
+	    http_response($self , $size);
+	    print STDERR "going to sleep\n";
+	    ${$self->{server}}->loggrep(qr/blocked write/, 8)
+		or die "no blocked write in server.log";
+	    read_char($self, $size);
+	    return;
+	},
+	rcvbuf => 2**12,
+	nocheck => 1,
+    },
+    relayd => {
+	protocol => [ "http",
+	    "tcp socket buffer 1024",
+	    "match request header log",
+	    "match request path log",
+	],
+    },
+    server => {
+	fast => 1,
+	func => \&http_server,
+	sndbuf => 2**12,
+	sndtimeo => 2,
+	loggrep => qr/blocked write .*: $errors/,
+
+    },
+    lengths => [$size],
+);
+
+1;
blob - /dev/null
blob + 3c9a4f3fc9bd3772e447c4156db9ab1b87610651 (mode 644)
--- /dev/null
+++ regress/args-http-tcp.pl
@@ -0,0 +1,17 @@
+# test http connection over tcp relay
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&http_client,
+    },
+    server => {
+	func => \&http_server,
+    },
+    len => 251,
+    md5 => "bc3a3f39af35fe5b1687903da2b00c7f",
+);
+
+1;
blob - /dev/null
blob + 180fe00d13811ecddaef35c59b1226a30e5b64cf (mode 644)
--- /dev/null
+++ regress/args-http.pl
@@ -0,0 +1,20 @@
+# test http connection over http relay
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&http_client,
+    },
+    relayd => {
+	protocol => [ "http" ],
+    },
+    server => {
+	func => \&http_server,
+    },
+    len => 251,
+    md5 => "bc3a3f39af35fe5b1687903da2b00c7f",
+);
+
+1;
blob - /dev/null
blob + f017f7c5519e956fbc7c6c2c66c7398fdf229b11 (mode 644)
--- /dev/null
+++ regress/args-https-callback.pl
@@ -0,0 +1,58 @@
+# test https connection over http relay invoking the callback.
+# The client uses a bad method in the second request.
+# Check that the relay handles the input after the error correctly.
+
+use strict;
+use warnings;
+
+my @lengths = (4, 3);
+our %args = (
+    client => {
+	func => sub {
+	    my $self = shift;
+	    print <<'EOF';
+PUT /4 HTTP/1.1
+Host: foo.bar
+Content-Length: 4
+
+123
+XXX
+PUT /3 HTTP/1.1
+Host: foo.bar
+Content-Length: 3
+
+12
+EOF
+	    print STDERR "LEN: 4\n";
+	    print STDERR "LEN: 3\n";
+	    # relayd does not forward the first request if the second one
+	    # is invalid.  So do not expect any response.
+	    #http_response($self, "without len");
+	},
+	ssl => 1,
+	http_vers => ["1.1"],
+	lengths => \@lengths,
+	method => "PUT",
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request header log foo",
+	    "match response header log bar",
+	],
+	forwardssl => 1,
+	listenssl => 1,
+	loggrep => {
+	    qr/, malformed, PUT/ => 1,
+	},
+    },
+    server => {
+	func => \&http_server,
+	ssl => 1,
+	# The server does not get any connection.
+	noserver => 1,
+	nocheck => 1,
+    },
+    lengths => \@lengths,
+);
+
+1;
blob - /dev/null
blob + a4fe57a33b95818e17cc1caed189b34baeae7d3b (mode 644)
--- /dev/null
+++ regress/args-https-chunked-callback.pl
@@ -0,0 +1,59 @@
+# test chunked https connection over http relay invoking the callback
+# The client writes a bad chunk length in the second chunk.
+# Check that the relay handles the input after the error correctly.
+
+use strict;
+use warnings;
+
+my @lengths = ([4, 3]);
+our %args = (
+    client => {
+	func => sub {
+	    my $self = shift;
+	    print <<'EOF';
+PUT /4/3 HTTP/1.1
+Host: foo.bar
+Transfer-Encoding: chunked
+
+4
+123
+
+XXX
+3
+12
+
+0
+
+EOF
+	    print STDERR "LEN: 4\n";
+	    print STDERR "LEN: 3\n";
+	    # relayd does not forward the first chunk if the second one
+	    # is invalid.  So do not expect any response.
+	    #http_response($self, "without len");
+	},
+	ssl => 1,
+	http_vers => ["1.1"],
+	lengths => \@lengths,
+	method => "PUT",
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request header log foo",
+	    "match response header log bar",
+	],
+	forwardssl => 1,
+	listenssl => 1,
+	loggrep => {
+	    qr/, invalid chunk size, PUT/ => 1,
+	},
+    },
+    server => {
+	func => \&http_server,
+	# relayd only connects but does no ssl handshake
+	#ssl => 1,
+	nocheck => 1,
+    },
+    lengths => \@lengths,
+);
+
+1;
blob - /dev/null
blob + 32c950b10d4d25ce03ca7c61db1a310ab9fbd69c (mode 644)
--- /dev/null
+++ regress/args-https-chunked-put.pl
@@ -0,0 +1,42 @@
+# test chunked https request over http relay
+
+use strict;
+use warnings;
+
+my @lengths = ([ 251, 10000, 10 ], 1, [2, 3]);
+our %args = (
+    client => {
+	func => \&http_client,
+	ssl => 1,
+	lengths => \@lengths,
+	http_vers => ["1.1"],
+	method => "PUT",
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request header log Transfer-Encoding",
+	    "match response header log bar",
+	],
+	forwardssl => 1,
+	listenssl => 1,
+	loggrep => {
+		qr/\[Transfer-Encoding: chunked\]/ => 1,
+		qr/\[\(null\)\]/ => 0,
+	},
+    },
+    server => {
+	func => \&http_server,
+	ssl => 1,
+    },
+    lengths => \@lengths,
+    md5 => [
+	"bc3a3f39af35fe5b1687903da2b00c7f",
+	"fccd8d69acceb0cc35f2fd4e2f6938d3",
+	"c47658d102d5b989e0da09ce403f7463",
+	"68b329da9893e34099c7d8ad5cb9c940",
+	"897316929176464ebc9ad085f31e7284",
+	"0ade138937c4b9cb36a28e2edb6485fc",
+    ],
+);
+
+1;
blob - /dev/null
blob + f13f5897271812e4b06caedd14213fcd158fc859 (mode 644)
--- /dev/null
+++ regress/args-https-chunked.pl
@@ -0,0 +1,41 @@
+# test chunked https 1.1 connection over http relay
+
+use strict;
+use warnings;
+
+my @lengths = ([ 251, 10000, 10 ], 1, [2, 3]);
+our %args = (
+    client => {
+	func => \&http_client,
+	lengths => \@lengths,
+	http_vers => ["1.1"],
+	ssl => 1,
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request header log foo",
+	    "match response header log Transfer-Encoding",
+	],
+	loggrep => {
+		"{Transfer-Encoding: chunked}" => 1,
+		qr/\[\(null\)\]/ => 0,
+	},
+	forwardssl => 1,
+	listenssl => 1,
+    },
+    server => {
+	func => \&http_server,
+	ssl => 1,
+    },
+    lengths => \@lengths,
+    md5 => [
+	"bc3a3f39af35fe5b1687903da2b00c7f",
+	"fccd8d69acceb0cc35f2fd4e2f6938d3",
+	"c47658d102d5b989e0da09ce403f7463",
+	"68b329da9893e34099c7d8ad5cb9c940",
+	"897316929176464ebc9ad085f31e7284",
+	"0ade138937c4b9cb36a28e2edb6485fc",
+    ],
+);
+
+1;
blob - /dev/null
blob + 227d255da8f26687232004b292c773901b3198b4 (mode 644)
--- /dev/null
+++ regress/args-https-contentlength.pl
@@ -0,0 +1,41 @@
+# test persistent https 1.1 connection and grep for content length
+
+use strict;
+use warnings;
+
+my @lengths = (1, 2, 0, 3, 4);
+our %args = (
+    client => {
+	func => \&http_client,
+	lengths => \@lengths,
+	ssl => 1,
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request header log foo",
+	    "match response header log Content-Length",
+	],
+	loggrep => [ map { "Content-Length: $_" } @lengths ],
+	forwardssl => 1,
+	listenssl => 1,
+    },
+    server => {
+	func => \&http_server,
+	ssl => 1,
+    },
+    lengths => \@lengths,
+    md5 => [
+	"68b329da9893e34099c7d8ad5cb9c940",
+	"897316929176464ebc9ad085f31e7284",
+	"d41d8cd98f00b204e9800998ecf8427e",
+	"0ade138937c4b9cb36a28e2edb6485fc",
+	"e686f5db1f8610b65f98f3718e1a5b72",
+	"68b329da9893e34099c7d8ad5cb9c940",
+	"897316929176464ebc9ad085f31e7284",
+	"d41d8cd98f00b204e9800998ecf8427e",
+	"0ade138937c4b9cb36a28e2edb6485fc",
+	"e686f5db1f8610b65f98f3718e1a5b72",
+    ],
+);
+
+1;
blob - /dev/null
blob + 5272c6a44a53359d44d88e089fc64f50520ad33b (mode 644)
--- /dev/null
+++ regress/args-https-filter-persistent.pl
@@ -0,0 +1,48 @@
+# test persistent https connection with request filter
+
+use strict;
+use warnings;
+
+my @lengths = (251, 16384, 0, 1, 2, 3, 4, 5);
+our %args = (
+    client => {
+	func => \&http_client,
+	lengths => \@lengths,
+	loggrep => qr/Client missing http 2 response/,
+	ssl => 1,
+    },
+    relayd => {
+	protocol => [ "http",
+	    'block request path "/2"',
+	],
+	loggrep => [
+	    qr/tls, tls client/ => 1,
+	    qr/Forbidden/ => 1,
+	],
+	forwardssl => 1,
+	listenssl => 1,
+    },
+    server => {
+	func => \&http_server,
+	ssl => 1,
+    },
+    lengths => [251, 16384, 0, 1, 3, 4, 5],
+    md5 => [
+	"bc3a3f39af35fe5b1687903da2b00c7f",
+	"52afece07e61264c3087ddf52f729376",
+	"d41d8cd98f00b204e9800998ecf8427e",
+	"68b329da9893e34099c7d8ad5cb9c940",
+	"0ade138937c4b9cb36a28e2edb6485fc",
+	"e686f5db1f8610b65f98f3718e1a5b72",
+	"e5870c1091c20ed693976546d23b4841",
+	"bc3a3f39af35fe5b1687903da2b00c7f",
+	"52afece07e61264c3087ddf52f729376",
+	"d41d8cd98f00b204e9800998ecf8427e",
+	"68b329da9893e34099c7d8ad5cb9c940",
+	"0ade138937c4b9cb36a28e2edb6485fc",
+	"e686f5db1f8610b65f98f3718e1a5b72",
+	"e5870c1091c20ed693976546d23b4841",
+    ],
+);
+
+1;
blob - /dev/null
blob + 0cd1600dc53e7ad4a54b1c9c911bab2ce79febac (mode 644)
--- /dev/null
+++ regress/args-https-headline-callback.pl
@@ -0,0 +1,58 @@
+# test persistent https connection over http relay invoking the callback
+# The client writes a bad header line in the second request.
+# Check that the relay handles the input after the error correctly.
+
+use strict;
+use warnings;
+
+my @lengths = (4, 3);
+our %args = (
+    client => {
+	func => sub {
+	    my $self = shift;
+	    print <<'EOF';
+PUT /4 HTTP/1.1
+Host: foo.bar
+Content-Length: 4
+
+123
+PUT /3 HTTP/1.1
+XXX
+Host: foo.bar
+Content-Length: 3
+
+12
+EOF
+	    print STDERR "LEN: 4\n";
+	    print STDERR "LEN: 3\n";
+	    # relayd does not forward the first request if the second one
+	    # is invalid.  So do not expect any response.
+	    #http_response($self, "without len");
+	},
+	ssl => 1,
+	http_vers => ["1.1"],
+	lengths => \@lengths,
+	method => "PUT",
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request header log foo",
+	    "match response header log bar",
+	],
+	forwardssl => 1,
+	listenssl => 1,
+	loggrep => {
+	    qr/, malformed, PUT/ => 1,
+	},
+    },
+    server => {
+	func => \&http_server,
+	ssl => 1,
+	# The server does not get any connection.
+	noserver => 1,
+	nocheck => 1,
+    },
+    lengths => \@lengths,
+);
+
+1;
blob - /dev/null
blob + 9ae14f06d3cfc4f1c6d740ead38f769a81ea7bb5 (mode 644)
--- /dev/null
+++ regress/args-https-headline-close.pl
@@ -0,0 +1,34 @@
+# test https connection over http relay
+# The client writes an incomplete header line and closes the connection.
+# Check that the relay establishes and also closes the session.
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => sub {
+	    print "GET ";  # missing new line
+	},
+	ssl => 1,
+	nocheck => 1,
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request header log foo",
+	    "match response header log bar",
+	],
+	forwardssl => 1,
+	listenssl => 1,
+	loggrep => {
+	    qr/session 1 established/ => 1,
+	    qr/session 1 .*, done/ => 1,
+	},
+    },
+    server => {
+	noserver => 1,
+	nocheck => 1,
+    },
+);
+
+1;
blob - /dev/null
blob + 5db6c695ab93617b601e94a9f31031edab0a6a11 (mode 644)
--- /dev/null
+++ regress/args-https-inspect.pl
@@ -0,0 +1,27 @@
+# test https connection over http relay with TLS inspection
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&http_client,
+	ssl => 1,
+	loggrep => 'Issuer.*/OU=ca/',
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request header log foo",
+	    "match response header log bar",
+	],
+	inspectssl => 1,
+    },
+    server => {
+	func => \&http_server,
+	ssl => 1,
+    },
+    len => 251,
+    md5 => "bc3a3f39af35fe5b1687903da2b00c7f",
+);
+
+1;
blob - /dev/null
blob + 32aa8fd6357fa34a561a4a1a8017f03cb28564eb (mode 644)
--- /dev/null
+++ regress/args-https-multi.pl
@@ -0,0 +1,29 @@
+# test 50 https get with length 1 over http relay
+
+use strict;
+use warnings;
+
+my @lengths = map { 1 } (1..50);
+our %args = (
+    client => {
+	func => \&http_client,
+	lengths => \@lengths,
+	method => "GET",
+	ssl => 1,
+    },
+    relayd => {
+	protocol => [ "http" ],
+	loggrep => {
+	    qr/, (?:done|last write \(done\)), GET/ => (1 + @lengths),
+	},
+	forwardssl => 1,
+	listenssl => 1,
+    },
+    server => {
+	func => \&http_server,
+	ssl => 1,
+    },
+    lengths => \@lengths,
+);
+
+1;
blob - /dev/null
blob + 30e690dae96f24996fbc524ceefb970f67329b05 (mode 644)
--- /dev/null
+++ regress/args-https-persistent.pl
@@ -0,0 +1,49 @@
+# test persistent https 1.1 connection over http relay
+
+use strict;
+use warnings;
+
+my @lengths = (251, 16384, 0, 1, 2, 3, 4, 5);
+our %args = (
+    client => {
+	func => \&http_client,
+	lengths => \@lengths,
+	ssl => 1,
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request header log foo",
+	    "match response header log bar",
+	],
+	loggrep => {
+	    qr/, (?:done|last write \(done\))/ => (1 + @lengths),
+	},
+	forwardssl => 1,
+	listenssl => 1,
+    },
+    server => {
+	func => \&http_server,
+	ssl => 1,
+    },
+    lengths => \@lengths,
+    md5 => [
+	"bc3a3f39af35fe5b1687903da2b00c7f",
+	"52afece07e61264c3087ddf52f729376",
+	"d41d8cd98f00b204e9800998ecf8427e",
+	"68b329da9893e34099c7d8ad5cb9c940",
+	"897316929176464ebc9ad085f31e7284",
+	"0ade138937c4b9cb36a28e2edb6485fc",
+	"e686f5db1f8610b65f98f3718e1a5b72",
+	"e5870c1091c20ed693976546d23b4841",
+	"bc3a3f39af35fe5b1687903da2b00c7f",
+	"52afece07e61264c3087ddf52f729376",
+	"d41d8cd98f00b204e9800998ecf8427e",
+	"68b329da9893e34099c7d8ad5cb9c940",
+	"897316929176464ebc9ad085f31e7284",
+	"0ade138937c4b9cb36a28e2edb6485fc",
+	"e686f5db1f8610b65f98f3718e1a5b72",
+	"e5870c1091c20ed693976546d23b4841",
+    ],
+);
+
+1;
blob - /dev/null
blob + 74ea56ee5921b7d7cac9ba8adb973b7b3b429700 (mode 644)
--- /dev/null
+++ regress/args-https-put-multi.pl
@@ -0,0 +1,29 @@
+# test 50 https put with length 1 over http relay
+
+use strict;
+use warnings;
+
+my @lengths = map { 1 } (1..50);
+our %args = (
+    client => {
+	func => \&http_client,
+	lengths => \@lengths,
+	method => "PUT",
+	ssl => 1,
+    },
+    relayd => {
+	protocol => [ "http" ],
+	loggrep => {
+	    qr/, (?:done|last write \(done\)), PUT/ => (1 + @lengths),
+	},
+	forwardssl => 1,
+	listenssl => 1,
+    },
+    server => {
+	func => \&http_server,
+	ssl => 1,
+    },
+    lengths => \@lengths,
+);
+
+1;
blob - /dev/null
blob + e0f87f12ab33dabf014937bca844e8bcc67a6356 (mode 644)
--- /dev/null
+++ regress/args-https-put.pl
@@ -0,0 +1,50 @@
+# test persistent https 1.1 put over http relay
+
+use strict;
+use warnings;
+
+my @lengths = (251, 16384, 0, 1, 2, 3, 4, 5);
+our %args = (
+    client => {
+	func => \&http_client,
+	lengths => \@lengths,
+	method => "PUT",
+	ssl => 1,
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request header log foo",
+	    "match response header log bar",
+	],
+	loggrep => {
+	    qr/, (?:done|last write \(done\))/ => (1 + @lengths),
+	},
+	forwardssl => 1,
+	listenssl => 1,
+    },
+    server => {
+	func => \&http_server,
+	ssl => 1,
+    },
+    lengths => \@lengths,
+    md5 => [
+	"bc3a3f39af35fe5b1687903da2b00c7f",
+	"52afece07e61264c3087ddf52f729376",
+	"d41d8cd98f00b204e9800998ecf8427e",
+	"68b329da9893e34099c7d8ad5cb9c940",
+	"897316929176464ebc9ad085f31e7284",
+	"0ade138937c4b9cb36a28e2edb6485fc",
+	"e686f5db1f8610b65f98f3718e1a5b72",
+	"e5870c1091c20ed693976546d23b4841",
+	"bc3a3f39af35fe5b1687903da2b00c7f",
+	"52afece07e61264c3087ddf52f729376",
+	"d41d8cd98f00b204e9800998ecf8427e",
+	"68b329da9893e34099c7d8ad5cb9c940",
+	"897316929176464ebc9ad085f31e7284",
+	"0ade138937c4b9cb36a28e2edb6485fc",
+	"e686f5db1f8610b65f98f3718e1a5b72",
+	"e5870c1091c20ed693976546d23b4841",
+    ],
+);
+
+1;
blob - /dev/null
blob + 6779cc864e4b72da51471d2e90419748c0112c99 (mode 644)
--- /dev/null
+++ regress/args-https.pl
@@ -0,0 +1,29 @@
+# test https connection over http relay
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&http_client,
+	ssl => 1,
+	loggrep => 'Issuer.*/OU=relayd/',
+    },
+    relayd => {
+	protocol => [ "http",
+	    "match request header log foo",
+	    "match response header log bar",
+	],
+	forwardssl => 1,
+	listenssl => 1,
+
+    },
+    server => {
+	func => \&http_server,
+	ssl => 1,
+    },
+    len => 251,
+    md5 => "bc3a3f39af35fe5b1687903da2b00c7f",
+);
+
+1;
blob - /dev/null
blob + d9005e6f00f33b767ce3b0ea8e40e0d76ffc29af (mode 644)
--- /dev/null
+++ regress/args-reverse.pl
@@ -0,0 +1,17 @@
+# test reverse sending from server to client
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&read_char,
+    },
+    server => {
+	func => \&write_char,
+    },
+    len => 251,
+    md5 => "bc3a3f39af35fe5b1687903da2b00c7f",
+);
+
+1;
blob - /dev/null
blob + 45099a1674b3a91061be2125a808a984162b578f (mode 644)
--- /dev/null
+++ regress/args-ssl-client-verify-fail.pl
@@ -0,0 +1,33 @@
+# test client ssl certificate verification
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	ssl => 1,
+	offertlscert => 0,
+	# no-op func as we cannot connect without presenting a client certificate,
+	# hence the default write_char function won't work here and block forever.
+	func => sub {
+		errignore();
+		sleep(2);
+	},
+	dryrun => 1,
+	nocheck => 1,
+    },
+    relayd => {
+	listenssl => 1,
+	verifyclient => 1,
+	loggrep => {
+		qr/peer did not return a certificate/ => 1,
+		qr/tls session \d+ established/ => 0,
+	},
+    },
+    server => {
+	noserver => 1,
+	nocheck => 1,
+    },
+);
+
+1;
blob - /dev/null
blob + c5cc3110724f1fb08eb66ca759e45e684d0abaf5 (mode 644)
--- /dev/null
+++ regress/args-ssl-client-verify.pl
@@ -0,0 +1,19 @@
+# test client ssl certificate verification
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	ssl => 1,
+	offertlscert => 1,
+    },
+    relayd => {
+	listenssl => 1,
+	verifyclient => 1,
+    },
+    len => 251,
+    md5 => "bc3a3f39af35fe5b1687903da2b00c7f",
+);
+
+1;
blob - /dev/null
blob + 4c08abeea68077f1f823d915d11329d079b0d056 (mode 644)
--- /dev/null
+++ regress/args-ssl-client.pl
@@ -0,0 +1,17 @@
+# test client ssl connection
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	ssl => 1,
+    },
+    relayd => {
+	listenssl => 1,
+    },
+    len => 251,
+    md5 => "bc3a3f39af35fe5b1687903da2b00c7f",
+);
+
+1;
blob - /dev/null
blob + b4059a7bf259690fca5a40e6140af541389fd56c (mode 644)
--- /dev/null
+++ regress/args-ssl-ec.pl
@@ -0,0 +1,22 @@
+# test ssl connection with EC key
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	ssl => 1,
+	loggrep => 'Issuer.*/OU=relayd/',
+    },
+    relayd => {
+	forwardssl => 1,
+	listenssl => 1,
+    },
+    server => {
+	ssl => 1,
+    },
+    len => 251,
+    md5 => "bc3a3f39af35fe5b1687903da2b00c7f",
+);
+
+1;
blob - /dev/null
blob + 3c360494eaa1d488dfa4d9ee5e9c06f7218e96de (mode 644)
--- /dev/null
+++ regress/args-ssl-inspect.pl
@@ -0,0 +1,21 @@
+# test both client and server ssl connection with TLS inspection
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	ssl => 1,
+	loggrep => 'Issuer.*/OU=ca/',
+    },
+    relayd => {
+	inspectssl => 1,
+    },
+    server => {
+	ssl => 1,
+    },
+    len => 251,
+    md5 => "bc3a3f39af35fe5b1687903da2b00c7f",
+);
+
+1;
blob - /dev/null
blob + 1f5e4106f6731b50913b79ee18b7e81d0d36e651 (mode 644)
--- /dev/null
+++ regress/args-ssl-server.pl
@@ -0,0 +1,17 @@
+# test server ssl connection
+
+use strict;
+use warnings;
+
+our %args = (
+    relayd => {
+	forwardssl => 1,
+    },
+    server => {
+	ssl => 1,
+    },
+    len => 251,
+    md5 => "bc3a3f39af35fe5b1687903da2b00c7f",
+);
+
+1;
blob - /dev/null
blob + a277f24b9aa4d7eb4c7e9ab1c49323fc40222bc2 (mode 644)
--- /dev/null
+++ regress/args-ssl.pl
@@ -0,0 +1,22 @@
+# test both client and server ssl connection
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	ssl => 1,
+	loggrep => 'Issuer.*/OU=relayd/',
+    },
+    relayd => {
+	forwardssl => 1,
+	listenssl => 1,
+    },
+    server => {
+	ssl => 1,
+    },
+    len => 251,
+    md5 => "bc3a3f39af35fe5b1687903da2b00c7f",
+);
+
+1;
blob - /dev/null
blob + 0e18d2f14abd3cdccec52a88311f4e4f273660f3 (mode 644)
--- /dev/null
+++ regress/args-timeget-http.pl
@@ -0,0 +1,25 @@
+# test that 2 seconds timeout does not occur while server writes for 4 seconds
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&http_client,
+	len => 5,
+	method => "GET",
+	timefile => "",
+    },
+    relayd => {
+	relay => [ "session timeout 2" ],
+	loggrep => { qr/(buffer event|splice) timeout/ => 0 },
+    },
+    server => {
+	func => \&http_server,
+	sleep => 1,
+	method => "GET",
+    },
+    len => 5,
+);
+
+1;
blob - /dev/null
blob + a2e381b5243936fac8c81acb66e9fb0712686587 (mode 644)
--- /dev/null
+++ regress/args-timeget-https.pl
@@ -0,0 +1,30 @@
+# test that 2 seconds timeout does not occur while server writes for 4 seconds
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&http_client,
+	len => 5,
+	method => "GET",
+	timefile => "",
+	ssl => 1,
+	loggrep => 'Issuer.*/OU=relayd/',
+    },
+    relayd => {
+	relay => [ "session timeout 2" ],
+	loggrep => { qr/buffer event timeout/ => 0 },
+	forwardssl => 1,
+	listenssl => 1,
+    },
+    server => {
+	func => \&http_server,
+	sleep => 1,
+	method => "GET",
+	ssl => 1,
+    },
+    len => 5,
+);
+
+1;
blob - /dev/null
blob + 5f2fcc7ed59bd2ca048ff4c63ed2cb5d3d8cbdab (mode 644)
--- /dev/null
+++ regress/args-timeget-ssl.pl
@@ -0,0 +1,28 @@
+# test that 2 seconds timeout does not occur while server writes for 4 seconds
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&read_char,
+	timefile => "",
+	ssl => 1,
+	loggrep => 'Issuer.*/OU=relayd/',
+    },
+    relayd => {
+	relay => [ "session timeout 2" ],
+	loggrep => { qr/buffer event timeout/ => 0 },
+	forwardssl => 1,
+	listenssl => 1,
+    },
+    server => {
+	func => \&write_char,
+	len => 5,
+	sleep => 1,
+	ssl => 1,
+    },
+    len => 5,
+);
+
+1;
blob - /dev/null
blob + ec915b86ab5f11ce1caa1914831441f4ab8dc387 (mode 644)
--- /dev/null
+++ regress/args-timeget.pl
@@ -0,0 +1,23 @@
+# test that 2 seconds timeout does not occur while server writes for 4 seconds
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&read_char,
+	timefile => "",
+    },
+    relayd => {
+	relay => [ "session timeout 2" ],
+	loggrep => { qr/(buffer event|splice) timeout/ => 0 },
+    },
+    server => {
+	func => \&write_char,
+	len => 5,
+	sleep => 1,
+    },
+    len => 5,
+);
+
+1;
blob - /dev/null
blob + 1bc155761b7870f87ec5b599b75641a8ab4a0fae (mode 644)
--- /dev/null
+++ regress/args-timein-http.pl
@@ -0,0 +1,30 @@
+# test that 3 seconds timeout does not occur within 2 seconds idle in http
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&http_client,
+	len => 5,
+	timefile => "",
+    },
+    relayd => {
+	protocol => [ "http" ],
+	relay => [ "session timeout 3" ],
+	loggrep => { qr/(buffer event|splice) timeout/ => 0 },
+    },
+    server => {
+	func => sub {
+	    errignore();
+	    http_server(@_);
+	    sleep 2;
+	    write_char(@_, 4);
+	},
+	sleep => 1,
+	nocheck => 1,
+    },
+    len => 9,
+);
+
+1;
blob - /dev/null
blob + 89e14493c4ff5a546774e8692226eeddc532cc08 (mode 644)
--- /dev/null
+++ regress/args-timein-https.pl
@@ -0,0 +1,35 @@
+# test that 3 seconds timeout does not occur within 2 seconds idle in http
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&http_client,
+	len => 5,
+	timefile => "",
+	ssl => 1,
+	loggrep => 'Issuer.*/OU=relayd/',
+    },
+    relayd => {
+	protocol => [ "http" ],
+	relay => [ "session timeout 3" ],
+	loggrep => { qr/buffer event timeout/ => 0 },
+	forwardssl => 1,
+	listenssl => 1,
+    },
+    server => {
+	func => sub {
+	    errignore();
+	    http_server(@_);
+	    sleep 2;
+	    write_char(@_, 4);
+	},
+	sleep => 1,
+	nocheck => 1,
+	ssl => 1,
+    },
+    len => 9,
+);
+
+1;
blob - /dev/null
blob + caa024bcf20b9e512a3a3675342b76ea2df2b575 (mode 644)
--- /dev/null
+++ regress/args-timein-ssl.pl
@@ -0,0 +1,32 @@
+# test that 3 seconds timeout does not occur within 2 seconds idle
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => sub {
+	    errignore();
+	    write_char(@_, 5);
+	    sleep 2;
+	    write_char(@_, 4);
+	},
+	sleep => 1,
+	timefile => "",
+	nocheck => 1,
+	ssl => 1,
+	loggrep => 'Issuer.*/OU=relayd/',
+    },
+    relayd => {
+	relay => [ "session timeout 3" ],
+	loggrep => { qr/buffer event timeout/ => 0 },
+	forwardssl => 1,
+	listenssl => 1,
+    },
+    server => {
+	ssl => 1,
+    },
+    len => 9,
+);
+
+1;
blob - /dev/null
blob + 81071616a880403dfab16c6a45cf07bdaf9c9e0d (mode 644)
--- /dev/null
+++ regress/args-timein.pl
@@ -0,0 +1,25 @@
+# test that 3 seconds timeout does not occur within 2 seconds idle
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => sub {
+	    errignore();
+	    write_char(@_, 5);
+	    sleep 2;
+	    write_char(@_, 4);
+	},
+	sleep => 1,
+	timefile => "",
+	nocheck => 1,
+    },
+    relayd => {
+	relay => [ "session timeout 3" ],
+	loggrep => { qr/(buffer event|splice) timeout/ => 0 },
+    },
+    len => 9,
+);
+
+1;
blob - /dev/null
blob + 38f9b28e96d11b428cf23aee12d7f2dd1353fb6d (mode 644)
--- /dev/null
+++ regress/args-timeout-http.pl
@@ -0,0 +1,31 @@
+# test that 3 seconds timeout occurs within 4 seconds idle in http
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&http_client,
+	len => 5,
+	timefile => "",
+    },
+    relayd => {
+	protocol => [ "http" ],
+	relay => [ "session timeout 3" ],
+	loggrep => { qr/(buffer event|splice) timeout/ => 1 },
+    },
+    server => {
+	func => sub {
+	    errignore();
+	    http_server(@_);
+	    sleep 4;
+	    write_char(@_, 4);
+	},
+	sleep => 1,
+	down => "Broken pipe",
+	nocheck => 1,
+    },
+    len => 5,
+);
+
+1;
blob - /dev/null
blob + 449a5391c772ceb770287662276c2636500b5505 (mode 644)
--- /dev/null
+++ regress/args-timeout-https.pl
@@ -0,0 +1,36 @@
+# test that 3 seconds timeout occurs within 4 seconds idle in http
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&http_client,
+	len => 5,
+	timefile => "",
+	ssl => 1,
+	loggrep => 'Issuer.*/OU=relayd/',
+    },
+    relayd => {
+	protocol => [ "http" ],
+	relay => [ "session timeout 3" ],
+	loggrep => { qr/buffer event timeout/ => 1 },
+	forwardssl => 1,
+	listenssl => 1,
+    },
+    server => {
+	func => sub {
+	    errignore();
+	    http_server(@_);
+	    sleep 4;
+	    write_char(@_, 4);
+	},
+	sleep => 1,
+	down => "Broken pipe",
+	nocheck => 1,
+	ssl => 1,
+    },
+    len => 5,
+);
+
+1;
blob - /dev/null
blob + 3cf7492d9731074fbd95695a9c0ce069921e06dd (mode 644)
--- /dev/null
+++ regress/args-timeout-ssl.pl
@@ -0,0 +1,33 @@
+# test that 3 seconds timeout occurs within 4 seconds idle
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => sub {
+	    errignore();
+	    write_char(@_, 5);
+	    sleep 4;
+	    write_char(@_, 4);
+	},
+	sleep => 1,
+	down => "Broken pipe",
+	timefile => "",
+	nocheck => 1,
+	ssl => 1,
+	loggrep => 'Issuer.*/OU=relayd/',
+    },
+    relayd => {
+	relay => [ "session timeout 3" ],
+	loggrep => { qr/buffer event timeout/ => 1 },
+	forwardssl => 1,
+	listenssl => 1,
+    },
+    server => {
+	ssl => 1,
+    },
+    len => 5,
+);
+
+1;
blob - /dev/null
blob + bf4319fa7967c02582897d3098ecc07a6b42909b (mode 644)
--- /dev/null
+++ regress/args-timeout.pl
@@ -0,0 +1,26 @@
+# test that 3 seconds timeout occurs within 4 seconds idle
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => sub {
+	    errignore();
+	    write_char(@_, 5);
+	    sleep 4;
+	    write_char(@_, 4);
+	},
+	sleep => 1,
+	down => "Broken pipe",
+	timefile => "",
+	nocheck => 1,
+    },
+    relayd => {
+	relay => [ "session timeout 3" ],
+	loggrep => { qr/(buffer event|splice) timeout/ => 1 },
+    },
+    len => 5,
+);
+
+1;
blob - /dev/null
blob + cfe709ab9a3be5032115368b00784fda4275dd99 (mode 644)
--- /dev/null
+++ regress/args-timeput-http.pl
@@ -0,0 +1,25 @@
+# test that 2 seconds timeout does not occur while client writes for 4 seconds
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&http_client,
+	len => 5,
+	sleep => 1,
+	method => "PUT",
+	timefile => "",
+    },
+    relayd => {
+	relay => [ "session timeout 2" ],
+	loggrep => { qr/(buffer event|splice) timeout/ => 0 },
+    },
+    server => {
+	func => \&http_server,
+	method => "PUT",
+    },
+    len => 5,
+);
+
+1;
blob - /dev/null
blob + bdc406bebfd73caf29b82a46835bfb69884621b5 (mode 644)
--- /dev/null
+++ regress/args-timeput-https.pl
@@ -0,0 +1,30 @@
+# test that 2 seconds timeout does not occur while client writes for 4 seconds
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&http_client,
+	len => 5,
+	sleep => 1,
+	method => "PUT",
+	timefile => "",
+	ssl => 1,
+	loggrep => 'Issuer.*/OU=relayd/',
+    },
+    relayd => {
+	relay => [ "session timeout 2" ],
+	loggrep => { qr/buffer event timeout/ => 0 },
+	forwardssl => 1,
+	listenssl => 1,
+    },
+    server => {
+	func => \&http_server,
+	method => "PUT",
+	ssl => 1,
+    },
+    len => 5,
+);
+
+1;
blob - /dev/null
blob + 3ae402dff4a970aafdf0db09e7204124212ea145 (mode 644)
--- /dev/null
+++ regress/args-timeput-ssl.pl
@@ -0,0 +1,27 @@
+# test that 2 seconds timeout does not occur while client writes for 4 seconds
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&write_char,
+	len => 5,
+	sleep => 1,
+	timefile => "",
+	ssl => 1,
+	loggrep => 'Issuer.*/OU=relayd/',
+    },
+    relayd => {
+	relay => [ "session timeout 2" ],
+	loggrep => { qr/buffer event timeout/ => 0 },
+	forwardssl => 1,
+	listenssl => 1,
+    },
+    server => {
+	ssl => 1,
+    },
+    len => 5,
+);
+
+1;
blob - /dev/null
blob + 8322d2e9107423fbfa848c8d356fcf64c07feeec (mode 644)
--- /dev/null
+++ regress/args-timeput.pl
@@ -0,0 +1,20 @@
+# test that 2 seconds timeout does not occur while client writes for 4 seconds
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => \&write_char,
+	len => 5,
+	sleep => 1,
+	timefile => "",
+    },
+    relayd => {
+	relay => [ "session timeout 2" ],
+	loggrep => { qr/(buffer event|splice) timeout/ => 0 },
+    },
+    len => 5,
+);
+
+1;
blob - /dev/null
blob + e8950440aaa1c0e156c678007b972944d59c5c03 (mode 755)
--- /dev/null
+++ regress/direct.pl
@@ -0,0 +1,62 @@
+#!/usr/bin/perl
+#	$OpenBSD: direct.pl,v 1.3 2014/08/18 22:58:19 bluhm Exp $
+
+# Copyright (c) 2010-2014 Alexander Bluhm <bluhm@openbsd.org>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+use strict;
+use warnings;
+use Socket;
+use Socket6;
+
+use Client;
+use Server;
+require 'funcs.pl';
+
+sub usage {
+	die "usage: direct.pl [test-args.pl]\n";
+}
+
+my $testfile;
+our %args;
+if (@ARGV and -f $ARGV[-1]) {
+	$testfile = pop;
+	do $testfile
+	    or die "Do test file $testfile failed: ", $@ || $!;
+}
+
+@ARGV == 0 or usage();
+
+my $s = Server->new(
+    func		=> \&read_char,
+    %{$args{server}},
+    listendomain	=> AF_INET,
+    listenaddr		=> "127.0.0.1",
+);
+my $c = Client->new(
+    func		=> \&write_char,
+    %{$args{client}},
+    connectdomain	=> AF_INET,
+    connectaddr		=> "127.0.0.1",
+    connectport		=> $s->{listenport},
+);
+
+$s->run;
+$c->run->up;
+$s->up;
+
+$c->down;
+$s->down;
+
+check_logs($c, undef, $s, %args);
blob - /dev/null
blob + 694a1c8ae07fe3d89735404489b459b8fc44e169 (mode 644)
--- /dev/null
+++ regress/funcs.pl
@@ -0,0 +1,616 @@
+#	$OpenBSD: funcs.pl,v 1.26 2024/06/14 15:12:57 bluhm Exp $
+
+# Copyright (c) 2010-2021 Alexander Bluhm <bluhm@openbsd.org>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+use strict;
+use warnings;
+use Errno;
+use Digest::MD5;
+use Socket;
+use Socket6;
+use IO::Socket;
+use IO::Socket::IP;
+
+sub find_ports {
+	my %args = @_;
+	my $num    = delete $args{num}    // 1;
+	my $domain = delete $args{domain} // AF_INET;
+	my $addr   = delete $args{addr}   // "127.0.0.1";
+
+	my @sockets = (1..$num);
+	foreach my $s (@sockets) {
+		$s = IO::Socket::IP->new(
+		    Proto  => "tcp",
+		    Family => $domain,
+		    $addr ? (LocalAddr => $addr) : (),
+		) or die "find_ports: create and bind socket failed: $!";
+	}
+	my @ports = map { $_->sockport() } @sockets;
+
+	return @ports;
+}
+
+########################################################################
+# Client funcs
+########################################################################
+
+sub write_syswrite {
+	my $self = shift;
+	my $buf = shift;
+
+	IO::Handle::flush(\*STDOUT);
+	my $size = length($buf);
+	my $len = 0;
+	while ($len < $size) {
+		my $n = syswrite(STDOUT, $buf, $size, $len);
+		if (!defined($n)) {
+			$!{EWOULDBLOCK}
+			    or die ref($self), " syswrite failed: $!";
+			print STDERR "blocked write at $len of $size: $!\n";
+			next;
+		}
+		if ($len + $n != $size) {
+			print STDERR "short write $n at $len of $size\n";
+		}
+		$len += $n;
+	}
+	return $len;
+}
+
+sub write_block {
+	my $self = shift;
+	my $len = shift;
+
+	my $data;
+	my $outb = 0;
+	my $blocks = int($len / 1000);
+	my $rest = $len % 1000;
+
+	for (my $i = 1; $i <= 100 ; $i++) {
+		$data .= "012345678\n";
+	}
+
+	my $opct = 0;
+	for (my $i = 1; $i <= $blocks; $i++) {
+		$outb += write_syswrite($self, $data);
+		my $pct = ($outb / $len) * 100.0;
+		if ($pct >= $opct + 1) {
+			printf(STDERR "%.2f%% $outb/$len\n", $pct);
+			$opct = $pct;
+		}
+	}
+
+	if ($rest>0) {
+		for (my $i = 1; $i < $rest-1 ; $i++) {
+		    $outb += write_syswrite($self, 'r');
+		    my $pct = ($outb / $len) * 100.0;
+		    if ($pct >= $opct + 1) {
+			    printf(STDERR "%.2f%% $outb/$len\n", $pct);
+			    $opct = $pct;
+		    }
+		}
+	}
+	$outb += write_syswrite($self, "\n\n");
+	IO::Handle::flush(\*STDOUT);
+	print STDERR "LEN: ", $outb, "\n";
+}
+
+sub write_char {
+	my $self = shift;
+	my $len = shift // $self->{len} // 251;
+	my $sleep = $self->{sleep};
+
+	if ($self->{fast}) {
+		write_block($self, $len);
+		return;
+	}
+
+	my $ctx = Digest::MD5->new();
+	my $char = '0';
+	for (my $i = 1; $i < $len; $i++) {
+		$ctx->add($char);
+		print $char
+		    or die ref($self), " print failed: $!";
+		if    ($char =~ /9/)  { $char = 'A' }
+		elsif ($char =~ /Z/)  { $char = 'a' }
+		elsif ($char =~ /z/)  { $char = "\n" }
+		elsif ($char =~ /\n/) { print STDERR "."; $char = '0' }
+		else                  { $char++ }
+		if ($self->{sleep}) {
+			IO::Handle::flush(\*STDOUT);
+			sleep $self->{sleep};
+		}
+	}
+	if ($len) {
+		$char = "\n";
+		$ctx->add($char);
+		print $char
+		    or die ref($self), " print failed: $!";
+		print STDERR ".\n";
+	}
+	IO::Handle::flush(\*STDOUT);
+
+	print STDERR "LEN: ", $len, "\n";
+	print STDERR "MD5: ", $ctx->hexdigest, "\n";
+}
+
+sub http_client {
+	my $self = shift;
+
+	unless ($self->{lengths}) {
+		# only a single http request
+		my $len = shift // $self->{len} // 251;
+		my $cookie = $self->{cookie};
+		http_request($self, $len, "1.0", $cookie);
+		http_response($self, $len);
+		return;
+	}
+
+	$self->{http_vers} ||= ["1.1", "1.0"];
+	my $vers = $self->{http_vers}[0];
+	my @lengths = @{$self->{redo}{lengths} || $self->{lengths}};
+	my @cookies = @{$self->{redo}{cookies} || $self->{cookies} || []};
+	while (defined (my $len = shift @lengths)) {
+		my $cookie = shift @cookies || $self->{cookie};
+		eval {
+			http_request($self, $len, $vers, $cookie);
+			http_response($self, $len);
+		};
+		warn $@ if $@;
+		if (@lengths && ($@ || $vers eq "1.0")) {
+			# reconnect and redo the outstanding requests
+			$self->{redo} = {
+			    lengths => \@lengths,
+			    cookies => \@cookies,
+			};
+			return;
+		}
+	}
+	delete $self->{redo};
+	shift @{$self->{http_vers}};
+	if (@{$self->{http_vers}}) {
+		# run the tests again with other persistence
+		$self->{redo} = {
+		    lengths => [@{$self->{lengths}}],
+		    cookies => [@{$self->{cookies} || []}],
+		};
+	}
+}
+
+sub http_request {
+	my ($self, $len, $vers, $cookie) = @_;
+	my $method = $self->{method} || "GET";
+	my %header = %{$self->{header} || {}};
+
+	# encode the requested length or chunks into the url
+	my $path = ref($len) eq 'ARRAY' ? join("/", @$len) : $len;
+	# overwrite path with custom path
+	if (defined($self->{path})) {
+		$path = $self->{path};
+	}
+	my @request = ("$method /$path HTTP/$vers");
+	push @request, "Host: foo.bar" unless defined $header{Host};
+	if ($vers eq "1.1" && $method eq "PUT") {
+		if (ref($len) eq 'ARRAY') {
+			push @request, "Transfer-Encoding: chunked"
+			    if !defined $header{'Transfer-Encoding'};
+		} else {
+			push @request, "Content-Length: $len"
+			    if !defined $header{'Content-Length'};
+		}
+	}
+	foreach my $key (sort keys %header) {
+		my $val = $header{$key};
+		if (ref($val) eq 'ARRAY') {
+			push @request, "$key: $_"
+			    foreach @{$val};
+		} else {
+			push @request, "$key: $val";
+		}
+	}
+	push @request, "Cookie: $cookie" if $cookie;
+	push @request, "";
+	print STDERR map { ">>> $_\n" } @request;
+	print map { "$_\r\n" } @request;
+	if ($method eq "PUT") {
+		if (ref($len) eq 'ARRAY') {
+			if ($vers eq "1.1") {
+				write_chunked($self, @$len);
+			} else {
+				write_char($self, $_) foreach (@$len);
+			}
+		} else {
+			write_char($self, $len);
+		}
+	}
+	IO::Handle::flush(\*STDOUT);
+	# XXX client shutdown seems to be broken in relayd
+	#shutdown(\*STDOUT, SHUT_WR)
+	#    or die ref($self), " shutdown write failed: $!"
+	#    if $vers ne "1.1";
+}
+
+sub http_response {
+	my ($self, $len) = @_;
+	my $method = $self->{method} || "GET";
+
+	my $vers;
+	my $chunked = 0;
+	{
+		local $/ = "\r\n";
+		local $_ = <STDIN>;
+		defined
+		    or die ref($self), " missing http $len response";
+		chomp;
+		print STDERR "<<< $_\n";
+		m{^HTTP/(\d\.\d) 200 OK$}
+		    or die ref($self), " http response not ok"
+		    unless $self->{httpnok};
+		$vers = $1;
+		while (<STDIN>) {
+			chomp;
+			print STDERR "<<< $_\n";
+			last if /^$/;
+			if (/^Content-Length: (.*)/) {
+				if ($self->{httpnok}) {
+					$len = $1;
+				} else {
+					$1 == $len or die ref($self),
+					    " bad content length $1";
+				}
+			}
+			if (/^Transfer-Encoding: chunked$/) {
+				$chunked = 1;
+			}
+		}
+	}
+	if ($method ne 'HEAD') {
+		if ($chunked) {
+			read_chunked($self);
+		} else {
+			undef $len unless defined($vers) && $vers eq "1.1";
+			read_char($self, $len)
+			    if $method eq "GET";
+		}
+	}
+}
+
+sub read_chunked {
+	my $self = shift;
+
+	for (;;) {
+		my $len;
+		{
+			local $/ = "\r\n";
+			local $_ = <STDIN>;
+			defined or die ref($self), " missing chunk size";
+			chomp;
+			print STDERR "<<< $_\n";
+			/^[[:xdigit:]]+$/
+			    or die ref($self), " chunk size not hex: $_";
+			$len = hex;
+		}
+		last unless $len > 0;
+		read_char($self, $len);
+		{
+			local $/ = "\r\n";
+			local $_ = <STDIN>;
+			defined or die ref($self), " missing chunk data end";
+			chomp;
+			print STDERR "<<< $_\n";
+			/^$/ or die ref($self), " no chunk data end: $_";
+		}
+	}
+	{
+		local $/ = "\r\n";
+		while (<STDIN>) {
+			chomp;
+			print STDERR "<<< $_\n";
+			last if /^$/;
+		}
+		defined or die ref($self), " missing chunk trailer";
+	}
+}
+
+sub errignore {
+	$SIG{PIPE} = 'IGNORE';
+	$SIG{__DIE__} = sub {
+		die @_ if $^S;
+		warn "Error ignored";
+		warn @_;
+		IO::Handle::flush(\*STDERR);
+		POSIX::_exit(0);
+	};
+}
+
+########################################################################
+# Common funcs
+########################################################################
+
+sub read_char {
+	my $self = shift;
+	my $max = shift // $self->{max};
+
+	if ($self->{fast}) {
+		read_block($self, $max);
+		return;
+	}
+
+	my $ctx = Digest::MD5->new();
+	my $len = 0;
+	if (defined($max) && $max == 0) {
+		print STDERR "Max\n";
+	} else {
+		while (<STDIN>) {
+			$len += length($_);
+			$ctx->add($_);
+			print STDERR ".";
+			if (defined($max) && $len >= $max) {
+				print STDERR "\nMax";
+				last;
+			}
+		}
+		print STDERR "\n";
+	}
+
+	print STDERR "LEN: ", $len, "\n";
+	print STDERR "MD5: ", $ctx->hexdigest, "\n";
+}
+
+sub read_block {
+	my $self = shift;
+	my $max = shift // $self->{max};
+
+	my $opct = 0;
+	my $ctx = Digest::MD5->new();
+	my $len = 0;
+	for (;;) {
+		if (defined($max) && $len >= $max) {
+			print STDERR "Max\n";
+			last;
+		}
+		my $rlen = POSIX::BUFSIZ;
+		if (defined($max) && $rlen > $max - $len) {
+			$rlen = $max - $len;
+		}
+		defined(my $n = read(STDIN, my $buf, $rlen))
+		    or die ref($self), " read failed: $!";
+		$n or last;
+		$len += $n;
+		$ctx->add($buf);
+		my $pct = ($len / $max) * 100.0;
+		if ($pct >= $opct + 1) {
+			printf(STDERR "%.2f%% $len/$max\n", $pct);
+			$opct = $pct;
+		}
+	}
+
+	print STDERR "LEN: ", $len, "\n";
+	print STDERR "MD5: ", $ctx->hexdigest, "\n";
+}
+
+########################################################################
+# Server funcs
+########################################################################
+
+sub http_server {
+	my $self = shift;
+	my %header = %{$self->{header} || { Server => "Perl/".$^V }};
+	my $cookie = $self->{cookie} || "";
+
+	my($method, $url, $vers);
+	do {
+		my $len;
+		{
+			local $/ = "\r\n";
+			local $_ = <STDIN>;
+			return unless defined $_;
+			chomp;
+			print STDERR "<<< $_\n";
+			($method, $url, $vers) = m{^(\w+) (.*) HTTP/(1\.[01])$}
+			    or die ref($self), " http request not ok";
+			$method =~ /^(GET|HEAD|PUT)$/
+			    or die ref($self), " unknown method: $method";
+			($len, my @chunks) = $url =~ /(\d+)/g;
+			$len = [ $len, @chunks ] if @chunks;
+			while (<STDIN>) {
+				chomp;
+				print STDERR "<<< $_\n";
+				last if /^$/;
+				if ($method eq "PUT" &&
+				    /^Content-Length: (.*)/) {
+					$1 == $len or die ref($self),
+					    " bad content length $1";
+				}
+				$cookie ||= $1 if /^Cookie: (.*)/;
+			}
+		}
+		if ($method eq "PUT" ) {
+			if (ref($len) eq 'ARRAY') {
+				read_chunked($self);
+			} else {
+				read_char($self, $len);
+			}
+		}
+
+		my @response = ("HTTP/$vers 200 OK");
+		$len = defined($len) ? $len : scalar(split /|/,$url);
+		if ($vers eq "1.1" && $method =~ /^(GET|HEAD)$/) {
+			if (ref($len) eq 'ARRAY') {
+				push @response, "Transfer-Encoding: chunked";
+			} else {
+				push @response, "Content-Length: $len";
+			}
+		}
+		foreach my $key (sort keys %header) {
+			my $val = $header{$key};
+			if (ref($val) eq 'ARRAY') {
+				push @response, "$key: $_"
+				    foreach @{$val};
+			} else {
+				push @response, "$key: $val";
+			}
+		}
+		push @response, "Set-Cookie: $cookie" if $cookie;
+		push @response, "";
+
+		print STDERR map { ">>> $_\n" } @response;
+		print map { "$_\r\n" } @response;
+
+		if ($method eq "GET") {
+			if (ref($len) eq 'ARRAY') {
+				if ($vers eq "1.1") {
+					write_chunked($self, @$len);
+				} else {
+					write_char($self, $_) foreach (@$len);
+				}
+			} else {
+				write_char($self, $len);
+			}
+		}
+		IO::Handle::flush(\*STDOUT);
+	} while ($vers eq "1.1");
+	$self->{redo}-- if $self->{redo};
+}
+
+sub write_chunked {
+	my $self = shift;
+	my @chunks = @_;
+
+	foreach my $len (@chunks) {
+		printf STDERR ">>> %x\n", $len;
+		printf "%x\r\n", $len;
+		write_char($self, $len);
+		printf STDERR ">>> \n";
+		print "\r\n";
+	}
+	my @trailer = ("0", "X-Chunk-Trailer: @chunks", "");
+	print STDERR map { ">>> $_\n" } @trailer;
+	print map { "$_\r\n" } @trailer;
+}
+
+########################################################################
+# Script funcs
+########################################################################
+
+sub check_logs {
+	my ($c, $r, $s, %args) = @_;
+
+	return if $args{nocheck};
+
+	check_len($c, $r, $s, %args);
+	check_md5($c, $r, $s, %args);
+	check_loggrep($c, $r, $s, %args);
+	$r->loggrep("lost child")
+	    and die "relayd lost child";
+}
+
+sub array_eq {
+	my ($a, $b) = @_;
+	return if @$a != @$b;
+	for (my $i = 0; $i < @$a; $i++) {
+		return if $$a[$i] ne $$b[$i];
+	}
+	return 1;
+}
+
+sub check_len {
+	my ($c, $r, $s, %args) = @_;
+
+	$args{len} ||= 251 unless $args{lengths};
+
+	my (@clen, @slen);
+	@clen = $c->loggrep(qr/^LEN: /) or die "no client len"
+	    unless $args{client}{nocheck};
+	@slen = $s->loggrep(qr/^LEN: /) or die "no server len"
+	    unless $args{server}{nocheck};
+	!@clen || !@slen || array_eq \@clen, \@slen
+	    or die "client: @clen", "server: @slen", "len mismatch";
+	!defined($args{len}) || !$clen[0] || $clen[0] eq "LEN: $args{len}\n"
+	    or die "client: $clen[0]", "len $args{len} expected";
+	!defined($args{len}) || !$slen[0] || $slen[0] eq "LEN: $args{len}\n"
+	    or die "server: $slen[0]", "len $args{len} expected";
+	my @lengths = map { ref eq 'ARRAY' ? @$_ : $_ }
+	    @{$args{lengths} || []};
+	foreach my $len (@lengths) {
+		unless ($args{client}{nocheck}) {
+			my $clen = shift @clen;
+			$clen eq "LEN: $len\n"
+			    or die "client: $clen", "len $len expected";
+		}
+		unless ($args{server}{nocheck}) {
+			my $slen = shift @slen;
+			$slen eq "LEN: $len\n"
+			    or die "server: $slen", "len $len expected";
+		}
+	}
+}
+
+sub check_md5 {
+	my ($c, $r, $s, %args) = @_;
+
+	my (@cmd5, @smd5);
+	@cmd5 = $c->loggrep(qr/^MD5: /) unless $args{client}{nocheck};
+	@smd5 = $s->loggrep(qr/^MD5: /) unless $args{server}{nocheck};
+	!@cmd5 || !@smd5 || $cmd5[0] eq $smd5[0]
+	    or die "client: $cmd5[0]", "server: $smd5[0]", "md5 mismatch";
+
+	my @md5 = ref($args{md5}) eq 'ARRAY' ? @{$args{md5}} : $args{md5} || ()
+	    or return;
+	foreach my $md5 (@md5) {
+		unless ($args{client}{nocheck}) {
+			my $cmd5 = shift @cmd5
+			    or die "too few md5 in client log";
+			$cmd5 =~ /^MD5: ($md5)$/
+			    or die "client: $cmd5", "md5 $md5 expected";
+		}
+		unless ($args{server}{nocheck}) {
+			my $smd5 = shift @smd5
+			    or die "too few md5 in server log";
+			$smd5 =~ /^MD5: ($md5)$/
+			    or die "server: $smd5", "md5 $md5 expected";
+		}
+	}
+	@cmd5 && ref($args{md5}) eq 'ARRAY'
+	    and die "too many md5 in client log";
+	@smd5 && ref($args{md5}) eq 'ARRAY'
+	    and die "too many md5 in server log";
+}
+
+sub check_loggrep {
+	my ($c, $r, $s, %args) = @_;
+
+	my %name2proc = (client => $c, relayd => $r, server => $s);
+	foreach my $name (qw(client relayd server)) {
+		my $p = $name2proc{$name} or next;
+		my $pattern = $args{$name}{loggrep} or next;
+		$pattern = [ $pattern ] unless ref($pattern) eq 'ARRAY';
+		foreach my $pat (@$pattern) {
+			if (ref($pat) eq 'HASH') {
+				while (my($re, $num) = each %$pat) {
+					my @matches = $p->loggrep($re);
+					@matches == $num
+					    or die "$name matches '@matches': ",
+					    "'$re' => $num";
+				}
+			} else {
+				$p->loggrep($pat)
+				    or die "$name log missing pattern: '$pat'";
+			}
+		}
+	}
+}
+
+1;
blob - /dev/null
blob + 07c34dab0f417f282f78b68c6cd2d50c50393a1c (mode 644)
--- /dev/null
+++ regress/relayd.pl
@@ -0,0 +1,89 @@
+#!/usr/bin/perl
+#	$OpenBSD: relayd.pl,v 1.15 2016/08/25 22:56:13 bluhm Exp $
+
+# Copyright (c) 2010-2014 Alexander Bluhm <bluhm@openbsd.org>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+use strict;
+use warnings;
+use Socket;
+use Socket6;
+
+use Client;
+use Relayd;
+use Server;
+require 'funcs.pl';
+
+sub usage {
+	die "usage: relay.pl copy|splice [test-args.pl]\n";
+}
+
+my $testfile;
+our %args;
+if (@ARGV and -f $ARGV[-1]) {
+	$testfile = pop;
+	do $testfile
+	    or die "Do test file $testfile failed: ", $@ || $!;
+}
+@ARGV == 1 or usage();
+
+my $redo = $args{lengths} && @{$args{lengths}};
+$redo = 0 if $args{client}{http_vers};  # run only one persistent connection
+my($sport, $rport) = find_ports(num => 2);
+my($s, $r, $c);
+$s = Server->new(
+    forward             => $ARGV[0],
+    func                => \&read_char,
+    listendomain        => AF_INET,
+    listenaddr          => "127.0.0.1",
+    listenport          => $sport,
+    redo                => $redo,
+    %{$args{server}},
+    testfile            => $testfile,
+    client              => \$c,
+) unless $args{server}{noserver};
+$r = Relayd->new(
+    forward             => $ARGV[0],
+    listendomain        => AF_INET,
+    listenaddr          => "127.0.0.1",
+    listenport          => $rport,
+    connectdomain       => AF_INET,
+    connectaddr         => "127.0.0.1",
+    connectport         => $sport,
+    %{$args{relayd}},
+    testfile            => $testfile,
+);
+$c = Client->new(
+    forward             => $ARGV[0],
+    func                => \&write_char,
+    connectdomain       => AF_INET,
+    connectaddr         => "127.0.0.1",
+    connectport         => $rport,
+    %{$args{client}},
+    testfile            => $testfile,
+    server              => \$s,
+) unless $args{client}{noclient};
+
+$s->run unless $args{server}{noserver};
+$r->run;
+$r->up;
+$c->run->up unless $args{client}{noclient};
+$s->up unless $args{server}{noserver};
+
+$c->down unless $args{client}{noclient};
+$s->down unless $args{server}{noserver};
+$r->kill_child;
+$r->down;
+
+check_logs($c, $r, $s, %args);
blob - /dev/null
blob + c8f29ca39cb752481c5274a96f1781eb4e49fab4 (mode 644)
--- /dev/null
+++ regress/remote.pl
@@ -0,0 +1,145 @@
+#!/usr/bin/perl
+#	$OpenBSD: remote.pl,v 1.9 2016/08/25 22:56:13 bluhm Exp $
+
+# Copyright (c) 2010-2014 Alexander Bluhm <bluhm@openbsd.org>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+use strict;
+use warnings;
+use File::Basename;
+use File::Copy;
+use Socket;
+use Socket6;
+
+use Client;
+use Relayd;
+use Server;
+use Remote;
+require 'funcs.pl';
+
+sub usage {
+	die <<"EOF";
+usage:
+    remote.pl localport remoteaddr remoteport [test-args.pl]
+	Run test with local client and server.  Remote relayd
+	forwarding from remoteaddr remoteport to server localport
+	has to be started manually.
+    remote.pl copy|splice listenaddr connectaddr connectport [test-args.pl]
+	Only start remote relayd.
+    remote.pl copy|splice localaddr remoteaddr remotessh [test-args.pl]
+	Run test with local client and server.  Remote relayd is
+	started automatically with ssh on remotessh.
+EOF
+}
+
+my $testfile;
+our %args;
+if (@ARGV and -f $ARGV[-1]) {
+	$testfile = pop;
+	do $testfile
+	    or die "Do test file $testfile failed: ", $@ || $!;
+}
+my $mode =
+	@ARGV == 3 && $ARGV[0] =~ /^\d+$/ && $ARGV[2] =~ /^\d+$/ ? "manual" :
+	@ARGV == 4 && $ARGV[1] !~ /^\d+$/ && $ARGV[3] =~ /^\d+$/ ? "relay"  :
+	@ARGV == 4 && $ARGV[1] !~ /^\d+$/ && $ARGV[3] !~ /^\d+$/ ? "auto"   :
+	usage();
+
+my($s, $r, $c);
+if ($mode eq "relay") {
+	my($rport) = find_ports(num => 1);
+	$r = Relayd->new(
+	    forward             => $ARGV[0],
+	    %{$args{relayd}},
+	    listendomain        => AF_INET,
+	    listenaddr          => $ARGV[1],
+	    listenport          => $rport,
+	    connectdomain       => AF_INET,
+	    connectaddr         => $ARGV[2],
+	    connectport         => $ARGV[3],
+	    logfile             => dirname($0)."/remote.log",
+	    conffile            => dirname($0)."/relayd.conf",
+	    testfile            => $testfile,
+	);
+	open(my $log, '<', $r->{logfile})
+	    or die "Remote log file open failed: $!";
+	$SIG{__DIE__} = sub {
+		die @_ if $^S;
+		copy($log, \*STDERR);
+		warn @_;
+		exit 255;
+	};
+	copy($log, \*STDERR);
+	$r->run;
+	copy($log, \*STDERR);
+	$r->up;
+	copy($log, \*STDERR);
+	print STDERR "listen sock: $ARGV[1] $rport\n";
+	<STDIN>;
+	copy($log, \*STDERR);
+	print STDERR "stdin closed\n";
+	$r->kill_child;
+	$r->down;
+	copy($log, \*STDERR);
+
+	exit;
+}
+
+my $redo = $args{lengths} && @{$args{lengths}};
+$redo = 0 if $args{client}{http_vers};  # run only one persistent connection
+$s = Server->new(
+    forward             => $ARGV[0],
+    func                => \&read_char,
+    redo                => $redo,
+    %{$args{server}},
+    listendomain        => AF_INET,
+    listenaddr          => ($mode eq "auto" ? $ARGV[1] : undef),
+    listenport          => ($mode eq "manual" ? $ARGV[0] : undef),
+    testfile            => $testfile,
+    client              => \$c,
+) unless $args{server}{noserver};
+if ($mode eq "auto") {
+	$r = Remote->new(
+	    forward             => $ARGV[0],
+	    logfile             => "relayd.log",
+	    %{$args{relayd}},
+	    remotessh           => $ARGV[3],
+	    listenaddr          => $ARGV[2],
+	    connectaddr         => $ARGV[1],
+	    connectport         => $s ? $s->{listenport} : 1,
+	    testfile            => $testfile,
+	);
+	$r->run->up;
+}
+$c = Client->new(
+    forward             => $ARGV[0],
+    func                => \&write_char,
+    %{$args{client}},
+    connectdomain       => AF_INET,
+    connectaddr         => ($mode eq "manual" ? $ARGV[1] : $r->{listenaddr}),
+    connectport         => ($mode eq "manual" ? $ARGV[2] : $r->{listenport}),
+    testfile            => $testfile,
+    server              => \$s,
+) unless $args{client}{noclient};
+
+$s->run unless $args{server}{noserver};
+$c->run->up unless $args{client}{noclient};
+$s->up unless $args{server}{noserver};
+
+$c->down unless $args{client}{noclient};
+$s->down unless $args{server}{noserver};
+$r->close_child;
+$r->down;
+
+check_logs($c, $r, $s, %args);