new I/O handling on top of bufferevents

This is a big change in how gmid handles I/O.  Initially we used a
hand-written loop over poll(2), that then was evolved into something
powered by libevent basic API.  This meant that there were a lot of
small "asynchronous" function that did one step, eventually scheduling
the re-execution, that called each others in a chain.

The new implementation revolves completely around libevent'
bufferevents.  It's more clear, as everything is implemented around the
client_read and client_write functions.

There is still space for improvements, like adding timeouts for one, but
it's solid enough to be committed as is and then further improved.
This commit is contained in:
Omar Polo 2021-10-02 17:20:56 +00:00
parent 403c422041
commit efe7d18029
3 changed files with 494 additions and 440 deletions

83
fcgi.c
View File

@ -223,16 +223,18 @@ fcgi_end_param(struct bufferevent *bev, int id)
return 0;
}
static int
fcgi_abort_request(struct bufferevent *bev, int id)
void
fcgi_abort_request(struct client *c)
{
struct fcgi *f;
struct fcgi_header h;
prepare_header(&h, FCGI_ABORT_REQUEST, id, 0, 0);
if (bufferevent_write(bev, &h, sizeof(h)) == -1)
return -1;
f = &fcgi[c->fcgi];
return 0;
prepare_header(&h, FCGI_ABORT_REQUEST, c->id, 0, 0);
if (bufferevent_write(f->bev, &h, sizeof(h)) == -1)
fcgi_close_backend(f);
}
static inline int
@ -247,55 +249,6 @@ reclen(struct fcgi_header *h)
return h->content_len0 + (h->content_len1 << 8);
}
static void
copy_mbuf(int fd, short ev, void *d)
{
struct client *c = d;
struct mbuf *mbuf;
size_t len;
ssize_t r;
char *data;
for (;;) {
mbuf = TAILQ_FIRST(&c->mbufhead);
if (mbuf == NULL)
break;
len = mbuf->len - mbuf->off;
data = mbuf->data + mbuf->off;
switch (r = tls_write(c->ctx, data, len)) {
case -1:
/*
* Can't close_conn here. The application
* needs to be informed first, otherwise it
* can interfere with future connections.
* Check also that we're not doing recursion
* (copy_mbuf -> handle_fcgi -> copy_mbuf ...)
*/
if (c->next != NULL)
goto end;
fcgi_abort_request(0, c->id);
return;
case TLS_WANT_POLLIN:
event_once(c->fd, EV_READ, &copy_mbuf, c, NULL);
return;
case TLS_WANT_POLLOUT:
event_once(c->fd, EV_WRITE, &copy_mbuf, c, NULL);
return;
}
mbuf->off += r;
if (mbuf->off == mbuf->len) {
TAILQ_REMOVE(&c->mbufhead, mbuf, mbufs);
free(mbuf);
}
}
end:
if (c->next != NULL)
c->next(0, 0, c);
}
void
fcgi_close_backend(struct fcgi *f)
{
@ -315,7 +268,6 @@ fcgi_read(struct bufferevent *bev, void *d)
struct fcgi_header hdr;
struct fcgi_end_req_body end;
struct client *c;
struct mbuf *mbuf;
size_t len;
#if DEBUG_FCGI
@ -372,8 +324,8 @@ fcgi_read(struct bufferevent *bev, void *d)
/* TODO: do something with the status? */
fcgi->pending--;
c->fcgi = -1;
c->next = close_conn;
event_once(c->fd, EV_WRITE, &copy_mbuf, c, NULL);
c->type = REQUEST_DONE;
client_write(c->bev, c);
break;
case FCGI_STDERR:
@ -382,16 +334,7 @@ fcgi_read(struct bufferevent *bev, void *d)
break;
case FCGI_STDOUT:
if ((mbuf = calloc(1, sizeof(*mbuf) + len)) == NULL)
fatal("calloc");
mbuf->len = len;
bufferevent_read(bev, mbuf->data, len);
if (TAILQ_EMPTY(&c->mbufhead))
event_once(c->fd, EV_WRITE, &copy_mbuf,
c, NULL);
TAILQ_INSERT_TAIL(&c->mbufhead, mbuf, mbufs);
bufferevent_write_buffer(c->bev, EVBUFFER_INPUT(bev));
break;
default:
@ -439,7 +382,7 @@ fcgi_error(struct bufferevent *bev, short err, void *d)
continue;
if (c->code != 0)
close_conn(0, 0, 0);
client_close(c);
else
start_reply(c, CGI_ERROR, "CGI error");
}
@ -466,8 +409,6 @@ fcgi_req(struct fcgi *f, struct client *c)
fatal("getnameinfo failed: %s (%s)",
gai_strerror(e), strerror(errno));
c->next = NULL;
fcgi_begin_request(f->bev, c->id);
fcgi_send_param(f->bev, c->id, "GATEWAY_INTERFACE", "CGI/1.1");
fcgi_send_param(f->bev, c->id, "GEMINI_URL_PATH", c->iri.path);

