api_valve 0.0.1.alpha → 0.0.1.beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/api_valve/error_responder.rb +3 -1
- data/lib/api_valve/forwarder/permission_handler.rb +40 -0
- data/lib/api_valve/forwarder/request.rb +29 -15
- data/lib/api_valve/forwarder/response.rb +20 -3
- data/lib/api_valve/forwarder.rb +41 -19
- data/lib/api_valve/proxy.rb +13 -9
- data/lib/api_valve/router.rb +2 -2
- data/lib/api_valve.rb +3 -0
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0db44e3a1e246e94e9c31ad856fef01d7b76626cfe665bd18dfa769223c385bc
|
4
|
+
data.tar.gz: 7a05b9b44d9606bb831da1709bacc50135115311d4517f87bdad59e17ea3340d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d86f709762d7cd80fe4d1d01184310ae422e38dac84c9244a968ce52cae0ede75365975d8bd86b3fc9bdc5e9dd16d34ac1e94bf3f6129d97d662cabb13ac72d8
|
7
|
+
data.tar.gz: a733954d49421ad1735f12c27b3361026c8a3f4d5378837090d5373dcfb75e8409adca6566b6d437cfb64d0512a8531b8de551a14cd65ed1c60771df4514e039
|
@@ -15,7 +15,9 @@ module ApiValve
|
|
15
15
|
private
|
16
16
|
|
17
17
|
def status
|
18
|
-
|
18
|
+
status = @error.try(:http_status)
|
19
|
+
return status if status&.is_a?(Integer)
|
20
|
+
Rack::Utils::SYMBOL_TO_STATUS_CODE[status] || 500
|
19
21
|
end
|
20
22
|
|
21
23
|
def json_error
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module ApiValve
|
2
|
+
# This class is responsible to decide if a request is allowed or not, and can
|
3
|
+
# be extended with more ACL related features, for example returning a list of
|
4
|
+
# attributes that can be read or written.
|
5
|
+
|
6
|
+
class Forwarder::PermissionHandler
|
7
|
+
module RequestIntegration
|
8
|
+
private
|
9
|
+
|
10
|
+
def permission_handler
|
11
|
+
permission_handler_klass.instance(@original_request, permission_handler_options)
|
12
|
+
end
|
13
|
+
|
14
|
+
def permission_handler_klass
|
15
|
+
permission_handler_options[:klass] || Forwarder::PermissionHandler
|
16
|
+
end
|
17
|
+
|
18
|
+
def permission_handler_options
|
19
|
+
@options[:permission_handler] || {}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns an instance of the PermissionHandler, cached in the request env
|
24
|
+
# This allows re-use of the PermissionHandler by both Request and Response instances
|
25
|
+
def self.instance(request, options)
|
26
|
+
request.env['permission_handler'] ||= new(request, options)
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(request, options = {})
|
30
|
+
@request = request
|
31
|
+
@options = options
|
32
|
+
end
|
33
|
+
|
34
|
+
# Tells the request class if the request is allowed
|
35
|
+
# Simple implementation is always true. Override in your implementation.
|
36
|
+
def request_allowed?
|
37
|
+
true
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -1,7 +1,12 @@
|
|
1
1
|
module ApiValve
|
2
|
-
#
|
3
|
-
#
|
2
|
+
# This class is wraps the original request. It's methods are called by
|
3
|
+
# the Forwarder to make the actual request in the target endpoint.
|
4
|
+
# So by changing the public methods in this call, we can control how the
|
5
|
+
# request is forwarded
|
6
|
+
|
4
7
|
class Forwarder::Request
|
8
|
+
include Forwarder::PermissionHandler::RequestIntegration
|
9
|
+
|
5
10
|
attr_reader :original_request, :options
|
6
11
|
|
7
12
|
WHITELISTED_HEADERS = %w(
|
@@ -21,13 +26,21 @@ module ApiValve
|
|
21
26
|
|
22
27
|
def initialize(original_request, options = {})
|
23
28
|
@original_request = original_request
|
24
|
-
@options =
|
29
|
+
@options = options.with_indifferent_access
|
30
|
+
end
|
31
|
+
|
32
|
+
# Is the request allowed? If it returns false, Forwarder will raise Error::Forbidden
|
33
|
+
def allowed?
|
34
|
+
permission_handler.request_allowed?
|
25
35
|
end
|
26
36
|
|
37
|
+
# HTTP method to use when forwarding. Must return sym.
|
38
|
+
# Returns original request method
|
27
39
|
def method
|
28
40
|
@method ||= original_request.request_method.downcase.to_sym
|
29
41
|
end
|
30
42
|
|
43
|
+
# URL path to use when forwarding
|
31
44
|
def path
|
32
45
|
path = options['endpoint'] || ''
|
33
46
|
if pattern = options['path']
|
@@ -35,26 +48,27 @@ module ApiValve
|
|
35
48
|
else
|
36
49
|
path += original_request.path_info
|
37
50
|
end
|
51
|
+
# we remove leading slash so we can use endpoints with deeper folder levels
|
38
52
|
path.gsub(%r{^/}, '')
|
39
53
|
end
|
40
54
|
|
55
|
+
# Returns a hash of headers to forward to the target endpoint
|
56
|
+
# Override to control the HTTP headers that will be passed through
|
41
57
|
def headers
|
42
58
|
whitelisted_headers.each_with_object({}) do |key, h|
|
43
59
|
h[key] = header(key)
|
44
60
|
end.merge('X-Request-Id' => Thread.current[:request_id]).compact
|
45
61
|
end
|
46
62
|
|
47
|
-
|
48
|
-
|
49
|
-
name = name.upcase.tr('-', '_')
|
50
|
-
original_request.get_header(name)
|
51
|
-
end
|
52
|
-
|
63
|
+
# Returns body to forward to the target endpoint
|
64
|
+
# Override to control the payload that is passed through
|
53
65
|
def body
|
54
66
|
return unless %i(put post patch).include? method
|
55
67
|
original_request.body.read
|
56
68
|
end
|
57
69
|
|
70
|
+
# Returns query params to forward to the target endpoint
|
71
|
+
# Override to control the query parameters that can be passed through
|
58
72
|
def url_params
|
59
73
|
return unless original_request.query_string.present?
|
60
74
|
@url_params ||= Rack::Utils.parse_nested_query(original_request.query_string)
|
@@ -62,14 +76,14 @@ module ApiValve
|
|
62
76
|
|
63
77
|
private
|
64
78
|
|
65
|
-
def
|
66
|
-
|
79
|
+
def header(name)
|
80
|
+
name = "HTTP_#{name}" unless NOT_PREFIXED_HEADERS.include? name
|
81
|
+
name = name.upcase.tr('-', '_')
|
82
|
+
original_request.get_header(name)
|
67
83
|
end
|
68
84
|
|
69
|
-
def
|
70
|
-
|
71
|
-
whitelisted_headers: WHITELISTED_HEADERS
|
72
|
-
}
|
85
|
+
def whitelisted_headers
|
86
|
+
@options[:whitelisted_headers] || WHITELISTED_HEADERS
|
73
87
|
end
|
74
88
|
end
|
75
89
|
end
|
@@ -1,18 +1,31 @@
|
|
1
1
|
module ApiValve
|
2
2
|
# Wraps faraday response
|
3
3
|
# Responsible for altering the response before it is returned
|
4
|
+
|
5
|
+
# This class is wraps the original response. The rack_response method
|
6
|
+
# is called by the Forwarder to build the rack response that will be
|
7
|
+
# returned by the proxy.
|
8
|
+
# By changing this class, we can control how the request is published
|
9
|
+
# to the original caller
|
10
|
+
|
4
11
|
class Forwarder::Response
|
5
|
-
|
12
|
+
include Forwarder::PermissionHandler::RequestIntegration
|
13
|
+
|
14
|
+
attr_reader :original_request, :original_response
|
6
15
|
|
7
16
|
WHITELISTED_HEADERS = %w(
|
8
17
|
Content-Type
|
9
18
|
Cache-Control
|
19
|
+
Location
|
10
20
|
).freeze
|
11
21
|
|
12
|
-
def initialize(original_response)
|
22
|
+
def initialize(original_request, original_response, options = {})
|
23
|
+
@original_request = original_request
|
13
24
|
@original_response = original_response
|
25
|
+
@options = options.with_indifferent_access
|
14
26
|
end
|
15
27
|
|
28
|
+
# Must return a rack compatible response array of status code, headers and body
|
16
29
|
def rack_response
|
17
30
|
[status, headers, [body]]
|
18
31
|
end
|
@@ -24,11 +37,15 @@ module ApiValve
|
|
24
37
|
end
|
25
38
|
|
26
39
|
def headers
|
27
|
-
|
40
|
+
whitelisted_headers.each_with_object({}) do |k, h|
|
28
41
|
h[k] = original_response.headers[k]
|
29
42
|
end.compact
|
30
43
|
end
|
31
44
|
|
45
|
+
def whitelisted_headers
|
46
|
+
@options[:whitelisted_headers] || WHITELISTED_HEADERS
|
47
|
+
end
|
48
|
+
|
32
49
|
def body
|
33
50
|
original_response.body.to_s
|
34
51
|
end
|
data/lib/api_valve/forwarder.rb
CHANGED
@@ -1,35 +1,50 @@
|
|
1
1
|
module ApiValve
|
2
|
+
# This class is responsible for forwarding the HTTP request to the
|
3
|
+
# designated endpoint. It is instanciated once per Proxy with relevant
|
4
|
+
# options, and called from the router.
|
5
|
+
|
2
6
|
class Forwarder
|
3
|
-
autoload :
|
4
|
-
autoload :
|
7
|
+
autoload :PermissionHandler, 'api_valve/forwarder/permission_handler'
|
8
|
+
autoload :Request, 'api_valve/forwarder/request'
|
9
|
+
autoload :Response, 'api_valve/forwarder/response'
|
5
10
|
|
6
11
|
include Benchmarking
|
7
12
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
}
|
13
|
+
# Initialized with global options. Possible values are:
|
14
|
+
# request: Options for the request wrapper. See Request#new.
|
15
|
+
# response: Options for the response wrapper. See Response#new
|
16
|
+
def initialize(options = {})
|
17
|
+
@options = options.with_indifferent_access
|
18
|
+
end
|
12
19
|
|
13
|
-
|
14
|
-
|
20
|
+
# Takes the original rack request with optional options and returns a rack response
|
21
|
+
# Instanciates the Request and Response classes and wraps them arround the original
|
22
|
+
# request and response.
|
23
|
+
def call(original_request, local_options = {})
|
24
|
+
request = request_klass.new(original_request, request_options.deep_merge(local_options))
|
25
|
+
raise Error::Forbidden unless request.allowed?
|
26
|
+
response_klass.new(original_request, run_request(request), response_options).rack_response
|
27
|
+
end
|
15
28
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
29
|
+
private
|
30
|
+
|
31
|
+
def request_klass
|
32
|
+
request_options[:klass] || Request
|
20
33
|
end
|
21
34
|
|
22
|
-
def
|
23
|
-
|
24
|
-
|
35
|
+
def request_options
|
36
|
+
# integrate permission handler options as it is instantiated in the request
|
37
|
+
(@options[:request] || {}).merge(@options.slice(:permission_handler))
|
25
38
|
end
|
26
39
|
|
27
|
-
|
28
|
-
|
29
|
-
@endpoint = File.join(endpoint, '')
|
40
|
+
def response_klass
|
41
|
+
response_options[:klass] || Response
|
30
42
|
end
|
31
43
|
|
32
|
-
|
44
|
+
def response_options
|
45
|
+
# integrate permission handler options as it is instantiated in the response
|
46
|
+
(@options[:response] || {}).merge(@options.slice(:permission_handler) || {})
|
47
|
+
end
|
33
48
|
|
34
49
|
def run_request(request)
|
35
50
|
log_request request
|
@@ -40,6 +55,8 @@ module ApiValve
|
|
40
55
|
end
|
41
56
|
log_response response, elapsed_time
|
42
57
|
response
|
58
|
+
rescue Faraday::ConnectionFailed
|
59
|
+
raise Error::ServiceUnavailable
|
43
60
|
end
|
44
61
|
|
45
62
|
def log_request(request)
|
@@ -71,5 +88,10 @@ module ApiValve
|
|
71
88
|
config.adapter Faraday.default_adapter
|
72
89
|
end
|
73
90
|
end
|
91
|
+
|
92
|
+
# Enforce trailing slash
|
93
|
+
def endpoint
|
94
|
+
@endpoint ||= File.join(@options[:endpoint], '')
|
95
|
+
end
|
74
96
|
end
|
75
97
|
end
|
data/lib/api_valve/proxy.rb
CHANGED
@@ -2,9 +2,11 @@ require 'byebug'
|
|
2
2
|
|
3
3
|
module ApiValve
|
4
4
|
class Proxy
|
5
|
-
include ActiveSupport::
|
5
|
+
include ActiveSupport::Callbacks
|
6
6
|
|
7
|
-
FORWARDER_OPTIONS = %w(endpoint).freeze
|
7
|
+
FORWARDER_OPTIONS = %w(endpoint request response permission_handler).freeze
|
8
|
+
|
9
|
+
define_callbacks :call
|
8
10
|
|
9
11
|
class << self
|
10
12
|
def from_yaml(file_path)
|
@@ -18,15 +20,16 @@ module ApiValve
|
|
18
20
|
end
|
19
21
|
end
|
20
22
|
|
21
|
-
attr_reader :forwarder, :router
|
23
|
+
attr_reader :request, :forwarder, :router
|
22
24
|
|
23
25
|
def initialize(forwarder)
|
24
26
|
@forwarder = forwarder
|
25
27
|
@router = Router.new
|
26
28
|
end
|
27
29
|
|
28
|
-
def call(
|
29
|
-
@
|
30
|
+
def call(env)
|
31
|
+
@request = Rack::Request.new(env)
|
32
|
+
run_callbacks(:call) { @router.call(@request) }
|
30
33
|
rescue ApiValve::Error::Base => e
|
31
34
|
ErrorResponder.new(e).call
|
32
35
|
end
|
@@ -35,20 +38,21 @@ module ApiValve
|
|
35
38
|
|
36
39
|
def build_routes_from_config(config)
|
37
40
|
config['routes']&.each do |route_config|
|
38
|
-
method, path_regexp,
|
41
|
+
method, path_regexp, request_override = *route_config.values_at('method', 'path', 'request')
|
42
|
+
method ||= %w(get head patch post put) # no method defined means all methods
|
39
43
|
if route_config['raise']
|
40
44
|
deny method, path_regexp, with: route_config['raise']
|
41
45
|
else
|
42
|
-
forward method, path_regexp,
|
46
|
+
forward method, path_regexp, request_override
|
43
47
|
end
|
44
48
|
end
|
45
49
|
forward_all
|
46
50
|
end
|
47
51
|
|
48
|
-
def forward(methods, path_regexp = nil,
|
52
|
+
def forward(methods, path_regexp = nil, request_override = {})
|
49
53
|
Array.wrap(methods).each do |method|
|
50
54
|
router.public_send(method, path_regexp, proc { |request, match_data|
|
51
|
-
forwarder.call request, {'match_data' => match_data}.merge(
|
55
|
+
forwarder.call request, {'match_data' => match_data}.merge(request_override || {})
|
52
56
|
})
|
53
57
|
end
|
54
58
|
end
|
data/lib/api_valve/router.rb
CHANGED
data/lib/api_valve.rb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
|
+
require 'active_support/callbacks'
|
1
2
|
require 'active_support/configurable'
|
2
3
|
require 'active_support/core_ext/class'
|
3
4
|
require 'active_support/core_ext/hash'
|
5
|
+
require 'active_support/core_ext/object'
|
4
6
|
require 'active_support/core_ext/module'
|
7
|
+
require 'active_support/json'
|
5
8
|
require 'active_support/rescuable'
|
6
9
|
require 'benchmark'
|
7
10
|
require 'faraday'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: api_valve
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.1.
|
4
|
+
version: 0.0.1.beta.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- mkon
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-05-
|
11
|
+
date: 2018-05-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -106,28 +106,28 @@ dependencies:
|
|
106
106
|
requirements:
|
107
107
|
- - '='
|
108
108
|
- !ruby/object:Gem::Version
|
109
|
-
version: 0.
|
109
|
+
version: 0.56.0
|
110
110
|
type: :development
|
111
111
|
prerelease: false
|
112
112
|
version_requirements: !ruby/object:Gem::Requirement
|
113
113
|
requirements:
|
114
114
|
- - '='
|
115
115
|
- !ruby/object:Gem::Version
|
116
|
-
version: 0.
|
116
|
+
version: 0.56.0
|
117
117
|
- !ruby/object:Gem::Dependency
|
118
118
|
name: rubocop-rspec
|
119
119
|
requirement: !ruby/object:Gem::Requirement
|
120
120
|
requirements:
|
121
121
|
- - '='
|
122
122
|
- !ruby/object:Gem::Version
|
123
|
-
version: 1.
|
123
|
+
version: 1.25.1
|
124
124
|
type: :development
|
125
125
|
prerelease: false
|
126
126
|
version_requirements: !ruby/object:Gem::Requirement
|
127
127
|
requirements:
|
128
128
|
- - '='
|
129
129
|
- !ruby/object:Gem::Version
|
130
|
-
version: 1.
|
130
|
+
version: 1.25.1
|
131
131
|
- !ruby/object:Gem::Dependency
|
132
132
|
name: simplecov
|
133
133
|
requirement: !ruby/object:Gem::Requirement
|
@@ -183,6 +183,7 @@ files:
|
|
183
183
|
- lib/api_valve/error.rb
|
184
184
|
- lib/api_valve/error_responder.rb
|
185
185
|
- lib/api_valve/forwarder.rb
|
186
|
+
- lib/api_valve/forwarder/permission_handler.rb
|
186
187
|
- lib/api_valve/forwarder/request.rb
|
187
188
|
- lib/api_valve/forwarder/response.rb
|
188
189
|
- lib/api_valve/middleware/error_handling.rb
|