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 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: