Commit Diff


commit - a5d822e542a927e88f655842273356225370c11f
commit + c39be742cf8348232f6a527b19c42f764e80aae0
blob - d60c900a549371abb7163f884116e2e8af7303ed
blob + bc44e2438232742dab541f12aadbc2b1760a7726
--- ChangeLog
+++ ChangeLog
@@ -1,3 +1,7 @@
+2021-07-09  Omar Polo  <op@omarpolo.com>
+
+	* parse.y (STRING): add `include' directive
+
 2021-07-08  Omar Polo  <op@omarpolo.com>
 
 	* parse.y (option): rename `mime MIME EXT' to `map MIME to-ext EXT', but retain the old `mime' for compatibility.
blob - 457ea98b2f921158136c1d15640307c71480ab92
blob + 8f450de756554371b618add63490d625cf99dc3a
--- parse.y
+++ parse.y
@@ -2,6 +2,13 @@
 
 /*
  * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ * Copyright (c) 2018 Florian Obser <florian@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>
+ * Copyright (c) 2001 Markus Friedl.  All rights reserved.
+ * Copyright (c) 2001 Daniel Hartmeier.  All rights reserved.
+ * Copyright (c) 2001 Theo de Raadt.  All rights reserved.
  *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
@@ -25,26 +32,38 @@
 
 #include "gmid.h"
 
-FILE *yyfp;
+TAILQ_HEAD(files, file)		 files = TAILQ_HEAD_INITIALIZER(files);
+static struct file {
+	TAILQ_ENTRY(file)	 entry;
+	FILE			*stream;
+	char			*name;
+	size_t	 		 ungetpos;
+	size_t			 ungetsize;
+	u_char			*ungetbuf;
+	int			 eof_reached;
+	int			 lineno;
+	int			 errors;
+} *file, *topfile;
 
-typedef struct {
-	union {
-		char	*str;
-		int	 num;
-	} v;
-	int lineno;
-	int colno;
-} yystype;
-#define YYSTYPE yystype
+struct file	*pushfile(const char *, int);
+int		 popfile(void);
+int		 yyparse(void);
+int		 yylex(void);
+void		 yyerror(const char *, ...)
+    __attribute__((__format__ (printf, 1, 2)))
+    __attribute__((__nonnull__ (1)));
+int		 kw_cmp(const void *, const void *);
+int		 lookup(char *);
+int		 igetc(void);
+int		 lgetc(int);
+void		 lungetc(int);
+int		 findeol(void);
 
 /*
  * #define YYDEBUG 1
  * int yydebug = 1;
  */
 
-/*
- * The idea behind this implementation of macros is from rad/parse.y
- */
 TAILQ_HEAD(symhead, sym) symhead = TAILQ_HEAD_INITIALIZER(symhead);
 struct sym {
 	TAILQ_ENTRY(sym)	 entry;
@@ -54,19 +73,12 @@ struct sym {
 	char			*val;
 };
 
-struct vhost *host;
-struct location *loc;
+int	 symset(const char *, const char *, int);
+char	*symget(const char *);
 
-static int goterror;
-
-static struct vhost	*new_vhost(void);
-static struct location	*new_location(void);
-
-void		 yyerror(const char*, ...);
-int		 kw_cmp(const void *, const void *);
-static int	 yylex(void);
+struct vhost	*new_vhost(void);
+struct location	*new_location(void);
 int		 parse_portno(const char*);
-void		 parse_conf(const char*);
 char		*ensure_absolute_path(char*);
 int		 check_block_code(int);
 char		*check_block_fmt(char*);
@@ -77,9 +89,19 @@ void		 only_once(const void*, const char*);
 void		 only_oncei(int, const char*);
 int		 fastcgi_conf(char *, char *, char *);
 void		 add_param(char *, char *, int);
-int		 symset(const char *, const char *, int);
-char		*symget(const char *);
 
+static struct vhost		*host;
+static struct location		*loc;
+static int			 errors;
+
+typedef struct {
+	union {
+		char	*string;
+		int	 number;
+	} v;
+	int lineno;
+} YYSTYPE;
+
 %}
 
 /* for bison: */
@@ -88,25 +110,47 @@ char		*symget(const char *);
 %token	TIPV6 TPORT TPROTOCOLS TMIME TDEFAULT TTYPE TCHROOT TUSER TSERVER
 %token	TPREFORK TLOCATION TCERT TKEY TROOT TCGI TENV TLANG TLOG TINDEX TAUTO
 %token	TSTRIP TBLOCK TRETURN TENTRYPOINT TREQUIRE TCLIENT TCA TALIAS TTCP
-%token	TFASTCGI TSPAWN TPARAM TMAP TTOEXT
+%token	TFASTCGI TSPAWN TPARAM TMAP TTOEXT INCLUDE TON TOFF
 
-%token	TERR
+%token	ERROR
 
-%token	<v.str>	TSTRING
-%token	<v.num>	TNUM
-%token	<v.num>	TBOOL
+%token	<v.string>	STRING
+%token	<v.number>	NUM
 
-%type	<v.str>	string
+%type	<v.number>	bool
+%type	<v.string>	string
 
 %%
 
 conf		: /* empty */
-		| conf var
-		| conf option
-		| conf vhost
+		| conf include '\n'
+		| conf '\n'
+		| conf varset '\n'
+		| conf option '\n'
+		| conf vhost '\n'
+		| conf error '\n'		{ file->errors++; }
 		;
 
-string		: string TSTRING {
+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');
+		}
+		;
+
+bool		: TON	{ $$ = 1; }
+		| TOFF	{ $$ = 0; }
+		;
+
+string		: string STRING	{
 			if (asprintf(&$$, "%s%s", $1, $2) == -1) {
 				free($1);
 				free($2);
@@ -116,13 +160,13 @@ string		: string TSTRING {
 			free($1);
 			free($2);
 		}
-		| TSTRING
+		| STRING
 		;
 
-var		: TSTRING '=' string {
+varset		: STRING '=' string		{
 			char *s = $1;
 			while (*s++) {
-				if (isspace(*s)) {
+				if (isspace((unsigned char)*s)) {
 					yyerror("macro name cannot contain "
 					    "whitespaces");
 					free($1);
@@ -137,8 +181,8 @@ var		: TSTRING '=' string {
 		;
 
 option		: TCHROOT string	{ conf.chroot = $2; }
-		| TIPV6 TBOOL		{ conf.ipv6 = $2; }
-		| TMIME TSTRING string	{
+		| TIPV6 bool		{ conf.ipv6 = $2; }
+		| TMIME STRING string	{
 			fprintf(stderr, "%s:%d: `mime MIME EXT' is deprecated and "
 			    "will be removed in a future version, "
 			    "please use the new syntax: `map MIME to-ext EXT'",
