Project

rmitm

0.0
Repository is archived
No commit activity in last 3 years
No release in over 3 years
rmitm provides a DSL and useful ruby classes and python scripts for using mitmdump for automated testing.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Runtime

>= 1.8.1, ~> 1.8
>= 0.5.6, ~> 0.5
 Project Readme

RMITM

RMITM provides a ruby interface to mitmdump - the command line version of mitmproxy.

Installation

gem install rmitm

Prerequisites

(Obviously) mitmproxy must be installed - version 0.10.1.

Note: Some corporate firewalls may block access to http://mitmproxy.org. In this case, the source of the site is available via github - specifically the installation page.

Compatibility

RMITM is developed and used on OSX. Although there is no obvious reason why RMITM wouldn't work on Linux, this is untested.

For Windows, there are specific reasons why some features of RMITM would not work as is. Fixing these would be fairly straightforward, should you need to, but up to now running on Windows hasn't been a requirement.

Motivation

RMITM came about from the need to automate a pack of manual functional web tests in Ruby. The manual tests used a proxy application to modify specific server responses, but the proxy application only had a limited API that enabled turning functionality on or off, not configure responses on a per test basis.

A number of alternative proxies were investigated and mitmproxy was identified as the closest match to the requirements, except that it was implemented in Python. The decision was therefore made to implement a new HTTP proxy in Ruby.

This worked fine until the requirements changed and some of the requests needed to be made over HTTPS. It was decided that adding "man-in-the-middle" functionality to the Ruby proxy would be more complex than creating an integration layer for mitmdump in Ruby. Hence RMITM was created.

License

Copyright (C) 2014  Marc Bleeze (marcbleeze<at>gmail<dot>com)

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see http://www.gnu.org/licenses/.

Usage

Mitmdump

Defining proxies

Mitmdump configurations are defined using a DSL:

mitmdump :p1 do
  port 8888
  output 'my.dump'
end
=> #<Mitmdump:0x007fc321a479f0 @name=:p1, @port=8888, @output="my.dump", @scripts=[], @params={}>

By default the proxy starts on port 8080 and outputs traffic flows to ./dumps/mitm.dump (any required directories will be created, subject to permissions).

mitmdump :default do
end
=> #<Mitmdump:0x007fc321a71610 @name=:default, @port=8080, @output="dumps/mitm.dump", @scripts=[], @params={}>

Since Mitmdump is intended for use with test automation the proxy will always start in quiet mode.

Loading proxies from file

At runtime the named configurations can be loaded from file(s) using a glob pattern. For example, if the configuration above is defined in ./features/support/mitm/config.mitm:

load_proxies('./features/support/mitm/*.mitm')

Loading the proxies creates a hash of proxy configs that can be retrieved by name using #proxy:

{:p1=>
  #<Mitmdump:0x007fc321a479f0
   @name=:p1,
   @output="my.dump",
   @params={},
   @port=8888,
   @scripts=[]>,
 :default=>
  #<Mitmdump:0x007fc321a71610
   @name=:default,
   @output="dumps/mitm.dump",
   @params={},
   @port=8080,
   @scripts=[]>}

proxy('p1')
=> #<Mitmdump:0x007fc321a479f0 @name=:p1, @port=8888, @output="my.dump", @scripts=[], @params={}>

Starting proxy

proxy('p1').start

Stopping proxy

proxy('p1').stop

Scripting

Altering specific requests or responses in your application traffic flow is achieved using mitmproxy's scripting API.

RMITM bundled scripts

RMITM includes Python scripts for some common functionality.

Currently these are:

  • Blacklist - returns a 404 response if the request path matches a regular expression
  • Map local - the response content for any request with a path matching a regular expression will contain the contents of the file provided
  • Replace - for any request with a path matching a regular expression, any text in the response content matching a second regular expression will be replaced with the provided string
  • Strip Encoding - removes the Accept-Encoding header from the request headers so that traffic is not compressed

DSL

Already demonstrated above

port - specifies the port for the proxy to listen on, defaults to 8080 if not provided

output - specifies the file for mitmdump to write traffic flows to, defaults to dumps/mitm.dump

Bundled scripts

mitmdump :example do
  blacklist '\/collect\?'

  map_local '\/',
    :file => 'local_homepage.html'

  replace '\/application_config\.js',
  	:swap => 'name=\"timeout\" value=\"\d+\"',
  	:with => "name=\\\"timeout\\\" value=\\\"10\\\""

  strip_encoding
end

Note that, since strings will be passed to mitmdump via the command line, special attention needs to be paid to escaping quotes.

Custom scripts

Your own custom Python scripts can also be added:

mitmdump :custom do
  script '/custom/add_header.py'
end

Non-anonymous script arguments can be passed in a hash:

mitmdump :custom_with_args do
  script 'lib/python/myscript.py', '-h' => 'host.com', '-u' => 'user1'
end

Parameterisation

Parameters, denoted by %, can be included in script argument strings, however for the replacement to succeed at runtime, they must also be declared using param:

