"GOT", but the "O" is a cute, smiling pufferfish. Index | Thread | Search

From:
Stefan Sperling <stsp@stsp.name>
Subject:
parse gotwebd querystrings under pledge "stdio"
To:
gameoftrees@openbsd.org
Date:
Sun, 7 Sep 2025 17:38:55 +0200

Download raw body.

Thread
Move parsing of gotwebd querystrings into the pledge("stdio") process.

Fields of the querystring structure become fixed-sized buffers for
easy passing over imsg. This means we can no longer check pointers
in this structure for NULL. Instead we have to check for empty strings.

ok?

M  gotwebd/config.c          |    3+    0-
M  gotwebd/fcgi.c            |  240+    5-
M  gotwebd/got_operations.c  |   16+   14-
M  gotwebd/gotweb.c          |   34+  289-
M  gotwebd/gotwebd.c         |    3+    0-
M  gotwebd/gotwebd.h         |   31+   29-
M  gotwebd/pages.tmpl        |   36+   35-
M  gotwebd/parse.y           |    3+    0-
M  gotwebd/sockets.c         |   56+    4-

9 files changed, 422 insertions(+), 376 deletions(-)

commit - f17dbdbfb4e1e00cf8a8371add4a43caafc8516e
commit + 6b6bf0256000275726aea5237815b01be46c57a9
blob - f5f240b7d414b6737f10eb222651623aef0067e3
blob + 24a9352100d646dad06a2b1e5b107b6a99b471b8
--- gotwebd/config.c
+++ gotwebd/config.c
@@ -35,9 +35,12 @@
 #include <util.h>
 #include <errno.h>
 #include <imsg.h>
+#include <sha1.h>
+#include <sha2.h>
 
 #include "got_opentemp.h"
 #include "got_reference.h"
+#include "got_object.h"
 
 #include "gotwebd.h"
 #include "log.h"
blob - 7aded7f0ba3070a6bf7233a97f5272a4baefc58b
blob + 89ead404546314fa3c7d49a1fe5e7f58d495a1c5
--- gotwebd/fcgi.c
+++ gotwebd/fcgi.c
@@ -22,9 +22,12 @@
 #include <sys/types.h>
 #include <sys/uio.h>
 
+#include <ctype.h>
 #include <errno.h>
 #include <event.h>
 #include <imsg.h>
+#include <sha1.h>
+#include <sha2.h>
 #include <signal.h>
 #include <stdarg.h>
 #include <stdlib.h>
@@ -35,6 +38,7 @@
 
 #include "got_error.h"
 #include "got_reference.h"
+#include "got_object.h"
 
 #include "got_lib_poll.h"
 
@@ -128,6 +132,8 @@ fcgi_parse_record(struct gotwebd_fcgi_record *rec)
 	uint8_t *record_body;
 	struct gotwebd_fcgi_params params = { 0 };
 
+	fcgi_init_querystring(&params.qs);
+
 	if (rec->record_len < sizeof(struct fcgi_record_header) ||
 	    rec->record_len > sizeof(rec->record)) {
 		log_warnx("invalid fcgi record size");
@@ -171,9 +177,230 @@ fcgi_parse_record(struct gotwebd_fcgi_record *rec)
 	}
 }
 
