File: [local] / src / usr.sbin / httpd / server_http.c (download)
Revision 1.154, Tue Feb 13 14:00:24 2024 UTC (3 months, 3 weeks ago) by claudio
Branch: MAIN
CVS Tags: OPENBSD_7_5_BASE, OPENBSD_7_5, HEAD Changes since 1.153: +5 -5 lines
Stop logging misleading errors when custom generic error pages are in use.
Only call the open(2) log_warn for errnos that are not ENOENT. Since
that is an error worth logging.
Based on a diff from Carsten Reith (carsten.reith t-online.de)
OK florian@ deraadt@
|
/* $OpenBSD: server_http.c,v 1.154 2024/02/13 14:00:24 claudio Exp $ */
/*
* Copyright (c) 2020 Matthias Pressfreund <mpfr@fn.de>
* Copyright (c) 2006 - 2018 Reyk Floeter <reyk@openbsd.org>
*
* 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 <sys/types.h>
#include <sys/queue.h>
#include <sys/socket.h>
#include <sys/tree.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <limits.h>
#include <fnmatch.h>
#include <stdio.h>
#include <time.h>
#include <resolv.h>
#include <event.h>
#include <ctype.h>
#include <vis.h>
#include <fcntl.h>
#include "httpd.h"
#include "http.h"
#include "patterns.h"
static int server_httpmethod_cmp(const void *, const void *);
static int server_httperror_cmp(const void *, const void *);
void server_httpdesc_free(struct http_descriptor *);
int server_http_authenticate(struct server_config *,
struct client *);
static int http_version_num(char *);
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 *);
static struct http_method http_methods[] = HTTP_METHODS;
static struct http_error http_errors[] = HTTP_ERRORS;
void
server_http(void)
{
DPRINTF("%s: sorting lookup tables, pid %d", __func__, getpid());
/* Sort the HTTP lookup arrays */
qsort(http_methods, sizeof(http_methods) /
sizeof(http_methods[0]) - 1,
sizeof(http_methods[0]), server_httpmethod_cmp);
qsort(http_errors, sizeof(http_errors) /
sizeof(http_errors[0]) - 1,
sizeof(http_errors[0]), server_httperror_cmp);
}
void
server_http_init(struct server *srv)
{
/* nothing */
}
int
server_httpdesc_init(struct client *clt)
{
struct http_descriptor *desc;
if ((desc = calloc(1, sizeof(*desc))) == NULL)
return (-1);
RB_INIT(&desc->http_headers);
clt->clt_descreq = desc;
if ((desc = calloc(1, sizeof(*desc))) == NULL) {
/* req will be cleaned up later */
return (-1);
}
RB_INIT(&desc->http_headers);
clt->clt_descresp = desc;
return (0);
}
void
server_httpdesc_free(struct http_descriptor *desc)
{
if (desc == NULL)
return;
free(desc->http_path);
desc->http_path = NULL;
free(desc->http_path_orig);
desc->http_path_orig = NULL;
free(desc->http_path_alias);
desc->http_path_alias = NULL;
free(desc->http_query);
desc->http_query = NULL;
free(desc->http_query_alias);
desc->http_query_alias = NULL;
free(desc->http_version);
desc->http_version = NULL;
free(desc->http_host);
desc->http_host = NULL;
kv_purge(&desc->http_headers);
desc->http_lastheader = NULL;
desc->http_method = 0;
desc->http_chunked = 0;
}
int
server_http_authenticate(struct server_config *srv_conf, struct client *clt)
{
char decoded[1024];
FILE *fp = NULL;
struct http_descriptor *desc = clt->clt_descreq;
const struct auth *auth = srv_conf->auth;
struct kv *ba, key;
size_t linesize = 0;
ssize_t linelen;
int ret = -1;
char *line = NULL, *user = NULL, *pass = NULL;
char *clt_user = NULL, *clt_pass = NULL;
memset(decoded, 0, sizeof(decoded));
key.kv_key = "Authorization";
if ((ba = kv_find(&desc->http_headers, &key)) == NULL ||
ba->kv_value == NULL)
goto done;
if (strncmp(ba->kv_value, "Basic ", strlen("Basic ")) != 0)
goto done;
if (b64_pton(strchr(ba->kv_value, ' ') + 1, (uint8_t *)decoded,
sizeof(decoded)) <= 0)
goto done;
if ((clt_pass = strchr(decoded, ':')) == NULL)
goto done;
clt_user = decoded;
*clt_pass++ = '\0';
if ((clt->clt_remote_user = strdup(clt_user)) == NULL)
goto done;
if ((fp = fopen(auth->auth_htpasswd, "r")) == NULL)
goto done;
while ((linelen = getline(&line, &linesize, fp)) != -1) {
if (line[linelen - 1] == '\n')
line[linelen - 1] = '\0';
user = line;
pass = strchr(line, ':');
if (pass == NULL) {
explicit_bzero(line, linelen);
continue;
}
*pass++ = '\0';
if (strcmp(clt_user, user) != 0) {
explicit_bzero(line, linelen);
continue;
}
if (crypt_checkpass(clt_pass, pass) == 0) {
explicit_bzero(line, linelen);
ret = 0;
break;
}
}
done:
free(line);
if (fp != NULL)
fclose(fp);
if (ba != NULL && ba->kv_value != NULL) {
explicit_bzero(ba->kv_value, strlen(ba->kv_value));
explicit_bzero(decoded, sizeof(decoded));
}
return (ret);
}
static int
http_version_num(char *version)
{
if (strlen(version) != 8 || strncmp(version, "HTTP/", 5) != 0
|| !isdigit((unsigned char)version[5]) || version[6] != '.'
|| !isdigit((unsigned char)version[7]))
return (-1);
if (version[5] == '0' && version[7] == '9')
return (9);
if (version[5] == '1') {
if (version[7] == '0')
return (10);
else
/* any other version 1.x gets downgraded to 1.1 */
return (11);
}
return (0);
}
void
server_read_http(struct bufferevent *bev, void *arg)
{
struct client *clt = arg;
struct http_descriptor *desc = clt->clt_descreq;
struct evbuffer *src = EVBUFFER_INPUT(bev);
char *line = NULL, *key, *value;
const char *errstr;
char *http_version, *query;
size_t size, linelen;
int version;
struct kv *hdr = NULL;
getmonotime(&clt->clt_tv_last);
size = EVBUFFER_LENGTH(src);
DPRINTF("%s: session %d: size %lu, to read %lld",
__func__, clt->clt_id, size, clt->clt_toread);
if (!size) {
clt->clt_toread = TOREAD_HTTP_HEADER;
goto done;
}
while (!clt->clt_headersdone) {
if (!clt->clt_line) {
/* Peek into the buffer to see if it looks like HTTP */
key = EVBUFFER_DATA(src);
if (!isalpha((unsigned char)*key)) {
server_abort_http(clt, 400,
"invalid request line");
goto abort;
}
}
if ((line = evbuffer_readln(src,
&linelen, EVBUFFER_EOL_CRLF_STRICT)) == NULL) {
/* No newline found after too many bytes */
if (size > SERVER_MAXHEADERLENGTH) {
server_abort_http(clt, 413,
"request line too long");
goto abort;
}
break;
}
/*
* An empty line indicates the end of the request.
* libevent already stripped the \r\n for us.
*/
if (!linelen) {
clt->clt_headersdone = 1;
free(line);
break;
}
key = line;
/* Limit the total header length minus \r\n */
clt->clt_headerlen += linelen;
if (clt->clt_headerlen > SERVER_MAXHEADERLENGTH) {
server_abort_http(clt, 413, "request too large");
goto abort;
}
/*
* The first line is the GET/POST/PUT/... request,
* subsequent lines are HTTP headers.
*/
if (++clt->clt_line == 1)
value = strchr(key, ' ');
else if (*key == ' ' || *key == '\t')
/* Multiline headers wrap with a space or tab */
value = NULL;
else {
/* Not a multiline header, should have a : */
value = strchr(key, ':');
if (value == NULL) {
server_abort_http(clt, 400, "malformed");
goto abort;
}
}
if (value == NULL) {
if (clt->clt_line == 1) {
server_abort_http(clt, 400, "malformed");
goto abort;
}
/* Append line to the last header, if present */
if (kv_extend(&desc->http_headers,
desc->http_lastheader, line) == NULL)
goto fail;
free(line);
continue;
}
if (*value == ':') {
*value++ = '\0';
value += strspn(value, " \t\r\n");
} else {
*value++ = '\0';
}
DPRINTF("%s: session %d: header '%s: %s'", __func__,
clt->clt_id, key, value);
/*
* Identify and handle specific HTTP request methods
*/
if (clt->clt_line == 1) {
if ((desc->http_method = server_httpmethod_byname(key))
== HTTP_METHOD_NONE) {
server_abort_http(clt, 400, "malformed");
goto abort;
}
/*
* Decode request path and query
*/
desc->http_path = strdup(value);
if (desc->http_path == NULL)
goto fail;
http_version = strchr(desc->http_path, ' ');
if (http_version == NULL) {
server_abort_http(clt, 400, "malformed");
goto abort;
}
*http_version++ = '\0';
/*
* We have to allocate the strings because they could
* be changed independently by the filters later.
* Allow HTTP version 0.9 to 1.1.
* Downgrade http version > 1.1 <= 1.9 to version 1.1.
* Return HTTP Version Not Supported for anything else.
*/
version = http_version_num(http_version);
if (version == -1) {
server_abort_http(clt, 400, "malformed");
goto abort;
} else if (version == 0) {
server_abort_http(clt, 505, "bad http version");
goto abort;
} else if (version == 11) {
if ((desc->http_version =
strdup("HTTP/1.1")) == NULL)
goto fail;
} else {
if ((desc->http_version =
strdup(http_version)) == NULL)
goto fail;
}
query = strchr(desc->http_path, '?');
if (query != NULL) {
*query++ = '\0';
if ((desc->http_query = strdup(query)) == NULL)
goto fail;
}
} else if (desc->http_method != HTTP_METHOD_NONE &&
strcasecmp("Content-Length", key) == 0) {
if (desc->http_method == HTTP_METHOD_TRACE ||
desc->http_method == HTTP_METHOD_CONNECT) {
/*
* These method should not have a body
* and thus no Content-Length header.
*/
server_abort_http(clt, 400, "malformed");
goto abort;
}
/*
* Need to read data from the client after the
* HTTP header.
* XXX What about non-standard clients not using
* the carriage return? And some browsers seem to
* include the line length in the content-length.
*/
clt->clt_toread = strtonum(value, 0, LLONG_MAX,
&errstr);
if (errstr) {
server_abort_http(clt, 500, errstr);
goto abort;
}
}
if (strcasecmp("Transfer-Encoding", key) == 0 &&
strcasecmp("chunked", value) == 0)
desc->http_chunked = 1;
if (clt->clt_line != 1) {
if ((hdr = kv_add(&desc->http_headers, key,
value)) == NULL)
goto fail;
desc->http_lastheader = hdr;
}
free(line);
}
if (clt->clt_headersdone) {
if (desc->http_method == HTTP_METHOD_NONE) {
server_abort_http(clt, 406, "no method");
return;
}
switch (desc->http_method) {
case HTTP_METHOD_CONNECT:
/* Data stream */
clt->clt_toread = TOREAD_UNLIMITED;
bev->readcb = server_read;
break;
case HTTP_METHOD_GET:
case HTTP_METHOD_HEAD:
/* WebDAV methods */
case HTTP_METHOD_COPY:
case HTTP_METHOD_MOVE:
clt->clt_toread = 0;
break;
case HTTP_METHOD_DELETE:
case HTTP_METHOD_OPTIONS:
case HTTP_METHOD_POST:
case HTTP_METHOD_PUT:
case HTTP_METHOD_RESPONSE:
/* WebDAV methods */
case HTTP_METHOD_PROPFIND:
case HTTP_METHOD_PROPPATCH:
case HTTP_METHOD_MKCOL:
case HTTP_METHOD_LOCK:
case HTTP_METHOD_UNLOCK:
case HTTP_METHOD_VERSION_CONTROL:
case HTTP_METHOD_REPORT:
case HTTP_METHOD_CHECKOUT:
case HTTP_METHOD_CHECKIN:
case HTTP_METHOD_UNCHECKOUT:
case HTTP_METHOD_MKWORKSPACE:
case HTTP_METHOD_UPDATE:
case HTTP_METHOD_LABEL:
case HTTP_METHOD_MERGE:
case HTTP_METHOD_BASELINE_CONTROL:
case HTTP_METHOD_MKACTIVITY:
case HTTP_METHOD_ORDERPATCH:
case HTTP_METHOD_ACL:
case HTTP_METHOD_MKREDIRECTREF:
case HTTP_METHOD_UPDATEREDIRECTREF:
case HTTP_METHOD_SEARCH:
case HTTP_METHOD_PATCH:
/* HTTP request payload */
if (clt->clt_toread > 0)
bev->readcb = server_read_httpcontent;
if (clt->clt_toread < 0 && !desc->http_chunked)
/* 7. of RFC 9112 Section 6.3 */
clt->clt_toread = 0;
break;
default:
server_abort_http(clt, 405, "method not allowed");
return;
}
if (desc->http_chunked) {
/* Chunked transfer encoding */
clt->clt_toread = TOREAD_HTTP_CHUNK_LENGTH;
bev->readcb = server_read_httpchunks;
}
done:
if (clt->clt_toread != 0)
bufferevent_disable(bev, EV_READ);
server_response(httpd_env, clt);
return;
}
if (clt->clt_done) {
server_close(clt, "done");
return;
}
if (EVBUFFER_LENGTH(src) && bev->readcb != server_read_http)
bev->readcb(bev, arg);
bufferevent_enable(bev, EV_READ);
return;
fail:
server_abort_http(clt, 500, strerror(errno));
abort:
free(line);
}
void
server_read_httpcontent(struct bufferevent *bev, void *arg)
{
struct client *clt = arg;
struct evbuffer *src = EVBUFFER_INPUT(bev);
size_t size;
getmonotime(&clt->clt_tv_last);
size = EVBUFFER_LENGTH(src);
DPRINTF("%s: session %d: size %lu, to read %lld", __func__,
clt->clt_id, size, clt->clt_toread);
if (!size)
return;
if (clt->clt_toread > 0) {
/* Read content data */
if ((off_t)size > clt->clt_toread) {
size = clt->clt_toread;
if (fcgi_add_stdin(clt, src) == -1)
goto fail;
clt->clt_toread = 0;
} else {
if (fcgi_add_stdin(clt, src) == -1)
goto fail;
clt->clt_toread -= size;
}
DPRINTF("%s: done, size %lu, to read %lld", __func__,
size, clt->clt_toread);
}
if (clt->clt_toread == 0) {
fcgi_add_stdin(clt, NULL);
clt->clt_toread = TOREAD_HTTP_HEADER;
bufferevent_disable(bev, EV_READ);
bev->readcb = server_read_http;
return;
}
if (clt->clt_done)
goto done;
if (bev->readcb != server_read_httpcontent)
bev->readcb(bev, arg);
return;
done:
return;
fail:
server_close(clt, strerror(errno));
}
void
server_read_httpchunks(struct bufferevent *bev, void *arg)
{
struct client *clt = arg;
struct evbuffer *src = EVBUFFER_INPUT(bev);
char *line;
long long llval;
size_t size;
getmonotime(&clt->clt_tv_last);
size = EVBUFFER_LENGTH(src);
DPRINTF("%s: session %d: size %lu, to read %lld", __func__,
clt->clt_id, size, clt->clt_toread);
if (!size)
return;
if (clt->clt_toread > 0) {
/* Read chunk data */
if ((off_t)size > clt->clt_toread) {
size = clt->clt_toread;
if (server_bufferevent_write_chunk(clt, src, size)
== -1)
goto fail;
clt->clt_toread = 0;
} else {
if (server_bufferevent_write_buffer(clt, src) == -1)
goto fail;
clt->clt_toread -= size;
}
DPRINTF("%s: done, size %lu, to read %lld", __func__,
size, clt->clt_toread);
}
switch (clt->clt_toread) {
case TOREAD_HTTP_CHUNK_LENGTH:
line = evbuffer_readln(src, NULL, EVBUFFER_EOL_CRLF_STRICT);
if (line == NULL) {
/* Ignore empty line, continue */
bufferevent_enable(bev, EV_READ);
return;
}
if (strlen(line) == 0) {
free(line);
goto next;
}
/*
* Read prepended chunk size in hex, ignore the trailer.
* The returned signed value must not be negative.
*/
if (sscanf(line, "%llx", &llval) != 1 || llval < 0) {
free(line);
server_close(clt, "invalid chunk size");
return;
}
if (server_bufferevent_print(clt, line) == -1 ||
server_bufferevent_print(clt, "\r\n") == -1) {
free(line);
goto fail;
}
free(line);
if ((clt->clt_toread = llval) == 0) {
DPRINTF("%s: last chunk", __func__);
clt->clt_toread = TOREAD_HTTP_CHUNK_TRAILER;
}
break;
case TOREAD_HTTP_CHUNK_TRAILER:
/* Last chunk is 0 bytes followed by trailer and empty line */
line = evbuffer_readln(src, NULL, EVBUFFER_EOL_CRLF_STRICT);
if (line == NULL) {
/* Ignore empty line, continue */
bufferevent_enable(bev, EV_READ);
return;
}
if (server_bufferevent_print(clt, line) == -1 ||
server_bufferevent_print(clt, "\r\n") == -1) {
free(line);
goto fail;
}
if (strlen(line) == 0) {
/* Switch to HTTP header mode */
clt->clt_toread = TOREAD_HTTP_HEADER;
bev->readcb = server_read_http;
}
free(line);
break;
case 0:
/* Chunk is terminated by an empty newline */
line = evbuffer_readln(src, NULL, EVBUFFER_EOL_CRLF_STRICT);
free(line);
if (server_bufferevent_print(clt, "\r\n") == -1)
goto fail;
clt->clt_toread = TOREAD_HTTP_CHUNK_LENGTH;
break;
}
next:
if (clt->clt_done)
goto done;
if (EVBUFFER_LENGTH(src))
bev->readcb(bev, arg);
bufferevent_enable(bev, EV_READ);
return;
done:
server_close(clt, "last http chunk read (done)");
return;
fail:
server_close(clt, strerror(errno));
}
void
server_read_httprange(struct bufferevent *bev, void *arg)
{
struct client *clt = arg;
struct evbuffer *src = EVBUFFER_INPUT(bev);
size_t size;
struct media_type *media;
struct range_data *r = &clt->clt_ranges;
struct range *range;
getmonotime(&clt->clt_tv_last);
if (r->range_toread > 0) {
size = EVBUFFER_LENGTH(src);
if (!size)
return;
/* Read chunk data */
if ((off_t)size > r->range_toread) {
size = r->range_toread;
if (server_bufferevent_write_chunk(clt, src, size)
== -1)
goto fail;
r->range_toread = 0;
} else {
if (server_bufferevent_write_buffer(clt, src) == -1)
goto fail;
r->range_toread -= size;
}
if (r->range_toread < 1)
r->range_toread = TOREAD_HTTP_RANGE;
DPRINTF("%s: done, size %lu, to read %lld", __func__,
size, r->range_toread);
}
switch (r->range_toread) {
case TOREAD_HTTP_RANGE:
if (r->range_index >= r->range_count) {
if (r->range_count > 1) {
/* Add end marker */
if (server_bufferevent_printf(clt,
"\r\n--%llu--\r\n",
clt->clt_boundary) == -1)
goto fail;
}
r->range_toread = TOREAD_HTTP_NONE;
break;
}
range = &r->range[r->range_index];
if (r->range_count > 1) {
media = r->range_media;
if (server_bufferevent_printf(clt,
"\r\n--%llu\r\n"
"Content-Type: %s/%s\r\n"
"Content-Range: bytes %lld-%lld/%zu\r\n\r\n",
clt->clt_boundary,
media->media_type, media->media_subtype,
range->start, range->end, r->range_total) == -1)
goto fail;
}
r->range_toread = range->end - range->start + 1;
if (lseek(clt->clt_fd, range->start, SEEK_SET) == -1)
goto fail;
/* Throw away bytes that are already in the input buffer */
evbuffer_drain(src, EVBUFFER_LENGTH(src));
/* Increment for the next part */
r->range_index++;
break;
case TOREAD_HTTP_NONE:
goto done;
case 0:
break;
}
if (clt->clt_done)
goto done;
if (EVBUFFER_LENGTH(EVBUFFER_OUTPUT(clt->clt_bev)) > (size_t)
SERVER_MAX_PREFETCH * clt->clt_sndbufsiz) {
bufferevent_disable(clt->clt_srvbev, EV_READ);
clt->clt_srvbev_throttled = 1;
}
return;
done:
(*bev->errorcb)(bev, EVBUFFER_READ, bev->cbarg);
return;
fail:
server_close(clt, strerror(errno));
}
void
server_reset_http(struct client *clt)
{
struct server *srv = clt->clt_srv;
server_log(clt, NULL);
server_httpdesc_free(clt->clt_descreq);
server_httpdesc_free(clt->clt_descresp);
clt->clt_headerlen = 0;
clt->clt_headersdone = 0;
clt->clt_done = 0;
clt->clt_line = 0;
clt->clt_chunk = 0;
free(clt->clt_remote_user);
clt->clt_remote_user = NULL;
clt->clt_bev->readcb = server_read_http;
clt->clt_srv_conf = &srv->srv_conf;
str_match_free(&clt->clt_srv_match);
}
ssize_t
server_http_time(time_t t, char *tmbuf, size_t len)
{
struct tm tm;
/* New HTTP/1.1 RFC 7231 prefers IMF-fixdate from RFC 5322 */
if (t == -1 || gmtime_r(&t, &tm) == NULL)
return (-1);
else
return (strftime(tmbuf, len, "%a, %d %h %Y %T %Z", &tm));
}
const char *
server_http_host(struct sockaddr_storage *ss, char *buf, size_t len)
{
char hbuf[HOST_NAME_MAX+1];
in_port_t port;
if (print_host(ss, buf, len) == NULL)
return (NULL);
port = ntohs(server_socket_getport(ss));
if (port == HTTP_PORT)
return (buf);
switch (ss->ss_family) {
case AF_INET:
if ((size_t)snprintf(hbuf, sizeof(hbuf),
"%s:%u", buf, port) >= sizeof(hbuf))
return (NULL);
break;
case AF_INET6:
if ((size_t)snprintf(hbuf, sizeof(hbuf),
"[%s]:%u", buf, port) >= sizeof(hbuf))
return (NULL);
break;
}
if (strlcpy(buf, hbuf, len) >= len)
return (NULL);
return (buf);
}
char *
server_http_parsehost(char *host, char *buf, size_t len, int *portval)
{
char *start, *end, *port;
const char *errstr = NULL;
if (strlcpy(buf, host, len) >= len) {
log_debug("%s: host name too long", __func__);
return (NULL);
}
start = buf;
end = port = NULL;
if (*start == '[' && (end = strchr(start, ']')) != NULL) {
/* Address enclosed in [] with port, eg. [2001:db8::1]:80 */
start++;
*end++ = '\0';
if ((port = strchr(end, ':')) == NULL || *port == '\0')
port = NULL;
else
port++;
memmove(buf, start, strlen(start) + 1);
} else if ((end = strchr(start, ':')) != NULL) {
/* Name or address with port, eg. www.example.com:80 */
*end++ = '\0';
port = end;
} else {
/* Name or address with default port, eg. www.example.com */
port = NULL;
}
if (port != NULL) {
/* Save the requested port */
*portval = strtonum(port, 0, 0xffff, &errstr);
if (errstr != NULL) {
log_debug("%s: invalid port: %s", __func__,
strerror(errno));
return (NULL);
}
*portval = htons(*portval);
} else {
/* Port not given, indicate the default port */
*portval = -1;
}
return (start);
}
void
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;
char *httpmsg, *body = NULL, *extraheader = NULL;
char tmbuf[32], hbuf[128], *hstsheader = NULL;
char *clenheader = NULL;
char buf[IBUF_READ_SIZE];
char *escapedmsg = NULL;
char cstr[5];
ssize_t bodylen;
if (code == 0) {
server_close(clt, "dropped");
return;
}
if ((httperr = server_httperror_byid(code)) == NULL)
httperr = "Unknown Error";
if (bev == NULL)
goto done;
if (server_log_http(clt, code, 0) == -1)
goto done;
/* Some system information */
if (print_host(&srv_conf->ss, hbuf, sizeof(hbuf)) == NULL)
goto done;
if (server_http_time(time(NULL), tmbuf, sizeof(tmbuf)) <= 0)
goto done;
/* Do not send details of the Internal Server Error */
switch (code) {
case 301:
case 302:
case 303:
case 307:
case 308:
if (msg == NULL)
break;
memset(buf, 0, sizeof(buf));
if (server_expand_http(clt, msg, buf, sizeof(buf)) == NULL)
goto done;
if (asprintf(&extraheader, "Location: %s\r\n", buf) == -1) {
code = 500;
extraheader = NULL;
}
msg = buf;
break;
case 401:
if (msg == NULL)
break;
if (stravis(&escapedmsg, msg, VIS_DQ) == -1) {
code = 500;
extraheader = NULL;
} else if (asprintf(&extraheader,
"WWW-Authenticate: Basic realm=\"%s\"\r\n", escapedmsg)
== -1) {
code = 500;
extraheader = NULL;
}
break;
case 416:
if (msg == NULL)
break;
if (asprintf(&extraheader,
"Content-Range: %s\r\n", msg) == -1) {
code = 500;
extraheader = NULL;
}
break;
default:
/*
* Do not send details of the error. Traditionally,
* web servers responsed with the request path on 40x
* errors which could be abused to inject JavaScript etc.
* Instead of sanitizing the path here, we just don't
* reprint it.
*/
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);
body = replace_var(body, "$SERVER_SOFTWARE", HTTPD_SERVERNAME);
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}";
/* 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"
"<hr>\n<address>%s</address>\n"
"</body>\n"
"</html>\n",
code, httperr, style, code, httperr, HTTPD_SERVERNAME)) == -1) {
body = NULL;
goto done;
}
send:
if (srv_conf->flags & SRVFLAG_SERVER_HSTS &&
srv_conf->flags & SRVFLAG_TLS) {
if (asprintf(&hstsheader, "Strict-Transport-Security: "
"max-age=%d%s%s\r\n", srv_conf->hsts_max_age,
srv_conf->hsts_flags & HSTSFLAG_SUBDOMAINS ?
"; includeSubDomains" : "",
srv_conf->hsts_flags & HSTSFLAG_PRELOAD ?
"; preload" : "") == -1) {
hstsheader = NULL;
goto done;
}
}
if ((code >= 100 && code < 200) || code == 204)
clenheader = NULL;
else {
if (asprintf(&clenheader,
"Content-Length: %zd\r\n", bodylen) == -1) {
clenheader = NULL;
goto done;
}
}
/* Add basic HTTP headers */
if (asprintf(&httpmsg,
"HTTP/1.0 %03d %s\r\n"
"Date: %s\r\n"
"Server: %s\r\n"
"Connection: close\r\n"
"Content-Type: text/html\r\n"
"%s"
"%s"
"%s"
"\r\n"
"%s",
code, httperr, tmbuf, HTTPD_SERVERNAME,
clenheader == NULL ? "" : clenheader,
extraheader == NULL ? "" : extraheader,
hstsheader == NULL ? "" : hstsheader,
desc->http_method == HTTP_METHOD_HEAD || clenheader == NULL ?
"" : body) == -1)
goto done;
/* Dump the message without checking for success */
server_dump(clt, httpmsg, strlen(httpmsg));
free(httpmsg);
done:
free(body);
free(extraheader);
free(hstsheader);
free(clenheader);
if (msg == NULL)
msg = "\"\"";
if (asprintf(&httpmsg, "%s (%03d %s)", msg, code, httperr) == -1) {
server_close(clt, msg);
} else {
server_close(clt, httpmsg);
free(httpmsg);
}
}
void
server_close_http(struct client *clt)
{
struct http_descriptor *desc;
desc = clt->clt_descreq;
server_httpdesc_free(desc);
free(desc);
clt->clt_descreq = NULL;
desc = clt->clt_descresp;
server_httpdesc_free(desc);
free(desc);
clt->clt_descresp = NULL;
free(clt->clt_remote_user);
clt->clt_remote_user = NULL;
str_match_free(&clt->clt_srv_match);
}
char *
server_expand_http(struct client *clt, const char *val, char *buf,
size_t len)
{
struct http_descriptor *desc = clt->clt_descreq;
struct server_config *srv_conf = clt->clt_srv_conf;
char ibuf[128], *str, *path, *query;
const char *errstr = NULL, *p;
size_t size;
int n, ret;
if (strlcpy(buf, val, len) >= len)
return (NULL);
/* Find previously matched substrings by index */
for (p = val; clt->clt_srv_match.sm_nmatch &&
(p = strstr(p, "%")) != NULL; p++) {
if (!isdigit((unsigned char)*(p + 1)))
continue;
/* Copy number, leading '%' char and add trailing \0 */
size = strspn(p + 1, "0123456789") + 2;
if (size >= sizeof(ibuf))
return (NULL);
(void)strlcpy(ibuf, p, size);
n = strtonum(ibuf + 1, 0,
clt->clt_srv_match.sm_nmatch - 1, &errstr);
if (errstr != NULL)
return (NULL);
/* Expand variable with matched value */
if ((str = url_encode(clt->clt_srv_match.sm_match[n])) == NULL)
return (NULL);
ret = expand_string(buf, len, ibuf, str);
free(str);
if (ret != 0)
return (NULL);
}
if (strstr(val, "$DOCUMENT_URI") != NULL) {
if ((path = url_encode(desc->http_path)) == NULL)
return (NULL);
ret = expand_string(buf, len, "$DOCUMENT_URI", path);
free(path);
if (ret != 0)
return (NULL);
}
if (strstr(val, "$QUERY_STRING_ENC") != NULL) {
if (desc->http_query == NULL) {
ret = expand_string(buf, len, "$QUERY_STRING_ENC", "");
} else {
if ((query = url_encode(desc->http_query)) == NULL)
return (NULL);
ret = expand_string(buf, len, "$QUERY_STRING_ENC", query);
free(query);
}
if (ret != 0)
return (NULL);
}
if (strstr(val, "$QUERY_STRING") != NULL) {
if (desc->http_query == NULL) {
ret = expand_string(buf, len, "$QUERY_STRING", "");
} else {
ret = expand_string(buf, len, "$QUERY_STRING",
desc->http_query);
}
if (ret != 0)
return (NULL);
}
if (strstr(val, "$HTTP_HOST") != NULL) {
if (desc->http_host == NULL)
return (NULL);
if ((str = url_encode(desc->http_host)) == NULL)
return (NULL);
expand_string(buf, len, "$HTTP_HOST", str);
free(str);
}
if (strstr(val, "$REMOTE_") != NULL) {
if (strstr(val, "$REMOTE_ADDR") != NULL) {
if (print_host(&clt->clt_ss,
ibuf, sizeof(ibuf)) == NULL)
return (NULL);
if (expand_string(buf, len,
"$REMOTE_ADDR", ibuf) != 0)
return (NULL);
}
if (strstr(val, "$REMOTE_PORT") != NULL) {
snprintf(ibuf, sizeof(ibuf),
"%u", ntohs(clt->clt_port));
if (expand_string(buf, len,
"$REMOTE_PORT", ibuf) != 0)
return (NULL);
}
if (strstr(val, "$REMOTE_USER") != NULL) {
if ((srv_conf->flags & SRVFLAG_AUTH) &&
clt->clt_remote_user != NULL) {
if ((str = url_encode(clt->clt_remote_user))
== NULL)
return (NULL);
} else
str = strdup("");
ret = expand_string(buf, len, "$REMOTE_USER", str);
free(str);
if (ret != 0)
return (NULL);
}
}
if (strstr(val, "$REQUEST_URI") != NULL) {
if ((path = url_encode(desc->http_path)) == NULL)
return (NULL);
if (desc->http_query == NULL) {
str = path;
} else {
ret = asprintf(&str, "%s?%s", path, desc->http_query);
free(path);
if (ret == -1)
return (NULL);
}
ret = expand_string(buf, len, "$REQUEST_URI", str);
free(str);
if (ret != 0)
return (NULL);
}
if (strstr(val, "$REQUEST_SCHEME") != NULL) {
if (srv_conf->flags & SRVFLAG_TLS) {
ret = expand_string(buf, len, "$REQUEST_SCHEME", "https");
} else {
ret = expand_string(buf, len, "$REQUEST_SCHEME", "http");
}
if (ret != 0)
return (NULL);
}
if (strstr(val, "$SERVER_") != NULL) {
if (strstr(val, "$SERVER_ADDR") != NULL) {
if (print_host(&srv_conf->ss,
ibuf, sizeof(ibuf)) == NULL)
return (NULL);
if (expand_string(buf, len,
"$SERVER_ADDR", ibuf) != 0)
return (NULL);
}
if (strstr(val, "$SERVER_PORT") != NULL) {
snprintf(ibuf, sizeof(ibuf), "%u",
ntohs(srv_conf->port));
if (expand_string(buf, len,
"$SERVER_PORT", ibuf) != 0)
return (NULL);
}
if (strstr(val, "$SERVER_NAME") != NULL) {
if ((str = url_encode(srv_conf->name))
== NULL)
return (NULL);
ret = expand_string(buf, len, "$SERVER_NAME", str);
free(str);
if (ret != 0)
return (NULL);
}
}
return (buf);
}
int
server_response(struct httpd *httpd, struct client *clt)
{
char path[PATH_MAX];
char hostname[HOST_NAME_MAX+1];
struct http_descriptor *desc = clt->clt_descreq;
struct http_descriptor *resp = clt->clt_descresp;
struct server *srv = clt->clt_srv;
struct server_config *srv_conf = &srv->srv_conf;
struct kv *kv, key, *host;
struct str_find sm;
int portval = -1, ret;
char *hostval, *query;
const char *errstr = NULL;
/* Preserve original path */
if (desc->http_path == NULL ||
(desc->http_path_orig = strdup(desc->http_path)) == NULL)
goto fail;
/* Decode the URL */
if (url_decode(desc->http_path) == NULL)
goto fail;
/* Canonicalize the request path */
if (canonicalize_path(desc->http_path, path, sizeof(path)) == NULL)
goto fail;
free(desc->http_path);
if ((desc->http_path = strdup(path)) == NULL)
goto fail;
key.kv_key = "Host";
if ((host = kv_find(&desc->http_headers, &key)) != NULL &&
host->kv_value == NULL)
host = NULL;
if (strcmp(desc->http_version, "HTTP/1.1") == 0) {
/* Host header is mandatory */
if (host == NULL)
goto fail;
/* Is the connection persistent? */
key.kv_key = "Connection";
if ((kv = kv_find(&desc->http_headers, &key)) != NULL &&
strcasecmp("close", kv->kv_value) == 0)
clt->clt_persist = 0;
else
clt->clt_persist++;
} else {
/* Is the connection persistent? */
key.kv_key = "Connection";
if ((kv = kv_find(&desc->http_headers, &key)) != NULL &&
strcasecmp("keep-alive", kv->kv_value) == 0)
clt->clt_persist++;
else
clt->clt_persist = 0;
}
/*
* Do we have a Host header and matching configuration?
* XXX the Host can also appear in the URL path.
*/
if (host != NULL) {
if ((hostval = server_http_parsehost(host->kv_value,
hostname, sizeof(hostname), &portval)) == NULL)
goto fail;
TAILQ_FOREACH(srv_conf, &srv->srv_hosts, entry) {
#ifdef DEBUG
if ((srv_conf->flags & SRVFLAG_LOCATION) == 0) {
DPRINTF("%s: virtual host \"%s:%u\""
" host \"%s\" (\"%s\")",
__func__, srv_conf->name,
ntohs(srv_conf->port), host->kv_value,
hostname);
}
#endif
if (srv_conf->flags & SRVFLAG_LOCATION)
continue;
else if (srv_conf->flags & SRVFLAG_SERVER_MATCH) {
str_find(hostname, srv_conf->name,
&sm, 1, &errstr);
ret = errstr == NULL ? 0 : -1;
} else {
ret = fnmatch(srv_conf->name,
hostname, FNM_CASEFOLD);
}
if (ret == 0 &&
(portval == -1 || portval == srv_conf->port)) {
/* Replace host configuration */
clt->clt_srv_conf = srv_conf;
srv_conf = NULL;
break;
}
}
}
if (srv_conf != NULL) {
/* Use the actual server IP address */
if (server_http_host(&clt->clt_srv_ss, hostname,
sizeof(hostname)) == NULL)
goto fail;
} else {
/* Host header was valid and found */
if (strlcpy(hostname, host->kv_value, sizeof(hostname)) >=
sizeof(hostname))
goto fail;
srv_conf = clt->clt_srv_conf;
}
if (clt->clt_persist >= srv_conf->maxrequests)
clt->clt_persist = 0;
/* pipelining should end after the first "idempotent" method */
if (clt->clt_pipelining && clt->clt_toread > 0)
clt->clt_persist = 0;
if ((desc->http_host = strdup(hostname)) == NULL)
goto fail;
/* Now fill in the mandatory parts of the response descriptor */
resp->http_method = desc->http_method;
if ((resp->http_version = strdup(desc->http_version)) == NULL)
goto fail;
/* Now search for the location */
if ((srv_conf = server_getlocation(clt, desc->http_path)) == NULL) {
server_abort_http(clt, 500, desc->http_path);
return (-1);
}
/* Optional rewrite */
if (srv_conf->flags & SRVFLAG_PATH_REWRITE) {
/* Expand macros */
if (server_expand_http(clt, srv_conf->path,
path, sizeof(path)) == NULL)
goto fail;
/*
* Reset and update the query. The updated query must already
* be URL encoded - either specified by the user or by using the
* original $QUERY_STRING.
*/
free(desc->http_query_alias);
desc->http_query_alias = NULL;
if ((query = strchr(path, '?')) != NULL) {
*query++ = '\0';
if ((desc->http_query_alias = strdup(query)) == NULL)
goto fail;
}
/* Canonicalize the updated request path */
if (canonicalize_path(path,
path, sizeof(path)) == NULL)
goto fail;
log_debug("%s: rewrote %s?%s -> %s?%s", __func__,
desc->http_path, desc->http_query ? desc->http_query : "",
path, query ? query : "");
free(desc->http_path_alias);
if ((desc->http_path_alias = strdup(path)) == NULL)
goto fail;
/* Now search for the updated location */
if ((srv_conf = server_getlocation(clt,
desc->http_path_alias)) == NULL) {
server_abort_http(clt, 500, desc->http_path_alias);
return (-1);
}
}
if (clt->clt_toread > 0 && (size_t)clt->clt_toread >
srv_conf->maxrequestbody) {
server_abort_http(clt, 413, "request body too large");
return (-1);
}
if (srv_conf->flags & SRVFLAG_BLOCK) {
server_abort_http(clt, srv_conf->return_code,
srv_conf->return_uri);
return (-1);
} else if (srv_conf->flags & SRVFLAG_AUTH &&
server_http_authenticate(srv_conf, clt) == -1) {
server_abort_http(clt, 401, srv_conf->auth_realm);
return (-1);
} else
return (server_file(httpd, clt));
fail:
server_abort_http(clt, 400, "bad request");
return (-1);
}
const char *
server_root_strip(const char *path, int n)
{
const char *p;
/* Strip strip leading directories. Leading '/' is ignored. */
for (; n > 0 && *path != '\0'; n--)
if ((p = strchr(++path, '/')) == NULL)
path = strchr(path, '\0');
else
path = p;
return (path);
}
struct server_config *
server_getlocation(struct client *clt, const char *path)
{
struct server *srv = clt->clt_srv;
struct server_config *srv_conf = clt->clt_srv_conf, *location;
const char *errstr = NULL;
int ret;
/* Now search for the location */
TAILQ_FOREACH(location, &srv->srv_hosts, entry) {
#ifdef DEBUG
if (location->flags & SRVFLAG_LOCATION) {
DPRINTF("%s: location \"%s\" path \"%s\"",
__func__, location->location, path);
}
#endif
if ((location->flags & SRVFLAG_LOCATION) &&
location->parent_id == srv_conf->parent_id) {
errstr = NULL;
if (location->flags & SRVFLAG_LOCATION_MATCH) {
ret = str_match(path, location->location,
&clt->clt_srv_match, &errstr);
} else {
ret = fnmatch(location->location,
path, FNM_CASEFOLD);
}
if (ret == 0 && errstr == NULL) {
if ((ret = server_locationaccesstest(location,
path)) == -1)
return (NULL);
if (ret)
continue;
/* Replace host configuration */
clt->clt_srv_conf = srv_conf = location;
break;
}
}
}
return (srv_conf);
}
int
server_locationaccesstest(struct server_config *srv_conf, const char *path)
{
int rootfd, ret;
struct stat sb;
if (((SRVFLAG_LOCATION_FOUND | SRVFLAG_LOCATION_NOT_FOUND) &
srv_conf->flags) == 0)
return (0);
if ((rootfd = open(srv_conf->root, O_RDONLY)) == -1)
return (-1);
path = server_root_strip(path, srv_conf->strip) + 1;
if ((ret = faccessat(rootfd, path, R_OK, 0)) != -1)
ret = fstatat(rootfd, path, &sb, 0);
close(rootfd);
return ((ret == -1 && SRVFLAG_LOCATION_FOUND & srv_conf->flags) ||
(ret == 0 && SRVFLAG_LOCATION_NOT_FOUND & srv_conf->flags));
}
int
server_response_http(struct client *clt, unsigned int code,
struct media_type *media, off_t size, time_t mtime)
{
struct server_config *srv_conf = clt->clt_srv_conf;
struct http_descriptor *desc = clt->clt_descreq;
struct http_descriptor *resp = clt->clt_descresp;
const char *error;
struct kv *ct, *cl;
char tmbuf[32];
if (desc == NULL || media == NULL ||
(error = server_httperror_byid(code)) == NULL)
return (-1);
if (server_log_http(clt, code, size >= 0 ? size : 0) == -1)
return (-1);
/* Add error codes */
if (kv_setkey(&resp->http_pathquery, "%u", code) == -1 ||
kv_set(&resp->http_pathquery, "%s", error) == -1)
return (-1);
/* Add headers */
if (kv_add(&resp->http_headers, "Server", HTTPD_SERVERNAME) == NULL)
return (-1);
/* Is it a persistent connection? */
if (clt->clt_persist) {
if (kv_add(&resp->http_headers,
"Connection", "keep-alive") == NULL)
return (-1);
} else if (kv_add(&resp->http_headers, "Connection", "close") == NULL)
return (-1);
/* Set media type */
if ((ct = kv_add(&resp->http_headers, "Content-Type", NULL)) == NULL ||
kv_set(ct, "%s/%s", media->media_type, media->media_subtype) == -1)
return (-1);
/* Set content length, if specified */
if (size >= 0 && ((cl =
kv_add(&resp->http_headers, "Content-Length", NULL)) == NULL ||
kv_set(cl, "%lld", (long long)size) == -1))
return (-1);
/* Set last modification time */
if (server_http_time(mtime, tmbuf, sizeof(tmbuf)) <= 0 ||
kv_add(&resp->http_headers, "Last-Modified", tmbuf) == NULL)
return (-1);
/* HSTS header */
if (srv_conf->flags & SRVFLAG_SERVER_HSTS &&
srv_conf->flags & SRVFLAG_TLS) {
if ((cl =
kv_add(&resp->http_headers, "Strict-Transport-Security",
NULL)) == NULL ||
kv_set(cl, "max-age=%d%s%s", srv_conf->hsts_max_age,
srv_conf->hsts_flags & HSTSFLAG_SUBDOMAINS ?
"; includeSubDomains" : "",
srv_conf->hsts_flags & HSTSFLAG_PRELOAD ?
"; preload" : "") == -1)
return (-1);
}
/* Date header is mandatory and should be added as late as possible */
if (server_http_time(time(NULL), tmbuf, sizeof(tmbuf)) <= 0 ||
kv_add(&resp->http_headers, "Date", tmbuf) == NULL)
return (-1);
/* Write completed header */
if (server_writeresponse_http(clt) == -1 ||
server_bufferevent_print(clt, "\r\n") == -1 ||
server_headers(clt, resp, server_writeheader_http, NULL) == -1 ||
server_bufferevent_print(clt, "\r\n") == -1)
return (-1);
if (size <= 0 || resp->http_method == HTTP_METHOD_HEAD) {
bufferevent_enable(clt->clt_bev, EV_READ|EV_WRITE);
if (clt->clt_persist)
clt->clt_toread = TOREAD_HTTP_HEADER;
else
clt->clt_toread = TOREAD_HTTP_NONE;
clt->clt_done = 0;
return (0);
}
return (1);
}
int
server_writeresponse_http(struct client *clt)
{
struct http_descriptor *desc = clt->clt_descresp;
DPRINTF("version: %s rescode: %s resmsg: %s", desc->http_version,
desc->http_rescode, desc->http_resmesg);
if (server_bufferevent_print(clt, desc->http_version) == -1 ||
server_bufferevent_print(clt, " ") == -1 ||
server_bufferevent_print(clt, desc->http_rescode) == -1 ||
server_bufferevent_print(clt, " ") == -1 ||
server_bufferevent_print(clt, desc->http_resmesg) == -1)
return (-1);
return (0);
}
int
server_writeheader_http(struct client *clt, struct kv *hdr, void *arg)
{
char *ptr;
const char *key;
/* The key might have been updated in the parent */
if (hdr->kv_parent != NULL && hdr->kv_parent->kv_key != NULL)
key = hdr->kv_parent->kv_key;
else
key = hdr->kv_key;
ptr = hdr->kv_value;
if (server_bufferevent_print(clt, key) == -1 ||
(ptr != NULL &&
(server_bufferevent_print(clt, ": ") == -1 ||
server_bufferevent_print(clt, ptr) == -1 ||
server_bufferevent_print(clt, "\r\n") == -1)))
return (-1);
DPRINTF("%s: %s: %s", __func__, key,
hdr->kv_value == NULL ? "" : hdr->kv_value);
return (0);
}
int
server_headers(struct client *clt, void *descp,
int (*hdr_cb)(struct client *, struct kv *, void *), void *arg)
{
struct kv *hdr, *kv;
struct http_descriptor *desc = descp;
RB_FOREACH(hdr, kvtree, &desc->http_headers) {
if ((hdr_cb)(clt, hdr, arg) == -1)
return (-1);
TAILQ_FOREACH(kv, &hdr->kv_children, kv_entry) {
if ((hdr_cb)(clt, kv, arg) == -1)
return (-1);
}
}
return (0);
}
enum httpmethod
server_httpmethod_byname(const char *name)
{
enum httpmethod id = HTTP_METHOD_NONE;
struct http_method method, *res = NULL;
/* Set up key */
method.method_name = name;
if ((res = bsearch(&method, http_methods,
sizeof(http_methods) / sizeof(http_methods[0]) - 1,
sizeof(http_methods[0]), server_httpmethod_cmp)) != NULL)
id = res->method_id;
return (id);
}
const char *
server_httpmethod_byid(unsigned int id)
{
const char *name = "<UNKNOWN>";
int i;
for (i = 0; http_methods[i].method_name != NULL; i++) {
if (http_methods[i].method_id == id) {
name = http_methods[i].method_name;
break;
}
}
return (name);
}
static int
server_httpmethod_cmp(const void *a, const void *b)
{
const struct http_method *ma = a;
const struct http_method *mb = b;
/*
* RFC 2616 section 5.1.1 says that the method is case
* sensitive so we don't do a strcasecmp here.
*/
return (strcmp(ma->method_name, mb->method_name));
}
const char *
server_httperror_byid(unsigned int id)
{
struct http_error error, *res;
/* Set up key */
error.error_code = (int)id;
if ((res = bsearch(&error, http_errors,
sizeof(http_errors) / sizeof(http_errors[0]) - 1,
sizeof(http_errors[0]), server_httperror_cmp)) != NULL)
return (res->error_name);
return (NULL);
}
static int
server_httperror_cmp(const void *a, const void *b)
{
const struct http_error *ea = a;
const struct http_error *eb = b;
return (ea->error_code - eb->error_code);
}
/*
* return -1 on failure, strlen() of read file otherwise.
* body is NULL on failure, contents of file with trailing \0 otherwise.
*/
char *
read_errdoc(const char *root, const char *file)
{
struct stat sb;
char *path;
int fd;
char *ret;
if (asprintf(&path, "%s/%s.html", root, file) == -1)
fatal("asprintf");
if ((fd = open(path, O_RDONLY)) == -1) {
free(path);
if (errno != ENOENT)
log_warn("%s: open", __func__);
return (NULL);
}
free(path);
if (fstat(fd, &sb) < 0) {
log_warn("%s: stat", __func__);
close(fd);
return (NULL);
}
if ((ret = calloc(1, sb.st_size + 1)) == NULL)
fatal("calloc");
if (sb.st_size == 0) {
close(fd);
return (ret);
}
if (read(fd, ret, sb.st_size) != sb.st_size) {
log_warn("%s: read", __func__);
close(fd);
free(ret);
return (NULL);
}
close(fd);
return (ret);
}
char *
replace_var(char *str, const char *var, const char *repl)
{
char *iv, *r;
size_t vlen;
vlen = strlen(var);
while ((iv = strstr(str, var)) != NULL) {
*iv = '\0';
if (asprintf(&r, "%s%s%s", str, repl, &iv[vlen]) == -1)
fatal("asprintf");
free(str);
str = r;
}
return (str);
}
int
server_log_http(struct client *clt, unsigned int code, size_t len)
{
static char tstamp[64];
static char ip[INET6_ADDRSTRLEN];
time_t t;
struct kv key, *agent, *referrer, *xff, *xfp;
struct tm *tm;
struct server_config *srv_conf;
struct http_descriptor *desc;
int ret = -1;
char *user = NULL;
char *path = NULL;
char *version = NULL;
char *referrer_v = NULL;
char *agent_v = NULL;
char *xff_v = NULL;
char *xfp_v = NULL;
if ((srv_conf = clt->clt_srv_conf) == NULL)
return (-1);
if ((srv_conf->flags & SRVFLAG_LOG) == 0)
return (0);
if ((desc = clt->clt_descreq) == NULL)
return (-1);
if ((t = time(NULL)) == -1)
return (-1);
if ((tm = localtime(&t)) == NULL)
return (-1);
if (strftime(tstamp, sizeof(tstamp), "%d/%b/%Y:%H:%M:%S %z", tm) == 0)
return (-1);
if (print_host(&clt->clt_ss, ip, sizeof(ip)) == NULL)
return (-1);
/*
* For details on common log format, see:
* https://httpd.apache.org/docs/current/mod/mod_log_config.html
*
* httpd's format is similar to these Apache LogFormats:
* "%v %h %l %u %t \"%r\" %>s %B"
* "%v %h %l %u %t \"%r\" %>s %B \"%{Referer}i\" \"%{User-agent}i\""
*/
switch (srv_conf->logformat) {
case LOG_FORMAT_COMMON:
/* Use vis to encode input values from the header */
if (clt->clt_remote_user &&
stravis(&user, clt->clt_remote_user, HTTPD_LOGVIS) == -1)
goto done;
if (desc->http_version &&
stravis(&version, desc->http_version, HTTPD_LOGVIS) == -1)
goto done;
/* The following should be URL-encoded */
if (desc->http_path &&
(path = url_encode(desc->http_path)) == NULL)
goto done;
ret = evbuffer_add_printf(clt->clt_log,
"%s %s - %s [%s] \"%s %s%s%s%s%s\" %03d %zu\n",
srv_conf->name, ip, clt->clt_remote_user == NULL ? "-" :
user, tstamp,
server_httpmethod_byid(desc->http_method),
desc->http_path == NULL ? "" : path,
desc->http_query == NULL ? "" : "?",
desc->http_query == NULL ? "" : desc->http_query,
desc->http_version == NULL ? "" : " ",
desc->http_version == NULL ? "" : version,
code, len);
break;
case LOG_FORMAT_COMBINED:
case LOG_FORMAT_FORWARDED:
key.kv_key = "Referer"; /* sic */
if ((referrer = kv_find(&desc->http_headers, &key)) != NULL &&
referrer->kv_value == NULL)
referrer = NULL;
key.kv_key = "User-Agent";
if ((agent = kv_find(&desc->http_headers, &key)) != NULL &&
agent->kv_value == NULL)
agent = NULL;
/* Use vis to encode input values from the header */
if (clt->clt_remote_user &&
stravis(&user, clt->clt_remote_user, HTTPD_LOGVIS) == -1)
goto done;
if (clt->clt_remote_user == NULL &&
clt->clt_tls_ctx != NULL &&
(srv_conf->tls_flags & TLSFLAG_CA) &&
tls_peer_cert_subject(clt->clt_tls_ctx) != NULL &&
stravis(&user, tls_peer_cert_subject(clt->clt_tls_ctx),
HTTPD_LOGVIS) == -1)
goto done;
if (desc->http_version &&
stravis(&version, desc->http_version, HTTPD_LOGVIS) == -1)
goto done;
if (agent &&
stravis(&agent_v, agent->kv_value, HTTPD_LOGVIS) == -1)
goto done;
/* The following should be URL-encoded */
if (desc->http_path &&
(path = url_encode(desc->http_path)) == NULL)
goto done;
if (referrer &&
(referrer_v = url_encode(referrer->kv_value)) == NULL)
goto done;
if ((ret = evbuffer_add_printf(clt->clt_log,
"%s %s - %s [%s] \"%s %s%s%s%s%s\""
" %03d %zu \"%s\" \"%s\"",
srv_conf->name, ip, user == NULL ? "-" :
user, tstamp,
server_httpmethod_byid(desc->http_method),
desc->http_path == NULL ? "" : path,
desc->http_query == NULL ? "" : "?",
desc->http_query == NULL ? "" : desc->http_query,
desc->http_version == NULL ? "" : " ",
desc->http_version == NULL ? "" : version,
code, len,
referrer == NULL ? "" : referrer_v,
agent == NULL ? "" : agent_v)) == -1)
break;
if (srv_conf->logformat == LOG_FORMAT_COMBINED)
goto finish;
xff = xfp = NULL;
key.kv_key = "X-Forwarded-For";
if ((xff = kv_find(&desc->http_headers, &key)) != NULL
&& xff->kv_value == NULL)
xff = NULL;
if (xff &&
stravis(&xff_v, xff->kv_value, HTTPD_LOGVIS) == -1)
goto finish;
key.kv_key = "X-Forwarded-Port";
if ((xfp = kv_find(&desc->http_headers, &key)) != NULL &&
(xfp->kv_value == NULL))
xfp = NULL;
if (xfp &&
stravis(&xfp_v, xfp->kv_value, HTTPD_LOGVIS) == -1)
goto finish;
if ((ret = evbuffer_add_printf(clt->clt_log, " %s %s",
xff == NULL ? "-" : xff_v,
xfp == NULL ? "-" : xfp_v)) == -1)
break;
finish:
ret = evbuffer_add_printf(clt->clt_log, "\n");
break;
case LOG_FORMAT_CONNECTION:
/* URL-encode the path */
if (desc->http_path &&
(path = url_encode(desc->http_path)) == NULL)
goto done;
ret = evbuffer_add_printf(clt->clt_log, " [%s]",
desc->http_path == NULL ? "" : path);
break;
}
done:
free(user);
free(path);
free(version);
free(referrer_v);
free(agent_v);
free(xff_v);
free(xfp_v);
return (ret);
}