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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 87eb6511a86298565a32bb4f9a6200773d39fe9e
4
- data.tar.gz: dec4a02af3306a9f348d38fa656b448a06a9c92c
3
+ metadata.gz: 2986cd40c0575541258ea30258a3647ba162d6c3
4
+ data.tar.gz: ffbeddde71442cb1fc00cab05f48f560cd6b2e19
5
5
  SHA512:
6
- metadata.gz: c54f259b962ff224ab1031d698c892c94fca926f2249905bb14f463c170ba4ebd5ed17c74a8e6c92ae36aca1e12c8fc70176b3c95b3adbc101f8b685c71e53b0
7
- data.tar.gz: 8cbfc1e0028fecf96dffc919f70230bc8c0fe97e4b4f5b874122632be0ad8d5704a421a6efcd306881744855558b562b92c35eedbbda6ca4195aa96b90c5b300
6
+ metadata.gz: 930f5970b48a7085b177b21da243ea8a4b598a150fdce183a1769036c7f8ede30d8f84080c230e883fc30d0204f687a434c3d5ac853bb3d97f6f9414357dc042
7
+ data.tar.gz: 7d5bf8c37b5335631e7b61cce9b0402ca99a4dbb6d33980b5670f32fc8741a0f51f5e4cc16e66698bfbc09fa043a00e24ff9079ff201c03f431c4969810995ba
data/.gitignore CHANGED
@@ -6,3 +6,5 @@ sbin
6
6
  keys/*.crt
7
7
  keys/*.key
8
8
  keys/*.csr
9
+ proxies.txt
10
+ mallory.log
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format documentation
3
+ #--profile
data/.travis.yml ADDED
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 2.0.0
5
+ - 1.9.3
6
+
7
+ gemfile:
8
+ - Gemfile
9
+
10
+ script: "echo 'COME ON!' && bundle exec rake spec"
data/README.md CHANGED
@@ -1,4 +1,9 @@
1
- ## mallory
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
- ### Example usage
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
- ### What mallory is not
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 [mallory](https://github.com/mallory/mallory)
58
+ - For mature general purpose mitm solution (in Python) see [mitmproxy](https://github.com/mitmproxy/mitmproxy)
27
59
 
28
- ### TODO
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
- ### Resources
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
- ### Contributors
71
+ ## Contributors
40
72
 
41
73
  - [Marcin Sawicki](https://github.com/odcinek)
42
74
  - [Maria Kacik](https://github.com/mkacik)
43
75
 
44
- ### License
76
+ ## License
45
77
 
46
78
  (The MIT License)
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+
6
+ desc "Run all RSpec tests"
7
+ RSpec::Core::RakeTask.new(:spec)
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,18 @@
1
+ module Mallory
2
+ module Backend
3
+ class Self
4
+ def initialize()
5
+ @proxies = []
6
+ end
7
+
8
+ def any
9
+ @proxies.sample
10
+ end
11
+
12
+ def all
13
+ @proxies
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,11 @@
1
+ module Mallory
2
+ class RequestBuilder
3
+ def initialize(config)
4
+ @config = config
5
+ end
6
+
7
+ def build(data)
8
+ Mallory::Request.new(data, @config.logger)
9
+ end
10
+ end
11
+ 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,9 @@
1
+ class Mallory::ResponseBuilder
2
+ def initialize(config)
3
+ @config = config
4
+ end
5
+
6
+ def build(data)
7
+ Mallory::Response.new(data, @config.logger)
8
+ end
9
+ 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
@@ -1,5 +1,3 @@
1
- module EventMachine
2
- class Mallory
3
- VERSION = "0.0.1"
4
- end
1
+ module Mallory
2
+ VERSION = "0.0.2"
5
3
  end
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 = EventMachine::Mallory::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,11 @@
1
+ require 'spec_helper'
2
+ require 'mallory/connection'
3
+
4
+ describe Mallory::Connection do
5
+
6
+ =begin
7
+ it 'should' do
8
+ end
9
+ =end
10
+
11
+ 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
@@ -0,0 +1,4 @@
1
+ require 'spec_helper'
2
+ require 'mallory'
3
+ options = {:listen => 6701}
4
+ Mallory.new(options).start!
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
@@ -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.1
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-09-27 00:00:00.000000000 Z
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: