commit - bd726b55be4df8535a2b200a252193649566007a
commit + 881a9dd9c2aebbf73f333dd3d8be4ce5400f717f
blob - d2e6c7b1d6c3249de0298437cdc454de069ffff9
blob + 6301b4682fee97651d2b72dfeab8a87065f65207
--- Makefile
+++ Makefile
y.tab.c: parse.y
${YACC} -b y -d parse.y
-OBJS = gmid.o iri.o utf8.o lex.yy.o y.tab.o cgi.o sandbox.o
+OBJS = gmid.o iri.o utf8.o lex.yy.o y.tab.o ex.o cgi.o sandbox.o
gmid: ${OBJS}
${CC} ${OBJS} -o gmid ${LDFLAGS}
-TAGS: gmid.c iri.c utf8.c
- -etags gmid.c iri.c utf8.c || true
+TAGS: gmid.c iri.c utf8.c ex.c cgi.c sandbox.c
+ -etags gmid.c iri.c utf8.c ex.c cgi.c sandbox.c || true
clean:
rm -f *.o lex.yy.c y.tab.c y.tab.h y.output gmid iri_test
blob - 658208fced0275052cc1b87f657a2d5675ea3f37
blob + 334315b6b01aab28576ef29e8108b4ff7c363a7c
--- README.md
+++ README.md
> dead simple, zero configuration Gemini server
-gmid is a simple and minimal Gemini server. It requires no
-configuration whatsoever so it's well suited for local development
-machines.
+gmid is a simple and minimal Gemini server. It can run without
+configuration, so it's well suited for local development, but at the
+same time has a configuration file flexible enough to meet the
+requirements of most capsules.
-Care has been taken to assure that gmid doesn't serve files outside
-the given directory, and it won't follow symlinks. Furthermore, on
-OpenBSD, gmid is also `pledge(2)`ed and `unveil(2)`ed: the set of
-pledges are `stdio rpath inet`, with the addition of `proc exec` if
-CGI scripts are enabled, while the given directory is unveiled with
-`rx`.
+gmid was initially written to serve static files, but can also
+optionally execute CGI scripts. It was also written with security in
+mind: on FreeBSD and OpenBSD is sandboxed via `capsicum(4)`and
+`pledge(2)`/`unveil(2)` respectively.
## Features
- (very) low memory footprint
- small codebase, easily hackable
- virtual hosts
- - sandboxed on OpenBSD and FreeBSD
+ - sandboxed by default on OpenBSD and FreeBSD
## Drawbacks
connection per-second you'd probably want to run multiple gmid
instances behind relayd/haproxy or a different server.
- - the sandbox on FreeBSD is **NOT** activated if CGI scripts are
- enabled: CGI script cannot be used with the way `capsicum(4)` works
-
-
## Building
gmid depends a POSIX libc and libtls. It can probably be linked
strip gmid
to enjoy your ~2.3M statically-linked gmid.
+
+
+## Architecture/Security considerations
+
+gmid is composed by two processes: a listener and an executor. The
+listener process is the only one that needs internet access and is
+sandboxed. When a CGI script needs to be executed, the executor
+(outside of the sandbox) sets up a pipe and gives one end to the
+listener, while the other is bound to the CGI script standard output.
+This way, is still possible to execute CGI scripts without restriction
+even if the presence of a sandbox.
+
+On OpenBSD, the listener process runs with the `stdio recvfd rpath
+inet` pledges and has `unveil(2)`ed only the directories that it
+serves; the executor has `stdio sendfd proc exec` as pledges.
+
+On FreeBSD, the executor process is sandboxed with `capsicum(4)`.
blob - 30f554ccbd5631180837cf6d545c069f17529de9
blob + b25aa5d7e844d05820da24098072ea9ab419f316
--- cgi.c
+++ cgi.c
#include <netdb.h>
+#include <err.h>
#include <errno.h>
#include <string.h>
#include "gmid.h"
-static inline void
-safe_setenv(const char *name, const char *val)
-{
- if (val == NULL)
- val = "";
- setenv(name, val, 1);
-}
-
/*
* the inverse of this algorithm, i.e. starting from the start of the
* path + strlen(cgi), and checking if each component, should be
start_cgi(const char *spath, const char *relpath, const char *query,
struct pollfd *fds, struct client *c)
{
- pid_t pid;
- int p[2]; /* read end, write end */
+ char addr[NI_MAXHOST];
+ const char *ruser, *cissuer, *chash;
+ int e;
- if (pipe(p) == -1)
+ e = getnameinfo((struct sockaddr*)&c->addr, sizeof(c->addr),
+ addr, sizeof(addr),
+ NULL, 0,
+ NI_NUMERICHOST);
+ if (e != 0)
goto err;
- switch (pid = fork()) {
- case -1:
- goto err;
-
- case 0: { /* child */
- char *ex, *requri, *portno;
- char addr[NI_MAXHOST];
- char *argv[] = { NULL, NULL, NULL };
- int ec;
-
- close(p[0]);
- if (dup2(p[1], 1) == -1)
- goto childerr;
-
- ec = getnameinfo((struct sockaddr*)&c->addr, sizeof(c->addr),
- addr, sizeof(addr),
- NULL, 0,
- NI_NUMERICHOST | NI_NUMERICSERV);
- if (ec != 0)
- goto childerr;
-
- if (asprintf(&portno, "%d", conf.port) == -1)
- goto childerr;
-
- if (asprintf(&ex, "%s/%s", c->host->dir, spath) == -1)
- goto childerr;
-
- if (asprintf(&requri, "%s%s%s", spath,
- *relpath == '\0' ? "" : "/", relpath) == -1)
- goto childerr;
-
- argv[0] = argv[1] = ex;
-
- /* fix the env */
- safe_setenv("GATEWAY_INTERFACE", "CGI/1.1");
- safe_setenv("SERVER_SOFTWARE", "gmid");
- safe_setenv("SERVER_PORT", portno);
-
- if (!strcmp(c->host->domain, "*"))
- safe_setenv("SERVER_NAME", c->host->domain)
-
- safe_setenv("SCRIPT_NAME", spath);
- safe_setenv("SCRIPT_EXECUTABLE", ex);
- safe_setenv("REQUEST_URI", requri);
- safe_setenv("REQUEST_RELATIVE", relpath);
- safe_setenv("QUERY_STRING", query);
- safe_setenv("REMOTE_HOST", addr);
- safe_setenv("REMOTE_ADDR", addr);
- safe_setenv("DOCUMENT_ROOT", c->host->dir);
-
- if (tls_peer_cert_provided(c->ctx)) {
- safe_setenv("AUTH_TYPE", "Certificate");
- safe_setenv("REMOTE_USER", tls_peer_cert_subject(c->ctx));
- safe_setenv("TLS_CLIENT_ISSUER", tls_peer_cert_issuer(c->ctx));
- safe_setenv("TLS_CLIENT_HASH", tls_peer_cert_hash(c->ctx));
- }
-
- execvp(ex, argv);
- goto childerr;
+ if (tls_peer_cert_provided(c->ctx)) {
+ ruser = tls_peer_cert_subject(c->ctx);
+ cissuer = tls_peer_cert_issuer(c->ctx);
+ chash = tls_peer_cert_hash(c->ctx);
+ } else {
+ ruser = NULL;
+ cissuer = NULL;
+ chash = NULL;
}
- default: /* parent */
- close(p[1]);
- close(c->fd);
- c->fd = p[0];
- c->child = pid;
- mark_nonblock(c->fd);
- c->state = S_SENDING;
- handle_cgi(fds, c);
- return 0;
- }
+ if (!send_string(exfd, spath)
+ || !send_string(exfd, relpath)
+ || !send_string(exfd, query)
+ || !send_string(exfd, addr)
+ || !send_string(exfd, ruser)
+ || !send_string(exfd, cissuer)
+ || !send_string(exfd, chash)
+ || !send_vhost(exfd, c->host))
+ goto err;
-err:
- if (!start_reply(fds, c, TEMP_FAILURE, "internal server error"))
+ close(c->fd);
+ if ((c->fd = recv_fd(exfd)) == -1) {
+ if (!start_reply(fds, c, TEMP_FAILURE, "internal server error"))
+ return 0;
+ goodbye(fds, c);
return 0;
- goodbye(fds, c);
+ }
+ c->child = 1;
+ c->state = S_SENDING;
+ cgi_poll_on_child(fds, c);
+ /* handle_cgi(fds, c); */
return 0;
-childerr:
- dprintf(p[1], "%d internal server error\r\n", TEMP_FAILURE);
- close(p[1]);
- _exit(1);
+err:
+ /* fatal("cannot talk to the executor process: %s", strerror(errno)); */
+ err(1, "cannot talk to the executor process");
}
void
blob - /dev/null
blob + 7ae4e9395827f73f056049f42a307889793ddc63 (mode 644)
--- /dev/null
+++ ex.c
+/*
+ * 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 <fcntl.h>
+#include <signal.h>
+#include <string.h>
+
+#include "gmid.h"
+
+int
+send_string(int fd, const char *str)
+{
+ ssize_t len;
+
+ if (str == NULL)
+ len = 0;
+ else
+ len = strlen(str);
+
+ if (write(fd, &len, sizeof(len)) != sizeof(len))
+ return 0;
+
+ if (len != 0)
+ if (write(fd, str, len) != len)
+ return 0;
+
+ return 1;
+}
+
+int
+recv_string(int fd, char **ret)
+{
+ ssize_t len;
+
+ if (read(fd, &len, sizeof(len)) != sizeof(len))
+ return 0;
+
+ if (len == 0) {
+ *ret = NULL;
+ return 1;
+ }
+
+ if ((*ret = calloc(1, len+1)) == NULL)
+ return 0;
+
+ if (read(fd, *ret, len) != len)
+ return 0;
+ return 1;
+}
+
+int
+send_vhost(int fd, struct vhost *vhost)
+{
+ ssize_t n;
+
+ if (vhost < hosts || vhost > hosts + HOSTSLEN)
+ return 0;
+
+ n = hosts - vhost;
+ return write(fd, &n, sizeof(n)) == sizeof(n);
+}
+
+int
+recv_vhost(int fd, struct vhost **vhost)
+{
+ ssize_t n;
+
+ if (read(fd, &n, sizeof(n)) != sizeof(n))
+ return 0;
+
+ if (n < 0 || n > HOSTSLEN)
+ return 0;
+
+ *vhost = &hosts[n];
+ if ((*vhost)->domain == NULL)
+ return 0;
+ return 1;
+}
+
+/* send d though fd. see /usr/src/usr.sbin/syslogd/privsep_fdpass.c
+ * for an example */
+int
+send_fd(int fd, int d)
+{
+ struct msghdr msg;
+ union {
+ struct cmsghdr hdr;
+ unsigned char buf[CMSG_SPACE(sizeof(int))];
+ } cmsgbuf;
+ struct cmsghdr *cmsg;
+ struct iovec vec;
+ int result = 1;
+ ssize_t n;
+
+ memset(&msg, 0, sizeof(msg));
+
+ if (d >= 0) {
+ msg.msg_control = &cmsgbuf.buf;
+ msg.msg_controllen = sizeof(cmsgbuf.buf);
+ cmsg = CMSG_FIRSTHDR(&msg);
+ cmsg->cmsg_len = CMSG_LEN(sizeof(int));
+ cmsg->cmsg_level = SOL_SOCKET;
+ cmsg->cmsg_type = SCM_RIGHTS;
+ *(int*)CMSG_DATA(cmsg) = d;
+ } else
+ result = 0;
+
+ vec.iov_base = &result;
+ vec.iov_len = sizeof(int);
+ msg.msg_iov = &vec;
+ msg.msg_iovlen = 1;
+
+ if ((n = sendmsg(fd, &msg, 0)) == -1 || n != sizeof(int)) {
+ fprintf(stderr, "sendmsg: got %zu but wanted %zu: (errno) %s",
+ n, sizeof(int), strerror(errno));
+ return 0;
+ }
+ return 1;
+}
+
+/* receive a descriptor via fd */
+int
+recv_fd(int fd)
+{
+ struct msghdr msg;
+ union {
+ struct cmsghdr hdr;
+ char buf[CMSG_SPACE(sizeof(int))];
+ } cmsgbuf;
+ struct cmsghdr *cmsg;
+ struct iovec vec;
+ ssize_t n;
+ int result;
+
+ memset(&msg, 0, sizeof(msg));
+ vec.iov_base = &result;
+ vec.iov_len = sizeof(int);
+ msg.msg_iov = &vec;
+ msg.msg_iovlen = 1;
+ msg.msg_control = &cmsgbuf.buf;
+ msg.msg_controllen = sizeof(cmsgbuf.buf);
+
+ if ((n = recvmsg(fd, &msg, 0)) != sizeof(int)) {
+ fprintf(stderr, "read %zu bytes bu wanted %zu\n", n, sizeof(int));
+ return -1;
+ }
+
+ if (result) {
+ cmsg = CMSG_FIRSTHDR(&msg);
+ if (cmsg == NULL || cmsg->cmsg_type != SCM_RIGHTS)
+ return -1;
+ return (*(int *)CMSG_DATA(cmsg));
+ } else
+ return -1;
+}
+
+static inline void
+safe_setenv(const char *name, const char *val)
+{
+ if (val == NULL)
+ val = "";
+ setenv(name, val, 1);
+}
+
+/* fd or -1 on error */
+static int
+launch_cgi(const char *spath, const char *relpath, const char *query,
+ const char *addr, const char *ruser, const char *cissuer, const char *chash,
+ struct vhost *vhost)
+{
+ int p[2]; /* read end, write end */
+
+ if (pipe2(p, O_NONBLOCK) == -1)
+ return -1;
+
+ switch (fork()) {
+ case -1:
+ return -1;
+
+ case 0: { /* child */
+ char *portno, *ex, *requri;
+ char *argv[] = { NULL, NULL, NULL };
+
+ close(p[0]);
+ if (dup2(p[1], 1) == -1)
+ goto childerr;
+
+ if (asprintf(&portno, "%d", conf.port) == -1)
+ goto childerr;
+
+ if (asprintf(&ex, "%s/%s", vhost->dir, spath) == -1)
+ goto childerr;
+
+ if (asprintf(&requri, "%s%s%s", spath,
+ (relpath != NULL && *relpath == '\0') ? "" : "/", relpath) == -1)
+ goto childerr;
+
+ argv[0] = argv[1] = ex;
+
+ safe_setenv("GATEWAY_INTERFACE", "CGI/1.1");
+ safe_setenv("SERVER_SOFTWARE", "gmid");
+ safe_setenv("SERVER_PORT", portno);
+
+ if (!strcmp(vhost->domain, "*"))
+ safe_setenv("SERVER_NAME", vhost->domain);
+
+ safe_setenv("SCRIPT_NAME", spath);
+ safe_setenv("SCRIPT_EXECUTABLE", ex);
+ safe_setenv("REQUEST_URI", requri);
+ safe_setenv("REQUEST_RELATIVE", relpath);
+ safe_setenv("QUERY_STRING", query);
+ safe_setenv("REMOTE_HOST", addr);
+ safe_setenv("REMOTE_ADDR", addr);
+ safe_setenv("DOCUMENT_ROOT", vhost->dir);
+
+ if (ruser != NULL) {
+ safe_setenv("AUTH_TYPE", "Certificate");
+ safe_setenv("REMOTE_USER", ruser);
+ safe_setenv("TLS_CLIENT_ISSUER", cissuer);
+ safe_setenv("TLS_CLIENT_HASH", chash);
+ }
+
+ execvp(ex, argv);
+ goto childerr;
+ }
+
+ default:
+ close(p[1]);
+ return p[0];
+ }
+
+childerr:
+ dprintf(p[1], "%d internal server error\r\n", TEMP_FAILURE);
+ _exit(1);
+}
+
+int
+executor_main(int fd)
+{
+ char *spath, *relpath, *query, *addr, *ruser, *cissuer, *chash;
+ struct vhost *vhost;
+ int d;
+
+#ifdef __OpenBSD__
+ pledge("stdio sendfd proc exec", NULL);
+#endif
+
+ for (;;) {
+ if (!recv_string(fd, &spath)
+ || !recv_string(fd, &relpath)
+ || !recv_string(fd, &query)
+ || !recv_string(fd, &addr)
+ || !recv_string(fd, &ruser)
+ || !recv_string(fd, &cissuer)
+ || !recv_string(fd, &chash)
+ || !recv_vhost(fd, &vhost))
+ break;
+
+ d = launch_cgi(spath, relpath, query,
+ addr, ruser, cissuer, chash, vhost);
+ if (!send_fd(fd, d))
+ break;
+
+ free(spath);
+ free(relpath);
+ free(query);
+ free(addr);
+ free(ruser);
+ free(cissuer);
+ free(chash);
+ }
+
+ /* kill all process in my group. This means the listener and
+ * every pending CGI script. */
+ kill(0, SIGINT);
+ return 1;
+}
blob - 7bd722b609bf1105d2ccb9431e3b7645685f0dd1
blob + 56250f46649c67604df41cf2164e1a96ab8de5ff
--- gmid.c
+++ gmid.c
int connected_clients;
int goterror;
+
+int exfd;
struct conf conf;
/* fallthrough */
case S_SENDING:
- if (client->child != -1)
+ if (client->child)
handle_cgi(fds, client);
else
send_file(NULL, NULL, fds, client);
clients[i].state = S_HANDSHAKE;
clients[i].fd = -1;
- clients[i].child = -1;
+ clients[i].child = 0;
+ clients[i].waiting_on_child = 0;
clients[i].buf = MAP_FAILED;
clients[i].af = AF_INET;
clients[i].addr = addr;
if ((h->dirfd = open(h->dir, O_RDONLY | O_DIRECTORY)) == -1)
err(1, "open %s for domain %s", h->dir, h->domain);
}
+}
+
+int
+listener_main()
+{
+ int sock4, sock6;
+ struct tls *ctx = NULL;
+ struct tls_config *tlsconf;
+
+ 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(tlsconf);
+ tls_config_insecure_noverifycert(tlsconf);
+
+ if (tls_config_set_protocols(tlsconf, conf.protos) == -1)
+ err(1, "tls_config_set_protocols");
+
+ if ((ctx = tls_server()) == NULL)
+ errx(1, "tls_server failure");
+
+ load_vhosts(tlsconf);
+
+ if (tls_configure(ctx, tlsconf) == -1)
+ errx(1, "tls_configure: %s", tls_error(ctx));
+
+ if (!conf.foreground && daemon(0, 1) == -1)
+ exit(1);
+
+ sock4 = make_socket(conf.port, AF_INET);
+ sock6 = -1;
+ if (conf.ipv6)
+ sock6 = make_socket(conf.port, AF_INET6);
+
+ sandbox();
+ loop(ctx, sock4, sock6);
+
+ return 0;
}
void
int
main(int argc, char **argv)
{
- struct tls *ctx = NULL;
- struct tls_config *tlsconf;
- int sock4, sock6, ch;
+ int ch, p[2];
const char *config_path = NULL;
size_t i;
int conftest = 0;
if (!conf.foreground)
signal(SIGHUP, SIG_IGN);
- if ((tlsconf = tls_config_new()) == NULL)
- err(1, "tls_config_new");
+ if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, p) == -1)
+ fatal("socketpair: %s", strerror(errno));
- /* optionally accept client certs, but don't try to verify them */
- tls_config_verify_client_optional(tlsconf);
- tls_config_insecure_noverifycert(tlsconf);
+ switch (fork()) {
+ case -1:
+ fatal("fork: %s", strerror(errno));
- if (tls_config_set_protocols(tlsconf, conf.protos) == -1)
- err(1, "tls_config_set_protocols");
+ case 0: /* child */
+ close(p[0]);
+ exfd = p[1];
+ listener_main();
+ _exit(0);
- load_vhosts(tlsconf);
-
- if ((ctx = tls_server()) == NULL)
- err(1, "tls_server");
-
- if (tls_configure(ctx, tlsconf) == -1)
- errx(1, "tls_configure: %s", tls_error(ctx));
-
- sock4 = make_socket(conf.port, AF_INET);
- if (conf.ipv6)
- sock6 = make_socket(conf.port, AF_INET6);
- else
- sock6 = -1;
-
- if (!conf.foreground && daemon(0, 1) == -1)
- exit(1);
-
- sandbox();
-
- loop(ctx, sock4, sock6);
-
- close(sock4);
- close(sock6);
- tls_free(ctx);
- tls_config_free(tlsconf);
-
- return 0;
+ default: /* parent */
+ close(p[1]);
+ return executor_main(p[0]);
+ }
}
blob - b595aabb87137c4f2958fbd36c8569e392d6fce1
blob + ed2a6f023dd1d1231cc6c03d473e21739014bd55
--- gmid.h
+++ gmid.h
};
extern struct conf conf;
+extern int exfd;
enum {
S_HANDSHAKE,
int code;
const char *meta;
int fd, waiting_on_child;
- pid_t child;
+ int child;
char sbuf[1024]; /* static buffer */
void *buf, *i; /* mmap buffer */
ssize_t len, off; /* mmap/static buffer */
void parse_conf(const char*);
void load_vhosts(struct tls_config*);
+int listener_main();
+
void usage(const char*);
/* provided by lex/yacc */
extern int yyparse(void);
extern int yylex(void);
+/* ex.c */
+int send_string(int, const char*);
+int recv_string(int, char**);
+int send_vhost(int, struct vhost*);
+int recv_vhost(int, struct vhost**);
+int send_fd(int, int);
+int recv_fd(int);
+int executor_main(int);
+
/* cgi.c */
int check_for_cgi(char *, char*, struct pollfd*, struct client*);
int start_cgi(const char*, const char*, const char*, struct pollfd*, struct client*);
blob - 6618e5dcce42d7857979866b6b22fe504b86a57c
blob + a8e301db2c554b8c7458a070d392692f8242d9f5
--- sandbox.c
+++ sandbox.c
if (h->cgi != NULL)
has_cgi = 1;
- if (has_cgi) {
- LOGW(NULL, "disabling sandbox because CGI scripts are enabled");
- return;
- }
-
if (cap_enter() == -1)
err(1, "cap_enter");
}
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)
+ if (pledge("stdio recvfd rpath inet", 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");
}
#else