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

From:
"Omar Polo" <op@omarpolo.com>
Subject:
Re: types {} block for gotwebd
To:
Stefan Sperling <stsp@stsp.name>
Cc:
gameoftrees@openbsd.org
Date:
Mon, 08 Dec 2025 02:19:29 +0100

Download raw body.

Thread
Stefan Sperling <stsp@stsp.name> wrote:
> On Mon, Nov 17, 2025 at 03:50:33PM +0100, Omar Polo wrote:
> > --- gotwebd/config.c
> > +++ gotwebd/config.c
> > @@ -44,6 +44,7 @@
> >  
> >  #include "gotwebd.h"
> >  #include "log.h"
> > +#include "media.h"
> >  
> >  int
> >  config_init(struct gotwebd *env)
> > @@ -60,6 +61,11 @@ config_init(struct gotwebd *env)
> >  	TAILQ_INIT(&env->addresses);
> >  	STAILQ_INIT(&env->access_rules);
> >  
> > +	env->mediatypes = calloc(1, sizeof(*env->mediatypes));
> > +	if (env->mediatypes == NULL)
> > +		return (-1);
> > +	RB_INIT(env->mediatypes);
> > +
> 
> The gotwebd tests can complain about memleaks with this if we
> forgot to free it before exiting.
> 
> Any reason the RB tree root cannot be a struct instead of a pointer?
> Then we could do: RB_INIT(&env.mediatypes); and avoid malloc/free.

The only reason for this is that i wanted to hide the fact that it was a
tree-based thing and thusly avoid having to pull sys/tree.h and media.h
in all files using gotwebd.

now that we have way more sys/tree.h around, i guess it's less of a
point :p

