tskrtt

Simple libev based gopher server
git clone https://git.inz.fi/tskrtt/
Log | Files | Refs | README

commit 2084d129e5ac67a557f19474657511ec9cce9653
Author: Santtu Lakkala <inz@inz.fi>
Date:   Fri, 14 May 2021 15:31:30 +0300

Initial import

Diffstat:
AMakefile | 13+++++++++++++
Aarg.h | 37+++++++++++++++++++++++++++++++++++++
Amain.c | 1200+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 1250 insertions(+), 0 deletions(-)

diff --git a/Makefile b/Makefile @@ -0,0 +1,13 @@ +LDFLAGS += -lev -ltls +CFLAGS += -W -Wall -std=c99 -DUSE_TLS +SOURCES := main.c +OBJS := $(patsubst %.c,%.o,$(SOURCES)) + +all: tskrtt + +tskrtt: $(OBJS) + $(CC) $(CFLAGS) -o $@ $(OBJS) $(LDFLAGS) + + +%.o: %.c + $(CC) $(CFLAGS) -c $< -o $@ diff --git a/arg.h b/arg.h @@ -0,0 +1,37 @@ +#ifndef ARG_H +#define ARG_H + +#define USED(x) ((void)(x)) + +extern char *argv0; + +#define ARGBEGIN for(argv0 = *argv, argv++, argc--;\ + argv[0] && argv[0][0] == '-'\ + && argv[0][1];\ + argc--, argv++) {\ + char _argc;\ + char **_argv;\ + if(argv[0][1] == '-' && argv[0][2] == '\0') {\ + argv++;\ + argc--;\ + break;\ + }\ + int i_;\ + for(i_ = 1, _argv = argv; argv[0][i_];\ + i_++) {\ + if(_argv != argv)\ + break;\ + _argc = argv[0][i_];\ + switch(_argc) + +#define ARGEND }\ + USED(_argc);\ + }\ + USED(argv);\ + USED(argc); + +#define EARGF(x) ((argv[1] == NULL)? ((x), abort(), (char *)0) :\ + (argc--, argv++, argv[0])) + +#endif + diff --git a/main.c b/main.c @@ -0,0 +1,1200 @@ +#define _DEFAULT_SOURCE +#include <ev.h> +#ifndef ev_io_modify +#define ev_io_modify(ev,events_) do { (ev)->events = (ev)->events & EV__IOFDSET | (events_); } while (0) +#endif +#include <sys/socket.h> +#include <sys/mman.h> +#include <stdbool.h> +#include <netinet/in.h> +#include <netdb.h> +#include <stdlib.h> +#include <stddef.h> +#include <dirent.h> +#include <fcntl.h> +#include <errno.h> +#include <string.h> +#include <stdio.h> +#include <unistd.h> +#include <pwd.h> +#include <grp.h> +#include <fcntl.h> +#ifdef USE_TLS +#include <tls.h> +#endif + +#include "arg.h" + +char dfl_hostname[128]; +const char dfl_port[] = "70"; +const char dfl_gopherroot[] = "/var/gopher"; + +const char *hostname = dfl_hostname; +const char *gopherroot = dfl_gopherroot; +const char *oport = NULL; + +char *argv0; + +#define FOFFSET(x, y) (ptrdiff_t)(&((x *)NULL)->y) +#define PTR_FROM_FIELD(x, y, z) ((x *)((size_t)z - FOFFSET(x, y))) + +enum task { + TASK_READ, + TASK_DIR, + TASK_TXT, + TASK_GOPHERMAP, + TASK_GPH, + TASK_BINARY, + TASK_ERROR, + TASK_REDIRECT +}; + +struct dir_task { + struct dirent **entries; + char *base; + int n; + int i; +}; + +struct txt_task { + char linebuf[512]; + int rfd; + size_t used; +}; + +struct gph_task { + char linebuf[512]; + int rfd; + size_t used; + char *base; +}; + +struct binary_task { + int rfd; +}; + +struct listener { + ev_io watcher; + int fd; +#ifdef USE_TLS + struct tls *tlsctx; +#endif +}; + +#ifdef USE_TLS +enum tls_state { + UNKNOWN, + PLAIN, + HANDSHAKE, + READY +}; +#endif + +struct client { + ev_io watcher; + ev_timer timeout; + struct sockaddr_storage addr; + socklen_t addrlen; + int fd; + char buffer[2048]; + size_t buffer_used; + enum task task; + union { + struct dir_task dt; + struct txt_task tt; + struct gph_task gpht; + struct binary_task bt; + } task_data; +#ifdef USE_TLS + struct tls *tlsctx; + enum tls_state tlsstate; +#endif +}; + +static void init_dir(EV_P_ struct client *c, int fd, struct stat *sb, const char *path, const char *fn, const char *qs); +static void init_text(EV_P_ struct client *, int fd, struct stat *sb, const char *path, const char *fn, const char *qs); +static void init_gph(EV_P_ struct client *c, int fd, struct stat *sb, const char *path, const char *fn, const char *qs); +static void init_gophermap(EV_P_ struct client *c, int fd, struct stat *sb, const char *path, const char *fn, const char *qs); +static void init_binary(EV_P_ struct client *c, int fd, struct stat *sb, const char *path, const char *fn, const char *qs); +static void init_error(EV_P_ struct client *c, int fd, struct stat *sb, const char *path, const char *fn, const char *qs); +static void init_redirect(EV_P_ struct client *c, int fd, struct stat *sb, const char *path, const char *fn, const char *qs); + +static void update_read(EV_P_ struct client *c, int events); +static void update_dir(EV_P_ struct client *c, int events); +static void update_text(EV_P_ struct client *c, int events); +static void update_gophermap(EV_P_ struct client *c, int events); +static void update_gph(EV_P_ struct client *c, int events); +static void update_binary(EV_P_ struct client *c, int events); +static void update_error(EV_P_ struct client *c, int events); +static void update_redirect(EV_P_ struct client *c, int events); + +static void finish_read(EV_P_ struct client *c); +static void finish_dir(EV_P_ struct client *c); +static void finish_text(EV_P_ struct client *c); +static void finish_gophermap(EV_P_ struct client *c); +static void finish_gph(EV_P_ struct client *c); +static void finish_binary(EV_P_ struct client *c); +static void finish_error(EV_P_ struct client *c); +static void finish_redirect(EV_P_ struct client *c); + +static const struct { + void (*init)(EV_P_ struct client *c, int fd, struct stat *sb, const char *path, const char *fn, const char *qs); + void (*update)(EV_P_ struct client *c, int events); + void (*finish)(EV_P_ struct client *c); +} tasks[] = { + { NULL, update_read, finish_read }, + { init_dir, update_dir, finish_dir }, + { init_text, update_text, finish_text }, + { init_gophermap, update_gophermap, finish_gophermap }, + { init_gph, update_gph, finish_gph }, + { init_binary, update_binary, finish_binary }, + { init_error, update_error, finish_error }, + { init_redirect, update_redirect, finish_redirect }, +}; + +struct listener listen_watcher; +ev_timer timeout_watcher; + +static bool strsfx_(const char *haystack, const char *needle, size_t needlelen) +{ + size_t hsl = strlen(haystack); + + if (hsl < needlelen) + return false; + return !strncmp(haystack + hsl - needlelen, needle, needlelen); +} +#define strsfx(x, y) strsfx_(x, y, sizeof(y) - 1) + +static char *strnpfx_(const char *haystack, size_t hsl, const char *needle, size_t needlelen) +{ + if (hsl >= needlelen && !strncmp(haystack, needle, needlelen)) + return (char *)haystack + needlelen; + return NULL; +} +#define strnpfx(x, y, z) strnpfx_(x, y, z, sizeof(z) - 1) + +bool strpfx(const char *haystack, const char *needle) +{ + while (*needle && *haystack++ == *needle++); + return !*needle; +} + +static inline void *xmemdup(const void *p, size_t l) +{ + return memcpy(malloc(l), p, l); +} + +static int filterdot(const struct dirent *e) +{ + return strcmp(e->d_name, ".") && strcmp(e->d_name, ".."); +} + +static int xfdscandir(int dfd, struct dirent ***namelist, int (*filter)(const struct dirent *), int compar(const struct dirent **, const struct dirent **)) +{ + size_t n = 0; + size_t sz = 64; + DIR *d; + struct dirent *e; + + d = fdopendir(dfd); + if (!d) + return 0; + + *namelist = malloc(sz * sizeof(**namelist)); + + while ((e = readdir(d))) { + size_t nl = strlen(e->d_name); + if (filter && !filter(e)) + continue; + if (n == sz) + *namelist = realloc(*namelist, (sz *= 2) * sizeof(**namelist)); + (*namelist)[n++] = xmemdup(e, FOFFSET(struct dirent, d_name) + nl + 1); + } + closedir(d); + + *namelist = realloc(*namelist, n * sizeof(**namelist)); + qsort(*namelist, n, sizeof(**namelist), (int (*)(const void *, const void *))compar); + + return n; +} + +static char *dupensurepath(const char *w) +{ + size_t l = strlen(w); + char *rv; + if (!l) + return strdup(""); + if (w[l - 1] == '/') + l--; + rv = memcpy(malloc(l + 2), w, l); + rv[l++] = '/'; + rv[l] = '\0'; + return rv; +} + +static bool tryfileat(int *fd, const char *fn) +{ + int f = openat(*fd, fn, O_RDONLY); + + if (f < 0) + return false; + close(*fd); + *fd = f; + + return true; +} + +void guess_task(EV_P_ struct client *c, int fd, struct stat *sb, const char *path, const char *fn, const char *qs) +{ + (void)qs; + if (sb->st_mode & S_IFDIR) { + if (tryfileat(&fd, "gophermap")) { + c->task = TASK_GOPHERMAP; + sb = NULL; + } else if (tryfileat(&fd, "index.gph")) { + c->task = TASK_GPH; + sb = NULL; + } else { + c->task = TASK_DIR; + } + } else if (!strcmp(fn, "gophermap")) { + c->task = TASK_GOPHERMAP; + } else if (strsfx(fn, ".gph")) { + c->task = TASK_GPH; + } else if (strsfx(fn, ".txt")) { + c->task = TASK_TXT; + } else { + c->task = TASK_BINARY; + } + tasks[c->task].init(EV_A_ c, fd, sb, path, fn, qs); +} + +static void client_close(EV_P_ struct client *c) +{ + tasks[c->task].finish(EV_A_ c); +#ifdef USE_TLS + if (c->tlsstate > PLAIN) + tls_close(c->tlsctx); +#endif + ev_timer_stop(EV_A_ &c->timeout); + ev_io_stop(EV_A_ &c->watcher); + close(c->fd); + free(c); +} + +static int client_write(struct client *c, void *buffer, size_t n) +{ +#ifdef USE_TLS + if (c->tlsstate == READY) + return tls_write(c->tlsctx, buffer, n); +#endif + return write(c->fd, buffer, n); +} + +static bool client_flush(struct client *c) +{ + int w; + + if (!c->buffer_used) + return true; + + w = client_write(c, c->buffer, c->buffer_used); + + if ((size_t)w < c->buffer_used) { + memmove(c->buffer, c->buffer + w, c->buffer_used - w); + c->buffer_used -= w; + } else { + c->buffer_used = 0; + } + + return true; +} + + +static char *cleanup_path(char *path, char **basename, size_t *pathlen) +{ + size_t parts[512]; + size_t np = 0; + size_t w = 0; + size_t r = 0; + + while (path[r] == '/' && r < *pathlen) + r++; + + if (r == *pathlen) { + *pathlen = 0; + *basename = path; + return path; + } + + while (r < *pathlen) { + if (!path[r]) + return NULL; + if (path[r] == '/') { + if (w) + path[w++] = '/'; + do { + r++; + } while (path[r] == '/'); + continue; + } else if (path[r] == '.') { + if (r + 1 == *pathlen || path[r + 1] == '/') { + for (r++; r < *pathlen && path[r] == '/'; r++); + continue; + } else if (path[r + 1] == '.') { + if (r + 2 == *pathlen || path[r + 2] == '/') { + if (!np) + return NULL; + w = parts[--np]; + if (r + 2 == *pathlen && w) + w--; + for (r += 2; r < *pathlen && path[r] == '/'; r++); + continue; + } + } + } + parts[np++] = w; + while (r < *pathlen && path[r] && path[r] != '/') + path[w++] = path[r++]; + } + + if (np) + *basename = path + parts[np - 1]; + else + *basename = path; + + if (w && path[w - 1] == '/') + w--; + *pathlen = w; + return path; +} + +static void child_cb(EV_P_ ev_io *w, int revents) +{ + struct client *c = PTR_FROM_FIELD(struct client, watcher, w); + + if (c->task > TASK_READ) { + if (!client_flush(c)) { + client_close(EV_A_ c); + return; + } + + if (c->buffer_used) + return; + } + tasks[c->task].update(EV_A_ c, revents); +} + +static char guess_type(struct dirent *e) +{ + if (e->d_type == DT_DIR) + return '1'; + if (strsfx(e->d_name, ".txt")) + return '0'; + if (strsfx(e->d_name, ".html") || + strsfx(e->d_name, ".xhtml")) + return 'h'; + if (strsfx(e->d_name, ".gif")) + return 'g'; + if (strsfx(e->d_name, ".jpg") || + strsfx(e->d_name, ".png") || + strsfx(e->d_name, ".jpeg")) + return 'I'; + + return '9'; +} + +static void init_dir(EV_P_ struct client *c, int fd, struct stat *sb, const char *path, const char *fn, const char *qs) +{ + (void)sb; + (void)qs; + + c->task_data.dt.base = dupensurepath(path); + if (*path) { + char b[fn - path]; + memcpy(b, path, fn - path); + c->buffer_used += sprintf(c->buffer, "1..\t/%.*s\t%s\t%s\r\n", (int)(fn - path), b, hostname, oport); + } + c->task_data.dt.n = xfdscandir(fd, &c->task_data.dt.entries, filterdot, alphasort); + c->task_data.dt.i = 0; +} + +static void init_text(EV_P_ struct client *c, int fd, struct stat *sb, const char *path, const char *fn, const char *qs) +{ + (void)sb; + (void)path; + (void)fn; + (void)qs; + c->task_data.tt.rfd = fd; + c->task_data.tt.used = 0; +} + +static void init_gophermap(EV_P_ struct client *c, int fd, struct stat *sb, const char *path, const char *fn, const char *qs) +{ + (void)sb; + (void)path; + (void)fn; + (void)qs; + c->task_data.tt.rfd = fd; + c->task_data.tt.used = 0; +} + +static void init_gph(EV_P_ struct client *c, int fd, struct stat *sb, const char *path, const char *fn, const char *qs) +{ + (void)sb; + (void)path; + (void)fn; + (void)qs; + c->task_data.gpht.rfd = fd; + c->task_data.gpht.base = dupensurepath(path); + c->task_data.gpht.used = 0; +} + +static void init_binary(EV_P_ struct client *c, int fd, struct stat *sb, const char *path, const char *fn, const char *qs) +{ + int sbsz = 0; + + (void)path; + (void)fn; + (void)qs; + + getsockopt(c->fd, SOL_SOCKET, SO_SNDBUF, &sbsz, &(socklen_t){ sizeof(sbsz) }); + + c->task_data.bt.rfd = fd; + if (sb->st_size * 2 < sbsz) { + void *data = mmap(NULL, sb->st_size, PROT_READ, MAP_PRIVATE, c->task_data.bt.rfd, 0); + ssize_t wr = 0; + int w; + + while (wr < sb->st_size) { + if ((w = client_write(c, data + wr, sb->st_size - wr)) <= 0) + break; + wr += w; + } + + munmap(data, sb->st_size); + + client_close(EV_A_ c); + + return; + } +} + +static void init_error(EV_P_ struct client *c, int fd, struct stat *sb, const char *path, const char *fn, const char *qs) +{ + (void)c; + (void)fd; + (void)sb; + (void)path; + (void)fn; + (void)qs; +} + +static void init_redirect(EV_P_ struct client *c, int fd, struct stat *sb, const char *path, const char *fn, const char *qs) +{ + (void)fd; + (void)sb; + (void)path; + (void)qs; + + size_t fnl = strlen(fn); + char b[fnl + 1]; + strcpy(b, fn); + c->buffer_used = sprintf(c->buffer, + "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">\r\n" + "<html>\r\n" + " <head>\r\n" + " <title>Redirect</title>\r\n" + " <meta http-equiv=\"refresh\" content=\"0;url=%s\">\r\n" + " </head>\r\n" + " <body>\r\n" + " <p>Redirecting to <a href=\"%s\">%s</a></p>\r\n" + " </body>\r\n" + "</html>\r\n", + b, b, b); +} + +static void update_dir(EV_P_ struct client *c, int revents) +{ + size_t pos = 0; + + (void)revents; + + if (c->task_data.dt.i == c->task_data.dt.n + 1) { + client_close(EV_A_ c); + return; + } + + for (; c->task_data.dt.i < c->task_data.dt.n; c->task_data.dt.i++) { + int b = snprintf(c->buffer + pos, sizeof(c->buffer) - pos, "%c%s\t/%s%s\t%s\t%s\r\n", + guess_type(c->task_data.dt.entries[c->task_data.dt.i]), + c->task_data.dt.entries[c->task_data.dt.i]->d_name, + c->task_data.dt.base, + c->task_data.dt.entries[c->task_data.dt.i]->d_name, + hostname, oport); + if ((size_t)b > sizeof(c->buffer) - pos) { + if (pos) + break; + b = sprintf(c->buffer, "3Filename too long\r\n"); + } + pos += b; + free(c->task_data.dt.entries[c->task_data.dt.i]); + } + + if (c->task_data.dt.i == c->task_data.dt.n) { + int b = snprintf(c->buffer + pos, sizeof(c->buffer) - pos, ".\r\n"); + if ((size_t)b <= sizeof(c->buffer) - pos) { + pos += b; + c->task_data.dt.i++; + } + } + + c->buffer_used = pos; +} + +static void update_binary(EV_P_ struct client *c, int revents) +{ + int r = read(c->task_data.bt.rfd, c->buffer + c->buffer_used, sizeof(c->buffer) - c->buffer_used); + + (void)revents; + + if (r <= 0) + client_close(EV_A_ c); + + c->buffer_used += r; +} + +static void update_error(EV_P_ struct client *c, int revents) +{ + (void)revents; + client_close(EV_A_ c); +} + +static void update_redirect(EV_P_ struct client *c, int revents) +{ + (void)revents; + client_close(EV_A_ c); +} + +static void update_text(EV_P_ struct client *c, int revents) +{ + int r; + char *nl; + char *base; + int n; + + (void)revents; + + if (c->task_data.tt.rfd < 0) { + client_close(EV_A_ c); + return; + } + + r = read(c->task_data.tt.rfd, c->task_data.tt.linebuf + c->task_data.tt.used, sizeof(c->task_data.tt.linebuf) - c->task_data.tt.used); + + if (r <= 0) { + if (c->task_data.tt.used) { + if (c->task_data.tt.linebuf[c->task_data.tt.used - 1] == '\r') + c->task_data.tt.used--; + + n = snprintf(c->buffer + c->buffer_used, sizeof(c->buffer) - c->buffer_used, "%.*s\r\n", (int)c->task_data.tt.used, c->task_data.tt.linebuf); + c->buffer_used += n; + c->task_data.tt.used = 0; + } + + n = snprintf(c->buffer + c->buffer_used, sizeof(c->buffer) - c->buffer_used, ".\r\n"); + c->buffer_used += n; + + close(c->task_data.tt.rfd); + c->task_data.tt.rfd = -1; + + return; + } + + c->task_data.tt.used += r; + nl = memchr(c->task_data.tt.linebuf, '\n', c->task_data.tt.used); + + if (!nl) { + if (c->task_data.tt.used == sizeof(c->task_data.tt.linebuf)) { + c->buffer_used += sprintf(c->buffer + c->buffer_used, "%.*s\r\n", (int)c->task_data.tt.used, c->task_data.tt.linebuf); + c->task_data.tt.used = 0; + } + return; + } + + base = c->task_data.tt.linebuf; + do { + char *t = nl; + + if (t > base && t[-1] == '\r') + t--; + + if (t == base + 1 && *base == '.') + n = snprintf(c->buffer + c->buffer_used, sizeof(c->buffer) - c->buffer_used, "..\r\n"); + else + n = snprintf(c->buffer + c->buffer_used, sizeof(c->buffer) - c->buffer_used, "%.*s\r\n", (int)(t - base), base); + + if ((size_t)n > sizeof(c->buffer) - c->buffer_used) + break; + c->buffer_used += n; + + base = nl + 1; + } while ((nl = memchr(base, '\n', c->task_data.tt.used - (base - c->task_data.tt.linebuf)))); + + memmove(c->task_data.tt.linebuf, base, c->task_data.tt.used - (base - c->task_data.tt.linebuf)); + c->task_data.tt.used -= base - c->task_data.tt.linebuf; +} + +static size_t strnchrcnt(const char *haystack, char needle, size_t hsl) +{ + size_t n = 0; + while (hsl--) + n += *haystack++ == needle; + return n; +} + +static void update_gophermap(EV_P_ struct client *c, int revents) +{ + int r; + char *nl; + char *base; + int n; + + (void)revents; + + if (c->task_data.tt.rfd < 0) { + client_close(EV_A_ c); + return; + } + + r = read(c->task_data.tt.rfd, c->task_data.tt.linebuf + c->task_data.tt.used, sizeof(c->task_data.tt.linebuf) - c->task_data.tt.used); + + if (r <= 0) { + if (c->task_data.tt.used) { + if (c->task_data.tt.linebuf[c->task_data.tt.used - 1] == '\r') + c->task_data.tt.used--; + + n = snprintf(c->buffer + c->buffer_used, sizeof(c->buffer) - c->buffer_used, "%.*s\r\n", (int)c->task_data.tt.used, c->task_data.tt.linebuf); + c->buffer_used += n; + c->task_data.tt.used = 0; + } + + n = snprintf(c->buffer + c->buffer_used, sizeof(c->buffer) - c->buffer_used, ".\r\n"); + c->buffer_used += n; + + close(c->task_data.tt.rfd); + c->task_data.tt.rfd = -1; + + return; + } + + c->task_data.tt.used += r; + nl = memchr(c->task_data.tt.linebuf, '\n', c->task_data.tt.used); + + if (!nl) { + if (c->task_data.tt.used == sizeof(c->task_data.tt.linebuf)) { + c->buffer_used += sprintf(c->buffer + c->buffer_used, "%.*s\r\n", (int)c->task_data.tt.used, c->task_data.tt.linebuf); + c->task_data.tt.used = 0; + } + return; + } + + base = c->task_data.tt.linebuf; + do { + char *t = nl; + + if (t > base && t[-1] == '\r') + t--; + + if (*base == 'i' || *base == '3') + n = snprintf(c->buffer + c->buffer_used, sizeof(c->buffer) - c->buffer_used, "%.*s\t.\t.\t.\r\n", (int)(t - base), base); + else { + size_t tabcount = strnchrcnt(base, '\t', t - base); + + if (tabcount > 2) + n = snprintf(c->buffer + c->buffer_used, sizeof(c->buffer) - c->buffer_used, "%.*s\r\n", (int)(t - base), base); + else if (tabcount > 1) + n = snprintf(c->buffer + c->buffer_used, sizeof(c->buffer) - c->buffer_used, "%.*s\t70\r\n", (int)(t - base), base); + else if (tabcount) + n = snprintf(c->buffer + c->buffer_used, sizeof(c->buffer) - c->buffer_used, "%.*s\t%s\t%s\r\n", (int)(t - base), base, hostname, oport); + else + n = snprintf(c->buffer + c->buffer_used, sizeof(c->buffer) - c->buffer_used, "i%.*s\t.\t.\t.\r\n", (int)(t - base), base); + } + + if ((size_t)n > sizeof(c->buffer) - c->buffer_used) + break; + c->buffer_used += n; + + base = nl + 1; + } while ((nl = memchr(base, '\n', c->task_data.tt.used - (base - c->task_data.tt.linebuf)))); + + memmove(c->task_data.tt.linebuf, base, c->task_data.tt.used - (base - c->task_data.tt.linebuf)); + c->task_data.tt.used -= base - c->task_data.tt.linebuf; +} + +static void update_gph(EV_P_ struct client *c, int revents) +{ + int r; + char *nl; + char *base; + int n; + + (void)revents; + + if (c->task_data.gpht.rfd < 0) { + client_close(EV_A_ c); + return; + } + + r = read(c->task_data.gpht.rfd, c->task_data.gpht.linebuf + c->task_data.gpht.used, sizeof(c->task_data.gpht.linebuf) - c->task_data.gpht.used); + + if (r <= 0) { + if (c->task_data.gpht.used) { + if (c->task_data.gpht.linebuf[c->task_data.gpht.used - 1] == '\r') + c->task_data.gpht.used--; + + n = snprintf(c->buffer + c->buffer_used, sizeof(c->buffer) - c->buffer_used, "%.*s\r\n", (int)c->task_data.gpht.used, c->task_data.gpht.linebuf); + c->buffer_used += n; + c->task_data.gpht.used = 0; + } + + n = snprintf(c->buffer + c->buffer_used, sizeof(c->buffer) - c->buffer_used, ".\r\n"); + c->buffer_used += n; + + close(c->task_data.gpht.rfd); + c->task_data.gpht.rfd = -1; + + return; + } + + c->task_data.gpht.used += r; + nl = memchr(c->task_data.gpht.linebuf, '\n', c->task_data.gpht.used); + + if (!nl) { + if (c->task_data.gpht.used == sizeof(c->task_data.gpht.linebuf)) { + c->buffer_used += sprintf(c->buffer + c->buffer_used, "%.*s\r\n", (int)c->task_data.gpht.used, c->task_data.gpht.linebuf); + c->task_data.gpht.used = 0; + } + return; + } + + base = c->task_data.gpht.linebuf; + do { + char *t = nl; + + if (t > base && t[-1] == '\r') + t--; + *t = '\0'; + + if (*base != '[' || *base == 't') { + if (*base == 't') + base++; + n = snprintf(c->buffer + c->buffer_used, sizeof(c->buffer) - c->buffer_used, "i%s\t.\t.\t.\r\n", base); + } else { + const char *type = strtok(base + 1, "|"); + const char *desc = type ? strtok(NULL, "|") : NULL; + const char *resource = desc ? strtok(NULL, "|") : NULL; + const char *server = resource ? strtok(NULL, "|") : NULL; + const char *port = server ? strtok(NULL, "|") : NULL; + + if (t[-1] == ']') + *--t = '\0'; + + if (!resource) + n = snprintf(c->buffer + c->buffer_used, sizeof(c->buffer) - c->buffer_used, "3Invalid line\r\n"); + else { + if (!server || !strcmp(server, "server")) + server = hostname; + else if (!port) + port = dfl_port; + if (!port || !strcmp(port, "port")) + port = oport; + if (strpfx(resource, "URI:") || strpfx(resource, "URL:") || *resource == '/' || strcmp(server, hostname) || strcmp(port, oport)) + n = snprintf(c->buffer + c->buffer_used, sizeof(c->buffer) - c->buffer_used, "%s%s\t%s\t%s\t%s\r\n", type, desc, resource, server, port); + else + n = snprintf(c->buffer + c->buffer_used, sizeof(c->buffer) - c->buffer_used, "%s%s\t/%s%s\t%s\t%s\r\n", type, desc, c->task_data.gpht.base, resource, server, port); + } + } + + if ((size_t)n > sizeof(c->buffer) - c->buffer_used) + break; + c->buffer_used += n; + + base = nl + 1; + } while ((nl = memchr(base, '\n', c->task_data.gpht.used - (base - c->task_data.gpht.linebuf)))); + + memmove(c->task_data.gpht.linebuf, base, c->task_data.gpht.used - (base - c->task_data.gpht.linebuf)); + c->task_data.gpht.used -= base - c->task_data.gpht.linebuf; +} + +static void update_read(EV_P_ struct client *c, int revents) +{ + int r; + char *nl; + + (void)revents; + +#ifdef USE_TLS + if (c->buffer_used == 0 && !listen_watcher.tlsctx) + c->tlsstate = PLAIN; + else if (c->buffer_used == 0 && c->tlsstate == UNKNOWN) { + char byte0; + if (recv(c->fd, &byte0, 1, MSG_PEEK) < 1) { + client_close(EV_A_ c); + return; + } + + if (byte0 == 22) { + c->tlsstate = HANDSHAKE; + if (tls_accept_socket(listen_watcher.tlsctx, &c->tlsctx, c->fd) < 0) { + client_close(EV_A_ c); + return; + } + c->tlsstate = HANDSHAKE; + } else { + c->tlsstate = PLAIN; + } + } + + if (c->tlsstate == HANDSHAKE) { + switch (tls_handshake(c->tlsctx)) { + case TLS_WANT_POLLIN: + ev_io_stop(EV_A_ &c->watcher); + ev_io_modify(&c->watcher, EV_READ); + ev_io_start(EV_A_ &c->watcher); + return; + case TLS_WANT_POLLOUT: + ev_io_stop(EV_A_ &c->watcher); + ev_io_modify(&c->watcher, EV_WRITE); + ev_io_start(EV_A_ &c->watcher); + break; + case 0: + ev_io_stop(EV_A_ &c->watcher); + ev_io_modify(&c->watcher, EV_READ); + ev_io_start(EV_A_ &c->watcher); + c->tlsstate = READY; + break; + default: + client_close(EV_A_ c); + return; + } + } + + if (c->tlsstate == READY) { + switch ((r = tls_read(c->tlsctx, c->buffer + c->buffer_used, sizeof(c->buffer) - c->buffer_used))) { + case TLS_WANT_POLLIN: + ev_io_modify(&c->watcher, EV_READ); + return; + case TLS_WANT_POLLOUT: + ev_io_modify(&c->watcher, EV_WRITE); + return; + default: + break; + } + } else +#endif + r = read(c->fd, c->buffer + c->buffer_used, sizeof(c->buffer) - c->buffer_used); + + if (r <= 0) { + client_close(EV_A_ c); + return; + } + + if ((nl = memchr(c->buffer + c->buffer_used, '\n', r))) { + char *p; + char *bn; + char *qs; + const char *uri; + size_t rl; + size_t qsl = 0; + if (nl > c->buffer && nl[-1] == '\r') + nl--; + qs = memchr(c->buffer, '\t', nl - c->buffer); + + ev_io_stop(EV_A_ &c->watcher); + ev_io_modify(&c->watcher, EV_WRITE); + ev_io_start(EV_A_ &c->watcher); + + if (qs) { + rl = qs - c->buffer; + qsl = nl - ++qs; + *nl = '\0'; + } else { + rl = nl - c->buffer; + } + (void)qsl; + + if ((uri = strnpfx(c->buffer, rl, "URI:")) || (uri = strnpfx(c->buffer, rl, "URL:"))) { + c->buffer[rl] = '\0'; + c->task = TASK_REDIRECT; + tasks[c->task].init(EV_A_ c, -1, NULL, c->buffer, uri, qs); + return; + } + + p = cleanup_path(c->buffer, &bn, &rl); + if (!p) { + client_close(EV_A_ c); + return; + } + + p[rl] = '\0'; + + int dfd = open(gopherroot, O_RDONLY | O_DIRECTORY); + if (dfd >= 0) { + int ffd = openat(dfd, rl ? p : ".", O_RDONLY); + if (ffd >= 0) { + struct stat sb; + + fstat(ffd, &sb); + + c->buffer_used = 0; + guess_task(EV_A_ c, ffd, &sb, p, bn, qs); + } else { + c->buffer_used = sprintf(c->buffer, "3Resource not found\r\n.\r\n"); + c->task = TASK_ERROR; + } + close(dfd); + } else { + c->buffer_used = sprintf(c->buffer, "3Internal server error\r\n.\r\n"); + c->task = TASK_ERROR; + } + + return; + } + + c->buffer_used += r; + + if (c->buffer_used == sizeof(c->buffer)) { + c->buffer_used = sprintf(c->buffer, "3Request size too large\r\n.\r\n"); + client_close(EV_A_ c); + } +} + +static void finish_read(EV_P_ struct client *c) +{ + (void)c; +} + +static void finish_dir(EV_P_ struct client *c) +{ + for (; c->task_data.dt.i < c->task_data.dt.n; c->task_data.dt.i++) + free(c->task_data.dt.entries[c->task_data.dt.i]); + free(c->task_data.dt.entries); + free(c->task_data.dt.base); +} + +static void finish_text(EV_P_ struct client *c) +{ + if (c->task_data.tt.rfd >= 0) + close(c->task_data.tt.rfd); +} + +static void finish_gophermap(EV_P_ struct client *c) +{ + if (c->task_data.tt.rfd >= 0) + close(c->task_data.tt.rfd); +} + +static void finish_gph(EV_P_ struct client *c) +{ + if (c->task_data.tt.rfd >= 0) + close(c->task_data.tt.rfd); + free(c->task_data.gpht.base); +} + +static void finish_binary(EV_P_ struct client *c) +{ + close(c->task_data.bt.rfd); +} + +static void finish_error(EV_P_ struct client *c) +{ + (void)c; +} + +static void finish_redirect(EV_P_ struct client *c) +{ + (void)c; +} + +static void child_timeout(EV_P_ ev_timer *w, int revents) +{ + struct client *c = PTR_FROM_FIELD(struct client, timeout, w); + + (void)revents; + + client_close(EV_A_ c); +} + +static void listen_cb (EV_P_ ev_io *w, int revents) +{ + struct listener *l = (struct listener *)w; + struct client *c = malloc(sizeof(*c)); + + c->addrlen = sizeof(c->addr); + + (void)revents; + + c->fd = accept(l->fd, (struct sockaddr *)&c->addr, &c->addrlen); + + fcntl(c->fd, F_SETFL, O_NONBLOCK); + c->buffer_used = 0; + c->task = TASK_READ; +#ifdef USE_TLS + c->tlsstate = UNKNOWN; +#endif + + ev_timer_init(&c->timeout, child_timeout, 60.0, 0); + ev_timer_start(EV_A_ &c->timeout); + + ev_io_init(&c->watcher, child_cb, c->fd, EV_READ); + ev_io_start(EV_A_ &c->watcher); +} + +void usage(void) +{ + exit(1); +} + +int main (int argc, char *argv[]) +{ +#ifdef USE_TLS + struct tls_config *tlscfg; + const char *keyfile = NULL; + const char *certfile = NULL; +#endif + struct addrinfo hints = { .ai_family = AF_UNSPEC, .ai_flags = AI_PASSIVE, .ai_socktype = SOCK_STREAM }; + struct addrinfo *addrs; + struct addrinfo *ai; + struct ev_loop *loop = EV_DEFAULT; + const char *bindto = NULL; + const char *port = dfl_port; + const char *user = NULL; + const char *group = NULL; + int lfd = -1; + bool dofork = true; + + ARGBEGIN { + case '4': + hints.ai_family = AF_INET; + break; + case '6': + hints.ai_family = AF_INET6; + break; + case 'd': + dofork = false; + break; +#ifdef USE_TLS + case 't': + keyfile = EARGF(usage()); + certfile = EARGF(usage()); + break; +#endif + case 'h': + hostname = EARGF(usage()); + break; + case 'i': + bindto = EARGF(usage()); + break; + case 'b': + gopherroot = EARGF(usage()); + break; + case 'p': + port = EARGF(usage()); + break; + case 'P': + oport = EARGF(usage()); + break; + case 'u': + user = EARGF(usage()); + break; + case 'g': + group = EARGF(usage()); + break; + } ARGEND; + + if (!oport) + oport = port; + + if (hostname == dfl_hostname) { + if (bindto) + hostname = bindto; + else + gethostname(dfl_hostname, sizeof(dfl_hostname)); + } + + if (getaddrinfo(bindto, port, &hints, &addrs)) + usage(); + + for (ai = addrs; ai; ai = ai->ai_next) { + lfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); + + if (lfd < 0) + continue; + + setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int)); + setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &(int){1}, sizeof(int)); + + if (!bind(lfd, ai->ai_addr, ai->ai_addrlen)) + break; + perror("Bind failed"); + + close(lfd); + } + + freeaddrinfo(addrs); + + if (listen(lfd, 10)) { + perror("Listen failed"); + } + +#ifdef USE_TLS + if (keyfile && certfile) { + tls_init(); + listen_watcher.tlsctx = tls_server(); + tlscfg = tls_config_new(); + tls_config_set_key_file(tlscfg, keyfile); + tls_config_set_cert_file(tlscfg, certfile); + tls_configure(listen_watcher.tlsctx, tlscfg); + } else + listen_watcher.tlsctx = NULL; +#endif + + if (group) { + struct group *g = getgrnam(group); + if (!g || setgid(g->gr_gid)) + usage(); + } + + if (user) { + struct passwd *u = getpwnam(user); + if (!u || setuid(u->pw_uid)) + usage(); + } + + if (dofork) { + if (fork()) { + close(lfd); + return 0; + } + setsid(); + if (fork()) { + close(lfd); + return 0; + } + } + + listen_watcher.fd = lfd; + ev_io_init(&listen_watcher.watcher, listen_cb, lfd, EV_READ); + ev_io_start(loop, &listen_watcher.watcher); + + ev_run (loop, 0); + +#ifdef USE_TLS + if (listen_watcher.tlsctx) + tls_close(listen_watcher.tlsctx); +#endif + close(listen_watcher.fd); + + return 0; +}