commit - f03e60a991b6bf54e115e3d187aede52f52ef56c
commit + 73675c3a673715141c3872d1f25eafaffb1ac0a5
blob - 04e58218bd0bcb1c6545b97400a903fb87f7a310
blob + 787e6174012ddab20988c46ebabd7429009f0b92
--- Makefile
+++ Makefile
server-regress:
${MAKE} -C regress/gotd
+webd-regress:
+ ${MAKE} -C regress/gotwebd
+
.include <bsd.subdir.mk>
blob - /dev/null
blob + 91d341dc319c3e0475087f52ae0c6e121584398a (mode 644)
--- /dev/null
+++ regress/gotwebd/Makefile
+.PATH:${.CURDIR}/../../lib
+
+REGRESS_TARGETS=test_gotwebd
+
+PROG = gotwebd_test
+SRCS = gotwebd_test.c error.c hash.c pollfd.c
+
+CPPFLAGS = -I${.CURDIR}/../../include -I${.CURDIR}/../../lib
+
+NOMAN = yes
+
+NOOBJ=Yes
+
+.PHONY: ensure_root prepare_test_env prepare_test_repo start_gotwebd
+
+GOTWEBD_TEST_TMPDIR=/tmp
+GOTWEBD_TEST_ROOT?!!=mktemp -d "${GOTWEBD_TEST_TMPDIR}/gotwebd-test-XXXXXXXXXX"
+GOTWEBD_TEST_CHROOT=${GOTWEBD_TEST_ROOT}/var/www
+GOTWEBD_TEST_CONF=${GOTWEBD_TEST_ROOT}/gotwebd.conf
+GOTWEBD_TEST_SOCK=${GOTWEBD_TEST_CHROOT}/gotweb.sock
+GOTWEBD_TEST_FCGI=${.OBJDIR}/${PROG}
+
+GOTWEBD_TEST_USER?=${DOAS_USER}
+.if empty(GOTWEBD_TEST_USER)
+GOTWEBD_TEST_USER=${SUDO_USER}
+.endif
+.if empty(GOTWEBD_TEST_USER)
+GOTWEBD_TEST_USER=${USER}
+.endif
+
+GOTWEBD_TEST_USER_HOME!=getent passwd ${GOTWEBD_TEST_USER} | cut -d: -f6
+
+PREFIX ?= /usr/local
+BINDIR ?= ${PREFIX}/sbin
+
+GOTWEBD_START_CMD?=${BINDIR}/gotwebd -vvf ${GOTWEBD_TEST_CONF}
+GOTWEBD_STOP_CMD?=pkill -TERM -fx '${GOTWEBD_START_CMD}'
+GOTWEBD_TRAP=trap "${GOTWEBD_STOP_CMD}" HUP INT QUIT PIPE TERM
+
+GOTWEBD_TEST_ENV=GOTWEBD_TEST_SOCK=${GOTWEBD_TEST_SOCK} \
+ GOTWEBD_TEST_CHROOT=${GOTWEBD_TEST_CHROOT} \
+ GOTWEBD_TEST_ROOT=${GOTWEBD_TEST_ROOT} \
+ GOTWEBD_TEST_CONF=${GOTWEBD_TEST_CONF} \
+ GOTWEBD_TEST_USER=${GOTWEBD_TEST_USER} \
+ GOTWEBD_TEST_FCGI=${GOTWEBD_TEST_FCGI} \
+ GOTWEBD_TEST_FCGI=${GOTWEBD_TEST_FCGI} \
+ PATH=$(GOTWEBD_TEST_USER_HOME)/bin:${PATH} \
+ HOME=$(GOTWEBD_TEST_USER_HOME)
+
+ensure_root:
+ @if [[ `id -u` -ne 0 ]]; then \
+ echo gotwebd test suite must be started by root >&2; \
+ false; \
+ fi ; \
+ if [[ "${GOTWEBD_TEST_USER}" = "root" ]]; then \
+ echo GOTWEBD_TEST_USER must be a non-root user >&2; \
+ false; \
+ fi
+
+gotwebd_libexec:
+ @su -m ${GOTWEBD_TEST_USER} -c \
+ '${MAKE} -C ${.CURDIR}/../../gotwebd/libexec' >/dev/null 2>&1
+
+prepare_test_env: gotwebd_libexec ensure_root
+ @mkdir -p "${GOTWEBD_TEST_CHROOT}"
+ @DESTDIR=${GOTWEBD_TEST_ROOT} \
+ ${MAKE} -C ${.CURDIR}/../../gotwebd/libexec install >/dev/null 2>&1
+ @chown ${GOTWEBD_TEST_USER} "${GOTWEBD_TEST_ROOT}" \
+ "${GOTWEBD_TEST_CHROOT}"
+
+prepare_test_repo: prepare_test_env
+ @su -m ${GOTWEBD_TEST_USER} -c 'env ${GOTWEBD_TEST_ENV} \
+ sh ./prepare_test_repo.sh "${GOTWEBD_TEST_CHROOT}"'
+
+start_gotwebd: prepare_test_repo gotwebd_test
+ @echo 'user "${GOTWEBD_TEST_USER}"' > ${GOTWEBD_TEST_CONF}
+ @echo 'chroot "${GOTWEBD_TEST_CHROOT}"' >> ${GOTWEBD_TEST_CONF}
+ @echo 'listen on socket "${GOTWEBD_TEST_SOCK}"' >> ${GOTWEBD_TEST_CONF}
+ @echo 'server "localhost" {' >> ${GOTWEBD_TEST_CONF}
+ @echo ' show_repo_owner off' >> ${GOTWEBD_TEST_CONF}
+ @echo '}' >> ${GOTWEBD_TEST_CONF}
+ @${GOTWEBD_TRAP}; ${GOTWEBD_START_CMD}
+ @${GOTWEBD_TRAP}; sleep .5
+
+test_gotwebd: start_gotwebd
+ @-$(GOTWEBD_TRAP); su -m ${GOTWEBD_TEST_USER} -c \
+ 'env $(GOTWEBD_TEST_ENV) sh ./test_gotwebd.sh'
+ @${GOTWEBD_STOP_CMD} 2>/dev/null
+
+.include <bsd.regress.mk>
blob - /dev/null
blob + 681cedc2e3bbeb0d09075359cb45e7be7e8a45df (mode 644)
--- /dev/null
+++ regress/gotwebd/action_commit.html
+Content-Security-Policy: default-src 'self'; script-src 'none'; object-src 'none';
+Content-Type: text/html
+
+<!doctype html><html><head><meta charset="utf-8" /><title>Gotweb</title><meta name="viewport" content="initial-scale=1.0" /><meta name="msapplication-TileColor" content="#da532c" /><meta name="theme-color" content="#ffffff"/><link rel="apple-touch-icon" sizes="180x180" href="/gotwebd_test_harness/apple-touch-icon.png" /><link rel="icon" type="image/png" sizes="32x32" href="/gotwebd_test_harness/favicon-32x32.png" /><link rel="icon" type="image/png" sizes="16x16" href="/gotwebd_test_harness/favicon-16x16.png" /><link rel="manifest" href="/gotwebd_test_harness/site.webmanifest"/><link rel="mask-icon" href="/gotwebd_test_harness/safari-pinned-tab.svg" /><link rel="stylesheet" type="text/css" href="/gotwebd_test_harness/gotweb.css" /></head><body><header id="header"><div id="got_link"><a href="https://gameoftrees.org" target="_blank"><img src="/gotwebd_test_harness/got.png" /></a></div></header><nav id="site_path"><div id="site_link"><a href="?index_page=0">Repos</a> / <a href="?action=summary&path=repo.git">repo.git</a> / diff</div></nav><main class="action-diff"><header class="subtitle"><h2>Commit Diff</h2></header><div id="diff_content"><div class="page_header_wrapper"><dl><dt>Commit:</dt><dd><code class="commit-id">${COMMIT_ID}</code></dd><dt>From:</dt><dd>${COMMITTER} <${COMMITTER_EMAIL}></dd><dt>Date:</dt><dd><time datetime="${COMMIT_YMDHMS}">${COMMIT_DATE}
+ UTC</time></dd><dt>Message:</dt><dd class="commit-msg">import the test tree
+</dd><dt>Actions:</dt><dd><a href="?action=patch&commit=${COMMIT_ID}&path=repo.git">Patch</a> | <a href="?action=tree&commit=${COMMIT_ID}&path=repo.git">Tree</a></dd></dl></div><hr /><pre id="diff"><span class="diff_line diff_meta">commit - /dev/null</span>
+<span class="diff_line diff_meta">commit + ${COMMIT_ID}</span>
+<span class="diff_line diff_meta">blob - /dev/null</span>
+<span class="diff_line diff_meta">blob + ${BLOB_ALPHA} (mode 644)</span>
+<span class="diff_line diff_minus">--- /dev/null</span>
+<span class="diff_line diff_plus">+++ alpha</span>
+<span class="diff_line diff_chunk_header">@@ -0,0 +1 @@</span>
+<span class="diff_line diff_plus">+alpha</span>
+<span class="diff_line diff_meta">blob - /dev/null</span>
+<span class="diff_line diff_meta">blob + ${BLOB_BETA} (mode 644)</span>
+<span class="diff_line diff_minus">--- /dev/null</span>
+<span class="diff_line diff_plus">+++ beta</span>
+<span class="diff_line diff_chunk_header">@@ -0,0 +1 @@</span>
+<span class="diff_line diff_plus">+beta</span>
+<span class="diff_line diff_meta">blob - /dev/null</span>
+<span class="diff_line diff_meta">blob + ${BLOB_ZETA} (mode 644)</span>
+<span class="diff_line diff_minus">--- /dev/null</span>
+<span class="diff_line diff_plus">+++ epsilon/zeta</span>
+<span class="diff_line diff_chunk_header">@@ -0,0 +1 @@</span>
+<span class="diff_line diff_plus">+zeta</span>
+<span class="diff_line diff_meta">blob - /dev/null</span>
+<span class="diff_line diff_meta">blob + ${BLOB_DELTA} (mode 644)</span>
+<span class="diff_line diff_minus">--- /dev/null</span>
+<span class="diff_line diff_plus">+++ gamma/delta</span>
+<span class="diff_line diff_chunk_header">@@ -0,0 +1 @@</span>
+<span class="diff_line diff_plus">+delta</span>
+</pre></div></main><footer id="site_owner_wrapper"><p id="site_owner">Got Owner</p></footer></body></html>
blob - /dev/null
blob + b976197617d10a91b220cac19df18a839c0c317b (mode 644)
--- /dev/null
+++ regress/gotwebd/action_index.html
+Content-Security-Policy: default-src 'self'; script-src 'none'; object-src 'none';
+Content-Type: text/html
+
+<!doctype html><html><head><meta charset="utf-8" /><title>Gotweb</title><meta name="viewport" content="initial-scale=1.0" /><meta name="msapplication-TileColor" content="#da532c" /><meta name="theme-color" content="#ffffff"/><link rel="apple-touch-icon" sizes="180x180" href="/gotwebd_test_harness/apple-touch-icon.png" /><link rel="icon" type="image/png" sizes="32x32" href="/gotwebd_test_harness/favicon-32x32.png" /><link rel="icon" type="image/png" sizes="16x16" href="/gotwebd_test_harness/favicon-16x16.png" /><link rel="manifest" href="/gotwebd_test_harness/site.webmanifest"/><link rel="mask-icon" href="/gotwebd_test_harness/safari-pinned-tab.svg" /><link rel="stylesheet" type="text/css" href="/gotwebd_test_harness/gotweb.css" /></head><body><header id="header"><div id="got_link"><a href="https://gameoftrees.org" target="_blank"><img src="/gotwebd_test_harness/got.png" /></a></div></header><nav id="site_path"><div id="site_link"><a href="?index_page=0">Repos</a> / <a href="?action=summary&path=repo.git">repo.git</a> / summary</div></nav><main class="action-summary"><dl id="summary_wrapper" class="page_header_wrapper"><dt>Description:</dt><dd>Unnamed repository; edit this file 'description' to name the repository.
+</dd><dt>Last Change:</dt><dd><time datetime="${COMMIT_YMDHMS}">right now</time></dd><dt>Clone URL:</dt><dd><pre class="clone-url"></pre></dd></dl><div class="summary-briefs"><header class='subtitle'><h2>Commit Briefs</h2></header><div id="briefs_content"><div class='brief'><p class='brief_meta'><span class='briefs_age'><time datetime="${COMMIT_YMDHMS}">right now</time></span> <span class='briefs_id'>${COMMIT_ID10}</span> <span class="briefs_author">Flan Hacker </span></p><p class="briefs_log"><a href="?action=diff&commit=${COMMIT_ID}&headref=HEAD&path=repo.git">import the test tree</a> <span class="refs_str">(main)</span></p></div><div class="navs_wrapper"><div class="navs"><a href="?action=diff&commit=${COMMIT_ID}&headref=HEAD&path=repo.git">diff</a> | <a href="?action=patch&commit=${COMMIT_ID}&headref=HEAD&path=repo.git">patch</a> | <a href="?action=tree&commit=${COMMIT_ID}&headref=HEAD&path=repo.git">tree</a></div></div><hr /></div></div><div class="summary-branches"><header class='subtitle'><h2>Branches</h2></header><div id="branches_content"><section class="branches_wrapper"><div class="branches_age"><time datetime="${COMMIT_YMDHMS}">right now</time></div><div class="branch"><a href="?action=summary&headref=main&path=repo.git">main</a></div><div class="navs_wrapper"><div class="navs"><a href="?action=summary&headref=main&path=repo.git">summary</a> | <a href="?action=briefs&headref=main&path=repo.git">commit briefs</a> | <a href="?action=commits&headref=main&path=repo.git">commits</a></div></div><hr /></section></div></div><div class="summary-tags"><header class='subtitle'><h2>Tags</h2></header><div id="tags_content"><div id="err_content">This repository contains no tags</div></div></div><div class="summary-tree"><header class='subtitle'><h2>Tree</h2></header><div id="tree_content"><table id="tree"><tr class="tree_wrapper"><td class="tree_line"><a href="?action=blob&commit=${COMMIT_ID}&file=alpha&folder=&path=repo.git">alpha</a></td><td class="tree_line_blank"><a href="?action=commits&commit=${COMMIT_ID}&file=alpha&folder=&path=repo.git">commits</a> | <a href="?action=blame&commit=${COMMIT_ID}&file=alpha&folder=&path=repo.git">blame</a></td></tr><tr class="tree_wrapper"><td class="tree_line"><a href="?action=blob&commit=${COMMIT_ID}&file=beta&folder=&path=repo.git">beta</a></td><td class="tree_line_blank"><a href="?action=commits&commit=${COMMIT_ID}&file=beta&folder=&path=repo.git">commits</a> | <a href="?action=blame&commit=${COMMIT_ID}&file=beta&folder=&path=repo.git">blame</a></td></tr><tr class="tree_wrapper"><td class="tree_line" colspan=2><a href="?action=tree&commit=${COMMIT_ID}&folder=%2Fepsilon&path=repo.git">epsilon/</a></td></tr><tr class="tree_wrapper"><td class="tree_line" colspan=2><a href="?action=tree&commit=${COMMIT_ID}&folder=%2Fgamma&path=repo.git">gamma/</a></td></tr></table></div></div></main><footer id="site_owner_wrapper"><p id="site_owner">Got Owner</p></footer></body></html>
blob - /dev/null
blob + d7ef5732c24f80f6d83250e1af12fa4f13c315c9 (mode 644)
--- /dev/null
+++ regress/gotwebd/common.sh
+#!/bin/sh
+#
+# Copyright (c) 2019, 2020 Stefan Sperling <stsp@openbsd.org>
+# Copyright (c) 2024 Mark Jamsek <mark@jamsek.dev>
+#
+# 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.
+
+. ../cmdline/common.sh
+
+interpolate()
+{
+ perl -p -e \
+ 's/\$\{(\w+)\}/(exists $ENV{$1} ? $ENV{$1} : "UNDEFINED $1")/eg' \
+ < "$1"
+}
+
+test_cleanup()
+{
+ local testroot="$1"
+ local repo="$2"
+
+ if [ -n "$repo" ]; then
+ git_fsck $testroot $repo
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ return $ret
+ fi
+ fi
+
+ rm -rf "$testroot"
+}
+
+test_done()
+{
+ local testroot="$1"
+ local repo="$2"
+ local result="$3"
+
+ if [ "$result" = "0" ]; then
+ test_cleanup "$testroot" "$repo" || return 1
+ if [ -z "$GOT_TEST_QUIET" ]; then
+ echo "ok"
+ fi
+ elif echo "$result" | grep -q "^xfail"; then
+ # expected test failure; test reproduces an unfixed bug
+ echo "$result"
+ test_cleanup "$testroot" "$repo" || return 1
+ else
+ echo "test failed; leaving test data in $testroot"
+ fi
+}
+
+test_init()
+{
+ local testname="$1"
+ local no_repo="$2"
+
+ if [ -z "$testname" ]; then
+ echo "No test name provided" >&2
+ return 1
+ fi
+
+ local testroot=$(mktemp -d \
+ "$GOTWEBD_TEST_ROOT/gotwebd-test-$testname-XXXXXXXXXX")
+
+ if [ -z "$no_repo" ]; then
+ mkdir $testroot/repo
+ git_init $testroot/repo
+ make_test_tree $testroot/repo
+ git -C $repo add .
+ git_commit $testroot/repo -m "adding the test tree"
+ fi
+
+ echo "$testroot"
+}
+
+run_test()
+{
+ testfunc="$1"
+
+ if [ -n "$regress_run_only" ]; then
+ case "$regress_run_only" in
+ *$testfunc*) ;;
+ *) return ;;
+ esac
+ fi
+
+ if [ -z "$GOT_TEST_QUIET" ]; then
+ echo -n "$testfunc "
+ fi
+
+ $testfunc
+}
blob - /dev/null
blob + c860ec205f79a0008dd231920f23f8fb1ba69716 (mode 644)
--- /dev/null
+++ regress/gotwebd/gotwebd_test.c
+/*
+ * Copyright (c) 2024 Mark Jamsek <mark@jamsek.dev>
+ * Copyright (c) 2014 Florian Obser <florian@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 <sys/socket.h>
+#include <sys/un.h>
+
+#include <err.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "got_error.h"
+#include "got_lib_poll.h"
+
+#ifndef nitems
+#define nitems(_a) (sizeof((_a)) / sizeof((_a)[0]))
+#endif
+
+#define GOTWEBD_TEST_HARNESS "gotwebd_test_harness"
+
+/*
+ * Socket path should be passed on the command line or set as an envvar.
+ * Query string and request method can be passed on the command line;
+ * if not provided, use the index summary page and GET request method.
+ */
+#define GOTWEBD_TEST_QUERYSTRING "action=summary&path=repo.git"
+
+#define GOTWEBD_TEST_PATH_INFO "/"GOTWEBD_TEST_HARNESS"/"
+#define GOTWEBD_TEST_REMOTE_ADDR "::1"
+#define GOTWEBD_TEST_REMOTE_PORT "32768"
+#define GOTWEBD_TEST_SERVER_ADDR "::1"
+#define GOTWEBD_TEST_SERVER_PORT "80"
+#define GOTWEBD_TEST_SERVER_NAME "gotwebd"
+#define GOTWEBD_TEST_SCRIPT_NAME GOTWEBD_TEST_HARNESS
+#define GOTWEBD_TEST_REQUEST_URI "/"GOTWEBD_TEST_HARNESS"/"
+#define GOTWEBD_TEST_DOCUMENT_URI "/"GOTWEBD_TEST_HARNESS"/"
+#define GOTWEBD_TEST_DOCUMENT_ROOT "/cgi-bin/"GOTWEBD_TEST_HARNESS
+#define GOTWEBD_TEST_REQUEST_METHOD "GET"
+#define GOTWEBD_TEST_SCRIPT_FILENAME "/cgi-bin/"GOTWEBD_TEST_HARNESS
+#define GOTWEBD_TEST_SERVER_PROTOCOL "HTTP/1.1"
+#define GOTWEBD_TEST_SERVER_SOFTWARE GOTWEBD_TEST_HARNESS
+#define GOTWEBD_TEST_GATEWAY_INTERFACE "CGI/1.1"
+
+#define PARAM(_p) { #_p, GOTWEBD_TEST_##_p }
+
+static const char *mock_params[][2] = {
+ PARAM(PATH_INFO),
+ PARAM(REMOTE_ADDR),
+ PARAM(REMOTE_PORT),
+ PARAM(SERVER_ADDR),
+ PARAM(SERVER_PORT),
+ PARAM(SERVER_NAME),
+ PARAM(SCRIPT_NAME),
+ PARAM(REQUEST_URI),
+ PARAM(DOCUMENT_URI),
+ PARAM(DOCUMENT_ROOT),
+ PARAM(REQUEST_METHOD),
+ PARAM(SCRIPT_FILENAME),
+ PARAM(SERVER_PROTOCOL),
+ PARAM(SERVER_SOFTWARE),
+ PARAM(GATEWAY_INTERFACE)
+};
+
+#undef PARAM
+
+#define FCGI_CONTENT_SIZE 65535
+#define FCGI_PADDING_SIZE 255
+#define FCGI_RECORD_SIZE \
+ (sizeof(struct fcgi_record_header) + FCGI_CONTENT_SIZE + FCGI_PADDING_SIZE)
+
+#define FCGI_BEGIN_REQUEST 1
+#define FCGI_ABORT_REQUEST 2
+#define FCGI_END_REQUEST 3
+#define FCGI_PARAMS 4
+#define FCGI_STDIN 5
+#define FCGI_STDOUT 6
+#define FCGI_STDERR 7
+#define FCGI_DATA 8
+#define FCGI_GET_VALUES 9
+#define FCGI_GET_VALUES_RESULT 10
+#define FCGI_UNKNOWN_TYPE 11
+#define FCGI_MAXTYPE (FCGI_UNKNOWN_TYPE)
+
+#define FCGI_RESPONDER 1
+
+struct fcgi_record_header {
+ uint8_t version;
+ uint8_t type;
+ uint16_t id;
+ uint16_t content_len;
+ uint8_t padding_len;
+ uint8_t reserved;
+}__attribute__((__packed__));
+
+struct fcgi_begin_request_body {
+ uint16_t role;
+ uint8_t flags;
+ uint8_t reserved[5];
+}__attribute__((__packed__));
+
+struct server_fcgi_param {
+ int total_len;
+ uint8_t buf[FCGI_RECORD_SIZE];
+};
+
+enum fcgistate {
+ FCGI_READ_HEADER,
+ FCGI_READ_CONTENT,
+ FCGI_READ_PADDING
+};
+
+struct fcgi_data {
+ enum fcgistate state;
+ int toread;
+ int padding_len;
+ int type;
+ int status;
+};
+
+__dead static void
+usage(void)
+{
+ fprintf(stderr, "usage: %s [-m method] [-q query] [-s socket]\n",
+ getprogname());
+ exit(1);
+}
+
+static const struct got_error *
+fcgi_writechunk(int type, uint8_t *dat, size_t datlen)
+{
+ if (type == FCGI_END_REQUEST)
+ datlen = 0;
+
+ if (datlen > 0) {
+ if (write(STDOUT_FILENO, dat, datlen) == -1)
+ return got_error_from_errno("write");
+ } else if (fputs("\r\n", stdout) == EOF)
+ return got_error_from_errno("fputs");
+
+ return NULL;
+}
+
+static const struct got_error *
+fcgi_read(int fd, struct fcgi_data *fcgi)
+{
+ const struct got_error *err;
+ struct fcgi_record_header *h;
+ char buf[FCGI_RECORD_SIZE];
+ size_t len;
+
+ do {
+ if (fcgi->toread > sizeof(buf)) {
+ /* cannot happen with gotwebd response */
+ return got_error_msg(GOT_ERR_NO_SPACE,
+ "bad fcgi response size");
+ }
+
+ err = got_poll_read_full(fd, &len, buf,
+ fcgi->toread, fcgi->toread);
+ if (err != NULL) {
+ if (err->code != GOT_ERR_EOF)
+ return err;
+ err = NULL;
+ break;
+ }
+
+ fcgi->toread -= len;
+ if (fcgi->toread != 0)
+ return got_error_msg(GOT_ERR_BAD_PACKET,
+ "short fcgi response");
+
+ switch (fcgi->state) {
+ case FCGI_READ_HEADER:
+ h = (struct fcgi_record_header *)buf;
+ fcgi->type = h->type;
+ fcgi->state = FCGI_READ_CONTENT;
+ fcgi->padding_len = h->padding_len;
+ fcgi->toread = ntohs(h->content_len);
+
+ if (fcgi->toread != 0)
+ break;
+
+ /* fallthrough if content_len == 0 */
+ case FCGI_READ_CONTENT:
+ switch (fcgi->type) {
+ case FCGI_STDERR: /* gotwebd doesn't send STDERR */
+ case FCGI_STDOUT:
+ case FCGI_END_REQUEST:
+ err = fcgi_writechunk(fcgi->type, buf, len);
+ if (err != NULL)
+ return err;
+ break;
+ }
+ if (fcgi->padding_len == 0) {
+ fcgi->state = FCGI_READ_HEADER;
+ fcgi->toread = sizeof(*h);
+ } else {
+ fcgi->state = FCGI_READ_PADDING;
+ fcgi->toread = fcgi->padding_len;
+ }
+ break;
+ case FCGI_READ_PADDING:
+ fcgi->state = FCGI_READ_HEADER;
+ fcgi->toread = sizeof(*h);
+ break;
+ default:
+ /* should not happen with gotwebd */
+ return got_error_msg(GOT_ERR_RANGE, "bad fcgi state");
+ }
+ } while (len > 0);
+
+ return NULL;
+}
+
+static const struct got_error *
+fcgi_add_stdin(int fd)
+{
+ struct fcgi_record_header h;
+
+ memset(&h, 0, sizeof(h));
+ h.version = 1;
+ h.type = FCGI_STDIN;
+ h.id = htons(1);
+ h.padding_len = 0;
+ h.content_len = 0;
+
+ return got_poll_write_full(fd, &h, sizeof(h));
+}
+
+static const struct got_error *
+fcgi_add_param(int fd, struct server_fcgi_param *p,
+ const char *key, const char *val)
+{
+ struct fcgi_record_header *h;
+ int len, key_len, val_len;
+ uint8_t *param;
+
+ key_len = strlen(key);
+ val_len = strlen(val);
+ len = key_len + val_len;
+ len += key_len > 127 ? 4 : 1;
+ len += val_len > 127 ? 4 : 1;
+
+ if (len > FCGI_CONTENT_SIZE)
+ return got_error_msg(GOT_ERR_RANGE, "parameter too large");
+
+ if (p->total_len + len > FCGI_CONTENT_SIZE) {
+ const struct got_error *err;
+
+ err = got_poll_write_full(fd, p->buf,
+ sizeof(*h) + p->total_len);
+ if (err != NULL)
+ return err;
+ p->total_len = 0;
+ }
+
+ h = (struct fcgi_record_header *)p->buf;
+ param = p->buf + sizeof(*h) + p->total_len;
+
+ if (key_len > 127) {
+ *param++ = ((key_len >> 24) & 0xff) | 0x80;
+ *param++ = ((key_len >> 16) & 0xff);
+ *param++ = ((key_len >> 8) & 0xff);
+ *param++ = (key_len & 0xff);
+ } else
+ *param++ = key_len;
+
+ if (val_len > 127) {
+ *param++ = ((val_len >> 24) & 0xff) | 0x80;
+ *param++ = ((val_len >> 16) & 0xff);
+ *param++ = ((val_len >> 8) & 0xff);
+ *param++ = (val_len & 0xff);
+ } else
+ *param++ = val_len;
+
+ memcpy(param, key, key_len);
+ param += key_len;
+ memcpy(param, val, val_len);
+
+ p->total_len += len;
+
+ h->content_len = htons(p->total_len);
+ return NULL;
+}
+
+static const struct got_error *
+fcgi_send_params(int fd, struct server_fcgi_param *param,
+ const char *meth, const char *qs)
+{
+ const struct got_error *err;
+ struct fcgi_record_header *h;
+ const char *k, *v;
+ int i;
+
+ h = (struct fcgi_record_header *)¶m->buf;
+ h->type = FCGI_PARAMS;
+ h->content_len = 0;
+
+ for (i = 0; i < nitems(mock_params); ++i) {
+ k = mock_params[i][0];
+ v = mock_params[i][1];
+ if ((err = fcgi_add_param(fd, param, k, v)) != NULL)
+ return err;
+ }
+ if (qs == NULL)
+ qs = GOTWEBD_TEST_QUERYSTRING;
+ if ((err = fcgi_add_param(fd, param, "QUERY_STRING", qs)) != NULL)
+ return err;
+ if (meth == NULL)
+ meth = GOTWEBD_TEST_REQUEST_METHOD;
+ if ((err = fcgi_add_param(fd, param, "REQUEST_METHOD", meth)) != NULL)
+ return err;
+
+ err = got_poll_write_full(fd, param->buf,
+ sizeof(*h) + ntohs(h->content_len));
+ if (err != NULL)
+ return err;
+
+ /* send "no more params" message */
+ h->content_len = 0;
+ return got_poll_write_full(fd, param->buf, sizeof(*h));
+}
+
+static const struct got_error *
+fcgi(const char *sock, const char *meth, const char *qs)
+{
+ const struct got_error *err;
+ struct server_fcgi_param param;
+ struct fcgi_record_header *h;
+ struct fcgi_begin_request_body *begin;
+ struct fcgi_data fcgi;
+ struct sockaddr_un sun;
+ int fd = -1;
+
+ if ((fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, 0)) == -1)
+ return got_error_from_errno("socket");
+
+ memset(&sun, 0, sizeof(sun));
+ sun.sun_family = AF_UNIX;
+
+ if (strlcpy(sun.sun_path, sock, sizeof(sun.sun_path))
+ >= sizeof(sun.sun_path)) {
+ err = got_error_fmt(GOT_ERR_NO_SPACE,
+ "socket path too long: %s", sock);
+ goto done;
+ }
+
+ if ((connect(fd, (struct sockaddr *)&sun, sizeof(sun))) == -1) {
+ err = got_error_from_errno_fmt("connect: %s", sock);
+ goto done;
+ }
+
+ if (pledge("stdio", NULL) == -1) {
+ err = got_error_from_errno("pledge");
+ goto done;
+ }
+
+ memset(&fcgi, 0, sizeof(fcgi));
+
+ fcgi.state = FCGI_READ_HEADER;
+ fcgi.toread = sizeof(*h);
+ fcgi.status = 200;
+
+ memset(¶m, 0, sizeof(param));
+
+ h = (struct fcgi_record_header *)¶m.buf;
+ h->version = 1;
+ h->type = FCGI_BEGIN_REQUEST;
+ h->id = htons(1);
+ h->content_len = htons(sizeof(*begin));
+ h->padding_len = 0;
+
+ begin = (struct fcgi_begin_request_body *)¶m.buf[sizeof(*h)];
+ begin->role = htons(FCGI_RESPONDER);
+
+ err = got_poll_write_full(fd, param.buf, sizeof(*h) + sizeof(*begin));
+ if (err != NULL)
+ goto done;
+
+ if ((err = fcgi_send_params(fd, ¶m, meth, qs)) != NULL)
+ goto done;
+
+ if ((err = fcgi_add_stdin(fd)) != NULL)
+ goto done;
+
+ err = fcgi_read(fd, &fcgi);
+
+ done:
+ if (fd != -1 && close(fd) == EOF && err == NULL)
+ err = got_error_from_errno("close");
+ return err;
+}
+
+int
+main(int argc, char *argv[])
+{
+ const struct got_error *error;
+ const char *meth = NULL, *qs = NULL, *sock = NULL;
+ int ch;
+
+ while ((ch = getopt(argc, argv, "m:q:s:")) != -1) {
+ switch (ch) {
+ case 'm':
+ meth = optarg;
+ break;
+ case 'q':
+ qs = optarg;
+ break;
+ case 's':
+ sock = optarg;
+ break;
+ default:
+ usage();
+ /* NOTREACHED */
+ }
+ }
+
+ argc -= optind;
+ argv += optind;
+
+ if (argc != 0)
+ usage();
+
+ if (sock == NULL) {
+ sock = getenv("GOTWEBD_TEST_SOCK");
+ if (sock == NULL)
+ errx(1, "socket path not provided");
+ }
+
+ if (unveil(sock, "rw") != 0)
+ err(1, "unveil");
+ if (pledge("stdio unix", NULL) == -1)
+ err(1, "pledge");
+
+ error = fcgi(sock, meth, qs);
+ if (error != NULL)
+ errx(1, "%s", error->msg);
+
+ return 0;
+}
blob - /dev/null
blob + c69f369c8092cbd1911705cfd13c37bb20a570b1 (mode 644)
--- /dev/null
+++ regress/gotwebd/prepare_test_repo.sh
+#!/bin/sh
+#
+# Copyright (c) 2024 Mark Jamsek <mark@jamsek.dev>
+# Copyright (c) 2022 Stefan Sperling <stsp@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.
+
+. ./common.sh
+
+make_repo()
+{
+ local chroot="$1"
+ local no_tree="$2"
+ local repo_path="${chroot}/got/public/repo.git"
+
+ if [ -e "${chroot}/got" ]; then
+ rm -rf "${chroot}/got"
+ fi
+
+ mkdir -p "${chroot}/got/public"
+ if [ $? -ne 0 ]; then
+ echo "failed to make gotweb public repositories tree"
+ return 1
+ fi
+
+ gotadmin init -A "$GOT_TEST_ALGO" "${repo_path}"
+
+ if [ -n "$no_tree" ]; then
+ return
+ fi
+
+ test_tree=$(mktemp -d "${chroot}/gotwebd-test-tree-XXXXXXXXXX")
+ make_test_tree "$test_tree"
+
+ got import -m "import the test tree" -r "${repo_path}" "$test_tree" \
+ > /dev/null
+ if [ $? -ne 0 ]; then
+ echo "failed to import test tree"
+ return 1
+ fi
+
+ rm -r "$test_tree" # TODO: trap
+}
+
+make_repo "$@"
blob - /dev/null
blob + 835f0a599f14f193c79d96780f45c2df410e6988 (mode 644)
--- /dev/null
+++ regress/gotwebd/test_gotwebd.sh
+#!/bin/sh
+#
+# Copyright (c) 2024 Mark Jamsek <mark@jamsek.dev>
+#
+# 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.
+
+. ./common.sh
+
+test_gotwebd_action_index()
+{
+ local testroot=$(test_init gotwebd_action_index 1)
+ local repo="${GOTWEBD_TEST_CHROOT}/got/public/repo.git"
+ local author_time=$(git_show_author_time $repo)
+ local id=$(git_show_head $repo)
+
+ COMMIT_ID=$id \
+ COMMIT_ID10=$(printf '%.10s' $id) \
+ COMMIT_YMDHMS=$(date -u -r $author_time +"%FT%TZ") \
+ interpolate action_index.html > $testroot/content.expected
+
+ $GOTWEBD_TEST_FCGI > $testroot/content
+
+ cmp -s $testroot/content.expected $testroot/content
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ diff -u $testroot/content.expected $testroot/content
+ test_done "$testroot" "" "$ret"
+ return 1
+ fi
+
+ test_done "$testroot" "" "$ret"
+}
+
+test_gotwebd_action_commit()
+{
+ local testroot=$(test_init gotwebd_action_commit 1)
+ local repo="${GOTWEBD_TEST_CHROOT}/got/public/repo.git"
+ local id=$(git_show_head $repo)
+ local author_time=$(git_show_author_time $repo)
+ local qs="action=diff&commit=${id}&headref=HEAD&path=repo.git"
+
+ COMMIT_ID=$id \
+ BLOB_ALPHA=$(get_blob_id $repo "" alpha) \
+ BLOB_BETA=$(get_blob_id $repo "" beta) \
+ BLOB_ZETA=$(get_blob_id $repo epsilon zeta) \
+ BLOB_DELTA=$(get_blob_id $repo gamma delta) \
+ COMMITTER="Flan Hacker" \
+ COMMITTER_EMAIL="flan_hacker@openbsd.org" \
+ COMMIT_YMDHMS=$(date -u -r $author_time +"%FT%TZ") \
+ COMMIT_DATE=$(date -u -r $author_time +"%a %b %e %X %Y") \
+ interpolate action_commit.html > $testroot/content.expected
+
+ $GOTWEBD_TEST_FCGI -q "$qs" > $testroot/content
+
+ cmp -s $testroot/content.expected $testroot/content
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ diff -u $testroot/content.expected $testroot/content
+ test_done "$testroot" "$repo" "$ret"
+ return 1
+ fi
+
+ test_done "$testroot" "$repo" "$ret"
+}
+
+test_parseargs "$@"
+run_test test_gotwebd_action_index
+run_test test_gotwebd_action_commit