ritm 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2eeab06c6fd5db111b87a1ff18694f4206f571c9
4
+ data.tar.gz: b1537590beb540d93cd9161b7a7fee6d4090f3cb
5
+ SHA512:
6
+ metadata.gz: a2e1a3bbb3b8c0b9ac87339152c9a3b726ea2295d3c14c48e67952e89a5b40ea5314446e445078b5e98556550faf8de6609af42d6e438509ae4b87604f98fd1a
7
+ data.tar.gz: 9ebf2c4991de1026d05cfd9e6226424f9feb0631b1d22a001819195d9d3e215f779a72feb0b65dc1f653c317d6dab62dac49ed45c29f592e37ed38ac0cc8897d
@@ -0,0 +1,32 @@
1
+ require 'certificate_authority'
2
+ require 'ritm/certs/certificate'
3
+
4
+ module Ritm
5
+ # Wrapper on a Certificate Authority with ability of signing certificates
6
+ class CA < Ritm::Certificate
7
+ def self.create(common_name: 'RubyInTheMiddle')
8
+ super(common_name, serial_number: 1) do |cert|
9
+ cert.signing_entity = true
10
+ cert.sign!(signing_profile)
11
+ yield cert if block_given?
12
+ end
13
+ end
14
+
15
+ def self.load(crt, private_key)
16
+ super(crt, private_key) do |cert|
17
+ cert.signing_entity = true
18
+ cert.sign!(signing_profile)
19
+ yield cert if block_given?
20
+ end
21
+ end
22
+
23
+ def sign(certificate)
24
+ certificate.cert.parent = @cert
25
+ certificate.cert.sign!
26
+ end
27
+
28
+ def self.signing_profile
29
+ { 'extensions' => { 'keyUsage' => { 'usage' => %w(critical keyCertSign keyEncipherment digitalSignature) } } }
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,41 @@
1
+ require 'certificate_authority'
2
+
3
+ module Ritm
4
+ # Wraps a SSL Certificate via on-the-fly creation or loading from files
5
+ class Certificate
6
+ attr_accessor :cert
7
+
8
+ def self.load(crt, private_key)
9
+ x509 = OpenSSL::X509::Certificate.new(crt)
10
+ cert = CertificateAuthority::Certificate.from_openssl(x509)
11
+ cert.key_material.private_key = OpenSSL::PKey::RSA.new(private_key)
12
+ yield cert if block_given?
13
+ new cert
14
+ end
15
+
16
+ def self.create(common_name, serial_number: nil)
17
+ cert = CertificateAuthority::Certificate.new
18
+ cert.subject.common_name = common_name
19
+ cert.serial_number.number = serial_number || common_name.hash.abs
20
+ cert.key_material.generate_key
21
+ yield cert if block_given?
22
+ new cert
23
+ end
24
+
25
+ def initialize(cert)
26
+ @cert = cert
27
+ end
28
+
29
+ def private_key
30
+ @cert.key_material.private_key
31
+ end
32
+
33
+ def public_key
34
+ @cert.key_material.public_key
35
+ end
36
+
37
+ def pem
38
+ @cert.to_pem
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,77 @@
1
+ require 'dot_hash'
2
+ require 'set'
3
+
4
+ module Ritm
5
+ # Global Ritm settings
6
+ class Configuration
7
+ DEFAULT_SETTINGS = {
8
+ proxy: {
9
+ bind_address: '127.0.0.1',
10
+ bind_port: 8080
11
+ },
12
+
13
+ ssl_reverse_proxy: {
14
+ bind_address: '127.0.0.1',
15
+ bind_port: 8081,
16
+ ca: {
17
+ pem: nil,
18
+ key: nil
19
+ }
20
+ },
21
+
22
+ intercept: {
23
+ # Is interception enabled
24
+ enabled: true,
25
+
26
+ # Do not intercept requests whose URLs match/start with the given regex/strings (blacklist)
27
+ skip_urls: [],
28
+
29
+ # Intercepts requests whose URLs match/start with the given regex/strings (whitelist)
30
+ # By default everything will be intercepted.
31
+ intercept_urls: []
32
+ },
33
+
34
+ misc: {
35
+ add_request_headers: {},
36
+ add_response_headers: { 'connection' => 'clone' },
37
+
38
+ strip_request_headers: [/proxy-*/],
39
+ strip_response_headers: ['strict-transport-security', 'transfer-encoding'],
40
+
41
+ unpack_gzip_deflate_in_requests: true,
42
+ unpack_gzip_deflate_in_responses: true,
43
+ process_chunked_encoded_transfer: true
44
+ }
45
+ }.freeze
46
+
47
+ def initialize(settings = {})
48
+ settings = DEFAULT_SETTINGS.merge(settings)
49
+ @values = {
50
+ dispatcher: Dispatcher.new,
51
+
52
+ # Is interception enabled
53
+ enabled: true
54
+ }
55
+
56
+ @settings = settings.to_properties
57
+ end
58
+
59
+ def method_missing(m, *args, &block)
60
+ @settings.send(m, *args, &block)
61
+ end
62
+
63
+ def [](setting)
64
+ @values[setting]
65
+ end
66
+
67
+ # Re-enable interception
68
+ def enable
69
+ @values[:enabled] = true
70
+ end
71
+
72
+ # Disable interception
73
+ def disable
74
+ @values[:enabled] = false
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,37 @@
1
+ module Ritm
2
+ # Keeps a list of subscribers and notifies them when requests/responses are intercepted
3
+ class Dispatcher
4
+ def initialize
5
+ @handlers = { on_request: [], on_response: [] }
6
+ end
7
+
8
+ def add_handler(handler)
9
+ on_request { |*args| handler.on_request(*args) } if handler.respond_to? :on_request
10
+ on_response { |*args| handler.on_response(*args) } if handler.respond_to? :on_response
11
+ end
12
+
13
+ def on_request(&block)
14
+ @handlers[:on_request] << block
15
+ end
16
+
17
+ def on_response(&block)
18
+ @handlers[:on_response] << block
19
+ end
20
+
21
+ def notify_request(request)
22
+ notify(:on_request, request)
23
+ end
24
+
25
+ def notify_response(request, response)
26
+ notify(:on_response, request, response)
27
+ end
28
+
29
+ private
30
+
31
+ def notify(event, *args)
32
+ @handlers[event].each do |handler|
33
+ handler.call(*args)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,65 @@
1
+ require 'zlib'
2
+
3
+ module Ritm
4
+ # ENCODER/DECODER of HTTP content
5
+ module Encodings
6
+ ENCODINGS = [:identity, :gzip, :deflate].freeze
7
+
8
+ def self.encode(encoding, data)
9
+ case encoding
10
+ when :gzip
11
+ encode_gzip(data)
12
+ when :deflate
13
+ encode_deflate(data)
14
+ when :identity
15
+ identity(data)
16
+ else
17
+ raise "Unsupported encoding #{encoding}"
18
+ end
19
+ end
20
+
21
+ def self.decode(encoding, data)
22
+ case encoding
23
+ when :gzip
24
+ decode_gzip(data)
25
+ when :deflate
26
+ decode_deflate(data)
27
+ when :identity
28
+ identity(data)
29
+ else
30
+ raise "Unsupported encoding #{encoding}"
31
+ end
32
+ end
33
+
34
+ class << self
35
+ private
36
+
37
+ # Returns data unchanged. Identity is the default value of Accept-Encoding headers.
38
+ def identity(data)
39
+ data
40
+ end
41
+
42
+ def encode_gzip(data)
43
+ wio = StringIO.new('wb')
44
+ w_gz = Zlib::GzipWriter.new(wio)
45
+ w_gz.write(data)
46
+ w_gz.close
47
+ wio.string
48
+ end
49
+
50
+ def decode_gzip(data)
51
+ io = StringIO.new(data, 'rb')
52
+ gz = Zlib::GzipReader.new(io)
53
+ gz.read
54
+ end
55
+
56
+ def encode_deflate(data)
57
+ Zlib::Deflate.deflate(data)
58
+ end
59
+
60
+ def decode_deflate(data)
61
+ Zlib::Inflate.inflate(data)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,53 @@
1
+ require 'webrick'
2
+ require 'webrick/httpproxy'
3
+ require 'ritm/helpers/utils'
4
+
5
+ # Patch WEBrick too short max uri length
6
+ Ritm::Utils.silence_warnings { WEBrick::HTTPRequest::MAX_URI_LENGTH = 4000 }
7
+
8
+ # Make request method writable
9
+ WEBrick::HTTPRequest.instance_eval { attr_accessor :request_method, :unparsed_uri }
10
+
11
+ module WEBrick
12
+ # Other patches to WEBrick::HTTPRequest
13
+ class HTTPRequest
14
+ def []=(header_name, *values)
15
+ @header[header_name.downcase] = values.map(&:to_s)
16
+ end
17
+
18
+ def body=(str)
19
+ body # Read current body
20
+ @body = str
21
+ end
22
+
23
+ def request_uri=(uri)
24
+ @request_uri = parse_uri(uri.to_s)
25
+ end
26
+ end
27
+
28
+ # Support other methods in HTTPServer
29
+ class HTTPServer
30
+ def do_DELETE(req, res)
31
+ perform_proxy_request(req, res) do |http, path, header|
32
+ http.delete(path, header)
33
+ end
34
+ end
35
+
36
+ def do_PUT(req, res)
37
+ perform_proxy_request(req, res) do |http, path, header|
38
+ http.put(path, req.body || '', header)
39
+ end
40
+ end
41
+
42
+ def do_PATCH(req, res)
43
+ perform_proxy_request(req, res) do |http, path, header|
44
+ http.patch(path, req.body || '', header)
45
+ end
46
+ end
47
+
48
+ # TODO: make sure options gets proxied too (so trace)
49
+ def do_OPTIONS(_req, res)
50
+ res['allow'] = 'GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS,CONNECT'
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,15 @@
1
+
2
+ module Ritm
3
+ # Shortcuts and other utilities to be included
4
+ # in modules/classes
5
+ module Utils
6
+ # Runs a block of code without warnings.
7
+ def self.silence_warnings
8
+ warn_level = $VERBOSE
9
+ $VERBOSE = nil
10
+ result = yield
11
+ $VERBOSE = warn_level
12
+ result
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+
2
+ dispatcher = Ritm.dispatcher
3
+
4
+ DEFAULT_REQUEST_HANDLER = proc do |req|
5
+ dispatcher.notify_request(req) if Ritm.conf[:enabled]
6
+ end
7
+
8
+ DEFAULT_RESPONSE_HANDLER = proc do |req, res|
9
+ dispatcher.notify_response(req, res) if Ritm.conf[:enabled]
10
+ end
@@ -0,0 +1,78 @@
1
+ require 'faraday'
2
+
3
+ require 'ritm/interception/intercept_utils'
4
+
5
+ module Ritm
6
+ # Forwarder that acts as a WEBrick <-> Faraday adaptor: Works this way:
7
+ # 1. A WEBrick request objects is received
8
+ # 2. The WEBrick request object is sent to the request interceptor
9
+ # 3. The (maybe modified) WEBrick request object is transformed into a Faraday request and sent to destination server
10
+ # 4. The Faraday response obtained from the server is transformed into a WEBrick response
11
+ # 5. The WEBrick response object is sent to the response interceptor
12
+ # 6. The (maybe modified) WEBrick response object is sent back to the client
13
+ #
14
+ # Besides the possible modifications to be done by interceptors there might be automated globally configured
15
+ # transformations like header stripping/adding.
16
+ class HTTPForwarder
17
+ include InterceptUtils
18
+
19
+ def initialize(request_interceptor, response_interceptor)
20
+ @request_interceptor = request_interceptor
21
+ @response_interceptor = response_interceptor
22
+ # TODO: make SSL verification a configuration setting
23
+ @client = Faraday.new(ssl: { verify: false }) do |conn|
24
+ conn.adapter :net_http
25
+ # TODO: support customizations (e.g. upstream proxies or different adapters)
26
+ end
27
+ end
28
+
29
+ def forward(request, response)
30
+ intercept_request(@request_interceptor, request)
31
+
32
+ faraday_response = faraday_forward request
33
+ to_webrick_response faraday_response, response
34
+ intercept_response(@response_interceptor, request, response)
35
+ end
36
+
37
+ private
38
+
39
+ def faraday_forward(request)
40
+ req_method = request.request_method.downcase
41
+ @client.send req_method do |req|
42
+ req.url request.request_uri
43
+ req.body = request.body
44
+ add_request_headers(req, request)
45
+ end
46
+ end
47
+
48
+ def add_request_headers(faraday_request, webrick_request)
49
+ webrick_request.header.each do |name, value|
50
+ faraday_request.headers[name] = value unless strip?(name, Ritm.conf.misc.strip_request_headers)
51
+ end
52
+ Ritm.conf.misc.add_request_headers.each { |k, v| faraday_request.headers[k] = v }
53
+ end
54
+
55
+ def to_webrick_response(faraday_response, webrick_response)
56
+ webrick_response.status = faraday_response.status
57
+ webrick_response.body = faraday_response.body
58
+ faraday_response.headers.each do |name, value|
59
+ webrick_response[name] = value unless strip?(name, Ritm.conf.misc.strip_response_headers)
60
+ end
61
+ Ritm.conf.misc.add_response_headers.each { |k, v| webrick_response[k] = v }
62
+ webrick_response
63
+ end
64
+
65
+ def strip?(header, rules)
66
+ header = header.to_s.downcase
67
+ rules.each do |rule|
68
+ case rule
69
+ when String
70
+ return true if header == rule
71
+ when Regexp
72
+ return true if header =~ rule
73
+ end
74
+ end
75
+ false
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,50 @@
1
+ require 'ritm/helpers/encodings'
2
+
3
+ module Ritm
4
+ # Interceptor callbacks calling logic shared by the HTTP Proxy Server and the SSL Reverse Proxy Server
5
+ # Passes request
6
+ module InterceptUtils
7
+ def intercept_request(handler, request)
8
+ return if handler.nil?
9
+ handler.call(request)
10
+ end
11
+
12
+ def intercept_response(handler, request, response)
13
+ return if handler.nil?
14
+ # TODO: Disable the automated decoding from config
15
+ encoding = content_encoding(response)
16
+ decoded(encoding, response) do |decoded_response|
17
+ handler.call(request, decoded_response)
18
+ end
19
+
20
+ response.header.delete('content-length') if chunked?(response)
21
+ end
22
+
23
+ private
24
+
25
+ def chunked?(response)
26
+ response.header.fetch('transfer-encoding', '').casecmp 'chunked'
27
+ end
28
+
29
+ def content_encoding(response)
30
+ case response.header.fetch('content-encoding', '').downcase
31
+ when 'gzip', 'x-gzip'
32
+ :gzip
33
+ when 'deflate'
34
+ :deflate
35
+ else
36
+ :identity
37
+ end
38
+ end
39
+
40
+ def decoded(encoding, res)
41
+ res.body = Encodings.decode(encoding, res.body)
42
+ _content_encoding = res.header.delete('content-encoding')
43
+ yield res
44
+ # TODO: should it be re-encoded?
45
+ # res.body = Encodings.encode(encoding, res.body)
46
+ # res.header['content-encoding'] = content_encoding
47
+ res.header['content-length'] = res.body.size.to_s
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,16 @@
1
+ require 'webrick'
2
+ require 'ritm/interception/http_forwarder'
3
+
4
+ module Ritm
5
+ # Actual implementation of the SSL Reverse Proxy service (decoupled from the certificate handling)
6
+ class RequestInterceptorServlet < WEBrick::HTTPServlet::AbstractServlet
7
+ def initialize(server, request_interceptor, response_interceptor)
8
+ super server
9
+ @forwarder = HTTPForwarder.new(request_interceptor, response_interceptor)
10
+ end
11
+
12
+ def service(request, response)
13
+ @forwarder.forward(request, response)
14
+ end
15
+ end
16
+ end
data/lib/ritm/main.rb ADDED
@@ -0,0 +1,70 @@
1
+ # Main module
2
+ module Ritm
3
+ # Define global settings
4
+ def self.configure(&block)
5
+ conf.instance_eval(&block)
6
+ end
7
+
8
+ # Re-enable fuzzing (if it was disabled)
9
+ def self.enable
10
+ conf.enable
11
+ end
12
+
13
+ # Disable fuzzing (if it was enabled)
14
+ def self.disable
15
+ conf.disable
16
+ end
17
+
18
+ # Access the current config settings
19
+ def self.conf
20
+ @configuration ||= Configuration.new
21
+ end
22
+
23
+ def self.dispatcher
24
+ conf[:dispatcher]
25
+ end
26
+
27
+ def self.add_handler(handler)
28
+ dispatcher.add_handler(handler)
29
+ end
30
+
31
+ def self.on_request(&block)
32
+ dispatcher.on_request(&block)
33
+ end
34
+
35
+ def self.on_response(&block)
36
+ dispatcher.on_response(&block)
37
+ end
38
+
39
+ # private class methods
40
+
41
+ def self.intercept?(request)
42
+ return false unless conf[:enabled]
43
+ url = request.url.to_s
44
+ whitelisted?(url) && !blacklisted?(url)
45
+ end
46
+
47
+ def self.whitelisted?(url)
48
+ conf[:attack_urls].empty? || url_matches_any?(url, conf[:attack_urls])
49
+ end
50
+
51
+ def self.blacklisted?(url)
52
+ url_matches_any? url, conf[:skip_urls]
53
+ end
54
+
55
+ def self.url_matches_any?(url, matchers)
56
+ matchers.each do |matcher|
57
+ case matcher
58
+ when Regexp
59
+ return true if url =~ matcher
60
+ when String
61
+ return true if url.include? matcher
62
+ else
63
+ raise 'URL matcher should be a String or Regexp'
64
+ end
65
+ end
66
+ false
67
+ end
68
+
69
+ private_class_method :intercept?, :whitelisted?, :blacklisted?, :url_matches_any?
70
+ end
@@ -0,0 +1,50 @@
1
+ require 'webrick'
2
+ require 'webrick/https'
3
+ require 'ritm/certs/certificate'
4
+
5
+ module Ritm
6
+ module Proxy
7
+ # Patches WEBrick::HTTPServer SSL context creation to get
8
+ # a callback on the 'Client Helo' step of the SSL-Handshake if SNI is specified
9
+ # So we can create self-signed certificates on the fly
10
+ class CertSigningHTTPSServer < WEBrick::HTTPServer
11
+ # Override
12
+ def setup_ssl_context(config)
13
+ ctx = super(config)
14
+ prepare_sni_callback(ctx, config[:ca])
15
+ ctx
16
+ end
17
+
18
+ private
19
+
20
+ # Keeps track of the created self-signed certificates
21
+ # TODO: this can grow a lot and take up memory, fix by either:
22
+ # 1. implementing wildcard certificates generation (so there's one certificate per top level domain)
23
+ # 2. Use the same key material (private/public keys) for all the server names and just do the signing on-the-fly
24
+ # 3. both of the above
25
+ def prepare_sni_callback(ctx, ca)
26
+ contexts = {}
27
+ mutex = Mutex.new
28
+
29
+ # Sets the SNI callback on the SSLTCPSocket
30
+ ctx.servername_cb = proc do |sock, servername|
31
+ mutex.synchronize do
32
+ unless contexts.include? servername
33
+ cert = Ritm::Certificate.create(servername)
34
+ ca.sign(cert)
35
+ contexts[servername] = context_with_cert(sock.context, cert)
36
+ end
37
+ end
38
+ contexts[servername]
39
+ end
40
+ end
41
+
42
+ def context_with_cert(original_ctx, cert)
43
+ ctx = original_ctx.dup
44
+ ctx.key = OpenSSL::PKey::RSA.new(cert.private_key)
45
+ ctx.cert = OpenSSL::X509::Certificate.new(cert.pem)
46
+ ctx
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,80 @@
1
+ require 'ritm/proxy/ssl_reverse_proxy'
2
+ require 'ritm/proxy/proxy_server'
3
+ require 'ritm/certs/ca'
4
+ require 'ritm/interception/handlers'
5
+
6
+ module Ritm
7
+ module Proxy
8
+ # Launches the Proxy server and the SSL Reverse Proxy with the given settings
9
+ class Launcher
10
+ # By default settings are read from Ritm::Configuration but you can override some via these named arguments:
11
+ # proxy_port [Fixnum]: the port where the main proxy listens (the one to be configured in the client)
12
+ # ssl_reverse_proxy_port [Fixnum]: the port where the reverse proxy for ssl traffic interception listens
13
+ # interface [String]: the host/address to bind the main proxy
14
+ # ca_crt_path [String]: the path to the certification authority certificate
15
+ # ca_key_path [String]: the path to the certification authority private key
16
+ # request_interceptor [Proc |request|]: the handler for request interception
17
+ # response_interceptor [Proc |request, response|]: the handler for response interception
18
+ def initialize(**args)
19
+ build_settings(**args)
20
+
21
+ build_reverse_proxy(@ssl_proxy_host, @ssl_proxy_port, @request_interceptor, @response_interceptor)
22
+ build_proxy(@proxy_host, @proxy_port, @https_forward, @request_interceptor, @response_interceptor)
23
+ end
24
+
25
+ # Starts the service (non blocking)
26
+ def start
27
+ @https.start_async
28
+ @http.start_async
29
+ end
30
+
31
+ # Stops the service
32
+ def shutdown
33
+ @https.shutdown
34
+ @http.shutdown
35
+ end
36
+
37
+ private
38
+
39
+ def build_settings(**args)
40
+ c = Ritm.conf
41
+ @proxy_host = args.fetch(:interface, c.proxy.bind_address)
42
+ @proxy_port = args.fetch(:proxy_port, c.proxy.bind_port)
43
+ @ssl_proxy_host = c.ssl_reverse_proxy.bind_address
44
+ @ssl_proxy_port = args.fetch(:ssl_reverse_proxy_port, c.ssl_reverse_proxy.bind_port)
45
+ @https_forward = "#{@ssl_proxy_host}:#{@ssl_proxy_port}"
46
+ @request_interceptor = args[:request_interceptor] || DEFAULT_REQUEST_HANDLER
47
+ @response_interceptor = args[:response_interceptor] || DEFAULT_RESPONSE_HANDLER
48
+
49
+ crt_path = args.fetch(:ca_crt_path, c.ssl_reverse_proxy.ca.pem)
50
+ key_path = args.fetch(:ca_key_path, c.ssl_reverse_proxy.ca.key)
51
+ @certificate = ca_certificate(crt_path, key_path)
52
+ end
53
+
54
+ def build_proxy(host, port, https_forward_to, req_intercept, res_intercept)
55
+ @http = Ritm::Proxy::ProxyServer.new(Port: port,
56
+ AccessLog: [],
57
+ BindAddress: host,
58
+ Logger: WEBrick::Log.new(File.open(File::NULL, 'w')),
59
+ https_forward: https_forward_to,
60
+ ProxyVia: nil,
61
+ request_interceptor: req_intercept,
62
+ response_interceptor: res_intercept)
63
+ end
64
+
65
+ def build_reverse_proxy(_host, port, req_intercept, res_intercept)
66
+ @https = Ritm::Proxy::SSLReverseProxy.new(port, @certificate,
67
+ request_interceptor: req_intercept,
68
+ response_interceptor: res_intercept)
69
+ end
70
+
71
+ def ca_certificate(pem, key)
72
+ if pem.nil? || key.nil?
73
+ Ritm::CA.create
74
+ else
75
+ Ritm::CA.load(File.read(pem), File.read(key))
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,49 @@
1
+ require 'webrick'
2
+ require 'webrick/httpproxy'
3
+ require 'ritm/helpers/patches'
4
+ require 'ritm/interception/intercept_utils'
5
+
6
+ module Ritm
7
+ module Proxy
8
+ # Proxy server that accepts request and response intercept handlers for HTTP traffic
9
+ # HTTPS traffic is redirected to the SSLReverseProxy for interception
10
+ class ProxyServer < WEBrick::HTTPProxyServer
11
+ include InterceptUtils
12
+
13
+ def start_async
14
+ trap(:TERM) { shutdown }
15
+ trap(:INT) { shutdown }
16
+ Thread.new { start }
17
+ end
18
+
19
+ # Override
20
+ # Patches the destination address on HTTPS connections to go via the HTTPS Reverse Proxy
21
+ def do_CONNECT(req, res)
22
+ req.class.send(:attr_accessor, :unparsed_uri)
23
+ req.unparsed_uri = @config[:https_forward]
24
+ super
25
+ end
26
+
27
+ # Override
28
+ # Handles HTTP (no SSL) traffic interception
29
+ def proxy_service(req, res)
30
+ # Proxy Authentication
31
+ proxy_auth(req, res)
32
+
33
+ # Request modifier handler
34
+ intercept_request(@config[:request_interceptor], req)
35
+
36
+ begin
37
+ send("do_#{req.request_method}", req, res)
38
+ rescue NoMethodError
39
+ raise WEBrick::HTTPStatus::MethodNotAllowed, "unsupported method `#{req.request_method}'."
40
+ rescue => err
41
+ raise WEBrick::HTTPStatus::ServiceUnavailable, err.message
42
+ end
43
+
44
+ # Response modifier handler
45
+ intercept_response(@config[:response_interceptor], req, res)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,59 @@
1
+ require 'ritm/helpers/patches'
2
+ require 'ritm/interception/request_interceptor_servlet'
3
+ require 'ritm/proxy/cert_signing_https_server'
4
+ require 'ritm/certs/certificate'
5
+
6
+ module Ritm
7
+ module Proxy
8
+ # SSL Intercept reverse proxy server. Supports interception of https request and responses
9
+ # It does man-in-the-middle with on-the-fly certificate signing using the given CA
10
+ class SSLReverseProxy
11
+ # Creates a HTTPS server with the given settings
12
+ # @param port [Fixnum]: TCP port to bind the service
13
+ # @param ca [Ritm::CA]: The certificate authority used to sign fake server certificates
14
+ # @param request_interceptor [Proc]: If given, it will be invoked before proxying the request
15
+ # @param response_interceptor [Proc]: If give, it will be invoked before sending back the response
16
+ def initialize(port, ca, request_interceptor: nil, response_interceptor: nil)
17
+ @ca = ca
18
+ default_vhost = 'localhost'
19
+ @server = CertSigningHTTPSServer.new(Port: port,
20
+ AccessLog: [],
21
+ Logger: WEBrick::Log.new(File.open(File::NULL, 'w')),
22
+ ca: ca,
23
+ **vhost_settings(default_vhost))
24
+
25
+ @server.mount '/', RequestInterceptorServlet, request_interceptor, response_interceptor
26
+ end
27
+
28
+ def start_async
29
+ trap(:TERM) { shutdown }
30
+ trap(:INT) { shutdown }
31
+ Thread.new { @server.start }
32
+ end
33
+
34
+ def shutdown
35
+ @server.shutdown
36
+ end
37
+
38
+ private
39
+
40
+ def gen_signed_cert(common_name)
41
+ cert = Ritm::Certificate.create(common_name)
42
+ @ca.sign(cert)
43
+ cert
44
+ end
45
+
46
+ def vhost_settings(hostname)
47
+ cert = gen_signed_cert(hostname)
48
+ {
49
+ ServerName: hostname,
50
+ SSLEnable: true,
51
+ SSLVerifyClient: OpenSSL::SSL::VERIFY_NONE,
52
+ SSLPrivateKey: OpenSSL::PKey::RSA.new(cert.private_key),
53
+ SSLCertificate: OpenSSL::X509::Certificate.new(cert.pem),
54
+ SSLCertName: [['CN', hostname]]
55
+ }
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,4 @@
1
+ # Ritm version
2
+ module Ritm
3
+ VERSION = '0.0.1'.freeze
4
+ end
data/lib/ritm.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'ritm/version'
2
+ require 'ritm/dispatcher'
3
+ require 'ritm/configuration'
4
+ require 'ritm/main'
5
+ require 'ritm/proxy/launcher'
metadata ADDED
@@ -0,0 +1,187 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ritm
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Sebastián Tello
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-05-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.9'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.9'
27
+ - !ruby/object:Gem::Dependency
28
+ name: webrick
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: certificate_authority
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.1.6
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.1.6
55
+ - !ruby/object:Gem::Dependency
56
+ name: dot_hash
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.5'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.5'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.4'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.4'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.40'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.40'
97
+ - !ruby/object:Gem::Dependency
98
+ name: sinatra
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.4'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.4'
111
+ - !ruby/object:Gem::Dependency
112
+ name: thin
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '1.6'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '1.6'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rake
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '11.1'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '11.1'
139
+ description: HTTP(S) Intercept Proxy
140
+ email: argos83+ritm@gmail.com
141
+ executables: []
142
+ extensions: []
143
+ extra_rdoc_files: []
144
+ files:
145
+ - lib/ritm.rb
146
+ - lib/ritm/certs/ca.rb
147
+ - lib/ritm/certs/certificate.rb
148
+ - lib/ritm/configuration.rb
149
+ - lib/ritm/dispatcher.rb
150
+ - lib/ritm/helpers/encodings.rb
151
+ - lib/ritm/helpers/patches.rb
152
+ - lib/ritm/helpers/utils.rb
153
+ - lib/ritm/interception/handlers.rb
154
+ - lib/ritm/interception/http_forwarder.rb
155
+ - lib/ritm/interception/intercept_utils.rb
156
+ - lib/ritm/interception/request_interceptor_servlet.rb
157
+ - lib/ritm/main.rb
158
+ - lib/ritm/proxy/cert_signing_https_server.rb
159
+ - lib/ritm/proxy/launcher.rb
160
+ - lib/ritm/proxy/proxy_server.rb
161
+ - lib/ritm/proxy/ssl_reverse_proxy.rb
162
+ - lib/ritm/version.rb
163
+ homepage: https://github.com/argos83/ritm
164
+ licenses:
165
+ - Apache License, v2.0
166
+ metadata: {}
167
+ post_install_message:
168
+ rdoc_options: []
169
+ require_paths:
170
+ - lib
171
+ required_ruby_version: !ruby/object:Gem::Requirement
172
+ requirements:
173
+ - - ">="
174
+ - !ruby/object:Gem::Version
175
+ version: '0'
176
+ required_rubygems_version: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ requirements: []
182
+ rubyforge_project:
183
+ rubygems_version: 2.4.5.1
184
+ signing_key:
185
+ specification_version: 4
186
+ summary: Ruby In The Middle
187
+ test_files: []