mallory 0.0.1 → 0.0.2
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/.gitignore +2 -0
- data/.rspec +3 -0
- data/.travis.yml +10 -0
- data/README.md +41 -9
- data/Rakefile +7 -0
- data/bin/mallory +65 -0
- data/keys/keygen.sh +6 -0
- data/lib/mallory/backend/file.rb +34 -0
- data/lib/mallory/backend/redis.rb +18 -0
- data/lib/mallory/backend/self.rb +18 -0
- data/lib/mallory/configuration.rb +60 -0
- data/lib/mallory/connection.rb +60 -0
- data/lib/mallory/proxy.rb +90 -0
- data/lib/mallory/proxy_builder.rb +10 -0
- data/lib/mallory/request.rb +45 -0
- data/lib/mallory/request_builder.rb +11 -0
- data/lib/mallory/response.rb +39 -0
- data/lib/mallory/response_builder.rb +9 -0
- data/lib/mallory/server.rb +29 -0
- data/lib/mallory/version.rb +2 -4
- data/lib/mallory.rb +13 -0
- data/mallory.gemspec +4 -1
- data/spec/mallory/configuration_spec.rb +21 -0
- data/spec/mallory/connection_spec.rb +11 -0
- data/spec/mallory/file_backend_spec.rb +33 -0
- data/spec/mallory/proxy_spec.rb +18 -0
- data/spec/mallory/request_spec.rb +51 -0
- data/spec/mallory/response_spec.rb +24 -0
- data/spec/mallory.rb +4 -0
- data/spec/responder.rb +43 -0
- data/spec/spec_helper.rb +23 -0
- metadata +82 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2986cd40c0575541258ea30258a3647ba162d6c3
|
4
|
+
data.tar.gz: ffbeddde71442cb1fc00cab05f48f560cd6b2e19
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 930f5970b48a7085b177b21da243ea8a4b598a150fdce183a1769036c7f8ede30d8f84080c230e883fc30d0204f687a434c3d5ac853bb3d97f6f9414357dc042
|
7
|
+
data.tar.gz: 7d5bf8c37b5335631e7b61cce9b0402ca99a4dbb6d33980b5670f32fc8741a0f51f5e4cc16e66698bfbc09fa043a00e24ff9079ff201c03f431c4969810995ba
|
data/.gitignore
CHANGED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,4 +1,9 @@
|
|
1
|
-
|
1
|
+
# mallory
|
2
|
+
|
3
|
+
[](https://rubygems.org/gems/mallory)
|
4
|
+
[](http://travis-ci.org/odcinek/mallory)
|
5
|
+
[](https://gemnasium.com/odcinek/mallory)
|
6
|
+
[](https://codeclimate.com/github/odcinek/mallory)
|
2
7
|
|
3
8
|
Man-in-the-middle http/https transparent http (CONNECT) proxy over bunch of (unreliable) backends.
|
4
9
|
It is intended to be used for running test suits / scrapers. It basically shields the proxied application from low responsiveness / poor reliability of underlying proxies.
|
@@ -7,11 +12,15 @@ Proxy list is provided by external backend (ActiveRecord model, Redis set) and i
|
|
7
12
|
|
8
13
|
For the mallory to work properly client certificate validation needs to be turned off.
|
9
14
|
|
10
|
-
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
### Command line
|
11
18
|
|
12
19
|
```bash
|
13
20
|
./keys/keygen.sh
|
14
|
-
bundle exec ./bin/mallory -v -l 9999
|
21
|
+
bundle exec ./bin/mallory -v -l 9999 #default (no proxy backend, direct requests)
|
22
|
+
bundle exec ./bin/mallory -v -b file://proxies.txt -l 9999 #start with proxy file
|
23
|
+
bundle exec ./bin/mallory -v -b redis://127.0.0.1:6379 -l 9999 #start with Redis backend
|
15
24
|
```
|
16
25
|
|
17
26
|
```bash
|
@@ -19,28 +28,51 @@ curl --insecure --proxy 127.0.0.1:9999 https://www.dropbox.com/login
|
|
19
28
|
phantomjs --debug=yes --ignore-ssl-errors=yes --ssl-protocol=sslv2 --proxy=127.0.0.1:9999 --proxy-type=http hello.js
|
20
29
|
```
|
21
30
|
|
22
|
-
###
|
31
|
+
### Interface
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
mb = Mallory::Backend::File.new('proxies.txt')
|
35
|
+
mp = Mallory::Proxy.new()
|
36
|
+
mp.backend = mb
|
37
|
+
mp.start!
|
38
|
+
```
|
39
|
+
|
40
|
+
### Proxy backends
|
41
|
+
|
42
|
+
#### Self
|
43
|
+
|
44
|
+
Just direct requests, no proxies, default
|
45
|
+
|
46
|
+
#### File
|
47
|
+
|
48
|
+
Text file, one http proxy per line, in ```proxy:port``` format.
|
49
|
+
|
50
|
+
#### Redis
|
51
|
+
|
52
|
+
Redis key TODO
|
53
|
+
|
54
|
+
## What mallory is not
|
23
55
|
- General purpose proxying daemon
|
24
56
|
- General purpose proxy load balancer
|
25
57
|
- Anything general purpose really
|
26
|
-
- For mature general purpose mitm solution (in Python) see [
|
58
|
+
- For mature general purpose mitm solution (in Python) see [mitmproxy](https://github.com/mitmproxy/mitmproxy)
|
27
59
|
|
28
|
-
|
60
|
+
## TODO
|
29
61
|
- CA support
|
30
62
|
- SOCKS5 backends (mixing http and SOCKS5 proxies)
|
31
63
|
- parallel requests
|
32
64
|
- even better response reliability
|
33
65
|
|
34
|
-
|
66
|
+
## Resources
|
35
67
|
|
36
68
|
- [HTTP Connect Tunneling](http://en.wikipedia.org/wiki/HTTP_tunnel#HTTP_CONNECT_Tunneling)
|
37
69
|
- [RFC2817, Upgrading to TLS Within HTTP/1.1](http://www.ietf.org/rfc/rfc2817.txt)
|
38
70
|
|
39
|
-
|
71
|
+
## Contributors
|
40
72
|
|
41
73
|
- [Marcin Sawicki](https://github.com/odcinek)
|
42
74
|
- [Maria Kacik](https://github.com/mkacik)
|
43
75
|
|
44
|
-
|
76
|
+
## License
|
45
77
|
|
46
78
|
(The MIT License)
|
data/Rakefile
ADDED
data/bin/mallory
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
lib = File.expand_path(File.dirname(__FILE__) + '/../lib')
|
4
|
+
$LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
|
5
|
+
|
6
|
+
require 'mallory'
|
7
|
+
require 'optparse'
|
8
|
+
require 'logging'
|
9
|
+
|
10
|
+
#ARGV << '--help' if ARGV.empty?
|
11
|
+
|
12
|
+
options = {}
|
13
|
+
OptionParser.new do |opts|
|
14
|
+
opts.banner = "Usage: proxybalancer [options]"
|
15
|
+
|
16
|
+
opts.on("-l", "--listen PORT", Integer, "Port to listen on (default 9999)") do |v|
|
17
|
+
options[:listen] = v
|
18
|
+
end
|
19
|
+
|
20
|
+
opts.on("-b", "--backend BACKEND", String, "Backend to use (default 'file://proxies.txt')") do |v|
|
21
|
+
options[:listen] = v
|
22
|
+
end
|
23
|
+
|
24
|
+
opts.on("-ct", "--connect-timeout SECONDS", Integer, "Proxy connect timeout (default 2s)") do |v|
|
25
|
+
options[:ct] = v
|
26
|
+
end
|
27
|
+
|
28
|
+
opts.on("-it", "--inactivity-timeout SECONDS", Integer, "Proxy inactivity timeout (default 2s)") do |v|
|
29
|
+
options[:it] = v
|
30
|
+
end
|
31
|
+
|
32
|
+
opts.on("-v", "--verbose", "Run in debug mode") do |v|
|
33
|
+
options[:verbose] = v
|
34
|
+
end
|
35
|
+
end.parse!
|
36
|
+
|
37
|
+
def get_logger(verbose)
|
38
|
+
# https://github.com/TwP/logging/blob/master/lib/logging/layouts/pattern.rb
|
39
|
+
layout = Logging::Layouts::Pattern.new({ :pattern => "%d %-5l : %m\n"})
|
40
|
+
logger = Logging.logger['mallory']
|
41
|
+
file_appender = Logging.appenders.file("mallory.log")
|
42
|
+
file_appender.layout = layout
|
43
|
+
file_appender.level = :info
|
44
|
+
logger.add_appenders(file_appender)
|
45
|
+
stdout_appender = Logging.appenders.stdout
|
46
|
+
stdout_appender.layout = layout
|
47
|
+
stdout_appender.level = verbose ? :debug : :info
|
48
|
+
logger.add_appenders(
|
49
|
+
stdout_appender,
|
50
|
+
file_appender
|
51
|
+
)
|
52
|
+
logger
|
53
|
+
end
|
54
|
+
|
55
|
+
config = Mallory::Configuration.register do |c|
|
56
|
+
c.logger = get_logger(options.delete(:verbose))
|
57
|
+
c.backend = Mallory::Backend::Self.new()
|
58
|
+
#c.backend = Mallory::Backend::File.new("#{Dir.pwd}/proxies.txt")
|
59
|
+
# c.backend = Mallory::Backend::Redis.new("127.0.0.1", 6379)
|
60
|
+
c.connect_timeout = options.delete(:connect_timeout) || 2
|
61
|
+
c.inactivity_timeout = options.delete(:inactivity_timeout) || 2
|
62
|
+
c.listen = options.delete(:listen) || 9999
|
63
|
+
end
|
64
|
+
|
65
|
+
Mallory::Server.new(config).start!
|
data/keys/keygen.sh
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
echo "Self signed dummy cert"
|
3
|
+
set -e
|
4
|
+
openssl genrsa -out ./keys/server.key 1024
|
5
|
+
openssl req -subj '/C=US/ST=CA/L=SF/CN=*.com' -new -key ./keys/server.key -out ./keys/server.csr
|
6
|
+
openssl x509 -req -days 365 -in ./keys/server.csr -signkey ./keys/server.key -out ./keys/server.crt
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Mallory
|
2
|
+
module Backend
|
3
|
+
class File
|
4
|
+
=begin
|
5
|
+
It would be cool to add signal trap to refresh proxy list when file contents change
|
6
|
+
(with initial validation, so if file is malformed, old list stays)
|
7
|
+
=end
|
8
|
+
def initialize(filename)
|
9
|
+
@proxies = []
|
10
|
+
begin
|
11
|
+
lines = ::File.readlines(filename)
|
12
|
+
raise if lines.nil?
|
13
|
+
raise if lines.empty?
|
14
|
+
rescue
|
15
|
+
raise("Proxy file missing or empty")
|
16
|
+
end
|
17
|
+
lines.each do |line|
|
18
|
+
if line.strip.match(/.*:\d{2,6}/)
|
19
|
+
@proxies << line.strip
|
20
|
+
else raise("Wrong format") end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def any
|
25
|
+
@proxies.sample
|
26
|
+
end
|
27
|
+
|
28
|
+
def all
|
29
|
+
@proxies
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'redis'
|
2
|
+
|
3
|
+
module Mallory
|
4
|
+
module Backend
|
5
|
+
class Redis
|
6
|
+
|
7
|
+
def initialize(host, port)
|
8
|
+
redis = ::Redis.new(:host => host, :port => port)
|
9
|
+
@proxies = redis.smembers("good_proxies")
|
10
|
+
end
|
11
|
+
|
12
|
+
def any
|
13
|
+
@proxies.sample
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Mallory
|
2
|
+
class Configuration
|
3
|
+
# defining instance variable at class level
|
4
|
+
# this will ensure that static config is
|
5
|
+
# not shared with objects created by calling
|
6
|
+
# Configuration.new
|
7
|
+
@settings = {}
|
8
|
+
|
9
|
+
def self.register
|
10
|
+
if block_given?
|
11
|
+
yield(self)
|
12
|
+
end
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.logger
|
17
|
+
@settings[:logger]
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.logger=(other)
|
21
|
+
@settings[:logger] = other
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.backend
|
25
|
+
@settings[:backend]
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.backend=(other)
|
29
|
+
@settings[:backend] = other
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.listen
|
33
|
+
@settings[:listen]
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.listen=(other)
|
37
|
+
@settings[:listen] = other
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.connect_timeout
|
41
|
+
@settings[:connect_timeout]
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.connect_timeout=(other)
|
45
|
+
@settings[:connect_timeout] = other
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.inactivity_timeout
|
49
|
+
@settings[:inactivity_timeout]
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.inactivity_timeout=(other)
|
53
|
+
@settings[:inactivity_timeout] = other
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.reset!
|
57
|
+
@settings = {}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'em-http-request'
|
3
|
+
require 'redis'
|
4
|
+
|
5
|
+
|
6
|
+
module Mallory
|
7
|
+
class Connection < EM::Connection
|
8
|
+
def initialize(request_builder, proxy_builder, logger)
|
9
|
+
@logger = logger
|
10
|
+
@request_builder = request_builder
|
11
|
+
@proxy_builder = proxy_builder
|
12
|
+
@start = Time.now
|
13
|
+
@secure = false
|
14
|
+
@proto = "http"
|
15
|
+
end
|
16
|
+
|
17
|
+
def ssl_handshake_completed # EM::Connection
|
18
|
+
@logger.debug "Secure connection intercepted"
|
19
|
+
@secure = true
|
20
|
+
end
|
21
|
+
|
22
|
+
def post_init # EM::Connection
|
23
|
+
@logger.debug "Start connection"
|
24
|
+
end
|
25
|
+
|
26
|
+
def unbind(reason=nil) # EM::Connection
|
27
|
+
@logger.debug "Close connection #{reason}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def error
|
31
|
+
@logger.info "Failure in #{Time.now-@start}s"
|
32
|
+
send_data "HTTP/1.1 500 Internal Server Error\nContent-Type: text/html\nConnection: close\n\n"
|
33
|
+
close_connection_after_writing
|
34
|
+
end
|
35
|
+
|
36
|
+
def receive_data(data) # EM::Connection
|
37
|
+
begin
|
38
|
+
request = @request_builder.build(data)
|
39
|
+
rescue
|
40
|
+
error
|
41
|
+
return
|
42
|
+
end
|
43
|
+
if not @secure and request.method.eql?('connect')
|
44
|
+
send_data "HTTP/1.0 200 Connection established\r\n\r\n"
|
45
|
+
start_tls :private_key_file => './keys/server.key', :cert_chain_file => './keys/server.crt', :verify_peer => false
|
46
|
+
return true
|
47
|
+
end
|
48
|
+
proxy = @proxy_builder.build
|
49
|
+
proxy.callback {
|
50
|
+
send_data proxy.response
|
51
|
+
close_connection_after_writing
|
52
|
+
}
|
53
|
+
proxy.errback {
|
54
|
+
error
|
55
|
+
}
|
56
|
+
request.protocol = 'https' if @secure
|
57
|
+
proxy.perform(request)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'em-http-request'
|
3
|
+
|
4
|
+
module Mallory
|
5
|
+
class Proxy
|
6
|
+
MAX_ATTEMPTS = 10
|
7
|
+
|
8
|
+
include EventMachine::Deferrable
|
9
|
+
|
10
|
+
def initialize(ct, it, backend, response_builder, logger)
|
11
|
+
@connect_timeout = ct
|
12
|
+
@inactivity_timeout = it
|
13
|
+
@backend = backend
|
14
|
+
@response_builder = response_builder
|
15
|
+
@logger = logger
|
16
|
+
@retries = 0
|
17
|
+
@response = ''
|
18
|
+
end
|
19
|
+
|
20
|
+
def resubmit
|
21
|
+
@proxy = @backend.any
|
22
|
+
submit
|
23
|
+
end
|
24
|
+
|
25
|
+
def perform request
|
26
|
+
@method = request.method.to_s
|
27
|
+
@uri = request.uri
|
28
|
+
@request_headers = request.headers
|
29
|
+
@body = request.body || ''
|
30
|
+
resubmit
|
31
|
+
end
|
32
|
+
|
33
|
+
def send_data data
|
34
|
+
@response << data
|
35
|
+
end
|
36
|
+
|
37
|
+
def response
|
38
|
+
@response
|
39
|
+
end
|
40
|
+
|
41
|
+
def options
|
42
|
+
options = {
|
43
|
+
:connect_timeout => @connect_timeout,
|
44
|
+
:inactivity_timeout => @inactivity_timeout,
|
45
|
+
}
|
46
|
+
if not @proxy.nil?
|
47
|
+
options[:proxy] = {
|
48
|
+
:host => @proxy.split(':')[0],
|
49
|
+
:port => @proxy.split(':')[1]
|
50
|
+
}
|
51
|
+
end
|
52
|
+
return options
|
53
|
+
end
|
54
|
+
|
55
|
+
def submit
|
56
|
+
@retries+=1
|
57
|
+
if @retries > MAX_ATTEMPTS
|
58
|
+
fail
|
59
|
+
return
|
60
|
+
end
|
61
|
+
@logger.debug "Attempt #{@retries} - #{@method.upcase} #{@uri} via #{@proxy}"
|
62
|
+
if [:post, :put].include?(@method)
|
63
|
+
request_params = {:head => @headers, :body => @body}
|
64
|
+
else
|
65
|
+
request_params = {:head => @headers}
|
66
|
+
end
|
67
|
+
http = EventMachine::HttpRequest.new(@uri, options).send(@method, request_params)
|
68
|
+
http.errback {
|
69
|
+
@logger.debug "Attempt #{@retries} - Failed"
|
70
|
+
resubmit
|
71
|
+
}
|
72
|
+
http.callback {
|
73
|
+
@logger.debug "Attempt #{@retries} - Success"
|
74
|
+
response = @response_builder.build(http)
|
75
|
+
if response.status > 400
|
76
|
+
@logger.debug "#{response.status} > 400"
|
77
|
+
resubmit
|
78
|
+
else
|
79
|
+
send_data "HTTP/1.1 #{response.status} #{response.description}\n"
|
80
|
+
send_data response.headers
|
81
|
+
send_data "\r\n\r\n"
|
82
|
+
send_data response.body
|
83
|
+
@logger.debug "Send content #{response.body.length} bytes"
|
84
|
+
@logger.info "Success (#{Time.now-Time.now}s, #{@retries} attempts)"
|
85
|
+
end
|
86
|
+
self.succeed
|
87
|
+
}
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
class Mallory::ProxyBuilder
|
2
|
+
def initialize(config, response_builder)
|
3
|
+
@config = config
|
4
|
+
@response_builder = response_builder
|
5
|
+
end
|
6
|
+
|
7
|
+
def build
|
8
|
+
Mallory::Proxy.new(@config.connect_timeout, @config.inactivity_timeout, @config.backend, @response_builder, @config.logger)
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'webrick'
|
2
|
+
|
3
|
+
module Mallory
|
4
|
+
class Request
|
5
|
+
|
6
|
+
attr_accessor :protocol
|
7
|
+
|
8
|
+
def initialize(data, logger)
|
9
|
+
@logger = logger
|
10
|
+
line = data.match(/([A-Z]{3,8})\s(?:(http\w*):\/\/)*(?:(\w*):*(\d{2,5})*)(\/{0,1}.*)\sHTTP/)
|
11
|
+
method = line[1]
|
12
|
+
@protocol = "http"
|
13
|
+
host = line[3] || data.match(/Host:\s(.*)\n/)[1]
|
14
|
+
port = line[4]
|
15
|
+
path = line[5]
|
16
|
+
data.sub!("#{method} #{line[3]}:#{port}", "#{method} #{line[3]}/")
|
17
|
+
data.sub!("Host: #{line[3]}:#{port}", "Host: #{line[3]}")
|
18
|
+
@request = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
|
19
|
+
@request.parse(StringIO.new(data))
|
20
|
+
rescue WEBrick::HTTPStatus::BadRequest
|
21
|
+
raise
|
22
|
+
end
|
23
|
+
|
24
|
+
def uri
|
25
|
+
"#{@protocol}://#{@request['host']}#{@request.path}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def method
|
29
|
+
@request.request_method.downcase
|
30
|
+
end
|
31
|
+
|
32
|
+
def headers
|
33
|
+
headers = {}
|
34
|
+
@request.each { |head| headers[head] = @request[head] }
|
35
|
+
headers
|
36
|
+
end
|
37
|
+
|
38
|
+
def body
|
39
|
+
@request.body
|
40
|
+
rescue WEBrick::HTTPStatus::LengthRequired
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Mallory
|
2
|
+
class Response
|
3
|
+
|
4
|
+
def initialize http, logger
|
5
|
+
@logger = logger #TODO: unsure if logger is needed in plain data structure
|
6
|
+
@http = http
|
7
|
+
end
|
8
|
+
|
9
|
+
def status
|
10
|
+
@http.response_header.status
|
11
|
+
end
|
12
|
+
|
13
|
+
def description
|
14
|
+
@http.response_header.http_reason
|
15
|
+
end
|
16
|
+
|
17
|
+
def body
|
18
|
+
@http.response
|
19
|
+
end
|
20
|
+
|
21
|
+
def headers
|
22
|
+
headers = []
|
23
|
+
@http.response_header.each do |header|
|
24
|
+
next if header[0].match(/^X_|^VARY|^VIA|^SERVER|^TRANSFER_ENCODING|^CONNECTION/)
|
25
|
+
header_name = "#{header[0].downcase.capitalize.gsub('_','-')}"
|
26
|
+
case header[1]
|
27
|
+
when Array
|
28
|
+
header[1].each do |header_value|
|
29
|
+
headers << "#{header_name}: #{header_value}"
|
30
|
+
end
|
31
|
+
when String
|
32
|
+
headers << "#{header_name}: #{header[1]}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
headers << "Connection: close"
|
36
|
+
return headers.join("\n")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
$stdout.sync = true
|
4
|
+
|
5
|
+
Signal.trap("INT") { puts "Gracefully exiting"; EventMachine.stop }
|
6
|
+
Signal.trap("TERM") { puts "Gracefully exiting"; EventMachine.stop }
|
7
|
+
# Signal.trap("USR1") { puts "Reloading log files" }
|
8
|
+
|
9
|
+
EM.kqueue if EM.kqueue? #osx
|
10
|
+
EM.epoll if EM.epoll? #linux
|
11
|
+
|
12
|
+
module Mallory
|
13
|
+
class Server
|
14
|
+
def initialize config
|
15
|
+
@logger = config.logger
|
16
|
+
@listen = config.listen
|
17
|
+
@request_builder = Mallory::RequestBuilder.new(config)
|
18
|
+
response_builder = Mallory::ResponseBuilder.new(config)
|
19
|
+
@proxy_builder = Mallory::ProxyBuilder.new(config, response_builder)
|
20
|
+
end
|
21
|
+
|
22
|
+
def start!
|
23
|
+
EventMachine.run {
|
24
|
+
@logger.info "Starting mallory"
|
25
|
+
EventMachine.start_server '127.0.0.1', @listen, Mallory::Connection, @request_builder, @proxy_builder, @logger
|
26
|
+
}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/mallory/version.rb
CHANGED
data/lib/mallory.rb
CHANGED
@@ -1 +1,14 @@
|
|
1
|
+
require 'mallory/configuration'
|
2
|
+
require 'mallory/backend/redis'
|
3
|
+
require 'mallory/backend/file'
|
4
|
+
require 'mallory/backend/self'
|
5
|
+
require 'mallory/backend/activerecord'
|
6
|
+
require 'mallory/request'
|
7
|
+
require 'mallory/response'
|
8
|
+
require 'mallory/proxy'
|
9
|
+
require 'mallory/connection'
|
10
|
+
require 'mallory/server'
|
11
|
+
require 'mallory/request_builder'
|
12
|
+
require 'mallory/response_builder'
|
13
|
+
require 'mallory/proxy_builder'
|
1
14
|
require 'mallory/version'
|
data/mallory.gemspec
CHANGED
@@ -4,7 +4,7 @@ require "mallory/version"
|
|
4
4
|
|
5
5
|
Gem::Specification.new do |s|
|
6
6
|
s.name = "mallory"
|
7
|
-
s.version =
|
7
|
+
s.version = Mallory::VERSION
|
8
8
|
s.platform = Gem::Platform::RUBY
|
9
9
|
s.authors = ["Marcin Sawicki"]
|
10
10
|
s.email = ["odcinek@gmail.com"]
|
@@ -16,8 +16,11 @@ Gem::Specification.new do |s|
|
|
16
16
|
s.add_dependency "eventmachine", "1.0.3"
|
17
17
|
s.add_dependency "redis"
|
18
18
|
s.add_dependency "em-http-request"
|
19
|
+
s.add_dependency "logging"
|
19
20
|
s.add_development_dependency "rspec"
|
20
21
|
s.add_development_dependency "sinatra"
|
22
|
+
s.add_development_dependency "sinatra-contrib"
|
23
|
+
s.add_development_dependency "thin"
|
21
24
|
s.add_development_dependency "rake"
|
22
25
|
|
23
26
|
s.files = `git ls-files`.split("\n")
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'mallory/configuration'
|
3
|
+
|
4
|
+
describe Mallory::Configuration do
|
5
|
+
before(:each) do
|
6
|
+
Mallory::Configuration.reset!
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'should set logger' do
|
10
|
+
logger = 'dummy'
|
11
|
+
Mallory::Configuration.logger = logger
|
12
|
+
expect(Mallory::Configuration.logger).to be(logger)
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should reset' do
|
16
|
+
Mallory::Configuration.logger = 'dummy'
|
17
|
+
expect(Mallory::Configuration.logger).to_not be_nil
|
18
|
+
Mallory::Configuration.reset!
|
19
|
+
expect(Mallory::Configuration.logger).to be_nil
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'mallory/backend/file'
|
3
|
+
|
4
|
+
describe Mallory::Backend::File do
|
5
|
+
|
6
|
+
let(:proxies){ ["127.0.0.1:5600","127.0.0.1:5601", "127.0.0.1:5602"] }
|
7
|
+
|
8
|
+
it "should return all proxies" do
|
9
|
+
file=File.should_receive(:readlines).with("proxies.txt").and_return(proxies)
|
10
|
+
Mallory::Backend::File.new("proxies.txt").all.should eq(proxies)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should return one proxy" do
|
14
|
+
File.should_receive(:readlines).with("proxies.txt").and_return([proxies.first])
|
15
|
+
Mallory::Backend::File.new("proxies.txt").any.should eq(proxies.first)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should raise on empty file" do
|
19
|
+
File.should_receive(:readlines).with("proxies.txt")
|
20
|
+
expect {Mallory::Backend::File.new("proxies.txt")}.to raise_error("Proxy file missing or empty")
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should raise on wrong format" do
|
24
|
+
File.should_receive(:readlines).with("proxies.txt").and_return(["wr0ng:f0rm4t"])
|
25
|
+
expect {Mallory::Backend::File.new("proxies.txt")}.to raise_error("Wrong format")
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should raise on missing file" do
|
29
|
+
File.should_receive(:readlines).with("proxies.txt").and_raise(Errno::ENOENT)
|
30
|
+
expect {Mallory::Backend::File.new("proxies.txt")}.to raise_error("Proxy file missing or empty")
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'mallory/proxy'
|
3
|
+
|
4
|
+
describe Mallory::Proxy do
|
5
|
+
|
6
|
+
=begin
|
7
|
+
it 'should' do
|
8
|
+
EM.run do
|
9
|
+
http = EventMachine::HttpRequest.new("http://127.0.0.1:6701/200", {}).get
|
10
|
+
http.callback do
|
11
|
+
EM.stop
|
12
|
+
end
|
13
|
+
http.errback { EM.stop; raise }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
=end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'mallory/request'
|
3
|
+
|
4
|
+
describe Mallory::Request do
|
5
|
+
let(:logger) { Logger.new(STDOUT) }
|
6
|
+
|
7
|
+
methods = ['GET', 'POST', 'HEAD', 'PUT', 'CONNECT', 'DELETE']
|
8
|
+
|
9
|
+
good_requests = [
|
10
|
+
{:path => "/", :host => "localhost"},
|
11
|
+
{:path => "/index.html", :host => "localhost"},
|
12
|
+
{:path => "/index.html?test", :host => "localhost:80"},
|
13
|
+
{:path => "http://localhost/index.html", :host => "localhost"},
|
14
|
+
{:path => "http://localhost/", :host => "localhost"},
|
15
|
+
{:path => "https://localhost/", :host => "localhost:443"},
|
16
|
+
{:path => "localhost:443", :host => "localhost:443"},
|
17
|
+
{:path => "/login", :host => "localhost"},
|
18
|
+
{:path => "localhost:443/index.html", :host => "localhost:443"}
|
19
|
+
]
|
20
|
+
|
21
|
+
bad_requests = [
|
22
|
+
{:path => "http://loca lhost :6700", :host => "localhost:6700"}
|
23
|
+
]
|
24
|
+
|
25
|
+
good_requests.each do |request|
|
26
|
+
it "should accept #{request[:path]}" do
|
27
|
+
methods.each do |method|
|
28
|
+
body =<<-HTTP.gsub(/^ +/, '')
|
29
|
+
#{method} #{request[:path]} HTTP/1.1
|
30
|
+
Host: #{request[:host]}
|
31
|
+
HTTP
|
32
|
+
rq = Mallory::Request.new(body, logger)
|
33
|
+
rq.method.should eq(method.downcase)
|
34
|
+
rq.body.should be(nil)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
bad_requests.each do |request|
|
40
|
+
it "should raise on #{request[:path]}" do
|
41
|
+
methods.each do |method|
|
42
|
+
body =<<-HTTP.gsub(/^ +/, '')
|
43
|
+
#{method} #{request[:path]} HTTP/1.1
|
44
|
+
Host: #{request[:host]}
|
45
|
+
HTTP
|
46
|
+
expect { Mallory::Request.new(body, logger) }.to raise_error
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'em-http-request'
|
3
|
+
require 'mallory/response'
|
4
|
+
require 'responder'
|
5
|
+
|
6
|
+
describe Mallory::Response do
|
7
|
+
let(:logger) { Logger.new(STDOUT) }
|
8
|
+
|
9
|
+
it "should filter out headers" do
|
10
|
+
EM.run do
|
11
|
+
http = EventMachine::HttpRequest.new("http://127.0.0.1:6701/200/headers", {}).get
|
12
|
+
http.callback do
|
13
|
+
r = Mallory::Response.new(http, logger)
|
14
|
+
r.description.should eq("OK")
|
15
|
+
r.status.should eq(200)
|
16
|
+
r.body.should eq("OK")
|
17
|
+
r.headers.split("\n").reject {|h| h.match(/^Date/)}.join("\n").should eq("Content-type: text/html;charset=utf-8\nContent-length: 2\nSet-cookie: cookie1=JohnDoe; domain=127.0.0.1; path=/; HttpOnly\nSet-cookie: cookie2=JaneRoe; domain=127.0.0.1; path=/; HttpOnly\nConnection: close")
|
18
|
+
EM.stop
|
19
|
+
end
|
20
|
+
http.errback { EM.stop; raise }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
data/spec/mallory.rb
ADDED
data/spec/responder.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require "sinatra/cookies"
|
3
|
+
|
4
|
+
class Responder < Sinatra::Base
|
5
|
+
|
6
|
+
helpers Sinatra::Cookies
|
7
|
+
|
8
|
+
set :protection, false
|
9
|
+
|
10
|
+
get '/200' do
|
11
|
+
status 200
|
12
|
+
headers "Server" => "Teapot Server"
|
13
|
+
"OK"
|
14
|
+
end
|
15
|
+
|
16
|
+
get '/200/headers' do
|
17
|
+
status 200
|
18
|
+
cookies[:cookie1] = 'JohnDoe'
|
19
|
+
cookies[:cookie2] = 'JaneRoe'
|
20
|
+
headers "Server" => "Teapot Server",
|
21
|
+
"Connection" => "Keep-Alive",
|
22
|
+
"Via" => "1.0 fred",
|
23
|
+
"Vary" => "Cookie",
|
24
|
+
"X-Powered-By" => "PHP/5.1.2+LOL"
|
25
|
+
"OK"
|
26
|
+
#"Transfer-encoding" => "chunked",
|
27
|
+
end
|
28
|
+
|
29
|
+
get '/418' do
|
30
|
+
status 418
|
31
|
+
headers "Allow" => "POST, GET, HEAD, PUT, DELETE, CONNECT",
|
32
|
+
"Server" => "Teapot Server"
|
33
|
+
body "I'm a tea pot!"
|
34
|
+
end
|
35
|
+
|
36
|
+
get '/500' do
|
37
|
+
status 500
|
38
|
+
headers "Server" => "Teapot Server",
|
39
|
+
"Connection" => "close"
|
40
|
+
true
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require 'webrick'
|
3
|
+
require 'em-http-request'
|
4
|
+
require 'responder'
|
5
|
+
require 'logger'
|
6
|
+
|
7
|
+
RSpec.configure do |config|
|
8
|
+
config.order = 'random'
|
9
|
+
|
10
|
+
config.before(:suite) do
|
11
|
+
trap(:INT) { EM.stop }
|
12
|
+
trap(:TERM){ EM.stop }
|
13
|
+
Thread.new do
|
14
|
+
Rack::Handler::WEBrick.run(
|
15
|
+
Responder.new,
|
16
|
+
:Port => 6701,
|
17
|
+
:AccessLog => [],
|
18
|
+
:Logger => WEBrick::Log::new("/dev/null", 7))
|
19
|
+
end
|
20
|
+
sleep 3
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mallory
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Marcin Sawicki
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-
|
11
|
+
date: 2013-10-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: eventmachine
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - '>='
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: logging
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
70
|
name: rspec
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,6 +94,34 @@ dependencies:
|
|
80
94
|
- - '>='
|
81
95
|
- !ruby/object:Gem::Version
|
82
96
|
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: sinatra-contrib
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: thin
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - '>='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
83
125
|
- !ruby/object:Gem::Dependency
|
84
126
|
name: rake
|
85
127
|
requirement: !ruby/object:Gem::Requirement
|
@@ -98,16 +140,43 @@ description: Man-in-the-middle http/https transparent http (CONNECT) proxy over
|
|
98
140
|
of (unreliable) backends
|
99
141
|
email:
|
100
142
|
- odcinek@gmail.com
|
101
|
-
executables:
|
143
|
+
executables:
|
144
|
+
- mallory
|
102
145
|
extensions: []
|
103
146
|
extra_rdoc_files: []
|
104
147
|
files:
|
105
148
|
- .gitignore
|
149
|
+
- .rspec
|
150
|
+
- .travis.yml
|
106
151
|
- Gemfile
|
107
152
|
- README.md
|
153
|
+
- Rakefile
|
154
|
+
- bin/mallory
|
155
|
+
- keys/keygen.sh
|
108
156
|
- lib/mallory.rb
|
157
|
+
- lib/mallory/backend/file.rb
|
158
|
+
- lib/mallory/backend/redis.rb
|
159
|
+
- lib/mallory/backend/self.rb
|
160
|
+
- lib/mallory/configuration.rb
|
161
|
+
- lib/mallory/connection.rb
|
162
|
+
- lib/mallory/proxy.rb
|
163
|
+
- lib/mallory/proxy_builder.rb
|
164
|
+
- lib/mallory/request.rb
|
165
|
+
- lib/mallory/request_builder.rb
|
166
|
+
- lib/mallory/response.rb
|
167
|
+
- lib/mallory/response_builder.rb
|
168
|
+
- lib/mallory/server.rb
|
109
169
|
- lib/mallory/version.rb
|
110
170
|
- mallory.gemspec
|
171
|
+
- spec/mallory.rb
|
172
|
+
- spec/mallory/configuration_spec.rb
|
173
|
+
- spec/mallory/connection_spec.rb
|
174
|
+
- spec/mallory/file_backend_spec.rb
|
175
|
+
- spec/mallory/proxy_spec.rb
|
176
|
+
- spec/mallory/request_spec.rb
|
177
|
+
- spec/mallory/response_spec.rb
|
178
|
+
- spec/responder.rb
|
179
|
+
- spec/spec_helper.rb
|
111
180
|
homepage: http://github.com/odcinek/mallory
|
112
181
|
licenses:
|
113
182
|
- MIT
|
@@ -133,5 +202,14 @@ signing_key:
|
|
133
202
|
specification_version: 4
|
134
203
|
summary: Man-in-the-middle http/https transparent http (CONNECT) proxy over bunch
|
135
204
|
of (unreliable) backends
|
136
|
-
test_files:
|
205
|
+
test_files:
|
206
|
+
- spec/mallory.rb
|
207
|
+
- spec/mallory/configuration_spec.rb
|
208
|
+
- spec/mallory/connection_spec.rb
|
209
|
+
- spec/mallory/file_backend_spec.rb
|
210
|
+
- spec/mallory/proxy_spec.rb
|
211
|
+
- spec/mallory/request_spec.rb
|
212
|
+
- spec/mallory/response_spec.rb
|
213
|
+
- spec/responder.rb
|
214
|
+
- spec/spec_helper.rb
|
137
215
|
has_rdoc:
|