/* $OpenBSD: relay_http.c,v 1.87 2023/12/01 16:48:40 millert Exp $ */ /* * Copyright (c) 2006 - 2016 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 "relayd.h" #include "http.h" static int _relay_lookup_url(struct ctl_relay_event *, char *, char *, char *, struct kv *); int relay_lookup_url(struct ctl_relay_event *, const char *, struct kv *); int relay_lookup_query(struct ctl_relay_event *, struct kv *); int relay_lookup_cookie(struct ctl_relay_event *, const char *, struct kv *); void relay_read_httpcontent(struct bufferevent *, void *); void relay_read_httpchunks(struct bufferevent *, void *); char *relay_expand_http(struct ctl_relay_event *, char *, char *, size_t); int relay_writeheader_kv(struct ctl_relay_event *, struct kv *); int relay_writeheader_http(struct ctl_relay_event *, struct ctl_relay_event *); int relay_writerequest_http(struct ctl_relay_event *, struct ctl_relay_event *); int relay_writeresponse_http(struct ctl_relay_event *, struct ctl_relay_event *); void relay_reset_http(struct ctl_relay_event *); static int relay_httpmethod_cmp(const void *, const void *); static int relay_httperror_cmp(const void *, const void *); int relay_httpquery_test(struct ctl_relay_event *, struct relay_rule *, struct kvlist *); int relay_httpheader_test(struct ctl_relay_event *, struct relay_rule *, struct kvlist *); int relay_httppath_test(struct ctl_relay_event *, struct relay_rule *, struct kvlist *); int relay_httpurl_test(struct ctl_relay_event *, struct relay_rule *, struct kvlist *); int relay_httpcookie_test(struct ctl_relay_event *, struct relay_rule *, struct kvlist *); int relay_apply_actions(struct ctl_relay_event *, struct kvlist *, struct relay_table *); int relay_match_actions(struct ctl_relay_event *, struct relay_rule *, struct kvlist *, struct kvlist *, struct relay_table **); void relay_httpdesc_free(struct http_descriptor *); char * server_root_strip(char *, int); static struct relayd *env = NULL; static struct http_method http_methods[] = HTTP_METHODS; static struct http_error http_errors[] = HTTP_ERRORS; void relay_http(struct relayd *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]), relay_httpmethod_cmp); qsort(http_errors, sizeof(http_errors) / sizeof(http_errors[0]) - 1, sizeof(http_errors[0]), relay_httperror_cmp); } void relay_http_init(struct relay *rlay) { rlay->rl_proto->close = relay_close_http; relay_http(NULL); /* Calculate skip step for the filter rules (may take a while) */ relay_calc_skip_steps(&rlay->rl_proto->rules); } int relay_http_priv_init(struct rsession *con) { struct http_session *hs; if ((hs = calloc(1, sizeof(*hs))) == NULL) return (-1); SIMPLEQ_INIT(&hs->hs_methods); DPRINTF("%s: session %d http_session %p", __func__, con->se_id, hs); con->se_priv = hs; return (relay_httpdesc_init(&con->se_in)); } int relay_httpdesc_init(struct ctl_relay_event *cre) { struct http_descriptor *desc; if ((desc = calloc(1, sizeof(*desc))) == NULL) return (-1); RB_INIT(&desc->http_headers); cre->desc = desc; return (0); } void relay_httpdesc_free(struct http_descriptor *desc) { if (desc == NULL) return; free(desc->http_path); desc->http_path = NULL; free(desc->http_query); desc->http_query = NULL; free(desc->http_version); desc->http_version = NULL; free(desc->query_key); desc->query_key = NULL; free(desc->query_val); desc->query_val = NULL; kv_purge(&desc->http_headers); desc->http_lastheader = NULL; } static int relay_http_header_name_valid(const char *name) { /* * RFC 9110 specifies that only the following characters are * permitted within HTTP header field names. */ const char token_chars[] = "!#$%&'*+-.^_`|~0123456789" "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; const size_t len = strspn(name, token_chars); return (name[len] == '\0'); } void relay_read_http(struct bufferevent *bev, void *arg) { struct ctl_relay_event *cre = arg; struct http_descriptor *desc = cre->desc; struct rsession *con = cre->con; struct relay *rlay = con->se_relay; struct protocol *proto = rlay->rl_proto; struct evbuffer *src = EVBUFFER_INPUT(bev); char *line = NULL, *key, *value; char *urlproto, *host, *path; int action, unique, ret; const char *errstr; size_t size, linelen; struct kv *hdr = NULL; struct kv *upgrade = NULL, *upgrade_ws = NULL; struct kv *connection_close = NULL; int ws_response = 0; struct http_method_node *hmn; struct http_session *hs; enum httpmethod request_method; getmonotime(&con->se_tv_last); cre->timedout = 0; size = EVBUFFER_LENGTH(src); DPRINTF("%s: session %d: size %lu, to read %lld", __func__, con->se_id, size, cre->toread); if (size == 0) { if (cre->dir == RELAY_DIR_RESPONSE) return; cre->toread = TOREAD_HTTP_HEADER; goto done; } for (;;) { line = evbuffer_readln(src, &linelen, EVBUFFER_EOL_CRLF); if (line == NULL) { /* * We do not process the last header on premature * EOF as it may not be complete. */ break; } /* * An empty line indicates the end of the request. * libevent already stripped the \r\n for us. */ if (linelen == 0) { cre->done = 1; free(line); line = NULL; if (cre->line > 1) { /* Process last (complete) header line. */ goto last_header; } break; } /* Limit the total header length minus \r\n */ cre->headerlen += linelen; if (cre->headerlen > proto->httpheaderlen) { relay_abort_http(con, 413, "request headers too large", 0); goto abort; } /* Reject requests with an embedded NUL byte. */ if (memchr(line, '\0', linelen) != NULL) { relay_abort_http(con, 400, "malformed", 0); goto abort; } hs = con->se_priv; DPRINTF("%s: session %d http_session %p", __func__, con->se_id, hs); /* * The first line is the GET/POST/PUT/... request, * subsequent lines are HTTP headers. */ if (++cre->line == 1) { key = line; if ((value = strchr(key, ' ')) == NULL) { relay_abort_http(con, 400, "malformed", 0); goto abort; } *value++ = '\0'; if (cre->dir == RELAY_DIR_RESPONSE) { desc->http_method = HTTP_METHOD_RESPONSE; hmn = SIMPLEQ_FIRST(&hs->hs_methods); /* * There is nothing preventing the relay from * sending an unbalanced response. Be prepared. */ if (hmn == NULL) { request_method = HTTP_METHOD_NONE; DPRINTF("%s: session %d unbalanced " "response", __func__, con->se_id); } else { SIMPLEQ_REMOVE_HEAD(&hs->hs_methods, hmn_entry); request_method = hmn->hmn_method; DPRINTF("%s: session %d dequeuing %s", __func__, con->se_id, relay_httpmethod_byid(request_method)); free(hmn); } /* * Decode response path and query */ desc->http_version = strdup(key); if (desc->http_version == NULL) { free(line); goto fail; } desc->http_rescode = strdup(value); if (desc->http_rescode == NULL) { free(line); goto fail; } desc->http_resmesg = strchr(desc->http_rescode, ' '); if (desc->http_resmesg == NULL) { free(line); goto fail; } *desc->http_resmesg++ = '\0'; desc->http_resmesg = strdup(desc->http_resmesg); if (desc->http_resmesg == NULL) { free(line); goto fail; } desc->http_status = strtonum(desc->http_rescode, 100, 599, &errstr); if (errstr) { DPRINTF( "%s: http_status %s: errno %d, %s", __func__, desc->http_rescode, errno, errstr); free(line); goto fail; } DPRINTF("http_version %s http_rescode %s " "http_resmesg %s", desc->http_version, desc->http_rescode, desc->http_resmesg); } else if (cre->dir == RELAY_DIR_REQUEST) { desc->http_method = relay_httpmethod_byname(key); if (desc->http_method == HTTP_METHOD_NONE) { free(line); goto fail; } if ((hmn = calloc(1, sizeof *hmn)) == NULL) { free(line); goto fail; } hmn->hmn_method = desc->http_method; DPRINTF("%s: session %d enqueuing %s", __func__, con->se_id, relay_httpmethod_byid(hmn->hmn_method)); SIMPLEQ_INSERT_TAIL(&hs->hs_methods, hmn, hmn_entry); /* * Decode request path and query */ desc->http_path = strdup(value); if (desc->http_path == NULL) { free(line); goto fail; } desc->http_version = strchr(desc->http_path, ' '); if (desc->http_version == NULL) { free(line); 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) { free(line); goto fail; } if (desc->http_query != NULL && (desc->http_query = strdup(desc->http_query)) == NULL) { free(line); goto fail; } } free(line); continue; } /* Multiline headers wrap with a space or tab. */ if (*line == ' ' || *line == '\t') { if (cre->line == 2) { /* First header line cannot start with space. */ relay_abort_http(con, 400, "malformed", 0); goto abort; } /* Append line to the last header, if present */ if (kv_extend(&desc->http_headers, desc->http_lastheader, line) == NULL) { free(line); goto fail; } free(line); continue; } /* Process the last complete header line. */ last_header: if (desc->http_lastheader != NULL) { key = desc->http_lastheader->kv_key; value = desc->http_lastheader->kv_value; DPRINTF("%s: session %d: header '%s: %s'", __func__, con->se_id, key, value); if (desc->http_method != HTTP_METHOD_NONE && strcasecmp("Content-Length", key) == 0) { switch (desc->http_method) { case HTTP_METHOD_TRACE: case HTTP_METHOD_CONNECT: /* * These methods should not have a body * and thus no Content-Length header. */ relay_abort_http(con, 400, "malformed", 0); goto abort; case HTTP_METHOD_GET: case HTTP_METHOD_HEAD: case HTTP_METHOD_COPY: case HTTP_METHOD_MOVE: /* * We strip the body (if present) from * the GET, HEAD, COPY and MOVE methods * so strip Content-Length too. */ kv_delete(&desc->http_headers, desc->http_lastheader); break; case HTTP_METHOD_RESPONSE: /* * Strip Content-Length header from * HEAD responses since there is no * actual payload in the response. */ if (request_method == HTTP_METHOD_HEAD) { kv_delete(&desc->http_headers, desc->http_lastheader); break; } /* FALLTHROUGH */ default: /* * 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. */ if (*value == '+' || *value == '-') { errstr = "invalid"; } else { cre->toread = strtonum(value, 0, LLONG_MAX, &errstr); } if (errstr) { relay_abort_http(con, 500, errstr, 0); goto abort; } break; } /* * Response with a status code of 1xx * (Informational) or 204 (No Content) MUST * not have a Content-Length (rfc 7230 3.3.3) * Instead we check for value != 0 because there * are servers that do not follow the rfc and * send Content-Length: 0. */ if (desc->http_method == HTTP_METHOD_RESPONSE && (((desc->http_status >= 100 && desc->http_status < 200) || desc->http_status == 204)) && cre->toread != 0) { relay_abort_http(con, 502, "Bad Gateway", 0); goto abort; } } if (strcasecmp("Transfer-Encoding", key) == 0) { /* We don't support other encodings. */ if (strcasecmp("chunked", value) != 0) { relay_abort_http(con, 400, "malformed", 0); goto abort; } desc->http_chunked = 1; } if (strcasecmp("Host", key) == 0) { /* * The path may contain a URL. The host in the * URL has to match the Host: value. */ if (parse_url(desc->http_path, &urlproto, &host, &path) == 0) { ret = strcasecmp(host, value); free(urlproto); free(host); free(path); if (ret != 0) { relay_abort_http(con, 400, "malformed host", 0); goto abort; } } } } if (cre->done) break; /* Validate header field name and check for missing value. */ key = line; if ((value = strchr(line, ':')) == NULL) { relay_abort_http(con, 400, "malformed", 0); goto abort; } *value++ = '\0'; value += strspn(value, " \t\r\n"); if (!relay_http_header_name_valid(key)) { relay_abort_http(con, 400, "malformed", 0); goto abort; } /* The "Host" header must only occur once. */ unique = strcasecmp("Host", key) == 0; if ((hdr = kv_add(&desc->http_headers, key, value, unique)) == NULL) { relay_abort_http(con, 400, "malformed header", 0); goto abort; } desc->http_lastheader = hdr; free(line); } if (cre->done) { if (desc->http_method == HTTP_METHOD_NONE) { relay_abort_http(con, 406, "no method", 0); return; } action = relay_test(proto, cre); switch (action) { case RES_FAIL: relay_close(con, "filter rule failed", 1); return; case RES_BAD: relay_abort_http(con, 400, "Bad Request", con->se_label); return; case RES_INTERNAL: relay_abort_http(con, 500, "Internal Server Error", con->se_label); return; } if (action != RES_PASS) { relay_abort_http(con, 403, "Forbidden", con->se_label); return; } /* * HTTP 101 Switching Protocols */ upgrade = kv_find_value(&desc->http_headers, "Connection", "upgrade", ","); upgrade_ws = kv_find_value(&desc->http_headers, "Upgrade", "websocket", ","); ws_response = 0; if (cre->dir == RELAY_DIR_REQUEST && upgrade_ws != NULL) { if ((proto->httpflags & HTTPFLAG_WEBSOCKETS) == 0) { relay_abort_http(con, 403, "Websocket Forbidden", 0); return; } else if (upgrade == NULL) { relay_abort_http(con, 400, "Bad Websocket Request", 0); return; } else if (desc->http_method != HTTP_METHOD_GET) { relay_abort_http(con, 405, "Websocket Method Not Allowed", 0); return; } } else if (cre->dir == RELAY_DIR_RESPONSE && desc->http_status == 101) { if (upgrade_ws != NULL && upgrade != NULL && (proto->httpflags & HTTPFLAG_WEBSOCKETS)) { ws_response = 1; cre->dst->toread = TOREAD_UNLIMITED; cre->dst->bev->readcb = relay_read; } else { relay_abort_http(con, 502, "Bad Websocket Gateway", 0); return; } } connection_close = kv_find_value(&desc->http_headers, "Connection", "close", ","); switch (desc->http_method) { case HTTP_METHOD_CONNECT: /* Data stream */ cre->toread = TOREAD_UNLIMITED; bev->readcb = relay_read; break; case HTTP_METHOD_GET: case HTTP_METHOD_HEAD: /* WebDAV methods */ case HTTP_METHOD_COPY: case HTTP_METHOD_MOVE: cre->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 (cre->toread > 0) { bev->readcb = relay_read_httpcontent; } /* Single-pass HTTP body */ if (cre->toread < 0) { cre->toread = TOREAD_UNLIMITED; bev->readcb = relay_read; } break; default: /* HTTP handler */ cre->toread = TOREAD_HTTP_HEADER; bev->readcb = relay_read_http; break; } if (desc->http_chunked) { /* Chunked transfer encoding */ cre->toread = TOREAD_HTTP_CHUNK_LENGTH; bev->readcb = relay_read_httpchunks; } /* * Ask the server to close the connection after this request * since we don't read any further request headers. Only add * this header if it does not already exist or if this is a * outbound websocket upgrade response. */ if (cre->toread == TOREAD_UNLIMITED && connection_close == NULL && !ws_response) if (kv_add(&desc->http_headers, "Connection", "close", 0) == NULL) goto fail; if (cre->dir == RELAY_DIR_REQUEST) { if (relay_writerequest_http(cre->dst, cre) == -1) goto fail; } else { if (relay_writeresponse_http(cre->dst, cre) == -1) goto fail; } if (relay_bufferevent_print(cre->dst, "\r\n") == -1 || relay_writeheader_http(cre->dst, cre) == -1 || relay_bufferevent_print(cre->dst, "\r\n") == -1) goto fail; relay_reset_http(cre); done: if (cre->dir == RELAY_DIR_REQUEST && cre->toread <= 0 && cre->dst->state != STATE_CONNECTED) { if (rlay->rl_conf.fwdmode == FWD_TRANS) { relay_bindanyreq(con, 0, IPPROTO_TCP); return; } if (relay_connect(con) == -1) { relay_abort_http(con, 502, "session failed", 0); return; } } } if (con->se_done) { relay_close(con, "last http read (done)", 0); return; } switch (relay_splice(cre)) { case -1: relay_close(con, strerror(errno), 1); case 1: return; case 0: break; } bufferevent_enable(bev, EV_READ); if (EVBUFFER_LENGTH(src) && bev->readcb != relay_read_http) bev->readcb(bev, arg); /* The callback readcb() might have freed the session. */ return; fail: relay_abort_http(con, 500, strerror(errno), 0); return; abort: free(line); } void relay_read_httpcontent(struct bufferevent *bev, void *arg) { struct ctl_relay_event *cre = arg; struct rsession *con = cre->con; struct protocol *proto = con->se_relay->rl_proto; struct evbuffer *src = EVBUFFER_INPUT(bev); size_t size; getmonotime(&con->se_tv_last); cre->timedout = 0; size = EVBUFFER_LENGTH(src); DPRINTF("%s: session %d: size %lu, to read %lld", __func__, con->se_id, size, cre->toread); if (!size) return; if (relay_spliceadjust(cre) == -1) goto fail; if (cre->toread > 0) { /* Read content data */ if ((off_t)size > cre->toread) { size = cre->toread; if (relay_bufferevent_write_chunk(cre->dst, src, size) == -1) goto fail; cre->toread = 0; } else { if (relay_bufferevent_write_buffer(cre->dst, src) == -1) goto fail; cre->toread -= size; } DPRINTF("%s: done, size %lu, to read %lld", __func__, size, cre->toread); } if (cre->toread == 0) { cre->toread = TOREAD_HTTP_HEADER; bev->readcb = relay_read_http; } if (con->se_done) goto done; bufferevent_enable(bev, EV_READ); if (cre->dst->bev && EVBUFFER_LENGTH(EVBUFFER_OUTPUT(cre->dst->bev)) > (size_t)RELAY_MAX_PREFETCH * proto->tcpbufsiz) bufferevent_disable(cre->bev, EV_READ); if (bev->readcb != relay_read_httpcontent) bev->readcb(bev, arg); /* The callback readcb() might have freed the session. */ return; done: relay_close(con, "last http content read", 0); return; fail: relay_close(con, strerror(errno), 1); } void relay_read_httpchunks(struct bufferevent *bev, void *arg) { struct ctl_relay_event *cre = arg; struct rsession *con = cre->con; struct protocol *proto = con->se_relay->rl_proto; struct evbuffer *src = EVBUFFER_INPUT(bev); char *line, *ep; long long llval; size_t size, linelen; getmonotime(&con->se_tv_last); cre->timedout = 0; size = EVBUFFER_LENGTH(src); DPRINTF("%s: session %d: size %lu, to read %lld", __func__, con->se_id, size, cre->toread); if (!size) return; if (relay_spliceadjust(cre) == -1) goto fail; if (cre->toread > 0) { /* Read chunk data */ if ((off_t)size > cre->toread) { size = cre->toread; if (relay_bufferevent_write_chunk(cre->dst, src, size) == -1) goto fail; cre->toread = 0; } else { if (relay_bufferevent_write_buffer(cre->dst, src) == -1) goto fail; cre->toread -= size; } DPRINTF("%s: done, size %lu, to read %lld", __func__, size, cre->toread); } switch (cre->toread) { case TOREAD_HTTP_CHUNK_LENGTH: line = evbuffer_readln(src, &linelen, EVBUFFER_EOL_CRLF); if (line == NULL) { /* Ignore empty line, continue */ bufferevent_enable(bev, EV_READ); return; } if (linelen == 0) { free(line); goto next; } /* * Read prepended chunk size in hex without leading +0[Xx]. * The returned signed value must not be negative. */ if (line[0] == '+' || line[0] == '-' || (line[0] == '0' && (line[1] == 'x' || line[1] == 'X'))) { /* Reject values like 0xdead and 0XBEEF or +FEED. */ ep = line; } else { errno = 0; llval = strtoll(line, &ep, 16); } if (ep == line || *ep != '\0' || llval < 0 || (errno == ERANGE && llval == LLONG_MAX)) { free(line); relay_close(con, "invalid chunk size", 1); return; } if (relay_bufferevent_print(cre->dst, line) == -1 || relay_bufferevent_print(cre->dst, "\r\n") == -1) { free(line); goto fail; } free(line); if ((cre->toread = llval) == 0) { DPRINTF("%s: last chunk", __func__); cre->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, &linelen, EVBUFFER_EOL_CRLF); if (line == NULL) { /* Ignore empty line, continue */ bufferevent_enable(bev, EV_READ); return; } if (relay_bufferevent_print(cre->dst, line) == -1 || relay_bufferevent_print(cre->dst, "\r\n") == -1) { free(line); goto fail; } if (linelen == 0) { /* Switch to HTTP header mode */ cre->toread = TOREAD_HTTP_HEADER; bev->readcb = relay_read_http; } free(line); break; case 0: /* Chunk is terminated by an empty newline */ line = evbuffer_readln(src, &linelen, EVBUFFER_EOL_CRLF); free(line); if (relay_bufferevent_print(cre->dst, "\r\n") == -1) goto fail; cre->toread = TOREAD_HTTP_CHUNK_LENGTH; break; } next: if (con->se_done) goto done; bufferevent_enable(bev, EV_READ); if (cre->dst->bev && EVBUFFER_LENGTH(EVBUFFER_OUTPUT(cre->dst->bev)) > (size_t)RELAY_MAX_PREFETCH * proto->tcpbufsiz) bufferevent_disable(cre->bev, EV_READ); if (EVBUFFER_LENGTH(src)) bev->readcb(bev, arg); /* The callback readcb() might have freed the session. */ return; done: relay_close(con, "last http chunk read (done)", 0); return; fail: relay_close(con, strerror(errno), 1); } void relay_reset_http(struct ctl_relay_event *cre) { struct http_descriptor *desc = cre->desc; relay_httpdesc_free(desc); desc->http_method = 0; desc->http_chunked = 0; cre->headerlen = 0; cre->line = 0; cre->done = 0; } static int _relay_lookup_url(struct ctl_relay_event *cre, char *host, char *path, char *query, struct kv *kv) { struct rsession *con = cre->con; char *val, *md = NULL; int ret = RES_FAIL; const char *str = NULL; if (asprintf(&val, "%s%s%s%s", host, path, query == NULL ? "" : "?", query == NULL ? "" : query) == -1) { relay_abort_http(con, 500, "failed to allocate URL", 0); return (RES_FAIL); } switch (kv->kv_digest) { case DIGEST_SHA1: case DIGEST_MD5: if ((md = digeststr(kv->kv_digest, val, strlen(val), NULL)) == NULL) { relay_abort_http(con, 500, "failed to allocate digest", 0); goto fail; } str = md; break; case DIGEST_NONE: str = val; break; } DPRINTF("%s: session %d: %s, %s: %d", __func__, con->se_id, str, kv->kv_key, strcasecmp(kv->kv_key, str)); if (strcasecmp(kv->kv_key, str) == 0) { ret = RES_DROP; goto fail; } ret = RES_PASS; fail: free(md); free(val); return (ret); } int relay_lookup_url(struct ctl_relay_event *cre, const char *host, struct kv *kv) { struct http_descriptor *desc = (struct http_descriptor *)cre->desc; int i, j, dots; char *hi[RELAY_MAXLOOKUPLEVELS], *p, *pp, *c, ch; char ph[HOST_NAME_MAX+1]; int ret; if (desc->http_path == NULL) return (RES_PASS); /* * This is an URL lookup algorithm inspired by * http://code.google.com/apis/safebrowsing/ * developers_guide.html#PerformingLookups */ DPRINTF("%s: host '%s', path '%s', query '%s'", __func__, host, desc->http_path, desc->http_query == NULL ? "" : desc->http_query); if (canonicalize_host(host, ph, sizeof(ph)) == NULL) { return (RES_BAD); } bzero(hi, sizeof(hi)); for (dots = -1, i = strlen(ph) - 1; i > 0; i--) { if (ph[i] == '.' && ++dots) hi[dots - 1] = &ph[i + 1]; if (dots > (RELAY_MAXLOOKUPLEVELS - 2)) break; } if (dots == -1) dots = 0; hi[dots] = ph; if ((pp = strdup(desc->http_path)) == NULL) { return (RES_INTERNAL); } for (i = (RELAY_MAXLOOKUPLEVELS - 1); i >= 0; i--) { if (hi[i] == NULL) continue; /* 1. complete path with query */ if (desc->http_query != NULL) if ((ret = _relay_lookup_url(cre, hi[i], pp, desc->http_query, kv)) != RES_PASS) goto done; /* 2. complete path without query */ if ((ret = _relay_lookup_url(cre, hi[i], pp, NULL, kv)) != RES_PASS) goto done; /* 3. traverse path */ for (j = 0, p = strchr(pp, '/'); p != NULL; p = strchr(p, '/'), j++) { if (j > (RELAY_MAXLOOKUPLEVELS - 2) || *(++p) == '\0') break; c = &pp[p - pp]; ch = *c; *c = '\0'; if ((ret = _relay_lookup_url(cre, hi[i], pp, NULL, kv)) != RES_PASS) goto done; *c = ch; } } ret = RES_PASS; done: free(pp); return (ret); } int relay_lookup_cookie(struct ctl_relay_event *cre, const char *str, struct kv *kv) { char *val, *ptr, *key, *value; int ret; if ((val = strdup(str)) == NULL) { return (RES_INTERNAL); } for (ptr = val; ptr != NULL && strlen(ptr);) { if (*ptr == ' ') *ptr++ = '\0'; key = ptr; if ((ptr = strchr(ptr, ';')) != NULL) *ptr++ = '\0'; /* * XXX We do not handle attributes * ($Path, $Domain, or $Port) */ if (*key == '$') continue; if ((value = strchr(key, '=')) == NULL || strlen(value) < 1) continue; *value++ = '\0'; if (*value == '"') *value++ = '\0'; if (value[strlen(value) - 1] == '"') value[strlen(value) - 1] = '\0'; DPRINTF("%s: key %s = %s, %s = %s : %d", __func__, key, value, kv->kv_key, kv->kv_value, strcasecmp(kv->kv_key, key)); if (strcasecmp(kv->kv_key, key) == 0 && ((kv->kv_value == NULL) || (fnmatch(kv->kv_value, value, FNM_CASEFOLD) != FNM_NOMATCH))) { ret = RES_DROP; goto done; } } ret = RES_PASS; done: free(val); return (ret); } int relay_lookup_query(struct ctl_relay_event *cre, struct kv *kv) { struct http_descriptor *desc = cre->desc; struct kv *match = &desc->http_matchquery; char *val, *ptr, *tmpkey = NULL, *tmpval = NULL; int ret = -1; if (desc->http_query == NULL) return (-1); if ((val = strdup(desc->http_query)) == NULL) { return (RES_INTERNAL); } ptr = val; while (ptr != NULL && strlen(ptr)) { tmpkey = ptr; if ((ptr = strchr(ptr, '&')) != NULL) *ptr++ = '\0'; if ((tmpval = strchr(tmpkey, '=')) == NULL || strlen(tmpval) < 1) continue; *tmpval++ = '\0'; if (fnmatch(kv->kv_key, tmpkey, 0) != FNM_NOMATCH && (kv->kv_value == NULL || fnmatch(kv->kv_value, tmpval, 0) != FNM_NOMATCH)) break; else tmpkey = NULL; } if (tmpkey == NULL || tmpval == NULL) goto done; match->kv_key = strdup(tmpkey); if (match->kv_key == NULL) goto done; match->kv_value = strdup(tmpval); if (match->kv_key == NULL) goto done; ret = 0; done: free(val); return (ret); } ssize_t relay_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)); } void relay_abort_http(struct rsession *con, u_int code, const char *msg, u_int16_t labelid) { struct relay *rlay = con->se_relay; struct bufferevent *bev = con->se_in.bev; const char *httperr = NULL, *text = ""; char *httpmsg, *body = NULL; char tmbuf[32], hbuf[128]; const char *style, *label = NULL; int bodylen; if ((httperr = relay_httperror_byid(code)) == NULL) httperr = "Unknown Error"; if (labelid != 0) label = label_id2name(labelid); /* In some cases this function may be called from generic places */ if (rlay->rl_proto->type != RELAY_PROTO_HTTP || (rlay->rl_proto->flags & F_RETURN) == 0) { relay_close(con, msg, 0); return; } if (bev == NULL) goto done; /* Some system information */ if (print_host(&rlay->rl_conf.ss, hbuf, sizeof(hbuf)) == NULL) goto done; if (relay_http_time(time(NULL), tmbuf, sizeof(tmbuf)) <= 0) goto done; /* Do not send details of the Internal Server Error */ switch (code) { case 500: break; default: text = msg; break; } /* A CSS stylesheet allows minimal customization by the user */ style = (rlay->rl_proto->style != NULL) ? rlay->rl_proto->style : "body { background-color: #a00000; color: white; font-family: " "'Comic Sans MS', 'Chalkboard SE', 'Comic Neue', sans-serif; }\n" "hr { border: 0; border-bottom: 1px dashed; }\n"; /* Generate simple HTTP+HTML error document */ if ((bodylen = asprintf(&body, "\n" "\n" "\n" "%03d %s\n" "\n" "\n" "\n" "

%s

\n" "
%s
\n" "
%s
\n" "
%s at %s port %d
\n" "\n" "\n", code, httperr, style, httperr, text, label == NULL ? "" : label, RELAYD_SERVERNAME, hbuf, ntohs(rlay->rl_conf.port))) == -1) goto done; /* Generate simple HTTP+HTML error document */ 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" "\r\n" "%s", code, httperr, tmbuf, RELAYD_SERVERNAME, bodylen, body) == -1) goto done; /* Dump the message without checking for success */ relay_dump(&con->se_in, httpmsg, strlen(httpmsg)); free(httpmsg); done: free(body); if (asprintf(&httpmsg, "%s (%03d %s)", msg, code, httperr) == -1) relay_close(con, msg, 1); else { relay_close(con, httpmsg, 1); free(httpmsg); } } void relay_close_http(struct rsession *con) { struct http_session *hs = con->se_priv; struct http_method_node *hmn; DPRINTF("%s: session %d http_session %p", __func__, con->se_id, hs); if (hs != NULL) while (!SIMPLEQ_EMPTY(&hs->hs_methods)) { hmn = SIMPLEQ_FIRST(&hs->hs_methods); SIMPLEQ_REMOVE_HEAD(&hs->hs_methods, hmn_entry); DPRINTF("%s: session %d freeing %s", __func__, con->se_id, relay_httpmethod_byid(hmn->hmn_method)); free(hmn); } relay_httpdesc_free(con->se_in.desc); free(con->se_in.desc); relay_httpdesc_free(con->se_out.desc); free(con->se_out.desc); } char * relay_expand_http(struct ctl_relay_event *cre, char *val, char *buf, size_t len) { struct rsession *con = cre->con; struct relay *rlay = con->se_relay; struct http_descriptor *desc = cre->desc; struct kv *host, key; char ibuf[128]; if (strlcpy(buf, val, len) >= len) return (NULL); if (strstr(val, "$HOST") != NULL) { key.kv_key = "Host"; host = kv_find(&desc->http_headers, &key); if (host) { if (host->kv_value == NULL) return (NULL); snprintf(ibuf, sizeof(ibuf), "%s", host->kv_value); } else { if (print_host(&rlay->rl_conf.ss, ibuf, sizeof(ibuf)) == NULL) return (NULL); } if (expand_string(buf, len, "$HOST", ibuf)) return (NULL); } if (strstr(val, "$REMOTE_") != NULL) { if (strstr(val, "$REMOTE_ADDR") != NULL) { if (print_host(&cre->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(cre->port)); if (expand_string(buf, len, "$REMOTE_PORT", ibuf) != 0) return (NULL); } } if (strstr(val, "$SERVER_") != NULL) { if (strstr(val, "$SERVER_ADDR") != NULL) { if (print_host(&rlay->rl_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(rlay->rl_conf.port)); if (expand_string(buf, len, "$SERVER_PORT", ibuf) != 0) return (NULL); } if (strstr(val, "$SERVER_NAME") != NULL) { if (expand_string(buf, len, "$SERVER_NAME", RELAYD_SERVERNAME) != 0) return (NULL); } } if (strstr(val, "$TIMEOUT") != NULL) { snprintf(ibuf, sizeof(ibuf), "%lld", (long long)rlay->rl_conf.timeout.tv_sec); if (expand_string(buf, len, "$TIMEOUT", ibuf) != 0) return (NULL); } return (buf); } int relay_writerequest_http(struct ctl_relay_event *dst, struct ctl_relay_event *cre) { struct http_descriptor *desc = (struct http_descriptor *)cre->desc; const char *name = NULL; if ((name = relay_httpmethod_byid(desc->http_method)) == NULL) return (-1); if (relay_bufferevent_print(dst, name) == -1 || relay_bufferevent_print(dst, " ") == -1 || relay_bufferevent_print(dst, desc->http_path) == -1 || (desc->http_query != NULL && (relay_bufferevent_print(dst, "?") == -1 || relay_bufferevent_print(dst, desc->http_query) == -1)) || relay_bufferevent_print(dst, " ") == -1 || relay_bufferevent_print(dst, desc->http_version) == -1) return (-1); return (0); } int relay_writeresponse_http(struct ctl_relay_event *dst, struct ctl_relay_event *cre) { struct http_descriptor *desc = (struct http_descriptor *)cre->desc; DPRINTF("version: %s rescode: %s resmsg: %s", desc->http_version, desc->http_rescode, desc->http_resmesg); if (relay_bufferevent_print(dst, desc->http_version) == -1 || relay_bufferevent_print(dst, " ") == -1 || relay_bufferevent_print(dst, desc->http_rescode) == -1 || relay_bufferevent_print(dst, " ") == -1 || relay_bufferevent_print(dst, desc->http_resmesg) == -1) return (-1); return (0); } int relay_writeheader_kv(struct ctl_relay_event *dst, struct kv *hdr) { 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 (relay_bufferevent_print(dst, key) == -1 || (ptr != NULL && (relay_bufferevent_print(dst, ": ") == -1 || relay_bufferevent_print(dst, ptr) == -1 || relay_bufferevent_print(dst, "\r\n") == -1))) return (-1); DPRINTF("%s: %s: %s", __func__, key, hdr->kv_value == NULL ? "" : hdr->kv_value); return (0); } int relay_writeheader_http(struct ctl_relay_event *dst, struct ctl_relay_event *cre) { struct kv *hdr, *kv; struct http_descriptor *desc = (struct http_descriptor *)cre->desc; RB_FOREACH(hdr, kvtree, &desc->http_headers) { if (relay_writeheader_kv(dst, hdr) == -1) return (-1); TAILQ_FOREACH(kv, &hdr->kv_children, kv_entry) { if (relay_writeheader_kv(dst, kv) == -1) return (-1); } } return (0); } enum httpmethod relay_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]), relay_httpmethod_cmp)) != NULL) id = res->method_id; return (id); } const char * relay_httpmethod_byid(u_int id) { const char *name = NULL; 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 relay_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 * relay_httperror_byid(u_int id) { struct http_error error, *res = NULL; /* Set up key */ error.error_code = (int)id; res = bsearch(&error, http_errors, sizeof(http_errors) / sizeof(http_errors[0]) - 1, sizeof(http_errors[0]), relay_httperror_cmp); return (res->error_name); } static int relay_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 relay_httpquery_test(struct ctl_relay_event *cre, struct relay_rule *rule, struct kvlist *actions) { struct http_descriptor *desc = cre->desc; struct kv *match = &desc->http_matchquery; struct kv *kv = &rule->rule_kv[KEY_TYPE_QUERY]; int res = 0; if (cre->dir == RELAY_DIR_RESPONSE || kv->kv_type != KEY_TYPE_QUERY) return (0); else if (kv->kv_key == NULL) return (0); else if ((res = relay_lookup_query(cre, kv)) != 0) return (res); relay_match(actions, kv, match, NULL); return (0); } int relay_httpheader_test(struct ctl_relay_event *cre, struct relay_rule *rule, struct kvlist *actions) { struct http_descriptor *desc = cre->desc; struct kv *kv = &rule->rule_kv[KEY_TYPE_HEADER]; struct kv *match; if (kv->kv_type != KEY_TYPE_HEADER) return (0); match = kv_find(&desc->http_headers, kv); if (kv->kv_option == KEY_OPTION_APPEND || kv->kv_option == KEY_OPTION_SET) { /* header can be NULL and will be added later */ } else if (match == NULL) { /* Fail if header doesn't exist */ return (-1); } else { if (fnmatch(kv->kv_key, match->kv_key, FNM_CASEFOLD) == FNM_NOMATCH) return (-1); if (kv->kv_value != NULL && match->kv_value != NULL && fnmatch(kv->kv_value, match->kv_value, 0) == FNM_NOMATCH) return (-1); } relay_match(actions, kv, match, &desc->http_headers); return (0); } int relay_httppath_test(struct ctl_relay_event *cre, struct relay_rule *rule, struct kvlist *actions) { struct http_descriptor *desc = cre->desc; struct kv *kv = &rule->rule_kv[KEY_TYPE_PATH]; struct kv *match = &desc->http_pathquery; const char *query; if (cre->dir == RELAY_DIR_RESPONSE || kv->kv_type != KEY_TYPE_PATH) return (0); else if (kv->kv_option != KEY_OPTION_STRIP) { if (kv->kv_key == NULL) return (0); else if (fnmatch(kv->kv_key, desc->http_path, 0) == FNM_NOMATCH) return (-1); else if (kv->kv_value != NULL && kv->kv_option == KEY_OPTION_NONE) { query = desc->http_query == NULL ? "" : desc->http_query; if (fnmatch(kv->kv_value, query, FNM_CASEFOLD) == FNM_NOMATCH) return (-1); } } relay_match(actions, kv, match, NULL); return (0); } int relay_httpurl_test(struct ctl_relay_event *cre, struct relay_rule *rule, struct kvlist *actions) { struct http_descriptor *desc = cre->desc; struct kv *host, key; struct kv *kv = &rule->rule_kv[KEY_TYPE_URL]; struct kv *match = &desc->http_pathquery; int res; if (cre->dir == RELAY_DIR_RESPONSE || kv->kv_type != KEY_TYPE_URL || kv->kv_key == NULL) return (0); key.kv_key = "Host"; host = kv_find(&desc->http_headers, &key); if (host == NULL || host->kv_value == NULL) return (0); else if (rule->rule_action != RULE_ACTION_BLOCK && kv->kv_option == KEY_OPTION_LOG && fnmatch(kv->kv_key, match->kv_key, FNM_CASEFOLD) != FNM_NOMATCH) { /* fnmatch url only for logging */ } else if ((res = relay_lookup_url(cre, host->kv_value, kv)) != 0) return (res); relay_match(actions, kv, match, NULL); return (0); } int relay_httpcookie_test(struct ctl_relay_event *cre, struct relay_rule *rule, struct kvlist *actions) { struct http_descriptor *desc = cre->desc; struct kv *kv = &rule->rule_kv[KEY_TYPE_COOKIE], key; struct kv *match = NULL; int res; if (kv->kv_type != KEY_TYPE_COOKIE) return (0); switch (cre->dir) { case RELAY_DIR_REQUEST: key.kv_key = "Cookie"; break; case RELAY_DIR_RESPONSE: key.kv_key = "Set-Cookie"; break; default: return (0); /* NOTREACHED */ break; } if (kv->kv_option == KEY_OPTION_APPEND || kv->kv_option == KEY_OPTION_SET) { /* no cookie, can be NULL and will be added later */ } else { match = kv_find(&desc->http_headers, &key); if (match == NULL) return (-1); if (kv->kv_key == NULL || match->kv_value == NULL) return (0); else if ((res = relay_lookup_cookie(cre, match->kv_value, kv)) != 0) return (res); } relay_match(actions, kv, match, &desc->http_headers); return (0); } int relay_match_actions(struct ctl_relay_event *cre, struct relay_rule *rule, struct kvlist *matches, struct kvlist *actions, struct relay_table **tbl) { struct rsession *con = cre->con; struct kv *kv; /* * Apply the following options instantly (action per match). */ if (rule->rule_table != NULL) { *tbl = rule->rule_table; con->se_out.ss.ss_family = AF_UNSPEC; } if (rule->rule_tag != 0) con->se_tag = rule->rule_tag == -1 ? 0 : rule->rule_tag; if (rule->rule_label != 0) con->se_label = rule->rule_label == -1 ? 0 : rule->rule_label; /* * Apply the remaining options once after evaluation. */ if (matches == NULL) { /* 'pass' or 'block' rule */ TAILQ_CONCAT(actions, &rule->rule_kvlist, kv_rule_entry); } else { /* 'match' rule */ TAILQ_FOREACH(kv, matches, kv_match_entry) { TAILQ_INSERT_TAIL(actions, kv, kv_action_entry); } } return (0); } int relay_apply_actions(struct ctl_relay_event *cre, struct kvlist *actions, struct relay_table *tbl) { struct rsession *con = cre->con; struct http_descriptor *desc = cre->desc; struct kv *host = NULL; const char *value; struct kv *kv, *match, *kp, *mp, kvcopy, matchcopy, key; int addkv, ret, nstrip; char buf[IBUF_READ_SIZE], *ptr; char *msg = NULL; const char *meth = NULL; memset(&kvcopy, 0, sizeof(kvcopy)); memset(&matchcopy, 0, sizeof(matchcopy)); ret = -1; kp = mp = NULL; TAILQ_FOREACH(kv, actions, kv_action_entry) { kp = NULL; match = kv->kv_match; addkv = 0; /* * Although marked as deleted, give a chance to non-critical * actions, ie. log, to be performed */ if (match != NULL && (match->kv_flags & KV_FLAG_INVALID)) goto matchdel; switch (kv->kv_option) { case KEY_OPTION_APPEND: case KEY_OPTION_SET: switch (kv->kv_type) { case KEY_TYPE_PATH: if (kv->kv_option == KEY_OPTION_APPEND) { if (kv_setkey(match, "%s%s", match->kv_key, kv->kv_key) == -1) goto fail; } else { if (kv_setkey(match, "%s", kv->kv_value) == -1) goto fail; } break; case KEY_TYPE_COOKIE: kp = &kvcopy; if (kv_inherit(kp, kv) == NULL) goto fail; if (kv_set(kp, "%s=%s;", kp->kv_key, kp->kv_value) == -1) goto fail; if (kv_setkey(kp, "%s", cre->dir == RELAY_DIR_REQUEST ? "Cookie" : "Set-Cookie") == -1) goto fail; /* FALLTHROUGH cookie is a header */ case KEY_TYPE_HEADER: if (match == NULL) { addkv = 1; break; } if (match->kv_value == NULL || kv->kv_option == KEY_OPTION_SET) { if (kv_set(match, "%s", kv->kv_value) == -1) goto fail; } else addkv = 1; break; default: /* query, url not supported */ break; } break; case KEY_OPTION_REMOVE: switch (kv->kv_type) { case KEY_TYPE_PATH: if (kv_setkey(match, "/") == -1) goto fail; break; case KEY_TYPE_COOKIE: case KEY_TYPE_HEADER: if (kv->kv_matchtree != NULL) match->kv_flags |= KV_FLAG_INVALID; else kv_free(match); match = kv->kv_match = NULL; break; default: /* query and url not supported */ break; } break; case KEY_OPTION_HASH: switch (kv->kv_type) { case KEY_TYPE_PATH: value = match->kv_key; break; default: value = match->kv_value; break; } SipHash24_Update(&con->se_siphashctx, value, strlen(value)); break; case KEY_OPTION_LOG: /* perform this later */ break; case KEY_OPTION_STRIP: nstrip = strtonum(kv->kv_value, 0, INT_MAX, NULL); if (kv->kv_type == KEY_TYPE_PATH) { if (kv_setkey(match, "%s", server_root_strip(match->kv_key, nstrip)) == -1) goto fail; } break; default: fatalx("%s: invalid action", __func__); /* NOTREACHED */ } /* from now on, reads from kp writes to kv */ if (kp == NULL) kp = kv; if (addkv && kv->kv_matchtree != NULL) { /* Add new entry to the list (eg. new HTTP header) */ if ((match = kv_add(kv->kv_matchtree, kp->kv_key, kp->kv_value, 0)) == NULL) goto fail; match->kv_option = kp->kv_option; match->kv_type = kp->kv_type; kv->kv_match = match; } if (match != NULL && kp->kv_flags & KV_FLAG_MACRO) { bzero(buf, sizeof(buf)); if ((ptr = relay_expand_http(cre, kp->kv_value, buf, sizeof(buf))) == NULL) goto fail; if (kv_set(match, "%s", ptr) == -1) goto fail; } matchdel: switch (kv->kv_option) { case KEY_OPTION_LOG: if (match == NULL) break; mp = &matchcopy; if (kv_inherit(mp, match) == NULL) goto fail; if (mp->kv_flags & KV_FLAG_INVALID) { if (kv_set(mp, "%s (removed)", mp->kv_value) == -1) goto fail; } switch (kv->kv_type) { case KEY_TYPE_URL: key.kv_key = "Host"; host = kv_find(&desc->http_headers, &key); switch (kv->kv_digest) { case DIGEST_NONE: if (host == NULL || host->kv_value == NULL) break; if (kv_setkey(mp, "%s%s", host->kv_value, mp->kv_key) == -1) goto fail; break; default: if (kv_setkey(mp, "%s", kv->kv_key) == -1) goto fail; break; } break; default: break; } if (kv_log(con, mp, con->se_label, cre->dir) == -1) goto fail; break; default: break; } /* actions applied, cleanup kv */ kv->kv_match = NULL; kv->kv_matchtree = NULL; TAILQ_REMOVE(actions, kv, kv_match_entry); kv_free(&kvcopy); kv_free(&matchcopy); } /* * Change the backend if the forward table has been changed. * This only works in the request direction. */ if (cre->dir == RELAY_DIR_REQUEST && con->se_table != tbl) { relay_reset_event(con, &con->se_out); con->se_table = tbl; con->se_haslog = 1; } /* * log tag for request and response, request method * and end of request marker "," */ if ((con->se_log != NULL) && ((meth = relay_httpmethod_byid(desc->http_method)) != NULL) && (asprintf(&msg, " %s", meth) != -1)) evbuffer_add(con->se_log, msg, strlen(msg)); free(msg); relay_log(con, cre->dir == RELAY_DIR_REQUEST ? "" : ";"); ret = 0; fail: kv_free(&kvcopy); kv_free(&matchcopy); return (ret); } #define RELAY_GET_SKIP_STEP(i) \ do { \ r = r->rule_skip[i]; \ DPRINTF("%s:%d: skip %d rules", __func__, __LINE__, i); \ } while (0) #define RELAY_GET_NEXT_STEP \ do { \ DPRINTF("%s:%d: next rule", __func__, __LINE__); \ goto nextrule; \ } while (0) int relay_test(struct protocol *proto, struct ctl_relay_event *cre) { struct rsession *con; struct http_descriptor *desc = cre->desc; struct relay_rule *r = NULL, *rule = NULL; struct relay_table *tbl = NULL; u_int cnt = 0; u_int action = RES_PASS; struct kvlist actions, matches; struct kv *kv; int res = 0; con = cre->con; TAILQ_INIT(&actions); r = TAILQ_FIRST(&proto->rules); while (r != NULL) { cnt++; TAILQ_INIT(&matches); TAILQ_INIT(&r->rule_kvlist); if (r->rule_dir && r->rule_dir != cre->dir) RELAY_GET_SKIP_STEP(RULE_SKIP_DIR); else if (proto->type != r->rule_proto) RELAY_GET_SKIP_STEP(RULE_SKIP_PROTO); else if (RELAY_AF_NEQ(r->rule_af, cre->ss.ss_family) || RELAY_AF_NEQ(r->rule_af, cre->dst->ss.ss_family)) RELAY_GET_SKIP_STEP(RULE_SKIP_AF); else if (RELAY_ADDR_CMP(&r->rule_src, &cre->ss) != 0) RELAY_GET_SKIP_STEP(RULE_SKIP_SRC); else if (RELAY_ADDR_CMP(&r->rule_dst, &con->se_sockname) != 0) RELAY_GET_SKIP_STEP(RULE_SKIP_DST); else if (r->rule_method != HTTP_METHOD_NONE && (desc->http_method == HTTP_METHOD_RESPONSE || desc->http_method != r->rule_method)) RELAY_GET_SKIP_STEP(RULE_SKIP_METHOD); else if (r->rule_tagged && con->se_tag != r->rule_tagged) RELAY_GET_NEXT_STEP; else if (relay_httpheader_test(cre, r, &matches) != 0) RELAY_GET_NEXT_STEP; else if ((res = relay_httpquery_test(cre, r, &matches)) != 0) RELAY_GET_NEXT_STEP; else if (relay_httppath_test(cre, r, &matches) != 0) RELAY_GET_NEXT_STEP; else if ((res = relay_httpurl_test(cre, r, &matches)) != 0) RELAY_GET_NEXT_STEP; else if ((res = relay_httpcookie_test(cre, r, &matches)) != 0) RELAY_GET_NEXT_STEP; else { DPRINTF("%s: session %d: matched rule %d", __func__, con->se_id, r->rule_id); if (r->rule_action == RULE_ACTION_MATCH) { if (relay_match_actions(cre, r, &matches, &actions, &tbl) != 0) { /* Something bad happened, drop */ action = RES_DROP; break; } RELAY_GET_NEXT_STEP; } else if (r->rule_action == RULE_ACTION_BLOCK) action = RES_DROP; else if (r->rule_action == RULE_ACTION_PASS) action = RES_PASS; /* Rule matched */ rule = r; /* Temporarily save actions */ TAILQ_FOREACH(kv, &matches, kv_match_entry) { TAILQ_INSERT_TAIL(&rule->rule_kvlist, kv, kv_rule_entry); } if (rule->rule_flags & RULE_FLAG_QUICK) break; nextrule: /* Continue to find last matching policy */ DPRINTF("%s: session %d, res %d", __func__, con->se_id, res); if (res == RES_BAD || res == RES_INTERNAL) return (res); res = 0; r = TAILQ_NEXT(r, rule_entry); } } if (rule != NULL && relay_match_actions(cre, rule, NULL, &actions, &tbl) != 0) { /* Something bad happened, drop */ action = RES_DROP; } if (relay_apply_actions(cre, &actions, tbl) != 0) { /* Something bad happened, drop */ action = RES_DROP; } DPRINTF("%s: session %d: action %d", __func__, con->se_id, action); return (action); } #define RELAY_SET_SKIP_STEPS(i) \ do { \ while (head[i] != cur) { \ head[i]->rule_skip[i] = cur; \ head[i] = TAILQ_NEXT(head[i], rule_entry); \ } \ } while (0) /* This code is derived from pf_calc_skip_steps() from pf.c */ void relay_calc_skip_steps(struct relay_rules *rules) { struct relay_rule *head[RULE_SKIP_COUNT], *cur, *prev; int i; cur = TAILQ_FIRST(rules); prev = cur; for (i = 0; i < RULE_SKIP_COUNT; ++i) head[i] = cur; while (cur != NULL) { if (cur->rule_dir != prev->rule_dir) RELAY_SET_SKIP_STEPS(RULE_SKIP_DIR); else if (cur->rule_proto != prev->rule_proto) RELAY_SET_SKIP_STEPS(RULE_SKIP_PROTO); else if (RELAY_AF_NEQ(cur->rule_af, prev->rule_af)) RELAY_SET_SKIP_STEPS(RULE_SKIP_AF); else if (RELAY_ADDR_NEQ(&cur->rule_src, &prev->rule_src)) RELAY_SET_SKIP_STEPS(RULE_SKIP_SRC); else if (RELAY_ADDR_NEQ(&cur->rule_dst, &prev->rule_dst)) RELAY_SET_SKIP_STEPS(RULE_SKIP_DST); else if (cur->rule_method != prev->rule_method) RELAY_SET_SKIP_STEPS(RULE_SKIP_METHOD); prev = cur; cur = TAILQ_NEXT(cur, rule_entry); } for (i = 0; i < RULE_SKIP_COUNT; ++i) RELAY_SET_SKIP_STEPS(i); } void relay_match(struct kvlist *actions, struct kv *kv, struct kv *match, struct kvtree *matchtree) { if (kv->kv_option != KEY_OPTION_NONE) { kv->kv_match = match; kv->kv_matchtree = matchtree; TAILQ_INSERT_TAIL(actions, kv, kv_match_entry); } } char * server_root_strip(char *path, int n) { char *p; /* Strip strip leading directories. Leading '/' is ignored. */ for (; n > 0 && *path != '\0'; n--) if ((p = strchr(++path, '/')) != NULL) path = p; else path--; return (path); }