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

From:
Stefan Sperling <stsp@stsp.name>
Subject:
allow gotsysd to configure gotwebd
To:
gameoftrees@openbsd.org
Date:
Mon, 19 Jan 2026 16:42:44 +0100

Download raw body.

Thread
Add support for configuring gotwebd to gotsysd and gotsys.conf.

Some features are still missing or incomplete. In particular:

- Users cannot set custom mime-types yet since some code is still missing.

- There is no special protection for gotsys.git, which should
  arguably never be exposed on the web.

Especially the second point should be addressed before making a release
with this feature.

But the basics are now in place and the gotsysd regression tests are passing.
From my point of view this is good enough to continue on the main branch.

oK?

M  gotd/libexec/gotsys-apply/Makefile                           |     2+    2-
M  gotd/libexec/gotsys-apply/gotsys-apply.c                     |     3+    1-
M  gotd/libexec/gotsys-check/Makefile                           |     2+    2-
M  gotd/libexec/gotsys-check/gotsys-check.c                     |     3+    1-
M  gotsys/Makefile                                              |     2+    2-
M  gotsys/gotsys.c                                              |     2+    0-
M  gotsys/gotsys.conf.5                                         |   210+    1-
M  gotsys/gotsys.h                                              |    69+    0-
M  gotsys/parse.y                                               |   731+  197-
M  gotsysd/Makefile                                             |     3+    2-
M  gotsysd/gotsysd.c                                            |     2+    1-
M  gotsysd/gotsysd.conf.5                                       |   354+    2-
M  gotsysd/gotsysd.h                                            |   147+    1-
M  gotsysd/helpers.c                                            |    74+    0-
M  gotsysd/libexec/Makefile                                     |     1+    1-
M  gotsysd/libexec/gotsys-apply-conf/Makefile                   |     1+    1-
M  gotsysd/libexec/gotsys-apply-conf/gotsys-apply-conf.c        |     3+    0-
A  gotsysd/libexec/gotsys-apply-webconf/Makefile                |    23+    0-
A  gotsysd/libexec/gotsys-apply-webconf/gotsys-apply-webconf.c  |   477+    0-
M  gotsysd/libexec/gotsys-groupadd/Makefile                     |     4+    2-
M  gotsysd/libexec/gotsys-groupadd/gotsys-groupadd.c            |     3+    0-
M  gotsysd/libexec/gotsys-read-conf/Makefile                    |     4+    2-
M  gotsysd/libexec/gotsys-read-conf/gotsys-read-conf.c          |    23+    0-
M  gotsysd/libexec/gotsys-repo-create/Makefile                  |     3+    2-
M  gotsysd/libexec/gotsys-repo-create/gotsys-repo-create.c      |     2+    0-
M  gotsysd/libexec/gotsys-rmkeys/Makefile                       |     5+    3-
M  gotsysd/libexec/gotsys-rmkeys/gotsys-rmkeys.c                |     3+    0-
M  gotsysd/libexec/gotsys-sshdconfig/Makefile                   |     2+    1-
M  gotsysd/libexec/gotsys-sshdconfig/gotsys-sshdconfig.c        |     3+    0-
M  gotsysd/libexec/gotsys-useradd/Makefile                      |     4+    2-
M  gotsysd/libexec/gotsys-useradd/gotsys-useradd.c              |     3+    0-
M  gotsysd/libexec/gotsys-userhome/Makefile                     |     3+    2-
M  gotsysd/libexec/gotsys-userhome/gotsys-userhome.c            |     3+    0-
M  gotsysd/libexec/gotsys-userkeys/Makefile                     |     3+    2-
M  gotsysd/libexec/gotsys-userkeys/gotsys-userkeys.c            |     3+    0-
M  gotsysd/libexec/gotsys-write-conf/Makefile                   |     3+    2-
M  gotsysd/libexec/gotsys-write-conf/gotsys-write-conf.c        |  1147+   42-
M  gotsysd/parse.y                                              |   482+    1-
M  gotsysd/sysconf.c                                            |   545+    9-
M  gotsysd/sysconf.h                                            |     2+    1-
M  include/got_error.h                                          |     1+    0-
M  lib/error.c                                                  |     1+    0-
M  lib/gotsys_conf.c                                            |   577+    0-
M  lib/gotsys_imsg.c                                            |   622+    2-
M  regress/gotsysd/Makefile                                     |    12+  103-
M  regress/gotsysd/test_gotwebd.sh                              |   212+    2-
M  regress/gotsysd/test_gotwebd_repos_www.sh                    |    16+    0-
M  regress/gotsysd/test_gotwebd_www.sh                          |    21+    0-

48 files changed, 5821 insertions(+), 392 deletions(-)

commit - d2d2e88db61f2b062715f1612bf12b51a977c61f
commit + ef70a05fbeefc91c97da4250d0225133f5342194
blob - 1264d96d3c0c3e9065c5303a80a8be4cc6d745a8
blob + 65bb9050e0194255acc591b7a0cd36a606d4c515
--- gotd/libexec/gotsys-apply/Makefile
+++ gotd/libexec/gotsys-apply/Makefile
@@ -10,13 +10,13 @@ SRCS=	gotsys-apply.c bloom.c buf.c date.c deflate.c de
 	object_create.c object_idset.c object_open_io.c object_parse.c \
 	object_qid.c opentemp.c pack.c path.c pollfd.c privsep_stub.c \
 	read_gitconfig.c read_gotconfig.c reference.c reference_parse.c \
-	repository.c sigs.c
+	repository.c sigs.c media.c
 
 NOMAN =	Yes
 
 CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib \
 	-I${.CURDIR}/../../../gotsysd -I${.CURDIR}/../../../gotsys \
-	-I${.CURDIR}/../../../gotd
+	-I${.CURDIR}/../../../gotwebd -I${.CURDIR}/../../../gotd
 
 .if defined(PROFILE)
 LDADD = -lutil_p -levent_p -lm_p -lz_p -lc_p
blob - 5d108b19dd6853d781252c801b0d551376017d63
blob + e9b0b75eb61bca09a252391911da38586a33b02c
--- gotd/libexec/gotsys-apply/gotsys-apply.c
+++ gotd/libexec/gotsys-apply/gotsys-apply.c
@@ -47,8 +47,10 @@
 
 #include "got_lib_poll.h"
 
-#include "gotsys.h"
 #include "gotsysd.h"
+#include "media.h"
+#include "gotwebd.h"
+#include "gotsys.h"
 #include "gotd.h"
 
 #ifndef nitems
blob - 39e46c7c142fde6a79dc25cef8856ab78c88c0a6
blob + 7aeb513254410e60430de2ad1b8ab007ec8de81b
--- gotd/libexec/gotsys-check/Makefile
+++ gotd/libexec/gotsys-check/Makefile
@@ -5,7 +5,7 @@
 
 PROG=	gotsys-check
 SRCS=	gotsys-check.c error.c gotd_imsg.c gotsys_conf.c hash.c parse.y \
-	path.c pollfd.c reference_parse.c
+	path.c pollfd.c reference_parse.c media.c
 
 NOMAN =	Yes
 
@@ -13,7 +13,7 @@ CLEANFILES = parse.c
 
 CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib \
 	-I${.CURDIR}/../../../gotsysd -I${.CURDIR}/../../../gotsys \
-	-I${.CURDIR}/../../../gotd
+	-I${.CURDIR}/../../../gotwebd -I${.CURDIR}/../../../gotd
 
 .if defined(PROFILE)
 LDADD = -lutil_p -levent_p -lc_p
blob - 6f8562879718d80585577311dd3bf39ea6b2cb95
blob + c45574a9813f2637577b661af1ec98666e877f19
--- gotd/libexec/gotsys-check/gotsys-check.c
+++ gotd/libexec/gotsys-check/gotsys-check.c
@@ -44,8 +44,10 @@
 
 #include "got_lib_poll.h"
 
-#include "gotsys.h"
 #include "gotsysd.h"
+#include "media.h"
+#include "gotwebd.h"
+#include "gotsys.h"
 #include "gotd.h"
 
 #ifndef nitems
blob - 4778684fe809d22171e152942d912ca5cb00d959
blob + 95e40ea28281e1f5b19fa816895aff9df1a8c732
--- gotsys/Makefile
+++ gotsys/Makefile
@@ -9,12 +9,12 @@ SRCS=	gotsys.c gotsys_conf.c bloom.c buf.c date.c defl
 	object_create.c object_idset.c object_open_io.c object_parse.c \
 	object_qid.c opentemp.c pack.c parse.y path.c pollfd.c \
 	privsep_stub.c read_gitconfig.c read_gotconfig.c reference.c \
-	reference_parse.c repository.c sigs.c
+	reference_parse.c repository.c sigs.c media.c
 
 MAN =		${PROG}.1 ${PROG}.conf.5
 
 CPPFLAGS = -I${.CURDIR}/../include -I${.CURDIR}/../lib \
-	-I${.CURDIR}/../gotsysd -I${.CURDIR}
+	-I${.CURDIR}/../gotsysd -I${.CURDIR}/../gotwebd -I${.CURDIR}
 
 .if defined(PROFILE)
 LDADD = -lutil_p -lm_p -lz_p -lc_p
blob - 74a5b2285673d08d23af1730c113c137d0f39ee9
blob + 5e9ae4dae66bcbb8c1a62f380c607b129b9fbafb
--- gotsys/gotsys.c
+++ gotsys/gotsys.c
@@ -43,6 +43,8 @@
 #include "got_reference.h"
 #include "got_object.h"
 
+#include "media.h"
+#include "gotwebd.h"
 #include "gotsys.h"
 #include "gotsysd.h"
 
blob - c9c7bfeca5a00b8b60ac22bf91def73339364ed8
blob + 480a9208153a07d8db5947246f0e40892aa925c5
--- gotsys/gotsys.conf.5
+++ gotsys/gotsys.conf.5
@@ -626,6 +626,214 @@ The tag message.
 .El
 .El
 .El
+.Sh WEB SERVER CONFIGURATION
+.Nm
+can set configuration parameters for
+.Xr gotwebd 8 
+to display Git repositories on the web,
+.Pp
+One or more web servers can be declared with the directive:
+.Pp
+.Ic web server Ar name Brq ...
+.Pp
+The given
+.Ar hostname
+should be the name which web browsers use to reach the host running the
+web server.
+.Pp
+All web servers share the same underlying repository directory managed by
+.Xr gotsysd 8 .
+However, each web server can be configured to display an arbitrary subset
+of repositories and/or web sites, providing a unique view on the shared data.
+.Pp
+If the given hostname does not exist in
+.Xr gotsysd.conf 5
+on the server then
+.Xr gotsysd 8
+will silently ignore web server parameters.
+The server administrator can use
+.Xr gotsysd.conf 5
+to set global defaults for web server parameters documented here.
+When in doubt, the correct host name to use and the server-side defaults
+should be obtained from the server administrator.
+.Pp
+Parameters for the web server must be given in curly braces and are as follows:
+.Bl -tag -width Ds
+.It Ic disable authentication
+Disable authentication, allowing any browser to view any web sites and
+any repository not hidden via the
+.Ic hide repositories
+or
+.Ic hide repository
+directives.
+.Pp
+Authentication can also be configured per repository and per web site.
+.It Ic enable authentication
+Enable authentication, requiring browsers to present a login token cookie
+before read-only repository or web site access is granted.
+.Pp
+Browsers presenting a valid login token cookie will be mapped to the
+user account which obtained the login token over SSH from the
+.Cm weblogin
+command of
+.Xr gotsh 1 .
+.Pp
+Authentication can also be configured per repository and per web site.
+.It Ic deny Ar identity
+Deny repository access to users with the username
+.Ar identity .
+Group names may be matched by prepending a colon
+.Pq Sq \&:
+to
+.Ar identity .
+Numeric IDs are also accepted.
+.It Ic permit Ar identity
+Permit repository access to users with the username
+.Ar identity .
+Group names may be matched by prepending a colon
+.Pq Sq \&:
+to
+.Ar identity .
+Numeric IDs are also accepted.
+.It Ic hide repositories Ar on | off
+Controls whether repositories are hidden by default.
+Hidden repositories cannot be browsed via
+.Xr gotwebd 8 .
+However, web sites can be served out of a repository even if the
+repository is hidden.
+.It Ic site owner Ar name
+Set the displayed site owner.
+If not set then no site owner will be displayed by
+.Xr gotwebd 8 .
+.It Ic repositories url path Ar url-path
+Sets the URL path under which Git repositories will be displayed by
+.Xr gotwebd 8 .
+This allows for displaying a web site at the root URL path, while still
+displaying the default
+.Xr gotwebd 8
+repository browser view at another URL path.
+.It Ic repository name Brq ...
+Set repository-specific parameters for
+.Xr gotwebd 8 .
+The given
+.Ar name
+must match a Git repository declared in the
+.Sx REPOSITORY CONFIGURATION
+section of
+.Nm .
+.Pp
+The available parameters are as follows:
+.Bl -tag -width Ds
+.It Ic hide repository Ar on | off
+Controls whether the repository is hidden.
+Hidden repositories cannot be browsed via
+.Xr gotwebd 8 .
+If not set, the web server context's
+.Ic hide repositories
+parameter determines whether
+.Xr gotwebd 8
+will display the repository.
+.Pp
+Web sites can be served out of a repository even if the
+repository is hidden.
+.It Ic disable authentication
+Disable authentication, allowing any browser to view this repository
+unless it is hidden via the
+.Ic hide repositories
+or
+.Ic hide repository
+directives.
+.It Ic enable authentication
+Enable authentication, requiring browsers to present a login token cookie
+before read-only access to this repository is granted.
+.Pp
+Browsers presenting a valid login token cookie will be mapped to the
+user account which obtained the login token over SSH from the
+.Cm weblogin
+command of
+.Xr gotsh 1 .
+.It Ic deny Ar identity
+Deny repository access to users with the username
+.Ar identity .
+Group names may be matched by prepending a colon
+.Pq Sq \&:
+to
+.Ar identity .
+Numeric IDs are also accepted.
+.It Ic permit Ar identity
+Permit repository access to users with the username
+.Ar identity .
+Group names may be matched by prepending a colon
+.Pq Sq \&:
+to
+.Ar identity .
+Numeric IDs are also accepted.
+.El
+.It Ic website Ar url-path Brq ...
+Declare a web site to be served by
+.Xr gotwebd 8
+when the browser visits the given
+.Ar url-path .
+.Pp
+The available web site configuration parameters are as follows:
+.Bl -tag -width Ds
+.It Ic repository Ar name
+Serve web site content from the specified Git repository.
+The given
+.Ar name
+must match a Git repository declared in the
+.Sx REPOSITORY CONFIGURATION
+section of
+.Nm .
+.It Ic path Ar path
+Look up files to serve as web site content at the specified path
+in the repository.
+Defaults to the root directory, 
+.Dq / .
+.It Ic branch Ar name
+Look up files to serve as web site content on the specified branch
+in the repository.
+By default, the branch resolved via the repository's HEAD reference is used.
+.Pp
+If the
+.Ar name
+does not begin with
+.Dq refs/heads
+then the
+.Ar name
+is searched in the
+.Dq refs/heads
+reference namespace.
+.It Ic disable authentication
+Disable authentication, allowing any browser to view this web site.
+.It Ic enable authentication
+Enable authentication, requiring browsers to present a login token cookie
+before web site access is granted.
+.Pp
+Browsers presenting a valid login token cookie will be mapped to the
+user account which obtained the login token over SSH from the
+.Cm weblogin
+command of
+.Xr gotsh 1 .
+.It Ic deny Ar identity
+Deny web site access to users with the username
+.Ar identity .
+Group names may be matched by prepending a colon
+.Pq Sq \&:
+to
+.Ar identity .
+Numeric IDs are also accepted.
+.It Ic permit Ar identity
+Permit web site access to users with the username
+.Ar identity .
+Group names may be matched by prepending a colon
+.Pq Sq \&:
+to
+.Ar identity .
+Numeric IDs are also accepted.
+.El
+\".It Ic media types Brq ...
+.El
 .Sh EXAMPLES
 .Bd -literal -offset indent
 group developers
