diff options
Diffstat (limited to 'data/lighttpd/lighttpd-1.4.53/src/mod_proxy.c')
-rw-r--r-- | data/lighttpd/lighttpd-1.4.53/src/mod_proxy.c | 1038 |
1 files changed, 1038 insertions, 0 deletions
diff --git a/data/lighttpd/lighttpd-1.4.53/src/mod_proxy.c b/data/lighttpd/lighttpd-1.4.53/src/mod_proxy.c new file mode 100644 index 000000000..91f9c2f3c --- /dev/null +++ b/data/lighttpd/lighttpd-1.4.53/src/mod_proxy.c @@ -0,0 +1,1038 @@ +#include "first.h" + +#include <string.h> +#include <stdlib.h> + +#include "gw_backend.h" +#include "base.h" +#include "array.h" +#include "buffer.h" +#include "http_kv.h" +#include "http_header.h" +#include "log.h" +#include "sock_addr.h" +#include "status_counter.h" + +/** + * + * HTTP reverse proxy + * + * TODO: - HTTP/1.1 + * - 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; + int upgrade; + int connect_method; + /*(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_FORWARDED_NONE = 0x00, + PROXY_FORWARDED_FOR = 0x01, + PROXY_FORWARDED_PROTO = 0x02, + PROXY_FORWARDED_HOST = 0x04, + PROXY_FORWARDED_BY = 0x08, + PROXY_FORWARDED_REMOTE_USER = 0x10 +} proxy_forwarded_t; + +typedef struct { + gw_plugin_config gw; + array *forwarded_params; + array *header_params; + unsigned short replace_http_host; + unsigned int forwarded; + + http_header_remap_opts header; +} plugin_config; + +typedef struct { + PLUGIN_DATA; + plugin_config **config_storage; + + plugin_config conf; +} plugin_data; + +static int proxy_check_extforward; + +typedef struct { + gw_handler_ctx gw; + http_response_opts opts; + plugin_config conf; +} handler_ctx; + + +INIT_FUNC(mod_proxy_init) { + plugin_data *p; + + p = calloc(1, sizeof(*p)); + + return p; +} + + +FREE_FUNC(mod_proxy_free) { + plugin_data *p = p_d; + + UNUSED(srv); + + if (p->config_storage) { + size_t i; + for (i = 0; i < srv->config_context->used; i++) { + plugin_config *s = p->config_storage[i]; + + if (NULL == s) continue; + + array_free(s->forwarded_params); + array_free(s->header_params); + + /*assert(0 == offsetof(s->gw));*/ + gw_plugin_config_free(&s->gw); + /*free(s);*//*free'd by gw_plugin_config_free()*/ + } + free(p->config_storage); + } + + free(p); + + return HANDLER_GO_ON; +} + +SETDEFAULTS_FUNC(mod_proxy_set_defaults) { + plugin_data *p = p_d; + data_unset *du; + size_t i = 0; + + config_values_t cv[] = { + { "proxy.server", NULL, T_CONFIG_LOCAL, T_CONFIG_SCOPE_CONNECTION }, /* 0 */ + { "proxy.debug", NULL, T_CONFIG_SHORT, T_CONFIG_SCOPE_CONNECTION }, /* 1 */ + { "proxy.balance", NULL, T_CONFIG_LOCAL, 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 */ + { "proxy.map-extensions", NULL, T_CONFIG_ARRAY, T_CONFIG_SCOPE_CONNECTION }, /* 6 */ + { NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET } + }; + + p->config_storage = calloc(1, srv->config_context->used * sizeof(plugin_config *)); + + for (i = 0; i < srv->config_context->used; i++) { + data_config const* config = (data_config const*)srv->config_context->data[i]; + plugin_config *s; + + s = calloc(1, sizeof(plugin_config)); + s->gw.debug = 0; + s->replace_http_host = 0; + s->forwarded_params = array_init(); + s->forwarded = PROXY_FORWARDED_NONE; + s->header_params = array_init(); + s->gw.ext_mapping = array_init(); + + cv[0].destination = NULL; /* T_CONFIG_LOCAL */ + cv[1].destination = &(s->gw.debug); + cv[2].destination = NULL; /* T_CONFIG_LOCAL */ + cv[3].destination = &(s->replace_http_host); + cv[4].destination = s->forwarded_params; + cv[5].destination = s->header_params; + cv[6].destination = s->gw.ext_mapping; + + p->config_storage[i] = s; + + if (0 != config_insert_values_global(srv, config->value, cv, i == 0 ? T_CONFIG_SCOPE_SERVER : T_CONFIG_SCOPE_CONNECTION)) { + return HANDLER_ERROR; + } + + du = array_get_element(config->value, "proxy.server"); + if (!gw_set_defaults_backend(srv, (gw_plugin_data *)p, du, i, 0)) { + return HANDLER_ERROR; + } + + du = array_get_element(config->value, "proxy.balance"); + if (!gw_set_defaults_balance(srv, &s->gw, du)) { + return HANDLER_ERROR; + } + + /* disable check-local for all exts (default enabled) */ + if (s->gw.exts) { /*(check after gw_set_defaults_backend())*/ + for (size_t j = 0; j < s->gw.exts->used; ++j) { + gw_extension *ex = s->gw.exts->exts[j]; + for (size_t n = 0; n < ex->used; ++n) { + ex->hosts[n]->check_local = 0; + } + } + } + + if (!array_is_kvany(s->forwarded_params)) { + log_error_write(srv, __FILE__, __LINE__, "s", + "unexpected value for proxy.forwarded; expected ( \"param\" => \"value\" )"); + return HANDLER_ERROR; + } + for (size_t j = 0, used = s->forwarded_params->used; j < used; ++j) { + proxy_forwarded_t param; + du = s->forwarded_params->data[j]; + if (buffer_is_equal_string(du->key, CONST_STR_LEN("by"))) { + param = PROXY_FORWARDED_BY; + } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("for"))) { + param = PROXY_FORWARDED_FOR; + } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("host"))) { + param = PROXY_FORWARDED_HOST; + } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("proto"))) { + param = PROXY_FORWARDED_PROTO; + } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("remote_user"))) { + param = PROXY_FORWARDED_REMOTE_USER; + } else { + log_error_write(srv, __FILE__, __LINE__, "sb", + "proxy.forwarded keys must be one of: by, for, host, proto, remote_user, but not:", du->key); + return HANDLER_ERROR; + } + if (du->type == TYPE_STRING) { + data_string *ds = (data_string *)du; + if (buffer_is_equal_string(ds->value, CONST_STR_LEN("enable"))) { + s->forwarded |= param; + } else if (!buffer_is_equal_string(ds->value, CONST_STR_LEN("disable"))) { + log_error_write(srv, __FILE__, __LINE__, "sb", + "proxy.forwarded values must be one of: 0, 1, enable, disable; error for key:", du->key); + return HANDLER_ERROR; + } + } else if (du->type == TYPE_INTEGER) { + data_integer *di = (data_integer *)du; + if (di->value) s->forwarded |= param; + } else { + log_error_write(srv, __FILE__, __LINE__, "sb", + "proxy.forwarded values must be one of: 0, 1, enable, disable; error for key:", du->key); + return HANDLER_ERROR; + } + } + + 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; + } + else if (buffer_is_equal_string(da->key, CONST_STR_LEN("upgrade"))) { + data_string *ds = (data_string *)da; + if (ds->type != TYPE_STRING) { + log_error_write(srv, __FILE__, __LINE__, "s", + "unexpected value for proxy.header; expected \"upgrade\" => \"enable\" or \"disable\""); + return HANDLER_ERROR; + } + s->header.upgrade = !buffer_is_equal_string(ds->value, CONST_STR_LEN("disable")) + && !buffer_is_equal_string(ds->value, CONST_STR_LEN("0")); + continue; + } + else if (buffer_is_equal_string(da->key, CONST_STR_LEN("connect"))) { + data_string *ds = (data_string *)da; + if (ds->type != TYPE_STRING) { + log_error_write(srv, __FILE__, __LINE__, "s", + "unexpected value for proxy.header; expected \"connect\" => \"enable\" or \"disable\""); + return HANDLER_ERROR; + } + s->header.connect_method = !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; + } + } + } + + for (i = 0; i < srv->srvconf.modules->used; i++) { + data_string *ds = (data_string *)srv->srvconf.modules->data[i]; + if (buffer_is_equal_string(ds->value, CONST_STR_LEN("mod_extforward"))) { + proxy_check_extforward = 1; + break; + } + } + + return HANDLER_GO_ON; +} + + +/* (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 size_t 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); + return buffer_string_length(ds->value);/*(replacement len)*/ + } + } + } + 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 buffer_string_length(ds->key); /*(replacement len)*/ + } + } + 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); + return buffer_string_length(ds->key); /*(replacement len)*/ + } + } + } + } + return 0; +} + + +/* (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 = b->ptr+off, *e; *s; s = e) { + size_t len; + { + while (*s != ';' && *s != '\n' && *s != '\0') ++s; + if (*s == '\n') { + /*(include +1 for '\n', but leave ' ' for ++s below)*/ + s += sizeof("Set-Cookie:"); + } + if ('\0' == *s) return; + do { ++s; } while (*s == ' ' || *s == '\t'); + if ('\0' == *s) return; + e = s+1; + if ('=' == *s) continue; + /*(interested only in Domain and Path attributes)*/ + while (*e != '=' && *e != '\0') ++e; + if ('\0' == *e) return; + ++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); + len = http_header_remap_urlpath(b, off, remap_hdrs, 0); + e = b->ptr+off+len; /*(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 buffer_append_string_backslash_escaped(buffer *b, const char *s, size_t len) { + /* (future: might move to buffer.c) */ + size_t j = 0; + char *p; + + buffer_string_prepare_append(b, len*2 + 4); + p = b->ptr + buffer_string_length(b); + + for (size_t i = 0; i < len; ++i) { + int c = s[i]; + if (c == '"' || c == '\\' || c == 0x7F || (c < 0x20 && c != '\t')) + p[j++] = '\\'; + p[j++] = c; + } + + buffer_commit(b, j); +} + +static void proxy_set_Forwarded(connection *con, const unsigned int flags) { + buffer *b = NULL, *efor = NULL, *eproto = NULL, *ehost = NULL; + int semicolon = 0; + + if (proxy_check_extforward) { + efor = + http_header_env_get(con, CONST_STR_LEN("_L_EXTFORWARD_ACTUAL_FOR")); + eproto = + http_header_env_get(con, CONST_STR_LEN("_L_EXTFORWARD_ACTUAL_PROTO")); + ehost = + http_header_env_get(con, CONST_STR_LEN("_L_EXTFORWARD_ACTUAL_HOST")); + } + + /* note: set "Forwarded" prior to updating X-Forwarded-For (below) */ + + if (flags) + b = http_header_request_get(con, HTTP_HEADER_FORWARDED, CONST_STR_LEN("Forwarded")); + + if (flags && NULL == b) { + buffer *xff = + http_header_request_get(con, HTTP_HEADER_X_FORWARDED_FOR, CONST_STR_LEN("X-Forwarded-For")); + http_header_request_set(con, HTTP_HEADER_FORWARDED, + CONST_STR_LEN("Forwarded"), + CONST_STR_LEN("x")); /*(must not be blank for _get below)*/ + #ifdef __COVERITY__ + force_assert(NULL != b); /*(not NULL because created directly above)*/ + #endif + b = http_header_request_get(con, HTTP_HEADER_FORWARDED, CONST_STR_LEN("Forwarded")); + buffer_clear(b); + if (NULL != xff) { + /* use X-Forwarded-For contents to seed Forwarded */ + char *s = xff->ptr; + size_t used = buffer_string_length(xff); + for (size_t i=0, j, ipv6; i < used; ++i) { + while (s[i] == ' ' || s[i] == '\t' || s[i] == ',') ++i; + if (s[i] == '\0') break; + j = i; + do { + ++i; + } while (s[i]!=' ' && s[i]!='\t' && s[i]!=',' && s[i]!='\0'); + buffer_append_string_len(b, CONST_STR_LEN("for=")); + /* over-simplified test expecting only IPv4 or IPv6 addresses, + * (not expecting :port, so treat existence of colon as IPv6, + * and not expecting unix paths, especially not containing ':') + * quote all strings, backslash-escape since IPs not validated*/ + ipv6 = (NULL != memchr(s+j, ':', i-j)); /*(over-simplified) */ + buffer_append_string_len(b, CONST_STR_LEN("\"")); + if (ipv6) + buffer_append_string_len(b, CONST_STR_LEN("[")); + buffer_append_string_backslash_escaped(b, s+j, i-j); + if (ipv6) + buffer_append_string_len(b, CONST_STR_LEN("]")); + buffer_append_string_len(b, CONST_STR_LEN("\"")); + buffer_append_string_len(b, CONST_STR_LEN(", ")); + } + } + } else if (flags) { /*(NULL != b)*/ + buffer_append_string_len(b, CONST_STR_LEN(", ")); + } + + if (flags & PROXY_FORWARDED_FOR) { + int family = sock_addr_get_family(&con->dst_addr); + buffer_append_string_len(b, CONST_STR_LEN("for=")); + if (NULL != efor) { + /* over-simplified test expecting only IPv4 or IPv6 addresses, + * (not expecting :port, so treat existence of colon as IPv6, + * and not expecting unix paths, especially not containing ':') + * quote all strings and backslash-escape since IPs not validated + * (should be IP from original con->dst_addr_buf, + * so trustable and without :port) */ + int ipv6 = (NULL != strchr(efor->ptr, ':')); + buffer_append_string_len(b, CONST_STR_LEN("\"")); + if (ipv6) buffer_append_string_len(b, CONST_STR_LEN("[")); + buffer_append_string_backslash_escaped( + b, CONST_BUF_LEN(efor)); + if (ipv6) buffer_append_string_len(b, CONST_STR_LEN("]")); + buffer_append_string_len(b, CONST_STR_LEN("\"")); + } else if (family == AF_INET) { + /*(Note: if :port is added, then must be quoted-string: + * e.g. for="...:port")*/ + buffer_append_string_buffer(b, con->dst_addr_buf); + } else if (family == AF_INET6) { + buffer_append_string_len(b, CONST_STR_LEN("\"[")); + buffer_append_string_buffer(b, con->dst_addr_buf); + buffer_append_string_len(b, CONST_STR_LEN("]\"")); + } else { + buffer_append_string_len(b, CONST_STR_LEN("\"")); + buffer_append_string_backslash_escaped( + b, CONST_BUF_LEN(con->dst_addr_buf)); + buffer_append_string_len(b, CONST_STR_LEN("\"")); + } + semicolon = 1; + } + + if (flags & PROXY_FORWARDED_BY) { + int family = sock_addr_get_family(&con->srv_socket->addr); + /* Note: getsockname() and inet_ntop() are expensive operations. + * (recommendation: do not to enable by=... unless required) + * future: might use con->srv_socket->srv_token if addr is not + * INADDR_ANY or in6addr_any, but must omit optional :port + * from con->srv_socket->srv_token for consistency */ + + if (semicolon) buffer_append_string_len(b, CONST_STR_LEN(";")); + buffer_append_string_len(b, CONST_STR_LEN("by=")); + buffer_append_string_len(b, CONST_STR_LEN("\"")); + #ifdef HAVE_SYS_UN_H + /* special-case: might need to encode unix domain socket path */ + if (family == AF_UNIX) { + buffer_append_string_backslash_escaped( + b, CONST_BUF_LEN(con->srv_socket->srv_token)); + } + else + #endif + { + sock_addr addr; + socklen_t addrlen = sizeof(addr); + if (0 == getsockname(con->fd,(struct sockaddr *)&addr, &addrlen)) { + sock_addr_stringify_append_buffer(b, &addr); + } + } + buffer_append_string_len(b, CONST_STR_LEN("\"")); + semicolon = 1; + } + + if (flags & PROXY_FORWARDED_PROTO) { + /* expecting "http" or "https" + * (not checking if quoted-string and encoding needed) */ + if (semicolon) buffer_append_string_len(b, CONST_STR_LEN(";")); + buffer_append_string_len(b, CONST_STR_LEN("proto=")); + if (NULL != eproto) { + buffer_append_string_buffer(b, eproto); + } else if (con->srv_socket->is_ssl) { + buffer_append_string_len(b, CONST_STR_LEN("https")); + } else { + buffer_append_string_len(b, CONST_STR_LEN("http")); + } + semicolon = 1; + } + + if (flags & PROXY_FORWARDED_HOST) { + if (NULL != ehost) { + if (semicolon) + buffer_append_string_len(b, CONST_STR_LEN(";")); + buffer_append_string_len(b, CONST_STR_LEN("host=\"")); + buffer_append_string_backslash_escaped( + b, CONST_BUF_LEN(ehost)); + buffer_append_string_len(b, CONST_STR_LEN("\"")); + semicolon = 1; + } else if (!buffer_string_is_empty(con->request.http_host)) { + if (semicolon) + buffer_append_string_len(b, CONST_STR_LEN(";")); + buffer_append_string_len(b, CONST_STR_LEN("host=\"")); + buffer_append_string_backslash_escaped( + b, CONST_BUF_LEN(con->request.http_host)); + buffer_append_string_len(b, CONST_STR_LEN("\"")); + semicolon = 1; + } + } + + if (flags & PROXY_FORWARDED_REMOTE_USER) { + buffer *remote_user = + http_header_env_get(con, CONST_STR_LEN("REMOTE_USER")); + if (NULL != remote_user) { + if (semicolon) + buffer_append_string_len(b, CONST_STR_LEN(";")); + buffer_append_string_len(b, CONST_STR_LEN("remote_user=\"")); + buffer_append_string_backslash_escaped( + b, CONST_BUF_LEN(remote_user)); + buffer_append_string_len(b, CONST_STR_LEN("\"")); + semicolon = 1; + } + } + + /* legacy X-* headers, including X-Forwarded-For */ + + b = (NULL != efor) ? efor : con->dst_addr_buf; + http_header_request_set(con, HTTP_HEADER_X_FORWARDED_FOR, + CONST_STR_LEN("X-Forwarded-For"), + CONST_BUF_LEN(b)); + + b = (NULL != ehost) ? ehost : con->request.http_host; + if (!buffer_string_is_empty(b)) { + http_header_request_set(con, HTTP_HEADER_OTHER, + CONST_STR_LEN("X-Host"), + CONST_BUF_LEN(b)); + http_header_request_set(con, HTTP_HEADER_OTHER, + CONST_STR_LEN("X-Forwarded-Host"), + CONST_BUF_LEN(b)); + } + + b = (NULL != eproto) ? eproto : con->uri.scheme; + http_header_request_set(con, HTTP_HEADER_X_FORWARDED_PROTO, + CONST_STR_LEN("X-Forwarded-Proto"), + CONST_BUF_LEN(b)); +} + + +static handler_t proxy_create_env(server *srv, gw_handler_ctx *gwhctx) { + handler_ctx *hctx = (handler_ctx *)gwhctx; + connection *con = hctx->gw.remote_conn; + const int remap_headers = (NULL != hctx->conf.header.urlpaths + || NULL != hctx->conf.header.hosts_request); + const int upgrade = hctx->conf.header.upgrade + && (NULL != http_header_request_get(con, HTTP_HEADER_UPGRADE, CONST_STR_LEN("Upgrade"))); + size_t rsz = (size_t)(con->read_queue->bytes_out - hctx->gw.wb->bytes_in); + buffer * const b = chunkqueue_prepend_buffer_open_sz(hctx->gw.wb, rsz < 65536 ? rsz : con->header_len); + + /* build header */ + + /* request line */ + http_method_append(b, 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->conf.header, 1); + if (!upgrade) + buffer_append_string_len(b, CONST_STR_LEN(" HTTP/1.0\r\n")); + else + buffer_append_string_len(b, CONST_STR_LEN(" HTTP/1.1\r\n")); + + if (hctx->conf.replace_http_host && !buffer_string_is_empty(hctx->gw.host->id)) { + if (hctx->gw.conf.debug > 1) { + log_error_write(srv, __FILE__, __LINE__, "SBS", + "proxy - using \"", hctx->gw.host->id, "\" as HTTP Host"); + } + buffer_append_string_len(b, CONST_STR_LEN("Host: ")); + buffer_append_string_buffer(b, hctx->gw.host->id); + 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->conf.header, 1, alen); + } + buffer_append_string_len(b, CONST_STR_LEN("\r\n")); + } + + /* "Forwarded" and legacy X- headers */ + proxy_set_Forwarded(con, hctx->conf.forwarded); + + if (HTTP_METHOD_GET != con->request.http_method + && HTTP_METHOD_HEAD != con->request.http_method + && con->request.content_length >= 0) { + /* set Content-Length if client sent Transfer-Encoding: chunked + * and not streaming to backend (request body has been fully received) */ + buffer *vb = http_header_request_get(con, HTTP_HEADER_CONTENT_LENGTH, CONST_STR_LEN("Content-Length")); + if (NULL == vb) { + char buf[LI_ITOSTRING_LENGTH]; + li_itostrn(buf, sizeof(buf), con->request.content_length); + http_header_request_set(con, HTTP_HEADER_CONTENT_LENGTH, CONST_STR_LEN("Content-Length"), buf, strlen(buf)); + } + } + + /* request header */ + 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; + + if (buffer_string_space(b) < klen + vlen + 4) { + size_t extend = b->size * 2 - buffer_string_length(b); + extend = extend > klen + vlen + 4 ? extend : klen + vlen + 4 + 4095; + buffer_string_prepare_append(b, extend); + } + + 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->conf.header, 1); + } + + if (!upgrade) + buffer_append_string_len(b, CONST_STR_LEN("Connection: close\r\n\r\n")); + else + buffer_append_string_len(b, CONST_STR_LEN("Connection: close, upgrade\r\n\r\n")); + + hctx->gw.wb_reqlen = buffer_string_length(b); + chunkqueue_prepend_buffer_commit(hctx->gw.wb); + + if (con->request.content_length) { + chunkqueue_append_chunkqueue(hctx->gw.wb, con->request_content_queue); + if (con->request.content_length > 0) + hctx->gw.wb_reqlen += con->request.content_length; /* total req size */ + else /* as-yet-unknown total request size (Transfer-Encoding: chunked)*/ + hctx->gw.wb_reqlen = -hctx->gw.wb_reqlen; + } + + status_counter_inc(srv, CONST_STR_LEN("proxy.requests")); + return HANDLER_GO_ON; +} + + +static handler_t proxy_create_env_connect(server *srv, gw_handler_ctx *gwhctx) { + handler_ctx *hctx = (handler_ctx *)gwhctx; + connection *con = hctx->gw.remote_conn; + con->http_status = 200; /* OK */ + con->file_started = 1; + gw_set_transparent(srv, &hctx->gw); + http_response_upgrade_read_body_unknown(srv, con); + + status_counter_inc(srv, CONST_STR_LEN("proxy.requests")); + return HANDLER_GO_ON; +} + + +#define PATCH(x) \ + p->conf.x = s->x; +#define PATCH_GW(x) \ + p->conf.gw.x = s->gw.x; +static int mod_proxy_patch_connection(server *srv, connection *con, plugin_data *p) { + size_t i, j; + plugin_config *s = p->config_storage[0]; + + PATCH_GW(exts); + PATCH_GW(exts_auth); + PATCH_GW(exts_resp); + PATCH_GW(debug); + PATCH_GW(ext_mapping); + PATCH_GW(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++) { + data_config *dc = (data_config *)srv->config_context->data[i]; + s = p->config_storage[i]; + + /* condition didn't match */ + if (!config_check_cond(srv, con, dc)) continue; + + /* merge config */ + for (j = 0; j < dc->value->used; j++) { + data_unset *du = dc->value->data[j]; + + if (buffer_is_equal_string(du->key, CONST_STR_LEN("proxy.server"))) { + PATCH_GW(exts); + PATCH_GW(exts_auth); + PATCH_GW(exts_resp); + } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("proxy.debug"))) { + PATCH_GW(debug); + } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("proxy.balance"))) { + PATCH_GW(balance); + } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("proxy.map-extensions"))) { + PATCH_GW(ext_mapping); + } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("proxy.replace-http-host"))) { + 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)*/ + } + } + } + + return 0; +} +#undef PATCH_GW +#undef PATCH + +static handler_t proxy_response_headers(server *srv, connection *con, struct http_response_opts_t *opts) { + /* response headers just completed */ + handler_ctx *hctx = (handler_ctx *)opts->pdata; + + if (con->response.htags & HTTP_HEADER_UPGRADE) { + if (hctx->conf.header.upgrade && con->http_status == 101) { + /* 101 Switching Protocols; transition to transparent proxy */ + gw_set_transparent(srv, &hctx->gw); + http_response_upgrade_read_body_unknown(srv, con); + } + else { + con->response.htags &= ~HTTP_HEADER_UPGRADE; + #if 0 + /* preserve prior questionable behavior; likely broken behavior + * anyway if backend thinks connection is being upgraded but client + * does not receive Connection: upgrade */ + http_header_response_unset(con, HTTP_HEADER_UPGRADE, + CONST_STR_LEN("Upgrade")) + #endif + } + } + + /* rewrite paths, if needed */ + + if (NULL == hctx->conf.header.urlpaths + && NULL == hctx->conf.header.hosts_response) + return HANDLER_GO_ON; + + if (con->response.htags & HTTP_HEADER_LOCATION) { + buffer *vb = http_header_response_get(con, HTTP_HEADER_LOCATION, CONST_STR_LEN("Location")); + if (vb) http_header_remap_uri(vb, 0, &hctx->conf.header, 0); + } + if (con->response.htags & HTTP_HEADER_CONTENT_LOCATION) { + buffer *vb = http_header_response_get(con, HTTP_HEADER_CONTENT_LOCATION, CONST_STR_LEN("Content-Location")); + if (vb) http_header_remap_uri(vb, 0, &hctx->conf.header, 0); + } + if (con->response.htags & HTTP_HEADER_SET_COOKIE) { + buffer *vb = http_header_response_get(con, HTTP_HEADER_SET_COOKIE, CONST_STR_LEN("Set-Cookie")); + if (vb) http_header_remap_setcookie(vb, 0, &hctx->conf.header); + } + + return HANDLER_GO_ON; +} + +static handler_t mod_proxy_check_extension(server *srv, connection *con, void *p_d) { + plugin_data *p = p_d; + handler_t rc; + + if (con->mode != DIRECT) return HANDLER_GO_ON; + + mod_proxy_patch_connection(srv, con, p); + if (NULL == p->conf.gw.exts) return HANDLER_GO_ON; + + rc = gw_check_extension(srv, con, (gw_plugin_data *)p, 1, sizeof(handler_ctx)); + if (HANDLER_GO_ON != rc) return rc; + + if (con->mode == p->id) { + handler_ctx *hctx = con->plugin_ctx[p->id]; + hctx->gw.create_env = proxy_create_env; + hctx->gw.response = chunk_buffer_acquire(); + hctx->gw.opts.backend = BACKEND_PROXY; + hctx->gw.opts.pdata = hctx; + hctx->gw.opts.headers = proxy_response_headers; + + hctx->conf = p->conf; /*(copies struct)*/ + hctx->conf.header.http_host = con->request.http_host; + hctx->conf.header.upgrade &= (con->request.http_version == HTTP_VERSION_1_1); + /* 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->conf.header.https_remap) { + hctx->conf.header.https_remap = + buffer_is_equal_string(con->uri.scheme, CONST_STR_LEN("https")); + } + + if (con->request.http_method == HTTP_METHOD_CONNECT) { + /*(note: not requiring HTTP/1.1 due to too many non-compliant + * clients such as 'openssl s_client')*/ + if (hctx->conf.header.connect_method) { + hctx->gw.create_env = proxy_create_env_connect; + } + else { + con->http_status = 405; /* Method Not Allowed */ + con->mode = DIRECT; + return HANDLER_FINISHED; + } + } + } + + return HANDLER_GO_ON; +} + + +int mod_proxy_plugin_init(plugin *p); +int mod_proxy_plugin_init(plugin *p) { + p->version = LIGHTTPD_VERSION_ID; + p->name = buffer_init_string("proxy"); + + p->init = mod_proxy_init; + p->cleanup = mod_proxy_free; + p->set_defaults = mod_proxy_set_defaults; + p->connection_reset = gw_connection_reset; + p->handle_uri_clean = mod_proxy_check_extension; + p->handle_subrequest = gw_handle_subrequest; + p->handle_trigger = gw_handle_trigger; + p->handle_waitpid = gw_handle_waitpid_cb; + + p->data = NULL; + + return 0; +} |