69
gmid.h
View File

@ -185,60 +185,33 @@ struct parser {
const char *err;
};
struct mbuf {
size_t len;
size_t off;
TAILQ_ENTRY(mbuf) mbufs;
char data[];
};
TAILQ_HEAD(mbufhead, mbuf);
typedef void (imsg_handlerfn)(struct imsgbuf*, struct imsg*, size_t);
typedef void (*statefn)(int, short, void*);
enum {
REQUEST_UNDECIDED,
REQUEST_FILE,
REQUEST_DIR,
REQUEST_CGI,
REQUEST_FCGI,
REQUEST_DONE,
};
#define IS_INTERNAL_REQUEST(x) ((x) != REQUEST_CGI && (x) != REQUEST_FCGI)
/*
* DFA: handle_handshake is the initial state, close_conn the final.
* Sometimes we have an enter_* function to handle the state switch.
*
* handle_handshake -> handle_open_conn
* handle_handshake -> close_conn // on err
*
* handle_open_conn -> handle_cgi_reply // via open_file/dir/...
* handle_open_conn -> send_fcgi_req // via apply_fastcgi, IMSG_FCGI_FD
* handle_open_conn -> handle_dirlist // ...same
* handle_open_conn -> send_file // ...same
* handle_open_conn -> start_reply // on error
*
* handle_cgi_reply -> handle_cgi // after logging the CGI reply
* handle_cgi_reply -> start_reply // on error
*
* handle_cgi -> close_conn
*
* send_fcgi_req -> copy_mbuf // via handle_fcgi
* handle_fcgi -> close_all // on error
* copy_mbuf -> close_conn // on success/error
*
* handle_dirlist -> send_directory_listing
* handle_dirlist -> close_conn // on error
*
* send_directory_listing -> close_conn
*
* send_file -> close_conn
*/
struct client {
int id;
struct tls *ctx;
char req[GEMINI_URL_LEN];
char *req;
struct iri iri;
char domain[DOMAIN_NAME_LEN];
/*
* start_reply uses this to know what function call after the
* reply. It's also used as sentinel value in fastcgi to know
* if the server has closed the request.
*/
statefn next;
struct bufferevent *bev;
int type;
struct bufferevent *cgibev;
char *header;
int code;
const char *meta;
@ -251,8 +224,6 @@ struct client {
char sbuf[1029];
ssize_t len, off;
struct mbufhead mbufhead;
struct sockaddr_storage addr;
struct vhost *host; /* host they're talking to */
size_t loc; /* location matched */
@ -356,8 +327,9 @@ X509_STORE *vhost_require_ca(struct vhost*, const char*);
int vhost_disable_log(struct vhost*, const char*);
void mark_nonblock(int);
void client_write(struct bufferevent *, void *);
void start_reply(struct client*, int, const char*);
void close_conn(int, short, void*);
void client_close(struct client *);
struct client *try_client_by_id(int);
void loop(struct tls*, int, int, struct imsgbuf*);
@ -382,6 +354,7 @@ int recv_fd(int);
int executor_main(struct imsgbuf*);
/* fcgi.c */
void fcgi_abort_request(struct client *);
void fcgi_close_backend(struct fcgi *);
void fcgi_read(struct bufferevent *, void *);
void fcgi_write(struct bufferevent *, void *);

782
server.c
View File

@ -19,6 +19,7 @@
#include <sys/stat.h>
#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <event.h>
#include <fcntl.h>
@ -26,6 +27,8 @@
#include <limits.h>
#include <string.h>
#define MIN(a, b) ((a) < (b) ? (a) : (b))
int shutting_down;
struct client clients[MAX_USERS];
@ -39,9 +42,6 @@ int connected_clients;
static inline int matches(const char*, const char*);
static inline void yield_read(int, struct client*, statefn);
static inline void yield_write(int, struct client*, statefn);
static int check_path(struct client*, const char*, int*);
static void open_file(struct client*);
static void check_for_cgi(struct client*);
@ -49,23 +49,33 @@ static void handle_handshake(int, short, void*);
static const char *strip_path(const char*, int);
static void fmt_sbuf(const char*, struct client*, const char*);
static int apply_block_return(struct client*);
static int apply_fastcgi(struct client*);
static int apply_require_ca(struct client*);
static void handle_open_conn(int, short, void*);
static void handle_start_reply(int, short, void*);
static size_t host_nth(struct vhost*);
static void start_cgi(const char*, const char*, struct client*);
static void open_dir(struct client*);
static void redirect_canonical_dir(struct client*);
static void enter_handle_dirlist(int, short, void*);
static void handle_dirlist(int, short, void*);
static int read_next_dir_entry(struct client*);
static void send_directory_listing(int, short, void*);
static void handle_cgi_reply(int, short, void*);
static void handle_copy(int, short, void*);
static void client_tls_readcb(int, short, void *);
static void client_tls_writecb(int, short, void *);
static void client_read(struct bufferevent *, void *);
void client_write(struct bufferevent *, void *);
static void client_error(struct bufferevent *, short, void *);
static void client_close_ev(int, short, void *);
static void cgi_read(struct bufferevent *, void *);
static void cgi_write(struct bufferevent *, void *);
static void cgi_error(struct bufferevent *, short, void *);
static void do_accept(int, short, void*);
static struct client *client_by_id(int);
static void handle_imsg_cgi_res(struct imsgbuf*, struct imsg*, size_t);
static void handle_imsg_fcgi_fd(struct imsgbuf*, struct imsg*, size_t);
static void handle_imsg_quit(struct imsgbuf*, struct imsg*, size_t);
static void handle_dispatch_imsg(int, short, void *);
static void handle_siginfo(int, short, void*);
static imsg_handlerfn *handlers[] = {
@ -82,18 +92,6 @@ matches(const char *pattern, const char *path)
return !fnmatch(pattern, path, 0);
}
static inline void
yield_read(int fd, struct client *c, statefn fn)
{
event_once(fd, EV_READ, fn, c, NULL);
}
static inline void
yield_write(int fd, struct client *c, statefn fn)
{
event_once(fd, EV_WRITE, fn, c, NULL);
}
const char *
vhost_lang(struct vhost *v, const char *path)
{
@ -362,7 +360,7 @@ open_file(struct client *c)
/* fallthrough */
case FILE_EXISTS:
c->next = handle_copy;
c->type = REQUEST_FILE;
start_reply(c, SUCCESS, mime(c->host, c->iri.path));
return;
@ -458,10 +456,10 @@ handle_handshake(int fd, short ev, void *d)
case -1: /* already handshaked */
break;
case TLS_WANT_POLLIN:
yield_read(fd, c, &handle_handshake);
event_once(c->fd, EV_READ, handle_handshake, c, NULL);
return;
case TLS_WANT_POLLOUT:
yield_write(fd, c, &handle_handshake);
event_once(c->fd, EV_WRITE, handle_handshake, c, NULL);
return;
default:
/* unreachable */
@ -495,16 +493,24 @@ found:
if (h != NULL) {
c->host = h;
handle_open_conn(fd, ev, c);
c->bev = bufferevent_new(fd, client_read, client_write,
client_error, c);
if (c->bev == NULL)
fatal("%s: failed to allocate client buffer: %s",
__func__, strerror(errno));
event_set(&c->bev->ev_read, c->fd, EV_READ,
client_tls_readcb, c->bev);
event_set(&c->bev->ev_write, c->fd, EV_WRITE,
client_tls_writecb, c->bev);
bufferevent_enable(c->bev, EV_READ);
return;
}
err:
if (servname != NULL)
strlcpy(c->req, servname, sizeof(c->req));
else
strlcpy(c->req, "null", sizeof(c->req));
start_reply(c, BAD_REQUEST, "Wrong/malformed host or missing SNI");
}
@ -647,110 +653,6 @@ apply_require_ca(struct client *c)
return 0;
}
static void
handle_open_conn(int fd, short ev, void *d)
{
struct client *c = d;
const char *parse_err = "invalid request";
char decoded[DOMAIN_NAME_LEN];
switch (tls_read(c->ctx, c->req, sizeof(c->req)-1)) {
case -1:
log_err(c, "tls_read: %s", tls_error(c->ctx));
close_conn(fd, ev, c);
return;
case TLS_WANT_POLLIN:
yield_read(fd, c, &handle_open_conn);
return;
case TLS_WANT_POLLOUT:
yield_write(fd, c, &handle_open_conn);
return;
}
if (!trim_req_iri(c->req, &parse_err) ||
!parse_iri(c->req, &c->iri, &parse_err) ||
!puny_decode(c->iri.host, decoded, sizeof(decoded), &parse_err)) {
log_info(c, "iri parse error: %s", parse_err);
start_reply(c, BAD_REQUEST, "invalid request");
return;
}
if (c->iri.port_no != conf.port ||
strcmp(c->iri.schema, "gemini") ||
strcmp(decoded, c->domain)) {
start_reply(c, PROXY_REFUSED, "won't proxy request");
return;
}
if (apply_require_ca(c))
return;
if (apply_block_return(c))
return;
if (apply_fastcgi(c))
return;
if (c->host->entrypoint != NULL) {
c->loc = 0;
start_cgi(c->host->entrypoint, c->iri.path, c);
return;
}
open_file(c);
}
void
start_reply(struct client *c, int code, const char *meta)
{
c->code = code;
c->meta = meta;
handle_start_reply(c->fd, 0, c);
}
static void
handle_start_reply(int fd, short ev, void *d)
{
struct client *c = d;
char buf[1030]; /* status + ' ' + max reply len + \r\n\0 */
const char *lang;
size_t len;
lang = vhost_lang(c->host, c->iri.path);
snprintf(buf, sizeof(buf), "%d ", c->code);
strlcat(buf, c->meta, sizeof(buf));
if (!strcmp(c->meta, "text/gemini") && lang != NULL) {
strlcat(buf, "; lang=", sizeof(buf));
strlcat(buf, lang, sizeof(buf));
}
len = strlcat(buf, "\r\n", sizeof(buf));
assert(len < sizeof(buf));
switch (tls_write(c->ctx, buf, len)) {
case -1:
close_conn(fd, ev, c);
return;
case TLS_WANT_POLLIN:
yield_read(fd, c, &handle_start_reply);
return;
case TLS_WANT_POLLOUT:
yield_write(fd, c, &handle_start_reply);
return;
}
if (!vhost_disable_log(c->host, c->iri.path))
log_request(c, buf, sizeof(buf));
if (c->code != SUCCESS)
close_conn(fd, ev, c);
else
c->next(fd, ev, c);
}
static size_t
host_nth(struct vhost *h)
{
@ -774,6 +676,8 @@ start_cgi(const char *spath, const char *relpath, struct client *c)
struct cgireq req;
int e;
c->type = REQUEST_CGI;
e = getnameinfo((struct sockaddr*)&c->addr, sizeof(c->addr),
addr, sizeof(addr),
NULL, 0,
@ -865,7 +769,7 @@ open_dir(struct client *c)
/* fallthrough */
case FILE_EXISTS:
c->next = handle_copy;
c->type = REQUEST_FILE;
start_reply(c, SUCCESS, mime(c->host, c->iri.path));
break;
@ -881,7 +785,7 @@ open_dir(struct client *c)
break;
}
c->next = enter_handle_dirlist;
c->type = REQUEST_DIR;
c->dirlen = scandir_fd(dirfd, &c->dir,
root ? select_non_dotdot : select_non_dot,
@ -896,6 +800,8 @@ open_dir(struct client *c)
c->off = 0;
start_reply(c, SUCCESS, "text/gemini");
evbuffer_add_printf(EVBUFFER_OUTPUT(c->bev),
"# Index of %s\n\n", c->iri.path);
return;
default:
@ -924,216 +830,327 @@ redirect_canonical_dir(struct client *c)
}
static void
enter_handle_dirlist(int fd, short ev, void *d)
client_tls_readcb(int fd, short event, void *d)
{
struct client *c = d;
char b[PATH_MAX];
size_t l;
struct bufferevent *bufev = d;
struct client *client = bufev->cbarg;
ssize_t ret;
size_t len;
int what = EVBUFFER_READ;
int howmuch = IBUF_READ_SIZE;
char buf[IBUF_READ_SIZE];
strlcpy(b, c->iri.path, sizeof(b));
l = snprintf(c->sbuf, sizeof(c->sbuf),
"# Index of %s\n\n", b);
if (l >= sizeof(c->sbuf)) {
if (event == EV_TIMEOUT) {
what |= EVBUFFER_TIMEOUT;
goto err;
}
if (bufev->wm_read.high != 0)
howmuch = MIN(sizeof(buf), bufev->wm_read.high);
switch (ret = tls_read(client->ctx, buf, howmuch)) {
case TLS_WANT_POLLIN:
case TLS_WANT_POLLOUT:
goto retry;
case -1:
what |= EVBUFFER_ERROR;
goto err;
}
len = ret;
if (len == 0) {
what |= EVBUFFER_EOF;
goto err;
}
if (evbuffer_add(bufev->input, buf, len) == -1) {
what |= EVBUFFER_ERROR;
goto err;
}
event_add(&bufev->ev_read, NULL);
if (bufev->wm_read.low != 0 && len < bufev->wm_read.low)
return;
if (bufev->wm_read.high != 0 && len > bufev->wm_read.high) {
/*
* This is impossible, given that we have enough space
* in c->sbuf to hold the ancilliary string plus the
* full path; but it wouldn't read nice without some
* error checking, and I'd like to avoid a strlen.
* here we could implement a read pressure policy.
*/
close_conn(fd, ev, c);
return;
}
c->len = l;
handle_dirlist(fd, ev, c);
if (bufev->readcb != NULL)
(*bufev->readcb)(bufev, bufev->cbarg);
return;
retry:
event_add(&bufev->ev_read, NULL);
return;
err:
(*bufev->errorcb)(bufev, what, bufev->cbarg);
}
static void
handle_dirlist(int fd, short ev, void *d)
client_tls_writecb(int fd, short event, void *d)
{
struct client *c = d;
ssize_t r;
struct bufferevent *bufev = d;
struct client *client = bufev->cbarg;
ssize_t ret;
size_t len;
short what = EVBUFFER_WRITE;
while (c->len > 0) {
switch (r = tls_write(c->ctx, c->sbuf + c->off, c->len)) {
case -1:
close_conn(fd, ev, c);
return;
case TLS_WANT_POLLOUT:
yield_read(fd, c, &handle_dirlist);
return;
if (event == EV_TIMEOUT) {
what |= EVBUFFER_TIMEOUT;
goto err;
}
if (EVBUFFER_LENGTH(bufev->output) != 0) {
ret = tls_write(client->ctx,
EVBUFFER_DATA(bufev->output),
EVBUFFER_LENGTH(bufev->output));
switch (ret) {
case TLS_WANT_POLLIN:
yield_write(fd, c, &handle_dirlist);
return;
default:
c->off += r;
c->len -= r;
}
}
send_directory_listing(fd, ev, c);
}
static int
read_next_dir_entry(struct client *c)
{
if (c->diroff == c->dirlen)
return 0;
/* XXX: url escape */
snprintf(c->sbuf, sizeof(c->sbuf), "=> %s\n",
c->dir[c->diroff]->d_name);
free(c->dir[c->diroff]);
c->diroff++;
c->len = strlen(c->sbuf);
c->off = 0;
return 1;
}
static void
send_directory_listing(int fd, short ev, void *d)
{
struct client *c = d;
ssize_t r;
while (1) {
if (c->len == 0) {
if (!read_next_dir_entry(c))
goto end;
}
while (c->len > 0) {
switch (r = tls_write(c->ctx, c->sbuf + c->off, c->len)) {
case -1:
goto end;
case TLS_WANT_POLLOUT:
yield_read(fd, c, &send_directory_listing);
return;
case TLS_WANT_POLLIN:
yield_write(fd, c, &send_directory_listing);
return;
default:
c->off += r;
c->len -= r;
break;
}
}
}
end:
close_conn(fd, ev, d);
}
/* accumulate the meta line from the cgi script. */
static void
handle_cgi_reply(int fd, short ev, void *d)
{
struct client *c = d;
void *buf, *e;
size_t len;
ssize_t r;
buf = c->sbuf + c->len;
len = sizeof(c->sbuf) - c->len;
r = read(c->pfd, buf, len);
if (r == 0 || r == -1) {
start_reply(c, CGI_ERROR, "CGI error");
return;
}
c->len += r;
/* TODO: error if the CGI script don't reply correctly */
e = strchr(c->sbuf, '\n');
if (e != NULL || c->len == sizeof(c->sbuf)) {
log_request(c, c->sbuf, c->len);
c->off = 0;
handle_copy(fd, ev, c);
return;
}
yield_read(fd, c, &handle_cgi_reply);
}
static void
handle_copy(int fd, short ev, void *d)
{
struct client *c = d;
ssize_t r;
while (1) {
while (c->len > 0) {
switch (r = tls_write(c->ctx, c->sbuf + c->off, c->len)) {
case -1:
goto end;
case TLS_WANT_POLLOUT:
yield_write(c->fd, c, &handle_copy);
return;
case TLS_WANT_POLLIN:
yield_read(c->fd, c, &handle_copy);
return;
default:
c->off += r;
c->len -= r;
break;
}
}
switch (r = read(c->pfd, c->sbuf, sizeof(c->sbuf))) {
case 0:
goto end;
case TLS_WANT_POLLOUT:
goto retry;
case -1:
if (errno == EAGAIN || errno == EWOULDBLOCK) {
yield_read(c->pfd, c, &handle_copy);
return;
}
goto end;
default:
c->len = r;
c->off = 0;
what |= EVBUFFER_ERROR;
goto err;
}
len = ret;
evbuffer_drain(bufev->output, len);
}
end:
close_conn(c->fd, ev, d);
if (EVBUFFER_LENGTH(bufev->output) != 0)
event_add(&bufev->ev_write, NULL);
if (bufev->writecb != NULL &&
EVBUFFER_LENGTH(bufev->output) <= bufev->wm_write.low)
(*bufev->writecb)(bufev, bufev->cbarg);
return;
retry:
event_add(&bufev->ev_write, NULL);
return;
err:
log_err(client, "tls error: %s", tls_error(client->ctx));
(*bufev->errorcb)(bufev, what, bufev->cbarg);
}
static void
client_read(struct bufferevent *bev, void *d)
{
struct client *c = d;
struct evbuffer *src = EVBUFFER_INPUT(bev);
const char *parse_err = "invalid request";
char decoded[DOMAIN_NAME_LEN];
size_t len;
bufferevent_disable(bev, EVBUFFER_READ);
/* max url len + \r\n */
if (EVBUFFER_LENGTH(src) > 1024 + 2) {
log_err(c, "too much data received");
start_reply(c, BAD_REQUEST, "bad request");
return;
}
c->req = evbuffer_readln(src, &len, EVBUFFER_EOL_CRLF_STRICT);
if (c->req == NULL) {
/* not enough data yet. */
bufferevent_enable(bev, EVBUFFER_READ);
return;
}
if (!parse_iri(c->req, &c->iri, &parse_err) ||
!puny_decode(c->iri.host, decoded, sizeof(decoded), &parse_err)) {
log_err(c, "IRI parse error: %s", parse_err);
start_reply(c, BAD_REQUEST, "bad request");
return;
}
if (c->iri.port_no != conf.port ||
strcmp(c->iri.schema, "gemini") ||
strcmp(decoded, c->domain)) {
start_reply(c, PROXY_REFUSED, "won't proxy request");
return;
}
if (apply_require_ca(c) ||
apply_block_return(c)||
apply_fastcgi(c))
return;
if (c->host->entrypoint != NULL) {
c->loc = 0;
start_cgi(c->host->entrypoint, c->iri.path, c);
return;
}
open_file(c);
}
void
close_conn(int fd, short ev, void *d)
client_write(struct bufferevent *bev, void *d)
{
struct client *c = d;
struct evbuffer *out = EVBUFFER_OUTPUT(bev);
char buf[BUFSIZ];
ssize_t r;
switch (c->type) {
case REQUEST_UNDECIDED:
/*
* Ignore spurious calls when we still don't have idea
* what to do with the request.
*/
break;
case REQUEST_FILE:
if ((r = read(c->pfd, buf, sizeof(buf))) == -1) {
log_warn(c, "read: %s", strerror(errno));
client_error(bev, EVBUFFER_ERROR, c);
return;
} else if (r == 0) {
client_close(c);
return;
} else if (r != sizeof(buf))
c->type = REQUEST_DONE;
bufferevent_write(bev, buf, r);
break;
case REQUEST_DIR:
/* TODO: handle big big directories better */
for (c->diroff = 0; c->diroff < c->dirlen; ++c->diroff) {
evbuffer_add_printf(out, "=> %s\n",
c->dir[c->diroff]->d_name);
free(c->dir[c->diroff]);
}
free(c->dir);
c->dir = NULL;
c->type = REQUEST_DONE;
event_add(&c->bev->ev_write, NULL);
break;
case REQUEST_CGI:
case REQUEST_FCGI:
/*
* Here we depend on on the cgi script or fastcgi
* connection to provide data.
*/
break;
case REQUEST_DONE:
if (EVBUFFER_LENGTH(out) == 0)
client_close(c);
break;
}
}
static void
client_error(struct bufferevent *bev, short error, void *d)
{
struct client *c = d;
if (c->type == REQUEST_FCGI)
fcgi_abort_request(c);
c->type = REQUEST_DONE;
if (error & EVBUFFER_TIMEOUT) {
log_warn(c, "timeout reached, "
"forcefully closing the connection");
if (c->code == 0)
start_reply(c, BAD_REQUEST, "timeout");
else
client_close(c);
return;
}
if (error & EVBUFFER_EOF) {
client_close(c);
return;
}
log_err(c, "unknown bufferevent error: %s", strerror(errno));
client_close(c);
}
void
start_reply(struct client *c, int code, const char *meta)
{
struct evbuffer *evb = EVBUFFER_OUTPUT(c->bev);
const char *lang;
int r, rr;
bufferevent_enable(c->bev, EVBUFFER_WRITE);
c->code = code;
c->meta = meta;
r = evbuffer_add_printf(evb, "%d %s", code, meta);
if (r == -1)
goto err;
/* 2 digit status + space + 1024 max reply */
if (r > 1027)
goto overflow;
if (c->type != REQUEST_CGI &&
c->type != REQUEST_FCGI &&
!strcmp(meta, "text/gemini") &&
(lang = vhost_lang(c->host, c->iri.path)) != NULL) {
rr = evbuffer_add_printf(evb, ";lang=%s", lang);
if (rr == -1)
goto err;
if (r + rr > 1027)
goto overflow;
}
bufferevent_write(c->bev, "\r\n", 2);
if (!vhost_disable_log(c->host, c->iri.path))
log_request(c, EVBUFFER_DATA(evb), EVBUFFER_LENGTH(evb));
if (code != 20 && IS_INTERNAL_REQUEST(c->type))
c->type = REQUEST_DONE;
return;
err:
log_err(c, "evbuffer_add_printf error: no memory");
evbuffer_drain(evb, EVBUFFER_LENGTH(evb));
client_close(c);
return;
overflow:
log_warn(c, "reply header overflow");
evbuffer_drain(evb, EVBUFFER_LENGTH(evb));
start_reply(c, TEMP_FAILURE, "internal error");
}
static void
client_close_ev(int fd, short event, void *d)
{
struct client *c = d;
struct mbuf *mbuf;
switch (tls_close(c->ctx)) {
case TLS_WANT_POLLIN:
yield_read(c->fd, c, &close_conn);
return;
event_once(c->fd, EV_READ, client_close_ev, c, NULL);
break;
case TLS_WANT_POLLOUT:
yield_read(c->fd, c, &close_conn);
return;
event_once(c->fd, EV_WRITE, client_close_ev, c, NULL);
break;
}
connected_clients--;
while ((mbuf = TAILQ_FIRST(&c->mbufhead)) != NULL) {
TAILQ_REMOVE(&c->mbufhead, mbuf, mbufs);
free(mbuf);
}
tls_free(c->ctx);
c->ctx = NULL;
free(c->header);
if (c->pfd != -1)
close(c->pfd);
@ -1144,6 +1161,122 @@ close_conn(int fd, short ev, void *d)
c->fd = -1;
}
void
client_close(struct client *c)
{
/*
* We may end up calling client_close in various situations
* and for the most unexpected reasons. Therefore, we need to
* ensure that everything is properly released once we reach
* this point.
*/
if (c->type == REQUEST_FCGI)
fcgi_abort_request(c);
if (c->cgibev != NULL) {
bufferevent_disable(c->cgibev, EVBUFFER_READ|EVBUFFER_WRITE);
bufferevent_free(c->cgibev);
c->cgibev = NULL;
close(c->pfd);
c->pfd = -1;
}
bufferevent_disable(c->bev, EVBUFFER_READ|EVBUFFER_WRITE);
bufferevent_free(c->bev);
c->bev = NULL;
client_close_ev(c->fd, 0, c);
}
static void
cgi_read(struct bufferevent *bev, void *d)
{
struct client *client = d;
struct evbuffer *src = EVBUFFER_INPUT(bev);
char *header;
size_t len;
int code;
/* intercept the header */
if (client->code == 0) {
header = evbuffer_readln(src, &len, EVBUFFER_EOL_CRLF_STRICT);
if (header == NULL) {
/* max reply + \r\n */
if (EVBUFFER_LENGTH(src) > 1026) {
log_warn(client, "CGI script is trying to "
"send a header too long.");
cgi_error(bev, EVBUFFER_READ, client);
}
/* wait a bit */
return;
}
if (len < 3 || len > 1029 ||
!isdigit(header[0]) ||
!isdigit(header[1]) ||
!isspace(header[2])) {
free(header);
log_warn(client, "CGI script is trying to send a "
"malformed header");
cgi_error(bev, EVBUFFER_READ, client);
return;
}
client->header = header;
code = (header[0] - '0') * 10 + (header[1] - '0');
if (code < 10 || code >= 70) {
log_warn(client, "CGI script is trying to send an "
"invalid reply code (%d)", code);
cgi_error(bev, EVBUFFER_READ, client);
return;
}
start_reply(client, code, header + 3);
if (client->code < 20 || client->code > 29) {
cgi_error(client->cgibev, EVBUFFER_EOF, client);
return;
}
}
bufferevent_write_buffer(client->bev, src);
}
static void
cgi_write(struct bufferevent *bev, void *d)
{
/*
* Never called. We don't send data to a CGI script.
*/
abort();
}
static void
cgi_error(struct bufferevent *bev, short error, void *d)
{
struct client *client = d;
if (error & EVBUFFER_ERROR)
log_err(client, "%s: evbuffer error (%x): %s",
__func__, error, strerror(errno));
bufferevent_disable(bev, EVBUFFER_READ|EVBUFFER_WRITE);
bufferevent_free(bev);
client->cgibev = NULL;
close(client->pfd);
client->pfd = -1;
client->type = REQUEST_DONE;
if (client->code != 0)
client_write(client->bev, client);
else
start_reply(client, CGI_ERROR, "CGI error");
}
static void
do_accept(int sock, short et, void *d)
{
@ -1179,9 +1312,9 @@ do_accept(int sock, short et, void *d)
c->addr = addr;
c->fcgi = -1;
TAILQ_INIT(&c->mbufhead);
event_once(c->fd, EV_READ|EV_WRITE, handle_handshake,
c, NULL);
yield_read(fd, c, &handle_handshake);
connected_clients++;
return;
}
@ -1213,10 +1346,17 @@ handle_imsg_cgi_res(struct imsgbuf *ibuf, struct imsg *imsg, size_t len)
c = client_by_id(imsg->hdr.peerid);
if ((c->pfd = imsg->fd) == -1)
if ((c->pfd = imsg->fd) == -1) {
start_reply(c, TEMP_FAILURE, "internal server error");
else
yield_read(c->pfd, c, &handle_cgi_reply);
return;
}
c->type = REQUEST_CGI;
c->cgibev = bufferevent_new(c->pfd, cgi_read, cgi_write,
cgi_error, c);
bufferevent_enable(c->cgibev, EV_READ);
}
static void