/* $OpenBSD: server_http.c,v 1.103 2015/12/07 20:30:17 mmcc Exp $ */ /* * Copyright (c) 2006 - 2015 Reyk Floeter * * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 *); char *server_expand_http(struct client *, const char *, char *, size_t); static struct httpd *env = NULL; static struct http_method http_methods[] = HTTP_METHODS; static struct http_error http_errors[] = HTTP_ERRORS; void server_http(struct httpd *x_env) { if (x_env != NULL) env = x_env; 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_alias); desc->http_path_alias = NULL; free(desc->http_query); desc->http_query = 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 (clt_pass == 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); } void server_read_http(struct bufferevent *bev, void *arg) { struct client *clt = arg; struct server_config *srv_conf = clt->clt_srv_conf; struct http_descriptor *desc = clt->clt_descreq; struct evbuffer *src = EVBUFFER_INPUT(bev); char *line = NULL, *key, *value; const char *errstr; size_t size, linelen; 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_done && (line = evbuffer_readln(src, NULL, EVBUFFER_EOL_CRLF_STRICT)) != NULL) { linelen = strlen(line); /* * An empty line indicates the end of the request. * libevent already stripped the \r\n for us. */ if (!linelen) { clt->clt_done = 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 value = strchr(key, ':'); 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; desc->http_version = strchr(desc->http_path, ' '); if (desc->http_version == NULL) goto fail; *desc->http_version++ = '\0'; desc->http_query = strchr(desc->http_path, '?'); if (desc->http_query != NULL) *desc->http_query++ = '\0'; /* * Have to allocate the strings because they could * be changed independently by the filters later. */ if ((desc->http_version = strdup(desc->http_version)) == NULL) goto fail; if (desc->http_query != NULL && (desc->http_query = strdup(desc->http_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 ((size_t)clt->clt_toread > srv_conf->maxrequestbody) { server_abort_http(clt, 413, NULL); 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_done) { 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_DELETE: case HTTP_METHOD_GET: case HTTP_METHOD_HEAD: case HTTP_METHOD_OPTIONS: /* WebDAV methods */ case HTTP_METHOD_COPY: case HTTP_METHOD_MOVE: clt->clt_toread = 0; break; 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; /* Single-pass HTTP body */ if (clt->clt_toread < 0) { clt->clt_toread = TOREAD_UNLIMITED; bev->readcb = server_read; } 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(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_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_line = 0; clt->clt_done = 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 buf[IBUF_READ_SIZE]; char *escapedmsg = NULL; int 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: 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 (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 (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); /* 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"; /* Generate simple HTML error document */ if ((bodylen = asprintf(&body, "\n" "\n" "\n" "%03d %s\n" "\n" "\n" "\n" "

%03d %s

\n" "
\n
%s
\n" "\n" "\n", code, httperr, style, code, httperr, HTTPD_SERVERNAME)) == -1) goto done; if (srv_conf->flags & SRVFLAG_SERVER_HSTS) { 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) 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" "Content-Length: %d\r\n" "%s" "%s" "\r\n" "%s", code, httperr, tmbuf, HTTPD_SERVERNAME, bodylen, extraheader == NULL ? "" : extraheader, hstsheader == NULL ? "" : hstsheader, desc->http_method == HTTP_METHOD_HEAD ? "" : 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); 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") != NULL) { if (desc->http_query == NULL) { ret = expand_string(buf, len, "$QUERY_STRING", ""); } else { if ((query = url_encode(desc->http_query)) == NULL) return (NULL); ret = expand_string(buf, len, "$QUERY_STRING", query); free(query); } if (ret != 0) return (NULL); } 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 { if ((query = url_encode(desc->http_query)) == NULL) { free(path); return (NULL); } ret = asprintf(&str, "%s?%s", path, query); free(path); free(query); if (ret == -1) return (NULL); } ret = expand_string(buf, len, "$REQUEST_URI", str); free(str); 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; const char *errstr = NULL; /* Canonicalize the request path */ if (desc->http_path == NULL || url_decode(desc->http_path) == NULL || 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; } if (clt->clt_persist >= srv_conf->maxrequests) 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 != -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 ((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 */ srv_conf = server_getlocation(clt, desc->http_path); 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) { /* Replace host configuration */ clt->clt_srv_conf = srv_conf = location; break; } } } return (srv_conf); } 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) == -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 ((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) { 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; if (hdr->kv_flags & KV_FLAG_INVALID) return (0); /* 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 = ""; 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); } 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; struct tm *tm; struct server_config *srv_conf; struct http_descriptor *desc; int ret = -1; char *user = NULL; char *path = NULL; char *query = NULL; char *version = NULL; char *referrer_v = NULL; char *agent_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; if (desc->http_query && (query = url_encode(desc->http_query)) == 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 ? "" : query, desc->http_version == NULL ? "" : " ", desc->http_version == NULL ? "" : version, code, len); break; case LOG_FORMAT_COMBINED: 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 (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 (desc->http_query && (query = url_encode(desc->http_query)) == NULL) goto done; if (referrer && (referrer_v = url_encode(referrer->kv_value)) == NULL) goto done; ret = evbuffer_add_printf(clt->clt_log, "%s %s - %s [%s] \"%s %s%s%s%s%s\"" " %03d %zu \"%s\" \"%s\"\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 ? "" : query, desc->http_version == NULL ? "" : " ", desc->http_version == NULL ? "" : version, code, len, referrer == NULL ? "" : referrer_v, agent == NULL ? "" : agent_v); 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(query); free(version); free(referrer_v); free(agent_v); return (ret); }