httpd: drain abort response via bufferevent

Rafael Sadowski <[email protected]>
Newsgroups gmane.os.openbsd.tech
Message-ID <[email protected]>
This is an all-in-one diff that replaces server_dump() with a
bufferevent aka event-loop approach.

server_dump() did one non-blocking write(2)/tls_write(3) and discarded
the return value, so partial writes were lost, meaning that large
user-defined error documents were silently truncated in transit before.

This diff merge three commits into one. I don’t think it's too long to
review. If we're OK, I'll split it into three separate commits:

1. ref: server_create_builtin
https://rsadowski.gothub.org/?action=diff&commit=7dc036be4782f5e6a31fa6bdd9ba1a57bea72f5a&path=httpd.git
2. drain abort response via bufferevent
https://rsadowski.gothub.org/?action=diff&commit=b07039fc1f28bfe260ab72963e644c7575cad267&path=httpd.git
3. error loading the document in server_create_errdoc()
https://rsadowski.gothub.org/?action=diff&commit=ea58fa5107a8182e42d5c180ce80c23fed0ad7d2&path=httpd.git

Cheers, Rafael

diff --git a/httpd.h b/httpd.h
index 12122c6..664524e 100644
--- a/httpd.h
+++ b/httpd.h
@@ -344,6 +344,8 @@ struct client {
 	unsigned int		 clt_pipelining;
 	int			 clt_line;
 	int			 clt_done;
+	int			 clt_close_after_write;
+	char			*clt_close_msg;
 	int			 clt_chunk;
 	int			 clt_inflight;
 	int			 clt_fcgi_count;
@@ -651,7 +653,6 @@ void	 server_log(struct client *, const char *);
 void	 server_sendlog(struct server_config *, int, const char *, ...)
 	    __attribute__((__format__ (printf, 3, 4)));
 void	 server_close(struct client *, const char *);
-void	 server_dump(struct client *, const void *, size_t);
 int	 server_client_cmp(struct client *, struct client *);
 int	 server_bufferevent_printf(struct client *, const char *, ...)
 	    __attribute__((__format__ (printf, 2, 3)));
@@ -662,6 +663,7 @@ int	 server_bufferevent_write_chunk(struct client *,
 	    struct evbuffer *, size_t);
 int	 server_bufferevent_add(struct event *, int);
 int	 server_bufferevent_write(struct client *, void *, size_t);
+int	 server_bufferevent_write_close(struct client *, void *, size_t);
 struct server *
 	 server_byaddr(struct sockaddr *, in_port_t);
 struct server_config *
diff --git a/server.c b/server.c
index 0f42ccd..43ea95e 100644
--- a/server.c
+++ b/server.c
@@ -937,6 +937,12 @@ server_write(struct bufferevent *bev, void *arg)
 	struct client		*clt = arg;
 	struct evbuffer		*dst = EVBUFFER_OUTPUT(bev);
 
+	if (EVBUFFER_LENGTH(dst) == 0 && clt->clt_close_after_write) {
+		server_close(clt, clt->clt_close_msg != NULL ?
+		    clt->clt_close_msg : "response sent");
+		return;
+	}
+
 	if (EVBUFFER_LENGTH(dst) == 0 &&
 	    clt->clt_toread == TOREAD_HTTP_NONE)
 		goto done;
@@ -957,24 +963,6 @@ server_write(struct bufferevent *bev, void *arg)
 	return;
 }
 
-void
-server_dump(struct client *clt, const void *buf, size_t len)
-{
-	if (!len)
-		return;
-
-	/*
-	 * This function will dump the specified message directly
-	 * to the underlying client, without waiting for success
-	 * of non-blocking events etc. This is useful to print an
-	 * error message before gracefully closing the client.
-	 */
-	if (clt->clt_tls_ctx != NULL)
-		(void)tls_write(clt->clt_tls_ctx, buf, len);
-	else
-		(void)write(clt->clt_s, buf, len);
-}
-
 void
 server_read(struct bufferevent *bev, void *arg)
 {
@@ -1340,6 +1328,8 @@ server_close(struct client *clt, const char *msg)
 	if (clt->clt_log != NULL)
 		evbuffer_free(clt->clt_log);
 
+	free(clt->clt_close_msg);
+
 	free(clt);
 	server_clients--;
 }
@@ -1472,6 +1462,20 @@ server_bufferevent_write(struct client *clt, void *data, size_t size)
 	return (bufferevent_write(clt->clt_bev, data, size));
 }
 
