agile-proxy 0.1.0
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 +7 -0
- data/.bowerrc +3 -0
- data/.gitignore +8 -0
- data/.rspec +2 -0
- data/.rubocop.yml +36 -0
- data/.travis.yml +8 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +267 -0
- data/Guardfile +20 -0
- data/LICENSE +22 -0
- data/README.md +93 -0
- data/Rakefile +13 -0
- data/agile-proxy.gemspec +50 -0
- data/assets/index.html +39 -0
- data/assets/ui/app/HttpFlexibleProxyApi.js +31 -0
- data/assets/ui/app/app.js +1 -0
- data/assets/ui/app/controller/Stubs.js +64 -0
- data/assets/ui/app/controller/main.js +12 -0
- data/assets/ui/app/directive/AppEnhancedFormElement.js +21 -0
- data/assets/ui/app/directive/AppFor.js +16 -0
- data/assets/ui/app/directive/AppResponseEditor.js +54 -0
- data/assets/ui/app/model/RequestSpec.js +6 -0
- data/assets/ui/app/routes.js +10 -0
- data/assets/ui/app/service/Dialog.js +49 -0
- data/assets/ui/app/service/DomId.js +10 -0
- data/assets/ui/app/service/Error.js +7 -0
- data/assets/ui/app/service/Stub.js +36 -0
- data/assets/ui/app/view/404.html +2 -0
- data/assets/ui/app/view/dialog/error.html +10 -0
- data/assets/ui/app/view/dialog/yesNo.html +8 -0
- data/assets/ui/app/view/responses/editForm.html +78 -0
- data/assets/ui/app/view/status.html +1 -0
- data/assets/ui/app/view/stubs.html +19 -0
- data/assets/ui/app/view/stubs/edit.html +58 -0
- data/assets/ui/css/main.css +3 -0
- data/bin/agile_proxy +113 -0
- data/bower.json +27 -0
- data/config.yml +6 -0
- data/db.yml +10 -0
- data/db/migrations/20140818110800_create_users.rb +9 -0
- data/db/migrations/20140818134700_create_applications.rb +10 -0
- data/db/migrations/20140818135200_create_request_specs.rb +13 -0
- data/db/migrations/20140821115300_create_responses.rb +14 -0
- data/db/migrations/20140823082900_add_method_to_request_specs.rb +7 -0
- data/db/migrations/20140823083900_rename_request_spec_columns.rb +8 -0
- data/db/migrations/20141031072100_add_url_type_to_request_specs.rb +8 -0
- data/db/migrations/20141105125600_add_conditions_to_request_specs.rb +7 -0
- data/db/migrations/20141106083100_add_username_and_password_to_applications.rb +8 -0
- data/db/migrations/20141119143800_add_record_to_applications.rb +7 -0
- data/db/migrations/20141119174300_create_recordings.rb +18 -0
- data/db/schema.rb +78 -0
- data/examples/README.md +1 -0
- data/examples/facebook_api.html +59 -0
- data/examples/tumblr_api.html +22 -0
- data/lib/agile_proxy.rb +8 -0
- data/lib/agile_proxy/api/applications.rb +77 -0
- data/lib/agile_proxy/api/recordings.rb +52 -0
- data/lib/agile_proxy/api/request_specs.rb +85 -0
- data/lib/agile_proxy/api/root.rb +41 -0
- data/lib/agile_proxy/config.rb +63 -0
- data/lib/agile_proxy/handlers/handler.rb +43 -0
- data/lib/agile_proxy/handlers/proxy_handler.rb +110 -0
- data/lib/agile_proxy/handlers/request_handler.rb +57 -0
- data/lib/agile_proxy/handlers/stub_handler.rb +113 -0
- data/lib/agile_proxy/mitm.crt +22 -0
- data/lib/agile_proxy/mitm.key +27 -0
- data/lib/agile_proxy/model/application.rb +20 -0
- data/lib/agile_proxy/model/recording.rb +16 -0
- data/lib/agile_proxy/model/request_spec.rb +47 -0
- data/lib/agile_proxy/model/response.rb +56 -0
- data/lib/agile_proxy/model/user.rb +17 -0
- data/lib/agile_proxy/proxy_connection.rb +113 -0
- data/lib/agile_proxy/route.rb +106 -0
- data/lib/agile_proxy/router.rb +99 -0
- data/lib/agile_proxy/server.rb +85 -0
- data/lib/agile_proxy/servers/api.rb +41 -0
- data/lib/agile_proxy/servers/request_spec.rb +30 -0
- data/lib/agile_proxy/version.rb +6 -0
- data/load_proxy.js +39 -0
- data/log/.gitkeep +0 -0
- data/spec/common_helper.rb +32 -0
- data/spec/fixtures/test-server.crt +15 -0
- data/spec/fixtures/test-server.key +15 -0
- data/spec/integration/helpers/request_spec_helper.rb +60 -0
- data/spec/integration/specs/lib/server_spec.rb +407 -0
- data/spec/integration_spec_helper.rb +18 -0
- data/spec/spec_helper.rb +39 -0
- data/spec/support/test_server.rb +75 -0
- data/spec/unit/agile_proxy/api/applications_spec.rb +102 -0
- data/spec/unit/agile_proxy/api/common_helper.rb +31 -0
- data/spec/unit/agile_proxy/api/recordings_spec.rb +115 -0
- data/spec/unit/agile_proxy/api/request_specs_spec.rb +159 -0
- data/spec/unit/agile_proxy/handlers/handler_spec.rb +8 -0
- data/spec/unit/agile_proxy/handlers/proxy_handler_spec.rb +138 -0
- data/spec/unit/agile_proxy/handlers/request_handler_spec.rb +55 -0
- data/spec/unit/agile_proxy/handlers/stub_handler_spec.rb +154 -0
- data/spec/unit/agile_proxy/model/recording_spec.rb +0 -0
- data/spec/unit/agile_proxy/model/request_spec_spec.rb +45 -0
- data/spec/unit/agile_proxy/model/response_spec.rb +38 -0
- data/spec/unit/agile_proxy/server_spec.rb +88 -0
- data/spec/unit/agile_proxy/servers/api_spec.rb +31 -0
- data/spec/unit/agile_proxy/servers/request_spec_spec.rb +32 -0
- metadata +618 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require_relative 'application'
|
|
2
|
+
module AgileProxy
|
|
3
|
+
#
|
|
4
|
+
# = The recording model
|
|
5
|
+
#
|
|
6
|
+
# When an application is set to allow recording, every HTTP(s) request/response cycle coming through the proxy
|
|
7
|
+
# will create an instance of this model and persist it to the database.
|
|
8
|
+
#
|
|
9
|
+
# An API is then available to access this data via REST for the UI or test suite etc...
|
|
10
|
+
#
|
|
11
|
+
class Recording < ActiveRecord::Base
|
|
12
|
+
belongs_to :application
|
|
13
|
+
serialize :request_headers, JSON
|
|
14
|
+
serialize :response_headers, JSON
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require_relative 'application'
|
|
2
|
+
require_relative 'user'
|
|
3
|
+
require_relative 'response'
|
|
4
|
+
module AgileProxy
|
|
5
|
+
#
|
|
6
|
+
# = The Request Spec model
|
|
7
|
+
# The request spec is an input/output specification that incoming HTTP(s) requests are matched against.
|
|
8
|
+
#
|
|
9
|
+
# It uses the action dispatch router to do this matching,
|
|
10
|
+
# which is fed data from the database as it's input - i.e. its routing
|
|
11
|
+
# table is generated on the fly.
|
|
12
|
+
#
|
|
13
|
+
# This model is responsible not only for retrieving and persisting these
|
|
14
|
+
# request specifications, but also for creating the response
|
|
15
|
+
#
|
|
16
|
+
class RequestSpec < ActiveRecord::Base
|
|
17
|
+
belongs_to :application
|
|
18
|
+
belongs_to :user
|
|
19
|
+
belongs_to :response
|
|
20
|
+
accepts_nested_attributes_for :response
|
|
21
|
+
validates_inclusion_of :url_type, in: %w(url regex)
|
|
22
|
+
def initialize(attrs = {})
|
|
23
|
+
attrs[:http_method] = attrs.delete(:method) if attrs.key?(:method)
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
# The conditions are at present, stored as a JSON string. This is editable as a string in the UI, and therefore
|
|
27
|
+
# accessible using 'conditions' as normal.
|
|
28
|
+
# This method returns a JSON decoded version of this as a HASH
|
|
29
|
+
# @return [Hash] decoded conditions
|
|
30
|
+
def conditions_json
|
|
31
|
+
ActiveSupport::JSON.decode(conditions)
|
|
32
|
+
end
|
|
33
|
+
# This method's output is a 'rack' response, but its input is not.
|
|
34
|
+
# When the router has determined that this request spec is the one that is going to be sent to the client,
|
|
35
|
+
# it will call this method with the request's parameters, headers and body.
|
|
36
|
+
#
|
|
37
|
+
# if no response has been specified, an empty body will be returned,
|
|
38
|
+
# otherwise a 'rack' version of the response is returned
|
|
39
|
+
# @param params [Hash] the request parameters
|
|
40
|
+
# @param headers [Hash] The request headers
|
|
41
|
+
# @param body [String] The request body
|
|
42
|
+
# @return [Array] The rack response
|
|
43
|
+
def call(params, headers, body)
|
|
44
|
+
response.nil? ? [204, { 'Content-Type' => 'text/plain' }, ''] : response.to_rack(params, headers, body)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
require 'flavour_saver'
|
|
2
|
+
require 'flavour_saver/runtime'
|
|
3
|
+
module AgileProxy
|
|
4
|
+
#
|
|
5
|
+
# = The associated response for the RequestSpec
|
|
6
|
+
#
|
|
7
|
+
# An instance of this class is expected to be stored alongside every RequestSpec.
|
|
8
|
+
#
|
|
9
|
+
# It is responsible for the following :-
|
|
10
|
+
#
|
|
11
|
+
# 1. Retrieving response
|
|
12
|
+
# 2. Persisting responses
|
|
13
|
+
# 3. Providing a 'rack' ouput of the response
|
|
14
|
+
# 4. Convenient setters for code, body and content_type
|
|
15
|
+
# 5. Template parsing
|
|
16
|
+
class Response < ActiveRecord::Base
|
|
17
|
+
PROTECTED_HEADERS = ['Content-Type']
|
|
18
|
+
has_many :request_specs
|
|
19
|
+
serialize :headers, JSON
|
|
20
|
+
# A convenient setter for the content_type within the header
|
|
21
|
+
# @param val [String] The content type
|
|
22
|
+
def content_type=(val)
|
|
23
|
+
write_attribute(:content_type, val)
|
|
24
|
+
headers.merge!('Content-Type' => val)
|
|
25
|
+
val
|
|
26
|
+
end
|
|
27
|
+
# Provides the response as a 'rack' response
|
|
28
|
+
#
|
|
29
|
+
# If the response is a template (by specifying is_template as true), the output will
|
|
30
|
+
# have its template values parsed and replaced with
|
|
31
|
+
# data from the input_params, input_headers and input_body
|
|
32
|
+
# Otherwise, the body of the output is sent as is.
|
|
33
|
+
# @param input_params [Hash] The input parameters as a hash
|
|
34
|
+
# @param _input_headers [Hash] The input headers as a hash
|
|
35
|
+
# @param _input_body [String] The input body
|
|
36
|
+
# @return [Array] A 'rack' response array (status, headers, body)
|
|
37
|
+
def to_rack(input_params, _input_headers, _input_body)
|
|
38
|
+
output_headers = headers.clone
|
|
39
|
+
output_content = content
|
|
40
|
+
output_status_code = status_code
|
|
41
|
+
if is_template
|
|
42
|
+
data = OpenStruct.new input_params
|
|
43
|
+
begin
|
|
44
|
+
template = Tilt['handlebars'].new { output_content }
|
|
45
|
+
output_content = template.render data
|
|
46
|
+
rescue ::FlavourSaver::UnknownHelperException => ex
|
|
47
|
+
method = ex.message.match(/Template context doesn't respond to method "(.*)"/)[1]
|
|
48
|
+
output_content = "Missing var or method '#{method}' in data."
|
|
49
|
+
output_status_code = 500
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
EventMachine::Synchrony.sleep(delay) if delay > 0
|
|
53
|
+
[output_status_code, output_headers, output_content]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require_relative 'application'
|
|
2
|
+
require 'active_record'
|
|
3
|
+
module AgileProxy
|
|
4
|
+
#
|
|
5
|
+
# = An API User
|
|
6
|
+
#
|
|
7
|
+
# The API access to the system is multi user in that each user
|
|
8
|
+
# can have many applications and each application can have many stubs etc...
|
|
9
|
+
#
|
|
10
|
+
# This class is responsible for :-
|
|
11
|
+
# 1. Retrieving users from storage
|
|
12
|
+
# 2. Persisting users to storage
|
|
13
|
+
# 3. Providing a list of applications that the user owns
|
|
14
|
+
class User < ActiveRecord::Base
|
|
15
|
+
has_many :applications
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
require 'uri'
|
|
2
|
+
require 'eventmachine'
|
|
3
|
+
require 'http/parser'
|
|
4
|
+
require 'em-http'
|
|
5
|
+
require 'evma_httpserver'
|
|
6
|
+
require 'em-synchrony'
|
|
7
|
+
require 'stringio'
|
|
8
|
+
require 'rack'
|
|
9
|
+
|
|
10
|
+
module AgileProxy
|
|
11
|
+
#
|
|
12
|
+
# = The Proxy Connection
|
|
13
|
+
#
|
|
14
|
+
# This class is the event machine connection used by the proxy. Every request creates a new instance of this
|
|
15
|
+
class ProxyConnection < EventMachine::Connection
|
|
16
|
+
attr_accessor :handler
|
|
17
|
+
def post_init
|
|
18
|
+
@parser = Http::Parser.new(self)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def receive_data(data)
|
|
22
|
+
@parser << data
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def on_message_begin
|
|
26
|
+
@headers = nil
|
|
27
|
+
@body = ''
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def on_headers_complete(headers)
|
|
31
|
+
@headers = headers
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def on_body(chunk)
|
|
35
|
+
@body << chunk
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def on_message_complete
|
|
39
|
+
if @parser.http_method == 'CONNECT'
|
|
40
|
+
restart_with_ssl(@parser.request_url)
|
|
41
|
+
else
|
|
42
|
+
if @ssl
|
|
43
|
+
uri = URI.parse(@parser.request_url)
|
|
44
|
+
@url = "https://#{@ssl}#{[uri.path, uri.query].compact.join('?')}"
|
|
45
|
+
else
|
|
46
|
+
@url = @parser.request_url
|
|
47
|
+
end
|
|
48
|
+
handle_request
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
protected
|
|
53
|
+
|
|
54
|
+
def restart_with_ssl(url)
|
|
55
|
+
@ssl = url
|
|
56
|
+
@parser = Http::Parser.new(self)
|
|
57
|
+
@original_headers = @headers.clone
|
|
58
|
+
send_data("HTTP/1.0 200 Connection established\r\nProxy-agent: Http-Flexible-Proxy/0.0.0\r\n\r\n")
|
|
59
|
+
start_tls(
|
|
60
|
+
private_key_file: File.expand_path('../mitm.key', __FILE__),
|
|
61
|
+
cert_chain_file: File.expand_path('../mitm.crt', __FILE__)
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def handle_request
|
|
66
|
+
EM.synchrony do
|
|
67
|
+
request = ActionDispatch::Request.new(env)
|
|
68
|
+
request.params # This will populate action_dispatch.request.parameters
|
|
69
|
+
handler.call(env).tap do |response|
|
|
70
|
+
send_response(response)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def env
|
|
78
|
+
return @__env if @__env
|
|
79
|
+
fake_input_buffer = StringIO.new(@body)
|
|
80
|
+
fake_error_buffer = StringIO.new
|
|
81
|
+
url_parsed = URI.parse(@url)
|
|
82
|
+
@__env = {
|
|
83
|
+
'rack.input' => Rack::Lint::InputWrapper.new(fake_input_buffer),
|
|
84
|
+
'rack.errors' => Rack::Lint::ErrorWrapper.new(fake_error_buffer),
|
|
85
|
+
'REQUEST_METHOD' => @parser.http_method,
|
|
86
|
+
'REQUEST_PATH' => url_parsed.path,
|
|
87
|
+
'PATH_INFO' => url_parsed.path,
|
|
88
|
+
'QUERY_STRING' => url_parsed.query || '',
|
|
89
|
+
'REQUEST_URI' => url_parsed.path + (url_parsed.query || ''),
|
|
90
|
+
'rack.url_scheme' => url_parsed.scheme,
|
|
91
|
+
'CONTENT_LENGTH' => @body.length,
|
|
92
|
+
'SERVER_NAME' => url_parsed.host,
|
|
93
|
+
'SERVER_PORT' => url_parsed.port
|
|
94
|
+
|
|
95
|
+
}
|
|
96
|
+
@headers.merge(@original_headers || {}).each do |name, value|
|
|
97
|
+
converted_name = "HTTP_#{name.gsub(/-/, '_').upcase}"
|
|
98
|
+
@__env[converted_name] = value
|
|
99
|
+
end
|
|
100
|
+
@__env['CONTENT_TYPE'] = @__env.delete('HTTP_CONTENT_TYPE') if @__env.key?('HTTP_CONTENT_TYPE')
|
|
101
|
+
@__env['CONTENT_LENGTH'] = @__env.delete('HTTP_CONTENT_LENGTH') if @__env.key?('HTTP_CONTENT_LENGTH')
|
|
102
|
+
@__env
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def send_response(response)
|
|
106
|
+
res = EM::DelegatedHttpResponse.new(self)
|
|
107
|
+
res.status = response[0]
|
|
108
|
+
res.headers = response[1]
|
|
109
|
+
res.content = response[2]
|
|
110
|
+
res.send_response
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
module AgileProxy
|
|
2
|
+
#
|
|
3
|
+
# An instance of this class represents a route within the system.
|
|
4
|
+
#
|
|
5
|
+
class Route
|
|
6
|
+
attr_accessor :request_method, :pattern, :app, :constraints, :name
|
|
7
|
+
|
|
8
|
+
PATH_INFO = 'PATH_INFO'.freeze
|
|
9
|
+
ROUTE_PARAMS = 'rack.route_params'.freeze
|
|
10
|
+
QUERY_STRING = 'QUERY_STRING'.freeze
|
|
11
|
+
FORM_HASH = 'rack.request.form_hash'.freeze
|
|
12
|
+
REQUEST_METHOD = 'REQUEST_METHOD'.freeze
|
|
13
|
+
HEAD = 'HEAD'.freeze
|
|
14
|
+
GET = 'GET'.freeze
|
|
15
|
+
POST = 'POST'.freeze
|
|
16
|
+
PUT = 'PUT'.freeze
|
|
17
|
+
DELETE = 'DELETE'.freeze
|
|
18
|
+
|
|
19
|
+
DEFAULT_WILDCARD_NAME = :paths
|
|
20
|
+
WILDCARD_PATTERN = /\/\*(.*)/.freeze
|
|
21
|
+
NAMED_SEGMENTS_PATTERN = /\/:([^$\/]+)/.freeze
|
|
22
|
+
NAMED_SEGMENTS_REPLACEMENT_PATTERN = /\/:([^$\/]+)/.freeze
|
|
23
|
+
DOT = '.'.freeze
|
|
24
|
+
|
|
25
|
+
def initialize(request_method, pattern, app, options = {})
|
|
26
|
+
fail ArgumentError, 'pattern cannot be blank' if pattern.to_s.strip.empty?
|
|
27
|
+
fail ArgumentError, 'app must be callable' unless app.respond_to?(:call)
|
|
28
|
+
@request_method = request_method
|
|
29
|
+
@pattern = pattern
|
|
30
|
+
@app = app
|
|
31
|
+
@constraints = options && options[:constraints]
|
|
32
|
+
@name = options && options[:as]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def regexp
|
|
36
|
+
@regexp ||= compile
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def compile
|
|
40
|
+
pattern_match = pattern.match(WILDCARD_PATTERN)
|
|
41
|
+
src = if pattern_match
|
|
42
|
+
@wildcard_name = if pattern_match[1].to_s.strip.empty?
|
|
43
|
+
DEFAULT_WILDCARD_NAME
|
|
44
|
+
else
|
|
45
|
+
pattern_match[1].to_sym
|
|
46
|
+
end
|
|
47
|
+
pattern.gsub(WILDCARD_PATTERN, '(?:/(.*)|)')
|
|
48
|
+
else
|
|
49
|
+
pattern_match = pattern.match(NAMED_SEGMENTS_PATTERN)
|
|
50
|
+
p = if pattern_match
|
|
51
|
+
pattern.gsub(NAMED_SEGMENTS_REPLACEMENT_PATTERN, '/(?<\1>[^.$/]+)')
|
|
52
|
+
else
|
|
53
|
+
pattern
|
|
54
|
+
end
|
|
55
|
+
p + '(?:\.(?<format>.*))?'
|
|
56
|
+
end
|
|
57
|
+
Regexp.new("\\A#{src}\\Z")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def match(env)
|
|
61
|
+
request_method = env[REQUEST_METHOD]
|
|
62
|
+
request_method = GET if request_method == HEAD
|
|
63
|
+
path = env[PATH_INFO]
|
|
64
|
+
qs = env[QUERY_STRING]
|
|
65
|
+
return nil unless request_method == self.request_method
|
|
66
|
+
fail ArgumentError, 'path is required' if path.to_s.strip.empty?
|
|
67
|
+
path_match = path.match(regexp)
|
|
68
|
+
return unless path_match
|
|
69
|
+
params = if @wildcard_name
|
|
70
|
+
{ @wildcard_name => path_match[1].to_s.split('/') }
|
|
71
|
+
else
|
|
72
|
+
Hash[path_match.names.map(&:to_sym).zip(path_match.captures)]
|
|
73
|
+
end
|
|
74
|
+
params.merge!(::Rack::Utils.parse_nested_query(qs).symbolize_keys) unless qs.nil? || qs.empty?
|
|
75
|
+
params.merge! env[FORM_HASH] if env.key? FORM_HASH
|
|
76
|
+
params.delete(:format) if params.key?(:format) && params[:format].nil?
|
|
77
|
+
|
|
78
|
+
params if meets_constraints(params)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def meets_constraints(params)
|
|
82
|
+
if constraints
|
|
83
|
+
constraints.each do |param, constraint|
|
|
84
|
+
unless params.symbolize_keys[param.to_sym].to_s.match(constraint)
|
|
85
|
+
return false
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
true
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def eql?(other)
|
|
93
|
+
other.is_a?(self.class) &&
|
|
94
|
+
other.request_method == request_method &&
|
|
95
|
+
other.pattern == pattern &&
|
|
96
|
+
other.app == app &&
|
|
97
|
+
other.constraints == constraints
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
alias_method :==, :eql?
|
|
101
|
+
|
|
102
|
+
def hash
|
|
103
|
+
request_method.hash ^ pattern.hash ^ app.hash ^ constraints.hash
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
require 'agile_proxy/route'
|
|
2
|
+
|
|
3
|
+
module AgileProxy
|
|
4
|
+
#
|
|
5
|
+
# A rack router used for selecting a 'request spec' from a shortlist
|
|
6
|
+
#
|
|
7
|
+
class Router
|
|
8
|
+
VERSION = '0.4.0'
|
|
9
|
+
|
|
10
|
+
HEAD = 'HEAD'.freeze
|
|
11
|
+
GET = 'GET'.freeze
|
|
12
|
+
POST = 'POST'.freeze
|
|
13
|
+
PUT = 'PUT'.freeze
|
|
14
|
+
DELETE = 'DELETE'.freeze
|
|
15
|
+
REQUEST_METHOD = 'REQUEST_METHOD'.freeze
|
|
16
|
+
PATH_INFO = 'PATH_INFO'.freeze
|
|
17
|
+
ROUTE_PARAMS = 'rack.route_params'.freeze
|
|
18
|
+
QUERY_STRING = 'QUERY_STRING'.freeze
|
|
19
|
+
FORM_HASH = 'rack.request.form_hash'.freeze
|
|
20
|
+
|
|
21
|
+
def initialize(&block)
|
|
22
|
+
@named_routes = {}
|
|
23
|
+
routes(&block)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def [](route_name)
|
|
27
|
+
@named_routes[route_name]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def routes(&block)
|
|
31
|
+
instance_eval(&block) if block
|
|
32
|
+
@routes
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def get(route_spec)
|
|
36
|
+
route(GET, route_spec)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def post(route_spec)
|
|
40
|
+
route(POST, route_spec)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def put(route_spec)
|
|
44
|
+
route(PUT, route_spec)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def delete(route_spec)
|
|
48
|
+
route(DELETE, route_spec)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def route(method, route_spec)
|
|
52
|
+
route = Route.new(
|
|
53
|
+
method,
|
|
54
|
+
route_spec.first.first,
|
|
55
|
+
route_spec.first.last,
|
|
56
|
+
route_spec.reject { |k, _| k == route_spec.first.first }
|
|
57
|
+
)
|
|
58
|
+
@routes ||= []
|
|
59
|
+
@routes << route
|
|
60
|
+
if route_spec && route_spec[:as]
|
|
61
|
+
# Using ||= so the first route with that name will be returned
|
|
62
|
+
@named_routes[route_spec[:as].to_sym] ||= route_spec.first.first
|
|
63
|
+
end
|
|
64
|
+
route
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def call(env)
|
|
68
|
+
app = match(env)
|
|
69
|
+
if app
|
|
70
|
+
app.call(env)
|
|
71
|
+
else
|
|
72
|
+
not_found(env)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def match(env)
|
|
77
|
+
routes.each do |route|
|
|
78
|
+
params = route.match(env)
|
|
79
|
+
if params
|
|
80
|
+
env[ROUTE_PARAMS] = params
|
|
81
|
+
return route.app
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def not_found(env)
|
|
88
|
+
body = "<h1>Not Found</h1><p>No route matches #{env[REQUEST_METHOD]} #{env[PATH_INFO]}</p>"
|
|
89
|
+
[
|
|
90
|
+
404,
|
|
91
|
+
{
|
|
92
|
+
'Content-Type' => 'text/html',
|
|
93
|
+
'Content-Length' => body.length.to_s
|
|
94
|
+
},
|
|
95
|
+
[body]
|
|
96
|
+
]
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|