Commit Diff


commit - 4d2ec6d7054c76105c6237251002df1acb5a6c2d
commit + 15902770073dd67df3a9af0f6da7d63bfb031d72
blob - 5660e44f37b2ae2d269500e367612556ee6b2e80
blob + bcbdc5f2e383a0bd338077fcddc98166b00a74ee
--- .gitignore
+++ .gitignore
@@ -5,3 +5,5 @@ gmid
 iri_test
 *.o
 docs
+lex.yy.c
+y.tab.*
blob - 57b63b4d1f46f550c45a73dabb9eb6c53b30f13b
blob + 55e81acad251e78087333268623b9b2d0bbdd4e5
--- ChangeLog
+++ ChangeLog
@@ -1,3 +1,12 @@
+2021-01-15  Omar Polo  <op@omarpolo.com>
+
+	* gmid.c (main): changed behaviour: daemon off by default
+	(main): changed -c in -C (cert option)
+	(main): changed -k in -K (key option, for consistency with -C)
+	(main): added -c to load a configuration
+	(main): certs, key and doc (-C -K and -d) doesn't have a default value anymore
+	(handle_handshake): add vhosts support
+
 2021-01-13  Omar Polo  <op@omarpolo.com>
 
 	* iri.c (parse_scheme): normalize scheme while parsing, so we're
blob - 3e0b72e31bb3c1c16b34e6062dbc4b93db793e29
blob + fe3b4ced6eb113cf1d47fa27bd1920da55b6cf96
--- Makefile
+++ Makefile
@@ -1,19 +1,27 @@
 CC =		cc
 CFLAGS =	-Wall -Wextra -g
 LDFLAGS =	-ltls
+LEX =		lex
+YACC =		yacc
 
 .PHONY: all clean test
 
 all: gmid TAGS README.md
 
-gmid: gmid.o iri.o utf8.o
-	${CC} gmid.o iri.o utf8.o -o gmid ${LDFLAGS}
+lex.yy.c: lex.l y.tab.c
+	${LEX} lex.l
 
+y.tab.c: parse.y
+	${YACC} -b y -d parse.y
+
+gmid: gmid.o iri.o utf8.o lex.yy.o y.tab.o
+	${CC} gmid.o iri.o utf8.o lex.yy.o y.tab.o -o gmid ${LDFLAGS}
+
 TAGS: gmid.c iri.c utf8.c
 	-etags gmid.c iri.c utf8.c || true
 
 clean:
-	rm -f *.o gmid iri_test
+	rm -f *.o lex.yy.c y.tab.c y.tab.h y.output gmid iri_test
 
 iri_test: iri_test.o iri.o utf8.o
 	${CC} iri_test.o iri.o utf8.o -o iri_test ${LDFLAGS}
blob - 46d2407e61b9dfeccfa9e8aa1523a1d735f3ec20
blob + 56e186e9e29a569d8801c7162f35dcb6bb7bc5ba
--- gmid.1
+++ gmid.1
@@ -20,10 +20,13 @@
 .Sh SYNOPSIS
 .Nm
 .Bk -words
+.Op Fl n
+.Op Fl c Ar config
+|
 .Op Fl 6fh
-.Op Fl c Ar cert.pem
-.Op Fl d Ar docs
-.Op Fl k Ar key.pem
+.Op Fl C Ar cert
+.Op Fl d Ar root
+.Op Fl K Ar key
 .Op Fl p Ar port
 .Op Fl x Ar cgi-bin
 .Ek
blob - 13d49e238fe9957f5d2580fda736d3f50e949676
blob + f69df77c026fc133d482b361d15021f2a7a1304a
--- gmid.c
+++ gmid.c
@@ -34,12 +34,13 @@
 #define LOGI(c, fmt, ...) logs(LOG_INFO,   c, fmt, __VA_ARGS__)
 #define LOGD(c, fmt, ...) logs(LOG_DEBUG,  c, fmt, __VA_ARGS__)
 