mitmdump :parameter_example do
  param 'new_value'
  replace '\/application_config\.js',
    :swap => 'name=\"timeout\" value=\"\d+\"',
    :with => "name=\\\"timeout\\\" value=\\\"%new_value\\\""

  param 'user'
  script 'lib/python/myscript.py', '-h' => 'host.com', '-u' => '%user'
end

Replacement values are specified in the #start call:

proxy('parameter_example').start 'new_value' => '20', 'user' => 'user2'

Config inheritance

It is possible to add scripts and parameters to a proxy configuration by 'inheriting' from previously defined configs:

mitmdump :default do
  strip_encoding
end
=> #<Mitmdump:0x007fc321abed48 @name=:default, @port=8080, @output="dumps/mitm.dump", @scripts=[[".../strip_encoding.py", {}]], @params={}>

mitmdump :extend do
  inherit :default
  param 'new_value'
  replace '\/application_config\.js',
    :swap => 'name=\"timeout\" value=\"\d+\"',
    :with => "name=\\\"timeout\\\" value=\\\"%new_value\\\""
end
=> #<Mitmdump:0x007fc321b9c6c0 @name=:extend, @port=8080, @output="dumps/mitm.dump", @scripts=[[".../strip_encoding.py", {}], [".../replace.py", {"-p"=>"\\/application_config\\.js", "-x"=>"name=\\\"timeout\\\" value=\\\"\\d+\\\"", "-r"=>"name=\\\"timeout\\\" value=\\\"%new_value\\\""}]], @params={"%new_value"=>""}>

mitmdump :extend2 do
  inherit :extend
  blacklist '\/'
end
=> #<Mitmdump:0x007fc321bcdc70 @name=:extend2, @port=8080, @output="dumps/mitm.dump", @scripts=[[".../strip_encoding.py", {}], [".../replace.py", {"-p"=>"\\/application_config\\.js", "-x"=>"name=\\\"timeout\\\" value=\\\"\\d+\\\"", "-r"=>"name=\\\"timeout\\\" value=\\\"%new_value\\\""}], [".../blacklist.py", {"-p"=>"\\/"}]], @params={"%new_value"=>""}>

Other public methods

#dumpfile - returns the location of the mitmdump flow dump

Example Cucumber integration

Define your proxy configurations in ./features/support/mitm/config.mitm:

mitmdump :default do
  strip_encoding 
  # by inheriting :default in all other proxies, compression can be turned off globally
end
.
.
.

Load your proxy config definitions in ./features/support/env.rb:

load_proxies('./features/support/mitm/config.mitm')

Define a step definition similar to the following:

When(/^I use (\S*)\s*proxy(?: with \s*(.+\s*=\s*[^,\s]+),?)?$/) do |p, args| 
  h = args ? Hash[*args.gsub(/\s+|"|'/, '').split(/,|=/)] : {}
  p = 'default' if p == ''
  $mitm = proxy p.to_sym
  $mitm.start(h)
end

Example steps matching this step definition:

When I use proxy

When I use custom proxy

When I use custom proxy with new_value = 20

When I use custom proxy with new_value = 20, user = 'user2', host = "my.host.com"

Finally, stop the proxy at the end of the scenario in the After hook (conventionally specified in ./features/support/hooks.rb):

After do |scenario|
  $mitm.stop if $mitm
end

Reading from an mitmdump output file

MitmdumpReader

MitmdumpReader enables reading from an mitmdump output file to JavaScript Object Notation (JSON).

reader = MitmdumpReader.new(proxy('default').dumpfile)
reader.get_flows_from_file    # returns an array of all flows in file in JSON format
reader.get_requests_from_file    # returns an array of all requests in file in JSON format
reader.get_responses_from_file    # returns an array of all responses in file in JSON format

If you need to run validations or queries on the contents of the dumpfile it is generally more practical to use MitmFlowArray instead of MitmdumpReader. MitmdumpReader is just a Ruby integration layer for a Python program that parses the mitmdump output file format into JSON.

MitmFlowArray

MitmFlowArray provides utility methods for filtering the recorded flows by one or more conditions and returning specific values from the JSON. JSONPath is used to achieve this.

f = MitmFlowArray.from_file(proxy('p1').dumpfile)
hosts = f.values_by_jpath('$..request.host')
response_codes = f.values_by_jpath('$..response.code')

conditions = [
	['$..request.host', /stackoverflow\.com/],
	['$..request.method', /POST/i]
]
so_post_request_paths = f.filter(conditions).values_by_jpath('$..request.path')

Given the structure of the JSON produced by MitmdumpReader it is unexpected that a given JSONPath expression will yield more than one value per 'flow'. Consequently, by default, #values_by_jpath returns an array of the first values found:

response_codes = f.values_by_jpath('$..response.code')
# ==> [200, 200]

An array of arrays of all values per flow that match the JSONPath expression can also be returned:

response_codes = f.values_by_jpath('$..response.code', false)
# ==> [[200], [200]]