+int
+server_bufferevent_write_close(struct client *clt, void *data, size_t size)
+{
+	if (clt->clt_bev == NULL)
+		return (-1);
+	if (clt->clt_close_after_write)
+		return (-1);
+
+	clt->clt_persist = 0;
+	clt->clt_close_after_write = 1;
+
+	return (bufferevent_write(clt->clt_bev, data, size));
+}
+
 int
 server_client_cmp(struct client *a, struct client *b)
 {
diff --git a/server_http.c b/server_http.c
index 5ecb65c..28667bf 100644
--- a/server_http.c
+++ b/server_http.c
@@ -55,6 +55,10 @@ char		*server_expand_http(struct client *, const char *,
 		    char *, size_t);
 char		*replace_var(char *, const char *, const char *);
 char		*read_errdoc(const char *, const char *);
+ssize_t		 server_create_builtin(struct server_config *, char **,
+		    unsigned int, const char *);
+char		*server_create_errdoc(struct server_config *, unsigned int,
+		    const char *);
 
 static struct http_method	 http_methods[] = HTTP_METHODS;
 static struct http_error	 http_errors[] = HTTP_ERRORS;
@@ -880,15 +884,15 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg)
 	struct server_config	*srv_conf = clt->clt_srv_conf;
 	struct bufferevent	*bev = clt->clt_bev;
 	struct http_descriptor	*desc = clt->clt_descreq;
-	const char		*httperr = NULL, *style;
+	const char		*httperr = NULL;
 	char			*httpmsg, *body = NULL, *extraheader = NULL;
 	char			 tmbuf[32], hbuf[128], *hstsheader = NULL;
 	char			*clenheader = NULL;
-	char			*bannerheader = NULL, *bannertoken = NULL;
+	char			*bannerheader = NULL;
 	char			 buf[IBUF_READ_SIZE];
 	char			*escapedmsg = NULL;
-	char			 cstr[5];
 	ssize_t			 bodylen;
+	ssize_t			 httpmsglen;
 
 	if (code == 0) {
 		server_close(clt, "dropped");
@@ -941,6 +945,7 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg)
 			code = 500;
 			extraheader = NULL;
 		}
+		free(escapedmsg);
 		break;
 	case 416:
 		if (msg == NULL)
@@ -962,66 +967,12 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg)
 		break;
 	}
 
-	free(escapedmsg);
-
-	if ((srv_conf->flags & SRVFLAG_ERRDOCS) == 0)
-		goto builtin; /* errdocs not enabled */
-	if ((size_t)snprintf(cstr, sizeof(cstr), "%03u", code) >= sizeof(cstr))
-		goto builtin;
-
-	if ((body = read_errdoc(srv_conf->errdocroot, cstr)) == NULL &&
-	    (body = read_errdoc(srv_conf->errdocroot, HTTPD_ERRDOCTEMPLATE))
-	    == NULL)
-		goto builtin;
-
-	body = replace_var(body, "$HTTP_ERROR", httperr);
-	body = replace_var(body, "$RESPONSE_CODE", cstr);
-	/* Check if server banner is suppressed */
-	if ((srv_conf->flags & SRVFLAG_NO_BANNER) == 0)
-		body = replace_var(body, "$SERVER_SOFTWARE", HTTPD_SERVERNAME);
-	else
-		body = replace_var(body, "$SERVER_SOFTWARE", "");
-	bodylen = strlen(body);
-	goto send;
-
- builtin:
-	/* A CSS stylesheet allows minimal customization by the user */
-	style = "body { background-color: white; color: black; font-family: "
-	    "'Comic Sans MS', 'Chalkboard SE', 'Comic Neue', sans-serif; }\n"
-	    "hr { border: 0; border-bottom: 1px dashed; }\n"
-	    "@media (prefers-color-scheme: dark) {\n"
-	    "body { background-color: #1E1F21; color: #EEEFF1; }\n"
-	    "a { color: #BAD7FF; }\n}";
-
-	/* If banner is suppressed, don't write it to the error document */
-	if ((srv_conf->flags & SRVFLAG_NO_BANNER) == 0)
-		if (asprintf(&bannertoken, "<hr>\n<address>%s</address>\n",
-		    HTTPD_SERVERNAME) == -1) {
-			bannertoken = NULL;
-			goto done;
-		}
-
-	/* Generate simple HTML error document */
-	if ((bodylen = asprintf(&body,
-	    "<!DOCTYPE html>\n"
-	    "<html>\n"
-	    "<head>\n"
-	    "<meta charset=\"utf-8\">\n"
-	    "<title>%03d %s</title>\n"
-	    "<style type=\"text/css\"><!--\n%s\n--></style>\n"
-	    "</head>\n"
-	    "<body>\n"
-	    "<h1>%03d %s</h1>\n"
-	    "%s"
-	    "</body>\n"
-	    "</html>\n",
-	    code, httperr, style, code, httperr,
-	    bannertoken == NULL ? "" : bannertoken)) == -1) {
-		body = NULL;
+	if ((body = server_create_errdoc(srv_conf, code, httperr)) != NULL) {
+		bodylen = strlen(body);
+	} else if ((bodylen = server_create_builtin(srv_conf, &body, code,
+	    httperr)) == -1)
 		goto done;
-	}
 
