diff --git a/CHANGELOG.md b/CHANGELOG.md index 59de1360c..74862a797 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added custom_metrics policy [PR #1188](https://github.com/3scale/APIcast/pull/1188) [THREESCALE-5098](https://issues.jboss.org/browse/THREESCALE-5098) - New apicast_status Prometheus metric [THREESCALE-5417](https://issues.jboss.org/browse/THREESCALE-5417) [PR #1200](https://github.com/3scale/APIcast/pull/1200) +- Added Camel policy [PR #1193](https://github.com/3scale/APIcast/pull/1193) [THREESCALE-4867](https://issues.jboss.org/browse/THREESCALE-4867) ## [3.8.0] - 2020-03-24 diff --git a/gateway/src/apicast/http_proxy.lua b/gateway/src/apicast/http_proxy.lua index 51143d1fb..e4c36e821 100644 --- a/gateway/src/apicast/http_proxy.lua +++ b/gateway/src/apicast/http_proxy.lua @@ -84,7 +84,7 @@ local function current_path(uri) return format('%s%s%s', uri.path or ngx.var.uri, ngx.var.is_args, ngx.var.query_string or '') end -local function forward_https_request(proxy_uri, uri) +local function forward_https_request(proxy_uri, uri, skip_https_connect) -- This is needed to call ngx.req.get_body_data() below. ngx.req.read_body() @@ -121,7 +121,7 @@ local function forward_https_request(proxy_uri, uri) end end - local httpc, err = http_proxy.new(request) + local httpc, err = http_proxy.new(request, skip_https_connect) if not httpc then ngx.log(ngx.ERR, 'could not connect to proxy: ', proxy_uri, ' err: ', err) @@ -172,7 +172,7 @@ function _M.request(upstream, proxy_uri) return elseif uri.scheme == 'https' then upstream:rewrite_request() - forward_https_request(proxy_uri, uri) + forward_https_request(proxy_uri, 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/camel/Readme.md b/gateway/src/apicast/policy/camel/Readme.md new file mode 100644 index 000000000..ee402aaef --- /dev/null +++ b/gateway/src/apicast/policy/camel/Readme.md @@ -0,0 +1,77 @@ +# Camel proxy policy + +This policy allows users to define a camel proxy where the traffic will be send +over the defined proxy, the example traffic flow is the following: + +``` + ,-. + `-' + /|\ + | ,-------. ,---------. ,----------. + / \ |Apicast| | CAMEL | |APIBackend| + User `---+---' `----+----' `----------' + | GET /resource | | | + | --------------->| | | + | | | | + | | Get /resource | | + | |------------------>| | + | | | | + | | | Get /resource/ | + | | | - - - - - - - - - >| + | | | | + | | | response | + | | |<- - - - - - - - - -| + | | | | + | | response | | + | |<------------------| | + | | | | + | | | | + | <---------------| | | + User ,---+---. ,----+----. ,----------. + ,-. |Apicast| | CAMEL | |APIBackend| + `-' `-------' `---------' `----------' + /|\ + | + / \ +``` + + +## Configuration + +``` +"policy_chain": [ + { + "name": "apicast.policy.apicast" + }, + { + "name": "apicast.policy.camel", + "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/" + } + } +] +``` + +- If http_proxy or https_proxy is not defined the all_proxy will be taken. + +## Caveats + +- This policy will disable all load-balancing policies and traffic will be + 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, if you need auth, please use + headers policy. + + +## Example Use case + +This policy was designed to be able to apply more fined grained policies and +transformation using Apache Camel. + +An example project can be found +[here](https://github.com/zregvart/camel-netty-proxy). This project is an HTTP +Proxy that transforms to uppercase all the response body given by the API +backend. diff --git a/gateway/src/apicast/policy/camel/apicast-policy.json b/gateway/src/apicast/policy/camel/apicast-policy.json new file mode 100644 index 000000000..b279e6a44 --- /dev/null +++ b/gateway/src/apicast/policy/camel/apicast-policy.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://apicast.io/policy-v1/schema#manifest#", + "name": "Camel Service", + "summary": "Adds an Camel proxy to the service.", + "description": [ + "With this policy all the traffic for this service will be routed accross ", + "the defined proxy" + ], + "version": "builtin", + "configuration": { + "type": "object", + "properties": { + "all_proxy": { + "description": "Defines a HTTP proxy to be used for connecting to services if a protocol-specific proxy is not specified. Authentication is not supported.", + "type": "string" + }, + "https_proxy": { + "description": "Defines a HTTPS proxy to be used for connecting to HTTPS services. Authentication is not supported", + "type": "string" + }, + "http_proxy": { + "description": "Defines a HTTP proxy to be used for connecting to HTTP services. Authentication is not supported", + "type": "string" + } + } + } +} diff --git a/gateway/src/apicast/policy/camel/camel.lua b/gateway/src/apicast/policy/camel/camel.lua new file mode 100644 index 000000000..0df3ff39b --- /dev/null +++ b/gateway/src/apicast/policy/camel/camel.lua @@ -0,0 +1,59 @@ +local policy = require('apicast.policy') +local _M = policy.new('http_proxy', 'builtin') + +local resty_url = require 'resty.url' +local ipairs = ipairs + +local new = _M.new + +local proxies = {"http", "https"} + +function _M.new(config) + local self = new(config) + self.proxies = {} + + if config.all_proxy then + local err + self.all_proxy, err = resty_url.parse(config.all_proxy) + if err then + ngx.log(ngx.WARN, "All proxy '", config.all_proxy, "' is not correctly defined, err:", err) + end + end + + for _, proto in ipairs(proxies) do + local val, err = resty_url.parse(config[string.format("%s_proxy", proto)]) + if err then + ngx.log(ngx.WARN, proto, " proxy is not correctly defined, err: ", err) + end + self.proxies[proto] = val or self.all_proxy + end + return self +end + +local function find_proxy(self, scheme) + return self.proxies[scheme] +end + +function _M:access(context) + local upstream = context.get_upstream() + if not upstream then + return + end + + upstream:set_skip_https_connect_on_proxy() +end + +function _M:export() + -- This get_http_proxy function will be called in upstream just in case if a + -- proxy is defined. + return { + get_http_proxy = function(uri) + if not uri.scheme then + return nil + end + return find_proxy(self, uri.scheme) + end + } +end + +return _M diff --git a/gateway/src/apicast/policy/camel/init.lua b/gateway/src/apicast/policy/camel/init.lua new file mode 100644 index 000000000..41c8df672 --- /dev/null +++ b/gateway/src/apicast/policy/camel/init.lua @@ -0,0 +1 @@ +return require("camel") diff --git a/gateway/src/apicast/upstream.lua b/gateway/src/apicast/upstream.lua index b4c246dcb..cff5b5f96 100644 --- a/gateway/src/apicast/upstream.lua +++ b/gateway/src/apicast/upstream.lua @@ -194,6 +194,9 @@ function _M:set_host_header() return host, nil end +function _M:set_skip_https_connect_on_proxy() + self.skip_https_connect = true +end --- Execute the upstream. --- @tparam table context any table (policy context, ngx.ctx) to store the upstream for later use by balancer function _M:call(context) diff --git a/gateway/src/resty/http/proxy.lua b/gateway/src/resty/http/proxy.lua index 5493498b2..4cc3ae583 100644 --- a/gateway/src/resty/http/proxy.lua +++ b/gateway/src/resty/http/proxy.lua @@ -37,6 +37,16 @@ local function connect_direct(httpc, request) return httpc end +local function _connect_tls_direct(httpc, request, host, port) + + local uri = request.uri + + local ok, err = httpc:ssl_handshake(nil, uri.host, request.ssl_verify) + if not ok then return nil, err end + + return httpc +end + local function _connect_proxy_https(httpc, request, host, port) -- When the connection is reused the tunnel is already established, so -- the second CONNECT request would reach the upstream instead of the proxy. @@ -61,7 +71,7 @@ local function _connect_proxy_https(httpc, request, host, port) return httpc end -local function connect_proxy(httpc, request) +local function connect_proxy(httpc, request, skip_https_connect) local uri = request.uri local host, port = httpc:resolve(uri.host, uri.port, uri) local proxy_uri = request.proxy @@ -88,9 +98,10 @@ local function connect_proxy(httpc, request) -- http proxy needs absolute URL as the request path request.path = format('%s://%s:%s%s', uri.scheme, host, port, uri.path or '/') return httpc - + elseif uri.scheme == 'https' and skip_https_connect then + request.path = format('%s://%s:%s%s', uri.scheme, host, port, request.path or '/') + return _connect_tls_direct(httpc, request, host, port) elseif uri.scheme == 'https' then - return _connect_proxy_https(httpc, request, host, port) else @@ -113,7 +124,7 @@ local function find_proxy_url(request) return request.proxy_uri or _M.find(uri) end -local function connect(request) +local function connect(request, skip_https_connect) local httpc = http.new() local proxy_uri = find_proxy_url(request) @@ -121,7 +132,7 @@ local function connect(request) request.proxy = proxy_uri if proxy_uri then - return connect_proxy(httpc, request) + return connect_proxy(httpc, request, skip_https_connect) else return connect_direct(httpc, request) end diff --git a/spec/policy/camel/camel_spec.lua b/spec/policy/camel/camel_spec.lua new file mode 100644 index 000000000..1136847ba --- /dev/null +++ b/spec/policy/camel/camel_spec.lua @@ -0,0 +1,61 @@ +local camel_policy = require('apicast.policy.camel') +local resty_url = require 'resty.url' + +describe('Camel policy', function() + local all_proxy_val = "http://all.com" + local http_proxy_val = "http://plain.com" + local https_proxy_val = "http://secure.com" + + local http_uri = {scheme="http"} + local https_uri = {scheme="https"} + + it("http[s] proxies are defined if all_proxy is in there", function() + local proxy = camel_policy.new({ + all_proxy = all_proxy_val + }) + local callback = proxy:export() + + assert.same(callback.get_http_proxy(http_uri), resty_url.parse(all_proxy_val)) + assert.same(callback.get_http_proxy(https_uri), resty_url.parse(all_proxy_val)) + end) + + it("all_proxy does not overwrite http/https proxies", function() + local proxy = camel_policy.new({ + all_proxy = all_proxy_val, + http_proxy = http_proxy_val, + https_proxy = https_proxy_val + }) + local callback = proxy:export() + + assert.same(callback.get_http_proxy(http_uri), resty_url.parse(http_proxy_val)) + assert.same(callback.get_http_proxy(https_uri), resty_url.parse(https_proxy_val)) + end) + + it("empty config return all nil", function() + local proxy = camel_policy.new({}) + local callback = proxy:export() + + assert.is_nil(callback.get_http_proxy(https_uri)) + assert.is_nil(callback.get_http_proxy(http_uri)) + end) + + describe("get_http_proxy callback", function() + local callback = camel_policy.new({ + all_proxy = all_proxy_val + }):export() + + it("Valid protocol", function() + + local result = callback.get_http_proxy( + resty_url.parse("http://google.com")) + assert.same(result, resty_url.parse(all_proxy_val)) + end) + + it("invalid protocol", function() + local result = callback:get_http_proxy( + {}, {scheme="invalid"}) + assert.is_nil(result) + end) + + end) +end) diff --git a/t/apicast-policy-camel.t b/t/apicast-policy-camel.t new file mode 100644 index 000000000..c0ca73a08 --- /dev/null +++ b/t/apicast-policy-camel.t @@ -0,0 +1,208 @@ +use lib 't'; +use Test::APIcast::Blackbox 'no_plan'; + +require("http_proxy.pl"); + +repeat_each(1); + +run_tests(); + +__DATA__ + +=== TEST 1: API backend connection uses http proxy +--- configuration +{ + "services": [ + { + "id": 42, + "backend_version": 1, + "backend_authentication_type": "service_token", + "backend_authentication_value": "token-value", + "proxy": { + "api_backend": "http://test:$TEST_NGINX_SERVER_PORT/", + "proxy_rules": [ + { "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 2 } + ], + "policy_chain": [ + { + "name": "apicast.policy.apicast" + }, + { + "name": "apicast.policy.camel", + "configuration": { + "http_proxy": "$TEST_NGINX_HTTP_PROXY" + } + } + ] + } + } + ] +} +--- backend + location /transactions/authrep.xml { + content_by_lua_block { + local expected = "service_token=token-value&service_id=42&usage%5Bhits%5D=2&user_key=value" + require('luassert').same(ngx.decode_args(expected), ngx.req.get_uri_args(0)) + } + } +--- upstream + location / { + access_by_lua_block { + local host = ngx.req.get_headers()["Host"] + local result = string.match(host, "^test:") + local assert = require('luassert') + assert.equals(result, "test:") + ngx.say("yay, api backend") + } + } +--- request +GET /?user_key=value +--- response_body +yay, api backend +--- error_code: 200 +--- error_log env +using proxy: $TEST_NGINX_HTTP_PROXY + +=== TEST 2: API backend using all_proxy +--- configuration +{ + "services": [ + { + "id": 42, + "backend_version": 1, + "backend_authentication_type": "service_token", + "backend_authentication_value": "token-value", + "proxy": { + "api_backend": "http://test:$TEST_NGINX_SERVER_PORT/", + "proxy_rules": [ + { "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 2 } + ], + "policy_chain": [ + { + "name": "apicast.policy.apicast" + }, + { + "name": "apicast.policy.http_proxy", + "configuration": { + "all_proxy": "$TEST_NGINX_HTTP_PROXY" + } + } + ] + } + } + ] +} +--- backend + location /transactions/authrep.xml { + content_by_lua_block { + local expected = "service_token=token-value&service_id=42&usage%5Bhits%5D=2&user_key=value" + require('luassert').same(ngx.decode_args(expected), ngx.req.get_uri_args(0)) + } + } +--- upstream + location / { + access_by_lua_block { + local host = ngx.req.get_headers()["Host"] + local result = string.match(host, "^test:") + local assert = require('luassert') + assert.equals(result, "test:") + ngx.say("yay, api backend") + } + } +--- request +GET /?user_key=value +--- response_body +yay, api backend +--- error_code: 200 +--- error_log env +using proxy: $TEST_NGINX_HTTP_PROXY + + +=== TEST 3: using HTTPS proxy for backend +--- init eval +$Test::Nginx::Util::PROXY_SSL_PORT = Test::APIcast::get_random_port(); +$Test::Nginx::Util::ENDPOINT_SSL_PORT = Test::APIcast::get_random_port(); +--- configuration random_port env eval +<