commit 15902770073dd67df3a9af0f6da7d63bfb031d72 from: Omar Polo date: Fri Jan 15 09:17:43 2021 UTC conf & vhosts * 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 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 + + * 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 * 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 + * + * 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 +#include + +#include "gmid.h" + +#include "y.tab.h" + +%} + +%x COMMENT +%x STRING + +%% + +# BEGIN(COMMENT); +.*\n yylineno++; BEGIN(INITIAL); + +\" BEGIN(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 + * + * 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 +#include + +#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 TSTRING +%token TNUM +%token 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++; +} + ; +