@@ -146,8 +190,8 @@ option		: TCHROOT string	{ conf.chroot = $2; }
 			add_mime(&conf.mime, $2, $3);
 		}
 		| TMAP string TTOEXT string { add_mime(&conf.mime, $2, $4); }
-		| TPORT TNUM		{ conf.port = $2; }
-		| TPREFORK TNUM		{ conf.prefork = check_prefork_num($2); }
+		| TPORT NUM		{ conf.port = $2; }
+		| TPREFORK NUM		{ conf.prefork = check_prefork_num($2); }
 		| TPROTOCOLS string {
 			if (tls_config_parse_protocols(&conf.protos, $2) == -1)
 				yyerror("invalid protocols string \"%s\"", $2);
@@ -166,13 +210,11 @@ vhost		: TSERVER string {
 			host->domain = $2;
 
 			if (strstr($2, "xn--") != NULL) {
-				warnx("%s:%d:%d \"%s\" looks like punycode: "
+				warnx("%s:%d \"%s\" looks like punycode: "
 				    "you should use the decoded hostname.",
-				    config_path, yylval.lineno+1, yylval.colno,
-				    $2);
+				    config_path, yylval.lineno+1, $2);
 			}
-		} '{' servopts locations '}' {
-
+		} '{' optnl servopts locations '}' {
 			if (host->cert == NULL || host->key == NULL)
 				yyerror("invalid vhost definition: %s", $2);
 		}
@@ -180,7 +222,7 @@ vhost		: TSERVER string {
 		;
 
 servopts	: /* empty */
-		| servopts servopt
+		| servopts servopt optnl
 		;
 
 servopt		: TALIAS string {
@@ -224,10 +266,10 @@ servopt		: TALIAS string {
 		;
 
 locations	: /* empty */
-		| locations location
+		| locations location optnl
 		;
 
-location	: TLOCATION { advance_loc(); } string '{' locopts '}'	{
+location	: TLOCATION { advance_loc(); } string '{' optnl locopts '}' {
 			/* drop the starting '/' if any */
 			if (*$3 == '/')
 				memmove($3, $3+1, strlen($3));
@@ -237,16 +279,16 @@ location	: TLOCATION { advance_loc(); } string '{' loc
 		;
 
 locopts		: /* empty */
-		| locopts locopt
+		| locopts locopt optnl
 		;
 
-locopt		: TAUTO TINDEX TBOOL	{ loc->auto_index = $3 ? 1 : -1; }
-		| TBLOCK TRETURN TNUM string {
+locopt		: TAUTO TINDEX bool	{ loc->auto_index = $3 ? 1 : -1; }
+		| TBLOCK TRETURN NUM string {
 			only_once(loc->block_fmt, "block");
 			loc->block_fmt = check_block_fmt($4);
 			loc->block_code = check_block_code($3);
 		}
-		| TBLOCK TRETURN TNUM {
+		| TBLOCK TRETURN NUM {
 			only_once(loc->block_fmt, "block");
 			loc->block_fmt = xstrdup("temporary failure");
 			loc->block_code = check_block_code($3);
@@ -271,7 +313,7 @@ locopt		: TAUTO TINDEX TBOOL	{ loc->auto_index = $3 ? 
 			only_once(loc->lang, "lang");
 			loc->lang = $2;
 		}
-		| TLOG TBOOL	{ loc->disable_log = !$2; }
+		| TLOG bool	{ loc->disable_log = !$2; }
 		| TREQUIRE TCLIENT TCA string {
 			only_once(loc->reqca, "require client ca");
 			ensure_absolute_path($4);
@@ -283,7 +325,7 @@ locopt		: TAUTO TINDEX TBOOL	{ loc->auto_index = $3 ? 
 			only_once(loc->dir, "root");
 			loc->dir  = ensure_absolute_path($2);
 		}
-		| TSTRIP TNUM		{ loc->strip = check_strip_no($2); }
+		| TSTRIP NUM		{ loc->strip = check_strip_no($2); }
 		;
 
 fastcgi		: TSPAWN string {
@@ -294,7 +336,7 @@ fastcgi		: TSPAWN string {
 			only_oncei(loc->fcgi, "fastcgi");
 			loc->fcgi = fastcgi_conf($1, NULL, NULL);
 		}
-		| TTCP string TPORT TNUM {
+		| TTCP string TPORT NUM {
 			char *c;
 			if (asprintf(&c, "%d", $4) == -1)
 				err(1, "asprintf");
@@ -311,39 +353,12 @@ fastcgi		: TSPAWN string {
 		}
 		;
 
-%%
-
-static struct vhost *
-new_vhost(void)
-{
-	return xcalloc(1, sizeof(struct vhost));
-}
-
-static struct location *
-new_location(void)
-{
-	struct location *l;
+optnl		: '\n' optnl		/* zero or more newlines */
+		| /*empty*/
+		;
 
-	l = xcalloc(1, sizeof(*l));
-	l->dirfd = -1;
-	l->fcgi = -1;
-	return l;
-}
+%%
 
-void
-yyerror(const char *msg, ...)
-{
-	va_list ap;
-
-	goterror = 1;
-
-	va_start(ap, msg);
-	fprintf(stderr, "%s:%d: ", config_path, yylval.lineno);
-	vfprintf(stderr, msg, ap);
-	fprintf(stderr, "\n");
-	va_end(ap);
-}
-
 static struct keyword {
 	const char *word;
 	int token;
@@ -369,6 +384,8 @@ static struct keyword {
 	{"log", TLOG},
 	{"map", TMAP},
 	{"mime", TMIME},
+	{"off", TOFF},
+	{"on", TON},
 	{"param", TPARAM},
 	{"port", TPORT},
 	{"prefork", TPREFORK},
@@ -385,227 +402,352 @@ static struct keyword {
 	{"user", TUSER},
 };
 
+void
+yyerror(const char *msg, ...)
+{
+	va_list ap;
+
+	file->errors++;
+
+	va_start(ap, msg);
+	fprintf(stderr, "%s:%d: ", config_path, yylval.lineno);
+	vfprintf(stderr, msg, ap);
+	fprintf(stderr, "\n");
+	va_end(ap);
+}
+
 int
 kw_cmp(const void *k, const void *e)
 {
 	return strcmp(k, ((struct keyword *)e)->word);
 }
 
-/*
- * Taken an adapted from doas' parse.y
- */
-static int
-yylex(void)
+int
+lookup(char *s)
 {
-	struct keyword *kw;
-	char buf[8096], *ebuf, *p, *str, *v, *val;
-	int c, quotes = 0, escape = 0, qpos = -1, nonkw = 0;
-	size_t len;
+	const struct keyword	*p;
 
-	p = buf;
-	ebuf = buf + sizeof(buf);
+	p = bsearch(s, keywords, sizeof(keywords)/sizeof(keywords[0]),
+	    sizeof(keywords[0]), kw_cmp);
 
-repeat:
-	/* skip whitespace first */
-	for (c = getc(yyfp); isspace(c); c = getc(yyfp)) {
-		yylval.colno++;
-		if (c == '\n') {
-			yylval.lineno++;
-			yylval.colno = 0;
-		}
+	if (p)
+		return p->token;
+	else
+		return STRING;
+}
+
+#define START_EXPAND	1
+#define DONE_EXPAND	2
+
+static int	expanding;
+
+int
+igetc(void)
+{
+	int	c;
+
+	while (1) {
+		if (file->ungetpos > 0)
+			c = file->ungetbuf[--file->ungetpos];
+		else
+			c = getc(file->stream);
+
+		if (c == START_EXPAND)
+			expanding = 1;
+		else if (c == DONE_EXPAND)
+			expanding = 0;
+		else
+			break;
 	}
+	return c;
+}
 
-	/* check for special one-character constructions */
-	switch (c) {
-	case '{':
-	case '}':
+int
+lgetc(int quotec)
+{
+	int		c, next;
+
+	if (quotec) {
+		if ((c = igetc()) == EOF) {
+			yyerror("reached end of file while parsing "
+			    "quoted string");
+			if (file == topfile || popfile() == EOF)
+				return EOF;
+			return quotec;
+		}
 		return c;
-	case '#':
-		/* skip comments; NUL is allowed; no continuation */
-		while ((c = getc(yyfp)) != '\n')
-			if (c == EOF)
-				goto eof;
-		yylval.colno = 0;
-		yylval.lineno++;
-		goto repeat;
-	case '=':
-		return c;
-	case EOF:
-		goto eof;
 	}
 
-	/* parsing next word */
-	for (;; c = getc(yyfp), yylval.colno++) {
-		switch (c) {
-		case '\0':
-			yyerror("unallowed character NULL in column %d",
-			    yylval.colno+1);
-			escape = 0;
-			continue;
-		case '\\':
-			escape = !escape;
-			if (escape)
-				continue;
+	while ((c = igetc()) == '\\') {
+		next = igetc();
+		if (next != '\n') {
+			c = next;
 			break;
+		}
+		yylval.lineno = file->lineno;
+		file->lineno++;
+	}
 
-		/* expand macros in-place */
-		case '$':
-			if (!escape && !quotes) {
-				v = p;
-				while (1) {
-					if ((c = getc(yyfp)) == EOF) {
-						yyerror("EOF during macro expansion");
-                                                return 0;
-					}
-					if (p + 1 >= ebuf - 1) {
-						yyerror("string too long");
-						return 0;
-					}
-					if (isalnum(c) || c == '_') {
-						*p++ = c;
-						continue;
-					}
-					*p = 0;
-					break;
-				}
-				p = v;
-				if ((val = symget(p)) == NULL) {
-					yyerror("macro '%s' not defined", v);
-					return TERR;
-				}
-				len = strlen(val);
-				if (p + len >= ebuf - 1) {
-					yyerror("after macro-expansion, "
-					    "string too long");
-					return TERR;
-				}
-				*p = '\0';
-				strlcat(p, val, ebuf - p);
-				p += len;
-				nonkw = 1;
-				goto eow;
-			}
+	if (c == EOF) {
+		/*
+		 * Fake EOL when hit EOF for the first time. This gets line
+		 * count right if last line in included file is syntactically
+		 * invalid and has no newline.
+		 */
+		if (file->eof_reached == 0) {
+			file->eof_reached = 1;
+			return '\n';
+		}
+		while (c == EOF) {
+			if (file == topfile || popfile() == EOF)
+				return EOF;
+			c = igetc();
+		}
+	}
+	return c;
+}
+
+void
+lungetc(int c)
+{
+	if (c == EOF)
+		return;
+
+	if (file->ungetpos >= file->ungetsize) {
+		void *p = reallocarray(file->ungetbuf, file->ungetsize, 2);
+		if (p == NULL)
+			err(1, "lungetc");
+		file->ungetbuf = p;
+		file->ungetsize *= 2;
+	}
+	file->ungetbuf[file->ungetpos++] = c;
+}
+
+int
+findeol(void)
+{
+	int	c;
+
+	/* Skip to either EOF or the first real EOL. */
+	while (1) {
+		c = lgetc(0);
+		if (c == '\n') {
+			file->lineno++;
 			break;
-		case '\n':
-			if (quotes)
-				yyerror("unterminated quotes in column %d",
-				    yylval.colno+1);
-			if (escape) {
-				nonkw = 1;
-				escape = 0;
-				yylval.colno = 0;
-				yylval.lineno++;
-			}
-			goto eow;
-		case EOF:
-			if (escape)
-				yyerror("unterminated escape in column %d",
-				    yylval.colno);
-			if (quotes)
-				yyerror("unterminated quotes in column %d",
-				    qpos+1);
-			goto eow;
-		case '{':
-		case '}':
-		case '#':
-		case ' ':
-		case '\t':
-                        if (!escape && !quotes)
-				goto eow;
+		}
+		if (c == EOF)
 			break;
-		case '"':
-			if (!escape) {
-				quotes = !quotes;
-				if (quotes) {
-					nonkw = 1;
-					qpos = yylval.colno;
-				}
+	}
+	return ERROR;
+}
+
+int
+yylex(void)
+{
+	unsigned char	 buf[8096];
+	unsigned char	*p, *val;
+	int		 quotec, next, c;
+	int		 token;
+
+top:
+	p = buf;
+	while ((c = lgetc(0)) == ' ' || c == '\t')
+		; /* nothing */
+
+	yylval.lineno = file->lineno;
+	if (c == '#')
+		while ((c = lgetc(0)) != '\n' && c != EOF)
+			; /* nothing */
+	if (c == '$' && !expanding) {
+		while (1) {
+			if ((c = lgetc(0)) == EOF)
+				return 0;
+
+			if (p + 1 >= buf + sizeof(buf) - 1) {
+				yyerror("string too long");
+				return findeol();
+			}
+			if (isalnum(c) || c == '_') {
+				*p++ = c;
 				continue;
 			}
+			*p = '\0';
+			lungetc(c);
+			break;
 		}
-		*p++ = c;
-		if (p == ebuf) {
-			yyerror("line too long");
-			p = buf;
+		val = symget(buf);
+		if (val == NULL) {
+			yyerror("macro '%s' not defined", buf);
+			return findeol();
 		}
-		escape = 0;
+		p = val + strlen(val) - 1;
+		lungetc(DONE_EXPAND);
+		while (p >= val) {
+			lungetc(*p);
+			p--;
+		}
+		lungetc(START_EXPAND);
+		goto top;
 	}
 
-eow:
-	*p = 0;
-	if (c != EOF)
-		ungetc(c, yyfp);
-	if (p == buf) {
-		/*
-		 * There could be a number of reason for empty buffer,
-		 * and we handle all of them here, to avoid cluttering
-		 * the main loop.
-		 */
-		if (c == EOF)
-			goto eof;
-		else if (qpos == -1) /* accept, e.g., empty args: cmd foo args "" */
-			goto repeat;
+	switch (c) {
+	case '\'':
+	case '"':
+		quotec = c;
+		while (1) {
+			if ((c = lgetc(quotec)) == EOF)
+				return 0;
+			if (c == '\n') {
+				file->lineno++;
+				continue;
+			} else if (c == '\\') {
+				if ((next = lgetc(quotec)) == EOF)
+					return (0);
+				if (next == quotec || next == ' ' ||
+				    next == '\t')
+					c = next;
+				else if (next == '\n') {
+					file->lineno++;
+					continue;
+				} else
+					lungetc(next);
+			} else if (c == quotec) {
+				*p = '\0';
+				break;
+			} else if (c == '\0') {
+				yyerror("syntax error");
+				return findeol();
+			}
+			if (p + 1 >= buf + sizeof(buf) - 1) {
+				yyerror("string too long");
+				return findeol();
+			}
+			*p++ = c;
+		}
+		yylval.v.string = strdup(buf);
+		if (yylval.v.string == NULL)
+			err(1, "yylex: strdup");
+		return STRING;
 	}
-	if (!nonkw) {
-		kw = bsearch(buf, keywords, sizeof(keywords)/sizeof(keywords[0]),
-		    sizeof(keywords[0]), kw_cmp);
-		if (kw != NULL)
-			return kw->token;
+
+#define allowed_to_end_number(x) \
+	(isspace(x) || x == ')' || x ==',' || x == '/' || x == '}' || x == '=')
+
+	if (c == '-' || isdigit(c)) {
+		do {
+			*p++ = c;
+			if ((size_t)(p-buf) >= sizeof(buf)) {
+				yyerror("string too long");
+				return findeol();
+			}
+		} while ((c = lgetc(0)) != EOF && isdigit(c));
+		lungetc(c);
+		if (p == buf + 1 && buf[0] == '-')
+			goto nodigits;
+		if (c == EOF || allowed_to_end_number(c)) {
+			const char *errstr = NULL;
+
+			*p = '\0';
+			yylval.v.number = strtonum(buf, LLONG_MIN,
+			    LLONG_MAX, &errstr);
+			if (errstr) {
+				yyerror("\"%s\" invalid number: %s",
+				    buf, errstr);
+				return findeol();
+			}
+			return NUM;
+		} else {
+nodigits:
+			while (p > buf + 1)
+				lungetc(*--p);
+			c = *--p;
+			if (c == '-')
+				return c;
+		}
 	}
-	c = *buf;
-	if (!nonkw && (c == '-' || isdigit(c))) {
-		yylval.v.num = parse_portno(buf);
-		return TNUM;
+
+#define allowed_in_string(x) \
+	(isalnum(x) || (ispunct(x) && x != '(' && x != ')' && \
+	x != '{' && x != '}' && \
+	x != '!' && x != '=' && x != '#' && \
+	x != ','))
+
+	if (isalnum(c) || c == ':' || c == '_' || c == '/' || c == '.') {
+		do {
+			*p++ = c;
+			if ((size_t)(p-buf) >= sizeof(buf)) {
+				yyerror("string too long");
+				return findeol();
+			}
+		} while ((c = lgetc(0)) != EOF && (allowed_in_string(c)));
+		lungetc(c);
+		*p = '\0';
+		if ((token = lookup(buf)) == STRING)
+			yylval.v.string = xstrdup(buf);
+		return token;
 	}
-	if (!nonkw && !strcmp(buf, "on")) {
-		yylval.v.num = 1;
-		return TBOOL;
+	if (c == '\n') {
+		yylval.lineno = file->lineno;
+		file->lineno++;
 	}
-	if (!nonkw && !strcmp(buf, "off")) {
-		yylval.v.num = 0;
-		return TBOOL;
+	if (c == EOF)
+		return 0;
+	return c;
+}
+
+struct file *
+pushfile(const char *name, int secret)
+{
+	struct file	*nfile;
+
+	nfile = xcalloc(1, sizeof(*nfile));
+	nfile->name = xstrdup(name);
+	if ((nfile->stream = fopen(nfile->name, "r")) == NULL) {
+		yyerror("can't open %s: %s", nfile->name,
+		    strerror(errno));
+		free(nfile->name);
+		free(nfile);
+		return NULL;
 	}
-	if ((str = strdup(buf)) == NULL)
-		err(1, "%s", __func__);
-	yylval.v.str = str;
-	return TSTRING;
+	nfile->lineno = TAILQ_EMPTY(&files) ? 1 : 0;
+	nfile->ungetsize = 16;
+	nfile->ungetbuf = xcalloc(1, nfile->ungetsize);
+	TAILQ_INSERT_TAIL(&files, nfile, entry);
+	return nfile;
+}
 
-eof:
-	if (ferror(yyfp))
-		yyerror("input error reading config");
-	return 0;
-}
+int
+popfile(void)
+{
+	struct file	*prev;
 
-int
-parse_portno(const char *p)
-{
-	const char *errstr;
-	int n;
+	if ((prev = TAILQ_PREV(file, files, entry)) != NULL)
+		prev->errors += file->errors;
 
-	n = strtonum(p, 0, UINT16_MAX, &errstr);
-	if (errstr != NULL)
-		yyerror("port number is %s: %s", errstr, p);
-	return n;
+	TAILQ_REMOVE(&files, file, entry);
+	fclose(file->stream);
+	free(file->name);
+	free(file->ungetbuf);
+	free(file);
+	file = prev;
+	return file ? 0 : EOF;
 }
 
 void
-parse_conf(const char *path)
+parse_conf(const char *filename)
 {
-	struct sym	*sym, *next;
+	struct sym		*sym, *next;
 
-	config_path = path;
-	if ((yyfp = fopen(path, "r")) == NULL)
-		err(1, "cannot open config: %s", path);
+	file = pushfile(filename, 0);
+	if (file == NULL)
+		return;
+	topfile = file;
+
 	yyparse();
-	fclose(yyfp);
+	errors = file->errors;
+	popfile();
 
-	if (goterror)
-		exit(1);
-
-	if (TAILQ_FIRST(&hosts)->domain == NULL)
-		errx(1, "no vhost defined in %s", path);
-
-	/* free unused macros */
+	/* Free macros and check which have not been used. */
 	TAILQ_FOREACH_SAFE(sym, &symhead, entry, next) {
 		/* TODO: warn if !sym->used */
 		if (!sym->persist) {
@@ -615,9 +757,101 @@ parse_conf(const char *path)
 			free(sym);
 		}
 	}
+
+	if (errors)
+		exit(1);
 }
 
+int
+symset(const char *name, const char *val, int persist)
+{
+	struct sym *sym;
+
+	TAILQ_FOREACH(sym, &symhead, entry) {
+		if (!strcmp(name, sym->name))
+			break;
+	}
+
+	if (sym != NULL) {
+		if (sym->persist)
+			return 0;
+		else {
+			free(sym->name);
+			free(sym->val);
+			TAILQ_REMOVE(&symhead, sym, entry);
+			free(sym);
+		}
+	}
+
+	sym = xcalloc(1, sizeof(*sym));
+	sym->name = xstrdup(name);
+	sym->val = xstrdup(val);
+	sym->used = 0;
+	sym->persist = persist;
+
+	TAILQ_INSERT_TAIL(&symhead, sym, entry);
+	return 0;
+}
+
+int
+cmdline_symset(char *s)
+{
+	char	*sym, *val;
+	int	 ret;
+
+	if ((val = strrchr(s, '=')) == NULL)
+		return -1;
+	sym = xcalloc(1, val - s + 1);
+	memcpy(sym, s, val - s);
+	ret = symset(sym, val + 1, 1);
+	free(sym);
+	return ret;
+}
+
 char *
+symget(const char *nam)
+{
+	struct sym	*sym;
+
+	TAILQ_FOREACH(sym, &symhead, entry) {
+		if (strcmp(nam, sym->name) == 0) {
+			sym->used = 1;
+			return sym->val;
+		}
+	}
+	return NULL;
+}
+
+struct vhost *
+new_vhost(void)
+{
+	return xcalloc(1, sizeof(struct vhost));
+}
+
+struct location *
+new_location(void)
+{
+	struct location *l;
+
+	l = xcalloc(1, sizeof(*l));
+	l->dirfd = -1;
+	l->fcgi = -1;
+	return l;
+}
+
+int
+parse_portno(const char *p)
+{
+	const char *errstr;
+	int n;
+
+	n = strtonum(p, 0, UINT16_MAX, &errstr);
+	if (errstr != NULL)
+		yyerror("port number is %s: %s", errstr, p);
+	return n;
+}
+
+char *
 ensure_absolute_path(char *path)
 {
 	if (path == NULL || *path != '/')
@@ -743,64 +977,3 @@ add_param(char *name, char *val, int env)
 	else
 		TAILQ_INSERT_TAIL(h, e, envs);
 }
-
-int
-symset(const char *name, const char *val, int persist)
-{
-	struct sym *sym;
-
-	TAILQ_FOREACH(sym, &symhead, entry) {
-		if (!strcmp(name, sym->name))
-			break;
-	}
-
-	if (sym != NULL) {
-		if (sym->persist)
-			return 0;
-		else {
-			free(sym->name);
-			free(sym->val);
-			TAILQ_REMOVE(&symhead, sym, entry);
-			free(sym);
-		}
-	}
-
-        sym = xcalloc(1, sizeof(*sym));
-	sym->name = xstrdup(name);
-	sym->val = xstrdup(val);
-	sym->used = 0;
-	sym->persist = persist;
-
-	TAILQ_INSERT_TAIL(&symhead, sym, entry);
-	return 0;
-}
-
-int
-cmdline_symset(char *s)
-{
-	char	*sym, *val;
-	int	 ret;
-
-	if ((val = strrchr(s, '=')) == NULL)
-		return -1;
-	sym = xcalloc(1, val - s + 1);
-	memcpy(sym, s, val - s);
-	ret = symset(sym, val + 1, 1);
-	free(sym);
-	return ret;
-}
-
-char *
-symget(const char *name)
-{
-	struct sym	*sym;
-
-	TAILQ_FOREACH(sym, &symhead, entry) {
-		if (!strcmp(name, sym->name)) {
-			sym->used = 1;
-			return sym->val;
-		}
-	}
-
-	return NULL;
-}