> >  	for (i = 0; i < PRIV_FDS__MAX; i++)
> >  		env->priv_fd[i] = -1;
> >  
> > @@ -79,6 +85,20 @@ config_getcfg(struct gotwebd *env, struct imsg *imsg)
> >  }
> >  
> >  int
> > +config_getmediatype(struct gotwebd *env, struct imsg *imsg)
> > +{
> > +	struct media_type mt;
> > +
> > +	if (imsg_get_data(imsg, &mt, sizeof(mt)) == -1)
> > +		fatal("%s: failed to receive media type", __func__);
> 
> We are reading arbitrary data here and blindly expect it to contain
> strings. We should check whether types added are printable-ASCII-only,
> and whether the strings are NUL-terminated.
> 
> This might sound very paranoid since we trust the parent to send a valid
> config. But in context of gotsysd the media types list may well be arbitrary
> user input which was read from gotsys.conf. It would be best if gotwebd
> rejected bad media types at config parse-time, since gotsys.conf will
> likely borrow related code from gotwebd. Then we could fail early on bad
> input while parsing gotsys.conf. Because media_add is also called from
> parse.y, adding such checks to media_add() would be best, I think.

not paranoid at all, we've seen issues in several deamons when we got
lazy and lower the guard at IPC layer, and it will always bite to do so.

I've improved this and straightened the parsing in general.

> > +	if (media_add(env->mediatypes, &mt) == NULL)
> > +		fatal("%s: failed to insert media type", __func__);
> > +
> > +	return (0);
> > +}
> > +
> > +int
> >  config_getserver(struct gotwebd *env, struct imsg *imsg)
> >  {
> >  	struct server *srv;
> 
> > --- gotwebd/gotwebd.c
> > +++ gotwebd/gotwebd.c
> 
> > @@ -586,7 +588,8 @@ main(int argc, char **argv)
> >  	env = calloc(1, sizeof(*env));
> >  	if (env == NULL)
> >  		fatal("%s: calloc", __func__);
> > -	config_init(env);
> > +	if (config_init(env) == -1)
> > +		fatal("%s: failed to initialize configuration", __func__);
> 
> Are we sure config_init() always sets errno? Should this use fatalx instead?

now config_init() cannot fail, just like now.

> >  	while ((ch = getopt(argc, argv, "A:D:dG:f:F:L:nS:vW:")) != -1) {
> >  		switch (ch) {
> 
> > --- /dev/null
> > +++ gotwebd/media.c
> 
> > +struct media_type *
> > +media_add(struct mediatypes *types, struct media_type *media)
> > +{
> > +	struct media_type	*entry;
> > +
> > +	if ((entry = RB_FIND(mediatypes, types, media)) != NULL) {
> > +		log_debug("%s: entry overwritten for \"%s\"", __func__,
> > +		    media->media_name);
> > +		media_delete(types, entry);
> > +	}
> > +
> > +	if ((entry = malloc(sizeof(*media))) == NULL)
> > +		return (NULL);
> > +
> > +	memcpy(entry, media, sizeof(*entry));
> 
> This is where I would like to have proper validation instead of memcpy.

i've sanitized the string at a slightly upper layer, in parse.y, as
doing it here is a bit awkward, also because we're dealing with an
already parsed string into the struct.

> > +	RB_INSERT(mediatypes, types, entry);
> > +
> > +	return (entry);
> > +}
> 
> The rest looks good to me.


diff refs/heads/main refs/heads/op/mime
commit - 222a9d10ecceca6ca66c57d1a0dc84d13f5a2047
commit + c873f667c7e3717ed606bb7d105941c2ed385e22
blob - cb1821eb9160b452f0b06c959c08118f05d37707
blob + 2898ed092da39dc54655a6ebd00b5995d6c50130
--- gotwebd/Makefile
+++ gotwebd/Makefile
@@ -6,7 +6,7 @@
 
 PROG =		gotwebd
 SRCS =		config.c sockets.c auth.c login.c gotwebd.c parse.y \
-		fcgi.c gotweb.c got_operations.c tmpl.c pages.c
+		fcgi.c gotweb.c got_operations.c tmpl.c pages.c media.c
 SRCS +=		blame.c commit_graph.c delta.c diff.c \
 		diffreg.c error.c object.c object_cache.c \
 		object_idset.c object_parse.c opentemp.c path.c pack.c \
blob - 9282838c15b7ace89c1eb4581c04faef543d6c00
blob + fee647670be0b0365e0f36d46ce347fd2522d0ac
--- gotwebd/auth.c
+++ gotwebd/auth.c
@@ -37,6 +37,7 @@
 #include "got_object.h"
 #include "got_path.h"
 
+#include "media.h"
 #include "gotwebd.h"
 #include "log.h"
 #include "tmpl.h"
blob - 254bf8dccfe73be47d35b6bd7293c29d8f912e4c
blob + 81a6e7549ba5fb191877024e3c107b2323226571
--- gotwebd/config.c
+++ gotwebd/config.c
@@ -45,6 +45,7 @@
 #include "got_path.h"
 #include "got_error.h"
 
+#include "media.h"
 #include "gotwebd.h"
 #include "log.h"
 
@@ -63,6 +64,8 @@ config_init(struct gotwebd *env)
 	TAILQ_INIT(&env->addresses);
 	STAILQ_INIT(&env->access_rules);
 
+	RB_INIT(&env->mediatypes);
+
 	for (i = 0; i < PRIV_FDS__MAX; i++)
 		env->priv_fd[i] = -1;
 
@@ -82,6 +85,25 @@ config_getcfg(struct gotwebd *env, struct imsg *imsg)
 }
 
 int
+config_getmediatype(struct gotwebd *env, struct imsg *imsg)
+{
+	struct media_type mt;
+
+	if (imsg_get_data(imsg, &mt, sizeof(mt)) == -1)
+		fatal("%s: failed to receive media type", __func__);
+
+	if (mt.media_name[sizeof(mt.media_name)-1] != '\0' ||
+	    mt.media_type[sizeof(mt.media_type)-1] != '\0' ||
+	    mt.media_subtype[sizeof(mt.media_subtype)-1] != '\0')
+		fatal("%s: received corrupted media type", __func__);
+
+	if (media_add(&env->mediatypes, &mt) == NULL)
+		fatal("%s: failed to insert media type", __func__);
+
+	return (0);
+}
+
+int
 config_getserver(struct gotwebd *env, struct imsg *imsg)
 {
 	struct server *srv;
blob - ff4e53e93502e25453a36bd7a656041afa2649af
blob + 01e5003ac8ede257607fe188eab2161e67dcae4e
--- gotwebd/fcgi.c
+++ gotwebd/fcgi.c
@@ -45,6 +45,7 @@
 
 #include "got_lib_poll.h"
 
+#include "media.h"
 #include "gotwebd.h"
 #include "log.h"
 #include "tmpl.h"
blob - 18120ebaddfa19651ad40001d9a26efde8f40271
blob + 9ce2aca4d5f6597c4536c9c0501d05dc69674373
--- gotwebd/got_operations.c
+++ gotwebd/got_operations.c
@@ -41,6 +41,7 @@
 #include "got_privsep.h"
 #include "got_opentemp.h"
 
+#include "media.h"
 #include "gotwebd.h"
 #include "log.h"
 
blob - 815155bc5d1de0f8132a20700f9262313f027833
blob + 4811204b8543b0f9a67166dafc4273eb9a04a90b
--- gotwebd/gotweb.c
+++ gotwebd/gotweb.c
@@ -50,6 +50,7 @@
 #include "got_blame.h"
 #include "got_privsep.h"
 
+#include "media.h"
 #include "gotwebd.h"
 #include "log.h"
 #include "tmpl.h"
@@ -264,10 +265,10 @@ gotweb_serve_htdocs(struct request *c, const char *req
 {
 	const struct got_error *error = NULL;
 	struct server *srv = c->srv;;
+	struct media_type *m;
 	char *ondisk_path = NULL;
-	int fd = -1;
-	const char *mime_type = "application/octet-stream";
-	char *ext;
+	int n, fd = -1;
+	char mime_type[MEDIA_STRMAX] = "application/octet-stream";
 
 	if (asprintf(&ondisk_path, "%s/%s/%s", gotwebd_env->httpd_chroot,
 	    srv->htdocs_path, request_path) == -1) {
@@ -284,29 +285,14 @@ gotweb_serve_htdocs(struct request *c, const char *req
 		goto done;
 	}
 
-	/*
-	 * TODO: Port generic mime-type handling from httpd.
-	 *
-	 * This hack works only because we know what files our static
-	 * assets contain. But we should account for other files which
-	 * might be dropped into our htdocs directory.
-	 */
-	ext = strrchr(ondisk_path, '.');
-	if (ext) {
-		if (strcmp(ext, ".css") == 0)
-			mime_type = "text/css";
-		if (strcmp(ext, ".ico") == 0)
-			mime_type = "image/x-icon";
-		if (strcmp(ext, ".png") == 0)
-			mime_type = "image/png";
-		if (strcmp(ext, ".svg") == 0)
-			mime_type = "image/svg+xml";
-		if (strcmp(ext, ".txt") == 0)
-			mime_type = "text/plain";
-		if (strcmp(ext, ".webmanifest") == 0)
-			mime_type = "application/manifest+json";
-		if (strcmp(ext, ".xml") == 0)
-			mime_type = "text/xml";
+	m = media_find(&gotwebd_env->mediatypes, ondisk_path);
+	if (m != NULL) {
+		n = snprintf(mime_type, sizeof(mime_type), "%s/%s",
+		    m->media_type, m->media_subtype);
+		if (n < 0 || (size_t)n >= sizeof(mime_type)) {
+			error = got_error(GOT_ERR_RANGE);
+			goto done;
+		}
 	}
 
 	if (gotweb_reply(c, 200, mime_type, NULL) == -1) {
@@ -349,8 +335,9 @@ serve_blob(int *response_code, struct request *c, stru
 {
 	const struct got_error *error = NULL;
 	struct got_blob_object *blob = NULL;
-	int binary;
-	const char *mime_type = "application/octet-stream";
+	struct media_type *m;
+	int binary, n;
+	char mime_type[MEDIA_STRMAX] = "application/octet-stream";
 
 	c->t->fd = dup(c->priv_fd[BLOB_FD_1]);
 	if (c->t->fd == -1) {
@@ -367,34 +354,21 @@ serve_blob(int *response_code, struct request *c, stru
 		goto done;
 
 	if (binary) {
-		if (gotweb_reply_file(c, "application/octet-stream",
-		    basename, NULL) == -1) {
+		if (gotweb_reply_file(c, mime_type, basename, NULL) == -1) {
 			error = got_error(GOT_ERR_IO);
 			*response_code = 500;
 			goto done;
 		}
 	} else {
-		char *ext;
-
-		/* TODO: Port generic mime-type handling from httpd. */
-		ext = strrchr(basename, '.');
-		if (ext) {
-			if (strcmp(ext, ".css") == 0)
-				mime_type = "text/css";
-			if (strcmp(ext, ".html") == 0)
-				mime_type = "text/html";
-			if (strcmp(ext, ".ico") == 0)
-				mime_type = "image/x-icon";
-			if (strcmp(ext, ".png") == 0)
-				mime_type = "image/png";
-			if (strcmp(ext, ".svg") == 0)
-				mime_type = "image/svg+xml";
-			if (strcmp(ext, ".txt") == 0)
-				mime_type = "text/plain";
-			if (strcmp(ext, ".webmanifest") == 0)
-				mime_type = "application/manifest+json";
-			if (strcmp(ext, ".xml") == 0)
-				mime_type = "text/xml";
+		m = media_find(&gotwebd_env->mediatypes, basename);
+		if (m != NULL) {
+			n = snprintf(mime_type, sizeof(mime_type), "%s/%s",
+			    m->media_type, m->media_subtype);
+			if (n < 0 || (size_t)n >= sizeof(mime_type)) {
+				error = got_error_msg(GOT_ERR_RANGE,
+				    "media type snprintf");
+				goto done;
+			}
 		}
 
 		if (gotweb_reply(c, 200, mime_type, NULL) == -1) {
@@ -1847,6 +1821,8 @@ gotweb_shutdown(void)
 	imsgbuf_clear(&gotwebd_env->iev_auth->ibuf);
 	free(gotwebd_env->iev_auth);
 
+	media_purge(&gotwebd_env->mediatypes);
+
 	config_free_access_rules(&gotwebd_env->access_rules);
 
 	while (!TAILQ_EMPTY(&gotwebd_env->servers)) {
@@ -2110,6 +2086,9 @@ gotweb_dispatch_main(int fd, short event, void *arg)
 				}
 			}
 			break;
+		case GOTWEBD_IMSG_CFG_MEDIA_TYPE:
+			config_getmediatype(env, &imsg);
+			break;
 		case GOTWEBD_IMSG_CFG_SRV:
 			config_getserver(env, &imsg);
 			break;
blob - 589946d8f305607c70ec6615af063261cd63768b
blob + 78d404dd92fadd2af905e6ef7be79bae2290a678
--- gotwebd/gotwebd.c
+++ gotwebd/gotwebd.c
@@ -47,6 +47,7 @@
 #include "got_object.h"
 #include "got_path.h"
 
+#include "media.h"
 #include "gotwebd.h"
 #include "log.h"
 
@@ -920,6 +921,7 @@ gotwebd_configure(struct gotwebd *env, uid_t uid, gid_
 	struct socket *sock;
 	struct gotwebd_repo *repo;
 	struct got_pathlist_entry *pe;
+	struct media_type *mt;
 	char secret[32];
 	int i;
 
@@ -935,6 +937,13 @@ gotwebd_configure(struct gotwebd *env, uid_t uid, gid_
 		    &env->access_rules);
 	}
 
+	/* send the mime mapping */
+	RB_FOREACH(mt, mediatypes, &env->mediatypes) {
+		if (main_compose_gotweb(env, GOTWEBD_IMSG_CFG_MEDIA_TYPE,
+		    -1, mt, sizeof(*mt)) == -1)
+			fatal("send_imsg GOTWEBD_IMSG_CFG_MEDIA_TYPE");
+	}
+
 	/* send our gotweb servers */
 	TAILQ_FOREACH(srv, &env->servers, entry) {
 		if (main_compose_sockets(env, GOTWEBD_IMSG_CFG_SRV,
blob - 76014c6e0170b7e9ca7cbcaf2d2878d193fe1b42
blob + 0064fe9416547008a5b17485946eb9b2b0772c53
--- gotwebd/gotwebd.h
+++ gotwebd/gotwebd.h
@@ -143,6 +143,7 @@ enum imsg_type {
 	GOTWEBD_IMSG_CFG_SOCK,
 	GOTWEBD_IMSG_CFG_FD,
 	GOTWEBD_IMSG_CFG_ACCESS_RULE,
+	GOTWEBD_IMSG_CFG_MEDIA_TYPE,
 	GOTWEBD_IMSG_CFG_REPO,
 	GOTWEBD_IMSG_CFG_WEBSITE,
 	GOTWEBD_IMSG_CFG_DONE,
@@ -481,6 +482,8 @@ struct gotwebd {
 	enum gotwebd_auth_config auth_config;
 	struct gotwebd_access_rule_list access_rules;
 
+	struct mediatypes mediatypes;
+
 	int		 pack_fds[GOTWEB_PACK_NUM_TEMPFILES];
 	int		 priv_fd[PRIV_FDS__MAX];
 
@@ -662,6 +665,7 @@ const struct got_error *got_output_file_blame(struct r
 
 /* config.c */
 int config_setserver(struct gotwebd *, struct server *);
+int config_getmediatype(struct gotwebd *, struct imsg *);
 int config_getserver(struct gotwebd *, struct imsg *);
 int config_setsock(struct gotwebd *, struct socket *, uid_t, gid_t);
 int config_getsock(struct gotwebd *, struct imsg *);
blob - 05a894bc5a6b1fdebdf747acaf7d215b24352cbd
blob + 83056ac23f4bacabca45e37c2efcdf61348bfe8e
--- gotwebd/login.c
+++ gotwebd/login.c
@@ -41,6 +41,7 @@
 #include "got_object.h"
 #include "got_path.h"
 
+#include "media.h"
 #include "gotwebd.h"
 #include "log.h"
 
blob - 119c384edf7ec7e2066f6e84b02c2cba0fed4f52
blob + 5488d4d84d40e5a7a8fee25624f264a66c596ef2
--- gotwebd/pages.tmpl
+++ gotwebd/pages.tmpl
@@ -36,6 +36,7 @@
 #include "got_reference.h"
 #include "got_path.h"
 
+#include "media.h"
 #include "gotwebd.h"
 #include "log.h"
 #include "tmpl.h"
blob - /dev/null
blob + 7913016add2d736f9bc5b85b16c244578a458e1c (mode 644)
--- /dev/null
+++ gotwebd/media.c
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2025 Omar Polo <op@openbsd.org>
+ * Copyright (c) 2014 Reyk Floeter <reyk@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/tree.h>
+
+#include <stdarg.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "media.h"
+#include "log.h"
+
+struct media_type *
+media_add(struct mediatypes *types, struct media_type *media)
+{
+	struct media_type	*entry;
+
+	if ((entry = RB_FIND(mediatypes, types, media)) != NULL) {
+		log_debug("%s: entry overwritten for \"%s\"", __func__,
+		    media->media_name);
+		media_delete(types, entry);
+	}
+
+	if ((entry = malloc(sizeof(*media))) == NULL)
+		return (NULL);
+
+	memcpy(entry, media, sizeof(*entry));
+	RB_INSERT(mediatypes, types, entry);
+
+	return (entry);
+}
+
+void
+media_delete(struct mediatypes *types, struct media_type *media)
+{
+	RB_REMOVE(mediatypes, types, media);
+
+	free(media);
+}
+
+void
+media_purge(struct mediatypes *types)
+{
+	struct media_type	*media;
+
+	while ((media = RB_MIN(mediatypes, types)) != NULL)
+		media_delete(types, media);
+}
+
+struct media_type *
+media_find(struct mediatypes *types, const char *file)
+{
+	struct media_type	*match, media;
+	char			*p;
+
+	/* Last component of the file name */
+	p = strchr(file, '\0');
+	while (p > file && p[-1] != '.' && p[-1] != '/')
+		p--;
+	if (*p == '\0')
+		return (NULL);
+
+	if (strlcpy(media.media_name, p,
+	    sizeof(media.media_name)) >=
+	    sizeof(media.media_name)) {
+		return (NULL);
+	}
+
+	/* Find media type by extension name */
+	match = RB_FIND(mediatypes, types, &media);
+
+	return (match);
+}
+
+static int
+media_cmp(struct media_type *a, struct media_type *b)
+{
+	return (strcasecmp(a->media_name, b->media_name));
+}
+
+RB_GENERATE(mediatypes, media_type, media_entry, media_cmp);
blob - 0804f9b818380c437ebb197db76175e8c24dbf02
blob + 86f6665ac565f2a5a977c110e607d395e3979018
--- gotwebd/parse.y
+++ gotwebd/parse.y
@@ -1,5 +1,6 @@
 /*
  * Copyright (c) 2016-2019, 2020-2021 Tracey Emery <tracey@traceyemery.net>
+ * Copyright (c) 2014 Reyk Floeter <reyk@openbsd.org>
  * Copyright (c) 2004, 2005 Esben Norby <norby@openbsd.org>
  * Copyright (c) 2004 Ryan McBride <mcbride@openbsd.org>
  * Copyright (c) 2002, 2003, 2004 Henning Brauer <henning@openbsd.org>
@@ -56,6 +57,7 @@
 #include "got_path.h"
 #include "got_error.h"
 
+#include "media.h"
 #include "gotwebd.h"
 #include "log.h"
 
@@ -105,6 +107,7 @@ static struct server		*new_srv;
 static struct server		*conf_new_server(const char *);
 int				 getservice(const char *);
 int				 n;
+struct media_type		 media;
 
 int		 get_addrs(const char *, const char *);
 struct address *get_unix_addr(const char *);
@@ -129,6 +132,20 @@ typedef struct {
 	int lineno;
 } YYSTYPE;
 
+static int
+mediatype_ok(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] != '.' &&
+		    s[i] != '/')
+			return (-1);
+	}
+	return (0);
+}
+
 %}
 
 %token	LISTEN GOTWEBD_LOGIN WWW SITE_NAME SITE_OWNER SITE_LINK LOGO
@@ -139,6 +156,7 @@ typedef struct {
 %token	SUMMARY_COMMITS_DISPLAY SUMMARY_TAGS_DISPLAY USER AUTHENTICATION
 %token	ENABLE DISABLE INSECURE REPOSITORY REPOSITORIES PERMIT DENY HIDE
 %token	WEBSITE PATH BRANCH REPOS_URL_PATH
+%token	TYPES INCLUDE
 
 %token	<v.string>	STRING
 %token	<v.number>	NUMBER
@@ -153,9 +171,25 @@ grammar		: /* empty */
 		| grammar varset '\n'
 		| grammar main '\n'
 		| grammar server '\n'
+		| grammar types '\n'
 		| grammar error '\n'		{ file->errors++; }
 		;
 
+include		: INCLUDE STRING		{
+			struct file	*nfile;
+
+			if ((nfile = pushfile($2, 0)) == NULL) {
+				yyerror("failed to include file %s", $2);
+				free($2);
+				YYERROR;
+			}
+			free($2);
+
+			file = nfile;
+			lungetc('\n');
+		}
+		;
+
 varset		: STRING '=' STRING	{
 			char *s = $1;
 			while (*s++) {
@@ -835,9 +869,75 @@ repoopts1	: DISABLE AUTHENTICATION {
 		}
 		;
 
+types		: TYPES	'{' optnl mediaopts_l '}'
+		;
+
+mediaopts_l	: mediaopts_l mediaoptsl nl
+		| mediaoptsl nl
+		;
+
+mediaoptsl	: mediastring medianames_l optsemicolon
+		| include
+		;
+
+mediastring	: STRING '/' STRING	{
+			if (mediatype_ok($1) == -1 || mediatype_ok($3)) {
+				yyerror("malformed media type: %s/%s", $1, $3);
+				free($1);
+				free($3);
+				YYERROR;
+			}
+
+			if (strlcpy(media.media_type, $1,
+			    sizeof(media.media_type)) >=
+			    sizeof(media.media_type) ||
+			    strlcpy(media.media_subtype, $3,
+			    sizeof(media.media_subtype)) >=
+			    sizeof(media.media_subtype)) {
+				yyerror("media type too long");
+				free($1);
+				free($3);
+				YYERROR;
+			}
+			free($1);
+			free($3);
+		}
+		;
+
+medianames_l	: medianames_l medianamesl
+		| medianamesl
+		;
+
+medianamesl	: numberstring				{
+			if (mediatype_ok($1) == -1) {
+				yyerror("malformed media name");
+				free($1);
+				YYERROR;
+			}
+
+			if (strlcpy(media.media_name, $1,
+			    sizeof(media.media_name)) >=
+			    sizeof(media.media_name)) {
+				yyerror("media name too long");
+				free($1);
+				YYERROR;
+			}
+			free($1);
+
+			if (media_add(&gotwebd->mediatypes, &media) == NULL) {
+				yyerror("failed to add media type");
+				YYERROR;
+			}
+		}
+		;
+
 nl		: '\n' optnl
 		;
 
+optsemicolon	: ';'
+		| /* empty */
+		;
+
 optnl		: '\n' optnl		/* zero or more newlines */
 		| /* empty */
 		;
@@ -887,6 +987,7 @@ lookup(char *s)
 		{ "hide",			HIDE },
 		{ "hint",			HINT },
 		{ "htdocs",			HTDOCS },
+		{ "include",			INCLUDE },
 		{ "insecure",			INSECURE },
 		{ "listen",			LISTEN },
 		{ "login",			GOTWEBD_LOGIN },
@@ -916,6 +1017,7 @@ lookup(char *s)
 		{ "socket",			SOCKET },
 		{ "summary_commits_display",	SUMMARY_COMMITS_DISPLAY },
 		{ "summary_tags_display",	SUMMARY_TAGS_DISPLAY },
+		{ "types",			TYPES },
 		{ "user",			USER },
 		{ "website",			WEBSITE },
 		{ "www",			WWW },
@@ -1170,7 +1272,7 @@ nodigits:
 	(isalnum(x) || (ispunct(x) && x != '(' && x != ')' && \
 	x != '{' && x != '}' && \
 	x != '!' && x != '=' && x != '#' && \
-	x != ','))
+	x != ',' && x != '/'))
 
 	if (isalnum(c) || c == ':' || c == '_') {
 		do {
@@ -1251,7 +1353,7 @@ pushfile(const char *name, int secret)
 		free(nfile);
 		return (NULL);
 	}
-	nfile->lineno = 1;
+	nfile->lineno = TAILQ_EMPTY(&files) ? 1 : 0;
 	nfile->ungetsize = 16;
 	nfile->ungetbuf = calloc(1, nfile->ungetsize);
 	if (nfile->ungetbuf == NULL) {
@@ -1328,6 +1430,57 @@ parse_config(const char *filename, struct gotwebd *env
 	if (TAILQ_EMPTY(&gotwebd->servers))
 		add_default_server();
 
+	/* load default mimes */
+	if (RB_EMPTY(&gotwebd->mediatypes)) {
+		struct media_type defaults[] = {
+			{
+				.media_name = "css",
+				.media_type = "text",
+				.media_subtype = "css",
+			},
+			{
+				.media_name = "ico",
+				.media_type = "image",
+				.media_subtype = "x-icon",
+			},
+			{
+				.media_name = "png",
+				.media_type = "image",
+				.media_subtype = "png",
+			},
+			{
+				.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(&gotwebd->mediatypes, &defaults[i]) == NULL) {
+				fprintf(stderr, "failed to load default"
+				    " MIME types\n");
+				errors++;
+				break;
+			}
+		}
+	}
+
 	/* add the implicit listen on socket */
 	if (TAILQ_EMPTY(&gotwebd->addresses)) {
 		char path[_POSIX_PATH_MAX];
blob - /dev/null
blob + 5dcd5ab3faa450463d5df07960fa1399d391a86d (mode 644)
--- /dev/null
+++ gotwebd/media.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2014 Reyk Floeter <reyk@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define MEDIATYPE_NAMEMAX	128	/* file name extension */
+#define MEDIATYPE_TYPEMAX	64	/* length of type/subtype */
+#define MEDIA_STRMAX		(MEDIATYPE_NAMEMAX + 1 + MEDIATYPE_TYPEMAX)
+
+struct media_type {
+	char			 media_name[MEDIATYPE_NAMEMAX];
+	char			 media_type[MEDIATYPE_TYPEMAX];
+	char			 media_subtype[MEDIATYPE_TYPEMAX];
+	RB_ENTRY(media_type)	 media_entry;
+};
+RB_HEAD(mediatypes, media_type);
+
+struct media_type	*media_add(struct mediatypes *, struct media_type *);
+void			 media_delete(struct mediatypes *, struct media_type *);
+void			 media_purge(struct mediatypes *);
+struct media_type	*media_find(struct mediatypes *, const char *);
+
+RB_PROTOTYPE(mediatypes, media_type, media_entry, media_cmp);
blob - f70495430f50e31b79ea07f70a6d4ee6f1447c05
blob + dfc46f68407be06d91de5ec11f3b91c189d4ec8a
--- gotwebd/sockets.c
+++ gotwebd/sockets.c
@@ -57,6 +57,7 @@
 #include "got_object.h"
 #include "got_path.h"
 
+#include "media.h"
 #include "gotwebd.h"
 #include "log.h"
 #include "tmpl.h"