Commit Diff


commit - 141a5f1e310cfcf1ce910dd6991641b2383779c4
commit + 432905dcdca8c4c7e19cd39c66a6a88739ada1a8
blob - /dev/null
blob + 964e6d5616441d99dcd4761954966c13f7f08d39 (mode 644)
--- /dev/null
+++ regress/Makefile
@@ -0,0 +1,5 @@
+#	$OpenBSD: Makefile,v 1.2 2015/07/16 16:35:57 reyk Exp $
+
+SUBDIR += patterns tests
+
+.include <bsd.subdir.mk>
blob - /dev/null
blob + 9e4f6a9f9746f439479281212106bd56f9014f0f (mode 644)
--- /dev/null
+++ regress/patterns/Makefile
@@ -0,0 +1,32 @@
+#	$OpenBSD: Makefile,v 1.2 2015/06/23 19:33:06 reyk Exp $
+
+HTTPDSRC =	${.CURDIR}/../../
+
+.PATH:	${HTTPDSRC}
+
+REGRESS_TARGETS=	test-patterns
+
+CLEANFILES +=		patterns-tester
+
+#LUA?=			lua53
+.ifdef LUA
+REGRESS_TARGETS+=	test-patterns-lua
+.endif
+
+patterns-tester: patterns-tester.c patterns.c patterns.h
+	${CC} -o $@ ${CFLAGS} ${.CURDIR}/patterns-tester.c ${HTTPDSRC}/patterns.c -I${HTTPDSRC}
+
+test-patterns: patterns-tester test-patterns.out test-patterns.in
+	cat ${.CURDIR}/test-patterns.in | grep -v '^#' |			\
+	while IFS='	' read string pattern comments ; do 			\
+		./patterns-tester "$${string}" "$${pattern}" 2>&1 || true;	\
+	done | diff -I 'OpenBSD' -u ${.CURDIR}/test-patterns.out -
+
+test-patterns-lua: patterns-tester.lua test-patterns-lua.out test-patterns.in
+	cat ${.CURDIR}/test-patterns.in | grep -v '^#' |			\
+	while IFS='	' read string pattern comments ; do 			\
+		${LUA} ${.CURDIR}/patterns-tester.lua "$${string}" "$${pattern}" 2>&1 || true;	\
+	done | sed "s/.*\/patterns\-tester\.lua/X_PATTERNS_TESTER_X/g" | \
+	diff -I 'OpenBSD' -u ${.CURDIR}/test-patterns-lua.out -
+
+.include <bsd.regress.mk>
blob - /dev/null
blob + 0a3fc2a7b562ab1fd4873e14b6352e396ddcc633 (mode 644)
--- /dev/null
+++ regress/patterns/patterns-tester.c
@@ -0,0 +1,93 @@
+/* $OpenBSD: patterns-tester.c,v 1.2 2025/05/26 06:18:49 anton Exp $ */
+/*
+ * Copyright (c) 2015 Sebastien Marie <semarie@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.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <err.h>
+#include <string.h>
+
+#include "patterns.h"
+
+static void read_string(char *, size_t);
+static void read_string_stop(void);
+
+static void
+read_string(char *buf, size_t len)
+{
+	size_t i;
+
+	/* init */
+	bzero(buf, len);
+
+	/* read */
+	if (fgets(buf, len, stdin) == NULL)
+		err(1, "fgets");
+
+	/* strip '\n' */
+	i = strnlen(buf, len);
+	if (i != 0)
+		buf[i-1] = '\0';
+}
+
+static void
+read_string_stop()
+{
+	if (getchar() != EOF)
+		errx(1, "read_string_stop: too many input");
+}
+
+int
+main(int argc, char *argv[])
+{
+	char string[1024];
+	char pattern[1024];
+	struct str_match m;
+	const char *errstr = NULL;
+	int ret;
+	size_t i;
+
+	/* read testcase */
+	if (argc != 3) {
+		/* from stdin (useful for afl) */
+		read_string(string, sizeof(string));
+		read_string(pattern, sizeof(pattern));
+		read_string_stop();
+	} else {
+		/* from arguments */
+		strlcpy(string, argv[1], sizeof(string));
+		strlcpy(pattern, argv[2], sizeof(pattern));
+	}
+
+	/* print testcase */
+	printf("string='%s'\n", string);
+	printf("pattern='%s'\n", pattern);
+
+	/* test it ! */
+	ret = str_match(string, pattern, &m, &errstr);
+	if (errstr != NULL)
+		errx(1, "str_match: %s", errstr);
+
+	/* print result */
+	printf("ret=%d num=%d\n", ret, m.sm_nmatch);
+	for (i=0; i<m.sm_nmatch; i++) {
+		printf("%ld: %s\n", i, m.sm_match[i]);
+	}
+
+	str_match_free(&m);
+
+	return (EXIT_SUCCESS);
+}
blob - /dev/null
blob + 8972aad19100224feff1507d4cf97442d30616f1 (mode 644)
--- /dev/null
+++ regress/patterns/patterns-tester.d
@@ -0,0 +1,27 @@
+patterns-tester: \
+  /home/rsadowski/src/rsadowski.gothub.org/httpd/regress/patterns/../..//patterns.c \
+  /usr/include/sys/types.h /usr/include/sys/cdefs.h \
+  /usr/include/machine/cdefs.h /usr/include/sys/endian.h \
+  /usr/include/sys/_endian.h /usr/include/sys/_types.h \
+  /usr/include/machine/_types.h /usr/include/machine/endian.h \
+  /usr/include/ctype.h /usr/include/errno.h /usr/include/sys/errno.h \
+  /usr/include/stddef.h /usr/include/sys/_null.h /usr/include/stdlib.h \
+  /usr/include/string.h /usr/include/strings.h \
+  /home/rsadowski/src/rsadowski.gothub.org/httpd/regress/patterns/../../patterns.h
+/usr/include/sys/types.h:
+/usr/include/sys/cdefs.h:
+/usr/include/machine/cdefs.h:
+/usr/include/sys/endian.h:
+/usr/include/sys/_endian.h:
+/usr/include/sys/_types.h:
+/usr/include/machine/_types.h:
+/usr/include/machine/endian.h:
+/usr/include/ctype.h:
+/usr/include/errno.h:
+/usr/include/sys/errno.h:
+/usr/include/stddef.h:
+/usr/include/sys/_null.h:
+/usr/include/stdlib.h:
+/usr/include/string.h:
+/usr/include/strings.h:
+/home/rsadowski/src/rsadowski.gothub.org/httpd/regress/patterns/../../patterns.h:
blob - /dev/null
blob + 5bc476a1a4b7cf4f39846599f80eb052232480cc (mode 644)
--- /dev/null
+++ regress/patterns/patterns-tester.lua
@@ -0,0 +1,3 @@
+-- $OpenBSD: patterns-tester.lua,v 1.1 2015/06/23 18:03:09 semarie Exp $
+print(string.format("string='%s'\npattern='%s'", arg[1], arg[2]));
+print(string.match(arg[1], arg[2]));
blob - /dev/null
blob + b1549258444401fa3dbfd12521b0a262416f0033 (mode 644)
--- /dev/null
+++ regress/patterns/test-patterns-lua.out
@@ -0,0 +1,127 @@
+string='/page/51'
+pattern='^/(%a+)/(%d+)$'
+page	51
+string='/Apage/51'
+pattern='/[^%d][%w%u][^%c]+()[%d]+'
+9
+string='/^page/51'
+pattern='/^(.a.e)/(.)'
+page	5
+string='/page/page-51'
+pattern='/(.*)/%1-(%d+)'
+page	51
+string='/page/[51]'
+pattern='/page/(%b[])'
+[51]
+string=':-]'
+pattern=']+'
+]
+string=':-)'
+pattern='[)]+'
+)
+string='/page/51'
+pattern='$^'
+nil
+string='1234567890'
+pattern='([2-5]-)'
+
+string='****'
+pattern='^**$'
+****
+string='xxxx'
+pattern='^x*$'
+xxxx
+string='/page/51'
+pattern='no-%d-match'
+nil
+string='/page/page-51'
+pattern='/(.*)/%9-(%d+)'
+X_PATTERNS_TESTER_X:3: invalid capture index %9
+stack traceback:
+	[C]: in function 'string.match'
+X_PATTERNS_TESTER_X:3: in main chunk
+	[C]: in ?
+string=':-)'
+pattern=')+'
+X_PATTERNS_TESTER_X:3: invalid pattern capture
+stack traceback:
+	[C]: in function 'string.match'
+X_PATTERNS_TESTER_X:3: in main chunk
+	[C]: in ?
+string='/page/51'
+pattern='/page/51('
+X_PATTERNS_TESTER_X:3: unfinished capture
+stack traceback:
+	[C]: in function 'string.match'
+X_PATTERNS_TESTER_X:3: in main chunk
+	[C]: in ?
+string='/page/51'
+pattern='/page/51%'
+X_PATTERNS_TESTER_X:3: malformed pattern (ends with '%')
+stack traceback:
+	[C]: in function 'string.match'
+X_PATTERNS_TESTER_X:3: in main chunk
+	[C]: in ?
+string='/page/51'
+pattern='/page/[51'
+X_PATTERNS_TESTER_X:3: malformed pattern (missing ']')
+stack traceback:
+	[C]: in function 'string.match'
+X_PATTERNS_TESTER_X:3: in main chunk
+	[C]: in ?
+string='/page/(51)'
+pattern='/page/%b('
+X_PATTERNS_TESTER_X:3: malformed pattern (missing arguments to '%b')
+stack traceback:
+	[C]: in function 'string.match'
+X_PATTERNS_TESTER_X:3: in main chunk
+	[C]: in ?
+string='/page/51'
+pattern='()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()'
+X_PATTERNS_TESTER_X:3: too many captures
+stack traceback:
+	[C]: in function 'string.match'
+X_PATTERNS_TESTER_X:3: in main chunk
+	[C]: in ?
+string='/page/51'
+pattern='/page/%f'
+X_PATTERNS_TESTER_X:3: missing '[' after '%f' in pattern
+stack traceback:
+	[C]: in function 'string.match'
+X_PATTERNS_TESTER_X:3: in main chunk
+	[C]: in ?
+string='/page/51'
+pattern='/page%f/51'
+X_PATTERNS_TESTER_X:3: missing '[' after '%f' in pattern
+stack traceback:
+	[C]: in function 'string.match'
+X_PATTERNS_TESTER_X:3: in main chunk
+	[C]: in ?
+string='q*********************************'
+pattern='*************************************q'
+X_PATTERNS_TESTER_X:3: max repetition items
+stack traceback:
+	[C]: in function 'string.match'
+X_PATTERNS_TESTER_X:3: in main chunk
+	[C]: in ?
+string='q+++++++++++++++++++++++++++++++++'
+pattern='+++++++++++++++++++++++++++++++++++++q'
+X_PATTERNS_TESTER_X:3: max repetition items
+stack traceback:
+	[C]: in function 'string.match'
+X_PATTERNS_TESTER_X:3: in main chunk
+	[C]: in ?
+string='q---------------------------------'
+pattern='-------------------------------------q'
+X_PATTERNS_TESTER_X:3: max repetition items
+stack traceback:
+	[C]: in function 'string.match'
+X_PATTERNS_TESTER_X:3: in main chunk
+	[C]: in ?
+string='q?????????????????????????????????'
+pattern='?????????????????????????????????????q'
+X_PATTERNS_TESTER_X:3: max repetition items
+stack traceback:
+	[C]: in function 'string.match'
+X_PATTERNS_TESTER_X:3: in main chunk
+	[C]: in ?
blob - /dev/null
blob + a988d81ef1a000a29c7d56ae643b6266fc63af35 (mode 644)
--- /dev/null
+++ regress/patterns/test-patterns.in
@@ -0,0 +1,27 @@
+# $OpenBSD: test-patterns.in,v 1.1 2015/06/23 18:03:09 semarie Exp $
+# string	pattern				comments
+/page/51	^/(%a+)/(%d+)$
+/Apage/51	/[^%d][%w%u][^%c]+()[%d]+	
+/^page/51	/^(.a.e)/(.)
+/page/page-51	/(.*)/%1-(%d+)
+/page/[51]	/page/(%b[])
+:-]		]+
+:-)		[)]+
+/page/51	$^
+1234567890	([2-5]-)
+****		^**$				equiv '[*]*'
+xxxx		^x*$				same as before
+/page/51	no-%d-match			no match
+/page/page-51	/(.*)/%9-(%d+)			invalid capture index
+:-)		)+				invalid pattern capture
+/page/51	/page/51(			unfinished capture
+/page/51	/page/51%			malformed pattern (ends with '%')
+/page/51	/page/[51			malformed pattern (missing ']')
+/page/(51)	/page/%b(			malformed pattern (missing arguments to '%b')
+/page/51	()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()	too many captures
+/page/51	/page/%f			missing '[' after '%f' in pattern
+/page/51	/page%f/51			missing '[' after '%f' in pattern
+q*********************************	*************************************q	max repetition items
+q+++++++++++++++++++++++++++++++++	+++++++++++++++++++++++++++++++++++++q	max repetition items
+q---------------------------------	-------------------------------------q	max repetition items
+q?????????????????????????????????	?????????????????????????????????????q	max repetition items
blob - /dev/null
blob + 9c562794f686bf54a51001314703d507b2e83417 (mode 644)
--- /dev/null
+++ regress/patterns/test-patterns.out
@@ -0,0 +1,99 @@
+# $OpenBSD: test-patterns.out,v 1.2 2015/06/26 10:09:42 semarie Exp $
+string='/page/51'
+pattern='^/(%a+)/(%d+)$'
+ret=0 num=3
+0: /page/51
+1: page
+2: 51
+string='/Apage/51'
+pattern='/[^%d][%w%u][^%c]+()[%d]+'
+ret=0 num=2
+0: /Apage/51
+1: 
+string='/^page/51'
+pattern='/^(.a.e)/(.)'
+ret=0 num=3
+0: /^page/51
+1: page
+2: 5
+string='/page/page-51'
+pattern='/(.*)/%1-(%d+)'
+ret=0 num=3
+0: /page/page-51
+1: page
+2: 51
+string='/page/[51]'
+pattern='/page/(%b[])'
+ret=0 num=2
+0: /page/[51]
+1: [51]
+string=':-]'
+pattern=']+'
+ret=0 num=2
+0: :-]
+1: ]
+string=':-)'
+pattern='[)]+'
+ret=0 num=2
+0: :-)
+1: )
+string='/page/51'
+pattern='$^'
+ret=-1 num=0
+string='1234567890'
+pattern='([2-5]-)'
+ret=0 num=2
+0: 1234567890
+1: 
+string='****'
+pattern='^**$'
+ret=0 num=2
+0: ****
+1: ****
+string='xxxx'
+pattern='^x*$'
+ret=0 num=2
+0: xxxx
+1: xxxx
+string='/page/51'
+pattern='no-%d-match'
+ret=-1 num=0
+patterns-tester: str_match: invalid capture index
+string='/page/page-51'
+pattern='/(.*)/%9-(%d+)'
+patterns-tester: str_match: invalid pattern capture
+string=':-)'
+pattern=')+'
+patterns-tester: str_match: unfinished capture
+string='/page/51'
+pattern='/page/51('
+patterns-tester: str_match: malformed pattern (ends with '%')
+string='/page/51'
+pattern='/page/51%'
+patterns-tester: str_match: malformed pattern (missing ']')
+string='/page/51'
+pattern='/page/[51'
+patterns-tester: str_match: malformed pattern (missing arguments to '%b')
+string='/page/(51)'
+pattern='/page/%b('
+patterns-tester: str_match: too many captures
+string='/page/51'
+pattern='()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()'
+patterns-tester: str_match: missing '[' after '%f' in pattern
+string='/page/51'
+pattern='/page/%f'
+patterns-tester: str_match: missing '[' after '%f' in pattern
+string='/page/51'
+pattern='/page%f/51'
+patterns-tester: str_match: max repetition items
+string='q*********************************'
+pattern='*************************************q'
+patterns-tester: str_match: max repetition items
+string='q+++++++++++++++++++++++++++++++++'
+pattern='+++++++++++++++++++++++++++++++++++++q'
+patterns-tester: str_match: max repetition items
+string='q---------------------------------'
+pattern='-------------------------------------q'
+patterns-tester: str_match: max repetition items
+string='q?????????????????????????????????'
+pattern='?????????????????????????????????????q'
blob - /dev/null
blob + e4730a60e833222e6dd89582ad5d942180a8682c (mode 644)
--- /dev/null
+++ regress/tests/Client.pm
@@ -0,0 +1,79 @@
+#	$OpenBSD: Client.pm,v 1.3 2021/12/22 15:54:01 bluhm Exp $
+
+# Copyright (c) 2010-2021 Alexander Bluhm <bluhm@openbsd.org>
+# Copyright (c) 2015 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.
+
+use strict;
+use warnings;
+
+package Client;
+use parent 'Proc';
+use Carp;
+use Socket;
+use Socket6;
+use IO::Socket;
+use IO::Socket::SSL;
+
+sub new {
+	my $class = shift;
+	my %args = @_;
+	$args{chroot} ||= ".";
+	$args{logfile} ||= $args{chroot}."/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->{tls} ? "IO::Socket::SSL" : "IO::Socket::IP";
+	my $cs = $iosocket->new(
+	    Proto		=> "tcp",
+	    Domain		=> $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} ?
+					$self->{chroot}."/client.crt" : "",
+	    SSL_key_file	=> $self->{offertlscert} ?
+					$self->{chroot}."/client.key" : "",
+	) or die ref($self), " $iosocket socket connect failed: $!,$SSL_ERROR";
+	print STDERR "connect sock: ",$cs->sockhost()," ",$cs->sockport(),"\n";
+	print STDERR "connect peer: ",$cs->peerhost()," ",$cs->peerport(),"\n";
+	if ($self->{tls}) {
+		print STDERR "tls version: ",$cs->get_sslversion(),"\n";
+		print STDERR "tls cipher: ",$cs->get_cipher(),"\n";
+		print STDERR "tls peer certificate:\n",
+		    $cs->dump_peer_certificate();
+	}
+
+	*STDIN = *STDOUT = $self->{cs} = $cs;
+}
+
+1;
blob - /dev/null
blob + 53fed72040ceaf2da1b2ef2d04ab7093080d5c63 (mode 644)
--- /dev/null
+++ regress/tests/Httpd.pm
@@ -0,0 +1,96 @@
+#	$OpenBSD: Httpd.pm,v 1.4 2021/10/05 17:40:08 anton Exp $
+
+# Copyright (c) 2010-2015 Alexander Bluhm <bluhm@openbsd.org>
+# Copyright (c) 2015 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.
+
+use strict;
+use warnings;
+
+package Httpd;
+use parent 'Proc';
+use Carp;
+use File::Basename;
+
+sub new {
+	my $class = shift;
+	my %args = @_;
+	$args{chroot} ||= ".";
+	$args{docroot} ||= "htdocs";
+	$args{logfile} ||= $args{chroot}."/httpd.log";
+	$args{up} ||= $args{dryrun} || "server_launch: ";
+	$args{down} ||= $args{dryrun} ? "httpd.conf:" : "parent terminating";
+	$args{func} = sub { Carp::confess "$class func may not be called" };
+	$args{conffile} ||= "httpd.conf";
+	my $self = Proc::new($class, %args);
+	ref($self->{http}) eq 'ARRAY'
+	    or $self->{http} = [ split("\n", $self->{http} || "") ];
+	$self->{listenaddr}
+	    or croak "$class listen addr not given";
+	$self->{listenport}
+	    or croak "$class listen 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: $!";
+
+	# substitute variables in config file
+	my $curdir = dirname($0) || ".";
+	my $connectport = $self->{connectport};
+	my $connectaddr = $self->{connectaddr};
+	my $listenaddr = $self->{listenaddr};
+	my $listenport = $self->{listenport};
+
+	print $fh "prefork 1\n";  # only crashes of first child are observed
+	print $fh "chroot \"".$args{docroot}."\"\n";
+	print $fh "logdir \"".$args{chroot}."\"\n";
+
+	my @http = @{$self->{http}};
+	print $fh  "server \"www.$test.local\" {";
+	my $tls = $self->{listentls} ? "tls " : "";
+	print $fh  "\n\tlisten on $self->{listenaddr} ".
+	    "${tls}port $self->{listenport}" unless grep { /^listen / } @http;
+	# substitute variables in config file
+	foreach (@http) {
+		s/(\$[a-z]+)/$1/eeg;
+	}
+	print $fh  map { "\n\t$_" } @http;
+	if ($self->{listentls}) {
+	    print $fh "\n";
+	    print $fh "\ttls certificate \"".$args{chroot}."/server.crt\"\n";
+	    print $fh "\ttls key \"".$args{chroot}."/server.key\"";
+	    $self->{verifytls}
+		and print $fh "\n\ttls client ca \"".$args{chroot}."/ca.crt\"";
+	}
+	print $fh "\n\troot \"/\"";
+	print $fh "\n\tlog style combined";
+	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 $httpd = $ENV{HTTPD} ? $ENV{HTTPD} : "httpd";
+	my @cmd = (@sudo, @ktrace, $httpd, "-dvv", "-f", $self->{conffile});
+	print STDERR "execute: @cmd\n";
+	exec @cmd;
+	die ref($self), " exec '@cmd' failed: $!";
+}
+
+1;
blob - /dev/null
blob + 55dd5410b11b089c12455316c6b6bd6c1a19fe36 (mode 644)
--- /dev/null
+++ regress/tests/LICENSE
@@ -0,0 +1,14 @@
+# Copyright (c) 2010-2021 Alexander Bluhm <bluhm@openbsd.org>
+# Copyright (c) 2014,2015 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 + 3afb588ce930d7e9c94ca5289999ca3109a320a7 (mode 644)
--- /dev/null
+++ regress/tests/Makefile
@@ -0,0 +1,99 @@
+#	$OpenBSD: Makefile,v 1.16 2021/12/22 15:54:01 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
+
+# Automatically generate regress targets from test cases in directory.
+
+ARGS !=			cd ${.CURDIR} && ls args-*.pl
+CLEANFILES +=		*.log httpd.conf ktrace.out stamp-*
+CLEANFILES +=		*.pem *.req *.crt *.key *.srl md5-*
+
+HTDOCS_FILES =		512 1048576 1073741824
+HTDOCS_MD5 =		${HTDOCS_FILES:S,^,md5-,}
+HTDOCS_SPARSE =		yes
+CLEANFILES +=		htdocs/*
+
+# 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, httpd, server.
+
+.for a in ${ARGS}
+REGRESS_TARGETS +=	run-$a
+REGRESS_ROOT_TARGETS +=	run-$a
+run-$a: $a ${HTDOCS_MD5}
+	time SUDO="${SUDO}" KTRACE=${KTRACE} HTTPD=${HTTPD} perl ${PERLINC} ${PERLPATH}httpd.pl ${.OBJDIR} ${PERLPATH}$a
+.endfor
+
+# populate htdocs
+
+.for d in ${HTDOCS_FILES}
+htdocs/$d:
+	mkdir -m 0755 -p ${@:H}
+.if (${HTDOCS_SPARSE} != "yes")
+	dd if=/dev/urandom of=$@ count=$$(($d / 512)) bs=512
+.else
+	dd of=$@ seek=$$(($d / 512)) bs=512 count=0 status=none
+.endif
+
+md5-$d: htdocs/$d
+	md5 -q htdocs/$d >$@
+.endfor
+
+# create certificates for TLS
+
+ca.crt:
+	openssl req -batch -new -subj /L=OpenBSD/O=httpd-regress/OU=ca/CN=root/ -nodes -newkey rsa -keyout ca.key -x509 -out ca.crt
+
+server.req:
+	openssl req -batch -new -subj /L=OpenBSD/O=httpd-regress/OU=server/CN=localhost/ -nodes -newkey rsa -keyout server.key -out server.req
+
+client.req:
+	openssl req -batch -new -subj /L=OpenBSD/O=httpd-regress/OU=client/CN=localhost/ -nodes -newkey rsa -keyout client.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: ca.crt client.req
+	openssl x509 -CAcreateserial -CAkey ca.key -CA ca.crt -req -in client.req -out $@
+
+${REGRESS_TARGETS:M*tls*} ${REGRESS_TARGETS:M*https*}: server.crt client.crt
+
+# make perl syntax check for all args files
+
+.PHONY: syntax
+
+syntax: stamp-syntax
+
+stamp-syntax: ${ARGS}
+.for a in ${ARGS}
+	@perl -c ${PERLPATH}$a
+.endfor
+	@date >$@
+
+.include <bsd.regress.mk>
blob - /dev/null
blob + e680e6927135ccfa82cbb93320aa2679c4a3533b (mode 644)
--- /dev/null
+++ regress/tests/Proc.pm
@@ -0,0 +1,201 @@
+#	$OpenBSD: Proc.pm,v 1.3 2021/10/05 17:40:08 anton 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 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;
+	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' ".
+		    "test='%s'\n",
+		    scalar(localtime(time())), $self->{end} - $self->{begin},
+		    basename($self->{testfile});
+	}
+
+	IO::Handle::flush(\*STDOUT);
+	IO::Handle::flush(\*STDERR);
+	POSIX::_exit(0);
+}
+
+sub wait {
+	my $self = shift;
+	my $flags = shift;
+
+	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 || 300;
+	$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 + 0d2c8c0a76a9af0de571795036d490c7a4e3438b (mode 644)
--- /dev/null
+++ regress/tests/README
@@ -0,0 +1,20 @@
+Run httpd regression tests.  The framework runs a client and an httpd.
+Each test creates a special httpd.conf and starts those two processes.
+All processes write log files that are checked for certain messages.
+The test arguments are kept in the args-*.pl files.
+
+SUDO=doas
+As httpd 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 httpd.  Note that
+ktrace is invoked after SUDO as SUDO would disable it.
+
+HTTPD=/usr/src/usr.sbin/httpd/obj/httpd
+Start an alternative httpd program that is not in the path.
+
+HTDOCS_SPARSE=no
+Set to anything other than "yes" to create real test files instead of
+sparse files.  This needs more than 1G of free disk space.
blob - /dev/null
blob + 3820fddfaf5727c47f42fbf7797108071e1f200b (mode 644)
--- /dev/null
+++ regress/tests/args-default.pl
@@ -0,0 +1,11 @@
+# test default values
+
+use strict;
+use warnings;
+
+our %args = (
+    len => 512,
+    md5 => path_md5("512")
+);
+
+1;
blob - /dev/null
blob + fa06bba63e6718b02663f156f47f178ba2614b5f (mode 644)
--- /dev/null
+++ regress/tests/args-get-1048576.pl
@@ -0,0 +1,15 @@
+use strict;
+use warnings;
+
+my $len = 1048576;
+our %args = (
+    client => {
+	path => "$len",
+	len => $len,
+	http_vers => [ "1.0" ],
+    },
+    len => 1048576,
+    md5 => path_md5("$len")
+);
+
+1;
blob - /dev/null
blob + c27e00daaa7a5040123c537fb52052e6fba3094d (mode 644)
--- /dev/null
+++ regress/tests/args-get-1073741824.pl
@@ -0,0 +1,16 @@
+use strict;
+use warnings;
+
+my $len = 1073741824;
+my @lengths = ($len, $len);
+our %args = (
+    client => {
+	path => "$len",
+	http_vers => [ "1.0" ],
+	lengths => \@lengths,
+    },
+    md5 => path_md5("$len"),
+    lengths => \@lengths,
+);
+
+1;
blob - /dev/null
blob + 6a0b79aa1f453fb54a7e9007a0bacbc1ffa1d72e (mode 644)
--- /dev/null
+++ regress/tests/args-get-512.pl
@@ -0,0 +1,16 @@
+use strict;
+use warnings;
+
+my $len = 512;
+my @lengths = ($len, $len, $len);
+our %args = (
+    client => {
+	path => "$len",
+	http_vers => [ "1.0" ],
+	lengths => \@lengths,
+    },
+    md5 => path_md5("$len"),
+    lengths => \@lengths,
+);
+
+1;
blob - /dev/null
blob + 2bac65c47bd1ba66eebc4d12f6ac4c6f0bb3c1cc (mode 644)
--- /dev/null
+++ regress/tests/args-get-range-512.pl
@@ -0,0 +1,19 @@
+use strict;
+use warnings;
+
+my $len = 512;
+my $path = 1048576;
+our %args = (
+    client => {
+	path => $path,
+	http_vers => [ "1.1" ],
+	code => "206 Partial Content",
+	header => {
+		"Range" => "bytes=0-511",
+	}
+    },
+    len => $len,
+    md5 => path_md5("$len")
+);
+
+1;
blob - /dev/null
blob + 7f5873774899d74af24d139bb7b84d69c4321349 (mode 644)
--- /dev/null
+++ regress/tests/args-get-range-multipart.pl
@@ -0,0 +1,19 @@
+use strict;
+use warnings;
+
+my $len = 512;
+our %args = (
+    client => {
+	path => $len,
+	http_vers => [ "1.1" ],
+	code => "206 Partial Content",
+	header => {
+		"Range" => "bytes=0-255,256-300,301-",
+	},
+	multipart => 1
+    },
+    len => $len,
+    md5 => path_md5("$len")
+);
+
+1;
blob - /dev/null
blob + e3e7a3bfc5468d26376ea4181f4d032d325d1a3d (mode 644)
--- /dev/null
+++ regress/tests/args-get-slash.pl
@@ -0,0 +1,20 @@
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	func => sub {
+	    my $self = shift;
+	    print "GET /\r\n\r\n";
+	},
+	nocheck => 1
+    },
+    httpd => {
+	loggrep => {
+	    qr/"GET \/" 400 0/ => 1,
+	},
+    },
+);
+
+1;
+
blob - /dev/null
blob + a8dec174b3741470e56eb7a485493e8457c2866a (mode 644)
--- /dev/null
+++ regress/tests/args-log-user-agent.pl
@@ -0,0 +1,17 @@
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	header => {
+		"User-Agent" => "regress\t\n\nGET / HTTP/1.0\r\n"
+	}
+    },
+    httpd => {
+	loggrep => {
+	    qr/\"regress\\t\\n\\nGET \/ HTTP\/1\.0\"/ => 1,
+	},
+    },
+);
+
+1;
blob - /dev/null
blob + 12af833769c2a4e2ed71fc3fc8877fcba63280c1 (mode 644)
--- /dev/null
+++ regress/tests/args-tls-get-1073741824.pl
@@ -0,0 +1,18 @@
+use strict;
+use warnings;
+
+my $len = 1073741824;
+our %args = (
+    client => {
+	tls => 1,
+	path => "$len",
+	len => $len,
+    },
+    httpd => {
+	listentls => 1,
+    },
+    len => $len,
+    md5 => path_md5("$len"),
+);
+
+1;
blob - /dev/null
blob + 47f41f277e71e99b0f64eaf701f162403a6b8da9 (mode 644)
--- /dev/null
+++ regress/tests/args-tls-get-range-512.pl
@@ -0,0 +1,23 @@
+use strict;
+use warnings;
+
+my $len = 512;
+my $path = 1048576;
+our %args = (
+    client => {
+	path => $path,
+	http_vers => [ "1.1" ],
+	code => "206 Partial Content",
+	header => {
+		"Range" => "bytes=0-511",
+	},
+	tls => 1
+    },
+    httpd => {
+	listentls => 1
+    },
+    len => $len,
+    md5 => path_md5("$len")
+);
+
+1;
blob - /dev/null
blob + 13b3ef18c9cac78b75f6555d0bed66fb4de12a65 (mode 644)
--- /dev/null
+++ regress/tests/args-tls-get-range-multipart.pl
@@ -0,0 +1,23 @@
+use strict;
+use warnings;
+
+my $len = 1048576;
+our %args = (
+    client => {
+	path => $len,
+	http_vers => [ "1.1" ],
+	code => "206 Partial Content",
+	header => {
+		"Range" => "bytes=0-255,256-10240,10241-",
+	},
+	multipart => 1,
+	tls => 1
+    },
+    httpd => {
+	listentls => 1
+    },
+    len => $len,
+    md5 => path_md5("$len")
+);
+
+1;
blob - /dev/null
blob + fcabf30cbeb083bec852756cc7859feffb26347f (mode 644)
--- /dev/null
+++ regress/tests/args-tls-verify.pl
@@ -0,0 +1,20 @@
+# test https connection, verifying client cert
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	tls => 1,
+	offertlscert => 1,
+	loggrep => 'Issuer.*/OU=ca/',
+    },
+    httpd => {
+	listentls => 1,
+	verifytls => 1,
+    },
+    len => 512,
+    md5 => path_md5("512")
+);
+
+1;
blob - /dev/null
blob + 29321a22dcf518c22f5ed7b84af97d990942d4b4 (mode 644)
--- /dev/null
+++ regress/tests/args-tls.pl
@@ -0,0 +1,18 @@
+# test https connection
+
+use strict;
+use warnings;
+
+our %args = (
+    client => {
+	tls => 1,
+	loggrep => 'Issuer.*/OU=ca/',
+    },
+    httpd => {
+	listentls => 1,
+    },
+    len => 512,
+    md5 => path_md5("512")
+);
+
+1;
blob - /dev/null
blob + 84c96f18832eb4d11a75d069e0be0db060c49aa9 (mode 644)
--- /dev/null
+++ regress/tests/funcs.pl
@@ -0,0 +1,468 @@
+#	$OpenBSD: funcs.pl,v 1.10 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 POSIX;
+use Socket;
+use Socket6;
+use IO::Socket;
+
+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",
+		    Domain => $domain,
+		    $addr ? (LocalAddr => $addr) : (),
+		) or die "find_ports: create and bind socket failed: $!";
+	}
+	my @ports = map { $_->sockport() } @sockets;
+
+	return @ports;
+}
+
+sub path_md5 {
+	my $name = shift;
+	my $val = `cat md5-$name`;
+}
+
+########################################################################
+# Client funcs
+########################################################################
+
+sub write_char {
+	my $self = shift;
+	my $len = shift // $self->{len} // 512;
+	my $sleep = $self->{sleep};
+
+	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} // 512;
+		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 httpd
+	#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 $code = $self->{code} || "200 OK";
+
+	my $vers;
+	my $chunked = 0;
+	my $multipart = 0;
+	my $boundary;
+	{
+		local $/ = "\r\n";
+		local $_ = <STDIN>;
+		defined
+		    or die ref($self), " missing http $len response";
+		chomp;
+		print STDERR "<<< $_\n";
+		m{^HTTP/(\d\.\d) $code$}
+		    or die ref($self), " http response not $code"
+		    unless $self->{httpnok};
+		$vers = $1;
+		while (<STDIN>) {
+			chomp;
+			print STDERR "<<< $_\n";
+			last if /^$/;
+			if (/^Content-Length: (.*)/) {
+				if ($self->{httpnok} or $self->{multipart}) {
+					$len = $1;
+				} else {
+					$1 == $len or die ref($self),
+					    " bad content length $1";
+				}
+			}
+			if (/^Transfer-Encoding: chunked$/) {
+				$chunked = 1;
+			}
+			if (/^Content-Type: multipart\/byteranges; boundary=(.*)$/) {
+				$multipart = 1;
+				$boundary = $1;
+			}
+		}
+	}
+	die ref($self), " no multipart response"
+	    if ($self->{multipart} && $multipart == 0);
+
+	if ($multipart) {
+		read_multipart($self, $boundary);
+	} elsif ($chunked) {
+		read_chunked($self);
+	} else {
+		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 read_multipart {
+	my $self = shift;
+	my $boundary = shift;
+	my $ctx = Digest::MD5->new();
+	my $len = 0;
+
+	for (;;) {
+		my $part = 0;
+		{
+			local $/ = "\r\n";
+			local $_ = <STDIN>;
+			local $_ = <STDIN>;
+			defined or die ref($self), " missing boundary";
+			chomp;
+			print STDERR "<<< $_\n";
+			/^--$boundary(--)?$/
+			    or die ref($self), " boundary not found: $_";
+			if (not $1) {
+				while (<STDIN>) {
+					chomp;
+					if (/^Content-Length: (.*)/) {
+						$part = $1;
+					}
+					if (/^Content-Range: bytes (\d+)-(\d+)\/(\d+)$/) {
+						$part = $2 - $1 + 1;
+					}
+					print STDERR "<<< $_\n";
+					last if /^$/;
+				}
+			}
+		}
+		last unless $part > 0;
+
+		$len += read_part($self, $ctx, $part);
+	}
+
+	print STDERR "LEN: ", $len, "\n";
+	print STDERR "MD5: ", $ctx->hexdigest, "\n";
+}
+
+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};
+
+	my $ctx = Digest::MD5->new();
+	my $len = read_part($self, $ctx, $max);
+
+	print STDERR "LEN: ", $len, "\n";
+	print STDERR "MD5: ", $ctx->hexdigest, "\n";
+}
+
+sub read_part {
+	my $self = shift;
+	my ($ctx, $max) = @_;
+
+	my $opct = 0;
+	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;
+		}
+	}
+	return $len;
+}
+
+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, %args) = @_;
+
+	return if $args{nocheck};
+
+	check_len($c, $r, %args);
+	check_md5($c, $r, %args);
+	check_loggrep($c, $r, %args);
+	$r->loggrep("lost child")
+	    and die "httpd lost child";
+}
+
+sub check_len {
+	my ($c, $r, %args) = @_;
+
+	$args{len} ||= 512 unless $args{lengths};
+
+	my @clen;
+	@clen = $c->loggrep(qr/^LEN: /) or die "no client len"
+	    unless $args{client}{nocheck};
+#	!@clen
+#	    or die "client: @clen", "len mismatch";
+	!defined($args{len}) || !$clen[0] || $clen[0] eq "LEN: $args{len}\n"
+	    or die "client: $clen[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";
+		}
+	}
+}
+
+sub check_md5 {
+	my ($c, $r, %args) = @_;
+
+	my @cmd5;
+	@cmd5 = $c->loggrep(qr/^MD5: /) unless $args{client}{nocheck};
+	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";
+		}
+	}
+	@cmd5 && ref($args{md5}) eq 'ARRAY'
+	    and die "too many md5 in client log";
+}
+
+sub check_loggrep {
+	my ($c, $r, %args) = @_;
+
+	my %name2proc = (client => $c, httpd => $r);
+	foreach my $name (qw(client httpd)) {
+		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 + 6ca2d83d88f0e8f9845540cc2464ff39efdc0155 (mode 644)
--- /dev/null
+++ regress/tests/httpd.pl
@@ -0,0 +1,74 @@
+#!/usr/bin/perl
+#	$OpenBSD: httpd.pl,v 1.2 2016/05/03 19:13:04 bluhm Exp $
+
+# Copyright (c) 2010-2015 Alexander Bluhm <bluhm@openbsd.org>
+# Copyright (c) 2015 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.
+
+use strict;
+use warnings;
+use Socket;
+use Socket6;
+
+use Client;
+use Httpd;
+require 'funcs.pl';
+
+sub usage {
+	die "usage: httpd.pl chroot [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($d, $c);
+$d = Httpd->new(
+    chroot              => $ARGV[0],
+    listendomain        => AF_INET,
+    listenaddr          => "127.0.0.1",
+    listenport          => $rport,
+    connectdomain       => AF_INET,
+    connectaddr         => "127.0.0.1",
+    connectport         => $sport,
+    %{$args{httpd}},
+    testfile            => $testfile,
+);
+$c = Client->new(
+    chroot              => $ARGV[0],
+    func                => \&http_client,
+    connectdomain       => AF_INET,
+    connectaddr         => "127.0.0.1",
+    connectport         => $rport,
+    %{$args{client}},
+    testfile            => $testfile,
+) unless $args{client}{noclient};
+
+$d->run;
+$d->up;
+$c->run->up unless $args{client}{noclient};
+
+$c->down unless $args{client}{noclient};
+$d->kill_child;
+$d->down;
+
+check_logs($c, $d, %args);