ritm 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []