cap_proxy 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/ayosec/cap_proxy.svg)](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: []
|