- send:
 	if (srv_conf->flags & SRVFLAG_SERVER_HSTS &&
 	    srv_conf->flags & SRVFLAG_TLS) {
 		if (asprintf(&hstsheader, "Strict-Transport-Security: "
@@ -1054,7 +1005,7 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg)
 		}
 
 	/* Add basic HTTP headers */
-	if (asprintf(&httpmsg,
+	if ((httpmsglen = asprintf(&httpmsg,
 	    "HTTP/1.0 %03d %s\r\n"
 	    "Date: %s\r\n"
 	    "%s"
@@ -1071,12 +1022,27 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg)
 	    extraheader == NULL ? "" : extraheader,
 	    hstsheader == NULL ? "" : hstsheader,
 	    desc->http_method == HTTP_METHOD_HEAD || clenheader == NULL ?
-	    "" : body) == -1)
+	    "" : body)) == -1)
 		goto done;
 
-	/* Dump the message without checking for success */
-	server_dump(clt, httpmsg, strlen(httpmsg));
+	free(clt->clt_close_msg);
+	if (asprintf(&clt->clt_close_msg, "%s (%03d %s)",
+	    msg == NULL ? "\"\"" : msg, code, httperr) == -1)
+		clt->clt_close_msg = NULL;
+
+	if (server_bufferevent_write_close(clt, httpmsg,
+	    (size_t)httpmsglen) == -1) {
+		/* fall back to synchronous close */
+		free(httpmsg);
+		goto done;
+	}
 	free(httpmsg);
+	free(body);
+	free(extraheader);
+	free(hstsheader);
+	free(clenheader);
+	free(bannerheader);
+	return;
 
  done:
 	free(body);
@@ -1084,7 +1050,6 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg)
 	free(hstsheader);
 	free(clenheader);
 	free(bannerheader);
-	free(bannertoken);
 	if (msg == NULL)
 		msg = "\"\"";
 	if (asprintf(&httpmsg, "%s (%03d %s)", msg, code, httperr) == -1) {
@@ -1095,6 +1060,80 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg)
 	}
 }
 
+char *
+server_create_errdoc(struct server_config *srv_conf, unsigned int code,
+    const char *httperr)
+{
+	char	 cstr[5];
+	char	*body;
+
+	if (!(srv_conf->flags & SRVFLAG_ERRDOCS))
+		return (NULL);
+
+	if ((size_t)snprintf(cstr, sizeof(cstr), "%03u", code) >= sizeof(cstr))
+		return (NULL);
+
+	if ((body = read_errdoc(srv_conf->errdocroot, cstr)) == NULL &&
+	    (body = read_errdoc(srv_conf->errdocroot, HTTPD_ERRDOCTEMPLATE))
+	    == NULL)
+		return (NULL);
+
+	body = replace_var(body, "$HTTP_ERROR", httperr);
+	body = replace_var(body, "$RESPONSE_CODE", cstr);
+	body = replace_var(body, "$SERVER_SOFTWARE",
+	    (srv_conf->flags & SRVFLAG_NO_BANNER) ? "" : HTTPD_SERVERNAME);
+
+	return (body);
+}
+
+ssize_t
+server_create_builtin(struct server_config *srv_conf, char **body,
+    unsigned int code, const char *httperr)
+{
+	const char	*style;
+	char		*bannertoken = NULL;
+	ssize_t		 bodylen;
+
+	/* A CSS stylesheet allows minimal customization by the user */
+	style = "body { background-color: white; color: black; font-family: "
+	    "'Comic Sans MS', 'Chalkboard SE', 'Comic Neue', sans-serif; }\n"
+	    "hr { border: 0; border-bottom: 1px dashed; }\n"
+	    "@media (prefers-color-scheme: dark) {\n"
+	    "body { background-color: #1E1F21; color: #EEEFF1; }\n"
+	    "a { color: #BAD7FF; }\n}";
+
+	/* If banner is suppressed, don't write it to the error document */
+	if ((srv_conf->flags & SRVFLAG_NO_BANNER) == 0) {
+		if (asprintf(&bannertoken, "<hr>\n<address>%s</address>\n",
+		    HTTPD_SERVERNAME) == -1)
+			return (-1);
+	}
+
+	/* Generate simple HTML error document */
+	bodylen = asprintf(body,
+	    "<!DOCTYPE html>\n"
+	    "<html>\n"
+	    "<head>\n"
+	    "<meta charset=\"utf-8\">\n"
+	    "<title>%03d %s</title>\n"
+	    "<style type=\"text/css\"><!--\n%s\n--></style>\n"
+	    "</head>\n"
+	    "<body>\n"
+	    "<h1>%03d %s</h1>\n"
+	    "%s"
+	    "</body>\n"
+	    "</html>\n",
+	    code, httperr, style, code, httperr,
+	    bannertoken == NULL ? "" : bannertoken);
+
+	free(bannertoken);
+	if (bodylen == -1) {
+		*body = NULL;
+		return (-1);
+	}
+	return (bodylen);
+}
+
 void
 server_close_http(struct client *clt)
 {
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.