diff --git a/src/base.h b/src/base.h index d6a4a1b8..f92b84f4 100644 --- a/src/base.h +++ b/src/base.h @@ -129,6 +129,8 @@ typedef union { #define HTTP_DATE BV(3) #define HTTP_LOCATION BV(4) #define HTTP_TRANSFER_ENCODING BV(5) +#define HTTP_CONTENT_LOCATION BV(6) +#define HTTP_SET_COOKIE BV(7) typedef struct { /** HEADER */ diff --git a/src/http-header-glue.c b/src/http-header-glue.c index 6a582067..b5238cd5 100644 --- a/src/http-header-glue.c +++ b/src/http-header-glue.c @@ -1083,6 +1083,9 @@ static int http_response_process_headers(server *srv, connection *con, http_resp (0 == strcasecmp(value, "Keep-Alive")) ? 1 : 0; con->parsed_response |= HTTP_CONNECTION; } + else if (0 == strncasecmp(key, "Set-Cookie", key_len)) { + con->parsed_response |= HTTP_SET_COOKIE; + } break; case 14: if (0 == strncasecmp(key, "Content-Length", key_len)) { @@ -1090,6 +1093,11 @@ static int http_response_process_headers(server *srv, connection *con, http_resp con->parsed_response |= HTTP_CONTENT_LENGTH; } break; + case 16: + if (0 == strncasecmp(key, "Content-Location", key_len)) { + con->parsed_response |= HTTP_CONTENT_LOCATION; + } + break; case 17: if (0 == strncasecmp(key, "Transfer-Encoding", key_len)) { if (opts->backend == BACKEND_PROXY) continue; diff --git a/src/mod_proxy.c b/src/mod_proxy.c index f91c6941..ed85e300 100644 --- a/src/mod_proxy.c +++ b/src/mod_proxy.c @@ -39,6 +39,18 @@ * - HTTP/1.1 persistent connection with upstream servers */ +/* (future: might split struct and move part to http-header-glue.c) */ +typedef struct http_header_remap_opts { + const array *urlpaths; + const array *hosts_request; + const array *hosts_response; + int https_remap; + /*(not used in plugin_config, but used in handler_ctx)*/ + const buffer *http_host; + const buffer *forwarded_host; + const data_string *forwarded_urlpath; +} http_header_remap_opts; + typedef enum { PROXY_BALANCE_UNSET, PROXY_BALANCE_FAIR, @@ -59,11 +71,13 @@ typedef enum { typedef struct { array *extensions; array *forwarded_params; + array *header_params; unsigned short debug; unsigned short replace_http_host; unsigned int forwarded; proxy_balance_t balance; + http_header_remap_opts header; } plugin_config; typedef struct { @@ -103,6 +117,7 @@ typedef struct { int fde_ndx; /* index into the fd-event buffer */ http_response_opts opts; + http_header_remap_opts remap_hdrs; plugin_config conf; connection *remote_conn; /* dumb pointer */ @@ -167,6 +182,7 @@ FREE_FUNC(mod_proxy_free) { array_free(s->extensions); array_free(s->forwarded_params); + array_free(s->header_params); free(s); } @@ -189,6 +205,7 @@ SETDEFAULTS_FUNC(mod_proxy_set_defaults) { { "proxy.balance", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 2 */ { "proxy.replace-http-host", NULL, T_CONFIG_BOOLEAN, T_CONFIG_SCOPE_CONNECTION }, /* 3 */ { "proxy.forwarded", NULL, T_CONFIG_ARRAY, T_CONFIG_SCOPE_CONNECTION }, /* 4 */ + { "proxy.header", NULL, T_CONFIG_ARRAY, T_CONFIG_SCOPE_CONNECTION }, /* 5 */ { NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET } }; @@ -204,12 +221,14 @@ SETDEFAULTS_FUNC(mod_proxy_set_defaults) { s->replace_http_host = 0; s->forwarded_params = array_init(); s->forwarded = PROXY_FORWARDED_NONE; + s->header_params = array_init(); cv[0].destination = s->extensions; cv[1].destination = &(s->debug); cv[2].destination = p->balance_buf; cv[3].destination = &(s->replace_http_host); cv[4].destination = s->forwarded_params; + cv[5].destination = s->header_params; buffer_reset(p->balance_buf); @@ -277,6 +296,45 @@ SETDEFAULTS_FUNC(mod_proxy_set_defaults) { } } + if (!array_is_kvany(s->header_params)) { + log_error_write(srv, __FILE__, __LINE__, "s", + "unexpected value for proxy.header; expected ( \"param\" => ( \"key\" => \"value\" ) )"); + return HANDLER_ERROR; + } + for (size_t j = 0, used = s->header_params->used; j < used; ++j) { + data_array *da = (data_array *)s->header_params->data[j]; + if (buffer_is_equal_string(da->key, CONST_STR_LEN("https-remap"))) { + data_string *ds = (data_string *)da; + if (ds->type != TYPE_STRING) { + log_error_write(srv, __FILE__, __LINE__, "s", + "unexpected value for proxy.header; expected \"enable\" or \"disable\" for https-remap"); + return HANDLER_ERROR; + } + s->header.https_remap = !buffer_is_equal_string(ds->value, CONST_STR_LEN("disable")) + && !buffer_is_equal_string(ds->value, CONST_STR_LEN("0")); + continue; + } + if (da->type != TYPE_ARRAY || !array_is_kvstring(da->value)) { + log_error_write(srv, __FILE__, __LINE__, "sb", + "unexpected value for proxy.header; expected ( \"param\" => ( \"key\" => \"value\" ) ) near key", da->key); + return HANDLER_ERROR; + } + if (buffer_is_equal_string(da->key, CONST_STR_LEN("map-urlpath"))) { + s->header.urlpaths = da->value; + } + else if (buffer_is_equal_string(da->key, CONST_STR_LEN("map-host-request"))) { + s->header.hosts_request = da->value; + } + else if (buffer_is_equal_string(da->key, CONST_STR_LEN("map-host-response"))) { + s->header.hosts_response = da->value; + } + else { + log_error_write(srv, __FILE__, __LINE__, "sb", + "unexpected key for proxy.header; expected ( \"param\" => ( \"key\" => \"value\" ) ) near key", da->key); + return HANDLER_ERROR; + } + } + if (NULL != (du = array_get_element(config->value, "proxy.server"))) { size_t j; data_array *da = (data_array *)du; @@ -670,6 +728,213 @@ static int proxy_establish_connection(server *srv, handler_ctx *hctx) { return 0; } + +/* (future: might move to http-header-glue.c) */ +static const buffer * http_header_remap_host_match (buffer *b, size_t off, http_header_remap_opts *remap_hdrs, int is_req, size_t alen) +{ + const array *hosts = is_req + ? remap_hdrs->hosts_request + : remap_hdrs->hosts_response; + if (hosts) { + const char * const s = b->ptr+off; + for (size_t i = 0, used = hosts->used; i < used; ++i) { + const data_string * const ds = (data_string *)hosts->data[i]; + const buffer *k = ds->key; + size_t mlen = buffer_string_length(k); + if (1 == mlen && k->ptr[0] == '-') { + /* match with authority provided in Host (if is_req) + * (If no Host in client request, then matching against empty + * string will probably not match, and no remap will be + * performed) */ + k = is_req + ? remap_hdrs->http_host + : remap_hdrs->forwarded_host; + if (NULL == k) continue; + mlen = buffer_string_length(k); + } + if (mlen == alen && 0 == strncasecmp(s, k->ptr, alen)) { + if (buffer_is_equal_string(ds->value, CONST_STR_LEN("-"))) { + return remap_hdrs->http_host; + } + else if (!buffer_string_is_empty(ds->value)) { + /*(save first matched request host for response match)*/ + if (is_req && NULL == remap_hdrs->forwarded_host) + remap_hdrs->forwarded_host = ds->value; + return ds->value; + } /*(else leave authority as-is and stop matching)*/ + break; + } + } + } + return NULL; +} + + +/* (future: might move to http-header-glue.c) */ +static size_t http_header_remap_host (buffer *b, size_t off, http_header_remap_opts *remap_hdrs, int is_req, size_t alen) +{ + const buffer * const m = + http_header_remap_host_match(b, off, remap_hdrs, is_req, alen); + if (NULL == m) return alen; /*(no match; return original authority length)*/ + + buffer_substr_replace(b, off, alen, m); + return buffer_string_length(m); /*(length of replacement authority)*/ +} + + +/* (future: might move to http-header-glue.c) */ +static void http_header_remap_urlpath (buffer *b, size_t off, http_header_remap_opts *remap_hdrs, int is_req) +{ + const array *urlpaths = remap_hdrs->urlpaths; + if (urlpaths) { + const char * const s = b->ptr+off; + const size_t plen = buffer_string_length(b) - off; /*(urlpath len)*/ + if (is_req) { /* request */ + for (size_t i = 0, used = urlpaths->used; i < used; ++i) { + const data_string * const ds = (data_string *)urlpaths->data[i]; + const size_t mlen = buffer_string_length(ds->key); + if (mlen <= plen && 0 == memcmp(s, ds->key->ptr, mlen)) { + if (NULL == remap_hdrs->forwarded_urlpath) + remap_hdrs->forwarded_urlpath = ds; + buffer_substr_replace(b, off, mlen, ds->value); + break; + } + } + } + else { /* response; perform reverse map */ + if (NULL != remap_hdrs->forwarded_urlpath) { + const data_string * const ds = remap_hdrs->forwarded_urlpath; + const size_t mlen = buffer_string_length(ds->value); + if (mlen <= plen && 0 == memcmp(s, ds->value->ptr, mlen)) { + buffer_substr_replace(b, off, mlen, ds->key); + return; + } + } + for (size_t i = 0, used = urlpaths->used; i < used; ++i) { + const data_string * const ds = (data_string *)urlpaths->data[i]; + const size_t mlen = buffer_string_length(ds->value); + if (mlen <= plen && 0 == memcmp(s, ds->value->ptr, mlen)) { + buffer_substr_replace(b, off, mlen, ds->key); + break; + } + } + } + } +} + + +/* (future: might move to http-header-glue.c) */ +static void http_header_remap_uri (buffer *b, size_t off, http_header_remap_opts *remap_hdrs, int is_req) +{ + /* find beginning of URL-path (might be preceded by scheme://authority + * (caller should make sure any leading whitespace is prior to offset) */ + if (b->ptr[off] != '/') { + char *s = b->ptr+off; + size_t alen; /*(authority len (host len))*/ + size_t slen; /*(scheme len)*/ + const buffer *m; + /* skip over scheme and authority of URI to find beginning of URL-path + * (value might conceivably be relative URL-path instead of URI) */ + if (NULL == (s = strchr(s, ':')) || s[1] != '/' || s[2] != '/') return; + slen = s - (b->ptr+off); + s += 3; + off = (size_t)(s - b->ptr); + if (NULL != (s = strchr(s, '/'))) { + alen = (size_t)(s - b->ptr) - off; + if (0 == alen) return; /*(empty authority, e.g. "http:///")*/ + } + else { + alen = buffer_string_length(b) - off; + if (0 == alen) return; /*(empty authority, e.g. "http:///")*/ + buffer_append_string_len(b, CONST_STR_LEN("/")); + } + + /* remap authority (if configured) and set offset to url-path */ + m = http_header_remap_host_match(b, off, remap_hdrs, is_req, alen); + if (NULL != m) { + if (remap_hdrs->https_remap + && (is_req ? 5==slen && 0==memcmp(b->ptr+off-slen-3,"https",5) + : 4==slen && 0==memcmp(b->ptr+off-slen-3,"http",4))){ + if (is_req) { + memcpy(b->ptr+off-slen-3+4,"://",3); /*("https"=>"http")*/ + --off; + ++alen; + } + else {/*(!is_req)*/ + memcpy(b->ptr+off-slen-3+4,"s://",4); /*("http" =>"https")*/ + ++off; + --alen; + } + } + buffer_substr_replace(b, off, alen, m); + alen = buffer_string_length(m);/*(length of replacement authority)*/ + } + off += alen; + } + + /* remap URLs (if configured) */ + http_header_remap_urlpath(b, off, remap_hdrs, is_req); +} + + +/* (future: might move to http-header-glue.c) */ +static void http_header_remap_setcookie (buffer *b, size_t off, http_header_remap_opts *remap_hdrs) +{ + /* Given the special-case of Set-Cookie and the (too) loosely restricted + * characters allowed, for best results, the Set-Cookie value should be the + * entire string in b from offset to end of string. In response headers, + * lighttpd may concatenate multiple Set-Cookie headers into single entry + * in con->response.headers, separated by "\r\nSet-Cookie: " */ + for (char *s, *n = b->ptr+off; (s = n); ) { + size_t len; + n = strchr(s, '\n'); + if (NULL == n) { + len = (size_t)(b->ptr + buffer_string_length(b) - s); + } + else { + len = (size_t)(n - s); + n += sizeof("Set-Cookie: "); /*(include +1 for '\n')*/ + } + for (char *e = s; NULL != (s = memchr(e, ';', len)); ) { + do { ++s; } while (*s == ' ' || *s == '\t'); + if ('\0' == s) return; + /*(interested only in Domain and Path attributes)*/ + e = memchr(s, '=', len - (size_t)(s - e)); + if (NULL == e) { e = s+1; continue; } + ++e; + switch ((int)(e - s - 1)) { + case 4: + if (0 == strncasecmp(s, "path", 4)) { + if (*e == '"') ++e; + if (*e != '/') continue; + off = (size_t)(e - b->ptr); + http_header_remap_urlpath(b, off, remap_hdrs, 0); + e = b->ptr+off; /*(b may have been reallocated)*/ + continue; + } + break; + case 6: + if (0 == strncasecmp(s, "domain", 6)) { + size_t alen = 0; + if (*e == '"') ++e; + if (*e == '.') ++e; + if (*e == ';') continue; + off = (size_t)(e - b->ptr); + for (char c; (c = e[alen]) != ';' && c != ' ' && c != '\t' + && c != '\r' && c != '\0'; ++alen); + len = http_header_remap_host(b, off, remap_hdrs, 0, alen); + e = b->ptr+off+len; /*(b may have been reallocated)*/ + continue; + } + break; + default: + break; + } + } + } +} + + static void proxy_append_header(connection *con, const char *key, const size_t klen, const char *value, const size_t vlen) { data_string *ds_dst; @@ -904,24 +1169,23 @@ static void proxy_set_Forwarded(connection *con, const unsigned int flags) { static int proxy_create_env(server *srv, handler_ctx *hctx) { - size_t i; - connection *con = hctx->remote_conn; - buffer *b; - int replace_http_host = 0; + buffer *b = buffer_init(); + const int remap_headers = (NULL != hctx->remap_hdrs.urlpaths + || NULL != hctx->remap_hdrs.hosts_request); + buffer_string_prepare_copy(b, 8192-1); /* build header */ - b = buffer_init(); - /* request line */ buffer_copy_string(b, get_http_method_name(con->request.http_method)); buffer_append_string_len(b, CONST_STR_LEN(" ")); - buffer_append_string_buffer(b, con->request.uri); + if (remap_headers) + http_header_remap_uri(b, buffer_string_length(b) - buffer_string_length(con->request.uri), &hctx->remap_hdrs, 1); buffer_append_string_len(b, CONST_STR_LEN(" HTTP/1.0\r\n")); + if (hctx->conf.replace_http_host && !buffer_string_is_empty(hctx->host->key)) { - replace_http_host = 1; if (hctx->conf.debug > 1) { log_error_write(srv, __FILE__, __LINE__, "SBS", "proxy - using \"", hctx->host->key, "\" as HTTP Host"); @@ -929,6 +1193,14 @@ static int proxy_create_env(server *srv, handler_ctx *hctx) { buffer_append_string_len(b, CONST_STR_LEN("Host: ")); buffer_append_string_buffer(b, hctx->host->key); buffer_append_string_len(b, CONST_STR_LEN("\r\n")); + } else if (!buffer_string_is_empty(con->request.http_host)) { + buffer_append_string_len(b, CONST_STR_LEN("Host: ")); + buffer_append_string_buffer(b, con->request.http_host); + if (remap_headers) { + size_t alen = buffer_string_length(con->request.http_host); + http_header_remap_host(b, buffer_string_length(b) - alen, &hctx->remap_hdrs, 1, alen); + } + buffer_append_string_len(b, CONST_STR_LEN("\r\n")); } /* "Forwarded" and legacy X- headers */ @@ -952,26 +1224,67 @@ static int proxy_create_env(server *srv, handler_ctx *hctx) { } /* request header */ - for (i = 0; i < con->request.headers->used; i++) { - data_string *ds; - - ds = (data_string *)con->request.headers->data[i]; - - if (!buffer_string_is_empty(ds->value) && !buffer_is_empty(ds->key)) { - if (replace_http_host && - buffer_is_equal_caseless_string(ds->key, CONST_STR_LEN("Host"))) continue; + for (size_t i = 0, used = con->request.headers->used; i < used; ++i) { + data_string *ds = (data_string *)con->request.headers->data[i]; + const size_t klen = buffer_string_length(ds->key); + size_t vlen; + switch (klen) { + default: + break; + case 4: + if (buffer_is_equal_caseless_string(ds->key, CONST_STR_LEN("Host"))) continue; /*(handled further above)*/ + break; + case 10: if (buffer_is_equal_caseless_string(ds->key, CONST_STR_LEN("Connection"))) continue; + if (buffer_is_equal_caseless_string(ds->key, CONST_STR_LEN("Set-Cookie"))) continue; /*(response header only; avoid accidental reflection)*/ + break; + case 16: if (buffer_is_equal_caseless_string(ds->key, CONST_STR_LEN("Proxy-Connection"))) continue; + break; + case 5: /* Do not emit HTTP_PROXY in environment. * Some executables use HTTP_PROXY to configure * outgoing proxy. See also https://httpoxy.org/ */ if (buffer_is_equal_caseless_string(ds->key, CONST_STR_LEN("Proxy"))) continue; + break; + case 0: + continue; + } + + vlen = buffer_string_length(ds->value); + if (0 == vlen) continue; - buffer_append_string_buffer(b, ds->key); - buffer_append_string_len(b, CONST_STR_LEN(": ")); - buffer_append_string_buffer(b, ds->value); - buffer_append_string_len(b, CONST_STR_LEN("\r\n")); + buffer_append_string_len(b, ds->key->ptr, klen); + buffer_append_string_len(b, CONST_STR_LEN(": ")); + buffer_append_string_len(b, ds->value->ptr, vlen); + buffer_append_string_len(b, CONST_STR_LEN("\r\n")); + + if (!remap_headers) continue; + + /* check for hdrs for which to remap URIs in-place after append to b */ + + switch (klen) { + default: + continue; + #if 0 /* "URI" is HTTP response header (non-standard; historical in Apache) */ + case 3: + if (buffer_is_equal_caseless_string(ds->key, CONST_STR_LEN("URI"))) break; + continue; + #endif + #if 0 /* "Location" is HTTP response header */ + case 8: + if (buffer_is_equal_caseless_string(ds->key, CONST_STR_LEN("Location"))) break; + continue; + #endif + case 11: /* "Destination" is WebDAV request header */ + if (buffer_is_equal_caseless_string(ds->key, CONST_STR_LEN("Destination"))) break; + continue; + case 16: /* "Content-Location" may be HTTP request or response header */ + if (buffer_is_equal_caseless_string(ds->key, CONST_STR_LEN("Content-Location"))) break; + continue; } + + http_header_remap_uri(b, buffer_string_length(b) - vlen - 2, &hctx->remap_hdrs, 1); } buffer_append_string_len(b, CONST_STR_LEN("Connection: close\r\n\r\n")); @@ -1150,6 +1463,7 @@ static int mod_proxy_patch_connection(server *srv, connection *con, plugin_data PATCH(balance); PATCH(replace_http_host); PATCH(forwarded); + PATCH(header); /*(copies struct)*/ /* skip the first, the global context */ for (i = 1; i < srv->config_context->used; i++) { @@ -1173,6 +1487,8 @@ static int mod_proxy_patch_connection(server *srv, connection *con, plugin_data PATCH(replace_http_host); } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("proxy.forwarded"))) { PATCH(forwarded); + } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("proxy.header"))) { + PATCH(header); /*(copies struct)*/ } } } @@ -1266,9 +1582,44 @@ SUBREQUEST_FUNC(mod_proxy_handle_subrequest) { } +static handler_t proxy_response_read(server *srv, handler_ctx *hctx) { + connection * const con = hctx->remote_conn; + const int file_started = con->file_started; + const handler_t rc = + http_response_read(srv, con, &hctx->opts, + hctx->response, hctx->fd, &hctx->fde_ndx); + + if (file_started || !con->file_started || con->mode == DIRECT) return rc; + + /* response headers just completed */ + + /* rewrite paths, if needed */ + + if (NULL == hctx->remap_hdrs.urlpaths + && NULL == hctx->remap_hdrs.hosts_response) + return rc; + + if (con->parsed_response & HTTP_LOCATION) { + data_string *ds = (data_string *) + array_get_element(con->response.headers, "Location"); + if (ds) http_header_remap_uri(ds->value, 0, &hctx->remap_hdrs, 0); + } + if (con->parsed_response & HTTP_CONTENT_LOCATION) { + data_string *ds = (data_string *) + array_get_element(con->response.headers, "Content-Location"); + if (ds) http_header_remap_uri(ds->value, 0, &hctx->remap_hdrs, 0); + } + if (con->parsed_response & HTTP_SET_COOKIE) { + data_string *ds = (data_string *) + array_get_element(con->response.headers, "Set-Cookie"); + if (ds) http_header_remap_setcookie(ds->value, 0, &hctx->remap_hdrs); + } + + return rc; +} + static handler_t proxy_recv_response(server *srv, handler_ctx *hctx) { - switch (http_response_read(srv, hctx->remote_conn, &hctx->opts, - hctx->response, hctx->fd, &hctx->fde_ndx)) { + switch (proxy_response_read(srv, hctx)) { default: return HANDLER_GO_ON; case HANDLER_ERROR: @@ -1409,6 +1760,17 @@ static handler_t mod_proxy_check_extension(server *srv, connection *con, void *p hctx->opts.xsendfile_allow = 0; hctx->opts.xsendfile_docroot = NULL; + hctx->remap_hdrs = p->conf.header; /*(copies struct)*/ + hctx->remap_hdrs.http_host = con->request.http_host; + /* mod_proxy currently sends all backend requests as http. + * https-remap is a flag since it might not be needed if backend + * honors Forwarded or X-Forwarded-Proto headers, e.g. by using + * lighttpd mod_extforward or similar functionality in backend*/ + if (hctx->remap_hdrs.https_remap) { + hctx->remap_hdrs.https_remap = + buffer_is_equal_string(con->uri.scheme, CONST_STR_LEN("https")); + } + con->plugin_ctx[p->id] = hctx; con->mode = p->id;