+static const struct querystring_keys querystring_keys[] = {
+	{ "action",		ACTION },
+	{ "commit",		COMMIT },
+	{ "file",		RFILE },
+	{ "folder",		FOLDER },
+	{ "headref",		HEADREF },
+	{ "index_page",		INDEX_PAGE },
+	{ "path",		PATH },
+};
+
+static const struct action_keys action_keys[] = {
+	{ "blame",	BLAME },
+	{ "blob",	BLOB },
+	{ "blobraw",	BLOBRAW },
+	{ "briefs",	BRIEFS },
+	{ "commits",	COMMITS },
+	{ "diff",	DIFF },
+	{ "error",	ERR },
+	{ "index",	INDEX },
+	{ "patch",	PATCH },
+	{ "summary",	SUMMARY },
+	{ "tag",	TAG },
+	{ "tags",	TAGS },
+	{ "tree",	TREE },
+	{ "rss",	RSS },
+};
+
+
+void
+fcgi_init_querystring(struct querystring *qs)
+{
+	memset(qs, 0, sizeof(*qs));
+	qs->action = NO_ACTION;
+	qs->index_page = -1;
+}
+
+/*
+ * Adapted from usr.sbin/httpd/httpd.c url_decode.
+ */
+static const struct got_error *
+urldecode(char *url)
+{
+	char		*p, *q;
+	char		 hex[3];
+	unsigned long	 x;
+
+	hex[2] = '\0';
+	p = q = url;
+
+	while (*p != '\0') {
+		switch (*p) {
+		case '%':
+			/* Encoding character is followed by two hex chars */
+			if (!isxdigit((unsigned char)p[1]) ||
+			    !isxdigit((unsigned char)p[2]) ||
+			    (p[1] == '0' && p[2] == '0'))
+				return got_error(GOT_ERR_BAD_QUERYSTRING);
+
+			hex[0] = p[1];
+			hex[1] = p[2];
+
+			/*
+			 * We don't have to validate "hex" because it is
+			 * guaranteed to include two hex chars followed by nul.
+			 */
+			x = strtoul(hex, NULL, 16);
+			*q = (char)x;
+			p += 2;
+			break;
+		default:
+			*q = *p;
+			break;
+		}
+		p++;
+		q++;
+	}
+	*q = '\0';
+
+	return NULL;
+}
+
+static const struct got_error *
+assign_querystring(struct querystring *qs, char *key, char *value)
+{
+	const struct got_error *error = NULL;
+	const char *errstr;
+	int a_cnt, el_cnt;
+
+	error = urldecode(value);
+	if (error)
+		return error;
+
+	for (el_cnt = 0; el_cnt < nitems(querystring_keys); el_cnt++) {
+		if (strcmp(key, querystring_keys[el_cnt].name) != 0)
+			continue;
+
+		switch (querystring_keys[el_cnt].element) {
+		case ACTION:
+			for (a_cnt = 0; a_cnt < nitems(action_keys); a_cnt++) {
+				if (strcmp(value, action_keys[a_cnt].name) != 0)
+					continue;
+				qs->action = action_keys[a_cnt].action;
+				break;
+			}
+			if (a_cnt == nitems(action_keys))
+				qs->action = ERR;
+			break;
+		case COMMIT:
+			if (strlcpy(qs->commit, value, sizeof(qs->commit)) >=
+			    sizeof(qs->commit)) {
+				error = got_error_msg(GOT_ERR_NO_SPACE,
+				    "requested commit ID too long");
+				goto done;
+			}
+			break;
+		case RFILE:
+			if (strlcpy(qs->file, value, sizeof(qs->file)) >=
+			    sizeof(qs->file)) {
+				error = got_error_msg(GOT_ERR_NO_SPACE,
+				    "requested in-repository path too long");
+				goto done;
+			}
+			break;
+		case FOLDER:
+			if (strlcpy(qs->folder, value[0] ? value : "/",
+			    sizeof(qs->folder)) >= sizeof(qs->folder)) {
+				error = got_error_msg(GOT_ERR_NO_SPACE,
+				    "requested repository folder path "
+				    "too long");
+				goto done;
+			}
+			break;
+		case HEADREF:
+			if (strlcpy(qs->headref, value, sizeof(qs->headref)) >=
+			    sizeof(qs->headref)) {
+				error = got_error_msg(GOT_ERR_NO_SPACE,
+				    "requested repository head ref too long");
+				goto done;
+			}
+			break;
+		case INDEX_PAGE:
+			if (*value == '\0')
+				break;
+			qs->index_page = strtonum(value, INT64_MIN,
+			    INT64_MAX, &errstr);
+			if (errstr) {
+				error = got_error_from_errno3(__func__,
+				    "strtonum", errstr);
+				goto done;
+			}
+			if (qs->index_page < 0)
+				qs->index_page = 0;
+			break;
+		case PATH:
+			if (strlcpy(qs->path, value, sizeof(qs->path)) >=
+			    sizeof(qs->path)) {
+				error = got_error_msg(GOT_ERR_NO_SPACE,
+				    "requested repository path too long");
+				goto done;
+			}
+			break;
+		}
+
+		/* entry found */
+		break;
+	}
+done:
+	return error;
+}
+
+static const struct got_error *
+parse_querystring(struct querystring *qs, char *qst)
+{
+	const struct got_error *error = NULL;
+	char *tok1 = NULL, *tok1_pair = NULL, *tok1_end = NULL;
+	char *tok2 = NULL, *tok2_pair = NULL, *tok2_end = NULL;
+
+	if (qst == NULL)
+		return error;
+
+	tok1 = strdup(qst);
+	if (tok1 == NULL)
+		return got_error_from_errno2(__func__, "strdup");
+
+	tok1_pair = tok1;
+	tok1_end = tok1;
+
+	while (tok1_pair != NULL) {
+		strsep(&tok1_end, "&");
+
+		tok2 = strdup(tok1_pair);
+		if (tok2 == NULL) {
+			free(tok1);
+			return got_error_from_errno2(__func__, "strdup");
+		}
+
+		tok2_pair = tok2;
+		tok2_end = tok2;
+
+		while (tok2_pair != NULL) {
+			strsep(&tok2_end, "=");
+			if (tok2_end) {
+				error = assign_querystring(qs, tok2_pair,
+				    tok2_end);
+				if (error)
+					goto err;
+			}
+			tok2_pair = tok2_end;
+		}
+		free(tok2);
+		tok1_pair = tok1_end;
+	}
+	free(tok1);
+	return error;
+err:
+	free(tok2);
+	free(tok1);
+	return error;
+}
+
 int
 fcgi_parse_params(uint8_t *buf, uint16_t n, struct gotwebd_fcgi_params *params)
 {
+	const struct got_error *error;
 	uint32_t name_len, val_len;
 	uint8_t *val;
 
@@ -218,12 +445,20 @@ fcgi_parse_params(uint8_t *buf, uint16_t n, struct got
 
 		val = buf + name_len;
 
-		if (val_len < MAX_QUERYSTRING &&
-		    name_len == 12 &&
+		if (val_len < MAX_QUERYSTRING && name_len == 12 &&
 		    strncmp(buf, "QUERY_STRING", 12) == 0) {
-			/* TODO: parse querystring here */
-			memcpy(params->querystring, val, val_len);
-			params->querystring[val_len] = '\0';
+			char querystring[MAX_QUERYSTRING];
+
+			memcpy(querystring, val, val_len);
+			querystring[val_len] = '\0';
+
+			fcgi_init_querystring(&params->qs);
+
+			error = parse_querystring(&params->qs, querystring);
+			if (error) {
+				log_warnx("%s: %s", __func__, error->msg);
+				return -1;
+			}
 		}
 
 		if (val_len < MAX_DOCUMENT_URI &&
blob - 2846bbbddb83f63677ac8b3ef56540c5407ccb7f
blob + 18120ebaddfa19651ad40001d9a26efde8f40271
--- gotwebd/got_operations.c
+++ gotwebd/got_operations.c
@@ -202,7 +202,7 @@ got_get_repo_commit(struct request *c, struct repo_com
 	struct got_object_id *id2 = NULL;
 	struct got_object_qid *parent_id;
 	struct transport *t = c->t;
-	struct querystring *qs = c->t->qs;
+	const struct querystring *qs = c->t->qs;
 	char *commit_msg = NULL, *commit_msg0;
 
 	TAILQ_FOREACH(re, refs, entry) {
@@ -332,7 +332,7 @@ got_get_repo_commits(struct request *c, size_t limit)
 	struct repo_commit *repo_commit = NULL;
 	struct transport *t = c->t;
 	struct got_repository *repo = t->repo;
-	struct querystring *qs = t->qs;
+	const struct querystring *qs = t->qs;
 	char *file_path = NULL;
 	int chk_next = 0;
 
@@ -350,12 +350,13 @@ got_get_repo_commits(struct request *c, size_t limit)
 
 	TAILQ_INIT(&refs);
 
-	if (qs->file != NULL && *qs->file != '\0')
-		if (asprintf(&file_path, "%s/%s", qs->folder ? qs->folder : "",
-		    qs->file) == -1)
+	if (qs->file[0]) {
+		if (asprintf(&file_path, "%s/%s",
+		    qs->folder[0] ? qs->folder : "", qs->file) == -1)
 			return got_error_from_errno("asprintf");
+	}
 
-	if (qs->commit) {
+	if (qs->commit[0]) {
 		error = got_repo_match_object_id_prefix(&id, qs->commit,
 		    GOT_OBJ_TYPE_COMMIT, repo);
 		if (error)
@@ -374,7 +375,7 @@ got_get_repo_commits(struct request *c, size_t limit)
 	if (error)
 		goto done;
 
-	if (qs->file != NULL && *qs->file != '\0') {
+	if (qs->file[0]) {
 		error = got_commit_graph_open(&graph, file_path, 0);
 		if (error)
 			goto done;
@@ -457,7 +458,7 @@ got_get_repo_tags(struct request *c, size_t limit)
 	struct server *srv = c->srv;
 	struct transport *t = c->t;
 	struct got_repository *repo = t->repo;
-	struct querystring *qs = t->qs;
+	const struct querystring *qs = t->qs;
 	struct repo_dir *repo_dir = t->repo_dir;
 	struct got_tag_object *tag = NULL;
 	char *repo_path = NULL, *id_str = NULL;
@@ -474,7 +475,8 @@ got_get_repo_tags(struct request *c, size_t limit)
 	    repo_dir->name) == -1)
 		return got_error_from_errno("asprintf");
 
-	if (qs->commit == NULL && (qs->action == TAGS || qs->action == RSS)) {
+	if (qs->commit[0] == '\0' &&
+	    (qs->action == TAGS || qs->action == RSS)) {
 		error = got_ref_open(&ref, repo, qs->headref, 0);
 		if (error)
 			goto done;
@@ -482,7 +484,7 @@ got_get_repo_tags(struct request *c, size_t limit)
 		got_ref_close(ref);
 		if (error)
 			goto done;
-	} else if (qs->commit == NULL && qs->action == TAG) {
+	} else if (qs->commit[0] == '\0' && qs->action == TAG) {
 		error = got_error_msg(GOT_ERR_EOF, "commit id missing");
 		goto done;
 	} else {
@@ -577,7 +579,7 @@ got_get_repo_tags(struct request *c, size_t limit)
 		if (new_repo_tag->commit_id == NULL)
 			goto done;
 
-		if (commit_found == 0 && qs->commit != NULL &&
+		if (commit_found == 0 && qs->commit[0] &&
 		    strncmp(id_str, qs->commit, strlen(id_str)) != 0) {
 			if (commit) {
 				got_object_commit_close(commit);
@@ -675,7 +677,7 @@ got_output_repo_tree(struct request *c, char **readme,
 	struct transport *t = c->t;
 	struct got_commit_object *commit = NULL;
 	struct got_repository *repo = t->repo;
-	struct querystring *qs = t->qs;
+	const struct querystring *qs = t->qs;
 	struct repo_commit *rc = NULL;
 	struct got_object_id *tree_id = NULL, *commit_id = NULL;
 	struct got_reflist_head refs;
@@ -701,7 +703,7 @@ got_output_repo_tree(struct request *c, char **readme,
 		goto done;
 
 	error = got_object_id_by_path(&tree_id, repo, commit,
-	    qs->folder ? qs->folder : "/");
+	    qs->folder[0] ? qs->folder : "/");
 	if (error)
 		goto done;
 
@@ -967,7 +969,7 @@ got_output_file_blame(struct request *c, got_render_bl
 	const struct got_error *error = NULL;
 	struct transport *t = c->t;
 	struct got_repository *repo = t->repo;
-	struct querystring *qs = c->t->qs;
+	const struct querystring *qs = c->t->qs;
 	struct got_object_id *obj_id = NULL, *commit_id = NULL;
 	struct got_commit_object *commit = NULL;
 	struct got_reflist_head refs;
blob - 0110849b3a6f7e927de2262f945532273047ca83
blob + a68f85d91a5e27ea18c3134c49e219999d434a19
--- gotwebd/gotweb.c
+++ gotwebd/gotweb.c
@@ -25,7 +25,6 @@
 #include <sys/stat.h>
 #include <sys/types.h>
 
-#include <ctype.h>
 #include <dirent.h>
 #include <errno.h>
 #include <event.h>
@@ -56,38 +55,6 @@
 #include "log.h"
 #include "tmpl.h"
 
-static const struct querystring_keys querystring_keys[] = {
-	{ "action",		ACTION },
-	{ "commit",		COMMIT },
-	{ "file",		RFILE },
-	{ "folder",		FOLDER },
-	{ "headref",		HEADREF },
-	{ "index_page",		INDEX_PAGE },
-	{ "path",		PATH },
-};
-
-static const struct action_keys action_keys[] = {
-	{ "blame",	BLAME },
-	{ "blob",	BLOB },
-	{ "blobraw",	BLOBRAW },
-	{ "briefs",	BRIEFS },
-	{ "commits",	COMMITS },
-	{ "diff",	DIFF },
-	{ "error",	ERR },
-	{ "index",	INDEX },
-	{ "patch",	PATCH },
-	{ "summary",	SUMMARY },
-	{ "tag",	TAG },
-	{ "tags",	TAGS },
-	{ "tree",	TREE },
-	{ "rss",	RSS },
-};
-
-static const struct got_error *gotweb_init_querystring(struct querystring **);
-static const struct got_error *gotweb_parse_querystring(struct querystring *,
-    char *);
-static const struct got_error *gotweb_assign_querystring(struct querystring *,
-    char *, char *);
 static int gotweb_render_index(struct template *);
 static const struct got_error *gotweb_load_got_path(struct repo_dir **,
     const char *, struct request *);
@@ -98,7 +65,6 @@ static const struct got_error *gotweb_get_repo_descrip
 static const struct got_error *gotweb_get_clone_url(char **, struct server *,
     const char *, int);
 
-static void gotweb_free_querystring(struct querystring *);
 static void gotweb_free_repo_dir(struct repo_dir *);
 
 struct server *gotweb_get_server(const char *);
@@ -267,49 +233,43 @@ gotweb_process_request(struct request *c)
 	size_t len;
 	int r, binary = 0;
 
-	/* parse our querystring */
-	error = gotweb_init_querystring(&qs);
-	if (error) {
-		log_warnx("%s: %s", __func__, error->msg);
-		goto err;
-	}
+	/* querystring */
+	qs = &c->fcgi_params.qs;
 	c->t->qs = qs;
-	error = gotweb_parse_querystring(qs, c->fcgi_params.querystring);
-	if (error) {
-		log_warnx("%s: %s", __func__, error->msg);
-		goto err;
-	}
 
 	/* Log the request. */
 	if (gotwebd_env->gotwebd_verbose > 0) {
 		struct gotwebd_fcgi_params *p = &c->fcgi_params;
 		char *server_name = NULL;
-		char *querystring = NULL;
 		char *document_uri = NULL;
+		const char *action_name = NULL;
 
 		if (p->server_name[0] &&
 		    stravis(&server_name, p->server_name, VIS_SAFE) == -1) {
 			log_warn("stravis");
 			server_name = NULL;
 		}
-		if (p->querystring[0] &&
-		    stravis(&querystring, p->querystring, VIS_SAFE) == -1) {
-			log_warn("stravis");
-			querystring = NULL;
-		}
+
 		if (p->document_uri[0] &&
 		    stravis(&document_uri, p->document_uri, VIS_SAFE) == -1) {
 			log_warn("stravis");
 			document_uri = NULL;
 		}
 
-		log_info("processing request: server='%s' query='%s' "
-		    "document_uri='%s'",
+		action_name = gotweb_action_name(qs->action);
+		log_info("processing request: server='%s' action='%s' "
+		    "commit='%s', file='%s', folder='%s', headref='%s' "
+		    "index_page=%d path='%s' document_uri='%s'",
 		    server_name ? server_name : "",
-		    querystring ? querystring : "",
+		    action_name ? action_name : "",
+		    qs->commit,
+		    qs->file,
+		    qs->folder,
+		    qs->headref,
+		    qs->index_page,
+		    qs->path,
 		    document_uri ? document_uri : "");
 		free(server_name);
-		free(querystring);
 		free(document_uri);
 	}
 
@@ -322,14 +282,14 @@ gotweb_process_request(struct request *c)
 	if (qs->action == BLAME || qs->action == BLOB ||
 	    qs->action == BLOBRAW || qs->action == DIFF ||
 	    qs->action == PATCH) {
-		if (qs->commit == NULL) {
+		if (qs->commit[0] == '\0') {
 			error = got_error(GOT_ERR_BAD_QUERYSTRING);
 			goto err;
 		}
 	}
 
 	if (qs->action != INDEX) {
-		if (qs->path == NULL) {
+		if (qs->path[0] == '\0') {
 			error = got_error(GOT_ERR_BAD_QUERYSTRING);
 			goto err;
 		}
@@ -341,7 +301,7 @@ gotweb_process_request(struct request *c)
 	}
 
 	if (qs->action == BLOBRAW || qs->action == BLOB) {
-		if (qs->folder == NULL || qs->file == NULL) {
+		if (qs->folder[0] == '\0' || qs->file[0] == '\0') {
 			error = got_error(GOT_ERR_BAD_QUERYSTRING);
 			goto err;
 		}
@@ -358,7 +318,7 @@ gotweb_process_request(struct request *c)
 
 	switch (qs->action) {
 	case BLAME:
-		if (qs->folder == NULL || qs->file == NULL) {
+		if (qs->folder[0] == '\0' || qs->file[0] == '\0') {
 			error = got_error(GOT_ERR_BAD_QUERYSTRING);
 			goto err;
 		}
@@ -375,10 +335,10 @@ gotweb_process_request(struct request *c)
 			struct gotweb_url url = {
 				.index_page = -1,
 				.action = BLOBRAW,
-				.path = qs->path,
-				.commit = qs->commit,
-				.folder = qs->folder,
-				.file = qs->file,
+				.path = qs->path[0] ? qs->path : NULL,
+				.commit = qs->commit[0] ? qs->commit : NULL,
+				.folder = qs->folder[0] ? qs->folder : NULL,
+				.file = qs->file[0] ? qs->file : NULL,
 			};
 
 			return gotweb_reply(c, 302, NULL, &url);
@@ -493,11 +453,12 @@ gotweb_process_request(struct request *c)
 		}
 		qs->action = SUMMARY;
 		commit = TAILQ_FIRST(&c->t->repo_commits);
-		if (commit && qs->commit == NULL) {
-			qs->commit = strdup(commit->commit_id);
-			if (qs->commit == NULL) {
-				error = got_error_from_errno("strdup");
-				log_warn("%s: strdup", __func__);
+		if (commit && qs->commit[0] == '\0') {
+			if (strlcpy(qs->commit, commit->commit_id,
+			    sizeof(qs->commit)) >= sizeof(qs->commit)) {
+				error = got_error_msg(GOT_ERR_NO_SPACE,
+				    "commit ID too long");
+				log_warn("%s: %s", __func__, error->msg);
 				goto err;
 			}
 		}
@@ -581,211 +542,6 @@ gotweb_init_transport(struct transport **t)
 	return error;
 }
 
-static const struct got_error *
-gotweb_init_querystring(struct querystring **qs)
-{
-	const struct got_error *error = NULL;
-
-	*qs = calloc(1, sizeof(**qs));
-	if (*qs == NULL)
-		return got_error_from_errno2(__func__, "calloc");
-
-	(*qs)->headref = strdup("HEAD");
-	if ((*qs)->headref == NULL) {
-		free(*qs);
-		*qs = NULL;
-		return got_error_from_errno2(__func__, "strdup");
-	}
-
-	(*qs)->action = INDEX;
-
-	return error;
-}
-
-static const struct got_error *
-gotweb_parse_querystring(struct querystring *qs, char *qst)
-{
-	const struct got_error *error = NULL;
-	char *tok1 = NULL, *tok1_pair = NULL, *tok1_end = NULL;
-	char *tok2 = NULL, *tok2_pair = NULL, *tok2_end = NULL;
-
-	if (qst == NULL)
-		return error;
-
-	tok1 = strdup(qst);
-	if (tok1 == NULL)
-		return got_error_from_errno2(__func__, "strdup");
-
-	tok1_pair = tok1;
-	tok1_end = tok1;
-
-	while (tok1_pair != NULL) {
-		strsep(&tok1_end, "&");
-
-		tok2 = strdup(tok1_pair);
-		if (tok2 == NULL) {
-			free(tok1);
-			return got_error_from_errno2(__func__, "strdup");
-		}
-
-		tok2_pair = tok2;
-		tok2_end = tok2;
-
-		while (tok2_pair != NULL) {
-			strsep(&tok2_end, "=");
-			if (tok2_end) {
-				error = gotweb_assign_querystring(qs, tok2_pair,
-				    tok2_end);
-				if (error)
-					goto err;
-			}
-			tok2_pair = tok2_end;
-		}
-		free(tok2);
-		tok1_pair = tok1_end;
-	}
-	free(tok1);
-	return error;
-err:
-	free(tok2);
-	free(tok1);
-	return error;
-}
-
-/*
- * Adapted from usr.sbin/httpd/httpd.c url_decode.
- */
-static const struct got_error *
-gotweb_urldecode(char *url)
-{
-	char		*p, *q;
-	char		 hex[3];
-	unsigned long	 x;
-
-	hex[2] = '\0';
-	p = q = url;
-
-	while (*p != '\0') {
-		switch (*p) {
-		case '%':
-			/* Encoding character is followed by two hex chars */
-			if (!isxdigit((unsigned char)p[1]) ||
-			    !isxdigit((unsigned char)p[2]) ||
-			    (p[1] == '0' && p[2] == '0'))
-				return got_error(GOT_ERR_BAD_QUERYSTRING);
-
-			hex[0] = p[1];
-			hex[1] = p[2];
-
-			/*
-			 * We don't have to validate "hex" because it is
-			 * guaranteed to include two hex chars followed by nul.
-			 */
-			x = strtoul(hex, NULL, 16);
-			*q = (char)x;
-			p += 2;
-			break;
-		default:
-			*q = *p;
-			break;
-		}
-		p++;
-		q++;
-	}
-	*q = '\0';
-
-	return NULL;
-}
-
-static const struct got_error *
-gotweb_assign_querystring(struct querystring *qs, char *key, char *value)
-{
-	const struct got_error *error = NULL;
-	const char *errstr;
-	int a_cnt, el_cnt;
-
-	error = gotweb_urldecode(value);
-	if (error)
-		return error;
-
-	for (el_cnt = 0; el_cnt < nitems(querystring_keys); el_cnt++) {
-		if (strcmp(key, querystring_keys[el_cnt].name) != 0)
-			continue;
-
-		switch (querystring_keys[el_cnt].element) {
-		case ACTION:
-			for (a_cnt = 0; a_cnt < nitems(action_keys); a_cnt++) {
-				if (strcmp(value, action_keys[a_cnt].name) != 0)
-					continue;
-				qs->action = action_keys[a_cnt].action;
-				break;
-			}
-			if (a_cnt == nitems(action_keys))
-				qs->action = ERR;
-			break;
-		case COMMIT:
-			qs->commit = strdup(value);
-			if (qs->commit == NULL) {
-				error = got_error_from_errno2(__func__,
-				    "strdup");
-				goto done;
-			}
-			break;
-		case RFILE:
-			qs->file = strdup(value);
-			if (qs->file == NULL) {
-				error = got_error_from_errno2(__func__,
-				    "strdup");
-				goto done;
-			}
-			break;
-		case FOLDER:
-			qs->folder = strdup(value);
-			if (qs->folder == NULL) {
-				error = got_error_from_errno2(__func__,
-				    "strdup");
-				goto done;
-			}
-			break;
-		case HEADREF:
-			free(qs->headref);
-			qs->headref = strdup(value);
-			if (qs->headref == NULL) {
-				error = got_error_from_errno2(__func__,
-				    "strdup");
-				goto done;
-			}
-			break;
-		case INDEX_PAGE:
-			if (*value == '\0')
-				break;
-			qs->index_page = strtonum(value, INT64_MIN,
-			    INT64_MAX, &errstr);
-			if (errstr) {
-				error = got_error_from_errno3(__func__,
-				    "strtonum", errstr);
-				goto done;
-			}
-			if (qs->index_page < 0)
-				qs->index_page = 0;
-			break;
-		case PATH:
-			qs->path = strdup(value);
-			if (qs->path == NULL) {
-				error = got_error_from_errno2(__func__,
-				    "strdup");
-				goto done;
-			}
-			break;
-		}
-
-		/* entry found */
-		break;
-	}
-done:
-	return error;
-}
-
 void
 gotweb_free_repo_tag(struct repo_tag *rt)
 {
@@ -816,19 +572,6 @@ gotweb_free_repo_commit(struct repo_commit *rc)
 }
 
 static void
-gotweb_free_querystring(struct querystring *qs)
-{
-	if (qs != NULL) {
-		free(qs->commit);
-		free(qs->file);
-		free(qs->folder);
-		free(qs->headref);
-		free(qs->path);
-	}
-	free(qs);
-}
-
-static void
 gotweb_free_repo_dir(struct repo_dir *repo_dir)
 {
 	if (repo_dir != NULL) {
@@ -859,7 +602,7 @@ gotweb_free_transport(struct transport *t)
 		gotweb_free_repo_tag(rt);
 	}
 	gotweb_free_repo_dir(t->repo_dir);
-	gotweb_free_querystring(t->qs);
+	t->qs = NULL;
 	free(t->more_id);
 	free(t->tags_more_id);
 	if (t->blob)
@@ -887,7 +630,7 @@ gotweb_index_navs(struct request *c, struct gotweb_url
     struct gotweb_url *next, int *have_next)
 {
 	struct transport *t = c->t;
-	struct querystring *qs = t->qs;
+	const struct querystring *qs = t->qs;
 	struct server *srv = c->srv;
 
 	*have_prev = *have_next = 0;
@@ -917,7 +660,7 @@ gotweb_render_index(struct template *tp)
 	struct request *c = tp->tp_arg;
 	struct server *srv = c->srv;
 	struct transport *t = c->t;
-	struct querystring *qs = t->qs;
+	const struct querystring *qs = t->qs;
 	struct repo_dir *repo_dir = NULL;
 	struct dirent **sd_dent = t->repos;
 	unsigned int d_i, d_disp = 0;
@@ -1062,6 +805,8 @@ const char *
 gotweb_action_name(int action)
 {
 	switch (action) {
+	case NO_ACTION:
+		return "no action";
 	case BLAME:
 		return "blame";
 	case BLOB:
blob - 17338707c2666ca04c1d07ca753f94cd50762191
blob + 5f72ef9e31fdd912c972f55367db8538e012b9b9
--- gotwebd/gotwebd.c
+++ gotwebd/gotwebd.c
@@ -33,6 +33,8 @@
 #include <fcntl.h>
 #include <imsg.h>
 #include <pwd.h>
+#include <sha1.h>
+#include <sha2.h>
 #include <signal.h>
 #include <syslog.h>
 #include <unistd.h>
@@ -41,6 +43,7 @@
 
 #include "got_opentemp.h"
 #include "got_reference.h"
+#include "got_object.h"
 
 #include "gotwebd.h"
 #include "log.h"
blob - 02bc544637cc043a756a9a812d12548947cd9442
blob + 264aea1b3238702f4213e6d79fcfb8d45ebb2f41
--- gotwebd/gotwebd.h
+++ gotwebd/gotwebd.h
@@ -212,7 +212,7 @@ struct transport {
 	struct got_reflist_head	 refs;
 	struct got_repository	*repo;
 	struct repo_dir		*repo_dir;
-	struct querystring	*qs;
+	const struct querystring *qs;
 	char			*more_id;
 	char			*tags_more_id;
 	unsigned int		 repos_total;
@@ -249,9 +249,37 @@ struct gotwebd_fcgi_record {
 	size_t				 record_len;
 };
 
+enum query_actions {
+	NO_ACTION = 0,
+	BLAME,
+	BLOB,
+	BLOBRAW,
+	BRIEFS,
+	COMMITS,
+	DIFF,
+	ERR,
+	INDEX,
+	PATCH,
+	SUMMARY,
+	TAG,
+	TAGS,
+	TREE,
+	RSS,
+};
+
+struct querystring {
+	enum query_actions action;
+	char		 commit[GOT_OBJECT_ID_HEX_MAXLEN];
+	char		 file[NAME_MAX];
+	char		 folder[PATH_MAX];
+	char		 headref[MAX_DOCUMENT_URI];
+	int		 index_page;
+	char		 path[PATH_MAX];
+};
+
 struct gotwebd_fcgi_params {
 	uint32_t			 request_id;
-	char				 querystring[MAX_QUERYSTRING];
+	struct querystring		 qs;
 	char				 document_uri[MAX_DOCUMENT_URI];
 	char				 server_name[MAX_SERVER_NAME];
 	int				 https;
@@ -413,16 +441,6 @@ struct gotweb_url {
 	const char	*path;
 };
 
-struct querystring {
-	uint8_t		 action;
-	char		*commit;
-	char		*file;
-	char		*folder;
-	char		*headref;
-	int		 index_page;
-	char		*path;
-};
-
 struct querystring_keys {
 	const char	*name;
 	int		 element;
@@ -443,23 +461,6 @@ enum querystring_elements {
 	PATH,
 };
 
-enum query_actions {
-	BLAME,
-	BLOB,
-	BLOBRAW,
-	BRIEFS,
-	COMMITS,
-	DIFF,
-	ERR,
-	INDEX,
-	PATCH,
-	SUMMARY,
-	TAG,
-	TAGS,
-	TREE,
-	RSS,
-};
-
 extern struct gotwebd	*gotwebd_env;
 
 typedef int (*got_render_blame_line_cb)(struct template *, const char *,
@@ -521,6 +522,7 @@ int parse_config(const char *, struct gotwebd *);
 int cmdline_symset(char *);
 
 /* fcgi.c */
+void fcgi_init_querystring(struct querystring *);
 void fcgi_cleanup_request(struct request *);
 void fcgi_create_end_record(struct request *);
 int fcgi_write(void *, const void *, size_t);
blob - c7e1ac5b684f3f72cb60e8a5260089c8262b08a9
blob + 9ce2f4f4149824c1fbfbbe7bb7aea41082f89a13
--- gotwebd/pages.tmpl
+++ gotwebd/pages.tmpl
@@ -105,7 +105,7 @@ nextsep(char *s, char **t)
 {{ define breadcumbs(struct template *tp) }}
 {!
 	struct request		*c = tp->tp_arg;
-	struct querystring	*qs = c->t->qs;
+	const struct querystring *qs = c->t->qs;
 	struct gotweb_url	 url;
 	const char		*folder = qs->folder;
 	const char		*action = "tree";
@@ -115,15 +115,15 @@ nextsep(char *s, char **t)
 	memset(&url, 0, sizeof(url));
 	url.index_page = -1;
 	url.action = TREE;
-	url.path = qs->path;
-	url.commit = qs->commit;
+	url.path = qs->path[0] ? qs->path : NULL;
+	url.commit = qs->commit[0] ? qs->commit : NULL;
 
 	if (qs->action != TREE && qs->action != BLOB) {
 		action = gotweb_action_name(qs->action);
 		url.action = qs->action;
 	}
 
-	if (folder && *folder != '\0') {
+	if (*folder != '\0') {
 		while (*folder == '/')
 			folder++;
 		dir = strdup(folder);
@@ -152,7 +152,7 @@ nextsep(char *s, char **t)
     {{ end }}
   {{ end }}
 
-  {{ if qs->file }}
+  {{ if qs->file[0] }}
     {{ qs->file }}
   {{ end}}
 
@@ -165,7 +165,7 @@ nextsep(char *s, char **t)
 {!
 	struct request		*c = tp->tp_arg;
 	struct server		*srv = c->srv;
-	struct querystring	*qs = c->t->qs;
+	const struct querystring *qs = c->t->qs;
 	struct gotweb_url	 u_path;
 	const char		*prfx = c->fcgi_params.document_uri;
 	const char		*css = srv->custom_css;
@@ -202,8 +202,8 @@ nextsep(char *s, char **t)
         <a href="?index_page={{ printf "%d", qs->index_page }}">
           {{ srv->site_link }}
         </a>
-        {{ if qs->path }}
-          {! u_path.path = qs->path; !}
+        {{ if qs->path[0] }}
+          {! u_path.path = qs->path[0] ? qs->path : NULL; !}
           {{ " / " }}
           <a href="{{ render gotweb_render_url(tp->tp_arg, &u_path)}}">
             {{ qs->path }}
@@ -344,7 +344,7 @@ nextsep(char *s, char **t)
 {!
 	struct request		*c = tp->tp_arg;
 	struct transport	*t = c->t;
-	struct querystring	*qs = c->t->qs;
+	const struct querystring *qs = c->t->qs;
 	struct repo_commit	*rc;
 	struct repo_dir		*repo_dir = t->repo_dir;
 	struct gotweb_url	 diff_url, patch_url, tree_url;
@@ -451,15 +451,15 @@ nextsep(char *s, char **t)
 {!
 	struct request		*c = tp->tp_arg;
 	struct transport	*t = c->t;
-	struct querystring	*qs = t->qs;
+	const struct querystring *qs = t->qs;
 	struct gotweb_url	 more = {
 		.action = action,
 		.index_page = -1,
-		.path = qs->path,
+		.path = qs->path[0] ? qs->path : NULL,
 		.commit = t->more_id,
 		.headref = qs->headref,
-		.folder = qs->folder,
-		.file = qs->file,
+		.folder = qs->folder[0] ? qs->folder : NULL,
+		.file = qs->file[0] ? qs->file : NULL,
 	};
 
 	if (action == TAGS)
@@ -576,7 +576,7 @@ nextsep(char *s, char **t)
 {!
 	struct request		*c = tp->tp_arg;
 	struct transport	*t = c->t;
-	struct querystring	*qs = t->qs;
+	const struct querystring *qs = t->qs;
 	struct got_blob_object	*blob = t->blob;
 	struct repo_commit	*rc = TAILQ_FIRST(&t->repo_commits);
 	struct gotweb_url	 briefs_url, blame_url, raw_url;
@@ -584,10 +584,10 @@ nextsep(char *s, char **t)
 	memset(&briefs_url, 0, sizeof(briefs_url));
 	briefs_url.index_page = -1,
 	briefs_url.action = BRIEFS,
-	briefs_url.path = qs->path,
-	briefs_url.commit = qs->commit,
-	briefs_url.folder = qs->folder,
-	briefs_url.file = qs->file,
+	briefs_url.path = qs->path[0] ? qs->path : NULL,
+	briefs_url.commit = qs->commit[0] ? qs->commit : NULL,
+	briefs_url.folder = qs->folder[0] ? qs->folder : NULL,
+	briefs_url.file = qs->file[0] ? qs->file : NULL,
 
 	memcpy(&blame_url, &briefs_url, sizeof(blame_url));
 	blame_url.action = BLAME;
@@ -653,7 +653,7 @@ nextsep(char *s, char **t)
 	const struct got_error	*error;
 	struct request		*c = tp->tp_arg;
 	struct transport	*t = c->t;
-	struct querystring	*qs = c->t->qs;
+	const struct querystring *qs = c->t->qs;
 	struct gotweb_url	 url;
 	char			*readme = NULL;
 	int			 binary;
@@ -666,7 +666,8 @@ nextsep(char *s, char **t)
   {{ if readme }}
     {!
 	error = got_open_blob_for_output(&t->blob, &t->fd, &binary, c,
-	    qs->folder, readme, qs->commit);
+	    qs->folder[0] ? qs->folder : NULL, readme,
+	    qs->commit[0] ? qs->commit : NULL);
 	if (error) {
 		free(readme);
 		return (-1);
@@ -675,10 +676,10 @@ nextsep(char *s, char **t)
 	memset(&url, 0, sizeof(url));
 	url.index_page = -1;
 	url.action = BLOB;
-	url.path = t->qs->path;
+	url.path = t->qs->path[0] ? t->qs->path : NULL;
 	url.file = readme;
-	url.folder = t->qs->folder ? t->qs->folder : "";
-	url.commit = t->qs->commit;
+	url.folder = t->qs->folder[0] ? t->qs->folder : "";
+	url.commit = t->qs->commit[0] ? t->qs->commit : NULL;
     !}
     {{ if !binary }}
       <h2>
@@ -742,7 +743,7 @@ nextsep(char *s, char **t)
 {!
 	struct request		*c = tp->tp_arg;
 	struct transport	*t = c->t;
-	struct querystring	*qs = t->qs;
+	const struct querystring *qs = t->qs;
 	struct repo_commit	*rc = TAILQ_FIRST(&t->repo_commits);
 	const char		*modestr = "";
 	const char		*name;
@@ -752,13 +753,13 @@ nextsep(char *s, char **t)
 	struct gotweb_url	 url = {
 	       .index_page = -1,
 	       .commit = rc->commit_id,
-	       .path = qs->path,
+	       .path = qs->path[0] ? qs->path : NULL,
 	};
 
 	name = got_tree_entry_get_name(te);
 	mode = got_tree_entry_get_mode(te);
 
-	folder = qs->folder ? qs->folder : "";
+	folder = qs->folder[0] ? qs->folder : "";
 	if (S_ISDIR(mode)) {
 		if (asprintf(&dir, "%s/%s", folder, name) == -1)
 			return (-1);
@@ -928,7 +929,7 @@ nextsep(char *s, char **t)
 {!
 	struct request		*c = tp->tp_arg;
 	struct transport	*t = c->t;
-	struct querystring	*qs = t->qs;
+	const struct querystring *qs = t->qs;
 	FILE			*fp = t->fp;
 	struct repo_commit	*rc = TAILQ_FIRST(&t->repo_commits);
 	char			*line = NULL;
@@ -937,7 +938,7 @@ nextsep(char *s, char **t)
 	struct gotweb_url	 patch_url, tree_url = {
 		.action = TREE,
 		.index_page = -1,
-		.path = qs->path,
+		.path = qs->path[0] ? qs->path : NULL,
 		.commit = rc->commit_id,
 	};
 
@@ -1038,13 +1039,13 @@ nextsep(char *s, char **t)
 {!
 	const struct got_error	*err;
 	struct request		*c = tp->tp_arg;
-	struct querystring	*qs = c->t->qs;
+	const struct querystring *qs = c->t->qs;
 	const char		*refname;
 	time_t			 age;
 	struct gotweb_url	 url = {
 		.action = SUMMARY,
 		.index_page = -1,
-		.path = qs->path,
+		.path = qs->path[0] ? qs->path : NULL,
 	};
 
 	refname = got_ref_get_name(re->ref);
@@ -1133,17 +1134,17 @@ nextsep(char *s, char **t)
 	const struct got_error	*err;
 	struct request		*c = tp->tp_arg;
 	struct transport	*t = c->t;
-	struct querystring	*qs = t->qs;
+	const struct querystring *qs = t->qs;
 	struct repo_commit	*rc = TAILQ_FIRST(&t->repo_commits);
 	struct gotweb_url	 briefs_url, blob_url, raw_url;
 
 	memset(&briefs_url, 0, sizeof(briefs_url));
 	briefs_url.index_page = -1,
 	briefs_url.action = BRIEFS,
-	briefs_url.path = qs->path,
-	briefs_url.commit = qs->commit,
-	briefs_url.folder = qs->folder,
-	briefs_url.file = qs->file,
+	briefs_url.path = qs->path[0] ? qs->path : NULL;
+	briefs_url.commit = qs->commit[0] ? qs->commit : NULL,
+	briefs_url.folder = qs->folder[0] ? qs->folder : NULL,
+	briefs_url.file = qs->file[0] ? qs->file : NULL,
 
 	memcpy(&blob_url, &briefs_url, sizeof(blob_url));
 	blob_url.action = BLOB;
blob - a985c4c448a29a5e34d9a497c1736397cd5ed2af
blob + f6da39132c94a229b98735aa2e9a44a4d415076d
--- gotwebd/parse.y
+++ gotwebd/parse.y
@@ -41,6 +41,8 @@
 #include <imsg.h>
 #include <limits.h>
 #include <netdb.h>
+#include <sha1.h>
+#include <sha2.h>
 #include <stdarg.h>
 #include <stdlib.h>
 #include <stdio.h>
@@ -49,6 +51,7 @@
 #include <unistd.h>
 
 #include "got_reference.h"
+#include "got_object.h"
 
 #include "gotwebd.h"
 #include "log.h"
blob - 588e89ebf1f5bc154b7f43bcc788380f6768cbbe
blob + 8fea3fb221fe335452f3036c4b770801d118b199
--- gotwebd/sockets.c
+++ gotwebd/sockets.c
@@ -42,6 +42,8 @@
 #include <netdb.h>
 #include <poll.h>
 #include <pwd.h>
+#include <sha1.h>
+#include <sha2.h>
 #include <siphash.h>
 #include <stddef.h>
 #include <stdio.h>
@@ -51,6 +53,7 @@
 #include <util.h>
 
 #include "got_reference.h"
+#include "got_object.h"
 
 #include "gotwebd.h"
 #include "log.h"
@@ -503,10 +506,24 @@ static int
 process_request(struct request *c)
 {
 	struct gotwebd *env = gotwebd_env;
+	struct querystring *qs = &c->fcgi_params.qs;
 	struct imsgev *iev_gotweb;
 	int ret, i;
 	struct request ic;
 
+	/* Fill in defaults for unspecified parameters where needed. */
+	if (qs->action == NO_ACTION)
+		qs->action = INDEX;
+	if (qs->index_page == -1)
+		qs->index_page = 0;
+	if (qs->headref[0] == '\0') {
+		if (strlcpy(qs->headref, GOT_REF_HEAD, sizeof(qs->headref)) >=
+		    sizeof(qs->headref)) {
+			log_warnx("head reference buffer too small");
+			return -1;
+		}
+	}
+
 	memcpy(&ic, c, sizeof(ic));
 
 	/* Don't leak pointers from our address space to another process. */
@@ -560,13 +577,47 @@ recv_parsed_params(struct imsg *imsg)
 
 	p = &c->fcgi_params;
 
-	if (params.querystring[0] != '\0' &&
-	    strlcpy(p->querystring, params.querystring,
-	    sizeof(p->querystring)) >= sizeof(p->querystring)) {
-		log_warnx("querystring too long");
+	if (params.qs.action != NO_ACTION)
+		p->qs.action = params.qs.action;
+
+	if (params.qs.commit[0] &&
+	    strlcpy(p->qs.commit, params.qs.commit,
+	    sizeof(p->qs.commit)) >= sizeof(p->qs.commit)) {
+		log_warnx("commit ID too long");
 		goto fail;
 	}
 
+	if (params.qs.file[0] &&
+	    strlcpy(p->qs.file, params.qs.file,
+	    sizeof(p->qs.file)) >= sizeof(p->qs.file)) {
+		log_warnx("file path too long");
+		goto fail;
+	}
+
+	if (params.qs.folder[0] &&
+	    strlcpy(p->qs.folder, params.qs.folder,
+	    sizeof(p->qs.folder)) >= sizeof(p->qs.folder)) {
+		log_warnx("folder path too long");
+		goto fail;
+	}
+
+	if (params.qs.headref[0] &&
+	    strlcpy(p->qs.headref, params.qs.headref,
+	    sizeof(p->qs.headref)) >= sizeof(p->qs.headref)) {
+		log_warnx("headref too long");
+		goto fail;
+	}
+
+	if (params.qs.index_page != -1)
+		p->qs.index_page = params.qs.index_page;
+
+	if (params.qs.path[0] &&
+	    strlcpy(p->qs.path, params.qs.path,
+	    sizeof(p->qs.path)) >= sizeof(p->qs.path)) {
+		log_warnx("path path too long");
+		goto fail;
+	}
+
 	if (params.document_uri[0] != '\0' &&
 	    strlcpy(p->document_uri, params.document_uri,
 	    sizeof(p->document_uri)) >= sizeof(p->document_uri)) {
@@ -1182,6 +1233,7 @@ sockets_socket_accept(int fd, short event, void *arg)
 		return;
 	}
 
+	fcgi_init_querystring(&c->fcgi_params.qs);
 	c->buf = buf;
 	c->fd = s;
 	c->sock = sock;