dongjia_ritm 1.0.4

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
+ 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: []