@@ -679,7 +887,8 @@ repository "secret" {
 .Xr got 1 ,
 .Xr gotsys 1 ,
 .Xr gotd 8 ,
-.Xr gotsysd 8
+.Xr gotsysd 8 ,
+.Xr gotwebd 8
 .Sh CAVEATS
 There is no way to rename or delete repositories via
 .Nm .
blob - c3480115504c26fbb8fed32ea51f34ad55dab693
blob + 56e3a7c711075e647ba019053c7fc6c5c749d724
--- gotsys/gotsys.h
+++ gotsys/gotsys.h
@@ -90,11 +90,62 @@ struct gotsys_access_rule {
 };
 STAILQ_HEAD(gotsys_access_rule_list, gotsys_access_rule);
 
+enum gotsys_auth_config {
+	GOTSYS_AUTH_UNSET	= 0,
+	GOTSYS_AUTH_DISABLED	= 0xf00000ff,
+	GOTSYS_AUTH_ENABLED	= 0x00808000,
+};
+
+struct gotsys_webrepo {
+	STAILQ_ENTRY(gotsys_webrepo) entry;
+
+	char repo_name[NAME_MAX];
+
+	enum gotsys_auth_config auth_config;
+	struct gotsys_access_rule_list access_rules;
+	int hidden;
+};
+STAILQ_HEAD(gotsys_webrepolist, gotsys_webrepo);
+
+struct gotsys_webserver {
+	STAILQ_ENTRY(gotsys_webserver) entry;
+
+	char server_name[MAX_SERVER_NAME];
+
+	enum gotsys_auth_config auth_config;
+	struct gotsys_access_rule_list access_rules;
+	int hide_repositories;
+
+	char css[PATH_MAX];
+	char logo[PATH_MAX];
+	char logo_url[GOTWEBD_MAXTEXT];
+	char site_owner[GOTWEBD_MAXNAME];
+	char repos_url_path[MAX_DOCUMENT_URI];
+
+	struct mediatypes mediatypes;
+
+	struct gotsys_webrepolist repos;
+	struct got_pathlist_head websites;
+};
+STAILQ_HEAD(gotsys_webserverlist, gotsys_webserver);
+
+struct gotsys_website {
+	char repo_name[NAME_MAX];
+
+	enum gotsys_auth_config auth_config;
+	struct gotsys_access_rule_list access_rules;
+
+	char url_path[MAX_DOCUMENT_URI];
+	char branch_name[MAX_BRANCH_NAME];
+	char path[PATH_MAX];
+};
+
 struct gotsys_repo {
 	TAILQ_ENTRY(gotsys_repo) entry;
 
 	char name[NAME_MAX];
 	char *headref;
+	char description[GOTWEBD_MAXDESCRSZ];
 
 	struct gotsys_access_rule_list access_rules;
 
@@ -118,9 +169,16 @@ struct gotsys_conf {
 	struct gotsys_grouplist groups;
 	struct gotsys_repolist repos;
 	int nrepos;
+	struct gotsys_webserverlist webservers;
+	struct mediatypes mediatypes;
 };
 
 void gotsys_conf_init(struct gotsys_conf *);
+const struct got_error *gotsys_conf_new_webserver(struct gotsys_webserver **,
+    const char *);
+const struct got_error *gotsys_conf_new_webrepo(struct gotsys_webrepo **,
+    const char *);
+const struct got_error *gotsys_conf_init_media_types(struct mediatypes *);
 const struct got_error *gotsys_conf_parse(const char *, struct gotsys_conf *,
     int *);
 int gotsys_ref_name_is_valid(char *);
@@ -130,6 +188,8 @@ void gotsys_user_free(struct gotsys_user *);
 void gotsys_userlist_purge(struct gotsys_userlist *);
 void gotsys_group_free(struct gotsys_group *);
 void gotsys_grouplist_purge(struct gotsys_grouplist *);
+void gotsys_webrepo_free(struct gotsys_webrepo *);
+void gotsys_webserver_free(struct gotsys_webserver *);
 void gotsys_access_rule_free(struct gotsys_access_rule *);
 void gotsys_notification_target_free(struct gotsys_notification_target *);
 void gotsys_repo_free(struct gotsys_repo *);
@@ -144,9 +204,18 @@ const struct got_error *gotsys_conf_new_group_member(s
     const char *, const char *);
 const struct got_error *gotsys_conf_new_repo(struct gotsys_repo **,
     const char *);
+const struct got_error *gotsys_conf_new_website(struct gotsys_website **,
+    const char *);
 const struct got_error *gotsys_conf_validate_name(const char *, const char *);
 const struct got_error *gotsys_conf_validate_repo_name(const char *);
 const struct got_error *gotsys_conf_validate_password(const char *, const char *);
+const struct got_error *gotsys_conf_validate_path(const char *);
+const struct got_error *gotsys_conf_validate_hostname(const char *);
+const struct got_error *gotsys_conf_validate_url(const char *);
+const struct got_error *gotsys_conf_parse_url(char **, char **, char **,
+    char **, const char *);
+const struct got_error *gotsys_conf_validate_mediatype(const char *);
+const struct got_error *gotsys_conf_validate_string(const char *);
 const struct got_error *gotsys_conf_new_access_rule(
     struct gotsys_access_rule **, enum gotsys_access, int, const char *,
     struct gotsys_userlist *, struct gotsys_grouplist *);
blob - 959f9ec9928e8cd9b64f94181a41e1e6a6e0c5d3
blob + a34ce1cda8d7732b1dc796415565655e57cdea0f
--- gotsys/parse.y
+++ gotsys/parse.y
@@ -47,9 +47,12 @@
 
 #include "got_error.h"
 #include "got_path.h"
+#include "got_object.h"
 #include "got_reference.h"
 
 #include "log.h"
+#include "media.h"
+#include "gotwebd.h"
 #include "gotsys.h"
 
 #ifndef nitems
@@ -78,11 +81,17 @@ int		 lgetc(int);
 int		 lungetc(int);
 int		 findeol(void);
 
+struct media_type		 media;
+
 static const struct got_error	*gerror;
 
 static struct gotsys_conf	*gotsysconf;
 static struct gotsys_repo	*new_repo;
 static struct gotsys_user	*new_user;
+static struct mediatypes	*new_mediatypes;
+static struct gotsys_webserver	*new_webserver;
+static struct gotsys_webrepo	*new_webrepo;
+static struct gotsys_website	*new_website;
 static const struct got_error	*conf_new_repo(struct gotsys_repo **,
 				    const char *);
 static const struct got_error	*conf_user_password(char *,
@@ -117,19 +126,22 @@ typedef struct {
 
 %token	ERROR USER GROUP REPOSITORY PERMIT DENY RO RW AUTHORIZED KEY
 %token	PROTECT NAMESPACE BRANCH TAG REFERENCE PORT PASSWORD
-%token	NOTIFY EMAIL FROM REPLY TO URL INSECURE HMAC HEAD
+%token	NOTIFY EMAIL FROM REPLY TO URL INSECURE HMAC HEAD WEB SERVER
+%token	HIDE REPOSITORIES STYLESHEET LOGO SITE OWNER WEBSITE PATH
+%token	DISABLE ENABLE AUTHENTICATION MEDIA TYPES DESCRIPTION
 
 %token	<v.string>	STRING
 %token	<v.number>	NUMBER
+%type	<v.number>	boolean
 %type	<v.string>	numberstring
 
 %%
 
 grammar		:
 		| grammar '\n'
-		| grammar varset '\n'
 		| grammar main '\n'
 		| grammar repository '\n'
+		| grammar webserver '\n'
 		;
 
 numberstring	: STRING
@@ -141,6 +153,29 @@ numberstring	: STRING
 		}
 		;
 
+boolean		: STRING {
+			if (strcasecmp($1, "1") == 0 ||
+			    strcasecmp($1, "on") == 0)
+				$$ = 1;
+			else if (strcasecmp($1, "0") == 0 ||
+			    strcasecmp($1, "off") == 0)
+				$$ = 0;
+			else {
+				yyerror("invalid boolean value '%s'", $1);
+				free($1);
+				YYERROR;
+			}
+			free($1);
+		}
+		| NUMBER {
+			if ($1 != 0 && $1 != 1) {
+				yyerror("invalid boolean value '%lld'", $1);
+				YYERROR;
+			}
+			$$ = $1;
+		}
+		;
+
 main		: USER STRING {
 			struct gotsys_user *user;
 			const struct got_error *err = NULL;
@@ -198,6 +233,9 @@ main		: USER STRING {
 			STAILQ_INSERT_TAIL(&gotsysconf->groups, group, entry);
 			free($2);
 		}
+		| MEDIA TYPES '{' optnl mediaopts_l '}' {
+			new_mediatypes = &gotsysconf->mediatypes;
+		}
 		;
 
 useropts1	: GROUP STRING {
@@ -442,6 +480,9 @@ repository	: REPOSITORY STRING {
 		}
 		;
 
+repoopts2	: repoopts2 repoopts1 nl
+		| repoopts1 optnl
+
 repoopts1	: PERMIT RO numberstring {
 			const struct got_error *err;
 			struct gotsys_access_rule *rule;
@@ -552,19 +593,612 @@ repoopts1	: PERMIT RO numberstring {
 			}
 			free($2);
 		}
+		| DESCRIPTION STRING {
+			const struct got_error *err;
+
+			err = gotsys_conf_validate_string($2);
+			if (err) {
+				yyerror("%s", err->msg);
+				free($2);
+				YYERROR;
+			}
+				
+			if (strlcpy(new_repo->description, $2,
+			    sizeof(new_repo->description)) >=
+			    sizeof(new_repo->description)) {
+				yyerror("HTML-escaped repository description "
+				    "too long, exceeds %zd bytes: %s",
+				    sizeof(new_repo->description) - 1, $2);
+				free($2);
+				YYERROR;
+			}
+			free($2);
+		}
 		;
 
-repoopts2	: repoopts2 repoopts1 nl
-		| repoopts1 optnl
+webrepo		: REPOSITORY STRING {
+			const struct got_error *err;
+			struct gotsys_webrepo *webrepo;
+
+			err = gotsys_conf_new_webrepo(&new_webrepo, $2);
+			if (err) {
+				err = got_error_from_errno("asprintf");
+				yyerror("%s", err->msg);
+				free($2);
+				YYERROR;
+			}
+
+			STAILQ_FOREACH(webrepo, &new_webserver->repos, entry) {
+				if (strcmp(webrepo->repo_name,
+				    new_webrepo->repo_name) == 0) {
+					err = got_error_fmt(
+					    GOT_ERR_PARSE_CONFIG,
+					    "duplicate repository '%s' for "
+					    "server '%s'",
+					    new_webrepo->repo_name,
+					    new_webserver->server_name);
+					yyerror("%s", err->msg);
+					free($2);
+					YYERROR;
+				}
+			}
+
+			STAILQ_INSERT_TAIL(&new_webserver->repos, new_webrepo,
+			    entry);
+			free($2);
+		} '{' optnl repowebopts2 '}' {
+		}
+		;
+
+repowebopts2	: repowebopts2 repowebopts1 nl
+		| repowebopts1 optnl
+
+repowebopts1	: HIDE REPOSITORY boolean {
+			new_webrepo->hidden = $3;
+		}
+		| DISABLE AUTHENTICATION {
+			if (new_webrepo->auth_config != 0) {
+				yyerror("ambiguous web authentication "
+				    "setting for repository %s",
+				    new_webrepo->repo_name);
+				YYERROR;
+			}
+			new_webrepo->auth_config = GOTSYS_AUTH_DISABLED;
+		}
+		| ENABLE AUTHENTICATION {
+			if (new_webrepo->auth_config != 0) {
+				yyerror("ambiguous web authentication "
+				    "setting for repository %s",
+				    new_webrepo->repo_name);
+				YYERROR;
+			}
+			new_webrepo->auth_config = GOTSYS_AUTH_ENABLED;
+		}
+		| DENY numberstring {
+			const struct got_error *err;
+			struct gotsys_access_rule *rule;
+
+			err = gotsys_conf_new_access_rule(&rule,
+			    GOTSYS_ACCESS_DENIED, 0, $2,
+			    &gotsysconf->users, &gotsysconf->groups);
+			if (err) {
+				yyerror("%s", err->msg);
+				free($2);
+				YYERROR;
+			}
+			STAILQ_INSERT_TAIL(&new_webrepo->access_rules, rule,
+			    entry);
+			free($2);
+		}
+		| PERMIT numberstring {
+			const struct got_error *err;
+			struct gotsys_access_rule *rule;
+
+			err = gotsys_conf_new_access_rule(&rule,
+			    GOTSYS_ACCESS_PERMITTED, GOTSYS_AUTH_READ, $2,
+			    &gotsysconf->users, &gotsysconf->groups);
+			if (err) {
+				yyerror("%s", err->msg);
+				free($2);
+				YYERROR;
+			}
+			STAILQ_INSERT_TAIL(&new_webrepo->access_rules, rule,
+			    entry);
+			free($2);
+		}
+		;
+
+websiteopts2	: websiteopts2 websiteopts1 nl
+		| websiteopts1 optnl
+
+websiteopts1	: PATH STRING {
+			const struct got_error *err;
+			size_t n;
+
+			err = gotsys_conf_validate_path($2);
+			if (err) {
+				yyerror("path %s: %s", $2, err->msg);
+				free($2);
+				YYERROR;
+			}
+
+			n = strlcpy(new_website->path, $2,
+			    sizeof(new_website->path));
+			if (n >= sizeof(new_website->path)) {
+				yyerror("website in-repository path too long, "
+				    "exceeds %zd bytes",
+				    sizeof(new_website->path) - 1);
+				free($2);
+				YYERROR;
+			}
+
+			if (new_website->path[0] != '/') {
+				yyerror("a website path must be an absolute "
+				    "path: bad path %s", $2);
+				free($2);
+				YYERROR;
+			}
+
+			free($2);
+		}
+		| REPOSITORY STRING {
+			const struct got_error *err;
+			size_t n;
+
+			err = gotsys_conf_validate_repo_name($2);
+			if (err) {
+				yyerror("repository name %s: %s", $2, err->msg);
+				free($2);
+				YYERROR;
+			}
+
+			n = strlcpy(new_website->repo_name, $2,
+			    sizeof(new_website->repo_name));
+			if (n >= sizeof(new_website->repo_name)) {
+				yyerror("website repository name too long, "
+				    "exceeds %zd bytes",
+				    sizeof(new_website->path) - 1);
+				free($2);
+				YYERROR;
+			}
+
+			free($2);
+		}
+		| BRANCH STRING {
+			size_t n;
+
+			if (!branch_name_is_valid($2)) {
+				yyerror("invalid reference name %s", $2);
+				free($2);
+				YYERROR;
+			}
+
+			n = strlcpy(new_website->branch_name, $2,
+			    sizeof(new_website->branch_name));
+			if (n >= sizeof(new_website->branch_name)) {
+				yyerror("website branch name too long, "
+				    "exceeds %zd bytes",
+				    sizeof(new_website->branch_name) - 1);
+				free($2);
+				YYERROR;
+			}
+			free($2);
+		}
+		| DISABLE AUTHENTICATION {
+			if (new_website->auth_config != 0) {
+				yyerror("ambiguous authentication "
+				    "setting for website %s",
+				    new_website->path);
+				YYERROR;
+			}
+			new_website->auth_config = GOTSYS_AUTH_DISABLED;
+		}
+		| ENABLE AUTHENTICATION {
+			if (new_website->auth_config != 0) {
+				yyerror("ambiguous authentication "
+				    "setting for website %s",
+				    new_website->path);
+				YYERROR;
+			}
+			new_website->auth_config = GOTSYS_AUTH_ENABLED;
+		}
+		| PERMIT numberstring {
+			const struct got_error *err;
+			struct gotsys_access_rule *rule;
+
+			err = gotsys_conf_new_access_rule(&rule,
+			    GOTSYS_ACCESS_PERMITTED, GOTSYS_AUTH_READ, $2,
+			    &gotsysconf->users, &gotsysconf->groups);
+			if (err) {
+				yyerror("%s", err->msg);
+				free($2);
+				YYERROR;
+			}
+			STAILQ_INSERT_TAIL(&new_website->access_rules, rule,
+			    entry);
+			free($2);
+		}
+		| DENY numberstring {
+			const struct got_error *err;
+			struct gotsys_access_rule *rule;
+
+			err = gotsys_conf_new_access_rule(&rule,
+			    GOTSYS_ACCESS_DENIED, 0, $2,
+			    &gotsysconf->users, &gotsysconf->groups);
+			if (err) {
+				yyerror("%s", err->msg);
+				free($2);
+				YYERROR;
+			}
+			STAILQ_INSERT_TAIL(&new_website->access_rules, rule,
+			    entry);
+			free($2);
+		}
+		;
+
+website		: WEBSITE STRING {
+			const struct got_error *err;
+			struct got_pathlist_entry *new;
+
+			err = gotsys_conf_new_website(&new_website, $2);
+			if (err) {
+				yyerror("%s", err->msg);
+				free($2);
+				YYERROR;
+			}
+
+			err = got_pathlist_insert(&new,
+			    &new_webserver->websites, new_website->url_path,
+			    new_website);
+			if (err) {
+				yyerror("%s", err->msg);
+				free($2);
+				YYERROR;
+			}
+			if (new == NULL) {
+				err = got_error_fmt(GOT_ERR_PARSE_CONFIG,
+				    "duplicate web site '%s' in "
+				    "web server '%s'", new_website->url_path,
+				    new_webserver->server_name);
+				yyerror("%s", err->msg);
+				free($2);
+				YYERROR;
+			}
+			free($2);
+		} '{' optnl websiteopts2 '}' {
+		}
+		;
+
+webserver	: WEB SERVER STRING {
+			const struct got_error *err;
+			struct gotsys_webserver *srv;
+
+			STAILQ_FOREACH(srv, &gotsysconf->webservers, entry) {
+				if (strcasecmp(srv->server_name, $3) != 0)
+					continue;
+
+				err = got_error_fmt(GOT_ERR_PARSE_CONFIG,
+				    "duplicate web server '%s' clashes "
+				    "with web server '%s'", $3,
+				    srv->server_name);
+				yyerror("%s", err->msg);
+				free($3);
+				YYERROR;
+			}
+
+			err = gotsys_conf_new_webserver(&new_webserver, $3);
+			if (err) {
+				yyerror("%s", err->msg);
+				free($3);
+				YYERROR;
+			}
+
+			STAILQ_INSERT_TAIL(&gotsysconf->webservers,
+			    new_webserver, entry);
+
+			free($3);
+		} '{' optnl webopts2 '}' {
+		}
 		;
 
+webopts1	: DISABLE AUTHENTICATION {
+			if (new_webserver->auth_config != 0) {
+				yyerror("ambiguous web authentication setting");
+				YYERROR;
+			}
+			new_webserver->auth_config = GOTSYS_AUTH_DISABLED;
+		}
+		| ENABLE AUTHENTICATION {
+			if (new_webserver->auth_config != 0) {
+				yyerror("ambiguous web authentication setting");
+				YYERROR;
+			}
+			new_webserver->auth_config = GOTSYS_AUTH_ENABLED;
+		}
+		| DENY numberstring {
+			const struct got_error *err;
+			struct gotsys_access_rule *rule;
+
+			err = gotsys_conf_new_access_rule(&rule,
+			    GOTSYS_ACCESS_DENIED, 0, $2,
+			    &gotsysconf->users, &gotsysconf->groups);
+			if (err) {
+				yyerror("%s", err->msg);
+				free($2);
+				YYERROR;
+			}
+			STAILQ_INSERT_TAIL(&new_webserver->access_rules, rule,
+			    entry);
+			free($2);
+		}
+		| PERMIT numberstring {
+			const struct got_error *err;
+			struct gotsys_access_rule *rule;
+
+			err = gotsys_conf_new_access_rule(&rule,
+			    GOTSYS_ACCESS_PERMITTED, GOTSYS_AUTH_READ, $2,
+			    &gotsysconf->users, &gotsysconf->groups);
+			if (err) {
+				yyerror("%s", err->msg);
+				free($2);
+				YYERROR;
+			}
+			STAILQ_INSERT_TAIL(&new_webserver->access_rules, rule,
+			    entry);
+			free($2);
+		}
+		| HIDE REPOSITORIES boolean {
+			new_webserver->hide_repositories = $3;
+		}
+		| STYLESHEET STRING {
+			const struct got_error *err;
+			size_t n;
+
+			err = gotsys_conf_validate_path($2);
+			if (err) {
+				yyerror("stylesheet path %s: %s", $2, err->msg);
+				free($2);
+				YYERROR;
+			}
+
+			n = strlcpy(new_webserver->css, $2,
+			    sizeof(new_webserver->css));
+			if (n >= sizeof(new_webserver->css)) {
+				yyerror("stylesheet path too long, "
+				    "exceeds %zd bytes",
+				    sizeof(new_webserver->css) - 1);
+				free($2);
+				YYERROR;
+			}
+			free($2);
+		}
+		| LOGO STRING {
+			const struct got_error *err;
+			size_t n;
+
+			err = gotsys_conf_validate_path($2);
+			if (err) {
+				yyerror("logo path %s: %s", $2, err->msg);
+				free($2);
+				YYERROR;
+			}
+
+			n = strlcpy(new_webserver->logo, $2,
+			    sizeof(new_webserver->logo));
+			if (n >= sizeof(new_webserver->logo)) {
+				yyerror("logo path too long, exceeds %zd bytes",
+				    sizeof(new_webserver->logo) - 1);
+				free($2);
+				YYERROR;
+			}
+			free($2);
+		}
+		| LOGO URL STRING {
+			const struct got_error *err;
+			size_t n;
+
+			err = gotsys_conf_validate_url($3);
+			if (err) {
+				yyerror("logo url %s: %s", $3, err->msg);
+				free($3);
+				YYERROR;
+			}
+
+			n = strlcpy(new_webserver->logo_url, $3,
+			    sizeof(new_webserver->logo_url));
+			if (n >= sizeof(new_webserver->logo_url)) {
+				yyerror("logo URL too long, "
+				    "exceeds %zd bytes: %s",
+				    sizeof(new_webserver->logo_url) -1, $3);
+				free($3);
+				YYERROR;
+			}
+			free($3);
+		}
+		| SITE OWNER STRING {
+			const struct got_error *err;
+
+			err = gotsys_conf_validate_string($3);
+			if (err) {
+				yyerror("%s", err->msg);
+				free($3);
+				YYERROR;
+			}
+				
+			if (strlcpy(new_webserver->site_owner, $3,
+			    sizeof(new_webserver->site_owner))
+			    >= sizeof(new_webserver->site_owner)) {
+				yyerror("site owner too long, "
+				    "exceeds %zd bytes: %s",
+				    sizeof(new_webserver->site_owner) - 1, $3);
+				free($3);
+				YYERROR;
+			}
+			free($3);
+		}
+		| REPOSITORIES URL PATH STRING {
+			const struct got_error *err;
+			struct gotsys_webserver *srv = new_webserver;
+
+			if (*$4 == '\0') {
+				yyerror("repositories URL path can't be "
+				    "an empty string");
+				free($4);
+				YYERROR;
+			}
+
+			if (!got_path_is_root_dir($4))
+				got_path_strip_trailing_slashes($4);
+
+			if (!got_path_is_absolute($4)) {
+				int ret;
+
+				ret = snprintf(srv->repos_url_path,
+				    sizeof(srv->repos_url_path),
+				    "/%s", $4);
+				if (ret == -1) {
+					yyerror("snprintf: %s",
+					    strerror(errno));
+					free($4);
+					YYERROR;
+				}
+				if ((size_t)ret >=
+				    sizeof(srv->repos_url_path)) {
+					yyerror("repositories URL path too "
+					    "long, exceeds %zd bytes: %s",
+					    sizeof(srv->repos_url_path), $4);
+					free($4);
+					YYERROR;
+				}
+			} else {
+				size_t n;
+
+				n = strlcpy(srv->repos_url_path, $4,
+				    sizeof(srv->repos_url_path));
+				if (n >= sizeof(srv->repos_url_path)) {
+					yyerror("repositories URL path too "
+					    "long, exceeds %zd bytes: %s",
+					    sizeof(srv->repos_url_path),
+					    $4);
+					free($4);
+					YYERROR;
+				}
+			}
+
+			err = gotsys_conf_validate_path(srv->repos_url_path);
+			if (err) {
+				yyerror("repositories URL path %s: %s",
+				    srv->repos_url_path, err->msg);
+				free($4);
+				YYERROR;
+			}
+			free($4);
+		}
+		| MEDIA TYPES {
+			new_mediatypes = &new_webserver->mediatypes;
+		} '{' optnl mediaopts_l '}' {
+		}
+		| webrepo
+		| website
+		; 
+
+webopts2	: webopts2 webopts1 nl
+		| webopts1 optnl
+		;
+
+mediaopts_l	: mediaopts_l mediaoptsl nl
+		| mediaoptsl nl
+		;
+
+mediaoptsl	: mediastring medianames_l optsemicolon
+		;
+
+mediastring	: STRING {
+			const struct got_error *err;
+			char *slash, *type, *subtype;
+
+			slash = strchr($1, '/');
+			if (slash == NULL) {
+				yyerror("%s: malformed media type", $1);
+				free($1);
+				YYERROR;
+			}
+
+
+			type = $1;
+			*slash = '\0';
+			subtype = &slash[1];
+
+			err = gotsys_conf_validate_mediatype(type);
+			if (err) {
+				yyerror("%s: %s", $1, err->msg);
+				free($1);
+				YYERROR;
+			}
+
+			err = gotsys_conf_validate_mediatype(subtype);
+			if (err) {
+				yyerror("%s: %s", $1, err->msg);
+				free($1);
+				YYERROR;
+			}
+
+			if (strlcpy(media.media_type, type,
+			    sizeof(media.media_type)) >=
+			    sizeof(media.media_type) ||
+			    strlcpy(media.media_subtype, subtype,
+			    sizeof(media.media_subtype)) >=
+			    sizeof(media.media_subtype)) {
+				yyerror("%s: media type too long", $1);
+				free($1);
+				YYERROR;
+			}
+			free($1);
+		}
+		;
+
+medianames_l	: medianames_l medianamesl
+		| medianamesl
+		;
+
+medianamesl	: numberstring {
+			const struct got_error *err;
+
+			err = gotsys_conf_validate_mediatype($1);
+			if (err) {
+				yyerror("%s: %s", $1, err->msg);
+				free($1);
+				YYERROR;
+			}
+
+			if (strlcpy(media.media_name, $1,
+			    sizeof(media.media_name)) >=
+			    sizeof(media.media_name)) {
+				yyerror("%s: media name too long", $1);
+				free($1);
+				YYERROR;
+			}
+			free($1);
+
+			if (media_add(new_mediatypes, &media) == NULL) {
+				yyerror("%s/%s %s: failed to add media type",
+				    media.media_type, media.media_subtype,
+				    media.media_name);
+				YYERROR;
+			}
+		}
+		;
+
 nl		: '\n' optnl
 		;
 
+optsemicolon	: ';'
+		| /* empty */
+		;
+
 optnl		: '\n' optnl		/* zero or more newlines */
 		| /* empty */
 		;
-
 %%
 
 struct keywords {
@@ -611,31 +1245,48 @@ lookup(char *s)
 {
 	/* This has to be sorted always. */
 	static const struct keywords keywords[] = {
+		{ "authentication",		AUTHENTICATION },
 		{ "authorized",			AUTHORIZED },
 		{ "branch",			BRANCH },
 		{ "deny",			DENY },
+		{ "description",		DESCRIPTION },
+		{ "disable",			DISABLE },
 		{ "email",			EMAIL },
+		{ "enable",			ENABLE },
 		{ "from",			FROM },
 		{ "group",			GROUP },
 		{ "head",			HEAD },
+		{ "hide",			HIDE },
 		{ "hmac",			HMAC },
 		{ "insecure",			INSECURE },
 		{ "key",			KEY },
+		{ "logo",			LOGO },
+		{ "media",			MEDIA },
 		{ "namespace",			NAMESPACE },
 		{ "notify",			NOTIFY },
+		{ "owner",			OWNER },
 		{ "password",			PASSWORD },
+		{ "path",			PATH },
 		{ "permit",			PERMIT },
 		{ "port",			PORT },
 		{ "protect",			PROTECT },
 		{ "reference",			REFERENCE },
 		{ "reply",			REPLY },
+		{ "repositories",		REPOSITORIES },
 		{ "repository",			REPOSITORY },
 		{ "ro",				RO },
 		{ "rw",				RW },
+		{ "server",			SERVER },
+		{ "site",			SITE },
+		{ "stylesheet",			STYLESHEET },
 		{ "tag",			TAG },
 		{ "to",				TO },
+		{ "types",			TYPES },
 		{ "url",			URL },
 		{ "user",			USER },
+		{ "web",			WEB },
+		{ "website",			WEBSITE },
+
 	};
 	const struct keywords *p;
 
@@ -902,6 +1553,7 @@ gotsys_conf_parse(const char *filename, struct gotsys_
 	struct gotsys_user *user;
 	struct gotsys_repo *repo;
 	struct gotsys_access_rule *rule;
+	struct gotsys_webserver *srv;
 
 	gotsysconf = pgotsysconf;
 
@@ -968,6 +1620,62 @@ gotsys_conf_parse(const char *filename, struct gotsys_
 		    "repository %s.git", GOTSYS_SYSTEM_REPOSITORY_NAME);
 	}
 
+	STAILQ_FOREACH(srv, &gotsysconf->webservers, entry) {
+		struct gotsys_webrepo *webrepo;
+		struct gotsys_repo *repo;
+		struct got_pathlist_entry *pe;
+
+		STAILQ_FOREACH(webrepo, &srv->repos, entry) {
+			if (webrepo->auth_config == GOTSYS_AUTH_UNSET &&
+			    srv->auth_config != GOTSYS_AUTH_UNSET)
+				webrepo->auth_config = srv->auth_config;
+
+			if (webrepo->hidden == -1 &&
+			    srv->hide_repositories != -1)
+				webrepo->hidden = srv->hide_repositories;
+
+			TAILQ_FOREACH(repo, &gotsysconf->repos, entry) {
+				if (strcmp(repo->name, webrepo->repo_name) == 0)
+					break;
+			}
+			if (repo == NULL) {
+				return got_error_fmt(GOT_ERR_PARSE_CONFIG,
+				    "unknown repository '%s' used on "
+				    "web server '%s'",
+				    webrepo->repo_name, srv->server_name);
+			}
+		}
+
+		RB_FOREACH(pe, got_pathlist_head, &srv->websites) {
+			struct gotsys_website *site = pe->data;
+
+			if (site->auth_config == GOTSYS_AUTH_UNSET &&
+			    srv->auth_config != GOTSYS_AUTH_UNSET)
+				site->auth_config = srv->auth_config;
+
+			TAILQ_FOREACH(repo, &gotsysconf->repos, entry) {
+				if (site->repo_name[0] == '\0') {
+					return got_error_fmt(
+					    GOT_ERR_PARSE_CONFIG, "no "
+					    "repository defined for website "
+					    "'%s' on server %s", pe->path,
+					    srv->server_name);
+				}
+
+				if (strcmp(repo->name, site->repo_name) == 0)
+					break;
+			}
+
+			if (repo == NULL) {
+				return got_error_fmt(GOT_ERR_PARSE_CONFIG,
+				    "unknown repository '%s' used for "
+				    "website '%s' on server '%s'",
+				    site->repo_name, pe->path,
+				    srv->server_name);
+			}
+		}
+	}
+
 	return NULL;
 }
 
@@ -1065,6 +1773,18 @@ gotsys_ref_name_is_valid(char *refname)
 }
 
 static int
+branch_name_is_valid(char *refname)
+{
+	if (!got_ref_name_is_valid(refname) ||
+	    !gotsys_ref_name_is_valid(refname)) {
+		yyerror("invalid reference name: %s", refname);
+		return 0;
+	}
+
+	return 1;
+}
+
+static int
 refname_is_valid(char *refname)
 {
 	if (strncmp(refname, "refs/", 5) != 0) {
@@ -1073,13 +1793,7 @@ refname_is_valid(char *refname)
 		return 0;
 	}
 
-	if (!got_ref_name_is_valid(refname) ||
-	    !gotsys_ref_name_is_valid(refname)) {
-		yyerror("invalid reference name: %s", refname);
-		return 0;
-	}
-
-	return 1;
+	return branch_name_is_valid(refname);
 }
 
 static int
@@ -1401,190 +2115,6 @@ free_target:
 	return -1;
 }
 
-static inline int
-should_urlencode(int c)
-{
-	if (c <= ' ' || c >= 127)
-		return 1;
-
-	switch (c) {
-		/* gen-delim */
-	case ':':
-	case '/':
-	case '?':
-	case '#':
-	case '[':
-	case ']':
-	case '@':
-		/* sub-delims */
-	case '!':
-	case '$':
-	case '&':
-	case '\'':
-	case '(':
-	case ')':
-	case '*':
-	case '+':
-	case ',':
-	case ';':
-	case '=':
-		/* needed because the URLs are embedded into gotd.conf */
-	case '\"':
-		return 1;
-	default:
-		return 0;
-	}
-}
-
-static char *
-urlencode(const char *str)
-{
-	const char *s;
-	char *escaped;
-	size_t i, len;
-	int a, b;
-
-	len = 0;
-	for (s = str; *s; ++s) {
-		len++;
-		if (len == 1 && *s == '/')
-			continue;
-		if (should_urlencode(*s))
-			len += 2;
-	}
-
-	escaped = calloc(1, len + 1);
-	if (escaped == NULL)
-		return NULL;
-
-	i = 0;
-	for (s = str; *s; ++s) {
-		if (i == 0 && *s == '/') {
-			escaped[i++] = *s;
-			continue;
-		}
-		if (should_urlencode(*s)) {
-			a = (*s & 0xF0) >> 4;
-			b = (*s & 0x0F);
-
-			escaped[i++] = '%';
-			escaped[i++] = a <= 9 ? ('0' + a) : ('7' + a);
-			escaped[i++] = b <= 9 ? ('0' + b) : ('7' + b);
-		} else
-			escaped[i++] = *s;
-	}
-
-	return escaped;
-}
-
-static const struct got_error *
-parse_url(char **proto, char **host, char **port,
-    char **request_path, const char *url)
-{
-	const struct got_error *err = NULL;
-	char *s, *p, *q;
-	size_t i, host_len;
-
-	*proto = *host = *port = *request_path = NULL;
-
-	p = strstr(url, "://");
-	if (!p) {
-		return got_error_msg(GOT_ERR_PARSE_URI,
-		    "no protocol specified");
-	}
-
-	*proto = strndup(url, p - url);
-	if (*proto == NULL) {
-		err = got_error_from_errno("strndup");
-		goto done;
-	}
-	s = p + 3;
-
-	p = strstr(s, "/");
-	if (p == NULL)
-		p = strchr(s, '\0');
-
-	q = memchr(s, ':', p - s);
-	if (q) {
-		*host = strndup(s, q - s);
-		if (*host == NULL) {
-			err = got_error_from_errno("strndup");
-			goto done;
-		}
-		if ((*host)[0] == '\0') {
-			err = got_error(GOT_ERR_PARSE_URI);
-			goto done;
-		}
-		*port = strndup(q + 1, p - (q + 1));
-		if (*port == NULL) {
-			err = got_error_from_errno("strndup");
-			goto done;
-		}
-		if ((*port)[0] == '\0') {
-			err = got_error(GOT_ERR_PARSE_URI);
-			goto done;
-		}
-		if (strcmp(*port, "http") != 0 &&
-		    strcmp(*port, "https") != 0) {
-			const char *errstr;
-
-			(void)strtonum(*port, 1, USHRT_MAX, &errstr);
-			if (errstr != NULL) {
-				err = got_error_fmt(GOT_ERR_PARSE_URI,
-				    "port number '%s' is %s", *port, errstr);
-				goto done;
-			}
-		}
-	} else {
-		*host = strndup(s, p - s);
-		if (*host == NULL) {
-			err = got_error_from_errno("strndup");
-			goto done;
-		}
-		if ((*host)[0] == '\0') {
-			err = got_error_msg(GOT_ERR_PARSE_URI,
-			    "hostname cannot be empty");
-			goto done;
-		}
-	}
-
-	host_len = strlen(*host);
-	for (i = 0; i < host_len; i++) {
-		if (isalnum((unsigned char)(*host)[i]) ||
-		    (*host)[i] == '.' || (*host)[i] == '-')
-			continue;
-		err = got_error_fmt(GOT_ERR_PARSE_URI,
-		    "invalid hostname: %s", *host);
-		goto done;
-
-	}
-
-	while (p[0] == '/' && p[1] == '/')
-		p++;
-	if (p[0] == '\0') {
-		*request_path = strdup("/");
-		if (*request_path == NULL) {
-			err = got_error_from_errno("strdup");
-		}
-	} else {
-		*request_path = urlencode(p);
-		if (*request_path == NULL)
-			err = got_error_from_errno("calloc");
-	}
-done:
-	if (err) {
-		free(*proto);
-		*proto = NULL;
-		free(*host);
-		*host = NULL;
-		free(*port);
-		*port = NULL;
-		free(*request_path);
-		*request_path = NULL;
-	}
-	return err;
-}
-
 static int
 basic_auth_user_is_valid(const char *s)
 {
@@ -1656,7 +2186,7 @@ conf_notify_http(struct gotsys_repo *repo, char *url, 
 	char *proto, *hostname, *port, *path;
 	int tls = 0, ret = -1;
 
-	error = parse_url(&proto, &hostname, &port, &path, url);
+	error = gotsys_conf_parse_url(&proto, &hostname, &port, &path, url);
 	if (error) {
 		yyerror("invalid HTTP notification URL '%s' in "
 		    "repository '%s': %s", url, repo->name, error->msg);
@@ -1671,6 +2201,10 @@ conf_notify_http(struct gotsys_repo *repo, char *url, 
 		goto done;
 	}
 
+	error = gotsys_conf_validate_hostname(hostname);
+	if (error)
+		goto done;
+
 	if (port == NULL) {
 		if (strcmp(proto, "http") == 0)
 			port = strdup("80");
blob - 89164dc1c11cbfc0140ad8a975c76a2d34ddebe3
blob + d6657cc641f50d56c8989ad23fd8fa878eece754
--- gotsysd/Makefile
+++ gotsysd/Makefile
@@ -10,14 +10,15 @@ BINDIR ?=	${PREFIX}/sbin
 
 PROG=		gotsysd
 SRCS=		gotsysd.c log.c error.c pollfd.c hash.c imsg.c parse.y path.c \
-		listen.c auth.c helpers.c sysconf.c gotsys_conf.c gotsys_imsg.c
+		listen.c auth.c helpers.c sysconf.c media.c gotsys_conf.c \
+		gotsys_imsg.c reference_parse.c
 
 CLEANFILES = parse.h
 
 MAN =		${PROG}.conf.5 ${PROG}.8
 
 CPPFLAGS = -I${.CURDIR}/../include -I${.CURDIR}/../lib -I${.CURDIR}/../gotsys \
-	-I${.CURDIR}
+	-I${.CURDIR}/../gotwebd -I${.CURDIR}
 YFLAGS =
 
 .if defined(PROFILE)
blob - 59f265132b4c3bb5c9a65a0446dcdab1fd40b67d
blob + 0178c4c160d5e76894b32d104602144f1435b3d2
--- gotsysd/gotsysd.c
+++ gotsysd/gotsysd.c
@@ -1468,6 +1468,7 @@ apply_unveil_priv_helpers(void)
 	    GOTSYSD_PATH_PROG_GROUPADD,
 	    GOTSYSD_PATH_PROG_WRITE_CONF,
 	    GOTSYSD_PATH_PROG_APPLY_CONF,
+	    GOTSYSD_PATH_PROG_APPLY_WEBCONF,
 	    GOTSYSD_PATH_PROG_SSHDCONFIG,
 	};
 	size_t i;
@@ -1933,7 +1934,7 @@ main(int argc, char **argv)
 		apply_unveil_none();
 
 		sysconf_main(title, gotsysd.uid_start, gotsysd.uid_end,
-		    gotsysd.global_repo_access_rules);
+		    gotsysd.global_repo_access_rules, &gotsysd.web);
 		/* NOTREACHED */
 		break;
 	default:
blob - 9ff79bd55e851c72c79fb7789521958b3eb86210
blob + 8b4a9ab369a5b0ebaa47004722ab8ea7223508e6
--- gotsysd/gotsysd.conf.5
+++ gotsysd/gotsysd.conf.5
@@ -45,7 +45,7 @@ path = "/var/run/gotsysd.sock"
 listen on $path
 .Ed
 .Sh GLOBAL CONFIGURATION
- The available global configuration directives are as follows:
+The available global configuration directives are as follows:
 .Bl -tag -width Ds
 .It Ic gotd Ic user Ar user
 The name of the
@@ -189,7 +189,323 @@ and to the
 user.
 If not specified, the user _gotsysd will be used.
 Numeric user IDs are also accepted.
+.EL
+.Sh WEB SERVER CONFIGURATION
+.Xr gotsysd 8
+can automatically manage 
+.Xr gotwebd 8
+by generating a
+.Xr gotwebd.conf 5
+configuration file based on configuration directives in
+.Nm
+and in
+.Xr gotsys.conf 5 ,
+and then starting or restarting
+.Xr gotwebd 8
+to apply configuration changes.
+.Pp
+To activate management of
+.Xr gotwebd 8
+by 
+.Xr gotsysd 8 ,
+at least one
+.Xr gotwebd.conf 5
+.Ic server
+must be declared by using the
+.Ic web server
+configuration directive in
+.Nm .
+.Pp
+Additionally, global parameters for
+.Xr gotwebd 8
+can be set using the
+.Ic gotweb
+configuration directive in
+.Nm .
+.Pp
+While
+.Xr gotwebd.conf 5
+will be automatically generated by
+.Xr gotsysd 8 ,
+the system administrator must manually
+configure
+.Xr httpd 8
+or another web server and make sure that appropriate requests are forwarded to
+.Xr gotwebd 8
+via FastCGI.
+.Pp
+Global parameters for
+.Xr gotwebd 8 ,
+declared inside curly braces of the
+.Ic gotweb Brq ...
+configuration directive, are as follows:
+.Bl -tag -width Ds
+.It Ic control socket Pa path
+Set the
+.Ar path
+to the
+.Ux Ns -domain
+socket for
+.Xr gotwebctl 8
+commands.
+By default the path
+.Pa /var/run/gotwebd.sock
+will be used.
+.It Ic prefork Ar number
+Spawn enough processes such that
+.Ar number
+requests can be handled in parallel.
+By default,
+.Xr gotwebd 8
+will handle up to 3 requests in parallel.
+The maximum allowed is 32.
+.It Ic chroot Pa path
+Set the path to the
+.Xr chroot 2
+environment of
+.Xr httpd 8 .
+If not specified, it defaults to
+.Pa /var/www ,
+the home directory of the www user.
+Setting the
+.Ar path
+to
+.Pa /
+effectively disables chroot.
+.It Ic htdocs Pa path
+Set the path to the directory which contains static files linked from
+HTML generated by
+.Xr gotwebd 8 ,
+such as
+.Pa gotweb.css .
+The specified
+.Ar path
+will be looked up relative to the
+.Ic chroot
+directory of
+.Xr httpd 8 .
+If not specified then
+.Pa htdocs/gotwebd
+will be used.
+.Pp
+The global
+.Ic htdocs
+directive can be overridden by
+.Nm .
+.Ic web server
+directives.
+.It Ic disable authentication
+Disable authentication, allowing any browser to view any repository not
+hidden via
+.Ic hide repositories
+and
+.Ic hide repository
+directives in either
+.Nm
+or
+.Xr gotsys.conf 5 .
+.Pp
+The global
+.Ic disable authentication
+directive can be overridden by
+.Ic web server
+directives in
+.Nm
+or
+.Xr gotsys.conf 5 .
+.It Ic enable authentication Oo Ic insecure Oc
+Enable authentication, requiring browsers to present a login token cookie
+before read-only repository access is granted.
+Unless the
+.Ic insecure
+keyword is used, the login token cookie will be marked as
+.Dq Secure ,
+which causes browsers to only send the cookie when connected to the
+web server over a TLS connection.
+.Pp
+The global
+.Ic enable authentication
+directive can be overridden by
+.Ic web server
+directives in
+.Nm
+or
+.Xr gotsys.conf 5 .
+However,
+.Xr gotsys.conf 5
+deliberately lacks the
+.Ic insecure
+keyword.
+.It Ic login hint user Ar name
+Sets the user name displayed in login hints which are shown on the error
+page if authentication has failed.
+.Pp
+If not set then no login hint will be displayed and users will somehow
+need to learn about using the
+.Xr gotsh 1
+weblogin command via other means.
+.It Ic login hint port Ar number
+Sets the SSH port number displayed in login hints which are shown on the error
+page if authentication has failed.
+.It Ic user Ar name
+Set the
+.Ar user
+which runs
+.Xr gotwebd 8 .
+Defaults to the user _gotwebd.
+.It Ic www user Ar name
+Set the
+.Ar user
+which runs
+.Xr httpd 8 .
+Defaults to the user www.
+.It Ic listen on socket Ar path
+Configure a
+.Ux Ns -domain
+socket for incoming FastCGI connections.
+May be specified multiple times to build up a list of listening sockets.
+.Pp
+While the specified
+.Ar path
+must be absolute, it should usually point inside the web server's chroot
+directory such that the web server can access the socket.
+.Pp
+If no
+.Ic listen
+directive is used,
+.Xr gotwebd 8
+will listen on the
+.Ux Ns -domain
+socket at
+.Pa /var/www/run/gotweb.sock .
+.It Ic listen on Ar address Ic port Ar number
+Configure an address and port for incoming FastCGI connections.
+May be specified multiple times to build up a list of listening sockets.
+.Pp
+Valid
+.Ar address
+arguments are hostnames, IPv4 and IPv6 addresses.
+The
+.Ar port
+argument may be number or a service name defined in
+.Xr services 5 .
+.Pp
+If no
+.Ic listen
+directive is used,
+.Xr gotwebd 8
+will listen on the
+.Ux Ns -domain
+socket at
+.Pa /var/www/run/gotweb.sock .
 .El
+.Pp
+A
+.Xr gotwebd.conf 5
+.Ic server
+is declared with:
+.Pp
+.Ic web server Ar hostname
+.Pp
+The
+.Ar hostname
+should be the name which web browsers use to reach the host running the
+instance of
+.Xr httpd 8
+which forwards requests to
+.Xr gotwebd 8 .
+.Pp
+Optional parameters for the web server may be given in curly braces:
+.Pp
+.Ic web server Ar hostname Brq ...
+.Pp
+The parameters are as follows:
+.Bl -tag -width Ds
+.It Ic gotweb_url_root Ar path
+Sets the URL path under which
+.Xr httpd 8
+is routing requests to this
+.Xr gotwebd 8
+server.
+Defaults to
+.Dq / .
+.Pp
+The specified
+.Ar path
+should match the path of the
+.Ic location
+block in
+.Xr httpd.conf 5
+which forwards requests to
+.Xr gotwebd 8 .
+.It Ic htdocs Pa path
+Set the path to the directory which contains static files linked from
+HTML generated by
+.Xr gotwebd 8 ,
+such as
+.Pa gotweb.css .
+The specified
+.Ar path
+will be looked up relative to the
+.Ic chroot
+directory of
+.Xr httpd 8 .
+.It Ic hide repositories Ar on | off
+Controls whether repositories are hidden by default.
+Hidden repositories cannot be browsed via
+.Xr gotwebd 8 .
+.Pp
+By default,
+.Ic hide repositories
+is set to
+.Ar off
+and all repositories found in the
+.Nm
+.Ic repository directory
+will be displayed.
+.It Ic disable authentication
+Disable authentication, allowing any browser to view any repository not
+hidden via the
+.Ic hide repositories
+and
+.Ic hide repository
+directives in
+.Xr gotsys.conf 5 .
+.Pp
+The web server's
+.Ic disable authentication
+directive can be overridden by directives in
+.Xr gotsys.conf 5 .
+.It Ic enable authentication Oo Ic insecure Oc
+Enable authentication, requiring browsers to present a login token cookie
+before read-only repository access is granted.
+Unless the
+.Ic insecure
+keyword is used, the login token cookie will be marked as
+.Dq Secure ,
+which causes browsers to only send the cookie when connected to the
+web server over a TLS connection.
+.Pp
+The web server's
+.Ic enable authentication
+directive can be overridden by directives in
+.Xr gotsys.conf 5 .
+However,
+.Xr gotsys.conf 5
+deliberately lacks the
+.Ic insecure
+keyword.
+.El
+.Pp
+Additional parameters for web servers can be set in
+.Xr gotsys.conf 5 .
+.Xr gotsysd 8
+will ignore
+.Xr gotsys.conf 5
+configuration directives which cannot be mapped to a known web server
+.Ar hostname
+declared in
+.Nm .
 .Sh EXAMPLES
 The following example shows default settings:
 .Bd -literal -offset indent
@@ -222,7 +538,43 @@ says, make all repositories inaccessible:
 .Bd -literal -offset indent
 repository deny "*"
 .Ed
+.Pp
+Display repositories in the default
+.Pa /git
+repository directory on the web with
+.Xr gotwebd 8 ,
+using default settings:
+.Bd -literal -offset indent
+web server "gotweb.example.com"
+.Ed
+.Pp
+Make
+.Xr gotwebd 8
+hide repositories which are not explicitly unhidden in
+.Xr gotsys.conf 5 ,
+and enable authentication unless overriden in on per-server or per-repository
+basis in
+.Xr gotsys.conf 5 :
+.Pp
+.Bd -literal -offset indent
+web server "gotweb.example.com" {
+	hide repositories on
+	enable authentication
+}
+.Ed
+.Pp
+Configure
+.Xr gotwebd 8
+to listen on a TCP socket:
+.Bd -literal -offset indent
+gotweb {
+	listen on 127.0.0.1 port 9000
+}
+.Ed
 .Sh SEE ALSO
 .Xr got 1 ,
+.Xr gotwebd.conf 5 ,
 .Xr gotd 8 ,
-.Xr gotsysd 8
+.Xr gotsysd 8 ,
+.Xr gotwebd 8 ,
+.Xr httpd 8
blob - 796adae0e635f2e520dc268d3131865039506913
blob + 52140e7d3d7c9f7ed619605274d60089239d9cf6
--- gotsysd/gotsysd.h
+++ gotsysd/gotsysd.h
@@ -100,6 +100,80 @@ struct gotsysd_pending_sysconf_cmd {
 STAILQ_HEAD(gotsysd_pending_sysconf_cmd_list,
     gotsysd_pending_sysconf_cmd);
 
+enum gotsysd_web_auth_config {
+	GOTSYSD_WEB_AUTH_UNSET		= 0x0,
+	GOTSYSD_WEB_AUTH_DISABLED	= 0xf00000ff,
+	GOTSYSD_WEB_AUTH_SECURE		= 0x00808000,
+	GOTSYSD_WEB_AUTH_INSECURE	= 0x0f7f7f00
+};
+
+enum gotsysd_web_address_family {
+	GOTSYSD_LISTEN_ADDR_UNIX = 0,
+	GOTSYSD_LISTEN_ADDR_INET,
+};
+
+struct gotsysd_web_inet_addr {
+	char address[48];
+	char port[8];
+	char reserved[8];
+};
+
+struct gotsysd_web_address {
+	TAILQ_ENTRY(gotsysd_web_address) entry;
+
+	enum gotsysd_web_address_family family;
+	union {
+		char unix_socket_path[PATH_MAX];
+		struct gotsysd_web_inet_addr inet;
+		
+	} addr;
+};
+TAILQ_HEAD(gotsysd_web_addresslist, gotsysd_web_address);
+
+/* XXX redundant definitions, see gotwebd.h */
+#ifndef MAX_DOCUMENT_URI
+#define MAX_DOCUMENT_URI	 255
+#endif
+#ifndef MAX_IDENTIFIER_SIZE
+#define MAX_IDENTIFIER_SIZE	 32
+#endif
+#ifndef MAX_SERVER_NAME
+#define MAX_SERVER_NAME		 255
+#endif
+
+struct gotsysd_web_server {
+	STAILQ_ENTRY(gotsysd_web_server) entry;
+
+	char server_name[MAX_SERVER_NAME];
+
+	char gotweb_url_root[MAX_DOCUMENT_URI];
+	char htdocs_path[PATH_MAX];
+
+	enum gotsysd_web_auth_config auth_config;
+	int hide_repositories;
+};
+STAILQ_HEAD(gotsysd_web_serverlist, gotsysd_web_server);
+
+struct gotsysd_web_config {
+	char control_socket[PATH_MAX];
+
+	char httpd_chroot[PATH_MAX];
+	char htdocs_path[PATH_MAX];
+	char repos_path[PATH_MAX];
+
+	char gotwebd_user[MAX_IDENTIFIER_SIZE];
+	char www_user[MAX_IDENTIFIER_SIZE];
+
+	char login_hint_user[MAX_IDENTIFIER_SIZE];
+	char login_hint_port[8];
+
+	enum gotsysd_web_auth_config auth_config;
+	struct gotsysd_web_addresslist listen_addrs;
+	struct gotsysd_web_serverlist servers;
+
+	uint16_t prefork;
+};
+
 struct gotsysd {
 	pid_t pid;
 	char unix_socket_path[_POSIX_PATH_MAX];
@@ -127,6 +201,8 @@ struct gotsysd {
 	const char *confpath;
 	int daemonize;
 	int verbosity;
+
+	struct gotsysd_web_config web;
 };
 
 enum gotsysd_imsg_type {
@@ -166,6 +242,7 @@ enum gotsysd_imsg_type {
 	GOTSYSD_IMSG_START_PROG_READ_CONF,
 	GOTSYSD_IMSG_START_PROG_WRITE_CONF,
 	GOTSYSD_IMSG_START_PROG_APPLY_CONF,
+	GOTSYSD_IMSG_START_PROG_APPLY_WEBCONF,
 	GOTSYSD_IMSG_START_PROG_SSHDCONFIG,
 	GOTSYSD_IMSG_PROG_READY,
 
@@ -181,12 +258,31 @@ enum gotsysd_imsg_type {
 	GOTSYSD_IMSG_SYSCONF_AUTHORIZED_KEYS_USER,
 	GOTSYSD_IMSG_SYSCONF_AUTHORIZED_KEYS,
 	GOTSYSD_IMSG_SYSCONF_AUTHORIZED_KEYS_DONE,
+	GOTSYSD_IMSG_SYSCONF_WEB_SERVER,
+	GOTSYSD_IMSG_SYSCONF_WEBREPO,
+	GOTSYSD_IMSG_SYSCONF_WEBREPO_ACCESS_RULE,
+	GOTSYSD_IMSG_SYSCONF_WEBREPO_ACCESS_RULES_DONE,
+	GOTSYSD_IMSG_SYSCONF_WEBREPOS_DONE,
+	GOTSYSD_IMSG_SYSCONF_WEBSITE_PATH,
+	GOTSYSD_IMSG_SYSCONF_WEBSITE,
+	GOTSYSD_IMSG_SYSCONF_WEBSITE_ACCESS_RULE,
+	GOTSYSD_IMSG_SYSCONF_WEBSITE_ACCESS_RULES_DONE,
+	GOTSYSD_IMSG_SYSCONF_WEBSITES_DONE,
+	GOTSYSD_IMSG_SYSCONF_WEB_SERVERS_DONE,
 	GOTSYSD_IMSG_SYSCONF_REPO,
 	GOTSYSD_IMSG_SYSCONF_REPOS_DONE,
 	GOTSYSD_IMSG_SYSCONF_GLOBAL_ACCESS_RULE,
 	GOTSYSD_IMSG_SYSCONF_GLOBAL_ACCESS_RULES_DONE,
 	GOTSYSD_IMSG_SYSCONF_ACCESS_RULE,
 	GOTSYSD_IMSG_SYSCONF_ACCESS_RULES_DONE,
+	GOTSYSD_IMSG_SYSCONF_WEB_ACCESS_RULE,
+	GOTSYSD_IMSG_SYSCONF_WEB_ACCESS_RULES_DONE,
+	GOTSYSD_IMSG_SYSCONF_MEDIA_TYPE,
+	GOTSYSD_IMSG_SYSCONF_MEDIA_TYPES_DONE,
+	GOTSYSD_IMSG_SYSCONF_WEB_REPOS,
+	GOTSYSD_IMSG_SYSCONF_WEB_REPOS_DONE,
+	GOTSYSD_IMSG_SYSCONF_GLOBAL_MEDIA_TYPE,
+	GOTSYSD_IMSG_SYSCONF_GLOBAL_MEDIA_TYPES_DONE,
 	GOTSYSD_IMSG_SYSCONF_PROTECTED_TAG_NAMESPACES,
 	GOTSYSD_IMSG_SYSCONF_PROTECTED_TAG_NAMESPACES_ELEM,
 	GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCH_NAMESPACES,
@@ -250,6 +346,18 @@ enum gotsysd_imsg_type {
 	GOTSYSD_IMSG_SYSCONF_APPLY_CONF_READY,
 	GOTSYSD_IMSG_SYSCONF_APPLY_CONF_DONE,
 
+	/* gotwebd.conf creation. */
+	GOTSYSD_IMSG_SYSCONF_GOTWEB_CFG,
+	GOTSYSD_IMSG_SYSCONF_GOTWEB_ADDR,
+	GOTSYSD_IMSG_SYSCONF_GOTWEB_ADDRS_DONE,
+	GOTSYSD_IMSG_SYSCONF_GOTWEB_SERVER,
+	GOTSYSD_IMSG_SYSCONF_GOTWEB_SERVERS_DONE,
+	GOTSYSD_IMSG_SYSCONF_GOTWEB_CFG_DONE,
+
+	/* Apply gotwebd configuration. */
+	GOTSYSD_IMSG_SYSCONF_APPLY_WEBCONF_READY,
+	GOTSYSD_IMSG_SYSCONF_APPLY_WEBCONF_DONE,
+
 	/* sshd configuration. */
 	GOTSYSD_IMSG_SYSCONF_SSHDCONFIG_READY,
 	GOTSYSD_IMSG_SYSCONF_INSTALL_SSHD_CONFIG,
@@ -407,8 +515,9 @@ struct gotsysd_imsg_sysconf_authorized_key {
 struct gotsysd_imsg_sysconf_repo {
 	size_t name_len;
 	size_t headref_len;
+	size_t description_len;
 
-	/* Followed by name_len + headref_len bytes. */
+	/* Followed by name_len + headref_len + description_len bytes. */
 
 	/*
 	 * Followed by GOTSYSD_IMSG_SYSCONF_ACCESS_RULE for access rules,
@@ -512,8 +621,10 @@ struct gotsysd_imsg_notitfication_target_http {
 #define GOTSYSD_PROG_READ_CONF		gotsys-read-conf
 #define GOTSYSD_PROG_WRITE_CONF		gotsys-write-conf
 #define GOTSYSD_PROG_APPLY_CONF		gotsys-apply-conf
+#define GOTSYSD_PROG_APPLY_WEBCONF	gotsys-apply-webconf
 #define GOTSYSD_PROG_SSHDCONFIG		gotsys-sshdconfig
 #define GOTSYSD_PROG_GOTD		gotd
+#define GOTSYSD_PROG_GOTWEBD		gotwebd
 
 #define GOTSYSD_PATH_PROG_REPO_CREATE \
 	GOTSYSD_STRINGVAL(GOT_LIBEXECDIR) "/" \
@@ -542,12 +653,18 @@ struct gotsysd_imsg_notitfication_target_http {
 #define GOTSYSD_PATH_PROG_APPLY_CONF \
 	GOTSYSD_STRINGVAL(GOT_LIBEXECDIR) "/" \
 	GOTSYSD_STRINGVAL(GOTSYSD_PROG_APPLY_CONF)
+#define GOTSYSD_PATH_PROG_APPLY_WEBCONF \
+	GOTSYSD_STRINGVAL(GOT_LIBEXECDIR) "/" \
+	GOTSYSD_STRINGVAL(GOTSYSD_PROG_APPLY_WEBCONF)
 #define GOTSYSD_PATH_PROG_SSHDCONFIG \
 	GOTSYSD_STRINGVAL(GOT_LIBEXECDIR) "/" \
 	GOTSYSD_STRINGVAL(GOTSYSD_PROG_SSHDCONFIG)
 #define GOTSYSD_PATH_PROG_GOTD \
 	GOTSYSD_STRINGVAL(GOT_SBINDIR) "/" \
 	GOTSYSD_STRINGVAL(GOTSYSD_PROG_GOTD)
+#define GOTSYSD_PATH_PROG_GOTWEBD \
+	GOTSYSD_STRINGVAL(GOT_SBINDIR) "/" \
+	GOTSYSD_STRINGVAL(GOTSYSD_PROG_GOTWEBD)
 
 extern const char *gotsysd_priv_helpers[];
 extern const size_t gotsysd_num_priv_helpers;
@@ -586,6 +703,12 @@ struct gotsys_repolist;
 struct gotsys_repo;
 struct gotsys_access_rule;
 struct gotsys_notification_target;
+struct gotsys_webserverlist;
+struct gotsys_webserver;
+struct gotsys_website;
+struct gotsys_webrepo;
+struct media_type;
+struct mediatypes;
 struct got_pathlist_head;
 
 const struct got_error *gotsys_imsg_send_users(struct gotsysd_imsgev *,
@@ -606,6 +729,22 @@ const struct got_error *gotsys_imsg_recv_authorized_ke
     struct gotsys_authorized_keys_list *); 
 const struct got_error *gotsys_imsg_send_access_rule(struct gotsysd_imsgev *,
     struct gotsys_access_rule *, int);
+const struct got_error *gotsys_imsg_recv_media_type(struct media_type *,
+    struct imsg *);
+const struct got_error *gotsys_imsg_send_webservers(struct gotsysd_imsgev *,
+    struct gotsys_webserverlist *);
+const struct got_error *gotsys_imsg_send_mediatypes(struct gotsysd_imsgev *,
+    struct mediatypes *, int, int);
+const struct got_error *gotsys_imsg_recv_web_cfg(struct gotsysd_web_config *,
+    struct imsg *);
+const struct got_error *gotsysd_conf_validate_inet_addr(const char *,
+    const char *);
+const struct got_error *gotsys_imsg_recv_webaddr(struct gotsysd_web_address **,
+    struct imsg *);
+const struct got_error *gotsys_imsg_recv_gotweb_server(
+    struct gotsysd_web_server **, struct imsg *);
+const struct got_error *gotsys_imsg_recv_web_server(
+    struct gotsys_webserver *, struct imsg *);
 const struct got_error *gotsys_imsg_send_repositories(struct gotsysd_imsgev *,
     struct gotsys_repolist *);
 const struct got_error *gotsys_imsg_recv_repository(struct gotsys_repo **,
@@ -613,6 +752,12 @@ const struct got_error *gotsys_imsg_recv_repository(st
 const struct got_error *gotsys_imsg_recv_access_rule(
     struct gotsys_access_rule **, struct imsg *, struct gotsys_userlist *,
     struct gotsys_grouplist *);
+const struct got_error *gotsys_imsg_recv_webrepo(
+    struct gotsys_webrepo *, struct imsg *);
+const struct got_error *gotsys_imsg_recv_website_path(struct gotsys_website **,
+    struct imsg *);
+const struct got_error *gotsys_imsg_recv_website(struct gotsys_website *,
+    struct imsg *);
 const struct got_error *gotsys_imsg_recv_pathlist(size_t *, struct imsg *);
 const struct got_error *gotsys_imsg_recv_pathlist_elem(struct imsg *,
     struct got_pathlist_head *);
@@ -642,3 +787,4 @@ const struct got_error *gotsys_uidset_for_each_element
     void *);
 void gotsys_uidset_remove_element(struct gotsys_uidset *,
     struct gotsys_uidset_element *);
+void gotsysd_web_config_init(struct gotsysd_web_config *);
blob - 92e28da9546ddf1755fbec09972f0a3fa16394b8
blob + 4cf62b0d9d9739168e4ed31f352562d117ec270a
--- gotsysd/helpers.c
+++ gotsysd/helpers.c
@@ -36,8 +36,11 @@
 #include "got_error.h"
 #include "got_path.h"
 #include "got_object.h"
+#include "got_reference.h"
 
 #include "gotsysd.h"
+#include "media.h"
+#include "gotwebd.h"
 #include "gotsys.h"
 #include "log.h"
 #include "helpers.h"
@@ -174,6 +177,8 @@ get_helper_prog_name(int imsg_type)
 		return GOTSYSD_PATH_PROG_WRITE_CONF;
 	case GOTSYSD_IMSG_START_PROG_APPLY_CONF:
 		return GOTSYSD_PATH_PROG_APPLY_CONF;
+	case GOTSYSD_IMSG_START_PROG_APPLY_WEBCONF:
+		return GOTSYSD_PATH_PROG_APPLY_WEBCONF;
 	case GOTSYSD_IMSG_START_PROG_SSHDCONFIG:
 		return GOTSYSD_PATH_PROG_SSHDCONFIG;
 	default:
@@ -441,6 +446,18 @@ send_apply_conf_ready(struct gotsysd_imsgev *iev)
 }
 
 static const struct got_error *
+send_apply_webconf_ready(struct gotsysd_imsgev *iev)
+{
+	if (gotsysd_imsg_compose_event(iev,
+	    GOTSYSD_IMSG_SYSCONF_APPLY_WEBCONF_READY, gotsysd_helpers.proc_id,
+	    -1, NULL, 0) == -1)
+		return got_error_from_errno("imsg_compose APPLY_WEBCONF_READY");
+	
+	return NULL;
+}
+
+
+static const struct got_error *
 send_sshdconfig_ready(struct gotsysd_imsgev *iev)
 {
 	if (gotsysd_imsg_compose_event(iev,
@@ -490,6 +507,9 @@ proc_ready(struct gotsysd_helper_proc *proc)
 	case GOTSYSD_IMSG_START_PROG_APPLY_CONF:
 		err = send_apply_conf_ready(sysconf_iev);
 		break;
+	case GOTSYSD_IMSG_START_PROG_APPLY_WEBCONF:
+		err = send_apply_webconf_ready(sysconf_iev);
+		break;
 	case GOTSYSD_IMSG_START_PROG_SSHDCONFIG:
 		err = send_sshdconfig_ready(sysconf_iev);
 		break;
@@ -577,10 +597,27 @@ dispatch_helper_child(int fd, short event, void *arg)
 		case GOTSYSD_IMSG_SYSCONF_GROUP_MEMBERS:
 		case GOTSYSD_IMSG_SYSCONF_GROUP_MEMBERS_DONE:
 		case GOTSYSD_IMSG_SYSCONF_GROUPS_DONE:
+		case GOTSYSD_IMSG_SYSCONF_WEB_SERVER:
+		case GOTSYSD_IMSG_SYSCONF_WEB_SERVERS_DONE:
+		case GOTSYSD_IMSG_SYSCONF_WEB_ACCESS_RULE:
+		case GOTSYSD_IMSG_SYSCONF_WEB_ACCESS_RULES_DONE:
 		case GOTSYSD_IMSG_SYSCONF_REPO:
+		case GOTSYSD_IMSG_SYSCONF_WEBREPO:
+		case GOTSYSD_IMSG_SYSCONF_WEBREPO_ACCESS_RULE:
+		case GOTSYSD_IMSG_SYSCONF_WEBREPO_ACCESS_RULES_DONE:
+		case GOTSYSD_IMSG_SYSCONF_WEBREPOS_DONE:
+		case GOTSYSD_IMSG_SYSCONF_WEBSITE_PATH:
+		case GOTSYSD_IMSG_SYSCONF_WEBSITE:
+		case GOTSYSD_IMSG_SYSCONF_WEBSITE_ACCESS_RULE:
+		case GOTSYSD_IMSG_SYSCONF_WEBSITE_ACCESS_RULES_DONE:
+		case GOTSYSD_IMSG_SYSCONF_WEBSITES_DONE:
+		case GOTSYSD_IMSG_SYSCONF_GLOBAL_MEDIA_TYPE:
+		case GOTSYSD_IMSG_SYSCONF_GLOBAL_MEDIA_TYPES_DONE:
 		case GOTSYSD_IMSG_SYSCONF_REPOS_DONE:
 		case GOTSYSD_IMSG_SYSCONF_ACCESS_RULE:
 		case GOTSYSD_IMSG_SYSCONF_ACCESS_RULES_DONE:
+		case GOTSYSD_IMSG_SYSCONF_MEDIA_TYPE:
+		case GOTSYSD_IMSG_SYSCONF_MEDIA_TYPES_DONE:
 		case GOTSYSD_IMSG_SYSCONF_PROTECTED_TAG_NAMESPACES:
 		case GOTSYSD_IMSG_SYSCONF_PROTECTED_TAG_NAMESPACES_ELEM:
 		case GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCH_NAMESPACES:
@@ -711,6 +748,19 @@ dispatch_helper_child(int fd, short event, void *arg)
 				err = got_error_from_errno("imsg_forward");
 			shut = 1;
 			break;
+		case GOTSYSD_IMSG_SYSCONF_APPLY_WEBCONF_DONE:
+			if (proc->type !=
+			    GOTSYSD_IMSG_START_PROG_APPLY_WEBCONF) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "unexpected message type %d from helper "
+				        "process type %d pid %u\n",
+				    imsg.hdr.type, proc->type, proc->pid);
+				break;
+			}
+			if (gotsysd_imsg_forward(sysconf_iev, &imsg, -1) == -1)
+				err = got_error_from_errno("imsg_forward");
+			shut = 1;
+			break;
 		case GOTSYSD_IMSG_SYSCONF_INSTALL_SSHD_CONFIG_DONE:
 			if (proc->type !=
 			    GOTSYSD_IMSG_START_PROG_SSHDCONFIG) {
@@ -911,6 +961,7 @@ helpers_dispatch_sysconf(int fd, short event, void *ar
 		case GOTSYSD_IMSG_START_PROG_GROUPADD:
 		case GOTSYSD_IMSG_START_PROG_WRITE_CONF:
 		case GOTSYSD_IMSG_START_PROG_APPLY_CONF:
+		case GOTSYSD_IMSG_START_PROG_APPLY_WEBCONF:
 		case GOTSYSD_IMSG_START_PROG_SSHDCONFIG:
 			prog = get_helper_prog_name(imsg.hdr.type);
 			if (prog == NULL)
@@ -1017,17 +1068,38 @@ helpers_dispatch_sysconf(int fd, short event, void *ar
 			if (gotsysd_imsg_forward(&proc->iev, &imsg, -1) == -1)
 				err = got_error_from_errno("imsg_forward");
 			break;
+		case GOTSYSD_IMSG_SYSCONF_GOTWEB_CFG:
+		case GOTSYSD_IMSG_SYSCONF_GOTWEB_ADDR:
+		case GOTSYSD_IMSG_SYSCONF_GOTWEB_ADDRS_DONE:
+		case GOTSYSD_IMSG_SYSCONF_GOTWEB_SERVER:
+		case GOTSYSD_IMSG_SYSCONF_GOTWEB_SERVERS_DONE:
+		case GOTSYSD_IMSG_SYSCONF_GOTWEB_CFG_DONE:
 		case GOTSYSD_IMSG_SYSCONF_WRITE_CONF_USERS:
 		case GOTSYSD_IMSG_SYSCONF_WRITE_CONF_USERS_DONE:
 		case GOTSYSD_IMSG_SYSCONF_WRITE_CONF_GROUP:
 		case GOTSYSD_IMSG_SYSCONF_WRITE_CONF_GROUP_MEMBERS:
 		case GOTSYSD_IMSG_SYSCONF_WRITE_CONF_GROUP_MEMBERS_DONE:
 		case GOTSYSD_IMSG_SYSCONF_WRITE_CONF_GROUPS_DONE:
+		case GOTSYSD_IMSG_SYSCONF_WEB_ACCESS_RULE:
+		case GOTSYSD_IMSG_SYSCONF_WEB_ACCESS_RULES_DONE:
 		case GOTSYSD_IMSG_SYSCONF_REPO:
+		case GOTSYSD_IMSG_SYSCONF_WEBREPO:
+		case GOTSYSD_IMSG_SYSCONF_WEBREPO_ACCESS_RULE:
+		case GOTSYSD_IMSG_SYSCONF_WEBREPO_ACCESS_RULES_DONE:
+		case GOTSYSD_IMSG_SYSCONF_WEBREPOS_DONE:
+		case GOTSYSD_IMSG_SYSCONF_WEBSITE_PATH:
+		case GOTSYSD_IMSG_SYSCONF_WEBSITE:
+		case GOTSYSD_IMSG_SYSCONF_WEBSITE_ACCESS_RULE:
+		case GOTSYSD_IMSG_SYSCONF_WEBSITE_ACCESS_RULES_DONE:
+		case GOTSYSD_IMSG_SYSCONF_WEBSITES_DONE:
 		case GOTSYSD_IMSG_SYSCONF_GLOBAL_ACCESS_RULE:
 		case GOTSYSD_IMSG_SYSCONF_GLOBAL_ACCESS_RULES_DONE:
+		case GOTSYSD_IMSG_SYSCONF_GLOBAL_MEDIA_TYPE:
+		case GOTSYSD_IMSG_SYSCONF_GLOBAL_MEDIA_TYPES_DONE:
 		case GOTSYSD_IMSG_SYSCONF_ACCESS_RULE:
 		case GOTSYSD_IMSG_SYSCONF_ACCESS_RULES_DONE:
+		case GOTSYSD_IMSG_SYSCONF_MEDIA_TYPE:
+		case GOTSYSD_IMSG_SYSCONF_MEDIA_TYPES_DONE:
 		case GOTSYSD_IMSG_SYSCONF_PROTECTED_TAG_NAMESPACES:
 		case GOTSYSD_IMSG_SYSCONF_PROTECTED_TAG_NAMESPACES_ELEM:
 		case GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCH_NAMESPACES:
@@ -1045,6 +1117,8 @@ helpers_dispatch_sysconf(int fd, short event, void *ar
 		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_TARGET_HTTP:
 		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_TARGETS_DONE:
 		case GOTSYSD_IMSG_SYSCONF_REPOS_DONE:
+		case GOTSYSD_IMSG_SYSCONF_WEB_SERVER:
+		case GOTSYSD_IMSG_SYSCONF_WEB_SERVERS_DONE:
 			proc = find_proc(GOTSYSD_IMSG_START_PROG_WRITE_CONF,
 			    1);
 			if (proc == NULL)
blob - bb20b6f233596597dd54450bc4e7b66f3b3b8cd2
blob + bd31c3be274be5f1611ddd643c8faf4ba9836e20
--- gotsysd/libexec/Makefile
+++ gotsysd/libexec/Makefile
@@ -1,5 +1,5 @@
 SUBDIR = gotsys-read-conf gotsys-useradd gotsys-groupadd gotsys-userhome \
 	gotsys-rmkeys gotsys-userkeys gotsys-repo-create gotsys-write-conf \
-	gotsys-apply-conf gotsys-sshdconfig
+	gotsys-apply-conf gotsys-apply-webconf gotsys-sshdconfig
 
 .include <bsd.subdir.mk>
blob - 4676b1e2c69a056cda7314123c334850fa72a09b
blob + cbef0cebe42baccb4f6cac6cc0693312e2040460
--- gotsysd/libexec/gotsys-apply-conf/Makefile
+++ gotsysd/libexec/gotsys-apply-conf/Makefile
@@ -8,7 +8,7 @@ SRCS=		gotsys-apply-conf.c error.c path.c hash.c pollf
 
 CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib \
 	-I${.CURDIR}/../../../gotsys -I${.CURDIR}/../../../gotd \
-	-I${.CURDIR}/../../ -I${.CURDIR}
+	-I${.CURDIR}/../../../gotwebd -I${.CURDIR}/../../ -I${.CURDIR}
 YFLAGS =
 
 .if defined(PROFILE)
blob - fe0a2f05c2dbbe860c3b503b97febf4566ff2ea6
blob + 5141d30d3ad8f8e91bbc26345641dcae3e14d73f
--- gotsysd/libexec/gotsys-apply-conf/gotsys-apply-conf.c
+++ gotsysd/libexec/gotsys-apply-conf/gotsys-apply-conf.c
@@ -37,8 +37,11 @@
 #include "got_path.h"
 #include "got_object.h"
 #include "got_opentemp.h"
+#include "got_reference.h"
 
 #include "gotsysd.h"
+#include "media.h"
+#include "gotwebd.h"
 #include "gotsys.h"
 #include "gotd.h"
 
blob - 70e6923010c18c47dd548591fd51f5e2cbe76ab4
blob + e592c178a4d8c5775f7280c2a9b52a87292646a0
--- gotsysd/libexec/gotsys-groupadd/Makefile
+++ gotsysd/libexec/gotsys-groupadd/Makefile
@@ -4,10 +4,12 @@
 
 PROG=		gotsys-groupadd
 SRCS=		gotsys-groupadd.c error.c hash.c pollfd.c path.c \
-		imsg.c gotsys_conf.c gotsys_imsg.c opentemp.c gotsys_uidset.c
+		imsg.c gotsys_conf.c media.c gotsys_imsg.c opentemp.c \
+		gotsys_uidset.c reference_parse.c
 
 CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib \
-	-I${.CURDIR}/../../../gotsys -I${.CURDIR}/../../ -I${.CURDIR}
+	-I${.CURDIR}/../../../gotsys -I${.CURDIR}/../../../gotwebd \
+	-I${.CURDIR}/../../ -I${.CURDIR}
 YFLAGS =
 
 .if defined(PROFILE)
blob - b089d372683108989a7c20f57dbe3fac47729c16
blob + d0585c9b7aac72a411be01c5c4ae8bf1bfeeebef
--- gotsysd/libexec/gotsys-groupadd/gotsys-groupadd.c
+++ gotsysd/libexec/gotsys-groupadd/gotsys-groupadd.c
@@ -69,8 +69,11 @@
 #include "got_path.h"
 #include "got_opentemp.h"
 #include "got_object.h"
+#include "got_reference.h"
 
 #include "gotsysd.h"
+#include "media.h"
+#include "gotwebd.h"
 #include "gotsys.h"
 
 static gid_t groupadd_gid_start = GOTSYSD_UID_DEFAULT_START;
blob - /dev/null
blob + 278da829bbf9d69b9b0c79aa4ebe4afb2ab7c792 (mode 644)
--- /dev/null
+++ gotsysd/libexec/gotsys-apply-webconf/Makefile
@@ -0,0 +1,23 @@
+.PATH: ${.CURDIR}/../../../gotsys ${.CURDIR}/../.. ${.CURDIR}/../../../lib
+
+.include "../../../got-version.mk"
+
+PROG=		gotsys-apply-webconf
+SRCS=		gotsys-apply-webconf.c error.c path.c hash.c pollfd.c \
+		log.c imsg.c
+
+CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib \
+	-I${.CURDIR}/../../../gotsys -I${.CURDIR}/../../../gotd \
+	-I${.CURDIR}/../../../gotwebd -I${.CURDIR}/../../ -I${.CURDIR}
+YFLAGS =
+
+.if defined(PROFILE)
+LDADD = -lutil_p -levent_p
+.else
+LDADD = -lutil -levent
+.endif
+
+DPADD = ${LIBUTIL} ${LIBEVENT}
+
+
+.include <bsd.prog.mk>
blob - /dev/null
blob + 499212672fa0efd636998e9d2e8fda7a3860097d (mode 644)
--- /dev/null
+++ gotsysd/libexec/gotsys-apply-webconf/gotsys-apply-webconf.c
@@ -0,0 +1,477 @@
+/*
+ * Copyright (c) 2026 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.
+ */
+
+#include <sys/queue.h>
+#include <sys/tree.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+
+#include <err.h>
+#include <errno.h>
+#include <event.h>
+#include <imsg.h>
+#include <limits.h>
+#include <sha1.h>
+#include <sha2.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "got_error.h"
+#include "got_path.h"
+#include "got_object.h"
+#include "got_opentemp.h"
+#include "got_reference.h"
+
+#include "media.h"
+#include "gotsysd.h"
+#include "gotwebd.h"
+#include "gotsys.h"
+
+static struct gotsysd_imsgev gotsysd_iev;
+static struct gotsysd_imsgev gotwebd_iev;
+static int gotwebd_sock = -1;
+static char *gotwebd_sockpath = NULL;
+static int gotwebd_stop_sent;
+static int flush_and_exit;
+
+static void
+sighdlr(int sig, short event, void *arg)
+{
+	/*
+	 * Normal signal handler rules don't apply because libevent
+	 * decouples for us.
+	 */
+
+	switch (sig) {
+	case SIGHUP:
+		break;
+	case SIGUSR1:
+		break;
+	case SIGTERM:
+	case SIGINT:
+		event_loopexit(NULL);
+		break;
+	default:
+		break;
+	}
+}
+
+static const struct got_error *
+start_child(pid_t *pid,
+    const char *argv0, const char *argv1, const char *argv2)
+{
+	const char	*argv[4];
+	int		 argc = 0;
+
+	switch (*pid = fork()) {
+	case -1:
+		return got_error_from_errno("fork");
+	case 0:
+		break;
+	default:
+		return NULL;
+	}
+
+	argv[argc++] = argv0;
+	if (argv1 != NULL)
+		argv[argc++] = argv1;
+	if (argv2 != NULL)
+		argv[argc++] = argv2;
+	argv[argc++] = NULL;
+
+	execvp(argv0, (char * const *)argv);
+	err(1, "execvp: %s", argv0);
+
+	/* NOTREACHED */
+	return NULL;
+}
+
+static const struct got_error *
+connect_gotwebd(const char *socket_path)
+{
+	const struct got_error *err = NULL;
+	struct sockaddr_un sun;
+
+	gotwebd_sock = socket(AF_UNIX, SOCK_STREAM, 0);
+	if (gotwebd_sock == -1)
+		return got_error_from_errno("socket");
+
+	memset(&sun, 0, sizeof(sun));
+	sun.sun_family = AF_UNIX;
+	if (strlcpy(sun.sun_path, socket_path, sizeof(sun.sun_path)) >=
+	    sizeof(sun.sun_path)) {
+		close(gotwebd_sock);
+		gotwebd_sock = -1;
+		return got_error_msg(GOT_ERR_NO_SPACE,
+		    "gotwebd socket path too long");
+	}
+	if (connect(gotwebd_sock, (struct sockaddr *)&sun, sizeof(sun)) == -1) {
+		err = got_error_from_errno2("connect", socket_path);
+		close(gotwebd_sock);
+		gotwebd_sock = -1;
+	}
+
+	return err;
+}
+
+static const struct got_error *
+start_gotwebd(void)
+{
+	pid_t pid;
+
+	/* TODO: fetch gotwebd_flags from rc.conf.local and pass them in? */
+	return start_child(&pid, GOTSYSD_PATH_PROG_GOTWEBD, NULL, NULL);
+}
+
+static const struct got_error *
+send_done(struct gotsysd_imsgev *iev)
+{
+	if (gotsysd_imsg_compose_event(iev,
+	    GOTSYSD_IMSG_SYSCONF_APPLY_WEBCONF_DONE,
+	    0, -1, NULL, 0) == -1) {
+		return got_error_from_errno("imsg_compose "
+		    "SYSCONF_APPLY_WEBCONF_DONE");
+	}
+
+	return NULL;
+}
+
+static void
+dispatch_gotwebd(int fd, short event, void *arg)
+{
+	const struct got_error *err = NULL;
+	struct gotsysd_imsgev *iev = arg;
+	struct imsgbuf *ibuf = &iev->ibuf;
+	struct imsg imsg;
+	ssize_t n;
+
+	if (event & EV_WRITE) {
+		err = gotsysd_imsg_flush(ibuf);
+		if (err) {
+			warn("%s", err->msg);
+			goto loopexit;
+		}
+
+		if (imsgbuf_queuelen(ibuf) == 0 && flush_and_exit)
+			event_del(&iev->ev);
+	}
+
+	if (flush_and_exit)
+		return;
+
+	if (event & EV_READ) {
+		if ((n = imsgbuf_read(ibuf)) == -1) {
+			warn("imsgbuf_read error");
+			goto loopexit;
+		}
+		if (n == 0) {	/* Connection closed. */
+			err = start_gotwebd();
+			if (err)
+				warn("%s", err->msg);
+
+			err = send_done(&gotsysd_iev);
+			if (err)
+				warn("%s", err->msg);
+			event_del(&iev->ev);
+			flush_and_exit = 1;
+			return;
+		}
+	}
+
+	for (;;) {
+		if ((n = imsg_get(ibuf, &imsg)) == -1) {
+			warn("%s: imsg_get", __func__);
+			goto loopexit;
+		}
+		if (n == 0)	/* No more messages. */
+			break;
+
+		switch (imsg.hdr.type) {
+		default:
+			err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+			    "unexpected imsg %d", imsg.hdr.type);
+			break;
+		}
+
+		if (err) {
+			warnx("imsg %d: %s", imsg.hdr.type, err->msg);
+			gotsysd_imsg_send_error(&iev->ibuf, 0, 0, err);
+			flush_and_exit = 1;
+		}
+
+		imsg_free(&imsg);
+	}
+
+	gotsysd_imsg_event_add(iev);
+	return;
+
+loopexit:
+	/* This pipe is dead. Remove its event handler */
+	event_del(&iev->ev);
+	event_loopexit(NULL);
+}
+
+static void
+dispatch_gotsysd(int fd, short event, void *arg)
+{
+	const struct got_error *err = NULL;
+	struct gotsysd_imsgev *iev = arg;
+	struct imsgbuf *ibuf = &iev->ibuf;
+	struct imsg imsg;
+	ssize_t n;
+	int shut = 0;
+
+	if (event & EV_WRITE) {
+		err = gotsysd_imsg_flush(ibuf);
+		if (err) {
+			warn("%s", err->msg);
+			goto loopexit;
+		}
+
+		if (imsgbuf_queuelen(ibuf) == 0)
+			event_del(&iev->ev);
+		else
+			gotsysd_imsg_event_add(iev);
+
+		if (gotwebd_sock != -1 && !gotwebd_stop_sent) {
+			if (gotsysd_imsg_compose_event(&gotwebd_iev,
+			    GOTWEBD_IMSG_CTL_STOP, 0, -1, NULL, 0) == -1) {
+				err = got_error_from_errno("imsg_compose "
+				    "CTL_STOP");
+				gotsysd_imsg_send_error(&iev->ibuf, 0, 0, err);
+				flush_and_exit = 1;
+			}
+
+			gotwebd_stop_sent = 1;
+		}
+	}
+	
+	if (flush_and_exit)
+		return;
+
+	if (event & EV_READ) {
+		if ((n = imsgbuf_read(ibuf)) == -1) {
+			warn("imsgbuf_read error");
+			goto loopexit;
+		}
+		if (n == 0)	/* Connection closed. */
+			shut = 1;
+	}
+
+	for (;;) {
+		if ((n = imsg_get(ibuf, &imsg)) == -1) {
+			warn("%s: imsg_get", __func__);
+			goto loopexit;
+		}
+		if (n == 0)	/* No more messages. */
+			break;
+
+		switch (imsg.hdr.type) {
+		default:
+			err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+			    "unexpected imsg %d", imsg.hdr.type);
+			break;
+		}
+
+		if (err) {
+			warnx("imsg %d: %s", imsg.hdr.type, err->msg);
+			gotsysd_imsg_send_error(&iev->ibuf, 0, 0, err);
+			flush_and_exit = 1;
+		}
+
+		imsg_free(&imsg);
+	}
+
+	if (!shut) {
+		gotsysd_imsg_event_add(iev);
+	} else {
+loopexit:
+		/* This pipe is dead. Remove its event handler */
+		event_del(&iev->ev);
+		event_loopexit(NULL);
+	}
+}
+
+__dead static void
+usage(void)
+{
+	fprintf(stderr, "usage: %s [-c config-file] [-f socket]\n",
+	    getprogname());
+	exit(1);
+}
+
+int
+main(int argc, char *argv[])
+{
+	const struct got_error *err = NULL;
+	struct event evsigint, evsigterm, evsighup, evsigusr1;
+	int ch;
+
+	gotsysd_iev.ibuf.fd = -1;
+	gotwebd_iev.ibuf.fd = -1;
+
+#if 0
+	static int attached;
+
+	while (!attached)
+		sleep(1);
+#endif
+	while ((ch = getopt(argc, argv, "s:")) != -1) {
+		switch (ch) {
+		case 's':
+			gotwebd_sockpath = realpath(optarg, NULL);
+			if (gotwebd_sockpath == NULL) {
+				err = got_error_from_errno2("realpath",
+				    optarg);
+				goto done;
+			}
+			break;
+		default:
+			usage();
+			/* NOTREACHED */
+		}
+	}
+
+	if (gotwebd_sockpath == NULL) {
+		gotwebd_sockpath = strdup(GOTWEBD_CONTROL_SOCKET);
+		if (gotwebd_sockpath == NULL) {
+			err = got_error_from_errno("strdup");
+			goto done;
+		}
+	}
+
+	event_init();
+
+	signal_set(&evsigint, SIGINT, sighdlr, NULL);
+	signal_set(&evsigterm, SIGTERM, sighdlr, NULL);
+	signal_set(&evsighup, SIGHUP, sighdlr, NULL);
+	signal_set(&evsigusr1, SIGUSR1, sighdlr, NULL);
+	signal(SIGPIPE, SIG_IGN);
+
+	signal_add(&evsigint, NULL);
+	signal_add(&evsigterm, NULL);
+	signal_add(&evsighup, NULL);
+	signal_add(&evsigusr1, NULL);
+
+	if (imsgbuf_init(&gotsysd_iev.ibuf, GOTSYSD_FILENO_MSG_PIPE) == -1) {
+		err = got_error_from_errno("imsgbuf_init");
+		goto done;
+	}
+
+#ifndef PROFILE
+	if (pledge("stdio proc exec unix unveil", NULL) == -1) {
+		err = got_error_from_errno("pledge");
+		goto done;
+	}
+#endif
+	if (unveil(gotwebd_sockpath, "w") != 0) {
+		err = got_error_from_errno2("unveil", gotwebd_sockpath);
+		goto done;
+	}
+
+	if (unveil(GOTSYSD_PATH_PROG_GOTWEBD, "x") != 0) {
+		err = got_error_from_errno2("unveil",
+		    GOTSYSD_PATH_PROG_GOTWEBD);
+		goto done;
+	}
+
+	if (unveil(NULL, NULL) != 0) {
+		err = got_error_from_errno("unveil");
+		goto done;
+	}
+
+	/*
+	 * If we cannot conncet to the control socket then gotwebd might have
+	 * crashed and restarting it could have bad consequences (such as
+	 * leaking info to a remote attacker). Log a warning and send 'done'
+	 * to gotsysd which can then proceed with system configuration tasks.
+	 */
+	err = connect_gotwebd(gotwebd_sockpath);
+	if (err) {
+		if (err->code != GOT_ERR_ERRNO ||
+		    (errno != ENOENT && errno != ECONNREFUSED))
+			goto done;
+
+		warnx("%s: %s", gotwebd_sockpath, err->msg);
+		err = NULL;
+#ifndef PROFILE
+		/* We will not attempt to restart gotwebd. */
+		if (pledge("stdio", NULL) == -1) {
+			err = got_error_from_errno("pledge");
+			goto done;
+		}
+#endif
+	} else {
+#ifndef PROFILE
+		/* We will attempt to restart gotwebd. */
+		if (pledge("stdio proc exec", NULL) == -1) {
+			err = got_error_from_errno("pledge");
+			goto done;
+		}
+#endif
+		if (imsgbuf_init(&gotwebd_iev.ibuf, gotwebd_sock) == -1) {
+			err = got_error_from_errno("imsgbuf_init");
+			goto done;
+		}
+
+		gotwebd_iev.handler = dispatch_gotwebd;
+		gotwebd_iev.events = EV_READ;
+		gotwebd_iev.handler_arg = NULL;
+		event_set(&gotwebd_iev.ev, gotwebd_iev.ibuf.fd, EV_READ,
+		    dispatch_gotwebd, &gotwebd_iev);
+	}
+
+	gotsysd_iev.handler = dispatch_gotsysd;
+	gotsysd_iev.events = EV_READ;
+	gotsysd_iev.handler_arg = NULL;
+	event_set(&gotsysd_iev.ev, gotsysd_iev.ibuf.fd, EV_READ,
+	    dispatch_gotsysd, &gotsysd_iev);
+
+	if (gotsysd_imsg_compose_event(&gotsysd_iev,
+	    GOTSYSD_IMSG_PROG_READY, 0, -1, NULL, 0) == -1) {
+		err = got_error_from_errno("imsg_compose PROG_READY");
+		goto done;
+	}
+
+	if (gotwebd_sock == -1) {
+		/* If gotwebd is not running then we are done. */
+		err = send_done(&gotsysd_iev);
+		if (err)
+			goto done;
+		flush_and_exit = 1;
+	}
+
+	event_dispatch();
+done:
+	free(gotwebd_sockpath);
+	if (gotwebd_iev.ibuf.fd != -1)
+		imsgbuf_clear(&gotwebd_iev.ibuf);
+	if (gotwebd_sock != -1 && close(gotwebd_sock) == -1 && err == NULL)
+		err = got_error_from_errno("close");
+	if (err)
+		gotsysd_imsg_send_error(&gotsysd_iev.ibuf, 0, 0, err);
+
+	if (gotsysd_iev.ibuf.fd != -1)
+		imsgbuf_clear(&gotsysd_iev.ibuf);
+	if (close(GOTSYSD_FILENO_MSG_PIPE) == -1 && err == NULL)
+		err = got_error_from_errno("close");
+	if (err)
+		fprintf(stderr, "%s: %s\n", getprogname(), err->msg);
+	return err ? 1 : 0;
+}
blob - 71187ecc734d8e2db905f5d2935575357c6a76c2
blob + 46370f2864af6acb11866e857391612c54333af9
--- gotsysd/libexec/gotsys-read-conf/Makefile
+++ gotsysd/libexec/gotsys-read-conf/Makefile
@@ -4,10 +4,12 @@
 
 PROG=		gotsys-read-conf
 SRCS=		gotsys-read-conf.c error.c path.c hash.c parse.y pollfd.c \
-		log.c reference_parse.c imsg.c gotsys_conf.c gotsys_imsg.c
+		log.c reference_parse.c imsg.c gotsys_conf.c media.c \
+		gotsys_imsg.c
 
 CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib \
-	-I${.CURDIR}/../../../gotsys -I${.CURDIR}/../../ -I${.CURDIR}
+	-I${.CURDIR}/../../../gotsys -I${.CURDIR}/../../../gotwebd \
+	-I${.CURDIR}/../../ -I${.CURDIR}
 YFLAGS =
 
 CLEANFILES = parse.c
blob - bf14d5a01337e3cd00432ebf69be96fb1010f03e
blob + 6591a44022103233a58421604b572c94d96d0803
--- gotsysd/libexec/gotsys-read-conf/gotsys-read-conf.c
+++ gotsysd/libexec/gotsys-read-conf/gotsys-read-conf.c
@@ -32,8 +32,11 @@
 #include "got_error.h"
 #include "got_path.h"
 #include "got_object.h"
+#include "got_reference.h"
 
 #include "gotsysd.h"
+#include "media.h"
+#include "gotwebd.h"
 #include "gotsys.h"
 
 static void
@@ -136,6 +139,10 @@ dispatch_event(int fd, short event, void *arg)
 			}
 
 			gotsys_conf_clear(&gotsysconf);
+			err = gotsys_conf_init_media_types(
+			    &gotsysconf.mediatypes);
+			if (err)
+				break;
 			if (gotsys_conf_parse(GOTSYSD_SYSCONF_FILENAME,
 			    &gotsysconf, &sysconf_fd))
 				err = got_error(GOT_ERR_PARSE_CONFIG);
@@ -182,6 +189,16 @@ dispatch_event(int fd, short event, void *arg)
 			    &gotsysconf.repos);
 			if (err)
 				break;
+			err = gotsys_imsg_send_mediatypes(iev,
+			    &gotsysconf.mediatypes,
+			    GOTSYSD_IMSG_SYSCONF_GLOBAL_MEDIA_TYPE,
+			    GOTSYSD_IMSG_SYSCONF_GLOBAL_MEDIA_TYPES_DONE);
+			if (err)
+				break;
+			err = gotsys_imsg_send_webservers(iev,
+			    &gotsysconf.webservers);
+			if (err)
+				break;
 			err = send_done(iev);
 			if (err)
 				break;
@@ -226,6 +243,12 @@ main(int argc, char *argv[])
 #endif
 	gotsys_conf_init(&gotsysconf);
 
+	err = gotsys_conf_init_media_types(&gotsysconf.mediatypes);
+	if (err) {
+		warnx("could not initialize media types: %s", err->msg);
+		return 1;
+	}
+		
 	event_init();
 
 	signal_set(&evsigint, SIGINT, sighdlr, NULL);
blob - 1fb9acda00b5b1a23dce239dab2f445698ef8859
blob + 321468836e2d36b6074793ecb5f0110c4f4b3f18
--- gotsysd/libexec/gotsys-repo-create/Makefile
+++ gotsysd/libexec/gotsys-repo-create/Makefile
@@ -4,11 +4,12 @@
 
 PROG=		gotsys-repo-create
 SRCS=		gotsys-repo-create.c error.c hash.c pollfd.c path.c \
-		imsg.c gotsys_conf.c gotsys_imsg.c repository_init.c \
+		imsg.c gotsys_conf.c media.c gotsys_imsg.c repository_init.c \
 		reference_parse.c lockfile.c
 
 CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib \
-	-I${.CURDIR}/../../../gotsys -I${.CURDIR}/../../ -I${.CURDIR}
+	-I${.CURDIR}/../../../gotsys -I${.CURDIR}/../../../gotwebd \
+	-I${.CURDIR}/../../ -I${.CURDIR}
 YFLAGS =
 
 .if defined(PROFILE)
blob - 276d7af33c86b42cee831d6de1400671bc2d6290
blob + b861b4337ebc4c5ee8fdf5cd0df985d760b7dcbe
--- gotsysd/libexec/gotsys-repo-create/gotsys-repo-create.c
+++ gotsysd/libexec/gotsys-repo-create/gotsys-repo-create.c
@@ -51,6 +51,8 @@
 #include "got_lib_lockfile.h"
 
 #include "gotsysd.h"
+#include "media.h"
+#include "gotwebd.h"
 #include "gotsys.h"
 
 static struct gotsys_conf gotsysconf;
blob - d392d65385bbb4ca45da2f5be6c5e4f93392e751
blob + fb061e3dbf61ab8b46ff1769c8cdf61f63e3c3e9
--- gotsysd/libexec/gotsys-rmkeys/Makefile
+++ gotsysd/libexec/gotsys-rmkeys/Makefile
@@ -3,11 +3,13 @@
 .include "../../../got-version.mk"
 
 PROG=		gotsys-rmkeys
-SRCS=		gotsys-rmkeys.c error.c hash.c pollfd.c path.c \
-		imsg.c gotsys_conf.c gotsys_imsg.c opentemp.c gotsys_uidset.c
+SRCS=		gotsys-rmkeys.c error.c hash.c pollfd.c path.c media.c \
+		imsg.c gotsys_conf.c gotsys_imsg.c opentemp.c gotsys_uidset.c \
+		reference_parse.c
 
 CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib \
-	-I${.CURDIR}/../../../gotsys -I${.CURDIR}/../../ -I${.CURDIR}
+	-I${.CURDIR}/../../../gotsys -I${.CURDIR}/../../../gotwebd \
+	-I${.CURDIR}/../../ -I${.CURDIR}
 YFLAGS =
 
 .if defined(PROFILE)
blob - 95d8ee39598d3a3e20ba91667a65ee5d15ae421d
blob + 7e67b29f962375e303c84fcfb80594a7f00bd379
--- gotsysd/libexec/gotsys-rmkeys/gotsys-rmkeys.c
+++ gotsysd/libexec/gotsys-rmkeys/gotsys-rmkeys.c
@@ -41,8 +41,11 @@
 #include "got_path.h"
 #include "got_opentemp.h"
 #include "got_object.h"
+#include "got_reference.h"
 
 #include "gotsysd.h"
+#include "media.h"
+#include "gotwebd.h"
 #include "gotsys.h"
 
 static int lockfd = -1;
blob - 441aae1f68d4359e9447a5429523dea54fb9a311
blob + 91eb762d740b380a8140de401b3d1047043f50e1
--- gotsysd/libexec/gotsys-sshdconfig/Makefile
+++ gotsysd/libexec/gotsys-sshdconfig/Makefile
@@ -7,7 +7,8 @@ SRCS=		gotsys-sshdconfig.c error.c hash.c pollfd.c pat
 		imsg.c
 
 CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib \
-	-I${.CURDIR}/../../../gotsys -I${.CURDIR}/../../ -I${.CURDIR}
+	-I${.CURDIR}/../../../gotsys -I${.CURDIR}/../../../gotwebd \
+	-I${.CURDIR}/../../ -I${.CURDIR}
 YFLAGS =
 
 .if defined(PROFILE)
blob - 017c7057b1d231cc82c3e9f8629590417e921a47
blob + 0549bb009ce83f72f040dfe2c232a82efb79201a
--- gotsysd/libexec/gotsys-sshdconfig/gotsys-sshdconfig.c
+++ gotsysd/libexec/gotsys-sshdconfig/gotsys-sshdconfig.c
@@ -40,8 +40,11 @@
 #include "got_path.h"
 #include "got_opentemp.h"
 #include "got_object.h"
+#include "got_reference.h"
 
 #include "gotsysd.h"
+#include "media.h"
+#include "gotwebd.h"
 #include "gotsys.h"
 
 #define SSHD_CONFIG_PATH "/etc/ssh/sshd_config"
blob - a51d8e218fb31829575218c17be5548e7d3a3694
blob + a9af756d09e5913f132ee1f5f8cce87b9c2d833d
--- gotsysd/libexec/gotsys-useradd/Makefile
+++ gotsysd/libexec/gotsys-useradd/Makefile
@@ -4,10 +4,12 @@
 
 PROG=		gotsys-useradd
 SRCS=		gotsys-useradd.c pwd_mkdb.c error.c hash.c pollfd.c path.c \
-		imsg.c gotsys_conf.c gotsys_imsg.c opentemp.c gotsys_uidset.c
+		imsg.c gotsys_conf.c media.c gotsys_imsg.c opentemp.c \
+		gotsys_uidset.c reference_parse.c
 
 CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib \
-	-I${.CURDIR}/../../../gotsys -I${.CURDIR}/../../ -I${.CURDIR}
+	-I${.CURDIR}/../../../gotsys -I${.CURDIR}/../../../gotwebd \
+	-I${.CURDIR}/../../ -I${.CURDIR}
 YFLAGS =
 
 .if defined(PROFILE)
blob - a36f393cbcb3177e1b10c06ded19e7f21c032c72
blob + d081094fbdf495ca1f60bb1ae1ff98c564918da3
--- gotsysd/libexec/gotsys-useradd/gotsys-useradd.c
+++ gotsysd/libexec/gotsys-useradd/gotsys-useradd.c
@@ -69,8 +69,11 @@
 #include "got_path.h"
 #include "got_opentemp.h"
 #include "got_object.h"
+#include "got_reference.h"
 
 #include "gotsysd.h"
+#include "media.h"
+#include "gotwebd.h"
 #include "gotsys.h"
 #include "pwd_mkdb.h"
 
blob - 20aeb92dbc1d30889d19192afe53591b9de4061b
blob + 9a3e3335faab8ef56b2f2c3266fd4abd3bea48db
--- gotsysd/libexec/gotsys-userhome/Makefile
+++ gotsysd/libexec/gotsys-userhome/Makefile
@@ -4,10 +4,11 @@
 
 PROG=		gotsys-userhome
 SRCS=		gotsys-userhome.c error.c hash.c pollfd.c path.c \
-		imsg.c gotsys_conf.c gotsys_imsg.c
+		imsg.c gotsys_conf.c media.c gotsys_imsg.c reference_parse.c
 
 CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib \
-	-I${.CURDIR}/../../../gotsys -I${.CURDIR}/../../ -I${.CURDIR}
+	-I${.CURDIR}/../../../gotsys -I${.CURDIR}/../../../gotwebd \
+	-I${.CURDIR}/../../ -I${.CURDIR}
 YFLAGS =
 
 .if defined(PROFILE)
blob - 6db22b4f35e5bcb9a93cd7bdfc34e856e3669285
blob + 4453471c40767aa1128cab6d58f177140526c993
--- gotsysd/libexec/gotsys-userhome/gotsys-userhome.c
+++ gotsysd/libexec/gotsys-userhome/gotsys-userhome.c
@@ -39,8 +39,11 @@
 #include "got_error.h"
 #include "got_path.h"
 #include "got_object.h"
+#include "got_reference.h"
 
 #include "gotsysd.h"
+#include "media.h"
+#include "gotwebd.h"
 #include "gotsys.h"
 
 enum gotsys_userhome_state {
blob - aa362761da40e137fb8d8565fce156b48885940f
blob + cd039c8867bb2b97f814e06d27d5b6faa8fb380a
--- gotsysd/libexec/gotsys-userkeys/Makefile
+++ gotsysd/libexec/gotsys-userkeys/Makefile
@@ -4,10 +4,11 @@
 
 PROG=		gotsys-userkeys
 SRCS=		gotsys-userkeys.c error.c hash.c pollfd.c path.c opentemp.c \
-		imsg.c gotsys_conf.c gotsys_imsg.c
+		imsg.c gotsys_conf.c media.c gotsys_imsg.c reference_parse.c
 
 CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib \
-	-I${.CURDIR}/../../../gotsys -I${.CURDIR}/../../ -I${.CURDIR}
+	-I${.CURDIR}/../../../gotsys -I${.CURDIR}/../../../gotwebd \
+	-I${.CURDIR}/../../ -I${.CURDIR}
 YFLAGS =
 
 .if defined(PROFILE)
blob - 85247e9112e6bb40448e21e69afb0073b62517f1
blob + cc7bbb4e42f961f932054dd9a8c25629e7f3135a
--- gotsysd/libexec/gotsys-userkeys/gotsys-userkeys.c
+++ gotsysd/libexec/gotsys-userkeys/gotsys-userkeys.c
@@ -40,8 +40,11 @@
 #include "got_path.h"
 #include "got_opentemp.h"
 #include "got_object.h"
+#include "got_reference.h"
 
 #include "gotsysd.h"
+#include "media.h"
+#include "gotwebd.h"
 #include "gotsys.h"
 
 static char authorized_keys_path[_POSIX_PATH_MAX];
blob - 4cf2c6bad5710597487e40327b1c1b47a9e65ba2
blob + e6de2d3a2e74cb50c2daa22b16e94dd26daf8c2c
--- gotsysd/libexec/gotsys-write-conf/Makefile
+++ gotsysd/libexec/gotsys-write-conf/Makefile
@@ -4,11 +4,12 @@
 
 PROG=		gotsys-write-conf
 SRCS=		gotsys-write-conf.c error.c path.c hash.c pollfd.c \
-		log.c imsg.c gotsys_conf.c gotsys_imsg.c opentemp.c \
+		log.c imsg.c gotsys_conf.c media.c gotsys_imsg.c opentemp.c \
 		reference_parse.c
 
 CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib \
-	-I${.CURDIR}/../../../gotsys -I${.CURDIR}/../../ -I${.CURDIR}
+	-I${.CURDIR}/../../../gotsys -I${.CURDIR}/../../../gotwebd \
+	-I${.CURDIR}/../../ -I${.CURDIR}
 YFLAGS =
 
 .if defined(PROFILE)
blob - f9542e2fe0f13d47685b868a95f099a0be351d9f
blob + 1c6455a8472ac1f9bd9af0392c509026ef880ada
--- gotsysd/libexec/gotsys-write-conf/gotsys-write-conf.c
+++ gotsysd/libexec/gotsys-write-conf/gotsys-write-conf.c
@@ -39,11 +39,15 @@
 #include "got_reference.h"
 
 #include "gotsysd.h"
+#include "media.h"
+#include "gotwebd.h"
 #include "gotsys.h"
 
+static struct gotsysd_web_config webcfg;
 static struct gotsys_conf gotsysconf;
 static struct gotsys_userlist *users_cur;
 static struct gotsys_repo *repo_cur;
+static struct gotsys_website *site_cur;
 static struct got_pathlist_head *protected_refs_cur;
 static size_t nprotected_refs_needed;
 static size_t nprotected_refs_received;
@@ -51,6 +55,8 @@ static int gotd_conf_tmpfd = -1;
 static char *gotd_conf_tmppath;
 static int gotd_secrets_tmpfd = -1;
 static char *gotd_secrets_tmppath;
+static int gotwebd_conf_tmpfd = -1;
+static char *gotwebd_conf_tmppath;
 static struct gotsys_access_rule_list global_repo_access_rules;
 static struct got_pathlist_head *notif_refs_cur;
 static size_t *num_notif_refs_cur;
@@ -58,14 +64,20 @@ static size_t num_notif_refs_needed;
 static size_t num_notif_refs_received;
 
 enum writeconf_state {
+	WRITECONF_STATE_EXPECT_GOTWEB_CFG,
+	WRITECONF_STATE_EXPECT_GOTWEB_ADDRS,
+	WRITECONF_STATE_EXPECT_GOTWEB_SERVERS,
 	WRITECONF_STATE_EXPECT_USERS,
 	WRITECONF_STATE_EXPECT_GROUPS,
+	WRITECONF_STATE_EXPECT_GLOBAL_ACCESS_RULES,
 	WRITECONF_STATE_EXPECT_REPOS,
+	WRITECONF_STATE_EXPECT_MEDIA_TYPES,
+	WRITECONF_STATE_EXPECT_WEB_SERVERS,
 	WRITECONF_STATE_WRITE_CONF,
 	WRITECONF_STATE_DONE
 };
 
-static enum writeconf_state writeconf_state = WRITECONF_STATE_EXPECT_USERS;
+static enum writeconf_state writeconf_state = WRITECONF_STATE_EXPECT_GOTWEB_CFG;
 
 static void
 sighdlr(int sig, short event, void *arg)
@@ -103,25 +115,104 @@ send_done(struct gotsysd_imsgev *iev)
 }
 
 static const struct got_error *
-write_access_rule(const char *access, const char * authorization,
-    const char *identifier)
+write_access_rule(int fd, const char *path, const char *prefix,
+    const char *access, const char * authorization, const char *identifier)
 {
 	int ret;
 
-	ret = dprintf(gotd_conf_tmpfd, "\t%s%s%s\n",
-	    access, authorization, identifier);
+	ret = dprintf(fd, "%s%s%s%s\n", prefix, access, authorization,
+	    identifier);
 	if (ret == -1)
-		return got_error_from_errno2("dprintf", gotd_conf_tmppath);
-	if (ret != 1 + strlen(access) + strlen(authorization) +
+		return got_error_from_errno2("dprintf", path);
+	if (ret != strlen(prefix) + strlen(access) + strlen(authorization) +
 	    strlen(identifier) + 1) {
 		return got_error_fmt(GOT_ERR_IO,
-		    "short write to %s", gotd_conf_tmppath);
+		    "short write to %s", path);
 	}
 
 	return NULL;
 }
 
 static const struct got_error *
+write_gotsys_auth_config(int fd, const char *path,
+    const char *prefix, enum gotsys_auth_config auth_config,
+    enum gotsysd_web_auth_config webd_auth_config)
+{
+	int ret;
+
+	switch (auth_config) {
+	case GOTSYS_AUTH_UNSET:
+		break;
+	case GOTSYS_AUTH_DISABLED:
+		ret = dprintf(fd, "%sdisable authentication\n", prefix);
+		if (ret == -1) 
+			return got_error_from_errno2("dprintf", path);
+		if (ret != strlen(prefix) + 22 + 1) {
+			return got_error_fmt(GOT_ERR_IO,
+			"short write to %s", path);
+		}
+		break;
+	case GOTSYS_AUTH_ENABLED:
+		if (webd_auth_config == GOTSYSD_WEB_AUTH_INSECURE) {
+			ret = dprintf(fd,
+			    "%senable authentication insecure\n", prefix);
+			if (ret == -1) 
+				return got_error_from_errno2("dprintf", path);
+			if (ret != strlen(prefix) + 30 + 1) {
+				return got_error_fmt(GOT_ERR_IO,
+				"short write to %s", path);
+			}
+		} else {
+			ret = dprintf(fd, "%senable authentication\n", prefix);
+			if (ret == -1) 
+				return got_error_from_errno2("dprintf", path);
+			if (ret != strlen(prefix) + 21 + 1) {
+				return got_error_fmt(GOT_ERR_IO,
+				"short write to %s", path);
+			}
+		}
+		break;
+	default:
+		return got_error_fmt(GOT_ERR_PARSE_CONFIG,
+		    "bad gotsysd web authentication mode %u", auth_config);
+	}
+
+	return NULL;
+}
+
+static const struct got_error *
+write_gotsysd_web_auth_config(int fd, const char *path,
+    enum gotsysd_web_auth_config auth_config)
+{
+	int ret;
+
+	switch (auth_config) {
+	case GOTSYSD_WEB_AUTH_UNSET:
+		break;
+	case GOTSYSD_WEB_AUTH_DISABLED:
+		ret = dprintf(fd, "\tdisable authentication\n");
+		if (ret == -1) 
+			return got_error_from_errno2("dprintf", path);
+		break;
+	case GOTSYSD_WEB_AUTH_SECURE:
+		ret = dprintf(fd, "\tenable authentication\n");
+		if (ret == -1) 
+			return got_error_from_errno2("dprintf", path);
+		break;
+	case GOTSYSD_WEB_AUTH_INSECURE:
+		ret = dprintf(fd, "\tenable authentication insecure\n");
+		if (ret == -1) 
+			return got_error_from_errno2("dprintf", path);
+		break;
+	default:
+		return got_error_fmt(GOT_ERR_PARSE_CONFIG,
+		    "bad gotsysd web authentication mode %u", auth_config);
+	}
+
+	return NULL;
+}
+
+static const struct got_error *
 write_global_access_rules(void)
 {
 	const struct got_error *err;
@@ -161,13 +252,15 @@ write_global_access_rules(void)
 				if (rule->access == GOTSYS_ACCESS_PERMITTED &&
 				    strcmp(user->name, "anonymous") == 0)
 					continue;
-				err = write_access_rule(access, authorization,
-				    user->name);
+				err = write_access_rule(gotd_conf_tmpfd,
+				    gotd_conf_tmppath, "\t",
+				    access, authorization, user->name);
 				if (err)
 					return err;
 			}
 		} else {
-			err = write_access_rule(access, authorization,
+			err = write_access_rule(gotd_conf_tmpfd,
+			    gotd_conf_tmppath, "\t", access, authorization,
 			    rule->identifier);
 			if (err)
 				return err;
@@ -178,7 +271,8 @@ write_global_access_rules(void)
 }
 
 static const struct got_error *
-write_access_rules(struct gotsys_access_rule_list *rules)
+write_access_rules(int fd, const char *path,
+    struct gotsys_access_rule_list *rules)
 {
 	const struct got_error *err;
 	struct gotsys_access_rule *rule;
@@ -206,8 +300,8 @@ write_access_rules(struct gotsys_access_rule_list *rul
 		else
 			authorization = "";
 
-		err = write_access_rule(access, authorization,
-		    rule->identifier);
+		err = write_access_rule(fd, path, "\t",
+		    access, authorization, rule->identifier);
 		if (err)
 			return err;
 	}
@@ -216,6 +310,37 @@ write_access_rules(struct gotsys_access_rule_list *rul
 }
 
 static const struct got_error *
+write_web_access_rules(int fd, const char *path,
+    const char *prefix, struct gotsys_access_rule_list *rules)
+{
+	const struct got_error *err;
+	struct gotsys_access_rule *rule;
+
+	STAILQ_FOREACH(rule, rules, entry) {
+		const char *access;
+
+		switch (rule->access) {
+		case GOTSYS_ACCESS_DENIED:
+			access = "deny ";
+			break;
+		case GOTSYS_ACCESS_PERMITTED:
+			access = "permit ";
+			break;
+		default:
+			return got_error_fmt(GOT_ERR_PARSE_CONFIG,
+			    "access rule with unknown access flag %d",
+			    rule->access);
+		}
+
+		err = write_access_rule(fd, path, prefix, access, "",
+		    rule->identifier); if (err)
+			return err;
+	}
+
+	return NULL;
+}
+
+static const struct got_error *
 refname_is_valid(const char *refname)
 {
 	if (strncmp(refname, "refs/", 5) != 0) {
@@ -743,7 +868,8 @@ write_gotd_conf(int *auth_idx)
 			    "short write to %s", gotd_conf_tmppath);
 		}
 
-		err = write_access_rules(&repo->access_rules);
+		err = write_access_rules(gotd_conf_tmpfd, gotd_conf_tmppath,
+		    &repo->access_rules);
 		if (err)
 			return err;
 
@@ -799,6 +925,502 @@ write_gotd_conf(int *auth_idx)
 	return NULL;
 }
 
+static const struct got_error *
+write_webrepo(int *show_repo_description, int fd, const char *path,
+    struct gotsys_webrepo *webrepo,
+    enum gotsysd_web_auth_config webd_auth_config)
+{
+	const struct got_error *err;
+	struct gotsys_repo *repo;
+	int ret;
+
+	TAILQ_FOREACH(repo, &gotsysconf.repos, entry) {
+		if (strcmp(repo->name, webrepo->repo_name) == 0)
+			break;
+	}
+
+	ret = dprintf(fd, "\trepository \"%s\" {\n", webrepo->repo_name);
+	if (ret == -1) 
+		return got_error_from_errno2("dprintf", path);
+	if (ret != 16 + strlen(webrepo->repo_name) + 1)
+		return got_error_fmt(GOT_ERR_IO, "short write to %s", path);
+
+	err = write_gotsys_auth_config(fd, path, "\t\t", webrepo->auth_config,
+	    webd_auth_config);
+	if (err)
+		return err;
+
+	err = write_web_access_rules(fd, path, "\t\t", &webrepo->access_rules);
+	if (err)
+		return err;
+
+	if (webrepo->hidden != -1) {
+		const char *val;
+
+		val = webrepo->hidden ? "on" : "off";
+		ret = dprintf(fd, "\t\thide repository %s\n", val);
+		if (ret == -1) 
+			return got_error_from_errno2("dprintf", path);
+		if (ret != 18 + strlen(val) + 1) {
+			return got_error_fmt(GOT_ERR_IO,
+			    "short write to %s", path);
+		}
+	}
+
+	if (repo && repo->description[0] != '\0') {
+		ret = dprintf(fd, "\t\tdescription \"%s\"\n", repo->description);
+		if (ret == -1) 
+			return got_error_from_errno2("dprintf", path);
+		if (ret != 16 + strlen(repo->description) + 1) {
+			return got_error_fmt(GOT_ERR_IO,
+			    "short write to %s", path);
+		}
+
+		*show_repo_description = 1;
+	}
+
+	ret = dprintf(fd, "\t}\n");
+	if (ret == -1) 
+		return got_error_from_errno2("dprintf", path);
+	if (ret != 3)
+		return got_error_fmt(GOT_ERR_IO, "short write to %s", path);
+
+	return NULL;
+}
+
+static const struct got_error *
+write_website(int fd, const char *path, struct gotsys_website *site,
+    enum gotsysd_web_auth_config webd_auth_config)
+{
+	const struct got_error *err;
+	int ret;
+
+	ret = dprintf(fd, "\twebsite \"%s\" {\n", site->url_path);
+	if (ret == -1) 
+		return got_error_from_errno2("dprintf", path);
+	if (ret != 13 + strlen(site->url_path) + 1)
+		return got_error_fmt(GOT_ERR_IO, "short write to %s", path);
+
+	ret = dprintf(fd, "\t\trepository \"%s\"\n", site->repo_name);
+	if (ret == -1) 
+		return got_error_from_errno2("dprintf", path);
+	if (ret != 15 + strlen(site->repo_name) + 1)
+		return got_error_fmt(GOT_ERR_IO, "short write to %s", path);
+
+	if (site->branch_name[0] != '\0') {
+		ret = dprintf(fd, "\t\tbranch \"%s\"\n", site->branch_name);
+		if (ret == -1) 
+			return got_error_from_errno2("dprintf", path);
+		if (ret != 11 + strlen(site->branch_name) + 1) {
+			return got_error_fmt(GOT_ERR_IO,
+			    "short write to %s", path);
+		}
+	}
+
+	ret = dprintf(fd, "\t\tpath \"%s\"\n", site->path[0] ? site->path : "/");
+	if (ret == -1) 
+		return got_error_from_errno2("dprintf", path);
+	if (ret != 9 + (site->path[0] ? strlen(site->path) : 1) + 1)
+		return got_error_fmt(GOT_ERR_IO, "short write to %s", path);
+
+	err = write_gotsys_auth_config(fd, path, "\t\t", site->auth_config,
+	    webd_auth_config);
+	if (err)
+		return err;
+
+	err = write_web_access_rules(fd, path, "\t\t", &site->access_rules);
+	if (err)
+		return err;
+
+	ret = dprintf(fd, "\t}\n");
+	if (ret == -1) 
+		return got_error_from_errno2("dprintf", path);
+	if (ret != 3)
+		return got_error_fmt(GOT_ERR_IO, "short write to %s", path);
+
+	return NULL;
+}
+
+static const struct got_error *
+write_gotwebd_conf(void)
+{
+	const struct got_error *err = NULL;
+	int ret, fd = gotwebd_conf_tmpfd;
+	const char *path = gotwebd_conf_tmppath;
+	struct gotsysd_web_address *addr;
+	struct gotsysd_web_server *srv_cfg;
+
+	err = got_opentemp_truncatefd(fd);
+	if (err)
+		return err;
+
+	/* TODO: show gotsys.git commit hash */
+	ret = dprintf(fd, "# generated by gotsysd, do not edit\n");
+	if (ret == -1)
+		return got_error_from_errno2("dprintf", path);
+	if (ret != 35 + 1)
+		return got_error_fmt(GOT_ERR_IO, "short write to %s", path);
+
+	if (webcfg.control_socket[0] != '\0') {
+		ret = dprintf(fd, "control socket \"%s\"\n",
+		    webcfg.control_socket);
+		if (ret == -1)
+			return got_error_from_errno2("dprintf", path);
+		if (ret != 17 + strlen(webcfg.control_socket) + 1) {
+			return got_error_fmt(GOT_ERR_IO, "short write to %s",
+			    path);
+		}
+	}
+
+	if (webcfg.httpd_chroot[0] != '\0') {
+		ret = dprintf(fd, "chroot \"%s\"\n", webcfg.httpd_chroot);
+		if (ret == -1)
+			return got_error_from_errno2("dprintf", path);
+		if (ret != 9 + strlen(webcfg.httpd_chroot) + 1) {
+			return got_error_fmt(GOT_ERR_IO, "short write to %s",
+			    path);
+		}
+	}
+
+	if (webcfg.htdocs_path[0] != '\0') {
+		ret = dprintf(fd, "htdocs \"%s\"\n", webcfg.htdocs_path);
+		if (ret == -1) 
+			return got_error_from_errno2("dprintf", path);
+		if (ret != 9 + strlen(webcfg.htdocs_path) + 1) {
+			return got_error_fmt(GOT_ERR_IO, "short write to %s",
+			    path);
+		}
+	}
+
+	if (webcfg.gotwebd_user[0] != '\0') {
+		ret = dprintf(fd, "user \"%s\"\n", webcfg.gotwebd_user);
+		if (ret == -1) 
+			return got_error_from_errno2("dprintf", path);
+		if (ret != 7 + strlen(webcfg.gotwebd_user) + 1) {
+			return got_error_fmt(GOT_ERR_IO, "short write to %s",
+			    path);
+		}
+	}
+
+	if (webcfg.www_user[0] != '\0') {
+		ret = dprintf(fd, "www user \"%s\"\n", webcfg.www_user);
+		if (ret == -1) 
+			return got_error_from_errno2("dprintf", path);
+		if (ret != 11 + strlen(webcfg.www_user) + 1) {
+			return got_error_fmt(GOT_ERR_IO, "short write to %s",
+			    path);
+		}
+	}
+
+	if (webcfg.login_hint_user[0] != '\0') {
+		ret = dprintf(fd, "login hint user \"%s\"\n",
+		    webcfg.login_hint_user);
+		if (ret == -1) 
+			return got_error_from_errno2("dprintf", path);
+		if (ret != 18 + strlen(webcfg.login_hint_user) + 1) {
+			return got_error_fmt(GOT_ERR_IO, "short write to %s",
+			    path);
+		}
+	}
+
+	if (webcfg.login_hint_port[0] != '\0') {
+		ret = dprintf(fd, "login hint port \"%s\"\n",
+		    webcfg.login_hint_port);
+		if (ret == -1) 
+			return got_error_from_errno2("dprintf", path);
+		if (ret != 18 + strlen(webcfg.login_hint_port) + 1) {
+			return got_error_fmt(GOT_ERR_IO, "short write to %s",
+			    path);
+		}
+	}
+
+	switch (webcfg.auth_config) {
+	case GOTSYSD_WEB_AUTH_UNSET:
+		break;
+	case GOTSYSD_WEB_AUTH_DISABLED:
+		ret = dprintf(fd, "disable authentication\n");
+		if (ret == -1) 
+			return got_error_from_errno2("dprintf", path);
+		if (ret != 22 + 1) {
+			return got_error_fmt(GOT_ERR_IO, "short write to %s",
+			    path);
+		}
+		break;
+	case GOTSYSD_WEB_AUTH_SECURE:
+		ret = dprintf(fd, "enable authentication\n");
+		if (ret == -1) 
+			return got_error_from_errno2("dprintf", path);
+		if (ret != 21 + 1) {
+			return got_error_fmt(GOT_ERR_IO, "short write to %s",
+			    path);
+		}
+		break;
+	case GOTSYSD_WEB_AUTH_INSECURE:
+		ret = dprintf(fd, "enable authentication insecure\n");
+		if (ret == -1) 
+			return got_error_from_errno2("dprintf", path);
+		if (ret != 30 + 1) {
+			return got_error_fmt(GOT_ERR_IO, "short write to %s",
+			    path);
+		}
+		break;
+	default:
+		return got_error_fmt(GOT_ERR_PARSE_CONFIG,
+		    "bad global authentication mode %u", webcfg.auth_config);
+	}
+
+	TAILQ_FOREACH(addr, &webcfg.listen_addrs, entry) {
+		switch (addr->family) {
+		case GOTSYSD_LISTEN_ADDR_UNIX:
+			ret = dprintf(fd, "listen on socket \"%s\"\n",
+			    addr->addr.unix_socket_path);
+			if (ret == -1) 
+				return got_error_from_errno2("dprintf", path);
+			if (ret != 19 +
+			    strlen(addr->addr.unix_socket_path) + 1) {
+				return got_error_fmt(GOT_ERR_IO,
+				    "short write to %s", path);
+			}
+			break;
+		case GOTSYSD_LISTEN_ADDR_INET:
+			ret = dprintf(fd, "listen on \"%s\" port \"%s\"\n",
+			    addr->addr.inet.address, addr->addr.inet.port);
+			if (ret == -1) 
+				return got_error_from_errno2("dprintf", path);
+			if (ret != 20 +
+			    strlen(addr->addr.inet.address) +
+			    strlen(addr->addr.inet.port) + 1) {
+				return got_error_fmt(GOT_ERR_IO,
+				    "short write to %s", path);
+			}
+			break;
+		default:
+			return got_error_fmt(GOT_ERR_PARSE_CONFIG,
+			    "bad listen address family %u", addr->family);
+		}
+	}
+
+	STAILQ_FOREACH(srv_cfg, &webcfg.servers, entry) {
+		struct gotsys_webserver *srv;
+		int hide_repositories = -1;
+
+		STAILQ_FOREACH(srv, &gotsysconf.webservers, entry) {
+			if (strcmp(srv->server_name, srv_cfg->server_name) == 0)
+				break;
+		}
+
+		ret = dprintf(fd, "server \"%s\" {\n", srv_cfg->server_name);
+		if (ret == -1) 
+			return got_error_from_errno2("dprintf", path);
+		if (ret != 11 + strlen(srv_cfg->server_name) + 1) {
+			return got_error_fmt(GOT_ERR_IO,
+			    "short write to %s", path);
+		}
+
+		ret = dprintf(fd, "\trepos_path \"%s\"\n", webcfg.repos_path);
+		if (ret == -1) 
+			return got_error_from_errno2("dprintf", path);
+		if (ret != 14 + strlen(webcfg.repos_path) + 1) {
+			return got_error_fmt(GOT_ERR_IO,
+			    "short write to %s", path);
+		}
+
+		if (srv_cfg->gotweb_url_root[0] != '\0') {
+			ret = dprintf(fd, "\tgotweb_url_root \"%s\"\n",
+			    srv_cfg->gotweb_url_root);
+			if (ret == -1) 
+				return got_error_from_errno2("dprintf", path);
+			if (ret != 19 + strlen(srv_cfg->gotweb_url_root) + 1) {
+				return got_error_fmt(GOT_ERR_IO,
+				    "short write to %s", path);
+			}
+		}
+
+		if (srv_cfg->htdocs_path[0] != '\0') {
+			ret = dprintf(fd, "\thtdocs \"%s\"\n",
+			    srv_cfg->htdocs_path);
+			if (ret == -1) 
+				return got_error_from_errno2("dprintf", path);
+			if (ret != 10 + strlen(srv_cfg->htdocs_path) + 1) {
+				return got_error_fmt(GOT_ERR_IO,
+				    "short write to %s", path);
+			}
+		}
+
+		if (srv && srv->auth_config != GOTSYS_AUTH_UNSET) {
+			err = write_gotsys_auth_config(fd, path, "\t",
+			    srv->auth_config, srv_cfg->auth_config);
+			if (err)
+				return err;
+		} else {
+			err = write_gotsysd_web_auth_config(fd, path,
+			    srv_cfg->auth_config);
+			if (err)
+				return err;
+		}
+
+		if (srv && srv->hide_repositories != -1)
+			hide_repositories = srv->hide_repositories;
+		else if (srv_cfg->hide_repositories != -1)
+			hide_repositories = srv_cfg->hide_repositories;
+
+		if (hide_repositories != -1) {
+			const char *val;
+
+			val = srv->hide_repositories ? "on" : "off";
+			ret = dprintf(fd, "\thide repositories %s\n", val);
+			if (ret == -1) 
+				return got_error_from_errno2("dprintf", path);
+			if (ret != 19 + strlen(val) + 1) {
+				return got_error_fmt(GOT_ERR_IO,
+				    "short write to %s", path);
+			}
+		}
+
+		if (srv) {
+			struct gotsys_webrepo *webrepo;
+			struct got_pathlist_entry *pe;
+			int show_repo_description = 0;
+
+			err = write_web_access_rules(fd, path, "\t",
+			    &srv->access_rules);
+			if (err)
+				return err;
+
+			/* TODO: css, logo, logo URL */
+
+			if (srv->site_owner[0] != '\0') {
+				ret = dprintf(fd, "\tsite_owner \"%s\"\n",
+				    srv->site_owner);
+				if (ret == -1) {
+					return got_error_from_errno2("dprintf",
+					    path);
+				}
+				if (ret != 14 + strlen(srv->site_owner) + 1) {
+					return got_error_fmt(GOT_ERR_IO,
+					    "short write to %s", path);
+				}
+			} else {
+				ret = dprintf(fd, "\tshow_site_owner off\n");
+				if (ret == -1)  {
+					return got_error_from_errno2("dprintf",
+					    path);
+				}
+				if (ret != 20 + 1) {
+					return got_error_fmt(GOT_ERR_IO,
+					    "short write to %s", path);
+				}
+			}
+
+			if (srv->repos_url_path[0] != '\0') {
+				ret = dprintf(fd, "\trepos_url_path \"%s\"\n",
+				    srv->repos_url_path);
+				if (ret == -1)  {
+					return got_error_from_errno2("dprintf",
+					    path);
+				}
+				if (ret != 18 +
+				    strlen(srv->repos_url_path) + 1) {
+					return got_error_fmt(GOT_ERR_IO,
+					    "short write to %s", path);
+				}
+			}
+
+			/* TODO mediatypes */
+
+			STAILQ_FOREACH(webrepo, &srv->repos, entry) {
+				err = write_webrepo(&show_repo_description,
+				    fd, path, webrepo, srv_cfg->auth_config);
+				if (err)
+					return err;
+			}
+			if (!show_repo_description) {
+				ret = dprintf(fd,
+				    "\tshow_repo_description off\n");
+				if (ret == -1)  {
+					return got_error_from_errno2("dprintf",
+					    path);
+				}
+				if (ret != 26 + 1) {
+					return got_error_fmt(GOT_ERR_IO,
+					    "short write to %s", path);
+				}
+			}
+
+			/*
+			 * Repository age and owner currently need to be off
+			 * to keep our regression tests passing: And these
+			 * options cannot be controlled via gotsys.conf yet.
+			 */
+			ret = dprintf(fd, "\tshow_repo_age off\n");
+			if (ret == -1) 
+				return got_error_from_errno2("dprintf", path);
+			if (ret != 18 + 1) {
+				return got_error_fmt(GOT_ERR_IO,
+				    "short write to %s", path);
+			}
+			ret = dprintf(fd, "\tshow_repo_owner off\n");
+			if (ret == -1) 
+				return got_error_from_errno2("dprintf", path);
+			if (ret != 20 + 1) {
+				return got_error_fmt(GOT_ERR_IO,
+				    "short write to %s", path);
+			}
+
+			RB_FOREACH(pe, got_pathlist_head, &srv->websites) {
+				struct gotsys_website *site = pe->data;
+
+				err = write_website(fd, path, site,
+				    srv_cfg->auth_config);
+				if (err)
+					return err;
+			}
+		}
+
+		ret = dprintf(fd, "}\n");
+		if (ret == -1) 
+			return got_error_from_errno2("dprintf", path);
+		if (ret != 2) {
+			return got_error_fmt(GOT_ERR_IO, "short write to %s",
+			    path);
+		}
+	}
+
+	if (webcfg.prefork != 0 && webcfg.prefork <= PROC_MAX_INSTANCES) {
+		char buf[8];
+
+		ret = snprintf(buf, sizeof(buf), "%d", webcfg.prefork);
+		if (ret == -1) 
+			return got_error_from_errno2("snprintf", path);
+		if ((size_t)ret >= sizeof(buf))
+			return got_error(GOT_ERR_NO_SPACE);
+
+		ret = dprintf(fd, "prefork %s\n", buf);
+		if (ret == -1) 
+			return got_error_from_errno2("dprintf", path);
+		if (ret != 8 + strlen(buf) + 1) {
+			return got_error_fmt(GOT_ERR_IO, "short write to %s",
+			    path);
+		}
+	}
+
+	if (fchmod(gotwebd_conf_tmpfd, 0644) == -1) {
+		return got_error_from_errno_fmt("chmod 0644 %s",
+		    gotwebd_conf_tmppath);
+	}
+		
+	if (rename(gotwebd_conf_tmppath, GOTWEBD_CONF) == -1) {
+		return got_error_from_errno_fmt("rename %s to %s",
+		    gotwebd_conf_tmppath, GOTWEBD_CONF);
+	}
+
+
+	free(gotwebd_conf_tmppath);
+	gotwebd_conf_tmppath = NULL;
+
+	return NULL;
+}
+
 static void
 dispatch_event(int fd, short event, void *arg)
 {
@@ -839,6 +1461,82 @@ dispatch_event(int fd, short event, void *arg)
 			break;
 
 		switch (imsg.hdr.type) {
+		case GOTSYSD_IMSG_SYSCONF_GOTWEB_CFG:
+			if (writeconf_state !=
+			    WRITECONF_STATE_EXPECT_GOTWEB_CFG) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+			err = gotsys_imsg_recv_web_cfg(&webcfg, &imsg);
+			if (err)
+				break;
+			writeconf_state = WRITECONF_STATE_EXPECT_GOTWEB_ADDRS;
+			break;
+		case GOTSYSD_IMSG_SYSCONF_GOTWEB_ADDR: {
+			struct gotsysd_web_address *addr;
+			const struct got_error *err;
+
+			if (writeconf_state !=
+			    WRITECONF_STATE_EXPECT_GOTWEB_ADDRS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+
+			err = gotsys_imsg_recv_webaddr(&addr, &imsg);
+			if (err)
+				break;
+			TAILQ_INSERT_TAIL(&webcfg.listen_addrs, addr, entry);
+			break;
+		}
+		case GOTSYSD_IMSG_SYSCONF_GOTWEB_ADDRS_DONE:
+			if (writeconf_state !=
+			    WRITECONF_STATE_EXPECT_GOTWEB_ADDRS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+			writeconf_state = WRITECONF_STATE_EXPECT_GOTWEB_SERVERS;
+			break;
+		case GOTSYSD_IMSG_SYSCONF_GOTWEB_SERVER: {
+			const struct got_error *err;
+			struct gotsysd_web_server *srv;
+
+			if (writeconf_state !=
+			    WRITECONF_STATE_EXPECT_GOTWEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+
+			err = gotsys_imsg_recv_gotweb_server(&srv, &imsg);
+			if (err)
+				break;
+			STAILQ_INSERT_TAIL(&webcfg.servers, srv, entry);
+			break;
+		}
+		case GOTSYSD_IMSG_SYSCONF_GOTWEB_SERVERS_DONE:
+			if (writeconf_state !=
+			    WRITECONF_STATE_EXPECT_GOTWEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+			break;
+		case GOTSYSD_IMSG_SYSCONF_GOTWEB_CFG_DONE:
+			writeconf_state = WRITECONF_STATE_EXPECT_USERS;
+			break;
 		case GOTSYSD_IMSG_SYSCONF_WRITE_CONF_USERS:
 			if (writeconf_state != WRITECONF_STATE_EXPECT_USERS) {
 				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
@@ -906,6 +1604,38 @@ dispatch_event(int fd, short event, void *arg)
 				    writeconf_state);
 				break;
 			}
+			writeconf_state =
+			    WRITECONF_STATE_EXPECT_GLOBAL_ACCESS_RULES;
+			break;
+		case GOTSYSD_IMSG_SYSCONF_GLOBAL_ACCESS_RULE: {
+			struct gotsys_access_rule_list *rules;
+			struct gotsys_access_rule *rule;
+
+			if (writeconf_state !=
+			    WRITECONF_STATE_EXPECT_GLOBAL_ACCESS_RULES) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+			err = gotsys_imsg_recv_access_rule(&rule, &imsg,
+			    NULL, NULL);
+			if (err)
+				break;
+			rules = &global_repo_access_rules;
+			STAILQ_INSERT_TAIL(rules, rule, entry);
+			break;
+		}
+		case GOTSYSD_IMSG_SYSCONF_GLOBAL_ACCESS_RULES_DONE:
+			if (writeconf_state !=
+			    WRITECONF_STATE_EXPECT_GLOBAL_ACCESS_RULES) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
 			writeconf_state = WRITECONF_STATE_EXPECT_REPOS;
 			break;
 		case GOTSYSD_IMSG_SYSCONF_REPO: {
@@ -923,34 +1653,9 @@ dispatch_event(int fd, short event, void *arg)
 				break;
 			TAILQ_INSERT_TAIL(&gotsysconf.repos, repo, entry);
 			repo_cur = repo;
+			site_cur = NULL;
 			break;
 		}
-		case GOTSYSD_IMSG_SYSCONF_GLOBAL_ACCESS_RULE: {
-			struct gotsys_access_rule *rule;
-			if (writeconf_state != WRITECONF_STATE_EXPECT_REPOS) {
-				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
-				    "received unexpected imsg %d while in "
-				    "state %d\n", imsg.hdr.type,
-				    writeconf_state);
-				break;
-			}
-			err = gotsys_imsg_recv_access_rule(&rule, &imsg,
-			    NULL, NULL);
-			if (err)
-				break;
-			STAILQ_INSERT_TAIL(&global_repo_access_rules, rule,
-			    entry);
-			break;
-		}
-		case GOTSYSD_IMSG_SYSCONF_GLOBAL_ACCESS_RULES_DONE:
-			if (writeconf_state != WRITECONF_STATE_EXPECT_REPOS) {
-				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
-				    "received unexpected imsg %d while in "
-				    "state %d\n", imsg.hdr.type,
-				    writeconf_state);
-				break;
-			}
-			break;
 		case GOTSYSD_IMSG_SYSCONF_ACCESS_RULE: {
 			struct gotsys_access_rule_list *rules;
 			struct gotsys_access_rule *rule;
@@ -1188,7 +1893,19 @@ dispatch_event(int fd, short event, void *arg)
 			break;
 		}
 		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_TARGETS_DONE:
-			break;
+			if (repo_cur == NULL ||
+			    num_notif_refs_needed != 0 ||
+			    writeconf_state != WRITECONF_STATE_EXPECT_REPOS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+
+			num_notif_refs_needed = 0;
+			notif_refs_cur = NULL;
+ 			break;
 		case GOTSYSD_IMSG_SYSCONF_REPOS_DONE:
 			if (writeconf_state != WRITECONF_STATE_EXPECT_REPOS) {
 				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
@@ -1198,7 +1915,377 @@ dispatch_event(int fd, short event, void *arg)
 				break;
 			}
 			repo_cur = NULL;
+			writeconf_state = WRITECONF_STATE_EXPECT_MEDIA_TYPES;
+			break;
+		case GOTSYSD_IMSG_SYSCONF_GLOBAL_MEDIA_TYPE: {
+			struct media_type media;
+
+			if (writeconf_state !=
+			    WRITECONF_STATE_EXPECT_MEDIA_TYPES) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+			err = gotsys_imsg_recv_media_type(&media, &imsg);
+			if (err)
+				break;
+			if (media_add(&gotsysconf.mediatypes, &media) == NULL)
+				err = got_error_from_errno("media_add");
+			break;
+		}
+		case GOTSYSD_IMSG_SYSCONF_GLOBAL_MEDIA_TYPES_DONE:
+			if (writeconf_state !=
+			    WRITECONF_STATE_EXPECT_MEDIA_TYPES) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+			writeconf_state = WRITECONF_STATE_EXPECT_WEB_SERVERS;
+			break;
+
+		case GOTSYSD_IMSG_SYSCONF_WEB_SERVER: {
+			struct gotsys_webserver *srv;
+
+			if (writeconf_state !=
+			    WRITECONF_STATE_EXPECT_WEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+
+			srv = calloc(1, sizeof(*srv));
+			if (srv == NULL) {
+				err = got_error_from_errno("calloc");
+				break;
+			}
+			err = gotsys_imsg_recv_web_server(srv, &imsg);
+			if (err) {
+				free(srv);
+				break;
+			}
+			STAILQ_INSERT_TAIL(&gotsysconf.webservers, srv, entry);
+			break;
+		}
+		case GOTSYSD_IMSG_SYSCONF_WEB_ACCESS_RULE: {
+			struct gotsys_webserver *srv;
+			struct gotsys_access_rule *rule;
+
+			if (writeconf_state !=
+			    WRITECONF_STATE_EXPECT_WEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+
+			srv = STAILQ_LAST(&gotsysconf.webservers,
+			    gotsys_webserver, entry);
+			if (srv == NULL) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+
+			err = gotsys_imsg_recv_access_rule(&rule, &imsg,
+			    &gotsysconf.users, &gotsysconf.groups);
+			if (err)
+				break;
+			STAILQ_INSERT_TAIL(&srv->access_rules, rule, entry);
+			break;
+		}
+		case GOTSYSD_IMSG_SYSCONF_WEB_ACCESS_RULES_DONE:
+			if (writeconf_state !=
+			    WRITECONF_STATE_EXPECT_WEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+			break;
+		case GOTSYSD_IMSG_SYSCONF_WEBREPO: {
+			struct gotsys_webserver *srv;
+			struct gotsys_webrepo *webrepo;
+			struct gotsys_repo *repo;
+
+			if (writeconf_state !=
+			    WRITECONF_STATE_EXPECT_WEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+
+			srv = STAILQ_LAST(&gotsysconf.webservers,
+			    gotsys_webserver, entry);
+			if (srv == NULL) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+
+			webrepo = calloc(1, sizeof(*webrepo));
+			if (webrepo == NULL) {
+				err = got_error_from_errno("calloc");
+				break;
+			}
+
+			err = gotsys_imsg_recv_webrepo(webrepo, &imsg);
+			if (err)
+				break;
+
+			TAILQ_FOREACH(repo, &gotsysconf.repos, entry) {
+				if (strcmp(repo->name, webrepo->repo_name) == 0)
+					break;
+			}
+			if (repo == NULL) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "web repository refers to nonexistent "
+				    "repository %s while in state %d\n",
+				    webrepo->repo_name, writeconf_state);
+				break;
+			}
+			STAILQ_INSERT_TAIL(&srv->repos, webrepo, entry);
+			break;
+		}
+		case GOTSYSD_IMSG_SYSCONF_WEBREPO_ACCESS_RULE: {
+			struct gotsys_webserver *srv;
+			struct gotsys_webrepo *webrepo;
+			struct gotsys_access_rule *rule;
+
+			if (writeconf_state !=
+			    WRITECONF_STATE_EXPECT_WEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+
+			srv = STAILQ_LAST(&gotsysconf.webservers,
+			    gotsys_webserver, entry);
+			if (srv == NULL) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+
+			webrepo = STAILQ_LAST(&srv->repos, gotsys_webrepo,
+			    entry);
+			if (webrepo == NULL) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+
+			err = gotsys_imsg_recv_access_rule(&rule, &imsg,
+			    &gotsysconf.users, &gotsysconf.groups);
+			if (err)
+				break;
+			STAILQ_INSERT_TAIL(&webrepo->access_rules, rule, entry);
+			break;
+		}
+		case GOTSYSD_IMSG_SYSCONF_WEBREPO_ACCESS_RULES_DONE:
+			if (writeconf_state !=
+			    WRITECONF_STATE_EXPECT_WEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+			break;
+		case GOTSYSD_IMSG_SYSCONF_WEBREPOS_DONE:
+			if (writeconf_state !=
+			    WRITECONF_STATE_EXPECT_WEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+			break;
+		case GOTSYSD_IMSG_SYSCONF_WEBSITE_PATH: {
+			struct gotsys_webserver *srv;
+			struct gotsys_website *site;
+			struct got_pathlist_entry *new;
+
+			if (writeconf_state !=
+			    WRITECONF_STATE_EXPECT_WEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+
+			srv = STAILQ_LAST(&gotsysconf.webservers,
+			    gotsys_webserver, entry);
+			if (srv == NULL) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+
+			err = gotsys_imsg_recv_website_path(&site, &imsg);
+			if (err)
+				break;
+			err = got_pathlist_insert(&new, &srv->websites,
+			    site->url_path, site);
+			if (err)
+				break;
+			if (new == NULL) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "duplicate web site '%s' in "
+				    "repository '%s'", site->url_path,
+				    repo_cur->name);
+				free(site);
+				break;
+			}
+			site_cur = site;
+			break;
+		}
+		case GOTSYSD_IMSG_SYSCONF_WEBSITE: {
+			struct gotsys_repo *repo;
+
+			if (site_cur == NULL ||
+			    writeconf_state !=
+			    WRITECONF_STATE_EXPECT_WEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+
+			err = gotsys_imsg_recv_website(site_cur, &imsg);
+			if (err)
+				break;
+
+			TAILQ_FOREACH(repo, &gotsysconf.repos, entry) {
+				if (strcmp(repo->name,
+				    site_cur->repo_name) == 0)
+					break;
+			}
+			if (repo == NULL) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "web repository refers to nonexistent "
+				    "repository %s while in state %d\n",
+				    site_cur->repo_name,
+				    writeconf_state);
+				break;
+			}
+			break;
+		}
+		case GOTSYSD_IMSG_SYSCONF_WEBSITE_ACCESS_RULE: {
+			struct gotsys_access_rule_list *rules;
+			struct gotsys_access_rule *rule;
+
+			if (site_cur == NULL ||
+			    writeconf_state !=
+			    WRITECONF_STATE_EXPECT_WEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+			err = gotsys_imsg_recv_access_rule(&rule, &imsg,
+			    &gotsysconf.users, &gotsysconf.groups);
+			if (err)
+				break;
+			rules = &site_cur->access_rules;
+			STAILQ_INSERT_TAIL(rules, rule, entry);
+			break;
+		}
+		case GOTSYSD_IMSG_SYSCONF_WEBSITE_ACCESS_RULES_DONE:
+			if (site_cur == NULL ||
+			    writeconf_state !=
+			    WRITECONF_STATE_EXPECT_WEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+			site_cur = NULL;
+			break;
+		case GOTSYSD_IMSG_SYSCONF_WEBSITES_DONE:
+			if (site_cur != NULL ||
+			    writeconf_state !=
+			    WRITECONF_STATE_EXPECT_WEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+			break;
+		case GOTSYSD_IMSG_SYSCONF_MEDIA_TYPE: {
+			struct gotsys_webserver *srv;
+			struct media_type media;
+
+			if (writeconf_state !=
+			    WRITECONF_STATE_EXPECT_WEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+
+			srv = STAILQ_LAST(&gotsysconf.webservers,
+			    gotsys_webserver, entry);
+			if (srv == NULL) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+
+			err = gotsys_imsg_recv_media_type(&media, &imsg);
+			if (err)
+				break;
+			if (media_add(&srv->mediatypes, &media) == NULL)
+				err = got_error_from_errno("media_add");
+			break;
+		}
+		case GOTSYSD_IMSG_SYSCONF_MEDIA_TYPES_DONE:
+			if (writeconf_state !=
+			    WRITECONF_STATE_EXPECT_WEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+			break;
+		case GOTSYSD_IMSG_SYSCONF_WEB_SERVERS_DONE:
 			writeconf_state = WRITECONF_STATE_WRITE_CONF;
+			if (!STAILQ_EMPTY(&webcfg.servers)) {
+				err = write_gotwebd_conf();
+				if (err)
+					break;
+			}
 			auth_idx = 0;
 			err = prepare_gotd_secrets(&auth_idx);
 			if (err)
@@ -1250,6 +2337,7 @@ main(int argc, char *argv[])
 #endif
 	STAILQ_INIT(&global_repo_access_rules);
 	gotsys_conf_init(&gotsysconf);
+	gotsysd_web_config_init(&webcfg);
 
 	event_init();
 
@@ -1278,6 +2366,10 @@ main(int argc, char *argv[])
 	    GOTD_CONF_PATH, "");
 	if (err)
 		goto done;
+	err = got_opentemp_named_fd(&gotwebd_conf_tmppath, &gotwebd_conf_tmpfd,
+	    GOTWEBD_CONF, "");
+	if (err)
+		goto done;
 #ifndef PROFILE
 	if (pledge("stdio rpath wpath cpath fattr chown unveil", NULL) == -1) {
 		err = got_error_from_errno("pledge");
@@ -1294,6 +2386,11 @@ main(int argc, char *argv[])
 		goto done;
 	}
 
+	if (unveil(gotwebd_conf_tmppath, "rwc") == -1) {
+		err = got_error_from_errno2("unveil rwc", gotwebd_conf_tmppath);
+		goto done;
+	}
+
 	if (unveil(GOTD_CONF_PATH, "rwc") == -1) {
 		err = got_error_from_errno2("unveil rwc", GOTD_CONF_PATH);
 		goto done;
@@ -1304,6 +2401,11 @@ main(int argc, char *argv[])
 		goto done;
 	}
 
+	if (unveil(GOTWEBD_CONF, "rwc") == -1) {
+		err = got_error_from_errno2("unveil rwc", GOTWEBD_CONF);
+		goto done;
+	}
+
 	if (unveil(NULL, NULL) == -1) {
 		err = got_error_from_errno("unveil");
 		goto done;
@@ -1335,6 +2437,9 @@ done:
 	if (gotd_secrets_tmpfd != -1 && close(gotd_secrets_tmpfd) == -1 &&
 	    err == NULL)
 		err = got_error_from_errno("close");
+	if (gotwebd_conf_tmpfd != -1 && close(gotwebd_conf_tmpfd) == -1 &&
+	    err == NULL)
+		err = got_error_from_errno("close");
 	if (err)
 		gotsysd_imsg_send_error(&iev.ibuf, 0, 0, err);
 	if (close(GOTSYSD_FILENO_MSG_PIPE) == -1 && err == NULL) {
blob - 80a2bbd997a897fadedd4e1868f741e63c974531
blob + 331307b081982171b72015d9704d8d018cefed9c
--- gotsysd/parse.y
+++ gotsysd/parse.y
@@ -26,6 +26,7 @@
 #include <sys/types.h>
 #include <sys/queue.h>
 #include <sys/tree.h>
+#include <sys/socket.h>
 #include <sys/stat.h>
 
 #include <ctype.h>
@@ -47,9 +48,12 @@
 #include "got_error.h"
 #include "got_path.h"
 #include "got_object.h"
+#include "got_reference.h"
 
 #include "log.h"
 #include "gotsysd.h"
+#include "media.h"
+#include "gotwebd.h"
 #include "gotsys.h"
 
 #ifndef GOTD_USER
@@ -110,12 +114,16 @@ typedef struct {
 
 %}
 
+%token	AUTHENTICATION CHROOT GOTSYSD_CONTROL GOTWEB_URL_ROOT GOTWEB
+%token	HTDOCS INSECURE PORT PREFORK SERVER NAME SOCKET WWW HINT
 %token	ERROR LISTEN ON USER GOTD DIRECTORY REPOSITORY UID RANGE PERMIT
-%token	DENY RO RW
+%token	DENY RO RW WEB GOTSYSD_LOGIN ENABLE DISABLE HIDE REPOSITORIES
 
 %token	<v.string>	STRING
 %token	<v.number>	NUMBER
 %type	<v.string>	numberstring
+%type	<v.number>	boolean
+%type	<v.string>	listen_addr
 
 %%
 
@@ -152,6 +160,34 @@ numberstring	: STRING
 		}
 		;
 
+boolean		: STRING {
+			if (strcasecmp($1, "1") == 0 ||
+			    strcasecmp($1, "on") == 0)
+				$$ = 1;
+			else if (strcasecmp($1, "0") == 0 ||
+			    strcasecmp($1, "off") == 0)
+				$$ = 0;
+			else {
+				yyerror("invalid boolean value '%s'", $1);
+				free($1);
+				YYERROR;
+			}
+			free($1);
+		}
+		| ON { $$ = 1; }
+		| NUMBER {
+			if ($1 != 0 && $1 != 1) {
+				yyerror("invalid boolean value '%lld'", $1);
+				YYERROR;
+			}
+			$$ = $1;
+		}
+		;
+
+listen_addr	: '*' { $$ = NULL; }
+		| STRING
+		;
+
 main		: LISTEN ON STRING {
 			if (!got_path_is_absolute($3))
 				yyerror("bad unix socket path \"%s\": "
@@ -282,8 +318,397 @@ main		: LISTEN ON STRING {
 			}
 			free($4);
 		}
+		| gotweb
+		| webserver
 		;
 
+gotweb		: GOTWEB '{' optnl webopts2 '}'
+		;
+
+webopts1	: GOTSYSD_CONTROL SOCKET STRING {
+			const struct got_error *err;
+
+			err = gotsys_conf_validate_path($3);
+			if (err) {
+				yyerror("%s", err->msg);
+				free($3);
+				YYERROR;
+			}
+
+			if (strlcpy(gotsysd->web.control_socket, $3,
+			    sizeof(gotsysd->web.control_socket)) >=
+			    sizeof(gotsysd->web.control_socket)) {
+				yyerror("gotwebd control socket path "
+				    "too long: %s", $3);
+				free($3);
+				YYERROR;
+			}
+		}
+		| PREFORK NUMBER {
+			if ($2 <= 0 || $2 > PROC_MAX_INSTANCES) {
+				yyerror("prefork is %s: %lld",
+				    $2 <= 0 ? "too small" : "too large", $2);
+				YYERROR;
+			}
+			gotsysd->web.prefork = $2;
+		}
+		| CHROOT STRING {
+			const struct got_error *err;
+
+			err = gotsys_conf_validate_path($2);
+			if (err) {
+				yyerror("%s", err->msg);
+				free($2);
+				YYERROR;
+			}
+
+			if (strlcpy(gotsysd->web.httpd_chroot, $2,
+			    sizeof(gotsysd->web.httpd_chroot)) >=
+			    sizeof(gotsysd->web.httpd_chroot)) {
+				yyerror("httpd chroot path too long: %s", $2);
+				free($2);
+				YYERROR;
+			}
+		}
+		| HTDOCS STRING {
+			const struct got_error *err;
+
+			err = gotsys_conf_validate_path($2);
+			if (err) {
+				yyerror("%s", err->msg);
+				free($2);
+				YYERROR;
+			}
+
+			if (strlcpy(gotsysd->web.htdocs_path, $2,
+			    sizeof(gotsysd->web.htdocs_path)) >=
+			    sizeof(gotsysd->web.htdocs_path)) {
+				yyerror("htdocs path too long: %s", $2);
+				free($2);
+				YYERROR;
+			}
+		}
+		| GOTSYSD_LOGIN HINT USER STRING {
+			const struct got_error *err;
+
+			err = gotsys_conf_validate_name($4, "user");
+			if (err) {
+				yyerror("%s", err->msg);
+				free($4);
+				YYERROR;
+			}
+
+			if (strlcpy(gotsysd->web.login_hint_user, $4,
+			    sizeof(gotsysd->web.login_hint_user)) >=
+			    sizeof(gotsysd->web.login_hint_user)) {
+				yyerror("login hint user too long: %s", $4);
+				free($4);
+				YYERROR;
+			}
+		}
+		| GOTSYSD_LOGIN HINT PORT NUMBER {
+			int n;
+
+			if ($4 < 1 || $4 > USHRT_MAX) {
+				fatalx("port number invalid: %lld",
+				    (long long)$4);
+			}
+
+			n = snprintf(gotsysd->web.login_hint_port,
+			    sizeof(gotsysd->web.login_hint_port), "%lld",
+			    (long long)$4);
+			if (n < 0) {
+				fatal("snprintf: port number %lld:",
+				    (long long)$4);
+			}
+			if ((size_t)n >= sizeof(gotsysd->web.login_hint_port)) {
+				fatalx("port number too long: %lld",
+				    (long long)$4);
+			}
+		}
+		| USER STRING {
+			const struct got_error *err;
+
+			err = gotsys_conf_validate_name($2, "user");
+			if (err) {
+				yyerror("%s", err->msg);
+				free($2);
+				YYERROR;
+			}
+
+			if (strlcpy(gotsysd->web.gotwebd_user, $2,
+			    sizeof(gotsysd->web.gotwebd_user)) >=
+			    sizeof(gotsysd->web.gotwebd_user)) {
+				yyerror("gotwebd user too long: %s", $2);
+				free($2);
+				YYERROR;
+			}
+		}
+		| WWW USER STRING {
+			const struct got_error *err;
+
+			err = gotsys_conf_validate_name($3, "user");
+			if (err) {
+				yyerror("%s", err->msg);
+				free($3);
+				YYERROR;
+			}
+
+			if (strlcpy(gotsysd->web.www_user, $3,
+			    sizeof(gotsysd->web.www_user)) >=
+			    sizeof(gotsysd->web.www_user)) {
+				yyerror("www user too long: %s", $3);
+				free($3);
+				YYERROR;
+			}
+		}
+		| LISTEN ON listen_addr PORT STRING {
+			const struct got_error *err;
+			struct gotsysd_web_address *addr;
+
+			err = gotsysd_conf_validate_inet_addr($3, $5);
+			if (err) {
+				yyerror("%s", err->msg);
+				free($3);
+				free($5);
+				YYERROR;
+			}
+
+			addr = calloc(1, sizeof(*addr));
+			if (addr == NULL)
+				fatal("calloc");
+
+			addr->family = GOTSYSD_LISTEN_ADDR_INET;
+
+			if (strlcpy(addr->addr.inet.address, $3,
+			    sizeof(addr->addr.inet.address)) >=
+			    sizeof(addr->addr.inet.address)) {
+				yyerror("listen address too long: %s", $3);
+				free($3);
+				free($5);
+				free(addr);
+				YYERROR;
+			}
+			if (strlcpy(addr->addr.inet.port, $5,
+			    sizeof(addr->addr.inet.port)) >=
+			    sizeof(addr->addr.inet.port)) {
+				yyerror("port number too long: %s", $5);
+				free($3);
+				free($5);
+				free(addr);
+				YYERROR;
+			}
+
+			free($3);
+			free($5);
+		}
+		| LISTEN ON listen_addr PORT NUMBER {
+			const struct got_error *err;
+			struct gotsysd_web_address *addr;
+			int n;
+
+			addr = calloc(1, sizeof(*addr));
+			if (addr == NULL)
+				fatal("calloc");
+
+			addr->family = GOTSYSD_LISTEN_ADDR_INET;
+
+			if (strlcpy(addr->addr.inet.address, $3,
+			    sizeof(addr->addr.inet.address)) >=
+			    sizeof(addr->addr.inet.address)) {
+				yyerror("listen address too long: %s", $3);
+				free($3);
+				free(addr);
+				YYERROR;
+			}
+
+			n = snprintf(addr->addr.inet.port,
+			    sizeof(addr->addr.inet.port),
+			    "%lld", (long long)$5);
+			if (n < 0 || (size_t)n >= sizeof(addr->addr.inet.port))
+				fatalx("port number too long: %lld",
+				    (long long)$5);
+
+			err = gotsysd_conf_validate_inet_addr($3,
+			    addr->addr.inet.port);
+			if (err) {
+				yyerror("%s", err->msg);
+				free($3);
+				YYERROR;
+			}
+			free($3);
+		}
+		| LISTEN ON SOCKET STRING {
+			const struct got_error *err;
+			struct gotsysd_web_address *addr;
+
+			err = gotsys_conf_validate_path($4);
+			if (err) {
+				yyerror("%s", err->msg);
+				free($4);
+				YYERROR;
+			}
+
+			addr = calloc(1, sizeof(*addr));
+			if (addr == NULL)
+				fatal("calloc");
+
+			addr->family = GOTSYSD_LISTEN_ADDR_UNIX;
+
+			if (strlcpy(addr->addr.unix_socket_path, $4,
+			    sizeof(addr->addr.unix_socket_path)) >=
+			    sizeof(addr->addr.unix_socket_path)) {
+				yyerror("unix socket path too long: %s", $4);
+				free($4);
+				free(addr);
+				YYERROR;
+			}
+
+			TAILQ_INSERT_TAIL(&gotsysd->web.listen_addrs, addr,
+			    entry);
+		}
+		| DISABLE AUTHENTICATION {
+			if (gotsysd->web.auth_config != 0) {
+				yyerror("ambiguous global web authentication "
+				    "setting");
+				YYERROR;
+			}
+			gotsysd->web.auth_config = GOTSYSD_WEB_AUTH_DISABLED;
+		}
+		| ENABLE AUTHENTICATION {
+			if (gotsysd->web.auth_config != 0) {
+				yyerror("ambiguous global web authentication "
+				    "setting");
+				YYERROR;
+			}
+			gotsysd->web.auth_config = GOTSYSD_WEB_AUTH_SECURE;
+		}
+		| ENABLE AUTHENTICATION INSECURE {
+			if (gotsysd->web.auth_config != 0) {
+				yyerror("ambiguous global web authentication "
+				    "setting");
+				YYERROR;
+			}
+			gotsysd->web.auth_config = GOTSYSD_WEB_AUTH_INSECURE;
+		}
+		;
+
+webopts2	: webopts2 webopts1 nl
+		| webopts1 optnl
+		;
+
+webserver	: WEB SERVER STRING {
+			const struct got_error *err;
+
+			err = conf_new_web_server($3);
+			if (err) {
+				yyerror("%s", err->msg);
+				free($3);
+				YYERROR;
+			}
+			free($3);
+		} '{' optnl webserveropts2 '}' {
+		}
+		| WEB SERVER STRING {
+			const struct got_error *err;
+
+			err = conf_new_web_server($3);
+			if (err) {
+				yyerror("%s", err->msg);
+				free($3);
+				YYERROR;
+			}
+			free($3);
+		}
+		;
+	
+webserveropts1	: GOTWEB_URL_ROOT STRING {
+			struct gotsysd_web_server *srv;
+
+			srv = STAILQ_LAST(&gotsysd->web.servers,
+			    gotsysd_web_server, entry);
+
+			if (strlcpy(srv->gotweb_url_root, $2,
+			    sizeof(srv->gotweb_url_root)) >=
+			    sizeof(srv->gotweb_url_root)) {
+				yyerror("gotweb URL root too long: %s", $2);
+				free($2);
+				YYERROR;
+			}
+		}
+		| HTDOCS STRING {
+			struct gotsysd_web_server *srv;
+
+			srv = STAILQ_LAST(&gotsysd->web.servers,
+			    gotsysd_web_server, entry);
+
+			if (strlcpy(srv->htdocs_path, $2,
+			    sizeof(srv->htdocs_path)) >=
+			    sizeof(srv->htdocs_path)) {
+				yyerror("htdocs path too long: %s", $2);
+				free($2);
+				YYERROR;
+			}
+		}
+		| DISABLE AUTHENTICATION {
+			struct gotsysd_web_server *srv;
+
+			srv = STAILQ_LAST(&gotsysd->web.servers,
+			    gotsysd_web_server, entry);
+
+			if (srv->auth_config != 0) {
+				yyerror("web server %s: ambiguous "
+				    "authentication setting", srv->server_name);
+				YYERROR;
+			}
+			srv->auth_config = GOTSYSD_WEB_AUTH_DISABLED;
+		}
+		| ENABLE AUTHENTICATION {
+			struct gotsysd_web_server *srv;
+
+			srv = STAILQ_LAST(&gotsysd->web.servers,
+			    gotsysd_web_server, entry);
+
+			if (srv->auth_config != 0) {
+				yyerror("web server %s: ambiguous "
+				    "authentication setting", srv->server_name);
+				YYERROR;
+			}
+			srv->auth_config = GOTSYSD_WEB_AUTH_SECURE;
+		}
+		| ENABLE AUTHENTICATION INSECURE {
+			struct gotsysd_web_server *srv;
+
+			srv = STAILQ_LAST(&gotsysd->web.servers,
+			    gotsysd_web_server, entry);
+
+			if (srv->auth_config != 0) {
+				yyerror("web server %s: ambiguous "
+				    "authentication setting", srv->server_name);
+				YYERROR;
+			}
+			srv->auth_config = GOTSYSD_WEB_AUTH_INSECURE;
+		}
+		| HIDE REPOSITORIES boolean {
+			struct gotsysd_web_server *srv;
+
+			srv = STAILQ_LAST(&gotsysd->web.servers,
+			    gotsysd_web_server, entry);
+
+			srv->hide_repositories = $3;
+		}
+		;
+
+webserveropts2	: webserveropts2 webserveropts1 nl
+		| webserveropts1 optnl
+		;
+
+optnl		: '\n' optnl		/* zero or more newlines */
+		| /* empty */
+		;
+
+nl		: '\n' optnl
+		;
 %%
 
 struct keywords {
@@ -318,18 +743,38 @@ lookup(char *s)
 {
 	/* This has to be sorted always. */
 	static const struct keywords keywords[] = {
+		{ "authentication",		AUTHENTICATION },
+		{ "chroot",			CHROOT },
+		{ "control",			GOTSYSD_CONTROL },
 		{ "deny",			DENY },
 		{ "directory",			DIRECTORY },
+		{ "disable",			DISABLE },
+		{ "enable",			ENABLE },
 		{ "gotd",			GOTD },
+		{ "gotweb",			GOTWEB },
+		{ "gotweb_url_root",		GOTWEB_URL_ROOT },
+		{ "hide",			HIDE },
+		{ "hint",			HINT },
+		{ "htdocs",			HTDOCS },
+		{ "insecure",			INSECURE },
 		{ "listen",			LISTEN },
+		{ "login",			GOTSYSD_LOGIN },
+		{ "name",			NAME },
 		{ "on",				ON },
 		{ "permit",			PERMIT },
+		{ "port",			PORT },
+		{ "prefork",			PREFORK },
 		{ "range",			RANGE },
+		{ "repositories",		REPOSITORIES },
 		{ "repository",			REPOSITORY },
 		{ "ro",				RO },
 		{ "rw",				RW },
+		{ "server",			SERVER },
+		{ "socket",			SOCKET },
 		{ "uid",			UID },
 		{ "user",			USER },
+		{ "web",			WEB },
+		{ "www",			WWW },
 	};
 	const struct keywords *p;
 
@@ -651,6 +1096,8 @@ gotsysd_parse_config(const char *filename, enum gotsys
 	STAILQ_INIT(gotsysd->global_repo_access_rules);
 	global_repo_access_rules = NULL;
 
+	gotsysd_web_config_init(&gotsysd->web);
+
 	/* Apply default values. */
 	if (strlcpy(gotsysd->unix_socket_path, GOTSYSD_UNIX_SOCKET,
 	    sizeof(gotsysd->unix_socket_path)) >=
@@ -703,6 +1150,13 @@ gotsysd_parse_config(const char *filename, enum gotsys
 			return (-1);
 	}
 
+	if (strlcpy(gotsysd->web.repos_path, gotsysd->repos_path,
+	    sizeof(gotsysd->web.repos_path)) >=
+	    sizeof(gotsysd->web.repos_path)) {
+		fprintf(stderr, "%s: repos path too long", __func__);
+		return -1;
+	}
+
 	if (proc_id == GOTSYSD_PROC_AUTH &&
 	    STAILQ_EMPTY(&gotsysd->access_rules)) {
 		char *identifier = strdup("0"); /* root */
@@ -836,3 +1290,30 @@ conf_new_repo_access_rule(enum gotsys_access access, i
 	STAILQ_INSERT_TAIL(gotsysd->global_repo_access_rules, rule, entry);
 	return NULL;
 }
+
+static const struct got_error *
+conf_new_web_server(const char *name)
+{
+	struct gotsysd_web_server *srv;
+
+	STAILQ_FOREACH(srv, &gotsysd->web.servers, entry) {
+		if (strcmp(srv->server_name, name) == 0) {
+			return got_error_fmt(GOT_ERR_PARSE_CONFIG,
+			    "duplicate web server: %s", name);
+		}
+	}
+
+	srv = calloc(1, sizeof(*srv));
+	if (srv == NULL)
+		fatal("calloc");
+
+	if (strlcpy(srv->server_name, name, sizeof(srv->server_name)) >=
+	    sizeof(srv->server_name)) {
+		return got_error_fmt(GOT_ERR_NO_SPACE,
+		    "server name too long: %s", name);
+	}
+
+	srv->hide_repositories = -1;
+	STAILQ_INSERT_TAIL(&gotsysd->web.servers, srv, entry);
+	return NULL;
+}
blob - 554badbe0a0be65f91f07315a9eaa4cf0ddb2391
blob + 9b633d4164e7b17e3e963e65cd4096713dee0be4
--- gotsysd/sysconf.c
+++ gotsysd/sysconf.c
@@ -34,13 +34,14 @@
 #include "got_error.h"
 #include "got_path.h"
 #include "got_object.h"
+#include "got_reference.h"
 
 #include "gotsysd.h"
+#include "media.h"
+#include "gotwebd.h"
 #include "gotsys.h"
 #include "log.h"
-#include "sysconf.h"
 
-
 #include "sysconf.h"
 
 enum gotsysd_sysconf_state {
@@ -49,14 +50,17 @@ enum gotsysd_sysconf_state {
 	SYSCONF_STATE_EXPECT_AUTHORIZED_KEYS,
 	SYSCONF_STATE_EXPECT_GROUPS,
 	SYSCONF_STATE_EXPECT_REPOS,
+	SYSCONF_STATE_EXPECT_MEDIA_TYPES,
+	SYSCONF_STATE_EXPECT_WEB_SERVERS,
 	SYSCONF_STATE_ADD_USERS,
 	SYSCONF_STATE_CREATE_HOMEDIRS,
 	SYSCONF_STATE_INSTALL_AUTHORIZED_KEYS,
 	SYSCONF_STATE_REMOVE_AUTHORIZED_KEYS,
 	SYSCONF_STATE_ADD_GROUPS,
 	SYSCONF_STATE_CREATE_REPOS,
-	SYSCONF_STATE_CREATE_GOTD_CONF,
+	SYSCONF_STATE_CREATE_CONF_FILES,
 	SYSCONF_STATE_RESTART_GOTD,
+	SYSCONF_STATE_RESTART_GOTWEBD,
 	SYSCONF_STATE_CONFIGURE_SSHD,
 	SYSCONF_STATE_DONE,
 };
@@ -84,6 +88,8 @@ static struct gotsysd_sysconf {
 	size_t *num_notif_refs_cur;
 	size_t num_notif_refs_needed;
 	size_t num_notif_refs_received;
+	struct gotsys_website *site_cur;
+	struct gotsysd_web_config *web;
 } gotsysd_sysconf;
 
 static struct gotsys_conf gotsysconf;
@@ -358,6 +364,7 @@ sysconf_dispatch_libexec(int fd, short event, void *ar
 			log_debug("received repository %s", repo->name);
 			TAILQ_INSERT_TAIL(&gotsysconf.repos, repo, entry);
 			gotsysd_sysconf.repo_cur = repo;
+			gotsysd_sysconf.site_cur = NULL;
 			break;
 		}
 		case GOTSYSD_IMSG_SYSCONF_ACCESS_RULE: {
@@ -639,6 +646,19 @@ sysconf_dispatch_libexec(int fd, short event, void *ar
 			break;
 		}
 		case GOTSYSD_IMSG_SYSCONF_NOTIFICATION_TARGETS_DONE:
+			if (gotsysd_sysconf.repo_cur == NULL ||
+			    gotsysd_sysconf.num_notif_refs_needed != 0 ||
+			    gotsysd_sysconf.state !=
+			    SYSCONF_STATE_EXPECT_REPOS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+				break;
+			}
+
+			gotsysd_sysconf.num_notif_refs_needed = 0;
+			gotsysd_sysconf.notif_refs_cur = NULL;
 			break;
 		case GOTSYSD_IMSG_SYSCONF_REPOS_DONE:
 			if (gotsysd_sysconf.state !=
@@ -651,8 +671,409 @@ sysconf_dispatch_libexec(int fd, short event, void *ar
 			}
 			log_debug("done receiving repositories");
 			gotsysd_sysconf.repo_cur = NULL;
+			gotsysd_sysconf.users_cur = NULL;
+			gotsysd_sysconf.state =
+			    SYSCONF_STATE_EXPECT_MEDIA_TYPES;
+			break;
+		case GOTSYSD_IMSG_SYSCONF_GLOBAL_MEDIA_TYPE: {
+			struct media_type media;
+
+			if (gotsysd_sysconf.state !=
+			    SYSCONF_STATE_EXPECT_MEDIA_TYPES) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+				break;
+			}
+			err = gotsys_imsg_recv_media_type(&media, &imsg);
+			if (err)
+				break;
+			if (media_add(&gotsysconf.mediatypes, &media) == NULL)
+				err = got_error_from_errno("media_add");
+			break;
+		}
+		case GOTSYSD_IMSG_SYSCONF_GLOBAL_MEDIA_TYPES_DONE:
+			if (gotsysd_sysconf.state !=
+			    SYSCONF_STATE_EXPECT_MEDIA_TYPES) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+				break;
+			}
+			gotsysd_sysconf.state =
+			    SYSCONF_STATE_EXPECT_WEB_SERVERS;
+			break;
+		case GOTSYSD_IMSG_SYSCONF_WEB_SERVER: {
+			struct gotsys_webserver *srv;
+
+			if (gotsysd_sysconf.state !=
+			    SYSCONF_STATE_EXPECT_WEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+				break;
+			}
+
+			srv = calloc(1, sizeof(*srv));
+			if (srv == NULL) {
+				err = got_error_from_errno("calloc");
+				break;
+			}
+			err = gotsys_imsg_recv_web_server(srv, &imsg);
+			if (err) {
+				free(srv);
+				break;
+			}
+			log_debug("received web server");
+			STAILQ_INSERT_TAIL(&gotsysconf.webservers, srv, entry);
+			break;
+		}
+		case GOTSYSD_IMSG_SYSCONF_WEB_ACCESS_RULE: {
+			struct gotsys_webserver *srv;
+			struct gotsys_access_rule *rule;
+
+			if (gotsysd_sysconf.state !=
+			    SYSCONF_STATE_EXPECT_WEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+				break;
+			}
+
+			srv = STAILQ_LAST(&gotsysconf.webservers,
+			    gotsys_webserver, entry);
+			if (srv == NULL) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+				break;
+			}
+
+			log_debug("receiving web access rule");
+			err = gotsys_imsg_recv_access_rule(&rule, &imsg,
+			    &gotsysconf.users, &gotsysconf.groups);
+			if (err)
+				break;
+			if (!gotsysd_sysconf.have_anonymous_user &&
+			    strcmp(rule->identifier, "anonymous") == 0) {
+				err = add_anonymous_user(&gotsysconf.users);
+				if (err)
+					break;
+				gotsysd_sysconf.have_anonymous_user = 1;
+			}
+
+			STAILQ_INSERT_TAIL(&srv->access_rules, rule, entry);
+			break;
+		}
+		case GOTSYSD_IMSG_SYSCONF_WEB_ACCESS_RULES_DONE:
+			if (gotsysd_sysconf.state !=
+			    SYSCONF_STATE_EXPECT_WEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+				break;
+			}
+			log_debug("done receiving web access rules");
+			break;
+		case GOTSYSD_IMSG_SYSCONF_WEBREPO: {
+			struct gotsys_webserver *srv;
+			struct gotsys_webrepo *webrepo;
+			struct gotsys_repo *repo;
+
+			if (gotsysd_sysconf.state !=
+			    SYSCONF_STATE_EXPECT_WEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+				break;
+			}
+
+			srv = STAILQ_LAST(&gotsysconf.webservers,
+			    gotsys_webserver, entry);
+			if (srv == NULL) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+				break;
+			}
+
+			webrepo = calloc(1, sizeof(*webrepo));
+			if (webrepo == NULL) {
+				err = got_error_from_errno("calloc");
+				break;
+			}
+
+			err = gotsys_imsg_recv_webrepo(webrepo, &imsg);
+			if (err)
+				break;
+
+			TAILQ_FOREACH(repo, &gotsysconf.repos, entry) {
+				if (strcmp(repo->name, webrepo->repo_name) == 0)
+					break;
+			}
+			if (repo == NULL) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "web repository refers to nonexistent "
+				    "repository %s while in state %d\n",
+				    webrepo->repo_name, gotsysd_sysconf.state);
+				break;
+			}
+			log_debug("received web server %s repo %s",
+			    srv->server_name, webrepo->repo_name);
+			STAILQ_INSERT_TAIL(&srv->repos, webrepo, entry);
+			break;
+		}
+		case GOTSYSD_IMSG_SYSCONF_WEBREPO_ACCESS_RULE: {
+			struct gotsys_webserver *srv;
+			struct gotsys_webrepo *webrepo;
+			struct gotsys_access_rule *rule;
+
+			if (gotsysd_sysconf.state !=
+			    SYSCONF_STATE_EXPECT_WEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+				break;
+			}
+
+			srv = STAILQ_LAST(&gotsysconf.webservers,
+			    gotsys_webserver, entry);
+			if (srv == NULL) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+				break;
+			}
+
+			webrepo = STAILQ_LAST(&srv->repos, gotsys_webrepo,
+			    entry);
+			if (webrepo == NULL) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+				break;
+			}
+			log_debug("receiving webrepo access rule");
+
+			err = gotsys_imsg_recv_access_rule(&rule, &imsg,
+			    &gotsysconf.users, &gotsysconf.groups);
+			if (err)
+				break;
+			if (!gotsysd_sysconf.have_anonymous_user &&
+			    strcmp(rule->identifier, "anonymous") == 0) {
+				err = add_anonymous_user(&gotsysconf.users);
+				if (err)
+					break;
+				gotsysd_sysconf.have_anonymous_user = 1;
+			}
+			STAILQ_INSERT_TAIL(&webrepo->access_rules, rule, entry);
+			break;
+		}
+		case GOTSYSD_IMSG_SYSCONF_WEBREPO_ACCESS_RULES_DONE:
+			if (gotsysd_sysconf.state !=
+			    SYSCONF_STATE_EXPECT_WEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+				break;
+			}
+			log_debug("done receiving webrepo access rules");
+			break;
+		case GOTSYSD_IMSG_SYSCONF_WEBREPOS_DONE:
+			if (gotsysd_sysconf.state !=
+			    SYSCONF_STATE_EXPECT_WEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+				break;
+			}
+			log_debug("done receiving webrepos");
+			break;
+		case GOTSYSD_IMSG_SYSCONF_WEBSITE_PATH: {
+			struct gotsys_webserver *srv;
+			struct gotsys_website *site;
+			struct got_pathlist_entry *new;
+
+			if (gotsysd_sysconf.state !=
+			    SYSCONF_STATE_EXPECT_WEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+				break;
+			}
+
+			srv = STAILQ_LAST(&gotsysconf.webservers,
+			    gotsys_webserver, entry);
+			if (srv == NULL) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+				break;
+			}
+
+			err = gotsys_imsg_recv_website_path(&site, &imsg);
+			if (err)
+				break;
+			err = got_pathlist_insert(&new, &srv->websites,
+			    site->url_path, site);
+			if (err)
+				break;
+			if (new == NULL) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "duplicate web site '%s' in "
+				    "repository '%s'", site->url_path,
+				    gotsysd_sysconf.repo_cur->name);
+				free(site);
+				break;
+			}
+			gotsysd_sysconf.site_cur = site;
+			break;
+		}
+		case GOTSYSD_IMSG_SYSCONF_WEBSITE: {
+			struct gotsys_repo *repo;
+
+			if (gotsysd_sysconf.site_cur == NULL ||
+			    gotsysd_sysconf.state !=
+			    SYSCONF_STATE_EXPECT_WEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+				break;
+			}
+
+			err = gotsys_imsg_recv_website(
+			    gotsysd_sysconf.site_cur, &imsg);
+			if (err)
+				break;
+
+			TAILQ_FOREACH(repo, &gotsysconf.repos, entry) {
+				if (strcmp(repo->name,
+				    gotsysd_sysconf.site_cur->repo_name) == 0)
+					break;
+			}
+			if (repo == NULL) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "web repository refers to nonexistent "
+				    "repository %s while in state %d\n",
+				    gotsysd_sysconf.site_cur->repo_name,
+				    gotsysd_sysconf.state);
+				break;
+			}
+			break;
+		}
+		case GOTSYSD_IMSG_SYSCONF_WEBSITE_ACCESS_RULE: {
+			struct gotsys_access_rule_list *rules;
+			struct gotsys_access_rule *rule;
+
+			if (gotsysd_sysconf.site_cur == NULL ||
+			    gotsysd_sysconf.state !=
+			    SYSCONF_STATE_EXPECT_WEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+				break;
+			}
+			log_debug("receiving website access rule");
+			err = gotsys_imsg_recv_access_rule(&rule, &imsg,
+			    &gotsysconf.users, &gotsysconf.groups);
+			if (err)
+				break;
+			if (!gotsysd_sysconf.have_anonymous_user &&
+			    strcmp(rule->identifier, "anonymous") == 0) {
+				err = add_anonymous_user(&gotsysconf.users);
+				if (err)
+					break;
+				gotsysd_sysconf.have_anonymous_user = 1;
+			}
+			rules = &gotsysd_sysconf.site_cur->access_rules;
+			STAILQ_INSERT_TAIL(rules, rule, entry);
+			break;
+		}
+		case GOTSYSD_IMSG_SYSCONF_WEBSITE_ACCESS_RULES_DONE:
+			if (gotsysd_sysconf.site_cur == NULL ||
+			    gotsysd_sysconf.state !=
+			    SYSCONF_STATE_EXPECT_WEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+				break;
+			}
+			log_debug("done receiving website access rules");
+			gotsysd_sysconf.site_cur = NULL;
+			break;
+		case GOTSYSD_IMSG_SYSCONF_WEBSITES_DONE:
+			if (gotsysd_sysconf.site_cur != NULL ||
+			    gotsysd_sysconf.state !=
+			    SYSCONF_STATE_EXPECT_WEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+				break;
+			}
+			log_debug("done receiving websites");
+			break;
+		case GOTSYSD_IMSG_SYSCONF_MEDIA_TYPE: {
+			struct gotsys_webserver *srv;
+			struct media_type media;
+
+			if (gotsysd_sysconf.state !=
+			    SYSCONF_STATE_EXPECT_WEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+				break;
+			}
+
+			srv = STAILQ_LAST(&gotsysconf.webservers,
+			    gotsys_webserver, entry);
+			if (srv == NULL) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+				break;
+			}
+
+			err = gotsys_imsg_recv_media_type(&media, &imsg);
+			if (err)
+				break;
+			if (media_add(&srv->mediatypes, &media) == NULL)
+				err = got_error_from_errno("media_add");
+			break;
+		}
+		case GOTSYSD_IMSG_SYSCONF_MEDIA_TYPES_DONE:
+			if (gotsysd_sysconf.state !=
+			    SYSCONF_STATE_EXPECT_WEB_SERVERS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+				break;
+			}
+			break;
+		case GOTSYSD_IMSG_SYSCONF_WEB_SERVERS_DONE:
+			log_debug("done receiving web servers");
 			gotsysd_sysconf.state = SYSCONF_STATE_ADD_USERS;
-			gotsysd_sysconf.users_cur = NULL;
 			err = start_useradd();
 			break;
 		default:
@@ -660,7 +1081,6 @@ sysconf_dispatch_libexec(int fd, short event, void *ar
 			break;
 		}
 
-
 		if (err)
 			fatalx("imsg %d: %s", imsg.hdr.type, err->msg);
 
@@ -871,6 +1291,67 @@ start_write_conf(struct gotsysd_imsgev *iev)
 }
 
 static const struct got_error *
+send_webaddr(struct gotsysd_imsgev *iev, struct gotsysd_web_address *addr)
+{
+	if (gotsysd_imsg_compose_event(iev,
+	    GOTSYSD_IMSG_SYSCONF_GOTWEB_ADDR, GOTSYSD_PROC_SYSCONF,
+	    -1, addr, sizeof(*addr)) == -1)
+		return got_error_from_errno("imsg compose SYSCONF_GOTWEB_ADDR");
+
+	return NULL;
+}
+
+static const struct got_error *
+send_webserver(struct gotsysd_imsgev *iev, struct gotsysd_web_server *srv)
+{
+	if (gotsysd_imsg_compose_event(iev,
+	    GOTSYSD_IMSG_SYSCONF_GOTWEB_SERVER, GOTSYSD_PROC_SYSCONF,
+	    -1, srv, sizeof(*srv)) == -1)
+		return got_error_from_errno("imsg compose SYSCONF_GOTWEB_ADDR");
+
+	return NULL;
+}
+
+static const struct got_error *
+send_web_config(struct gotsysd_imsgev *iev)
+{
+	const struct got_error *err;
+	struct gotsysd_web_address *addr;
+	struct gotsysd_web_server *srv;
+	
+	if (gotsysd_imsg_compose_event(iev,
+	    GOTSYSD_IMSG_SYSCONF_GOTWEB_CFG, 0, -1,
+	    gotsysd_sysconf.web, sizeof(*gotsysd_sysconf.web)) == -1)
+		return got_error_from_errno("gotsysd_imsg_compose_event");
+
+	TAILQ_FOREACH(addr, &gotsysd_sysconf.web->listen_addrs, entry) {
+		err = send_webaddr(iev, addr);
+		if (err)
+			return err;
+	}
+
+	if (gotsysd_imsg_compose_event(iev,
+	    GOTSYSD_IMSG_SYSCONF_GOTWEB_ADDRS_DONE, 0, -1, NULL, 0) == -1)
+		return got_error_from_errno("gotsysd_imsg_compose_event");
+
+	STAILQ_FOREACH(srv, &gotsysd_sysconf.web->servers, entry) {
+		err = send_webserver(iev, srv);
+		if (err)
+			return err;
+	}
+
+	if (gotsysd_imsg_compose_event(iev,
+	    GOTSYSD_IMSG_SYSCONF_GOTWEB_SERVERS_DONE, 0, -1, NULL, 0) == -1)
+		return got_error_from_errno("gotsysd_imsg_compose_event");
+
+	if (gotsysd_imsg_compose_event(iev,
+	    GOTSYSD_IMSG_SYSCONF_GOTWEB_CFG_DONE, 0, -1, NULL, 0) == -1)
+		return got_error_from_errno("gotsysd_imsg_compose_event");
+
+	return NULL;
+}
+
+static const struct got_error *
 send_gotsysconf(struct gotsysd_imsgev *iev)
 {
 	const struct got_error *err;
@@ -905,7 +1386,17 @@ send_gotsysconf(struct gotsysd_imsgev *iev)
 	err = gotsys_imsg_send_repositories(iev, &gotsysconf.repos);
 	if (err)
 		return err;
+
+	err = gotsys_imsg_send_mediatypes(iev, &gotsysconf.mediatypes,
+	    GOTSYSD_IMSG_SYSCONF_GLOBAL_MEDIA_TYPE,
+	    GOTSYSD_IMSG_SYSCONF_GLOBAL_MEDIA_TYPES_DONE);
+	if (err)
+		return err;
 	
+	err = gotsys_imsg_send_webservers(iev, &gotsysconf.webservers);
+	if (err)
+		return err;
+
 	return NULL;
 }
 
@@ -921,6 +1412,17 @@ start_apply_conf(struct gotsysd_imsgev *iev)
 }
 
 static const struct got_error *
+start_apply_webconf(struct gotsysd_imsgev *iev)
+{
+	if (gotsysd_imsg_compose_event(iev,
+	    GOTSYSD_IMSG_START_PROG_APPLY_WEBCONF, GOTSYSD_PROC_SYSCONF,
+	    -1, NULL, 0) == -1)
+		return got_error_from_errno("imsg compose START_APPLY_CONF");
+
+	return NULL;
+}
+
+static const struct got_error *
 start_sshdconf(struct gotsysd_imsgev *iev)
 {
 	if (gotsysd_imsg_compose_event(iev,
@@ -1139,23 +1641,26 @@ sysconf_dispatch_priv(int fd, short event, void *arg)
 				    gotsysd_sysconf.state);
 				break;
 			}
-			gotsysd_sysconf.state = SYSCONF_STATE_CREATE_GOTD_CONF;
+			gotsysd_sysconf.state = SYSCONF_STATE_CREATE_CONF_FILES;
 			err = start_write_conf(iev);
 			break;
 		case GOTSYSD_IMSG_SYSCONF_WRITE_CONF_READY:
 			if (gotsysd_sysconf.state !=
-			    SYSCONF_STATE_CREATE_GOTD_CONF) {
+			    SYSCONF_STATE_CREATE_CONF_FILES) {
 				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
 				    "received unexpected imsg %d while in "
 				    "state %d\n", imsg.hdr.type,
 				    gotsysd_sysconf.state);
 				break;
 			}
+			err = send_web_config(iev);
+			if (err)
+				break;
 			err = send_gotsysconf(iev);
 			break;
 		case GOTSYSD_IMSG_SYSCONF_WRITE_CONF_DONE:
 			if (gotsysd_sysconf.state !=
-			    SYSCONF_STATE_CREATE_GOTD_CONF) {
+			    SYSCONF_STATE_CREATE_CONF_FILES) {
 				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
 				    "received unexpected imsg %d while in "
 				    "state %d\n", imsg.hdr.type,
@@ -1182,6 +1687,35 @@ sysconf_dispatch_priv(int fd, short event, void *arg)
 				    "state %d\n", imsg.hdr.type,
 				    gotsysd_sysconf.state);
 			}
+			if (!STAILQ_EMPTY(&gotsysd_sysconf.web->servers)) {
+				gotsysd_sysconf.state =
+				    SYSCONF_STATE_RESTART_GOTWEBD;
+				err = start_apply_webconf(iev);
+			} else {
+				gotsysd_sysconf.state =
+				    SYSCONF_STATE_CONFIGURE_SSHD;
+				err = start_sshdconf(iev);
+			}
+			break;
+		case GOTSYSD_IMSG_SYSCONF_APPLY_WEBCONF_READY:
+			if (gotsysd_sysconf.state !=
+			    SYSCONF_STATE_RESTART_GOTWEBD) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+			}
+			log_debug("webconf ready received");
+			break;
+		case GOTSYSD_IMSG_SYSCONF_APPLY_WEBCONF_DONE:
+			if (gotsysd_sysconf.state !=
+			    SYSCONF_STATE_RESTART_GOTWEBD) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+			}
+			log_debug("webconf done received");
 			gotsysd_sysconf.state = SYSCONF_STATE_CONFIGURE_SSHD;
 			err = start_sshdconf(iev);
 			break;
@@ -1393,7 +1927,8 @@ sysconf_dispatch(int fd, short event, void *arg)
 
 void
 sysconf_main(const char *title, uid_t uid_start, uid_t uid_end,
-    struct gotsys_access_rule_list *global_repo_access_rules)
+    struct gotsys_access_rule_list *global_repo_access_rules,
+    struct gotsysd_web_config *web)
 {
 	struct event evsigint, evsigterm, evsighup, evsigusr1;
 	struct gotsysd_imsgev *iev = &gotsysd_sysconf.parent_iev;
@@ -1409,6 +1944,7 @@ sysconf_main(const char *title, uid_t uid_start, uid_t
 	gotsysd_sysconf.uid_start = uid_start;
 	gotsysd_sysconf.uid_end = uid_end;
 	gotsysd_sysconf.global_repo_access_rules = global_repo_access_rules;
+	gotsysd_sysconf.web = web;
 
 	signal_set(&evsigint, SIGINT, sysconf_sighdlr, NULL);
 	signal_set(&evsigterm, SIGTERM, sysconf_sighdlr, NULL);
blob - 8391a0256347fa9315b8a27c887e89719ade65f5
blob + 6ae62f270fc72e0b9c2013126a52b68c2fef2420
--- gotsysd/sysconf.h
+++ gotsysd/sysconf.h
@@ -14,4 +14,5 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-void sysconf_main(const char *, uid_t, uid_t, struct gotsys_access_rule_list *);
+void sysconf_main(const char *, uid_t, uid_t, struct gotsys_access_rule_list *,
+    struct gotsysd_web_config *);
blob - a95325914b29d50c3c459fdeb4fb86e2d076ac6d
blob + 1868cf47328cce63d6b5b9fec9cd8152af55ed29
--- include/got_error.h
+++ include/got_error.h
@@ -200,6 +200,7 @@
 #define GOT_ERR_LOGIN_FAILED	192
 #define GOT_ERR_UNKNOWN_COMMAND	193
 #define GOT_ERR_NOT_FOUND	194
+#define GOT_ERR_MEDIA_TYPE	195
 
 struct got_error {
         int code;
blob - 577dd1bf907e50ca8e8df206486ea926317f32ab
blob + 6d25b5840fd9283ff9543c6405e6dca52a0825f8
--- lib/error.c
+++ lib/error.c
@@ -251,6 +251,7 @@ static const struct got_error got_errors[] = {
 	{ GOT_ERR_LOGIN_FAILED, "login failed" },
 	{ GOT_ERR_UNKNOWN_COMMAND, "command not found" },
 	{ GOT_ERR_NOT_FOUND, "not found" },
+	{ GOT_ERR_MEDIA_TYPE,	"malformed media type" },
 };
 
 static struct got_custom_error {
blob - 05fad5e984044a6e291365ac1358ec288a6da1c0
blob + b49d477662eaa23cbb49af5068e20d15b06917dd
--- lib/gotsys_conf.c
+++ lib/gotsys_conf.c
@@ -19,16 +19,23 @@
 #include <sys/queue.h>
 
 #include <ctype.h>
+#include <event.h>
 #include <imsg.h>
 #include <limits.h>
 #include <pwd.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <sha1.h>
+#include <sha2.h>
 
 #include "got_error.h"
 #include "got_path.h"
+#include "got_object.h"
+#include "got_reference.h"
 
+#include "media.h"
+#include "gotwebd.h"
 #include "gotsys.h"
 
 #ifndef nitems
@@ -43,8 +50,158 @@ gotsys_conf_init(struct gotsys_conf *gotsysconf)
 	STAILQ_INIT(&gotsysconf->users);
 	STAILQ_INIT(&gotsysconf->groups);
 	TAILQ_INIT(&gotsysconf->repos);
+	STAILQ_INIT(&gotsysconf->webservers);
+	RB_INIT(&gotsysconf->mediatypes);
 }
 
+const struct got_error *
+gotsys_conf_new_webserver(struct gotsys_webserver **new, const char *name)
+{
+	const struct got_error *err;
+	struct gotsys_webserver *srv;
+
+	*new = NULL;
+
+	err = gotsys_conf_validate_hostname(name);
+	if (err)
+		return err;
+
+	srv = calloc(1, sizeof(*srv));
+	if (srv == NULL)
+		return got_error_from_errno("calloc");
+
+	if (strlcpy(srv->server_name, name,
+	    sizeof(srv->server_name)) >= sizeof(srv->server_name)) {
+		err = got_error_fmt(GOT_ERR_PARSE_CONFIG,
+		    "web server name '%s' too long, exceeds %zd bytes",
+		    name, sizeof(srv->server_name) - 1);
+		free(srv);
+		return err;
+	}
+
+	RB_INIT(&srv->mediatypes);
+	err = gotsys_conf_init_media_types(&srv->mediatypes);
+	if (err) {
+		free(srv);
+		return err;
+	}
+
+	STAILQ_INIT(&srv->access_rules);
+	STAILQ_INIT(&srv->repos);
+	RB_INIT(&srv->websites);
+	srv->hide_repositories = -1;
+
+	*new = srv;
+	return NULL;
+}
+
+const struct got_error *
+gotsys_conf_new_webrepo(struct gotsys_webrepo **new, const char *name)
+{
+	const struct got_error *err;
+	struct gotsys_webrepo *repo;
+
+	*new = NULL;
+
+	err = gotsys_conf_validate_repo_name(name);
+	if (err)
+		return err;
+
+	repo = calloc(1, sizeof(*repo));
+	if (repo == NULL)
+		return got_error_from_errno("calloc");
+
+	if (strlcpy(repo->repo_name, name,
+	    sizeof(repo->repo_name)) >= sizeof(repo->repo_name)) {
+		err = got_error_fmt(GOT_ERR_PARSE_CONFIG,
+		    "repository name '%s' too long, exceeds %zd bytes",
+		    name, sizeof(repo->repo_name) - 1);
+		free(repo);
+		return err;
+	}
+
+	STAILQ_INIT(&repo->access_rules);
+	repo->hidden = -1;
+
+	*new = repo;
+	return NULL;
+}
+
+const struct got_error *
+gotsys_conf_init_media_types(struct mediatypes *mediatypes)
+{
+	struct media_type defaults[] = {
+		{
+			.media_name = "css",
+			.media_type = "text",
+			.media_subtype = "css",
+		},
+		{
+			.media_name = "gif",
+			.media_type = "image",
+			.media_subtype = "gif",
+		},
+		{
+			.media_name = "html",
+			.media_type = "text",
+			.media_subtype = "html",
+		},
+		{
+			.media_name = "ico",
+			.media_type = "image",
+			.media_subtype = "x-icon",
+		},
+		{
+			.media_name = "png",
+			.media_type = "image",
+			.media_subtype = "png",
+		},
+		{
+			.media_name = "jpeg",
+			.media_type = "image",
+			.media_subtype = "jpeg",
+		},
+		{
+			.media_name = "jpg",
+			.media_type = "image",
+			.media_subtype = "jpeg",
+		},
+		{
+			.media_name = "js",
+			.media_type = "application",
+			.media_subtype = "javascript",
+		},
+		{
+			.media_name = "svg",
+			.media_type = "image",
+			.media_subtype = "svg+xml",
+		},
+		{
+			.media_name = "txt",
+			.media_type = "text",
+			.media_subtype = "plain",
+		},
+		{
+			.media_name = "webmanifest",
+			.media_type = "application",
+			.media_subtype = "manifest+json",
+		},
+		{
+			.media_name = "xml",
+			.media_type = "text",
+			.media_subtype = "xml",
+		},
+	};
+	size_t i;
+
+	for (i = 0; i < nitems(defaults); ++i) {
+		if (media_add(mediatypes, &defaults[i]) == NULL)
+			return got_error_from_errno("media_add");
+	}
+
+	return NULL;
+}
+
 void
 gotsys_authorized_key_free(struct gotsys_authorized_key *key)
 {
@@ -205,6 +362,66 @@ gotsys_grouplist_purge(struct gotsys_grouplist *groups
 }
 
 void
+gotsys_webrepo_free(struct gotsys_webrepo *webrepo)
+{
+	if (webrepo == NULL)
+		return;
+
+	while (!STAILQ_EMPTY(&webrepo->access_rules)) {
+		struct gotsys_access_rule *rule;
+
+		rule = STAILQ_FIRST(&webrepo->access_rules);
+		STAILQ_REMOVE_HEAD(&webrepo->access_rules, entry);
+		gotsys_access_rule_free(rule);
+	}
+
+	free(webrepo);
+}
+
+void
+gotsys_webserver_free(struct gotsys_webserver *srv)
+{
+	struct got_pathlist_entry *pe;
+
+	if (srv == NULL)
+		return;
+
+	while (!STAILQ_EMPTY(&srv->access_rules)) {
+		struct gotsys_access_rule *rule;
+
+		rule = STAILQ_FIRST(&srv->access_rules);
+		STAILQ_REMOVE_HEAD(&srv->access_rules, entry);
+		gotsys_access_rule_free(rule);
+	}
+
+	media_purge(&srv->mediatypes);
+
+	while (!STAILQ_EMPTY(&srv->repos)) {
+		struct gotsys_webrepo *webrepo;
+
+		webrepo = STAILQ_FIRST(&srv->repos);
+		STAILQ_REMOVE_HEAD(&srv->repos, entry);
+		gotsys_webrepo_free(webrepo);
+	}
+
+	RB_FOREACH(pe, got_pathlist_head, &srv->websites) {
+		struct gotsys_website *site = pe->data;
+
+		while (!STAILQ_EMPTY(&site->access_rules)) {
+			struct gotsys_access_rule *rule;
+
+			rule = STAILQ_FIRST(&srv->access_rules);
+			STAILQ_REMOVE_HEAD(&srv->access_rules, entry);
+			gotsys_access_rule_free(rule);
+		}
+	}
+	got_pathlist_free(&srv->websites,
+	    GOT_PATHLIST_FREE_PATH | GOT_PATHLIST_FREE_DATA);
+
+	free(srv);
+}
+
+void
 gotsys_conf_clear(struct gotsys_conf *gotsysconf)
 {
 	gotsys_userlist_purge(&gotsysconf->users);
@@ -218,6 +435,16 @@ gotsys_conf_clear(struct gotsys_conf *gotsysconf)
 		TAILQ_REMOVE(&gotsysconf->repos, repo, entry);
 		gotsys_repo_free(repo);
 	}
+
+	while (!STAILQ_EMPTY(&gotsysconf->webservers)) {
+		struct gotsys_webserver *srv;
+
+		srv = STAILQ_FIRST(&gotsysconf->webservers);
+		STAILQ_REMOVE_HEAD(&gotsysconf->webservers, entry);
+		gotsys_webserver_free(srv);
+	}
+
+	media_purge(&gotsysconf->mediatypes);
 }
 
 static const char *wellknown_users[] = {
@@ -773,3 +1000,353 @@ gotsys_conf_new_access_rule(struct gotsys_access_rule 
 
 	return err;
 }
+
+const struct got_error *
+gotsys_conf_new_website(struct gotsys_website **site, const char *url_path)
+{
+	const struct got_error *err = NULL;
+	int i;
+
+	*site = NULL;
+
+	if (url_path[0] == '\0') {
+		return got_error_msg(GOT_ERR_PARSE_CONFIG,
+		    "empty URL path in configuration file");
+	}
+
+	for (i = 0; i < strlen(url_path); i++) {
+		if (isalnum((unsigned char)url_path[i]) ||
+		    url_path[i] == '-' || url_path[i] == '_' ||
+		    url_path[i] == '/')
+			continue;
+
+		return got_error_fmt(GOT_ERR_PARSE_CONFIG,
+		    "URL paths may only contain alphanumeric ASCII characters, "
+		    "hyphens, underscores, and slashes: %s", url_path);
+	}
+	
+	*site = calloc(1, sizeof(**site));
+	if (*site == NULL)
+		return got_error_from_errno("calloc");
+
+	STAILQ_INIT(&(*site)->access_rules);
+
+	if (!got_path_is_absolute(url_path)) {
+		int ret;
+
+		ret = snprintf((*site)->url_path, sizeof((*site)->url_path),
+		    "/%s", url_path);
+		if (ret == -1) {
+			err = got_error_from_errno("snprintf");
+			goto done;
+		}
+		if ((size_t)ret >= sizeof((*site)->url_path)) {
+			err = got_error_fmt(GOT_ERR_PARSE_CONFIG,
+			    "URL path too long (exceeds %zd bytes): %s",
+			    sizeof((*site)->url_path) - 1, url_path);
+			goto done;
+		}
+	} else {
+		if (strlcpy((*site)->url_path, url_path,
+		    sizeof((*site)->url_path)) >=
+		    sizeof((*site)->url_path)) {
+			err = got_error_fmt(GOT_ERR_PARSE_CONFIG,
+			    "URL path too long (exceeds %zd bytes): %s",
+			    sizeof((*site)->url_path) - 1, url_path);
+			goto done;
+		}
+	}
+
+done:
+	if (err) {
+		free(*site);
+		*site = NULL;
+	}
+
+	return err;
+}
+
+
+const struct got_error *
+gotsys_conf_validate_path(const char *path)
+{
+	size_t i;
+
+	for (i = 0; i < strlen(path); i++) {
+		if (isalnum((unsigned char)path[i]) ||
+		    path[i] == '.' ||
+		    path[i] == '-' ||
+		    path[i] == '_' ||
+		    path[i] == '/')
+			continue;
+
+		return got_error_fmt(GOT_ERR_PARSE_CONFIG,
+		    "paths may only contain alphanumeric characters, dots, "
+		    "hyphens, underscores, and slashes; bad path %s", path);
+	}
+
+	return NULL;
+}
+
+const struct got_error *
+gotsys_conf_validate_hostname(const char *host)
+{
+	size_t i, len;
+
+	len = strlen(host);
+	if (len == 0) {
+		return got_error_msg(GOT_ERR_PARSE_URI,
+		    "hostname cannot be empty");
+	}
+
+	for (i = 0; i < len; i++) {
+		if (isalnum((unsigned char)host[i]) ||
+		    host[i] == '.' || host[i] == '-')
+			continue;
+
+		return got_error_fmt(GOT_ERR_PARSE_URI,
+		    "hostnames may only contain alphanumeric characters, "
+		    "dots, and hyphens; bad hostname %s", host);
+	}
+
+	return NULL;
+}
+
+static inline int
+should_urlencode(int c)
+{
+	if (c <= ' ' || c >= 127)
+		return 1;
+
+	switch (c) {
+		/* gen-delim */
+	case ':':
+	case '/':
+	case '?':
+	case '#':
+	case '[':
+	case ']':
+	case '@':
+		/* sub-delims */
+	case '!':
+	case '$':
+	case '&':
+	case '\'':
+	case '(':
+	case ')':
+	case '*':
+	case '+':
+	case ',':
+	case ';':
+	case '=':
+		/* needed because the URLs are embedded into gotd.conf */
+	case '\"':
+		return 1;
+	default:
+		return 0;
+	}
+}
+
+static char *
+urlencode(const char *str)
+{
+	const char *s;
+	char *escaped;
+	size_t i, len;
+	int a, b;
+
+	len = 0;
+	for (s = str; *s; ++s) {
+		len++;
+		if (len == 1 && *s == '/')
+			continue;
+		if (should_urlencode(*s))
+			len += 2;
+	}
+
+	escaped = calloc(1, len + 1);
+	if (escaped == NULL)
+		return NULL;
+
+	i = 0;
+	for (s = str; *s; ++s) {
+		if (i == 0 && *s == '/') {
+			escaped[i++] = *s;
+			continue;
+		}
+		if (should_urlencode(*s)) {
+			a = (*s & 0xF0) >> 4;
+			b = (*s & 0x0F);
+
+			escaped[i++] = '%';
+			escaped[i++] = a <= 9 ? ('0' + a) : ('7' + a);
+			escaped[i++] = b <= 9 ? ('0' + b) : ('7' + b);
+		} else
+			escaped[i++] = *s;
+	}
+
+	return escaped;
+}
+
+const struct got_error *
+gotsys_conf_parse_url(char **proto, char **host, char **port,
+    char **request_path, const char *url)
+{
+	const struct got_error *err = NULL;
+	char *s, *p, *q;
+
+	*proto = *host = *port = *request_path = NULL;
+
+	p = strstr(url, "://");
+	if (!p) {
+		return got_error_msg(GOT_ERR_PARSE_URI,
+		    "no protocol specified");
+	}
+
+	*proto = strndup(url, p - url);
+	if (*proto == NULL) {
+		err = got_error_from_errno("strndup");
+		goto done;
+	}
+	s = p + 3;
+
+	p = strstr(s, "/");
+	if (p == NULL)
+		p = strchr(s, '\0');
+
+	q = memchr(s, ':', p - s);
+	if (q) {
+		*host = strndup(s, q - s);
+		if (*host == NULL) {
+			err = got_error_from_errno("strndup");
+			goto done;
+		}
+		if ((*host)[0] == '\0') {
+			err = got_error(GOT_ERR_PARSE_URI);
+			goto done;
+		}
+		*port = strndup(q + 1, p - (q + 1));
+		if (*port == NULL) {
+			err = got_error_from_errno("strndup");
+			goto done;
+		}
+		if ((*port)[0] == '\0') {
+			err = got_error(GOT_ERR_PARSE_URI);
+			goto done;
+		}
+		if (strcmp(*port, "http") != 0 &&
+		    strcmp(*port, "https") != 0) {
+			const char *errstr;
+
+			(void)strtonum(*port, 1, USHRT_MAX, &errstr);
+			if (errstr != NULL) {
+				err = got_error_fmt(GOT_ERR_PARSE_URI,
+				    "port number '%s' is %s", *port, errstr);
+				goto done;
+			}
+		}
+	} else {
+		*host = strndup(s, p - s);
+		if (*host == NULL) {
+			err = got_error_from_errno("strndup");
+			goto done;
+		}
+	}
+
+	err = gotsys_conf_validate_hostname(*host);
+	if (err)
+		goto done;
+
+	while (p[0] == '/' && p[1] == '/')
+		p++;
+	if (p[0] == '\0') {
+		*request_path = strdup("/");
+		if (*request_path == NULL) {
+			err = got_error_from_errno("strdup");
+		}
+	} else {
+		*request_path = urlencode(p);
+		if (*request_path == NULL)
+			err = got_error_from_errno("calloc");
+	}
+done:
+	if (err) {
+		free(*proto);
+		*proto = NULL;
+		free(*host);
+		*host = NULL;
+		free(*port);
+		*port = NULL;
+		free(*request_path);
+		*request_path = NULL;
+	}
+	return err;
+}
+
+const struct got_error *
+gotsys_conf_validate_url(const char *url)
+{
+	const struct got_error *err;
+	char *proto, *hostname, *port, *path;
+
+	err = gotsys_conf_parse_url(&proto, &hostname, &port, &path, url);
+	if (err)
+		return err;
+
+	if (strcmp(proto, "http") != 0 &&
+	    strcmp(proto, "https") != 0) {
+		err = got_error_fmt(GOT_ERR_PARSE_CONFIG,
+		    "invalid protocol %s", proto);
+		goto done;
+	}
+
+	err = gotsys_conf_validate_hostname(hostname);
+	if (err)
+		goto done;
+
+	err = gotsys_conf_validate_path(path);
+	if (err)
+		goto done;
+done:
+	free(proto);
+	free(hostname);
+	free(port);
+	free(path);
+	return err;
+}
+
+const struct got_error *
+gotsys_conf_validate_mediatype(const char *s)
+{
+	size_t i;
+
+	for (i = 0; s[i] != '\0'; ++i) {
+		if (!isalnum((unsigned char)s[i]) &&
+		    s[i] != '-' && s[i] != '+' && s[i] != '.')
+			return got_error_path(s, GOT_ERR_MEDIA_TYPE);
+	}
+	return (0);
+}
+
+const struct got_error *
+gotsys_conf_validate_string(const char *s)
+{
+	int i;
+
+	for (i = 0; s[i] != '\0'; ++i) {
+		char x = s[i];
+
+		/* keep in sync with gotwebd/parse.y allowed_in_string() */
+		if (isalnum((unsigned char)x) ||
+		    (ispunct((unsigned char)x) && x != '(' && x != ')' &&
+		    x != '{' && x != '}' &&
+		    x != '!' && x != '=' && x != '#' &&
+		    x != ',' && x != '/'))
+			continue;
+
+		return got_error_fmt(GOT_ERR_PARSE_CONFIG,
+		    "character '%c' (0x%.2x) is not allowed in %s", x, x, s);
+	}
+
+	return NULL;
+}
blob - f1655dcef5d0d634b534c6bce9610af42b951d0f
blob + 5aaa262772c025066c2bdc5685c5681511e97da9
--- lib/gotsys_imsg.c
+++ lib/gotsys_imsg.c
@@ -17,7 +17,14 @@
 #include <sys/types.h>
 #include <sys/queue.h>
 #include <sys/tree.h>
+#include <sys/un.h>
 
+#include <net/if.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+#include <netdb.h>
+
 #include <event.h>
 #include <imsg.h>
 #include <limits.h>
@@ -31,8 +38,11 @@
 #include "got_error.h"
 #include "got_path.h"
 #include "got_object.h"
+#include "got_reference.h"
 
+#include "media.h"
 #include "gotsysd.h"
+#include "gotwebd.h"
 #include "gotsys.h"
 
 #ifndef MIN
@@ -999,7 +1009,372 @@ send_notification_config(struct gotsysd_imsgev *iev, s
 	return send_notification_targets(iev, repo->name, &repo->notification_targets);
 }
 
+
+void
+gotsysd_web_config_init(struct gotsysd_web_config *webcfg)
+{
+	memset(webcfg, 0, sizeof(*webcfg));
+	TAILQ_INIT(&webcfg->listen_addrs);
+	STAILQ_INIT(&webcfg->servers);
+	webcfg->auth_config = GOTSYSD_WEB_AUTH_UNSET;
+}
+
+const struct got_error *
+gotsys_imsg_recv_web_cfg(struct gotsysd_web_config *new, struct imsg *imsg)
+{
+	const struct got_error *err;
+	struct gotsysd_web_config cfg;
+
+	if (imsg_get_data(imsg, &cfg, sizeof(cfg)) == -1)
+		return got_error_from_errno("imsg_get_data");
+
+	switch (cfg.auth_config) {
+	case GOTSYSD_WEB_AUTH_UNSET:
+	case GOTSYSD_WEB_AUTH_DISABLED:
+	case GOTSYSD_WEB_AUTH_SECURE:
+	case GOTSYSD_WEB_AUTH_INSECURE:
+		break;
+	default:
+		return got_error_msg(GOT_ERR_PRIVSEP_MSG,
+		    "bad web authentication setting");
+	}
+
+	if (cfg.control_socket[nitems(cfg.control_socket) - 1] != '\0' ||
+	    cfg.httpd_chroot[nitems(cfg.httpd_chroot) - 1] != '\0'||
+	    cfg.htdocs_path[nitems(cfg.htdocs_path) - 1] != '\0' ||
+	    cfg.repos_path[nitems(cfg.repos_path) - 1] != '\0' ||
+	    cfg.gotwebd_user[nitems(cfg.gotwebd_user) - 1] != '\0' ||
+	    cfg.www_user[nitems(cfg.www_user) - 1] != '\0' ||
+	    cfg.login_hint_user[nitems(cfg.login_hint_user) - 1] != '\0' ||
+	    cfg.login_hint_port[nitems(cfg.login_hint_port) - 1] != '\0')
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+
+	if (cfg.control_socket[0] != '\0') {
+		err = gotsys_conf_validate_path(cfg.control_socket);
+		if (err)
+			return err;
+	}
+
+	if (cfg.httpd_chroot[0] != '\0') {
+		err = gotsys_conf_validate_path(cfg.httpd_chroot);
+		if (err)
+			return err;
+	}
+
+	if (cfg.htdocs_path[0] != '\0') {
+		err = gotsys_conf_validate_path(cfg.htdocs_path);
+		if (err)
+			return err;
+	}
+
+	if (cfg.repos_path[0] == '\0') {
+		return got_error_msg(GOT_ERR_PRIVSEP_LEN,
+		    "empty repos_path in web config");
+	}
+	err = gotsys_conf_validate_path(cfg.repos_path);
+	if (err)
+		return err;
+
+	if (cfg.gotwebd_user[0] != '\0') {
+		err = gotsys_conf_validate_name(cfg.gotwebd_user, "user");
+		if (err)
+			return err;
+	}
+
+	if (cfg.www_user[0] != '\0') {
+		err = gotsys_conf_validate_name(cfg.www_user, "user");
+		if (err)
+			return err;
+	}
+
+	if (cfg.login_hint_user[0] != '\0') {
+		err = gotsys_conf_validate_name(cfg.login_hint_user, "user");
+		if (err)
+			return err;
+	}
+
+	if (cfg.login_hint_port[0] != '\0') {
+		const char *errstr = NULL;
+
+		strtonum(cfg.login_hint_port, 1, USHRT_MAX, &errstr);
+		if (errstr) {
+			return got_error_fmt(GOT_ERR_PRIVSEP_LEN,
+			    "port number %s is %s", cfg.login_hint_port,
+			    errstr);
+		}
+
+	}
+
+	memcpy(new, &cfg, sizeof(*new));
+	TAILQ_INIT(&new->listen_addrs);
+	STAILQ_INIT(&new->servers);
+
+	return NULL;
+}
+
+const struct got_error *
+gotsysd_conf_validate_inet_addr(const char *hostname, const char *servname)
+{
+	struct addrinfo hints, *res0;
+	int error;
+
+	memset(&hints, 0, sizeof(hints));
+	hints.ai_family = AF_UNSPEC;
+	hints.ai_socktype = SOCK_STREAM;
+	hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG;
+	error = getaddrinfo(hostname, servname, &hints, &res0);
+	if (error) {
+		return got_error_fmt(GOT_ERR_PARSE_CONFIG,
+		    "could not parse \"%s:%s\": %s", hostname, servname,
+		    gai_strerror(error));
+	}
+
+	freeaddrinfo(res0);
+	return NULL;
+}
+
+const struct got_error *
+gotsys_imsg_recv_webaddr(struct gotsysd_web_address **new, struct imsg *imsg)
+{
+	const struct got_error *err = NULL;
+	struct gotsysd_web_address *addr = NULL;
+
+	*new = NULL;
+
+	addr = calloc(1, sizeof(*addr));
+	if (addr == NULL)
+		return got_error_from_errno("calloc");
+
+	if (imsg_get_data(imsg, addr, sizeof(*addr)) == -1)
+		return got_error_from_errno("imsg_get_data");
+
+	switch (addr->family) {
+	case GOTSYSD_LISTEN_ADDR_UNIX:
+		err = gotsys_conf_validate_path(addr->addr.unix_socket_path);
+		break;
+	case GOTSYSD_LISTEN_ADDR_INET:
+		err = gotsysd_conf_validate_inet_addr(
+		    addr->addr.inet.address, addr->addr.inet.port);
+		break;
+	default:
+		return got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+		    "bad listen address family %u", addr->family);
+	}
+
+	if (err)
+		free(addr);
+	else
+		*new = addr;
+
+	return err;
+}
+
+const struct got_error *
+gotsys_imsg_recv_gotweb_server(struct gotsysd_web_server **new,
+    struct imsg *imsg)
+{
+	const struct got_error *err = NULL;
+	struct gotsysd_web_server *srv = NULL;
+
+	*new = NULL;
+
+	srv = calloc(1, sizeof(*srv));
+	if (srv == NULL)
+		return got_error_from_errno("calloc");
+
+	if (imsg_get_data(imsg, srv, sizeof(*srv)) == -1)
+		return got_error_from_errno("imsg_get_data");
+
+	switch (srv->auth_config) {
+	case GOTSYSD_WEB_AUTH_UNSET:
+	case GOTSYSD_WEB_AUTH_DISABLED:
+	case GOTSYSD_WEB_AUTH_SECURE:
+	case GOTSYSD_WEB_AUTH_INSECURE:
+		break;
+	default:
+		return got_error_msg(GOT_ERR_PRIVSEP_MSG,
+		    "bad web authentication setting");
+	}
+
+	if (srv->server_name[nitems(srv->server_name) - 1] != '\0' ||
+	    srv->gotweb_url_root[nitems(srv->gotweb_url_root) - 1] != '\0' ||
+	    srv->htdocs_path[nitems(srv->htdocs_path) - 1] != '\0')
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+
+	if (srv->server_name[0] != '\0') {
+		err = gotsys_conf_validate_hostname(srv->server_name);
+		if (err)
+			goto done;
+	}
+
+	if (srv->gotweb_url_root[0] != '\0') {
+		err = gotsys_conf_validate_path(srv->gotweb_url_root);
+		if (err)
+			goto done;
+	}
+
+	if (srv->htdocs_path[0] != '\0') {
+		err = gotsys_conf_validate_path(srv->htdocs_path);
+		if (err)
+			goto done;
+	}
+done:
+	if (err)
+		free(srv);
+	else
+		*new = srv;
+
+	return err;
+}
+
+
 static const struct got_error *
+send_webrepo(struct gotsysd_imsgev *iev, struct gotsys_webrepo *webrepo)
+{
+	const struct got_error *err;
+	struct gotsys_access_rule *rule;
+
+	if (gotsysd_imsg_compose_event(iev, GOTSYSD_IMSG_SYSCONF_WEBREPO,
+	    0, -1, webrepo, sizeof(*webrepo)) == -1)
+		return got_error_from_errno("gotsysd_imsg_compose_event");
+
+	STAILQ_FOREACH(rule, &webrepo->access_rules, entry) {
+		err = gotsys_imsg_send_access_rule(iev, rule,
+		    GOTSYSD_IMSG_SYSCONF_WEBREPO_ACCESS_RULE);
+		if (err)
+			return err;
+	}
+
+	if (gotsysd_imsg_compose_event(iev,
+	    GOTSYSD_IMSG_SYSCONF_WEBREPO_ACCESS_RULES_DONE, 0, -1,
+	    NULL, 0) == -1)
+		return got_error_from_errno("gotsysd_imsg_compose_event");
+
+	return NULL;
+}
+
+static const struct got_error *
+send_website(struct gotsysd_imsgev *iev, const char *path,
+    struct gotsys_website *web)
+{
+	const struct got_error *err;
+	struct gotsys_access_rule *rule;
+
+	if (gotsysd_imsg_compose_event(iev, GOTSYSD_IMSG_SYSCONF_WEBSITE_PATH,
+	    0, -1, (void *)path, strlen(path)) == -1)
+		return got_error_from_errno("gotsysd_imsg_compose_event");
+
+	if (gotsysd_imsg_compose_event(iev, GOTSYSD_IMSG_SYSCONF_WEBSITE,
+	    0, -1, web, sizeof(*web)) == -1)
+		return got_error_from_errno("gotsysd_imsg_compose_event");
+
+	STAILQ_FOREACH(rule, &web->access_rules, entry) {
+		err = gotsys_imsg_send_access_rule(iev, rule,
+		    GOTSYSD_IMSG_SYSCONF_WEBSITE_ACCESS_RULE);
+		if (err)
+			return err;
+	}
+
+	if (gotsysd_imsg_compose_event(iev,
+	    GOTSYSD_IMSG_SYSCONF_WEBSITE_ACCESS_RULES_DONE, 0, -1,
+	    NULL, 0) == -1)
+		return got_error_from_errno("gotsysd_imsg_compose_event");
+
+	return NULL;
+}
+
+const struct got_error *
+gotsys_imsg_recv_website_path(struct gotsys_website **site, struct imsg *imsg)
+{
+	const struct got_error *err;
+	size_t datalen;
+	char *url_path = NULL;
+
+	*site = NULL;
+
+	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+	if (datalen >= sizeof((*site)->url_path))
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+	
+	url_path = strndup((const char *)imsg->data, datalen);
+	if (url_path == NULL)
+		return got_error_from_errno("strndup");
+	
+	err = gotsys_conf_new_website(site, url_path);
+	free(url_path);
+	return err;
+}
+
+const struct got_error *
+gotsys_imsg_recv_website(struct gotsys_website *new, struct imsg *imsg)
+{
+	const struct got_error *err;
+	struct gotsys_website site;
+
+	if (imsg_get_data(imsg, &site, sizeof(site)) == -1)
+		return got_error_from_errno("imsg_get_data");
+
+	switch (site.auth_config) {
+	case GOTSYS_AUTH_UNSET:
+	case GOTSYS_AUTH_DISABLED:
+	case GOTSYS_AUTH_ENABLED:
+		break;
+	default:
+		return got_error_msg(GOT_ERR_PRIVSEP_MSG,
+		    "bad website authentication setting");
+	}
+
+	if (site.url_path[nitems(site.url_path) - 1] != '\0' ||
+	    site.branch_name[nitems(site.branch_name) - 1] != '\0'||
+	    site.path[nitems(site.path) - 1] != '\0')
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+
+	if (strcmp(new->url_path, site.url_path) != 0)
+		return got_error(GOT_ERR_PRIVSEP_MSG);
+
+	if (site.branch_name[0] != '\0') {
+		if (!got_ref_name_is_valid(site.branch_name))
+			return got_error_path(site.branch_name,
+			    GOT_ERR_BAD_REF_NAME);
+	}
+
+	if (site.path[0] != '\0') {
+		err = gotsys_conf_validate_path(site.path);
+		if (err)
+			return err;
+	}
+
+	memcpy(new, &site, sizeof(*new));
+	STAILQ_INIT(&new->access_rules);
+
+	return NULL;
+}
+
+const struct got_error *
+gotsys_imsg_recv_webrepo(struct gotsys_webrepo *new, struct imsg *imsg)
+{
+	struct gotsys_webrepo web;
+
+	if (imsg_get_data(imsg, &web, sizeof(web)) == -1)
+		return got_error_from_errno("imsg_get_data");
+
+	switch (web.auth_config) {
+	case GOTSYS_AUTH_UNSET:
+	case GOTSYS_AUTH_DISABLED:
+	case GOTSYS_AUTH_ENABLED:
+		break;
+	default:
+		return got_error_msg(GOT_ERR_PRIVSEP_MSG,
+		    "bad web authentication setting");
+	}
+
+	memcpy(new, &web, sizeof(*new));
+	STAILQ_INIT(&new->access_rules);
+
+	return NULL;
+}
+
+static const struct got_error *
 send_repo(struct gotsysd_imsgev *iev, struct gotsys_repo *repo)
 {
 	const struct got_error *err;
@@ -1012,6 +1387,7 @@ send_repo(struct gotsysd_imsgev *iev, struct gotsys_re
 	irepo.name_len = strlen(repo->name);
 	if (repo->headref)
 		irepo.headref_len = strlen(repo->headref);
+	irepo.description_len = strlen(repo->description);
 
 	wbuf = imsg_create(&iev->ibuf, GOTSYSD_IMSG_SYSCONF_REPO,
 	    0, 0, sizeof(irepo) + irepo.name_len + irepo.headref_len);
@@ -1025,6 +1401,9 @@ send_repo(struct gotsysd_imsgev *iev, struct gotsys_re
 	if (repo->headref &&
 	    imsg_add(wbuf, repo->headref, irepo.headref_len) == -1)
 		return got_error_from_errno("imsg_add SYSCONF_REPO");
+	if (irepo.description_len > 0 &&
+	    imsg_add(wbuf, repo->description, irepo.description_len) == -1)
+		return got_error_from_errno("imsg_add SYSCONF_REPO");
 
 	imsg_close(&iev->ibuf, wbuf);
 
@@ -1051,7 +1430,212 @@ send_repo(struct gotsysd_imsgev *iev, struct gotsys_re
 	return NULL;
 }
 
+static const struct got_error *
+send_media_type(struct gotsysd_imsgev *iev,
+    struct media_type *media, int imsg_type)
+{
+	if (gotsysd_imsg_compose_event(iev, imsg_type, 0, -1,
+	        media, sizeof(*media)) == -1)
+		return got_error_from_errno("gotsysd_imsg_compose_event");
+	
+	return NULL;
+}
+
 const struct got_error *
+gotsys_imsg_recv_media_type(struct media_type *new, struct imsg *imsg)
+{
+	const struct got_error *err;
+	struct media_type media;
+
+	if (imsg_get_data(imsg, &media, sizeof(media)) == -1)
+		return got_error_from_errno("imsg_get_data");
+
+	if (media.media_name[nitems(media.media_name) - 1] != '\0' ||
+	    media.media_type[nitems(media.media_type) - 1] != '\0' ||
+	    media.media_subtype[nitems(media.media_subtype) - 1] != '\0')
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+
+	err = gotsys_conf_validate_mediatype(media.media_name);
+	if (err)
+		return err;
+
+	err = gotsys_conf_validate_mediatype(media.media_type);
+	if (err)
+		return err;
+	
+	err = gotsys_conf_validate_mediatype(media.media_subtype);
+	if (err)
+		return err;
+
+	memcpy(new, &media, sizeof(*new));
+	return NULL;
+}
+
+
+static const struct got_error *
+send_webserver(struct gotsysd_imsgev *iev, struct gotsys_webserver *srv)
+{
+	const struct got_error *err = NULL;
+	struct gotsys_access_rule *rule;
+	struct gotsys_webrepo *webrepo;
+	struct got_pathlist_entry *pe;
+
+	if (gotsysd_imsg_compose_event(iev,
+	    GOTSYSD_IMSG_SYSCONF_WEB_SERVER, 0, -1, srv, sizeof(*srv)) == -1)
+		return got_error_from_errno("gotsysd_imsg_compose_event");
+	
+	STAILQ_FOREACH(rule, &srv->access_rules, entry) {
+		err = gotsys_imsg_send_access_rule(iev, rule,
+		    GOTSYSD_IMSG_SYSCONF_WEB_ACCESS_RULE);
+		if (err)
+			return err;
+	}
+
+	if (gotsysd_imsg_compose_event(iev,
+	    GOTSYSD_IMSG_SYSCONF_WEB_ACCESS_RULES_DONE, 0, -1, NULL, 0) == -1)
+		return got_error_from_errno("gotsysd_imsg_compose_event");
+
+	STAILQ_FOREACH(webrepo, &srv->repos, entry) {
+		err = send_webrepo(iev, webrepo);
+		if (err)
+			return err;
+	}
+
+	if (gotsysd_imsg_compose_event(iev,
+	    GOTSYSD_IMSG_SYSCONF_WEBREPOS_DONE, 0, -1, NULL, 0) == -1)
+		return got_error_from_errno("gotsysd_imsg_compose_event");
+
+	RB_FOREACH(pe, got_pathlist_head, &srv->websites) {
+		struct gotsys_website *site = pe->data;
+
+		err = send_website(iev, pe->path, site);
+		if (err)
+			return err;
+	}
+
+	if (gotsysd_imsg_compose_event(iev,
+	    GOTSYSD_IMSG_SYSCONF_WEBSITES_DONE, 0, -1, NULL, 0) == -1)
+		return got_error_from_errno("gotsysd_imsg_compose_event");
+
+	err = gotsys_imsg_send_mediatypes(iev, &srv->mediatypes,
+	    GOTSYSD_IMSG_SYSCONF_MEDIA_TYPE,
+	    GOTSYSD_IMSG_SYSCONF_MEDIA_TYPES_DONE);
+	if (err)
+		return err;
+
+	return NULL;
+}
+
+const struct got_error *
+gotsys_imsg_send_webservers(struct gotsysd_imsgev *iev,
+    struct gotsys_webserverlist *servers)
+{
+	const struct got_error *err = NULL;
+	struct gotsys_webserver *srv;
+
+	STAILQ_FOREACH(srv, servers, entry) {
+		err = send_webserver(iev, srv);
+		if (err)
+			return err;
+	}
+
+	if (gotsysd_imsg_compose_event(iev,
+	    GOTSYSD_IMSG_SYSCONF_WEB_SERVERS_DONE, 0, -1, NULL, 0) == -1) {
+		return got_error_from_errno("gotsysd_imsg_compose_event");
+	}
+
+	return NULL;
+}
+
+const struct got_error *
+gotsys_imsg_send_mediatypes(struct gotsysd_imsgev *iev,
+    struct mediatypes *mediatypes, int imsg_code, int done_code)
+{
+	const struct got_error *err;
+	struct media_type *media;
+
+	RB_FOREACH(media, mediatypes, mediatypes) {
+		err = send_media_type(iev, media, imsg_code);
+		if (err)
+			return err;
+	}
+
+	if (gotsysd_imsg_compose_event(iev, done_code, 0, -1, NULL, 0) == -1)
+		return got_error_from_errno("gotsysd_imsg_compose_event");
+
+	return NULL;
+}
+
+const struct got_error *
+gotsys_imsg_recv_web_server(struct gotsys_webserver *new, struct imsg *imsg)
+{
+	const struct got_error *err;
+	struct gotsys_webserver srv;
+
+	if (imsg_get_data(imsg, &srv, sizeof(srv)) == -1)
+		return got_error_from_errno("imsg_get_data");
+
+	err = gotsys_conf_validate_hostname(srv.server_name);
+	if (err)
+		return err;
+
+	switch (srv.auth_config) {
+	case GOTSYS_AUTH_UNSET:
+	case GOTSYS_AUTH_DISABLED:
+	case GOTSYS_AUTH_ENABLED:
+		break;
+	default:
+		return got_error_msg(GOT_ERR_PRIVSEP_MSG,
+		    "bad web server authentication setting");
+	}
+
+	if (srv.css[nitems(srv.css) - 1] != '\0' ||
+	    srv.logo[nitems(srv.logo) - 1] != '\0'||
+	    srv.logo_url[nitems(srv.logo_url) - 1] != '\0'||
+	    srv.site_owner[nitems(srv.site_owner) - 1] != '\0' ||
+	    srv.repos_url_path[nitems(srv.repos_url_path) - 1] != '\0')
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+
+	if (srv.css[0] != '\0') {
+		err = gotsys_conf_validate_path(srv.css);
+		if (err)
+			return err;
+	}
+
+	if (srv.logo[0] != '\0') {
+		err = gotsys_conf_validate_path(srv.logo);
+		if (err)
+			return err;
+	}
+
+	if (srv.logo_url[0] != '\0') {
+		err = gotsys_conf_validate_url(srv.logo_url);
+		if (err)
+			return err;
+	}
+
+	if (srv.site_owner[0] != '\0') {
+		err = gotsys_conf_validate_string(srv.site_owner);
+		if (err)
+			return err;
+	}
+
+	if (srv.repos_url_path[0] != '\0') {
+		err = gotsys_conf_validate_path(srv.repos_url_path);
+		if (err)
+			return err;
+	}
+
+	memcpy(new, &srv, sizeof(*new));
+	STAILQ_INIT(&new->access_rules);
+	RB_INIT(&new->mediatypes);
+	STAILQ_INIT(&new->repos);
+	RB_INIT(&new->websites);
+
+	return NULL;
+}
+
+const struct got_error *
 gotsys_imsg_send_repositories(struct gotsysd_imsgev *iev,
     struct gotsys_repolist *repos)
 {
@@ -1077,7 +1661,7 @@ gotsys_imsg_recv_repository(struct gotsys_repo **repo,
 	const struct got_error *err;
 	struct gotsysd_imsg_sysconf_repo irepo;
 	size_t datalen;
-	char *name = NULL, *headref = NULL;
+	char *name = NULL, *headref = NULL, *description = NULL;
 
 	*repo = NULL;
 
@@ -1086,7 +1670,8 @@ gotsys_imsg_recv_repository(struct gotsys_repo **repo,
 		return got_error(GOT_ERR_PRIVSEP_LEN);
 	
 	memcpy(&irepo, imsg->data, sizeof(irepo));
-	if (datalen != sizeof(irepo) + irepo.name_len + irepo.headref_len ||
+	if (datalen != sizeof(irepo) +
+	    irepo.name_len + irepo.headref_len + irepo.description_len ||
 	    irepo.name_len == 0)
 		return got_error(GOT_ERR_PRIVSEP_LEN);
 
@@ -1097,6 +1682,9 @@ gotsys_imsg_recv_repository(struct gotsys_repo **repo,
 		err = got_error(GOT_ERR_PRIVSEP_LEN);
 		goto done;
 	}
+	err = gotsys_conf_validate_repo_name(name);
+	if (err)
+		goto done;
 
 	if (irepo.headref_len > 0) {
 		headref = strndup(imsg->data + sizeof(irepo) + irepo.name_len,
@@ -1109,15 +1697,47 @@ gotsys_imsg_recv_repository(struct gotsys_repo **repo,
 			err = got_error(GOT_ERR_PRIVSEP_LEN);
 			goto done;
 		}
+
+		if (!got_ref_name_is_valid(headref)) {
+			err = got_error_path(headref, GOT_ERR_BAD_REF_NAME);
+			goto done;
+		}
 	}
 
+	if (irepo.description_len > 0) {
+		description = strndup(imsg->data + sizeof(irepo) +
+		    irepo.name_len + irepo.headref_len,
+		    irepo.description_len);
+		if (description == NULL) {
+			err = got_error_from_errno("strndup");
+			goto done;
+		}
+		if (strlen(description) != irepo.description_len) {
+			err = got_error(GOT_ERR_PRIVSEP_LEN);
+			goto done;
+		}
+
+		err = gotsys_conf_validate_string(description);
+		if (err)
+			goto done;
+	}
+
 	err = gotsys_conf_new_repo(repo, name);
 	if (err)
 		goto done;
 
 	(*repo)->headref = headref;
+	if (description != NULL) {
+		if (strlcpy((*repo)->description, description,
+		    sizeof((*repo)->description)) >=
+		    sizeof((*repo)->description)) {
+			err = got_error(GOT_ERR_PRIVSEP_LEN);
+			goto done;
+		}
+	}
 done:
 	free(name);
+	free(description);
 	if (err)
 		free(headref);
 	return err;
blob - d47bc250f8f0ef47d5cb6931a419568b58558e14
blob + 043e545b9d2a8a7eca916b98b7f04690bb3a3e8d
--- regress/gotsysd/Makefile
+++ regress/gotsysd/Makefile
@@ -11,7 +11,7 @@ CLEANFILES= SHA256.sig bsd.rd bsd.rd.fs bsd.rd.decomp 
 	${GOTSYSD_BSD_RD} ${GOTSYSD_SSH_KEY} ${GOTSYSD_SSH_PUBKEY} \
 	${GOTSYSD_TEST_VM_BASE_IMAGE} ${GOTSYSD_VM_PASSWD_FILE} ${GOTD_CONF} \
 	${GOTSYSD_CONF} ${GOTSYS_CONF} ${GOT_CONF} ${INSTALL_SITE} \
-	${HTTPD_CONF} ${GOTWEBD_CONF} ${GOTWEBD_WWW_CONF}
+	${HTTPD_CONF}
 
 .PHONY: ensure_root vm start_test_vm build_got
 
@@ -40,9 +40,6 @@ GOTSYSD_TEST_SMTP_PORT=2525
 GOTSYSD_TEST_HTTP_PORT=8000
 GOTSYSD_TEST_HMAC_SECRET!=openssl rand -base64 32
 HTTPD_CONF=httpd.conf
-GOTWEBD_CONF=gotwebd.conf
-GOTWEBD_WWW_CONF=gotwebd-www.conf
-GOTWEBD_REPOS_WWW_CONF=gotwebd-repos-www.conf
 
 GOTSYSD_TEST_USER?=${DOAS_USER}
 .if empty(GOTSYSD_TEST_USER)
@@ -199,7 +196,13 @@ ${GOTD_CONF}:
 ${GOTSYSD_CONF}:
 	@${UNPRIV} "echo permit root > $@"
 	@${UNPRIV} "echo permit _gotd >> $@"
-	@${UNPRIV} "echo listen on \\'${GOTSYSD_UNIX_SOCKET}\\' >> $@"
+	@${UNPRIV} "echo listen on \\\"${GOTSYSD_UNIX_SOCKET}\\\" >> $@"
+	@${UNPRIV} "echo 'gotweb {' >> $@"
+	@${UNPRIV} "printf '\tprefork 1\n'  >> $@"
+	@${UNPRIV} "printf '\tenable authentication insecure\n' >> $@"
+	@${UNPRIV} "printf '\tlogin hint user \"${GOTSYSD_TEST_USER}\"\n' >> $@"
+	@${UNPRIV} "echo '}' >> $@"
+	@${UNPRIV} "echo 'web server \"VMIP\"' >> $@"
 
 ${GOTSYS_CONF}: ${GOTSYSD_SSH_PUBKEY}
 	@${UNPRIV} "echo user ${GOTSYSD_TEST_USER} { >> $@"
@@ -222,85 +225,6 @@ $(HTTPD_CONF):
 	@${UNPRIV} 'echo \ \ fastcgi socket \"/run/gotweb.sock\" >> $@'
 	@${UNPRIV} 'echo } >> $@'
 
-$(GOTWEBD_CONF):
-	@${UNPRIV} 'echo prefork 1 > $@'
-	@${UNPRIV} 'echo enable authentication insecure >> $@'
-	@${UNPRIV} 'echo permit ${GOTSYSD_TEST_USER} >> $@'
-	@${UNPRIV} 'echo deny ${GOTSYSD_DEV_USER} >> $@'
-	@${UNPRIV} 'echo server \"VMIP\" { >> $@'
-	@${UNPRIV} 'echo \ \ repos_path \"/git\" >> $@'
-	@${UNPRIV} 'echo \ \ hide repositories on >> $@'
-	@${UNPRIV} 'echo \ \ login hint user ${GOTSYSD_TEST_USER} >> $@'
-	@${UNPRIV} 'echo \ \ show_repo_age off >> $@'
-	@${UNPRIV} 'echo \ \ show_repo_description off >> $@'
-	@${UNPRIV} 'echo \ \ show_repo_owner off >> $@'
-	@${UNPRIV} 'echo \ \ show_site_owner off >> $@'
-	@${UNPRIV} 'echo \ \ repository \"public\" { >> $@'
-	@${UNPRIV} 'echo \ \ \ \ disable authentication >> $@'
-	@${UNPRIV} 'echo \ \ \ \ hide repository off >> $@'
-	@${UNPRIV} 'echo \ \ } >> $@'
-	@${UNPRIV} 'echo \ \ repository \"gotdev\" { >> $@'
-	@${UNPRIV} 'echo \ \ \ \ permit ${GOTSYSD_DEV_USER} >> $@'
-	@${UNPRIV} 'echo \ \ \ \ deny ${GOTSYSD_TEST_USER} >> $@'
-	@${UNPRIV} 'echo \ \ \ \ hide repository off >> $@'
-	@${UNPRIV} 'echo \ \ } >> $@'
-	@${UNPRIV} 'echo \ \ repository \"gottest\" { >> $@'
-	@${UNPRIV} 'echo \ \ \ \ permit ${GOTSYSD_TEST_USER} >> $@'
-	@${UNPRIV} 'echo \ \ \ \ deny ${GOTSYSD_DEV_USER} >> $@'
-	@${UNPRIV} 'echo \ \ \ \ hide repository off >> $@'
-	@${UNPRIV} 'echo \ \ } >> $@'
-	@${UNPRIV} 'echo \ \ repository \"hidden\" { >> $@'
-	@${UNPRIV} 'echo \ \ \ \ permit ${GOTSYSD_TEST_USER} >> $@'
-	@${UNPRIV} 'echo \ \ \ \ deny ${GOTSYSD_DEV_USER} >> $@'
-	@${UNPRIV} 'echo \ \ } >> $@'
-	@${UNPRIV} 'echo \ \ repository \"gotsys\" { >> $@'
-	@${UNPRIV} 'echo \ \ \ \ hide repository off >> $@'
-	@${UNPRIV} 'echo \ \ } >> $@'
-	@${UNPRIV} 'echo } >> $@'
-
-
-$(GOTWEBD_WWW_CONF):
-	@${UNPRIV} 'echo prefork 1 > $@'
-	@${UNPRIV} 'echo enable authentication insecure >> $@'
-	@${UNPRIV} 'echo permit ${GOTSYSD_TEST_USER} >> $@'
-	@${UNPRIV} 'echo deny ${GOTSYSD_DEV_USER} >> $@'
-	@${UNPRIV} 'echo server \"VMIP\" { >> $@'
-	@${UNPRIV} 'echo \ \ repos_path \"/git\" >> $@'
-	@${UNPRIV} 'echo \ \ hide repositories on >> $@'
-	@${UNPRIV} 'echo \ \ login hint user ${GOTSYSD_TEST_USER} >> $@'
-	@${UNPRIV} 'echo \ \ show_repo_age off >> $@'
-	@${UNPRIV} 'echo \ \ show_repo_description off >> $@'
-	@${UNPRIV} 'echo \ \ show_repo_owner off >> $@'
-	@${UNPRIV} 'echo \ \ show_site_owner off >> $@'
-	@${UNPRIV} 'echo \ \ repos_url_path \"/repos\" >> $@'
-	@${UNPRIV} 'echo \ \ website \"/\" { >> $@'
-	@${UNPRIV} 'echo \ \ \ \ repository \"www\" >> $@'
-	@${UNPRIV} 'echo \ \ \ \ disable authentication >> $@'
-	@${UNPRIV} 'echo \ \ } >> $@'
-	@${UNPRIV} 'echo } >> $@'
-
-$(GOTWEBD_REPOS_WWW_CONF):
-	@${UNPRIV} 'echo prefork 1 > $@'
-	@${UNPRIV} 'echo enable authentication insecure >> $@'
-	@${UNPRIV} 'echo permit ${GOTSYSD_TEST_USER} >> $@'
-	@${UNPRIV} 'echo deny ${GOTSYSD_DEV_USER} >> $@'
-	@${UNPRIV} 'echo server \"VMIP\" { >> $@'
-	@${UNPRIV} 'echo \ \ repos_path \"/git\" >> $@'
-	@${UNPRIV} 'echo \ \ repository \"gotsys\" { >> $@'
-	@${UNPRIV} 'echo \ \ \ \ hide repository on >> $@'
-	@${UNPRIV} 'echo \ \ } >> $@'
-	@${UNPRIV} 'echo \ \ login hint user ${GOTSYSD_TEST_USER} >> $@'
-	@${UNPRIV} 'echo \ \ show_repo_age off >> $@'
-	@${UNPRIV} 'echo \ \ show_repo_description off >> $@'
-	@${UNPRIV} 'echo \ \ show_repo_owner off >> $@'
-	@${UNPRIV} 'echo \ \ show_site_owner off >> $@'
-	@${UNPRIV} 'echo \ \ repos_url_path \"/\" >> $@'
-	@${UNPRIV} 'echo \ \ website \"/website\" { >> $@'
-	@${UNPRIV} 'echo \ \ \ \ repository \"www\" >> $@'
-	@${UNPRIV} 'echo \ \ \ \ disable authentication >> $@'
-	@${UNPRIV} 'echo \ \ } >> $@'
-	@${UNPRIV} 'echo } >> $@'
-
 build_got:
 	@set -e; \
 	VMID=`vmctl status ${GOTSYSD_VM_NAME} | tail -n1 | \
@@ -324,7 +248,7 @@ build_got:
 	${UNPRIV} "${GOTSYSD_SSH_CMD} -- root@$${VMIP} \
 		ln -f -s gitwrapper /usr/local/bin/git-receive-pack"
 
-setup_test_vm: start_test_vm build_got ${GOTD_CONF} ${GOTSYSD_CONF} ${GOTSYS_CONF} ${GOT_CONF} ${HTTPD_CONF} ${GOTWEBD_CONF}
+setup_test_vm: start_test_vm build_got ${GOTD_CONF} ${GOTSYSD_CONF} ${GOTSYS_CONF} ${GOT_CONF} ${HTTPD_CONF}
 	@set -e; \
 	VMID=`vmctl status ${GOTSYSD_VM_NAME} | tail -n1 | \
 		awk '{print $$1}'`; \
@@ -371,10 +295,9 @@ setup_test_vm: start_test_vm build_got ${GOTD_CONF} ${
 	${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} \
 		useradd -d /nonexistent -s /sbin/nologin \
 		-u ${GOTWEBD_UID} -g ${GOTWEBD_UID} -G _gotd _gotwebd"; \
-	${UNPRIV} "${GOTSYSD_SCP_CMD} ${GOTWEBD_CONF} root@$${VMIP}:/etc/"; \
 	${UNPRIV} "${GOTSYSD_SCP_CMD} ${HTTPD_CONF} root@$${VMIP}:/etc/"; \
 	${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} \
-		sed -i s/VMIP/$${VMIP}/ /etc/httpd.conf /etc/gotwebd.conf"; \
+		sed -i s/VMIP/$${VMIP}/ /etc/httpd.conf /etc/gotsysd.conf"; \
 	${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} rcctl -f start httpd"; \
 	${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} \
 		/usr/local/sbin/gotwebd -v"; \
@@ -418,7 +341,7 @@ test_gotwebd:
 	${UNPRIV} "env ${GOTSYSD_TEST_ENV} VMIP=$${VMIP} GWIP=$${GWIP} \
 		sh ./test_gotwebd.sh"
 
-test_gotwebd_www: ${GOTWEBD_WWW_CONF}
+test_gotwebd_www:
 	@set -e; \
 	VMID=`vmctl status ${GOTSYSD_VM_NAME} | tail -n1 | \
 		awk '{print $$1}'`; \
@@ -441,17 +364,10 @@ test_gotwebd_www: ${GOTWEBD_WWW_CONF}
 	${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} gotctl reload"; \
 	${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} \
 		gotsys apply -w > /dev/null"; \
-	${UNPRIV} "${GOTSYSD_SCP_CMD} \
-		${GOTWEBD_WWW_CONF} root@$${VMIP}:/etc/gotwebd.conf"; \
-	${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} \
-		sed -i s/VMIP/$${VMIP}/ /etc/httpd.conf /etc/gotwebd.conf"; \
-	${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} pkill -x gotwebd || true"; \
-	${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} \
-		/usr/local/sbin/gotwebd -v"; \
 	${UNPRIV} "env ${GOTSYSD_TEST_ENV} VMIP=$${VMIP} GWIP=$${GWIP} \
 		sh ./test_gotwebd_www.sh"
 
-test_gotwebd_repos_www: ${GOTWEBD_REPOS_WWW_CONF}
+test_gotwebd_repos_www:
 	@set -e; \
 	VMID=`vmctl status ${GOTSYSD_VM_NAME} | tail -n1 | \
 		awk '{print $$1}'`; \
@@ -474,13 +390,6 @@ test_gotwebd_repos_www: ${GOTWEBD_REPOS_WWW_CONF}
 	${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} gotctl reload"; \
 	${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} \
 		gotsys apply -w > /dev/null"; \
-	${UNPRIV} "${GOTSYSD_SCP_CMD} \
-		${GOTWEBD_REPOS_WWW_CONF} root@$${VMIP}:/etc/gotwebd.conf"; \
-	${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} \
-		sed -i s/VMIP/$${VMIP}/ /etc/httpd.conf /etc/gotwebd.conf"; \
-	${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} pkill -x gotwebd || true"; \
-	${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} \
-		/usr/local/sbin/gotwebd -v"; \
 	${UNPRIV} "env ${GOTSYSD_TEST_ENV} VMIP=$${VMIP} GWIP=$${GWIP} \
 		sh ./test_gotwebd_repos_www.sh"
 
blob - 5cc01663c72d19b29b85b44eccec2a7b4f4cfd1e
blob + 450010d156afe918a733370bf639412bc7b92364
--- regress/gotsysd/test_gotwebd.sh
+++ regress/gotsysd/test_gotwebd.sh
@@ -20,6 +20,89 @@
 test_login() {
 	local testroot=`test_init login 1`
 
+	got checkout -q $testroot/${GOTSYS_REPO} $testroot/wt >/dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got checkout failed unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	crypted_vm_pw=`echo ${GOTSYSD_VM_PASSWORD} | encrypt | tr -d '\n'`
+	crypted_pw=`echo ${GOTSYSD_DEV_PASSWORD} | encrypt | tr -d '\n'`
+	sshkey=`cat ${GOTSYSD_SSH_PUBKEY}`
+	cat > ${testroot}/wt/gotsys.conf <<EOF
+user ${GOTSYSD_TEST_USER} {
+	password "${crypted_vm_pw}" 
+	authorized key ${sshkey}
+}
+user ${GOTSYSD_DEV_USER} {
+	password "${crypted_pw}" 
+	authorized key ${sshkey}
+}
+repository gotsys.git {
+	permit rw ${GOTSYSD_TEST_USER}
+	permit rw ${GOTSYSD_DEV_USER}
+}
+repository gotdev.git {
+	permit rw ${GOTSYSD_DEV_USER}
+}
+repository hidden.git {
+	permit rw ${GOTSYSD_TEST_USER}
+}
+web server "${VMIP}" {
+	repository gotsys.git {
+		permit ${GOTSYSD_TEST_USER}
+	}
+	repository gotdev.git {
+		permit ${GOTSYSD_DEV_USER}
+		deny ${GOTSYSD_TEST_USER}
+	}
+	repository hidden.git {
+		permit ${GOTSYSD_TEST_USER}
+		deny ${GOTSYSD_DEV_USER}
+		hide repository on
+	}
+}
+EOF
+	(cd ${testroot}/wt && gotsys check -q)
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "bad gotsys.conf written by test" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	(cd ${testroot}/wt && got commit -m "configure web server" >/dev/null)
+	local commit_id=`git_show_head $testroot/${GOTSYS_REPO}`
+
+	got send -q -i ${GOTSYSD_SSH_KEY} -r ${testroot}/${GOTSYS_REPO}
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got send failed unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	# Wait for gotsysd to apply the new configuration.
+	echo "$commit_id" > $testroot/stdout.expected
+	for i in 1 2 3 4 5; do
+		sleep 1
+		ssh -i ${GOTSYSD_SSH_KEY} root@${VMIP} \
+			cat /var/db/gotsysd/commit > $testroot/stdout
+		if cmp -s $testroot/stdout.expected $testroot/stdout; then
+			break;
+		fi
+	done
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "gotsysd failed to apply configuration" >&2
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
 	# Attempt unauthorized access.
 	w3m "http://${VMIP}/" -dump > $testroot/stdout
 	cat > $testroot/stdout.expected <<EOF
@@ -90,7 +173,7 @@ Tree:
 Date:
     $d
 Message:
-    init
+    configure web server
 
 -------------------------------------------------------------------------------
 
@@ -191,9 +274,40 @@ repository gotdev.git {
 repository gottest.git {
 	permit rw ${GOTSYSD_TEST_USER}
 }
+repository hidden.git {
+	permit rw ${GOTSYSD_TEST_USER}
+}
+web server "${VMIP}" {
+	repository gotsys.git {
+		permit ${GOTSYSD_TEST_USER}
+	}
+	repository public.git {
+		disable authentication
+	}
+	repository gotdev.git {
+		permit ${GOTSYSD_DEV_USER}
+		deny ${GOTSYSD_TEST_USER}
+	}
+	repository gottest.git {
+		permit ${GOTSYSD_TEST_USER}
+	}
+	repository hidden.git {
+		permit ${GOTSYSD_TEST_USER}
+		deny ${GOTSYSD_DEV_USER}
+		hide repository on
+	}
+}
 EOF
+	(cd ${testroot}/wt && gotsys check -q)
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "bad gotsys.conf written by test" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
 	(cd ${testroot}/wt && got commit \
-		-m "create user ${GOTSYSD_DEV_USER}" >/dev/null)
+		-m "add public and gottest repositories" >/dev/null)
 	local commit_id=`git_show_head $testroot/${GOTSYS_REPO}`
 
 	got send -q -i ${GOTSYSD_SSH_KEY} -r ${testroot}/${GOTSYS_REPO}
@@ -312,6 +426,102 @@ EOF
 test_access_rules_tree_page() {
 	local testroot=`test_init access_rules_tree_page 1`
 
+	got checkout -q $testroot/${GOTSYS_REPO} $testroot/wt >/dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got checkout failed unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	crypted_vm_pw=`echo ${GOTSYSD_VM_PASSWORD} | encrypt | tr -d '\n'`
+	crypted_pw=`echo ${GOTSYSD_DEV_PASSWORD} | encrypt | tr -d '\n'`
+	sshkey=`cat ${GOTSYSD_SSH_PUBKEY}`
+	cat > ${testroot}/wt/gotsys.conf <<EOF
+user ${GOTSYSD_TEST_USER} {
+	password "${crypted_vm_pw}" 
+	authorized key ${sshkey}
+}
+user ${GOTSYSD_DEV_USER} {
+	password "${crypted_pw}" 
+	authorized key ${sshkey}
+}
+repository gotsys.git {
+	permit rw ${GOTSYSD_TEST_USER}
+	permit rw ${GOTSYSD_DEV_USER}
+}
+repository public.git {
+	permit rw ${GOTSYSD_TEST_USER}
+	permit rw ${GOTSYSD_DEV_USER}
+}
+repository gotdev.git {
+	permit rw ${GOTSYSD_DEV_USER}
+}
+repository gottest.git {
+	permit rw ${GOTSYSD_TEST_USER}
+}
+repository hidden.git {
+	permit rw ${GOTSYSD_TEST_USER}
+}
+web server "${VMIP}" {
+	repository gotsys.git {
+		permit ${GOTSYSD_TEST_USER}
+	}
+	repository public.git {
+		disable authentication
+	}
+	repository gotdev.git {
+		permit ${GOTSYSD_DEV_USER}
+		deny ${GOTSYSD_TEST_USER}
+	}
+	repository gottest.git {
+		permit ${GOTSYSD_TEST_USER}
+	}
+	repository hidden.git {
+		permit ${GOTSYSD_TEST_USER}
+		deny ${GOTSYSD_DEV_USER}
+		hide repository on
+	}
+}
+EOF
+	(cd ${testroot}/wt && gotsys check -q)
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "bad gotsys.conf written by test" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	(cd ${testroot}/wt && got commit -m "no-op update" >/dev/null)
+	local commit_id=`git_show_head $testroot/${GOTSYS_REPO}`
+
+	got send -q -i ${GOTSYSD_SSH_KEY} -r ${testroot}/${GOTSYS_REPO}
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got send failed unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	# Wait for gotsysd to apply the new configuration.
+	echo "$commit_id" > $testroot/stdout.expected
+	for i in 1 2 3 4 5; do
+		sleep 1
+		ssh -i ${GOTSYSD_SSH_KEY} root@${VMIP} \
+			cat /var/db/gotsysd/commit > $testroot/stdout
+		if cmp -s $testroot/stdout.expected $testroot/stdout; then
+			break;
+		fi
+	done
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "gotsysd failed to apply configuration" >&2
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
 	# Attempt to access a public repository's tree
 	w3m "http://${VMIP}/?action=tree&path=public.git" -dump \
 		> $testroot/stdout
blob - b06c59dc38cab81acce2ebdbee93ad031bbb5307
blob + bde5eb6dedc5a1485928126d3e54e01952843ebb
--- regress/gotsysd/test_gotwebd_repos_www.sh
+++ regress/gotsysd/test_gotwebd_repos_www.sh
@@ -49,6 +49,22 @@ repository www {
 	permit rw ${GOTSYSD_TEST_USER}
 	permit rw ${GOTSYSD_DEV_USER}
 }
+web server "${VMIP}" {
+	repository gotsys {
+		hide repository on
+	}
+
+	repositories url path "/"
+
+	website "/website" {
+		repository "www"
+		disable authentication
+	}
+
+	repository www {
+		permit ${GOTSYSD_TEST_USER}
+	}
+}
 EOF
 	(cd ${testroot}/wt && got commit  -m "add www.git" >/dev/null)
 	local commit_id=`git_show_head $testroot/${GOTSYS_REPO}`
blob - 7506c40e00eb1198f1b6f2afd1cc14622ae0dc2e
blob + 458acd7a387ae3f64dd6717ab997684bc5158fac
--- regress/gotsysd/test_gotwebd_www.sh
+++ regress/gotsysd/test_gotwebd_www.sh
@@ -49,7 +49,28 @@ repository www {
 	permit rw ${GOTSYSD_TEST_USER}
 	permit rw ${GOTSYSD_DEV_USER}
 }
+
+web server "${VMIP}" {
+	repository gotsys {
+		hide repository on
+	}
+
+	repositories url path "/repos"
+
+	website "/" {
+		repository "www"
+		disable authentication
+	}
+}
 EOF
+	(cd ${testroot}/wt && gotsys check -q)
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "bad gotsys.conf written by test" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
 	(cd ${testroot}/wt && got commit  -m "add www.git" >/dev/null)
 	local commit_id=`git_show_head $testroot/${GOTSYS_REPO}`