Skip to content

Commit

Permalink
Merge pull request #800 from 3scale/use-http-proxy
Browse files Browse the repository at this point in the history
[resty.http_ng] use http/s proxy for backend and upstream connections
  • Loading branch information
davidor authored Aug 22, 2018
2 parents 34ed827 + d763b88 commit fb579bc
Show file tree
Hide file tree
Showing 15 changed files with 912 additions and 63 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Conditional policy. This policy includes a condition and a policy chain, and only executes the chain when the condition is true [PR #812](https://github.com/3scale/apicast/pull/812), [PR #814](https://github.com/3scale/apicast/pull/814), [PR #820](https://github.com/3scale/apicast/pull/820)
- Request headers are now exposed in the context available when evaluating Liquid [PR #819](https://github.com/3scale/apicast/pull/819)
- Rewrite URL captures policy. This policy captures arguments in a URL and rewrites the URL using them [PR #827](https://github.com/3scale/apicast/pull/827), [THREESCALE-1139](https://issues.jboss.org/browse/THREESCALE-1139)
- Support for HTTP Proxy [THREESCALE-221](https://issues.jboss.org/browse/THREESCALE-221), [PR #835](https://github.com/3scale/apicast/pull/835)
- Support for HTTP Proxy [THREESCALE-221](https://issues.jboss.org/browse/THREESCALE-221), [#709](https://github.com/3scale/apicast/issues/709)
- Conditions for the limits of the rate-limit policy [PR #839](https://github.com/3scale/apicast/pull/839)

### Changed
Expand Down
12 changes: 12 additions & 0 deletions doc/parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,3 +311,15 @@ Path to a file with X.509 certificate in the PEM format for HTTPS.
**Default:** no value

Path to a file with the X.509 certificate secret key in the PEM format.

### `http_proxy`, `HTTP_PROXY`

**Default:** no value

Defines a HTTP proxy to be used for connecting to HTTP services.

### `https_proxy`, `HTTPS_PROXY`

**Default:** no value

Defines a HTTPS (TLS) proxy to be used for connecting to HTTPS services.
22 changes: 22 additions & 0 deletions gateway/conf/proxy.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
daemon off;

events {
worker_connections 1024;
}

error_log stderr debug;

stream {
lua_code_cache on;

resolver local=on;
lua_socket_log_errors off;

init_by_lua_block { proxy = require('t.fixtures.proxy') }

# define a TCP server listening on the port 1234:
server {
listen 8099;
content_by_lua_block { proxy() }
}
}
29 changes: 27 additions & 2 deletions gateway/src/apicast/backend_client.lua
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,34 @@ local user_agent = require('apicast.user_agent')
local resty_url = require('resty.url')
local resty_env = require('resty.env')

local http_proxy = require('resty.http.proxy')
local http_ng_ngx = require('resty.http_ng.backend.ngx')
-- resty.http_ng.backend.ngx is using ngx.location.capture, which is available only
-- on rewrite, access and content phases. We need to use cosockets (http_ng default backend)
-- everywhere else (like timers).
local http_ng_backend_phase = {
access = http_ng_ngx,
rewrite = http_ng_ngx,
content = http_ng_ngx,
}

local _M = {
endpoint = resty_env.get("BACKEND_ENDPOINT_OVERRIDE")

}

local mt = { __index = _M }

local function detect_http_client(endpoint)
local uri = resty_url.parse(endpoint)
local proxy = http_proxy.find(uri)

if proxy then -- use default client
return
else
return http_ng_backend_phase[ngx.get_phase()]
end
end

--- Return new instance of backend client
-- @tparam Service service object with service definition
-- @tparam http_ng.backend http_client async/test/custom http backend
Expand All @@ -52,6 +73,10 @@ function _M:new(service, http_client)
return nil, err
end

if not http_client then
http_client = detect_http_client(endpoint)
end

local client = http_ng.new{
backend = http_client,
options = {
Expand Down Expand Up @@ -106,7 +131,7 @@ local function call_backend_transaction(self, path, options, ...)
local url = build_url(self, path, ...)
local res = http_client.get(url, options)

ngx.log(ngx.INFO, 'backend client uri: ', url, ' ok: ', res.ok, ' status: ', res.status, ' body: ', res.body)
ngx.log(ngx.INFO, 'backend client uri: ', url, ' ok: ', res.ok, ' status: ', res.status, ' body: ', res.body, ' error: ', res.error)

return res
end
Expand Down
150 changes: 150 additions & 0 deletions gateway/src/apicast/http_proxy.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
local format = string.format

local http = require 'resty.resolver.http'
local resty_url = require "resty.url"
local resty_resolver = require 'resty.resolver'
local round_robin = require 'resty.balancer.round_robin'
local http_proxy = require 'resty.http.proxy'

local _M = { }

function _M.reset()
_M.balancer = round_robin.new()
_M.resolver = resty_resolver
_M.http_backend = require('resty.http_ng.backend.resty')
_M.dns_resolution = 'apicast' -- can be set to 'proxy' to let proxy do the name resolution

return _M
end

local function resolve_servers(uri)
local resolver = _M.resolver:instance()

if not resolver then
return nil, 'not initialized'
end

if not uri then
return nil, 'no url'
end

return resolver:get_servers(uri.host, uri)
end

function _M.resolve(uri)
local balancer = _M.balancer

if not balancer then
return nil, 'not initialized'
end

local servers, err = resolve_servers(uri)

if err then
return nil, err
end

local peers = balancer:peers(servers)
local peer = balancer:select_peer(peers)

local ip = uri.host
local port = uri.port

if peer then
ip = peer[1]
port = peer[2]
end

return ip, port
end

local function resolve(uri)
local host = uri.host
local port = uri.port

if _M.dns_resolution == 'apicast' then
host, port = _M.resolve(uri)
end

return host, port or resty_url.default_port(uri.scheme)
end

local function absolute_url(uri)
local host, port = resolve(uri)

return format('%s://%s:%s%s',
uri.scheme,
host,
port,
uri.path or ngx.var.uri or ''
)
end

local function current_path(uri)
return format('%s%s%s', uri.path or ngx.var.uri, ngx.var.is_args, ngx.var.query_string)
end

local function forward_https_request(proxy_uri, uri)
local request = {
uri = uri,
method = ngx.req.get_method(),
headers = ngx.req.get_headers(0, true),
path = current_path(uri),
body = http:get_client_body_reader(),
}

local httpc, err = http_proxy.new(request)

if not httpc then
ngx.log(ngx.ERR, 'could not connect to proxy: ', proxy_uri, ' err: ', err)

return ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE)
end

local res
res, err = httpc:request(request)

if res then
httpc:proxy_response(res)
httpc:set_keepalive()
else
ngx.log(ngx.ERR, 'failed to proxy request to: ', proxy_uri, ' err : ', err)
return ngx.exit(ngx.HTTP_BAD_GATEWAY)
end
end

local function get_proxy_uri(uri)
local proxy_uri, err = http_proxy.find(uri)
if not proxy_uri then return nil, err or 'invalid proxy url' end

if not proxy_uri.port then
proxy_uri.port = resty_url.default_port(proxy_uri.scheme)
end

return proxy_uri
end

function _M.find(upstream)
return get_proxy_uri(upstream.uri)
end

function _M.request(upstream, proxy_uri)
local uri = upstream.uri

if uri.scheme == 'http' then -- rewrite the request to use http_proxy
upstream.host = uri.host -- to keep correct Host header in case we need to resolve it to IP
upstream.servers = resolve_servers(proxy_uri)
upstream.uri.path = absolute_url(uri)
upstream:rewrite_request()
return
elseif uri.scheme == 'https' then
upstream:rewrite_request()
forward_https_request(proxy_uri, uri)
return ngx.exit(ngx.OK) -- terminate phase
else
ngx.log(ngx.ERR, 'could not connect to proxy: ', proxy_uri, ' err: ', 'invalid request scheme')
return ngx.exit(ngx.HTTP_BAD_GATEWAY)
end
end

return _M.reset()
2 changes: 1 addition & 1 deletion gateway/src/apicast/policy/apicast/apicast.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
local balancer = require('apicast.balancer')

local math = math
local setmetatable = setmetatable
local assert = assert
Expand Down Expand Up @@ -85,7 +86,6 @@ function _M:content(context)
local upstream = assert(context[self], 'missing upstream')

if upstream then
upstream:set_request_host()
upstream:call(context)
end
end
Expand Down
6 changes: 3 additions & 3 deletions gateway/src/apicast/policy/upstream/upstream.lua
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ local function init_config(config)
local upstream, err = Upstream.new(rule.url)

if upstream then
tab_insert(res, { regex = rule.regex, upstream = upstream })
tab_insert(res, { regex = rule.regex, url = rule.url })
else
ngx.log(ngx.WARN, 'failed to initialize upstream from url: ', rule.url, ' err: ', err)
end
Expand All @@ -49,7 +49,8 @@ function _M:rewrite(context)
for _, rule in ipairs(self.rules) do
if match(req_uri, rule.regex) then
ngx.log(ngx.DEBUG, 'upstream policy uri: ', req_uri, ' regex: ', rule.regex, ' match: true')
context[self] = rule.upstream
-- better to allocate new object for each request as it is going to get mutated
context[self] = Upstream.new(rule.url)
break
else
ngx.log(ngx.DEBUG, 'upstream policy uri: ', req_uri, ' regex: ', rule.regex, ' match: false')
Expand All @@ -61,7 +62,6 @@ function _M:content(context)
local upstream = context[self]

if upstream then
upstream:set_request_host()
upstream:call(context)
else
return nil, 'no upstream'
Expand Down
23 changes: 7 additions & 16 deletions gateway/src/apicast/proxy.lua
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ local setmetatable = setmetatable
local ipairs = ipairs
local encode_args = ngx.encode_args
local backend_client = require('apicast.backend_client')
local http_ng_ngx = require('resty.http_ng.backend.ngx')

local response_codes = env.enabled('APICAST_RESPONSE_CODES')
local reporting_executor = require('resty.concurrent.immediate_executor')
Expand Down Expand Up @@ -64,6 +63,7 @@ function _M.new(configuration)
configuration = assert(configuration, 'missing proxy configuration'),
cache = cache,
cache_handler = cache_handler,
http_ng_backend = nil,

-- Params to send in 3scale backend calls that are not the typical ones
-- (credentials, usage, etc.).
Expand Down Expand Up @@ -113,6 +113,10 @@ local function matched_patterns(matched_rules)
return patterns
end

local function build_backend_client(self, service)
return assert(backend_client:new(service, self.http_ng_backend), 'missing backend')
end

function _M:authorize(service, usage, credentials, ttl)
if not usage or not credentials then return nil, 'missing usage or credentials' end

Expand Down Expand Up @@ -140,7 +144,7 @@ function _M:authorize(service, usage, credentials, ttl)
-- set cached_key to nil to avoid doing the authrep in post_action
ngx.var.cached_key = nil

local backend = assert(backend_client:new(service, http_ng_ngx), 'missing backend')
local backend = build_backend_client(self, service)
local res = backend:authrep(formatted_usage, credentials, self.extra_params_backend_authrep)

local authorized, rejection_reason = self:handle_backend_response(cached_key, res, ttl)
Expand Down Expand Up @@ -295,21 +299,8 @@ local function response_codes_data()
return params
end

-- resty.http_ng.backend.ngx is using ngx.location.capture, which is available only
-- on rewrite, access and content phases. We need to use cosockets (http_ng default backend)
-- everywhere else (like timers).
local http_ng_backend_phase = {
access = http_ng_ngx,
rewrite = http_ng_ngx,
content = http_ng_ngx,
}

local function build_backend_client(service)
return backend_client:new(service, http_ng_backend_phase[ngx.get_phase()])
end

local function post_action(self, cached_key, service, credentials, formatted_usage)
local backend = assert(build_backend_client(service), 'missing backend')
local backend = build_backend_client(self, service)
local res = backend:authrep(
formatted_usage,
credentials,
Expand Down
Loading

0 comments on commit fb579bc

Please sign in to comment.