-const char *dir, *cgi;
-int dirfd;
-int port;
-int foreground;
+struct vhost hosts[HOSTSLEN];
+
 int connected_clients;
+int goterror;
 
+struct conf conf;
+
 struct etm {			/* file extension to mime */
 	const char	*mime;
 	const char	*ext;
@@ -79,7 +80,7 @@ fatal(const char *fmt, ...)
 
 	va_start(ap, fmt);
 
-	if (foreground) {
+	if (conf.foreground) {
 		vfprintf(stderr, fmt, ap);
 		fprintf(stderr, "\n");
 	} else
@@ -113,7 +114,7 @@ logs(int priority, struct client *c,
 	if (vasprintf(&fmted, fmt, ap) == -1)
 		fatal("vasprintf: %s", strerror(errno));
 
-	if (foreground)
+	if (conf.foreground)
 		fprintf(stderr, "%s:%s %s\n", hbuf, sbuf, fmted);
 	else {
 		if (asprintf(&s, "%s:%s %s", hbuf, sbuf, fmted) == -1)
@@ -221,7 +222,7 @@ check_path(struct client *c, const char *path, int *fd
 	struct stat sb;
 
 	assert(path != NULL);
-	if ((*fd = openat(dirfd, *path ? path : ".",
+	if ((*fd = openat(c->host->dirfd, *path ? path : ".",
 		    O_RDONLY | O_NOFOLLOW | O_CLOEXEC)) == -1) {
 		return FILE_MISSING;
 	}
@@ -289,7 +290,7 @@ open_file(char *fpath, char *query, struct pollfd *fds
 {
 	switch (check_path(c, fpath, &c->fd)) {
 	case FILE_EXECUTABLE:
-		if (cgi != NULL && starts_with(fpath, cgi))
+		if (c->host->cgi != NULL && starts_with(fpath, c->host->cgi))
 			return start_cgi(fpath, "", query, fds, c);
 
 		/* fallthrough */
@@ -318,7 +319,7 @@ open_file(char *fpath, char *query, struct pollfd *fds
 		return 0;
 
 	case FILE_MISSING:
-		if (cgi != NULL && starts_with(fpath, cgi))
+		if (c->host->cgi != NULL && starts_with(fpath, c->host->cgi))
 			return check_for_cgi(fpath, query, fds, c);
 
 		if (!start_reply(fds, c, NOT_FOUND, "not found"))
@@ -366,10 +367,10 @@ start_cgi(const char *spath, const char *relpath, cons
 		if (ec != 0)
 			goto childerr;
 
-		if (asprintf(&portno, "%d", port) == -1)
+		if (asprintf(&portno, "%d", conf.port) == -1)
 			goto childerr;
 
-		if (asprintf(&ex, "%s/%s", dir, spath) == -1)
+		if (asprintf(&ex, "%s/%s", c->host->dir, spath) == -1)
 			goto childerr;
 
 		if (asprintf(&requri, "%s%s%s", spath,
@@ -391,7 +392,7 @@ start_cgi(const char *spath, const char *relpath, cons
 		safe_setenv("QUERY_STRING", query);
 		safe_setenv("REMOTE_HOST", addr);
 		safe_setenv("REMOTE_ADDR", addr);
-		safe_setenv("DOCUMENT_ROOT", dir);
+		safe_setenv("DOCUMENT_ROOT", c->host->dir);
 
 		if (tls_peer_cert_provided(c->ctx)) {
 			safe_setenv("AUTH_TYPE", "Certificate");
@@ -571,6 +572,9 @@ send_dir(char *path, struct pollfd *fds, struct client
 void
 handle_handshake(struct pollfd *fds, struct client *c)
 {
+	struct vhost *h;
+	const char *servname;
+
 	switch (tls_handshake(c->ctx)) {
 	case 0:  /* success */
 	case -1: /* already handshaked */
@@ -586,11 +590,27 @@ handle_handshake(struct pollfd *fds, struct client *c)
 		abort();
 	}
 
-	/* TODO: check SNI here */
-	logs(LOG_DEBUG, c, "client wanted to talk to: %s", tls_conn_servername(c->ctx));
+	servname = tls_conn_servername(c->ctx);
+	if (servname == NULL)
+		goto wronghost;
 
-	c->state = S_OPEN;
-	handle_open_conn(fds, c);
+	for (h = hosts; h->domain != NULL; ++h) {
+		if (!strcmp(h->domain, servname) || !strcmp(h->domain, "*"))
+			break;
+	}
+
+	if (h->domain != NULL) {
+		c->state = S_OPEN;
+		c->host = h;
+		handle_open_conn(fds, c);
+		return;
+	}
+
+wronghost:
+	/* XXX: check the correct response */
+	if (!start_reply(fds, c, BAD_REQUEST, "Wrong host or missing SNI"))
+		return;
+	goodbye(fds, c);
 }
 
 void
@@ -886,78 +906,158 @@ absolutify_path(const char *path)
 		err(1, "asprintf");
 	free(wd);
 	return r;
+}
+
+void
+yyerror(const char *msg)
+{
+	goterror = 1;
+	fprintf(stderr, "%d: %s\n", yylineno, msg);
+}
+
+int
+parse_portno(const char *p)
+{
+	char *ep;
+	long lval;
+
+	errno = 0;
+	lval = strtol(p, &ep, 10);
+	if (p[0] == '\0' || *ep != '\0')
+		errx(1, "not a number: %s", p);
+	if (lval < 0 || lval > UINT16_MAX)
+		errx(1, "port number out of range for domain %s: %ld", p, lval);
+	return lval;
 }
 
 void
+parse_conf(const char *path)
+{
+	if ((yyin = fopen(path, "r")) == NULL)
+		err(1, "cannot open config %s", path);
+	yyparse();
+	fclose(yyin);
+
+	if (goterror)
+		exit(1);
+}
+
+void
+load_vhosts(struct tls_config *tlsconf)
+{
+	struct vhost *h;
+
+	/* we need to set something, then we can add how many key we want */
+	if (tls_config_set_keypair_file(tlsconf, hosts->cert, hosts->key))
+		errx(1, "tls_config_set_keypair_file failed");
+
+	for (h = hosts; h->domain != NULL; ++h) {
+		if (tls_config_add_keypair_file(tlsconf, h->cert, h->key) == -1)
+			errx(1, "failed to load the keypair (%s, %s)",
+			    h->cert, h->key);
+
+		if ((h->dirfd = open(h->dir, O_RDONLY | O_DIRECTORY)) == -1)
+			err(1, "open %s for domain %s", h->dir, h->domain);
+	}
+}
+
+/* we can augment this function to handle also capsicum and seccomp eventually */
+void
+sandbox()
+{
+        struct vhost *h;
+	int has_cgi = 0;
+
+	for (h = hosts; h->domain != NULL; ++h) {
+		if (unveil(h->dir, "rx") == -1)
+			err(1, "unveil %s for domain %s", h->dir, h->domain);
+
+		if (h->cgi != NULL)
+			has_cgi = 1;
+	}
+
+	if (pledge("stdio rpath inet proc exec", NULL) == -1)
+		err(1, "pledge");
+
+	/* drop proc and exec if cgi isn't enabled */
+	if (!has_cgi)
+		if (pledge("stdio rpath inet", NULL) == -1)
+			err(1, "pledge");
+}
+
+void
 usage(const char *me)
 {
 	fprintf(stderr,
-	    "USAGE: %s [-h] [-c cert.pem] [-d docs] [-k key.pem] "
-	    "[-l logfile] [-p port] [-x cgi-bin]\n",
+	    "USAGE: %s [-n] [-c config] | [-6fh] [-C cert] [-d root] [-K key] "
+	    "[-p port] [-x cgi-bin]\n",
 	    me);
 }
 
 int
 main(int argc, char **argv)
 {
-	const char *cert = "cert.pem", *key = "key.pem";
 	struct tls *ctx = NULL;
-	struct tls_config *conf;
-	int sock4, sock6, enable_ipv6, ch;
-	connected_clients = 0;
+	struct tls_config *tlsconf;
+	int sock4, sock6, ch;
+	const char *config_path = NULL;
+	size_t i;
+	int conftest = 0, has_cgi = 0;
 
-	if ((dir = absolutify_path("docs")) == NULL)
-		err(1, "absolutify_path");
+	bzero(hosts, sizeof(hosts));
+	for (i = 0; i < HOSTSLEN; ++i)
+		hosts[i].dirfd = -1;
 
-	cgi = NULL;
-	port = 1965;
-	foreground = 0;
-	enable_ipv6 = 0;
+	conf.foreground = 1;
+	conf.port = 1965;
+	conf.ipv6 = 0;
 
-	while ((ch = getopt(argc, argv, "6c:d:fhk:p:x:")) != -1) {
+	connected_clients = 0;
+
+	while ((ch = getopt(argc, argv, "6C:c:d:fhK:np:x:")) != -1) {
 		switch (ch) {
 		case '6':
-			enable_ipv6 = 1;
+			conf.ipv6 = 1;
 			break;
 
+		case 'C':
+			hosts[0].cert = optarg;
+			break;
+
 		case 'c':
-			cert = optarg;
+			config_path = optarg;
 			break;
 
 		case 'd':
-			free((char*)dir);
-			if ((dir = absolutify_path(optarg)) == NULL)
+			free((char*)hosts[0].dir);
+			if ((hosts[0].dir = absolutify_path(optarg)) == NULL)
 				err(1, "absolutify_path");
 			break;
 
 		case 'f':
-			foreground = 1;
+			conf.foreground = 1;
 			break;
 
 		case 'h':
 			usage(*argv);
 			return 0;
 
-		case 'k':
-			key = optarg;
+		case 'K':
+			hosts[0].key = optarg;
 			break;
 
-		case 'p': {
-			char *ep;
-			long lval;
-
-			errno = 0;
-			lval = strtol(optarg, &ep, 10);
-			if (optarg[0] == '\0' || *ep != '\0')
-				err(1, "not a number: %s", optarg);
-			if (lval < 0 || lval > UINT16_MAX)
-				err(1, "port number out of range: %s", optarg);
-			port = lval;
+		case 'n':
+			conftest = 1;
 			break;
-		}
 
+		case 'p':
+			conf.port = parse_portno(optarg);
+
 		case 'x':
-			cgi = optarg;
+			/* drop the starting / (if any) */
+			if (*optarg == '/')
+				optarg++;
+			hosts[0].cgi = optarg;
 			break;
 
 		default:
@@ -966,6 +1066,21 @@ main(int argc, char **argv)
 		}
 	}
 
+	if (config_path != NULL) {
+		if (hosts[0].cert != NULL || hosts[0].key != NULL ||
+		    hosts[0].dir != NULL)
+			errx(1, "can't specify options in conf mode");
+		parse_conf(config_path);
+	} else {
+		if (hosts[0].cert == NULL || hosts[0].key == NULL ||
+		    hosts[0].dir == NULL)
+			errx(1, "missing cert, key or root directory to serve");
+		hosts[0].domain = "*";
+	}
+
+	if (conftest)
+                errx(0, "config OK");
+
 	signal(SIGPIPE, SIG_IGN);
 	signal(SIGCHLD, SIG_IGN);
 
@@ -974,60 +1089,46 @@ main(int argc, char **argv)
 #endif
 	signal(SIGUSR2, sig_handler);
 
-	if (!foreground)
+	if (!conf.foreground)
 		signal(SIGHUP, SIG_IGN);
 
-	if ((conf = tls_config_new()) == NULL)
+	if ((tlsconf = tls_config_new()) == NULL)
 		err(1, "tls_config_new");
 
 	/* optionally accept client certs, but don't try to verify them */
-	tls_config_verify_client_optional(conf);
-	tls_config_insecure_noverifycert(conf);
+	tls_config_verify_client_optional(tlsconf);
+	tls_config_insecure_noverifycert(tlsconf);
 
-	if (tls_config_set_protocols(conf,
+	if (tls_config_set_protocols(tlsconf,
 	    TLS_PROTOCOL_TLSv1_2 | TLS_PROTOCOL_TLSv1_3) == -1)
 		err(1, "tls_config_set_protocols");
 
-	if (tls_config_set_cert_file(conf, cert) == -1)
-		err(1, "tls_config_set_cert_file: %s", cert);
+	load_vhosts(tlsconf);
 
-	if (tls_config_set_key_file(conf, key) == -1)
-		err(1, "tls_config_set_key_file: %s", key);
-
 	if ((ctx = tls_server()) == NULL)
 		err(1, "tls_server");
 
-	if (tls_configure(ctx, conf) == -1)
+	if (tls_configure(ctx, tlsconf) == -1)
 		errx(1, "tls_configure: %s", tls_error(ctx));
 
-	sock4 = make_socket(port, AF_INET);
-	if (enable_ipv6)
-		sock6 = make_socket(port, AF_INET6);
+	sock4 = make_socket(conf.port, AF_INET);
+	if (conf.ipv6)
+		sock6 = make_socket(conf.port, AF_INET6);
 	else
 		sock6 = -1;
 
-	if ((dirfd = open(dir, O_RDONLY | O_DIRECTORY)) == -1)
-		err(1, "open: %s", dir);
 
-	if (!foreground && daemon(0, 1) == -1)
+	if (!conf.foreground && daemon(0, 1) == -1)
 		exit(1);
 
-	if (unveil(dir, "rx") == -1)
-		err(1, "unveil");
+	sandbox();
 
-	if (pledge("stdio rpath inet proc exec", NULL) == -1)
-		err(1, "pledge");
-
-	/* drop proc and exec if cgi isn't enabled */
-	if (cgi == NULL && pledge("stdio rpath inet", NULL) == -1)
-		err(1, "pledge");
-
 	loop(ctx, sock4, sock6);
 
 	close(sock4);
 	close(sock6);
 	tls_free(ctx);
-	tls_config_free(conf);
+	tls_config_free(tlsconf);
 
 	return 0;
 }
blob - 570d38cc4b4fc4d38bcdf32cd7af64ceed755f42
blob + cbf9da3a94d38b5df3a8bccbdfd732a482c719c9
--- gmid.h
+++ gmid.h
@@ -50,6 +50,27 @@
 
 #define MAX_USERS	64
 
+#define HOSTSLEN	64
+
+struct vhost {
+	const char	*domain;
+	const char	*cert;
+	const char	*key;
+	const char	*dir;
+	const char	*cgi;
+	int		 dirfd;
+};
+
+extern struct vhost hosts[HOSTSLEN];
+
+struct conf {
+	int	foreground;
+	int	port;
+	int	ipv6;
+};
+
+extern struct conf conf;
+
 enum {
 	S_HANDSHAKE,
 	S_OPEN,
@@ -70,6 +91,7 @@ struct client {
 	ssize_t		 len, off;	  /* mmap/static buffer  */
 	int		 af;
 	struct sockaddr_storage	 addr;
+	struct vhost	*host;	/* host he's talking to */
 };
 
 struct iri {
@@ -122,8 +144,20 @@ void		 do_accept(int, struct tls*, struct pollfd*, str
 void		 goodbye(struct pollfd*, struct client*);
 void		 loop(struct tls*, int, int);
 
+char		*absolutify_path(const char*);
+void		 yyerror(const char*);
+int		 parse_portno(const char*);
+void		 parse_conf(const char*);
+void		 load_vhosts(struct tls_config*);
+void		 sandbox();
+
 void		 usage(const char*);
 
+extern FILE *yyin;
+extern int yylineno;
+extern int yyparse(void);
+extern int yylex(void);
+
 /* utf8.c */
 int		 valid_multibyte_utf8(struct parser*);
 
blob - /dev/null
blob + 083c4409990aaa2c9968d4ba869d29e56870cfb5 (mode 644)
--- /dev/null
+++ lex.l
@@ -0,0 +1,78 @@
+/* -*- mode: fundamental; indent-tabs-mode: t; -*- */
+%{
+
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * 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 <err.h>
+#include <errno.h>
+
+#include "gmid.h"
+
+#include "y.tab.h"
+
+%}
+
+%x COMMENT
+%x STRING
+
+%%
+
+<INITIAL>#	BEGIN(COMMENT);
+<COMMENT>.*\n	yylineno++; BEGIN(INITIAL);
+
+<INITIAL>\"	BEGIN(STRING);
+<STRING>[^"]*\"	{
+	if ((yylval.str = strdup(yytext)) == NULL)
+		err(1, "strdup");
+	yylval.str[strlen(yylval.str)-1] = '\0'; /* remove the closing quote */
+	BEGIN(INITIAL);
+	return TSTRING;
+}
+
+[0-9]+		{
+	yylval.num = parse_portno(yytext);
+	return TNUM;
+}
+
+on		yylval.num = 1; return TBOOL;
+off		yylval.num = 0; return TBOOL;
+
+daemon		return TDAEMON;
+ipv6		return TIPV6;
+port		return TPORT;
+server		return TSERVER;
+
+cert		return TCERT;
+key		return TKEY;
+root		return TROOT;
+cgi		return TCGI;
+
+[{}]		return *yytext;
+
+\n		yylineno++;
+
+[ \t]+		;
+
+.		errx(1, "%d: unexpected character %c", yylineno, *yytext);
+
+%%
+
+int
+yywrap(void)
+{
+	return 1;
+}
blob - /dev/null
blob + 9e6b63a900c2e6992e6b3e5ac860ac1433014927 (mode 644)
--- /dev/null
+++ parse.y
@@ -0,0 +1,98 @@
+/* -*- mode: fundamental; indent-tabs-mode: t; -*- */
+%{
+
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * 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 <err.h>
+#include <stdio.h>
+
+#include "gmid.h"
+
+/*
+ * #define YYDEBUG 1
+ * int yydebug = 1;
+ */
+
+struct vhost *host = &hosts[0];
+size_t ihost = 0;
+
+extern void yyerror(const char*);
+
+%}
+
+/* for bison: */
+/* %define parse.error verbose */
+
+%union {
+	char		*str;
+	int		 num;
+}
+
+%token TBOOL TSTRING TNUM
+%token TDAEMON TIPV6 TPORT TSERVER
+%token TCERT TKEY TROOT TCGI
+%token TERR
+
+%token <str>	TSTRING
+%token <num>	TNUM
+%token <num>	TBOOL
+
+%%
+
+conf		: options vhosts ;
+
+options		: /* empty */
+		| options option
+		;
+
+option		: TDAEMON TBOOL		{ conf.foreground = !$2; }
+		| TIPV6 TBOOL		{ conf.ipv6 = $2; }
+		| TPORT TNUM		{ conf.port = $2; }
+		;
+
+vhosts		: /* empty */
+		| vhosts vhost
+		;
+
+vhost		: TSERVER TSTRING '{' servopts '}' {
+			host->domain = $2;
+
+			if (host->cert == NULL || host->key == NULL ||
+			    host->dir == NULL)
+				errx(1, "invalid vhost definition: %s", $2);
+			if (++ihost == HOSTSLEN)
+				errx(1, "too much vhosts defined");
+                        host++;
+		}
+		| error '}'		{ yyerror("error in server directive"); }
+		;
+
+servopts	: /* empty */
+		| servopts servopt
+		;
+
+servopt		: TCERT TSTRING		{ host->cert = $2; }
+		| TKEY TSTRING		{ host->key = $2; }
+		| TROOT TSTRING		{ host->dir = $2; }
+		| TCGI TSTRING		{
+			host->cgi = $2;
+			/* drop the starting '/', if any */
+			if (*host->cgi == '/')
+				host->cgi++;
+}
+		;
+