dongjia_ritm 1.0.4

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
+ SHA256:
3
+ metadata.gz: a153a707f90726c604a8ee1d7f4b5c8b56b46bdfbe5508b3eaa73ccd83c4e752
4
+ data.tar.gz: db8814c5492caafa00c56334520c24eba69efc81d5e85482cc1479ca6ac71ffe
5
+ SHA512:
6
+ metadata.gz: 66da915d817a478faaa7e7551ef1a9e7f0798a5bccd980d7e593b9143511d444f9bb19ca2d6474f929767abc3d2b4ed3bb1a65ffe51f092d8e4b8ee73b721bd7
7
+ data.tar.gz: fa0b5609a8c211da90b70748b70a301577200993d842c9ca753ad3a8c52bc1a00f133db7f57abcb0f5ff327fdb3402fe23fb7dcfbd11c1a6916d70c72003e35f
@@ -0,0 +1,41 @@
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!(ca_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!(ca_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!(self.class.signing_profile)
26
+ end
27
+
28
+ def self.signing_profile
29
+ {
30
+ 'extensions' => {
31
+ 'keyUsage' => { 'usage' => %w[keyEncipherment digitalSignature] },
32
+ 'extendedKeyUsage' => { 'usage' => %w[serverAuth clientAuth] }
33
+ }
34
+ }
35
+ end
36
+
37
+ def self.ca_signing_profile
38
+ { 'extensions' => { 'keyUsage' => { 'usage' => %w[critical keyCertSign keyEncipherment digitalSignature] } } }
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,48 @@
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.subject.organization = cert.subject.organizational_unit = 'RubyInTheMiddle'
20
+ cert.subject.country = 'AR'
21
+ cert.not_before = cert.not_before - 3600 * 24 * 30 # Substract 30 days
22
+ cert.serial_number.number = serial_number || common_name.hash.abs
23
+ cert.key_material.generate_key(2048)
24
+ yield cert if block_given?
25
+ new cert
26
+ end
27
+
28
+ def initialize(cert)
29
+ @cert = cert
30
+ end
31
+
32
+ def private_key
33
+ @cert.key_material.private_key
34
+ end
35
+
36
+ def public_key
37
+ @cert.key_material.public_key
38
+ end
39
+
40
+ def pem
41
+ @cert.to_pem
42
+ end
43
+
44
+ def x509
45
+ @cert.openssl_body
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,74 @@
1
+ require 'dot_hash'
2
+ require 'set'
3
+
4
+ module Ritm
5
+ class Configuration
6
+ def default_settings # rubocop:disable Metrics/MethodLength
7
+ {
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
+ enabled: true,
24
+ request: {
25
+ add_headers: {},
26
+ strip_headers: [/proxy-*/],
27
+ unpack_gzip_deflate: true,
28
+ update_content_length: true
29
+ },
30
+ response: {
31
+ add_headers: { 'connection' => 'close' },
32
+ strip_headers: ['strict-transport-security'],
33
+ unpack_gzip_deflate: true,
34
+ update_content_length: true
35
+ },
36
+ process_chunked_encoded_transfer: true
37
+ },
38
+
39
+ misc: {
40
+ ssl_pass_through: [],
41
+ upstream_proxy: nil
42
+ }
43
+ }
44
+ end
45
+
46
+ def initialize
47
+ reset
48
+ end
49
+
50
+ def reset
51
+ @settings = default_settings.to_properties
52
+ end
53
+
54
+ def method_missing(m, *args, &block)
55
+ if @settings.respond_to?(m)
56
+ @settings.send(m, *args, &block)
57
+ else
58
+ super
59
+ end
60
+ end
61
+
62
+ def enable
63
+ @settings.intercept[:enabled] = true
64
+ end
65
+
66
+ def disable
67
+ @settings.intercept[:enabled] = false
68
+ end
69
+
70
+ def respond_to_missing?(method_name, _include_private = false)
71
+ @settings.respond_to?(method_name) || super
72
+ end
73
+ end
74
+ 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,63 @@
1
+ require 'zlib'
2
+
3
+ module Ritm
4
+ module Encodings
5
+ ENCODINGS = %i[identity gzip deflate].freeze
6
+
7
+ def self.encode(encoding, data)
8
+ case encoding
9
+ when :gzip
10
+ encode_gzip(data)
11
+ when :deflate
12
+ encode_deflate(data)
13
+ when :identity
14
+ identity(data)
15
+ else
16
+ raise "Unsupported encoding #{encoding}"
17
+ end
18
+ end
19
+
20
+ def self.decode(encoding, data)
21
+ case encoding
22
+ when :gzip
23
+ decode_gzip(data)
24
+ when :deflate
25
+ decode_deflate(data)
26
+ when :identity
27
+ identity(data)
28
+ else
29
+ raise "Unsupported encoding #{encoding}"
30
+ end
31
+ end
32
+
33
+ class << self
34
+ private
35
+
36
+ def identity(data)
37
+ data
38
+ end
39
+
40
+ def encode_gzip(data)
41
+ wio = StringIO.new('wb')
42
+ w_gz = Zlib::GzipWriter.new(wio)
43
+ w_gz.write(data)
44
+ w_gz.close
45
+ wio.string
46
+ end
47
+
48
+ def decode_gzip(data)
49
+ io = StringIO.new(data, 'rb')
50
+ gz = Zlib::GzipReader.new(io)
51
+ gz.read
52
+ end
53
+
54
+ def encode_deflate(data)
55
+ Zlib::Deflate.deflate(data)
56
+ end
57
+
58
+ def decode_deflate(data)
59
+ Zlib::Inflate.inflate(data)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,58 @@
1
+ require 'openssl'
2
+ require 'webrick'
3
+ require 'webrick/httpproxy'
4
+ require 'ritm/helpers/utils'
5
+
6
+ # Patch WEBrick too short max uri length
7
+ Ritm::Utils.silence_warnings { WEBrick::HTTPRequest::MAX_URI_LENGTH = 4000 }
8
+
9
+ # Digest::Digest is deprecated. This has been fixed in certificate_authority master
10
+ # but the project is no longer maintained and the fixed version was never published
11
+ Ritm::Utils.silence_warnings { OpenSSL::Digest::Digest = OpenSSL::Digest }
12
+
13
+ # Make request method writable
14
+ WEBrick::HTTPRequest.instance_eval { attr_accessor :request_method, :unparsed_uri }
15
+
16
+ module WEBrick
17
+ # Other patches to WEBrick::HTTPRequest
18
+ class HTTPRequest
19
+ def []=(header_name, *values)
20
+ @header[header_name.downcase] = values.map(&:to_s)
21
+ end
22
+
23
+ def body=(str)
24
+ body # Read current body
25
+ @body = str
26
+ end
27
+
28
+ def request_uri=(uri)
29
+ @request_uri = parse_uri(uri.to_s)
30
+ end
31
+ end
32
+
33
+ # Support other methods in HTTPServer
34
+ class HTTPServer
35
+ def do_DELETE(req, res)
36
+ perform_proxy_request(req, res) do |http, path, header|
37
+ http.delete(path, header)
38
+ end
39
+ end
40
+
41
+ def do_PUT(req, res)
42
+ perform_proxy_request(req, res) do |http, path, header|
43
+ http.put(path, req.body || '', header)
44
+ end
45
+ end
46
+
47
+ def do_PATCH(req, res)
48
+ perform_proxy_request(req, res) do |http, path, header|
49
+ http.patch(path, req.body || '', header)
50
+ end
51
+ end
52
+
53
+ # TODO: make sure options gets proxied too (so trace)
54
+ def do_OPTIONS(_req, res)
55
+ res['allow'] = 'GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS,CONNECT'
56
+ end
57
+ end
58
+ 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,12 @@
1
+
2
+ def default_request_handler(session)
3
+ proc do |req|
4
+ session.dispatcher.notify_request(req) if session.conf.intercept.enabled
5
+ end
6
+ end
7
+
8
+ def default_response_handler(session)
9
+ proc do |req, res|
10
+ session.dispatcher.notify_response(req, res) if session.conf.intercept.enabled
11
+ end
12
+ end
@@ -0,0 +1,80 @@
1
+ require 'faraday'
2
+ require 'ritm/interception/intercept_utils'
3
+ require 'webrick/cookie'
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
+
17
+ class ParamEncoder
18
+ def encode(params)
19
+ pairs = params.map { |k, v| "#{k}=#{v}" }
20
+ pairs.join('&')
21
+ end
22
+
23
+ def decode(query)
24
+ query.split('&').each_with_object({}) do |param, params|
25
+ k, v = param.split('=')
26
+ params[k] = v
27
+ end
28
+ end
29
+ end
30
+
31
+ class HTTPForwarder
32
+ include InterceptUtils
33
+
34
+ def initialize(request_interceptor, response_interceptor, context_config)
35
+ @request_interceptor = request_interceptor
36
+ @response_interceptor = response_interceptor
37
+ @config = context_config
38
+ # TODO: make SSL verification a configuration setting
39
+ @client = Faraday.new(
40
+ ssl: { verify: false },
41
+ request: { params_encoder: ParamEncoder.new }
42
+ ) do |conn|
43
+ conn.adapter :net_http
44
+ end
45
+ end
46
+
47
+ def forward(request, response)
48
+ intercept_request(@request_interceptor, request, @config.intercept.request)
49
+ faraday_response = faraday_forward request
50
+ to_webrick_response faraday_response, response
51
+ intercept_response(@response_interceptor, request, response, @config.intercept.response)
52
+ end
53
+
54
+ private
55
+
56
+ def faraday_forward(request)
57
+ req_method = request.request_method.downcase
58
+ @client.send req_method do |req|
59
+ req.options[:proxy] = @config.misc.upstream_proxy
60
+ req.url request.request_uri
61
+ req.body = request.body
62
+ request.header.each { |name, value| req.headers[name] = value }
63
+ end
64
+ end
65
+
66
+ def to_webrick_response(faraday_response, webrick_response)
67
+ webrick_response.status = faraday_response.status
68
+ webrick_response.body = faraday_response.body
69
+ faraday_response.headers.each do |name, value|
70
+ case name
71
+ when 'set-cookie'
72
+ WEBrick::Cookie.parse_set_cookies(value).each { |cookie| webrick_response.cookies << cookie }
73
+ else
74
+ webrick_response[name] = value
75
+ end
76
+ end
77
+ webrick_response
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,82 @@
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
+ module InterceptUtils
6
+ def intercept_request(handler, request, intercept_request_settings)
7
+ return if handler.nil?
8
+ preprocess(request, intercept_request_settings)
9
+ handler.call(request)
10
+ postprocess(request, intercept_request_settings)
11
+ end
12
+
13
+ def intercept_response(handler, request, response, intercept_response_settings)
14
+ return if handler.nil?
15
+ preprocess(response, intercept_response_settings)
16
+ handler.call(request, response)
17
+ postprocess(response, intercept_response_settings)
18
+ end
19
+
20
+ private
21
+
22
+ def preprocess(req_res, settings)
23
+ headers = header_obj(req_res)
24
+ decode(req_res) if settings.unpack_gzip_deflate
25
+ req_res.header.delete_if { |name, _v| strip?(name, settings.strip_headers) }
26
+ settings.add_headers.each { |name, value| headers[name] = value }
27
+ req_res.header.delete('transfer-encoding') if chunked?(headers)
28
+ end
29
+
30
+ def postprocess(req_res, settings)
31
+ header_obj(req_res)['content-length'] = (req_res.body || '').bytesize.to_s if settings.update_content_length
32
+ end
33
+
34
+ def chunked?(headers)
35
+ headers['transfer-encoding'] && headers['transfer-encoding'].casecmp('chunked')
36
+ end
37
+
38
+ def content_encoding(req_res)
39
+ ce = header_obj(req_res)['content-encoding'] || ''
40
+ case ce.downcase
41
+ when 'gzip', 'x-gzip'
42
+ :gzip
43
+ when 'deflate'
44
+ :deflate
45
+ else
46
+ :identity
47
+ end
48
+ end
49
+
50
+ def header_obj(req_res)
51
+ case req_res
52
+ when WEBrick::HTTPRequest
53
+ req_res
54
+ when WEBrick::HTTPResponse
55
+ req_res.header
56
+ end
57
+ end
58
+
59
+ def decode(req_res)
60
+ encoding = content_encoding(req_res)
61
+ return if encoding == :identity
62
+
63
+ req_res.body = Encodings.decode(encoding, req_res.body)
64
+ req_res.header.delete('content-encoding')
65
+ headers = header_obj(req_res)
66
+ headers['content-length'] = (req_res.body || '').bytesize.to_s
67
+ end
68
+
69
+ def strip?(header, rules)
70
+ header = header.to_s.downcase
71
+ rules.each do |rule|
72
+ case rule
73
+ when String
74
+ return true if header == rule
75
+ when Regexp
76
+ return true if header =~ rule
77
+ end
78
+ end
79
+ false
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,15 @@
1
+ require 'webrick'
2
+
3
+ module Ritm
4
+ # Actual implementation of the SSL Reverse Proxy service (decoupled from the certificate handling)
5
+ class RequestInterceptorServlet < WEBrick::HTTPServlet::AbstractServlet
6
+ def initialize(server, forwarder)
7
+ super server
8
+ @forwarder = forwarder
9
+ end
10
+
11
+ def service(request, response)
12
+ @forwarder.forward(request, response)
13
+ end
14
+ end
15
+ end
data/lib/ritm/main.rb ADDED
@@ -0,0 +1,17 @@
1
+ require 'ritm/session'
2
+
3
+ module Ritm
4
+ GLOBAL_SESSION = Session.new
5
+
6
+ def self.method_missing(m, *args, &block)
7
+ if GLOBAL_SESSION.respond_to?(m)
8
+ GLOBAL_SESSION.send(m, *args, &block)
9
+ else
10
+ super
11
+ end
12
+ end
13
+
14
+ def self.respond_to_missing?(method_name, _include_private = false)
15
+ GLOBAL_SESSION.respond_to?(method_name) || super
16
+ end
17
+ end
@@ -0,0 +1,67 @@
1
+ require 'openssl'
2
+ require 'webrick'
3
+ require 'webrick/https'
4
+ require 'ritm/certs/certificate'
5
+
6
+ IS_RUBY_2_4_OR_OLDER = Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.4')
7
+
8
+ module Ritm
9
+ module Proxy
10
+ # Patches WEBrick::HTTPServer SSL context creation to get
11
+ # a callback on the 'Client Helo' step of the SSL-Handshake if SNI is specified
12
+ # So RitM can create self-signed certificates on the fly
13
+ class CertSigningHTTPSServer < WEBrick::HTTPServer
14
+ # Override
15
+ def setup_ssl_context(config)
16
+ ctx = super(config)
17
+ prepare_sni_callback(ctx, config[:ca])
18
+ ctx
19
+ end
20
+
21
+ private
22
+
23
+ # Keeps track of the created self-signed certificates
24
+ # TODO: this can grow a lot and take up memory, fix by either:
25
+ # 1. implementing wildcard certificates generation (so there's one certificate per top level domain)
26
+ # 2. Use the same key material (private/public keys) for all the server names and just do the signing on-the-fly
27
+ # 3. both of the above
28
+ def prepare_sni_callback(ctx, ca)
29
+ contexts = {}
30
+ mutex = Mutex.new
31
+
32
+ # Sets the SNI callback on the SSLTCPSocket
33
+ ctx.servername_cb = proc do |sock, servername|
34
+ mutex.synchronize do
35
+ unless contexts.include? servername
36
+ cert = Ritm::Certificate.create(servername)
37
+ cert.cert.extensions['subjectAltName'].dns_names = [servername]
38
+ ca.sign(cert)
39
+ contexts[servername] = context_with_cert(sock.context, cert)
40
+ end
41
+ end
42
+ contexts[servername]
43
+ end
44
+ end
45
+
46
+ def context_with_cert(original_ctx, cert)
47
+ ctx = duplicate_context(original_ctx)
48
+ ctx.key = cert.private_key
49
+ ctx.cert = cert.x509
50
+ ctx
51
+ end
52
+
53
+ def duplicate_context(original_ctx)
54
+ return original_ctx.dup unless IS_RUBY_2_4_OR_OLDER
55
+
56
+ ctx = OpenSSL::SSL::SSLContext.new
57
+
58
+ original_ctx.instance_variables.each do |variable_name|
59
+ prop_name = variable_name.to_s.sub(/^@/, '')
60
+ set_prop_method = "#{prop_name}="
61
+ ctx.send(set_prop_method, original_ctx.send(prop_name)) if ctx.respond_to? set_prop_method
62
+ end
63
+ ctx
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,70 @@
1
+ require 'ritm/proxy/ssl_reverse_proxy'
2
+ require 'ritm/proxy/proxy_server'
3
+ require 'ritm/certs/ca'
4
+ require 'ritm/interception/handlers'
5
+ require 'ritm/interception/http_forwarder'
6
+
7
+ module Ritm
8
+ module Proxy
9
+ # Launches the Proxy server and the SSL Reverse Proxy with the given settings
10
+ class Launcher
11
+ def initialize(session)
12
+ build_settings(session)
13
+ build_reverse_proxy
14
+ build_proxy
15
+ end
16
+
17
+ def start
18
+ @https.start_async
19
+ @http.start_async
20
+ end
21
+
22
+ def shutdown
23
+ @https.shutdown
24
+ @http.shutdown
25
+ end
26
+
27
+ private
28
+
29
+ def build_settings(session)
30
+ @conf = session.conf
31
+ ssl_proxy_host = @conf.ssl_reverse_proxy.bind_address
32
+ ssl_proxy_port = @conf.ssl_reverse_proxy.bind_port
33
+ @https_forward = "#{ssl_proxy_host}:#{ssl_proxy_port}"
34
+
35
+ request_interceptor = default_request_handler(session)
36
+ response_interceptor = default_response_handler(session)
37
+ @forwarder = HTTPForwarder.new(request_interceptor, response_interceptor, @conf)
38
+
39
+ crt_path = @conf.ssl_reverse_proxy.ca.pem
40
+ key_path = @conf.ssl_reverse_proxy.ca.key
41
+ @certificate = ca_certificate(crt_path, key_path)
42
+ end
43
+
44
+ def build_proxy
45
+ @http = Ritm::Proxy::ProxyServer.new(BindAddress: @conf.proxy.bind_address,
46
+ Port: @conf.proxy.bind_port,
47
+ AccessLog: [],
48
+ Logger: WEBrick::Log.new(File.open(File::NULL, 'w')),
49
+ https_forward: @https_forward,
50
+ ProxyVia: nil,
51
+ forwarder: @forwarder,
52
+ ritm_conf: @conf)
53
+ end
54
+
55
+ def build_reverse_proxy
56
+ @https = Ritm::Proxy::SSLReverseProxy.new(@conf.ssl_reverse_proxy.bind_port,
57
+ @certificate,
58
+ @forwarder)
59
+ end
60
+
61
+ def ca_certificate(pem, key)
62
+ if pem.nil? || key.nil?
63
+ Ritm::CA.create
64
+ else
65
+ Ritm::CA.load(File.read(pem), File.read(key))
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,57 @@
1
+ require 'webrick'
2
+ require 'webrick/httpproxy'
3
+ require 'ritm/helpers/patches'
4
+
5
+ module Ritm
6
+ module Proxy
7
+ # Proxy server that accepts request and response intercept handlers for HTTP traffic
8
+ # HTTPS traffic is redirected to the SSLReverseProxy for interception
9
+ class ProxyServer < WEBrick::HTTPProxyServer
10
+ def start_async
11
+ trap(:TERM) { shutdown }
12
+ trap(:INT) { shutdown }
13
+ Thread.new { start }
14
+ end
15
+
16
+ # Override
17
+ # Patches the destination address on HTTPS connections to go via the HTTPS Reverse Proxy
18
+ def do_CONNECT(req, res)
19
+ req.unparsed_uri = @config[:https_forward] unless ssl_pass_through? req.unparsed_uri
20
+ super
21
+ end
22
+
23
+ # Override
24
+ # Handles HTTP (no SSL) traffic interception
25
+ def proxy_service(req, res)
26
+ # Proxy Authentication
27
+ proxy_auth(req, res)
28
+ @config[:forwarder].forward(req, res)
29
+ end
30
+
31
+ # Override
32
+ def proxy_uri(req, _res)
33
+ if req.request_method == 'CONNECT'
34
+ # Let the reverse proxy handle upstream proxies for https
35
+ nil
36
+ else
37
+ proxy = @config[:ritm_conf].misc.upstream_proxy
38
+ proxy.nil? ? nil : URI.parse(proxy)
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def ssl_pass_through?(destination)
45
+ @config[:ritm_conf].misc.ssl_pass_through.each do |matcher|
46
+ case matcher
47
+ when String
48
+ return true if destination == matcher
49
+ when Regexp
50
+ return true if destination =~ matcher
51
+ end
52
+ end
53
+ false
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,57 @@
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 forwarder [Ritm::HTTPForwarder]: Forwards http traffic with interception
15
+ def initialize(port, ca, forwarder)
16
+ @ca = ca
17
+ default_vhost = 'localhost'
18
+ @server = CertSigningHTTPSServer.new(Port: port,
19
+ AccessLog: [],
20
+ Logger: WEBrick::Log.new(File.open(File::NULL, 'w')),
21
+ ca: ca,
22
+ **vhost_settings(default_vhost))
23
+ @server.mount '/', RequestInterceptorServlet, forwarder
24
+ end
25
+
26
+ def start_async
27
+ trap(:TERM) { shutdown }
28
+ trap(:INT) { shutdown }
29
+ Thread.new { @server.start }
30
+ end
31
+
32
+ def shutdown
33
+ @server.shutdown
34
+ end
35
+
36
+ private
37
+
38
+ def gen_signed_cert(common_name)
39
+ cert = Ritm::Certificate.create(common_name)
40
+ @ca.sign(cert)
41
+ cert
42
+ end
43
+
44
+ def vhost_settings(hostname)
45
+ cert = gen_signed_cert(hostname)
46
+ {
47
+ ServerName: hostname,
48
+ SSLEnable: true,
49
+ SSLVerifyClient: OpenSSL::SSL::VERIFY_NONE,
50
+ SSLPrivateKey: OpenSSL::PKey::RSA.new(cert.private_key),
51
+ SSLCertificate: OpenSSL::X509::Certificate.new(cert.pem),
52
+ SSLCertName: [['CN', hostname]]
53
+ }
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,57 @@
1
+ require 'ritm/dispatcher'
2
+ require 'ritm/proxy/launcher'
3
+ require 'ritm/configuration'
4
+
5
+ module Ritm
6
+ # Holds the context of a interception session.
7
+ # Changes in the context configuration should affect only this session
8
+ class Session
9
+ attr_reader :conf, :dispatcher
10
+
11
+ def initialize
12
+ @conf = Configuration.new
13
+ @dispatcher = Dispatcher.new
14
+ @proxy = nil
15
+ end
16
+
17
+ # Define configuration settings
18
+ def configure(&block)
19
+ conf.instance_eval(&block)
20
+ end
21
+
22
+ # Re-enable fuzzing (if it was disabled)
23
+ def enable
24
+ conf.enable
25
+ end
26
+
27
+ # Disable fuzzing (if it was enabled)
28
+ def disable
29
+ conf.disable
30
+ end
31
+
32
+ # Start the proxy service
33
+ def start
34
+ raise 'Proxy already started' unless @proxy.nil?
35
+ @proxy = Proxy::Launcher.new(self)
36
+ @proxy.start
37
+ end
38
+
39
+ # Shutdown the proxy service
40
+ def shutdown
41
+ @proxy.shutdown unless @proxy.nil?
42
+ @proxy = nil
43
+ end
44
+
45
+ def add_handler(handler)
46
+ dispatcher.add_handler(handler)
47
+ end
48
+
49
+ def on_request(&block)
50
+ dispatcher.on_request(&block)
51
+ end
52
+
53
+ def on_response(&block)
54
+ dispatcher.on_response(&block)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,3 @@
1
+ module Ritm
2
+ VERSION = '1.0.4'.freeze
3
+ end
data/lib/ritm.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'ritm/version'
2
+ require 'ritm/main'
metadata ADDED
@@ -0,0 +1,229 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dongjia_ritm
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.4
5
+ platform: ruby
6
+ authors:
7
+ - dongjia
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-03-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: certificate_authority
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.1.6
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.1.6
27
+ - !ruby/object:Gem::Dependency
28
+ name: dot_hash
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.5'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: faraday
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.13'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.13'
55
+ - !ruby/object:Gem::Dependency
56
+ name: webrick
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.3'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.3'
69
+ - !ruby/object:Gem::Dependency
70
+ name: httpclient
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.8'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.8'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '12.2'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '12.2'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.7'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.7'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.51'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.51'
125
+ - !ruby/object:Gem::Dependency
126
+ name: simplecov
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '0.15'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '0.15'
139
+ - !ruby/object:Gem::Dependency
140
+ name: sinatra
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '2.0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '2.0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: sinatra-contrib
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '2.0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '2.0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: thin
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '1.7'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: '1.7'
181
+ description: HTTP(S) Intercept Proxy
182
+ email: ios@idongjia.cn
183
+ executables: []
184
+ extensions: []
185
+ extra_rdoc_files: []
186
+ files:
187
+ - lib/ritm.rb
188
+ - lib/ritm/certs/ca.rb
189
+ - lib/ritm/certs/certificate.rb
190
+ - lib/ritm/configuration.rb
191
+ - lib/ritm/dispatcher.rb
192
+ - lib/ritm/helpers/encodings.rb
193
+ - lib/ritm/helpers/patches.rb
194
+ - lib/ritm/helpers/utils.rb
195
+ - lib/ritm/interception/handlers.rb
196
+ - lib/ritm/interception/http_forwarder.rb
197
+ - lib/ritm/interception/intercept_utils.rb
198
+ - lib/ritm/interception/request_interceptor_servlet.rb
199
+ - lib/ritm/main.rb
200
+ - lib/ritm/proxy/cert_signing_https_server.rb
201
+ - lib/ritm/proxy/launcher.rb
202
+ - lib/ritm/proxy/proxy_server.rb
203
+ - lib/ritm/proxy/ssl_reverse_proxy.rb
204
+ - lib/ritm/session.rb
205
+ - lib/ritm/version.rb
206
+ homepage: https://github.com/argos83/ritm
207
+ licenses:
208
+ - MIT
209
+ metadata: {}
210
+ post_install_message:
211
+ rdoc_options: []
212
+ require_paths:
213
+ - lib
214
+ required_ruby_version: !ruby/object:Gem::Requirement
215
+ requirements:
216
+ - - ">="
217
+ - !ruby/object:Gem::Version
218
+ version: '0'
219
+ required_rubygems_version: !ruby/object:Gem::Requirement
220
+ requirements:
221
+ - - ">="
222
+ - !ruby/object:Gem::Version
223
+ version: '0'
224
+ requirements: []
225
+ rubygems_version: 3.1.2
226
+ signing_key:
227
+ specification_version: 4
228
+ summary: Ruby In The Middle
229
+ test_files: []