gmid/parse.y
Omar Polo bfb076ed7e don't expand macros inside the quotes
Now that we have this auto concat string thingy, macros can simply
expand to standalone strings in place, as single words.

Forgot to point it out in previous commits, but now we can

	cert = "/etc/keys"

	server "foo" {
		cert $cert "/foo.crt"
		...
	}
2021-06-29 16:35:06 +00:00

789 lines
15 KiB
Plaintext

%{
/*
* 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 <ctype.h>
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "gmid.h"
FILE *yyfp;
typedef struct {
union {
char *str;
int num;
} v;
int lineno;
int colno;
} yystype;
#define YYSTYPE yystype
/*
* #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;
int used;
int persist;
char *name;
char *val;
};
struct vhost *host;
struct location *loc;
static int goterror;
static struct vhost *new_vhost(void);
static struct location *new_location(void);
void yyerror(const char*, ...);
static int yylex(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*);
int check_strip_no(int);
int check_prefork_num(int);
void advance_loc(void);
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 *);
%}
/* for bison: */
/* %define parse.error verbose */
%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
%token TERR
%token <v.str> TSTRING
%token <v.num> TNUM
%token <v.num> TBOOL
%type <v.str> string
%%
conf : /* empty */
| conf var
| conf option
| conf vhost
;
string : string TSTRING {
if (asprintf(&$$, "%s%s", $1, $2) == -1) {
free($1);
free($2);
yyerror("string: asprintf: %s", strerror(errno));
YYERROR;
}
free($1);
free($2);
}
| TSTRING
;
var : TSTRING '=' string {
char *s = $1;
while (*s++) {
if (isspace(*s)) {
yyerror("macro name cannot contain "
"whitespaces");
free($1);
free($3);
YYERROR;
}
}
symset($1, $3, 0);
free($1);
free($3);
}
;
option : TCHROOT string { conf.chroot = $2; }
| TIPV6 TBOOL { conf.ipv6 = $2; }
| TMIME TSTRING string { add_mime(&conf.mime, $2, $3); }
| TPORT TNUM { conf.port = $2; }
| TPREFORK TNUM { conf.prefork = check_prefork_num($2); }
| TPROTOCOLS string {
if (tls_config_parse_protocols(&conf.protos, $2) == -1)
yyerror("invalid protocols string \"%s\"", $2);
}
| TUSER string { conf.user = $2; }
;
vhost : TSERVER string {
host = new_vhost();
TAILQ_INSERT_HEAD(&hosts, host, vhosts);
loc = new_location();
TAILQ_INSERT_HEAD(&host->locations, loc, locations);
loc->match = xstrdup("*");
host->domain = $2;
if (strstr($2, "xn--") != NULL) {
warnx("%s:%d:%d \"%s\" looks like punycode: "
"you should use the decoded hostname.",
config_path, yylval.lineno+1, yylval.colno,
$2);
}
} '{' servopts locations '}' {
if (host->cert == NULL || host->key == NULL)
yyerror("invalid vhost definition: %s", $2);
}
| error '}' { yyerror("error in server directive"); }
;
servopts : /* empty */
| servopts servopt
;
servopt : TALIAS string {
struct alist *a;
a = xcalloc(1, sizeof(*a));
a->alias = $2;
if (TAILQ_EMPTY(&host->aliases))
TAILQ_INSERT_HEAD(&host->aliases, a, aliases);
else
TAILQ_INSERT_TAIL(&host->aliases, a, aliases);
}
| TCERT string {
only_once(host->cert, "cert");
host->cert = ensure_absolute_path($2);
}
| TCGI string {
only_once(host->cgi, "cgi");
/* drop the starting '/', if any */
if (*$2 == '/')
memmove($2, $2+1, strlen($2));
host->cgi = $2;
}
| TENTRYPOINT string {
only_once(host->entrypoint, "entrypoint");
while (*$2 == '/')
memmove($2, $2+1, strlen($2));
host->entrypoint = $2;
}
| TENV string string {
add_param($2, $3, 1);
}
| TKEY string {
only_once(host->key, "key");
host->key = ensure_absolute_path($2);
}
| TPARAM string string {
add_param($2, $3, 0);
}
| locopt
;
locations : /* empty */
| locations location
;
location : TLOCATION { advance_loc(); } string '{' locopts '}' {
/* drop the starting '/' if any */
if (*$3 == '/')
memmove($3, $3+1, strlen($3));
loc->match = $3;
}
| error '}'
;
locopts : /* empty */
| locopts locopt
;
locopt : TAUTO TINDEX TBOOL { loc->auto_index = $3 ? 1 : -1; }
| TBLOCK TRETURN TNUM string {
only_once(loc->block_fmt, "block");
loc->block_fmt = check_block_fmt($4);
loc->block_code = check_block_code($3);
}
| TBLOCK TRETURN TNUM {
only_once(loc->block_fmt, "block");
loc->block_fmt = xstrdup("temporary failure");
loc->block_code = check_block_code($3);
if ($3 >= 30 && $3 < 40)
yyerror("missing `meta' for block return %d", $3);
}
| TBLOCK {
only_once(loc->block_fmt, "block");
loc->block_fmt = xstrdup("temporary failure");
loc->block_code = 40;
}
| TDEFAULT TTYPE string {
only_once(loc->default_mime, "default type");
loc->default_mime = $3;
}
| TFASTCGI fastcgi
| TINDEX string {
only_once(loc->index, "index");
loc->index = $2;
}
| TLANG string {
only_once(loc->lang, "lang");
loc->lang = $2;
}
| TLOG TBOOL { loc->disable_log = !$2; }
| TREQUIRE TCLIENT TCA string {
only_once(loc->reqca, "require client ca");
ensure_absolute_path($4);
if ((loc->reqca = load_ca($4)) == NULL)
yyerror("couldn't load ca cert: %s", $4);
free($4);
}
| TROOT string {
only_once(loc->dir, "root");
loc->dir = ensure_absolute_path($2);
}
| TSTRIP TNUM { loc->strip = check_strip_no($2); }
;
fastcgi : TSPAWN string {
only_oncei(loc->fcgi, "fastcgi");
loc->fcgi = fastcgi_conf(NULL, NULL, $2);
}
| string {
only_oncei(loc->fcgi, "fastcgi");
loc->fcgi = fastcgi_conf($1, NULL, NULL);
}
| TTCP string TNUM {
char *c;
if (asprintf(&c, "%d", $3) == -1)
err(1, "asprintf");
only_oncei(loc->fcgi, "fastcgi");
loc->fcgi = fastcgi_conf($2, c, NULL);
}
| TTCP string {
only_oncei(loc->fcgi, "fastcgi");
loc->fcgi = fastcgi_conf($2, xstrdup("9000"), NULL);
}
| TTCP string string {
only_oncei(loc->fcgi, "fastcgi");
loc->fcgi = fastcgi_conf($2, $3, NULL);
}
;
%%
static struct vhost *
new_vhost(void)
{
return xcalloc(1, sizeof(struct vhost));
}
static struct location *
new_location(void)
{
struct location *l;
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;
} keywords[] = {
{"alias", TALIAS},
{"auto", TAUTO},
{"block", TBLOCK},
{"ca", TCA},
{"cert", TCERT},
{"cgi", TCGI},
{"chroot", TCHROOT},
{"client", TCLIENT},
{"default", TDEFAULT},
{"entrypoint", TENTRYPOINT},
{"env", TENV},
{"fastcgi", TFASTCGI},
{"index", TINDEX},
{"ipv6", TIPV6},
{"key", TKEY},
{"lang", TLANG},
{"location", TLOCATION},
{"log", TLOG},
{"mime", TMIME},
{"param", TPARAM},
{"port", TPORT},
{"prefork", TPREFORK},
{"protocols", TPROTOCOLS},
{"require", TREQUIRE},
{"return", TRETURN},
{"root", TROOT},
{"server", TSERVER},
{"spawn", TSPAWN},
{"strip", TSTRIP},
{"tcp", TTCP},
{"type", TTYPE},
{"user", TUSER},
};
/*
* Taken an adapted from doas' parse.y
*/
static int
yylex(void)
{
char buf[8096], *ebuf, *p, *str, *v, *val;
int c, quotes = 0, escape = 0, qpos = -1, nonkw = 0;
size_t i, len;
p = buf;
ebuf = buf + sizeof(buf);
repeat:
/* skip whitespace first */
for (c = getc(yyfp); isspace(c); c = getc(yyfp)) {
yylval.colno++;
if (c == '\n') {
yylval.lineno++;
yylval.colno = 0;
}
}
/* check for special one-character constructions */
switch (c) {
case '{':
case '}':
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;
break;
/* 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;
}
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;
break;
case '"':
if (!escape) {
quotes = !quotes;
if (quotes) {
nonkw = 1;
qpos = yylval.colno;
}
continue;
}
}
*p++ = c;
if (p == ebuf) {
yyerror("line too long");
p = buf;
}
escape = 0;
}
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;
}
if (!nonkw) {
for (i = 0; i < sizeof(keywords) / sizeof(keywords[0]); ++i) {
if (!strcmp(buf, keywords[i].word))
return keywords[i].token;
}
}
c = *buf;
if (!nonkw && (c == '-' || isdigit(c))) {
yylval.v.num = parse_portno(buf);
return TNUM;
}
if (!nonkw && !strcmp(buf, "on")) {
yylval.v.num = 1;
return TBOOL;
}
if (!nonkw && !strcmp(buf, "off")) {
yylval.v.num = 0;
return TBOOL;
}
if ((str = strdup(buf)) == NULL)
err(1, "%s", __func__);
yylval.v.str = str;
return TSTRING;
eof:
if (ferror(yyfp))
yyerror("input error reading config");
return 0;
}
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;
}
void
parse_conf(const char *path)
{
struct sym *sym, *next;
config_path = path;
if ((yyfp = fopen(path, "r")) == NULL)
err(1, "cannot open config: %s", path);
yyparse();
fclose(yyfp);
if (goterror)
exit(1);
if (TAILQ_FIRST(&hosts)->domain == NULL)
errx(1, "no vhost defined in %s", path);
/* free unused macros */
TAILQ_FOREACH_SAFE(sym, &symhead, entry, next) {
/* TODO: warn if !sym->used */
if (!sym->persist) {
free(sym->name);
free(sym->val);
TAILQ_REMOVE(&symhead, sym, entry);
free(sym);
}
}
}
char *
ensure_absolute_path(char *path)
{
if (path == NULL || *path != '/')
yyerror("not an absolute path: %s", path);
return path;
}
int
check_block_code(int n)
{
if (n < 10 || n >= 70 || (n >= 20 && n <= 29))
yyerror("invalid block code %d", n);
return n;
}
char *
check_block_fmt(char *fmt)
{
char *s;
for (s = fmt; *s; ++s) {
if (*s != '%')
continue;
switch (*++s) {
case '%':
case 'p':
case 'q':
case 'P':
case 'N':
break;
default:
yyerror("invalid format specifier %%%c", *s);
}
}
return fmt;
}
int
check_strip_no(int n)
{
if (n <= 0)
yyerror("invalid strip number %d", n);
return n;
}
int
check_prefork_num(int n)
{
if (n <= 0 || n >= PROC_MAX)
yyerror("invalid prefork number %d", n);
return n;
}
void
advance_loc(void)
{
loc = new_location();
TAILQ_INSERT_TAIL(&host->locations, loc, locations);
}
void
only_once(const void *ptr, const char *name)
{
if (ptr != NULL)
yyerror("`%s' specified more than once", name);
}
void
only_oncei(int i, const char *name)
{
if (i != -1)
yyerror("`%s' specified more than once", name);
}
int
fastcgi_conf(char *path, char *port, char *prog)
{
struct fcgi *f;
int i;
for (i = 0; i < FCGI_MAX; ++i) {
f = &fcgi[i];
if (f->path == NULL) {
f->id = i;
f->path = path;
f->port = port;
f->prog = prog;
return i;
}
/* XXX: what to do with prog? */
if (!strcmp(f->path, path) &&
((port == NULL && f->port == NULL) ||
!strcmp(f->port, port))) {
free(path);
free(port);
return i;
}
}
yyerror("too much `fastcgi' rules defined.");
return -1;
}
void
add_param(char *name, char *val, int env)
{
struct envlist *e;
struct envhead *h;
if (env)
h = &host->env;
else
h = &host->params;
e = xcalloc(1, sizeof(*e));
e->name = name;
e->value = val;
if (TAILQ_EMPTY(h))
TAILQ_INSERT_HEAD(h, e, envs);
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;
}