mallory 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Gem Version](https://badge.fury.io/rb/mallory.png)](https://rubygems.org/gems/mallory)
|
4
|
+
[![Build Status](https://secure.travis-ci.org/odcinek/mallory.png?branch=master)](http://travis-ci.org/odcinek/mallory)
|
5
|
+
[![Dependency Status](https://gemnasium.com/odcinek/mallory.png?travis)](https://gemnasium.com/odcinek/mallory)
|
6
|
+
[![Code Climate](https://codeclimate.com/github/odcinek/mallory.png)](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:
|