diff --git a/CHANGELOG.md b/CHANGELOG.md index b0ab10eab..c44cac79c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added - Detect number of CPU shares when running on Cgroups V2 [PR #1410](https://github.com/3scale/apicast/pull/1410) [THREESCALE-10167](https://issues.redhat.com/browse/THREESCALE-10167) +### Added + +* Add support to use Basic Authentication with the forward proxy. [PR #1409](https://github.com/3scale/APIcast/pull/1409) ## [3.14.0] 2023-07-25 diff --git a/gateway/src/apicast/http_proxy.lua b/gateway/src/apicast/http_proxy.lua index 66d5dca07..624aa0502 100644 --- a/gateway/src/apicast/http_proxy.lua +++ b/gateway/src/apicast/http_proxy.lua @@ -5,6 +5,7 @@ local resty_resolver = require 'resty.resolver' local round_robin = require 'resty.balancer.round_robin' local http_proxy = require 'resty.http.proxy' local file_reader = require("resty.file").file_reader +local concat = table.concat local _M = { } @@ -81,7 +82,7 @@ local function absolute_url(uri) ) end -local function forward_https_request(proxy_uri, uri, skip_https_connect) +local function forward_https_request(proxy_uri, proxy_auth, uri, skip_https_connect) -- This is needed to call ngx.req.get_body_data() below. ngx.req.read_body() @@ -101,7 +102,8 @@ local function forward_https_request(proxy_uri, uri, skip_https_connect) -- nil, so after this we need to read the temp file. -- https://github.com/openresty/lua-nginx-module#ngxreqget_body_data body = ngx.req.get_body_data(), - proxy_uri = proxy_uri + proxy_uri = proxy_uri, + proxy_auth = proxy_auth } if not request.body then @@ -155,8 +157,23 @@ end function _M.request(upstream, proxy_uri) local uri = upstream.uri + local proxy_auth + + if proxy_uri.user or proxy_uri.password then + proxy_auth = "Basic " .. ngx.encode_base64(concat({ proxy_uri.user or '', proxy_uri.password or '' }, ':')) + end if uri.scheme == 'http' then -- rewrite the request to use http_proxy + -- Only set "Proxy-Authorization" when sending HTTP request. When sent over HTTPS, + -- the `Proxy-Authorization` header must be sent in the CONNECT request as the proxy has + -- no visibility into the tunneled request. + -- + -- Also DO NOT set the header if using the camel proxy to avoid unintended leak of + -- Proxy-Authorization header in requests + if not ngx.var.http_proxy_authorization and proxy_auth and not upstream.skip_https_connect then + ngx.req.set_header("Proxy-Authorization", proxy_auth) + end + local err local host = upstream:set_host_header() upstream:use_host_header(host) @@ -169,7 +186,7 @@ function _M.request(upstream, proxy_uri) return elseif uri.scheme == 'https' then upstream:rewrite_request() - forward_https_request(proxy_uri, uri, upstream.skip_https_connect) + forward_https_request(proxy_uri, proxy_auth, uri, upstream.skip_https_connect) return ngx.exit(ngx.OK) -- terminate phase else ngx.log(ngx.ERR, 'could not connect to proxy: ', proxy_uri, ' err: ', 'invalid request scheme') diff --git a/gateway/src/apicast/policy/http_proxy/Readme.md b/gateway/src/apicast/policy/http_proxy/Readme.md index e7234d774..579edfa98 100644 --- a/gateway/src/apicast/policy/http_proxy/Readme.md +++ b/gateway/src/apicast/policy/http_proxy/Readme.md @@ -43,6 +43,8 @@ used. ## Configuration +The policy expect the URLS following the `http://[[:]@][:]` format, e.g.: + ``` "policy_chain": [ { @@ -51,15 +53,17 @@ used. { "name": "apicast.policy.http_proxy", "configuration": { - "all_proxy": "http://192.168.15.103:8888/", - "https_proxy": "https://192.168.15.103:8888/", - "http_proxy": "https://192.168.15.103:8888/" + "all_proxy": "http://foo:bar@192.168.15.103:8888/", + "https_proxy": "http://foo:bar@192.168.15.103:8888/", + "http_proxy": "http://foo:bar@192.168.15.103:8888/" } } ] ``` -- If http_proxy or https_proxy is not defined the all_proxy will be taken. +- If http_proxy or https_proxy is not defined the all_proxy will be taken. +- The policy supports for proxy authentication via the `` and `` options. +- The `` and `` are optional, all other components are required. ## Caveats @@ -67,7 +71,7 @@ used. always send to the proxy. - In case of HTTP_PROXY, HTTPS_PROXY or ALL_PROXY parameters are defined, this policy will overwrite those values. -- Proxy connection does not support authentication. +- 3scale currently does not support connecting to an HTTP proxy via TLS. For this reason, the scheme of the HTTPS_PROXY value is restricted to http. ## Example Use case diff --git a/gateway/src/resty/http/proxy.lua b/gateway/src/resty/http/proxy.lua index 2dc472d58..8f644869d 100644 --- a/gateway/src/resty/http/proxy.lua +++ b/gateway/src/resty/http/proxy.lua @@ -56,17 +56,22 @@ local function _connect_proxy_https(httpc, request, host, port) local uri = request.uri - local ok, err = httpc:request({ + local res, err = httpc:request({ method = 'CONNECT', path = format('%s:%s', host, port or default_port(uri)), headers = { ['Host'] = request.headers.host or format('%s:%s', uri.host, default_port(uri)), + ['Proxy-Authorization'] = request.proxy_auth or '' } }) - if not ok then return nil, err end + if not res then return nil, err end - ok, err = httpc:ssl_handshake(nil, uri.host, request.ssl_verify) - if not ok then return nil, err end + if res.status < 200 or res.status > 299 then + return nil, "failed to establish a tunnel through a proxy: " .. res.status + end + + res, err = httpc:ssl_handshake(nil, uri.host, request.ssl_verify) + if not res then return nil, err end return httpc end diff --git a/t/apicast-policy-camel.t b/t/apicast-policy-camel.t index f95dcc081..ddaac389c 100644 --- a/t/apicast-policy-camel.t +++ b/t/apicast-policy-camel.t @@ -315,3 +315,194 @@ ETag: foobar <[^:\s]+):\s*(?.+)\r\n$]]) + ngx.log(ngx.DEBUG, 'proxy http request - got header line: ', header_line) if header and str_lower(header.name) == 'content-length' then body_length = tonumber(header.value) diff --git a/t/http-proxy.t b/t/http-proxy.t index d896c8a2b..c19e0b9bc 100644 --- a/t/http-proxy.t +++ b/t/http-proxy.t @@ -1227,3 +1227,96 @@ proxy request: CONNECT test-upstream.lvh.me:$TEST_NGINX_RANDOM_PORT HTTP/1.1 --- no_error_log [error] --- user_files fixture=tls.pl eval + + +=== TEST 23: upstream API connection uses http proxy with BasicAuth +--- env eval +( + "http_proxy" => "http://foo:bar\@127.0.0.1:$ENV{TEST_NGINX_HTTP_PROXY_PORT}", + 'BACKEND_ENDPOINT_OVERRIDE' => "http://test_backend.lvh.me:$ENV{TEST_NGINX_SERVER_PORT}" +) +--- configuration +{ + "services": [ + { + "backend_version": 1, + "proxy": { + "api_backend": "http://test-upstream.lvh.me:$TEST_NGINX_SERVER_PORT", + "proxy_rules": [ + { "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 2 } + ] + } + } + ] +} +--- backend +server_name test_backend.lvh.me; + location /transactions/authrep.xml { + content_by_lua_block { + ngx.exit(ngx.OK) + } + } +--- upstream +server_name test-upstream.lvh.me; + location / { + echo 'yay, api backend!'; + } +--- request +GET /?user_key=value +--- error_code: 200 +--- error_log env +using proxy: http://foo:bar@127.0.0.1:$TEST_NGINX_HTTP_PROXY_PORT +proxy http request - got header line: Proxy-Authorization: Basic Zm9vOmJhcg== +--- no_error_log +[error] + + +=== TEST 24: upstream API connection uses proxy for https with BasicAuth +--- env eval +( + "https_proxy" => "http://foo:bar\@127.0.0.1:$ENV{TEST_NGINX_HTTP_PROXY_PORT}", + 'BACKEND_ENDPOINT_OVERRIDE' => "http://test_backend.lvh.me:$ENV{TEST_NGINX_SERVER_PORT}" +) +--- configuration random_port env +{ + "services": [ + { + "backend_version": 1, + "proxy": { + "api_backend": "https://test-upstream.lvh.me:$TEST_NGINX_RANDOM_PORT", + "proxy_rules": [ + { "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 2 } + ] + } + } + ] +} +--- backend +server_name test_backend.lvh.me; + + location /transactions/authrep.xml { + content_by_lua_block { + ngx.exit(ngx.OK) + } + } +--- upstream env +server_name test-upstream.lvh.me; + +listen $TEST_NGINX_RANDOM_PORT ssl; +ssl_certificate $TEST_NGINX_SERVER_ROOT/html/server.crt; +ssl_certificate_key $TEST_NGINX_SERVER_ROOT/html/server.key; +location / { + echo_foreach_split '\r\n' $echo_client_request_headers; + echo $echo_it; + echo_end; +} +--- request +GET /test?user_key=test3 +--- error_code: 200 +--- error_log env +using proxy: http://foo:bar@127.0.0.1:$TEST_NGINX_HTTP_PROXY_PORT +proxy request: CONNECT test-upstream.lvh.me:$TEST_NGINX_RANDOM_PORT HTTP/1.1 +got header line: Proxy-Authorization: Basic Zm9vOmJhcg== +--- no_error_log +[error] +--- user_files fixture=tls.pl eval