cap_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.
- data/README.md +194 -0
- data/lib/cap_proxy/client.rb +90 -0
- data/lib/cap_proxy/filter.rb +75 -0
- data/lib/cap_proxy/http_codes.rb +48 -0
- data/lib/cap_proxy/remote_connection.rb +21 -0
- data/lib/cap_proxy/server.rb +49 -0
- data/lib/cap_proxy/testkit.rb +1 -0
- data/lib/cap_proxy/testkit/em_wrapper.rb +45 -0
- data/spec/filters_spec.rb +72 -0
- data/spec/server_spec.rb +80 -0
- data/spec/spec_helper.rb +3 -0
- metadata +138 -0
data/README.md
ADDED
@@ -0,0 +1,194 @@
|
|
1
|
+
# CapProxy
|
2
|
+
|
3
|
+
[](https://travis-ci.org/ayosec/cap_proxy)
|
4
|
+
|
5
|
+
HTTP proxy with the ability to capture requests and generate a fake response. It is *not* intended to use in production environments, but only in integration tests.
|
6
|
+
|
7
|
+
It is tested in Ruby MRI (1.9, 2.0, 2.1), JRuby and Rubinius. Check out the [Travis page](https://travis-ci.org/ayosec/cap_proxy) to see the current status on every platform.
|
8
|
+
|
9
|
+
## Usage
|
10
|
+
|
11
|
+
### Installation
|
12
|
+
|
13
|
+
Install the gem with
|
14
|
+
|
15
|
+
$ gem install cap_proxy
|
16
|
+
|
17
|
+
Or add it to your `Gemfile`
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
group :test do
|
21
|
+
gem "cap_proxy"
|
22
|
+
end
|
23
|
+
```
|
24
|
+
|
25
|
+
Finally, load it into your application
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
require "cap_proxy/server"
|
29
|
+
```
|
30
|
+
|
31
|
+
### Create a proxy instance
|
32
|
+
|
33
|
+
The main class to use the proxy is `CapProxy::Server`. You have to indicate the address where the proxy will be listening, and the address of the HTTP server which receives every non-captured request.
|
34
|
+
|
35
|
+
When the proxy is initialized, you have to call to the `#run!` method to start the server. This method has to be invoked when [EventMachine](http://eventmachine.rubyforge.org/) is running.
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
proxy = CapProxy::Server.new("localhost:1234", "localhost:5678")
|
39
|
+
|
40
|
+
EM.run do
|
41
|
+
proxy.run!
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
You can use a logger with the proxy:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
logger = Logger.new(STDOUT)
|
49
|
+
proxy = CapProxy::Server.new("localhost:1234", "localhost:5678", logger)
|
50
|
+
|
51
|
+
EM.run do
|
52
|
+
proxy.run!
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
Now, every connection to `http://localhost:1234` will be forwarded to `http://localhost:5678`.
|
57
|
+
|
58
|
+
### Testing
|
59
|
+
|
60
|
+
For your convenience, if you are using RSpec you can use `CapProxy::TestWrapper` to wrap every example.
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
require "cap_proxy/testkit"
|
64
|
+
|
65
|
+
# ...
|
66
|
+
|
67
|
+
around :each do |example|
|
68
|
+
CapProxy::TestWrapper.run(example, "localhost:4001", "localhost:3000") do |proxy|
|
69
|
+
@proxy = proxy
|
70
|
+
end
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
74
|
+
`CapProxy::TestWrapper` will initialize EventMachine, creates a proxy running in it, and launch the example in a different thread. When the example is finished, the EventMachine is stopped.
|
75
|
+
|
76
|
+
### Capturing and manipulating requests
|
77
|
+
|
78
|
+
With `#capture` you can capture any request, and generate a dummy response for it. You have to define a filter, and a block to generate the response.
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
@proxy.capture(method: "get", path: "/users") do |client, request|
|
82
|
+
client.respond 404, {}, "Dummy response"
|
83
|
+
end
|
84
|
+
```
|
85
|
+
|
86
|
+
Details about how to capture requests, and to generate a response for it, are in the section *Capturing requests*.
|
87
|
+
|
88
|
+
## Capturing requests
|
89
|
+
|
90
|
+
The first step is to define a filter. If a request matches multiple filters, it will be managed by the oldest one. If no filter matches the request, it will be forwarded to the default HTTP server.
|
91
|
+
|
92
|
+
### Defining a filter
|
93
|
+
|
94
|
+
The easiest way to define a filter is with a hash, which can contain any of the following items:
|
95
|
+
|
96
|
+
* `:method`
|
97
|
+
* `:path`
|
98
|
+
* `:headers`
|
99
|
+
|
100
|
+
`:method` can accept any string to represent a HTTP method (`"get"` and `"GET"` are equivalent).
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
@proxy.capture(method: "get") { ... }
|
104
|
+
```
|
105
|
+
|
106
|
+
`:path` can be defined with a string (full URI has to match), or with a regular expression (matched with the `=~` operator).
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
@proxy.capture(path: "/users") { ... }
|
110
|
+
|
111
|
+
@proxy.capture(method: "post", path: %r{/users/\d+}) { ... }
|
112
|
+
```
|
113
|
+
|
114
|
+
`:headers` expects a hash with the headers to be matched. The value of every header can be a string or a regular expression.
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
@proxy.capture(path: "/", headers: { "content-type" => /json/ }) do
|
118
|
+
...
|
119
|
+
end
|
120
|
+
```
|
121
|
+
|
122
|
+
### Custom filters
|
123
|
+
|
124
|
+
If you need more control to filter the request, you can create your own filter
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
class MyFilter < CapProxy::Filter
|
128
|
+
|
129
|
+
def apply?(request)
|
130
|
+
# Conditions
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
```
|
135
|
+
|
136
|
+
The `#apply?` method receives an instance of `Http::Parser`, from the [http_parser.rb gem](https://github.com/tmm1/http_parser.rb). You can use methods like `http_method`, `request_url` or `headers` to query info about the request.
|
137
|
+
|
138
|
+
If `#apply?` returns `false` or `nil`, the filter will skip this request.
|
139
|
+
|
140
|
+
To use your filter, create an instance of it:
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
@proxy.capture(MyFilter.new) { ... }
|
144
|
+
```
|
145
|
+
|
146
|
+
### Generating responses
|
147
|
+
|
148
|
+
The block of the `capture` method is invoked with two arguments: the first one is an instance of `CapProxy::Client`. The last one is the instance of `HTTP::Parser`.
|
149
|
+
|
150
|
+
The simplest way to generate a response is calling to `client.respond(status, headers, body)`.
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
@proxy.capture(path: "/users", method: "post") do |client, request|
|
154
|
+
client.respond 201, {"Content-Type" => "application/json"}, %q[{"id": 123}]
|
155
|
+
end
|
156
|
+
```
|
157
|
+
|
158
|
+
### Chunked responses
|
159
|
+
|
160
|
+
You can generate a response in [multiple chunks](http://en.wikipedia.org/wiki/Chunked_transfer_encoding).
|
161
|
+
|
162
|
+
First, you have to call to `client.chunks_start(status, headers)`, to initialize the chunked response. Then, for every chunk, call to `client.chunks_send(data)`. Finally, finish the connection with `client.chunks_finish`
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
@proxy.capture(path: "/chunks") do |client, request|
|
166
|
+
client.chunks_start 200, "content-type" => "text/plain"
|
167
|
+
EM.add_timer(0.4) { client.chunks_send("Cap") }
|
168
|
+
EM.add_timer(0.8) { client.chunks_send("Proxy\n") }
|
169
|
+
EM.add_timer(1.2) { client.chunks_finish }
|
170
|
+
end
|
171
|
+
```
|
172
|
+
|
173
|
+
## Example
|
174
|
+
|
175
|
+
```ruby
|
176
|
+
require "cap_proxy/server"
|
177
|
+
require "cap_proxy/testkit"
|
178
|
+
|
179
|
+
describe MyApp do
|
180
|
+
|
181
|
+
around :each do |test|
|
182
|
+
CapProxy::TestWrapper.run(test, "localhost:4001", "localhost:3000") do |proxy|
|
183
|
+
@proxy = proxy
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
it "should do it" do
|
188
|
+
@proxy.capture(method: "get", path: "/users") do |client, request|
|
189
|
+
request.request_url.should include("foo_id")
|
190
|
+
client.respond 404, {}, "Dummy response"
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
```
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require "eventmachine"
|
2
|
+
require "http/parser"
|
3
|
+
require_relative "remote_connection"
|
4
|
+
require_relative "http_codes"
|
5
|
+
|
6
|
+
module CapProxy
|
7
|
+
class Client < EM::Connection
|
8
|
+
attr_reader :server, :head
|
9
|
+
|
10
|
+
def initialize(server)
|
11
|
+
@server = server
|
12
|
+
@remote = nil
|
13
|
+
@data = nil
|
14
|
+
@http_parser = HTTP::Parser.new
|
15
|
+
|
16
|
+
@http_parser.on_headers_complete = proc do
|
17
|
+
verify_headers!
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def unbind
|
22
|
+
if @remote
|
23
|
+
@remote.close_connection_after_writing
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def write_head(status, headers)
|
28
|
+
head = [ "HTTP/1.1 #{status} #{HTTPCodes[status]}\r\n" ]
|
29
|
+
|
30
|
+
if headers
|
31
|
+
headers.each_pair do |key, value|
|
32
|
+
head << "#{key}: #{value}\r\n"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
head << "\r\n"
|
37
|
+
send_data head.join
|
38
|
+
end
|
39
|
+
|
40
|
+
def respond(status, headers, body = nil)
|
41
|
+
write_head(status, headers)
|
42
|
+
send_data(body) if body
|
43
|
+
close_connection_after_writing
|
44
|
+
end
|
45
|
+
|
46
|
+
def verify_headers!
|
47
|
+
parser = @http_parser
|
48
|
+
filter = server.filters.find {|f| f[:filter].apply?(parser) }
|
49
|
+
|
50
|
+
server.log.info "#{parser.http_method} #{parser.request_url} - #{filter ? "filtered" : "raw"}" if server.log
|
51
|
+
if filter
|
52
|
+
filter[:handler].call self, parser
|
53
|
+
else
|
54
|
+
@remote = EM.connect(server.target_host, server.target_port, RemoteConnection, self)
|
55
|
+
@remote.send_data @data
|
56
|
+
@data = nil
|
57
|
+
@http_parser = nil
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def receive_data(data)
|
62
|
+
if @remote
|
63
|
+
@remote.send_data data
|
64
|
+
else
|
65
|
+
if @data
|
66
|
+
@data << data
|
67
|
+
else
|
68
|
+
@data = data
|
69
|
+
end
|
70
|
+
@http_parser << data
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def chunks_start(status, headers = {})
|
75
|
+
write_head(status, headers.merge("Transfer-Encoding" => "chunked"))
|
76
|
+
end
|
77
|
+
|
78
|
+
def chunks_send(data)
|
79
|
+
send_data "#{data.bytesize.to_s(16)}\r\n"
|
80
|
+
send_data data
|
81
|
+
send_data "\r\n"
|
82
|
+
end
|
83
|
+
|
84
|
+
def chunks_finish
|
85
|
+
send_data "0\r\n\r\n"
|
86
|
+
close_connection_after_writing
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module CapProxy
|
2
|
+
|
3
|
+
class InvalidFilterParam < RuntimeError; end
|
4
|
+
class InvalidRule < RuntimeError; end
|
5
|
+
|
6
|
+
class Filter
|
7
|
+
def self.from_hash(hash)
|
8
|
+
RulesEngine.new(
|
9
|
+
hash.each_pair.map do |param, value|
|
10
|
+
case param
|
11
|
+
when :method
|
12
|
+
[ [ :method, value.upcase ] ]
|
13
|
+
when :path
|
14
|
+
case value
|
15
|
+
when Regexp
|
16
|
+
[ [ :path_regexp, value ] ]
|
17
|
+
when String
|
18
|
+
[ [ :path, value ] ]
|
19
|
+
else
|
20
|
+
raise InvalidFilterParam.new("Invalid value #{value.inspect} for :path")
|
21
|
+
end
|
22
|
+
when :headers
|
23
|
+
value.each_pair.map do |header, value|
|
24
|
+
case value
|
25
|
+
when String
|
26
|
+
[ :header, header.downcase, value ]
|
27
|
+
when Regexp
|
28
|
+
[ :header_regexp, header.downcase, value ]
|
29
|
+
else
|
30
|
+
raise InvalidRule.new("Invalid header rule #{value.inspect}")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
else
|
34
|
+
raise InvalidFilterParam.new("Invalid item #{param.inspect}")
|
35
|
+
end
|
36
|
+
end.inject {|a, b| a.concat(b)}
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
def apply?(request)
|
41
|
+
raise NotImplementedError.new("apply? has to be implemented by inherited classes")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class RulesEngine < Filter
|
46
|
+
def initialize(rules)
|
47
|
+
raise InvalidRule.new("At least one rule is required") if rules.empty?
|
48
|
+
@rules = rules
|
49
|
+
end
|
50
|
+
|
51
|
+
def apply?(request)
|
52
|
+
@rules.all? do |rule|
|
53
|
+
case rule[0]
|
54
|
+
when :method
|
55
|
+
rule[1] == request.http_method
|
56
|
+
when :path_regexp
|
57
|
+
request.request_url =~ rule[1]
|
58
|
+
when :path
|
59
|
+
request.request_url == rule[1]
|
60
|
+
when :header_regexp
|
61
|
+
request.headers.each_pair.any? do |rh, rv|
|
62
|
+
rh.downcase == rule[1] && rv =~ rule[2]
|
63
|
+
end
|
64
|
+
when :header
|
65
|
+
request.headers.each_pair.any? do |rh, rv|
|
66
|
+
rh.downcase == rule[1] && rv == rule[2]
|
67
|
+
end
|
68
|
+
else
|
69
|
+
raise InvalidRule.new("Invalid rule #{rule.inspect}")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module CapProxy
|
2
|
+
|
3
|
+
HTTPCodes = {
|
4
|
+
|
5
|
+
100 => "Continue",
|
6
|
+
101 => "Switch Protocol",
|
7
|
+
200 => "OK",
|
8
|
+
201 => "Created",
|
9
|
+
202 => "Accepted",
|
10
|
+
203 => "Non Authoritative Information",
|
11
|
+
204 => "No Content",
|
12
|
+
205 => "Reset Content",
|
13
|
+
206 => "Partial Content",
|
14
|
+
300 => "Multiple Choice",
|
15
|
+
301 => "Moved Permanently",
|
16
|
+
302 => "Found",
|
17
|
+
303 => "See Other",
|
18
|
+
304 => "Not Modified",
|
19
|
+
305 => "Use Proxy",
|
20
|
+
307 => "Temporary Redirect",
|
21
|
+
400 => "Bad Request",
|
22
|
+
401 => "Unauthorized",
|
23
|
+
402 => "Payment Required",
|
24
|
+
403 => "Forbidden",
|
25
|
+
404 => "Not Found",
|
26
|
+
405 => "Method Not Allowed",
|
27
|
+
406 => "Not Acceptable",
|
28
|
+
407 => "Proxy Authentication Required",
|
29
|
+
408 => "Request Time Out",
|
30
|
+
409 => "Conflict",
|
31
|
+
410 => "Gone",
|
32
|
+
411 => "Length Required",
|
33
|
+
412 => "Precondition Failed",
|
34
|
+
413 => "Request Entity Too Large",
|
35
|
+
414 => "Request URIToo Long",
|
36
|
+
415 => "Unsupported Media Type",
|
37
|
+
416 => "Requested Range Not Satisfiable",
|
38
|
+
417 => "Expectation Failed",
|
39
|
+
500 => "Internal Server Error",
|
40
|
+
501 => "Not Implemented",
|
41
|
+
502 => "Bad Gateway",
|
42
|
+
503 => "Service Unavailable",
|
43
|
+
504 => "Gateway Time Out",
|
44
|
+
505 => "Version Not Supported"
|
45
|
+
|
46
|
+
}
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require "eventmachine"
|
2
|
+
|
3
|
+
module CapProxy
|
4
|
+
class RemoteConnection < EM::Connection
|
5
|
+
attr_reader :proxy_connection
|
6
|
+
|
7
|
+
def initialize(proxy_connection)
|
8
|
+
@proxy_connection = proxy_connection
|
9
|
+
end
|
10
|
+
|
11
|
+
def receive_data(data)
|
12
|
+
log = proxy_connection.server.log
|
13
|
+
log.debug("Closing #{proxy_connection.head}") if log
|
14
|
+
proxy_connection.send_data data
|
15
|
+
end
|
16
|
+
|
17
|
+
def unbind
|
18
|
+
proxy_connection.close_connection_after_writing
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require "eventmachine"
|
2
|
+
require "thread_safe"
|
3
|
+
require_relative "client"
|
4
|
+
require_relative "filter"
|
5
|
+
|
6
|
+
module CapProxy
|
7
|
+
class Server
|
8
|
+
|
9
|
+
attr_reader :proxy_port, :proxy_host, :target_host, :target_port, :log, :filters
|
10
|
+
|
11
|
+
def initialize(bind_address, target, log = nil)
|
12
|
+
|
13
|
+
proxy_host, proxy_port = bind_address.split(":", 2)
|
14
|
+
target_host, target_port = target.split(":", 2)
|
15
|
+
|
16
|
+
@log = log
|
17
|
+
@proxy_port = proxy_port
|
18
|
+
@proxy_host = proxy_host
|
19
|
+
@target_host = target_host
|
20
|
+
@target_port = target_port
|
21
|
+
reset_filters!
|
22
|
+
end
|
23
|
+
|
24
|
+
def reset_filters!
|
25
|
+
@filters = ThreadSafe::Array.new
|
26
|
+
end
|
27
|
+
|
28
|
+
def capture(filter_param, &handler)
|
29
|
+
filter =
|
30
|
+
case filter_param
|
31
|
+
when Hash
|
32
|
+
Filter.from_hash(filter_param)
|
33
|
+
when Filter
|
34
|
+
filter_param
|
35
|
+
else
|
36
|
+
raise RuntimeError("#capture requires a hash or a Filter object")
|
37
|
+
end
|
38
|
+
|
39
|
+
@filters.push filter: filter, handler: handler
|
40
|
+
filter
|
41
|
+
end
|
42
|
+
|
43
|
+
def run!
|
44
|
+
log.info "CapProxy: Listening on #{proxy_host}:#{proxy_port}" if log
|
45
|
+
EM.start_server proxy_host, proxy_port, Client, self
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative "testkit/em_wrapper"
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require "eventmachine"
|
2
|
+
require "uri"
|
3
|
+
require "logger"
|
4
|
+
|
5
|
+
module CapProxy
|
6
|
+
|
7
|
+
module TestWrapper
|
8
|
+
|
9
|
+
class SimpleResponder < EM::Connection
|
10
|
+
def receive_data(_)
|
11
|
+
send_data "TCPServer\n"
|
12
|
+
close_connection_after_writing
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.run(test, bind_address, target)
|
17
|
+
|
18
|
+
EM.run do
|
19
|
+
|
20
|
+
proxy = nil
|
21
|
+
|
22
|
+
EM.error_handler do |error|
|
23
|
+
STDERR.puts error
|
24
|
+
STDERR.puts error.backtrace.map {|l| "\t#{l}" }
|
25
|
+
end
|
26
|
+
|
27
|
+
logger = Logger.new(STDERR)
|
28
|
+
logger.level = Logger::ERROR
|
29
|
+
proxy = Server.new(bind_address, target, logger)
|
30
|
+
proxy.run!
|
31
|
+
|
32
|
+
if block_given?
|
33
|
+
yield proxy
|
34
|
+
end
|
35
|
+
|
36
|
+
Thread.new do
|
37
|
+
test.run
|
38
|
+
end
|
39
|
+
EM.stop_event_loop
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe CapProxy::Filter do
|
4
|
+
|
5
|
+
context "Filters from hashes" do
|
6
|
+
|
7
|
+
def hash_filter?(hash, request)
|
8
|
+
parser = HTTP::Parser.new
|
9
|
+
parser << request
|
10
|
+
|
11
|
+
filter = CapProxy::Filter.from_hash(hash)
|
12
|
+
filter.apply?(parser)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "filter by method and path" do
|
16
|
+
hash_filter?(
|
17
|
+
{method: "get", path: "/foo/bar"},
|
18
|
+
%[GET /foo/bar HTTP/1.0\r\n\r\n]
|
19
|
+
).should be_true
|
20
|
+
|
21
|
+
hash_filter?(
|
22
|
+
{method: "post", path: "/foo/bar"},
|
23
|
+
%[GET /foo/bar HTTP/1.0\r\n\r\n]
|
24
|
+
).should be_false
|
25
|
+
|
26
|
+
hash_filter?(
|
27
|
+
{method: "post", path: %r{/foo/bar/(\d+)}},
|
28
|
+
%[GET /foo/bar/100 HTTP/1.0\r\n\r\n]
|
29
|
+
).should be_false
|
30
|
+
end
|
31
|
+
|
32
|
+
it "filter by path and headers" do
|
33
|
+
|
34
|
+
hash_filter?(
|
35
|
+
{
|
36
|
+
path: "/",
|
37
|
+
headers: {
|
38
|
+
"content-type" => /json/
|
39
|
+
}
|
40
|
+
},
|
41
|
+
%[GET / HTTP/1.0\r\nContent-Type: application/json\r\n\r\n]
|
42
|
+
).should be_true
|
43
|
+
|
44
|
+
hash_filter?(
|
45
|
+
{
|
46
|
+
path: "/",
|
47
|
+
headers: {
|
48
|
+
"content-type" => /json/,
|
49
|
+
"user-agent" => "none"
|
50
|
+
}
|
51
|
+
},
|
52
|
+
%[GET / HTTP/1.0\r\nContent-Type: application/json\r\n\r\n]
|
53
|
+
).should be_false
|
54
|
+
|
55
|
+
hash_filter?(
|
56
|
+
{
|
57
|
+
path: "/",
|
58
|
+
headers: {
|
59
|
+
"user-agent" => "none",
|
60
|
+
"Accept" => "*"
|
61
|
+
}
|
62
|
+
},
|
63
|
+
%[GET / HTTP/1.0\r\n] +
|
64
|
+
%[User-Agent: none\r\n] +
|
65
|
+
%[accept: *\r\n] +
|
66
|
+
%[\r\n]
|
67
|
+
).should be_true
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
data/spec/server_spec.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
require "net/http"
|
5
|
+
|
6
|
+
describe CapProxy::Server do
|
7
|
+
|
8
|
+
around :each do |test|
|
9
|
+
CapProxy::TestWrapper.run(test, "localhost:50300", "localhost:50301") do |proxy|
|
10
|
+
EM.start_server "localhost", 50301, CapProxy::TestWrapper::SimpleResponder
|
11
|
+
@proxy = proxy
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def proxy_req!(packet)
|
16
|
+
conn = TCPSocket.new("localhost", 50300)
|
17
|
+
conn.write(packet)
|
18
|
+
response = conn.read(512)
|
19
|
+
conn.close
|
20
|
+
response
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should proxy basic HTTP requests" do
|
24
|
+
proxy_req!("GET / HTTP/1.0\r\n\r\n").should == "TCPServer\n"
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should capture requests" do
|
28
|
+
@proxy.capture(method: "get", path: /x/) do |client, request|
|
29
|
+
request.request_url.should include("x")
|
30
|
+
client.respond 404, {}, "foobar"
|
31
|
+
end
|
32
|
+
|
33
|
+
proxy_req!("GET /a/x/ HTTP/1.0\r\n\r\n").should =~ %r[\AHTTP/1.1 404 Not Found\r\n.*foobar\Z]m
|
34
|
+
proxy_req!("POST /a/x/ HTTP/1.0\r\n\r\n").should == "TCPServer\n"
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should respond with custom headers" do
|
38
|
+
@proxy.capture(method: "put") do |client, request|
|
39
|
+
request.http_method.should == "PUT"
|
40
|
+
client.respond 200, {"x-foo" => "that"}, "."
|
41
|
+
end
|
42
|
+
|
43
|
+
resp = proxy_req!("PUT / HTTP/1.0\r\n\r\n")
|
44
|
+
resp.should =~ %r[\AHTTP/1.1 200 OK\r\n]
|
45
|
+
resp.should include("x-foo: that\r\n")
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should use a custom filter" do
|
49
|
+
class CaptureAll < CapProxy::Filter
|
50
|
+
def apply?(request)
|
51
|
+
true
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
@proxy.capture(CaptureAll.new) do |client, request|
|
56
|
+
client.respond 200, {}, "-captured-"
|
57
|
+
end
|
58
|
+
|
59
|
+
proxy_req!("PUT / HTTP/1.0\r\n\r\n").should include("-captured-")
|
60
|
+
proxy_req!("GET /foo HTTP/1.0\r\n\r\n").should include("-captured-")
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should manage chunked responsed" do
|
64
|
+
@proxy.capture(method: "get") do |client, request|
|
65
|
+
client.chunks_start 201, "content-type" => "text/foo"
|
66
|
+
EM.add_timer(0.1) { client.chunks_send([-30, -100, -108, 97].pack("c*")) }
|
67
|
+
EM.add_timer(0.3) { client.chunks_send("abc") }
|
68
|
+
EM.add_timer(0.5) { client.chunks_finish }
|
69
|
+
end
|
70
|
+
|
71
|
+
start_time = Time.now.to_f
|
72
|
+
resp = Net::HTTP.get_response(URI("http://localhost:50300"))
|
73
|
+
resp.code.should == "201"
|
74
|
+
resp["Content-Type"].should == "text/foo"
|
75
|
+
resp.body.bytes.to_a.should == "✔aabc".bytes.to_a
|
76
|
+
|
77
|
+
(Time.now.to_f - start_time).should >= 0.5
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cap_proxy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ayose Cazorla
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-04-23 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: eventmachine
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.0.3
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.0.3
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: http_parser.rb
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: thread_safe
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rake
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 10.3.0
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 10.3.0
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: rspec
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
description: HTTP proxy with the ability to capture requests and generate a fake response.
|
95
|
+
It is *not* intended to use in production environments, but only in integration
|
96
|
+
tests.
|
97
|
+
email: ayosec@gmail.com
|
98
|
+
executables: []
|
99
|
+
extensions: []
|
100
|
+
extra_rdoc_files: []
|
101
|
+
files:
|
102
|
+
- README.md
|
103
|
+
- lib/cap_proxy/client.rb
|
104
|
+
- lib/cap_proxy/filter.rb
|
105
|
+
- lib/cap_proxy/http_codes.rb
|
106
|
+
- lib/cap_proxy/remote_connection.rb
|
107
|
+
- lib/cap_proxy/server.rb
|
108
|
+
- lib/cap_proxy/testkit.rb
|
109
|
+
- lib/cap_proxy/testkit/em_wrapper.rb
|
110
|
+
- spec/filters_spec.rb
|
111
|
+
- spec/server_spec.rb
|
112
|
+
- spec/spec_helper.rb
|
113
|
+
homepage: https://github.com/ayosec/cap_proxy
|
114
|
+
licenses:
|
115
|
+
- MIT
|
116
|
+
post_install_message:
|
117
|
+
rdoc_options: []
|
118
|
+
require_paths:
|
119
|
+
- lib
|
120
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
127
|
+
none: false
|
128
|
+
requirements:
|
129
|
+
- - ! '>='
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
requirements: []
|
133
|
+
rubyforge_project:
|
134
|
+
rubygems_version: 1.8.23
|
135
|
+
signing_key:
|
136
|
+
specification_version: 3
|
137
|
+
summary: HTTP Proxy with ability to capture and manipulate requests.
|
138
|
+
test_files: []
|