manticore 0.8.0-java → 0.9.0-java

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 063b0fd32e3dfbee4335d04293e098e8b1bc054ecd727ff51011e6eef3675c4b
4
- data.tar.gz: 8a5da3e5e6433d7d57e388069e95393fbc66220f1695ab7b1443f9dc49f84d11
3
+ metadata.gz: 281400646207bd8e4b34bed53610787e5b5d222c72fe5e82dd1d42d8f071f1be
4
+ data.tar.gz: 421a4a8f1c4f8e06a7d12377812da2613877877904c73d3d8375348fbb9e2103
5
5
  SHA512:
6
- metadata.gz: db22357ef64973e1452bb970dbb2d7a6c960c895db7d0afe1ae96339f414a06dba8c1039b7f5d3d0ce493443317a9d72c7dce04e7c7fbab98704e55e124d2766
7
- data.tar.gz: 6744d52a35f0e94d23db237b9f364353eeec4bb3667eb35a06620cf16b676e5b49bd76bb12ba7296f6924b2b7ae052cb4f1d443a6f1e87b00d32b9d1af60c507
6
+ metadata.gz: c72e625dc8aa526a1336c239ced7d46cebf9b16817f9c309b1caaa80028a14e959cbe342e745db69dfef8fd284b1413085a518322406ecab3e7a28725815a34f
7
+ data.tar.gz: 37774c46167abf4c8cb637580dc2cf91afc4311b8a284c17d48441ba00824ba0af41e47121780837caffef60632aa3c26bb075b323011a2fcb56fb055495e83c
data/.travis.yml CHANGED
@@ -4,18 +4,18 @@ cache:
4
4
  - bundler
5
5
  - directories:
6
6
  - $HOME/.m2
7
- rvm:
8
- - jruby-9.2.19.0 # Ruby 2.5
9
- jdk:
10
- - oraclejdk8
11
- - openjdk8
12
- - openjdk11
13
7
  before_install:
14
8
  - gem install bundler -v 1.17.3
15
9
  matrix:
16
10
  include:
17
11
  - rvm: jruby-head
18
12
  jdk: openjdk11
13
+ - rvm: jruby-9.3.4.0 # Ruby 2.6
14
+ jdk: openjdk11
15
+ - rvm: jruby-9.2.20.0 # Ruby 2.5
16
+ jdk: oraclejdk8
17
+ - rvm: jruby-9.2.20.0
18
+ jdk: openjdk11
19
19
  - rvm: jruby-9.1.17.0 # Ruby 2.3
20
20
  jdk: openjdk8
21
21
  allow_failures:
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ### v0.9.0
2
+
3
+ * [feat] revamped client.close to release resources (#108)
4
+ * [fix] null password handling when loading keystore
5
+ * [fix] ambiguous Java method selection
6
+ * [refactor] revisit hostname verification (#103)
7
+ * [feat] ssl[:trust_strategy]: wrapper for `org.apache.http.conn.ssl.TrustStrategy` (#106)
8
+
1
9
  ### v0.8.0
2
10
 
3
11
  * [feat] restore compat with (legacy) verify: false (#102)
data/Gemfile CHANGED
@@ -4,13 +4,15 @@ source 'https://rubygems.org'
4
4
  gemspec
5
5
 
6
6
  group :development, :test do
7
- gem "net-http-server", "~> 0.2"
7
+ gem "rake-compiler", require: false
8
+ gem "simplecov"
9
+
8
10
  gem "rspec", "~> 3.0"
9
11
  gem "rspec-its"
10
- gem "httpclient", "~> 2.3"
11
- gem "rack", ">= 2.1.4"
12
- gem "rake-compiler"
13
- gem "gserver"
14
- gem "simplecov"
15
- gem "json"
12
+
13
+ gem "rack", ">= 2.1.4", require: false
14
+ gem "json", require: false
15
+ gem "webrick", require: false
16
+ gem "net-http-server", require: false
17
+ gem "gserver", require: false
16
18
  end
@@ -0,0 +1,119 @@
1
+ module Manticore
2
+ class Client
3
+ ##
4
+ # TrustStrategies is a utility module that provides helpers for
5
+ # working with org.apache.http.conn.ssl.TrustStrategy
6
+ module TrustStrategies
7
+ # Coerces to org.apache.http.conn.ssl.TrustStrategy, allowing nil pass-through
8
+ #
9
+ # @overload coerce(coercible)
10
+ # @param coercible [nil|TrustStrategy]
11
+ # @return [nil,TrustStrategy]
12
+ # @overload coerce(coercible)
13
+ # @param coercible [Proc<(Array<OpenSSL::X509::Certificate>,String)>:Boolean]
14
+ # A proc that accepts two arguments and returns a boolean value, and is effectively a
15
+ # Ruby-native implementation of `org.apache.http.conn.ssl.TrustStrategy#isTrusted`.
16
+ # @param cert_chain [Enumerable<OpenSSL::X509::Certificate>]: the peer's certificate chain
17
+ # @param auth_type [String]: the authentication type based on the client certificate
18
+ # @raise [OpenSSL::X509::CertificateError]: thrown if the certificate is not trusted or invalid
19
+ # @return [Boolean]: true if the certificate can be trusted without verification by the trust manager,
20
+ # false otherwise.
21
+ # @example: CA Trusted Fingerprint
22
+ # ca_trusted_fingerprint = lambda do |cert_chain, type|
23
+ # cert_chain.lazy
24
+ # .map(&:to_der)
25
+ # .map(&::Digest::SHA256.method(:hexdigest))
26
+ # .include?("324a87eebb19265ffb675dc345eb0f3b5d9de3f015159227a00fe552291d4cc4")
27
+ # end
28
+ # TrustStrategies.coerce(ca_trusted_fingerprint)
29
+ def self.coerce(coercible)
30
+ case coercible
31
+ when org.apache.http.conn.ssl.TrustStrategy, nil then coercible
32
+ when ::Proc then CustomTrustStrategy.new(coercible)
33
+ else fail(ArgumentError, "No implicit conversion of #{coercible} to #{self}")
34
+ end
35
+ end
36
+
37
+ # Combines two possibly-nil TrustStrategies-coercible objects into a
38
+ # single org.apache.http.conn.ssl.TrustStrategy, or to nil if both are nil.
39
+ #
40
+ # @param lhs [nil|TrustStrategie#coerce]
41
+ # @param rhs [nil|TrustStrategies#coerce]
42
+ # @return [nil,org.apache.http.conn.ssl.TrustStrategy]
43
+ def self.combine(lhs, rhs)
44
+ return coerce(rhs) if lhs.nil?
45
+ return coerce(lhs) if rhs.nil?
46
+
47
+ CombinedTrustStrategy.new(coerce(lhs), coerce(rhs))
48
+ end
49
+ end
50
+
51
+ ##
52
+ # @api private
53
+ # A CombinedTrustStrategy can be used to bypass the Trust Manager if
54
+ # *EITHER* TrustStrategy trusts the provided certificate chain.
55
+ # @see TrustStrategies::combine
56
+ class CombinedTrustStrategy
57
+ include org.apache.http.conn.ssl.TrustStrategy
58
+
59
+ ##
60
+ # @api private
61
+ # @see TrustStrategies::combine
62
+ def initialize(lhs, rhs)
63
+ @lhs = lhs
64
+ @rhs = rhs
65
+ super()
66
+ end
67
+
68
+ ##
69
+ # @override (see org.apache.http.conn.ssl.TrustStrategy#isTrusted)
70
+ def trusted?(chain, type)
71
+ @lhs.trusted?(chain, type) || @rhs.trusted?(chain, type)
72
+ end
73
+ end
74
+
75
+
76
+ ##
77
+ # @api private
78
+ # A CustomTrustStrategy is an org.apache.http.conn.ssl.TrustStrategy
79
+ # defined with a proc that uses Ruby OpenSSL::X509::Certificates
80
+ # @see TrustStrategies::coerce(Proc)
81
+ class CustomTrustStrategy
82
+ include org.apache.http.conn.ssl.TrustStrategy
83
+
84
+ ##
85
+ # @see TrustStrategies.coerce(Proc)
86
+ def initialize(proc)
87
+ fail(ArgumentError, "2-arity proc required") unless proc.arity == 2
88
+ @trust_strategy = proc
89
+ end
90
+
91
+ CONVERT_JAVA_CERTIFICATE_TO_RUBY = -> (java_cert) { ::OpenSSL::X509::Certificate.new(java_cert.encoded) }
92
+ private_constant :CONVERT_JAVA_CERTIFICATE_TO_RUBY
93
+
94
+ ##
95
+ # @override (see org.apache.http.conn.ssl.TrustStrategy#isTrusted)
96
+ def trusted?(java_chain, type)
97
+ @trust_strategy.call(java_chain.lazy.map(&CONVERT_JAVA_CERTIFICATE_TO_RUBY), String.new(type))
98
+ rescue OpenSSL::X509::CertificateError => e
99
+ raise java_certificate_exception(e)
100
+ end
101
+
102
+ private
103
+
104
+ begin
105
+ # Ruby exceptions can be converted to Throwable since JRuby 9.2
106
+ Exception.new("sentinel").to_java(java.lang.Throwable)
107
+ def java_certificate_exception(ruby_certificate_error)
108
+ throwable = ruby_certificate_error.to_java(java.lang.Throwable)
109
+ java.security.cert.CertificateException.new(throwable)
110
+ end
111
+ rescue TypeError
112
+ def java_certificate_exception(ruby_certificate_error)
113
+ message = ruby_certificate_error.message
114
+ java.security.cert.CertificateException.new(message)
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -86,25 +86,38 @@ module Manticore
86
86
  java_import "org.manticore.HttpGetWithEntity"
87
87
  java_import "org.manticore.HttpDeleteWithEntity"
88
88
  java_import "org.apache.http.auth.UsernamePasswordCredentials"
89
+ java_import "org.apache.http.conn.ssl.DefaultHostnameVerifier"
90
+ java_import "org.apache.http.conn.ssl.NoopHostnameVerifier"
89
91
  java_import "org.apache.http.conn.ssl.SSLConnectionSocketFactory"
90
- java_import "org.apache.http.conn.ssl.SSLContextBuilder"
91
92
  java_import "org.apache.http.conn.ssl.TrustAllStrategy"
92
93
  java_import "org.apache.http.conn.ssl.TrustSelfSignedStrategy"
93
94
  java_import "org.apache.http.client.utils.URIBuilder"
94
95
  java_import "org.apache.http.impl.DefaultConnectionReuseStrategy"
95
96
  java_import "org.apache.http.impl.auth.BasicScheme"
97
+ java_import "org.apache.http.ssl.SSLContextBuilder"
98
+ java_import "org.apache.http.ssl.TrustStrategy"
96
99
 
97
100
  # This is a class rather than a proc because the proc holds a closure around
98
101
  # the instance of the Client that creates it.
99
102
  class ExecutorThreadFactory
100
- include ::Java::JavaUtilConcurrent::ThreadFactory
103
+ include java.util.concurrent.ThreadFactory
104
+
105
+ java_import 'java.lang.Thread'
106
+
107
+ @@factory_no = java.util.concurrent.atomic.AtomicInteger.new
108
+
109
+ def initialize(client)
110
+ @thread_no = java.util.concurrent.atomic.AtomicInteger.new
111
+ @name_prefix = "manticore##{client.object_id}-#{@@factory_no.increment_and_get}-"
112
+ end
101
113
 
102
114
  def newThread(runnable)
103
- thread = Executors.defaultThreadFactory.newThread(runnable)
104
- thread.daemon = true
105
- return thread
115
+ thread = Thread.new(runnable, @name_prefix + @thread_no.increment_and_get.to_s)
116
+ thread.setDaemon(true)
117
+ thread
106
118
  end
107
119
  end
120
+ private_constant :ExecutorThreadFactory
108
121
 
109
122
  include ProxiesInterface
110
123
 
@@ -162,10 +175,12 @@ module Manticore
162
175
  # @option options [Hash] ssl Hash of options for configuring SSL
163
176
  # @option options [Array<String>] ssl[:protocols] (nil) A list of protocols that Manticore should accept
164
177
  # @option options [Array<String>] ssl[:cipher_suites] (nil) A list of cipher suites that Manticore should accept
165
- # @option options [Symbol] ssl[:verify] (:strict) Hostname verification setting. Set to `:disable` to turn off hostname verification. Setting to `:browser` will
178
+ # @option options [Symbol] ssl[:verify] (:default) Hostname verification setting. Set to `:none` to turn off hostname verification. Setting to `:browser` will
166
179
  # cause Manticore to accept a certificate for *.foo.com for all subdomains and sub-subdomains (eg a.b.foo.com).
167
- # The default `:strict` is like `:browser` except it'll only accept a single level of subdomains for wildcards,
180
+ # The default `:default` is like `:browser` but more strict - only accepts a single level of subdomains for wildcards,
168
181
  # eg `b.foo.com` will be accepted for a `*.foo.com` certificate, but `a.b.foo.com` will not be.
182
+ # @option options [Client::TrustStrategiesInterface] ssl[:trust_strategy] (nil) A trust strategy to use in addition to any built by `ssl[:verify]`.
183
+ # @see Client::TrustStrategiesInterface#coerce
169
184
  # @option options [String] ssl[:truststore] (nil) Path to a custom trust store to use the verifying SSL connections
170
185
  # @option options [String] ssl[:truststore_password] (nil) Password used for decrypting the server trust store
171
186
  # @option options [String] ssl[:truststore_type] (nil) Format of the trust store, ie "JKS" or "PKCS12". If left nil, the type will be inferred from the truststore filename.
@@ -178,8 +193,8 @@ module Manticore
178
193
  # @option options [boolean] ssl[:track_state] (false) Turn on or off connection state tracking. This helps prevent SSL information from leaking across threads, but means that connections
179
194
  # can't be shared across those threads. This should generally be left off unless you know what you're doing.
180
195
  def initialize(options = {})
181
- @finalizers = []
182
- self.class.shutdown_on_finalize self, @finalizers
196
+ @finalizer = Finalizer.new
197
+ self.class.shutdown_on_finalize self, @finalizer
183
198
 
184
199
  builder = client_builder
185
200
  builder.set_user_agent options.fetch(:user_agent, "Manticore #{VERSION}")
@@ -345,21 +360,58 @@ module Manticore
345
360
  end
346
361
  end
347
362
 
348
- # Free resources associated with the CloseableHttpClient
349
- def close
350
- @client.close if @client
363
+ # Release resources held by this client, namely:
364
+ # - close the internal http client
365
+ # - shutdown the connection pool
366
+ # - stops accepting async requests in the executor
367
+ #
368
+ # After this call the client is no longer usable.
369
+ # @note In versions before 0.9 this method only closed the underlying `CloseableHttpClient`
370
+ def close(await: nil)
371
+ ObjectSpace.undefine_finalizer(self)
372
+ @finalizer.call # which does ~ :
373
+ # @executor&.shutdown rescue nil
374
+ # @client&.close
375
+ # @pool&.shutdown rescue nil
376
+ @async_requests.close
377
+ case await
378
+ when false, nil
379
+ @executor&.shutdown_now rescue nil
380
+ when Numeric
381
+ # NOTE: the concept of awaiting gracefully might/should also be used with the pool/client closing
382
+ millis = java.util.concurrent.TimeUnit::MILLISECONDS
383
+ @executor&.await_termination(await * 1000, millis) rescue nil
384
+ else
385
+ nil
386
+ end
351
387
  end
352
388
 
389
+ # @private
390
+ class Finalizer
391
+
392
+ def initialize
393
+ @_objs = []
394
+ end
395
+
396
+ def push(obj, args)
397
+ @_objs.unshift [java.lang.ref.WeakReference.new(obj), Array(args)]
398
+ end
399
+
400
+ def call(id = nil) # when called on finalization an object id arg is passed
401
+ @_objs.each { |obj, args| obj.get&.send(*args) rescue nil }
402
+ end
403
+
404
+ end
405
+ private_constant :Finalizer
406
+
353
407
  # Get at the underlying ExecutorService used to invoke asynchronous calls.
354
408
  def executor
355
- create_executor_if_needed
356
- @executor
409
+ @executor ||= create_executor
357
410
  end
358
411
 
359
- def self.shutdown_on_finalize(client, objs)
360
- ObjectSpace.define_finalizer client, -> {
361
- objs.each { |obj, args| obj.send(*args) rescue nil }
362
- }
412
+ # @private
413
+ def self.shutdown_on_finalize(client, finalizer)
414
+ ObjectSpace.define_finalizer(client, finalizer)
363
415
  end
364
416
 
365
417
  protected
@@ -367,8 +419,9 @@ module Manticore
367
419
  # Takes an object and a message to pass to the object to destroy it. This is done rather than
368
420
  # a proc to avoid creating a closure that would maintain a reference to this client, which
369
421
  # would prevent the client from being cleaned up.
422
+ # @private
370
423
  def finalize(object, args)
371
- @finalizers << [WeakRef.new(object), Array(args)]
424
+ @finalizer.push(object, args)
372
425
  end
373
426
 
374
427
  def url_as_regex(url)
@@ -388,7 +441,7 @@ module Manticore
388
441
 
389
442
  # :nocov:
390
443
  if options[:ignore_ssl_validation]
391
- $stderr.puts "The options[:ignore_ssl_validation] setting is deprecated in favor of options[:ssl][:verify]"
444
+ warn "The options[:ignore_ssl_validation] setting is deprecated in favor of options[:ssl][:verify]"
392
445
  options[:ssl] ||= {}
393
446
  options[:ssl] = {:verify => !options.delete(:ignore_ssl_validation)}.merge(options[:ssl])
394
447
  end
@@ -420,17 +473,16 @@ module Manticore
420
473
  socket_config_builder.build
421
474
  end
422
475
 
423
- def create_executor_if_needed
424
- return @executor if @executor
425
- @executor = Executors.new_cached_thread_pool(ExecutorThreadFactory.new)
426
- finalize @executor, :shutdown
476
+ def create_executor
477
+ executor = Executors.new_cached_thread_pool(ExecutorThreadFactory.new(self))
478
+ finalize executor, :shutdown
479
+ executor
427
480
  end
428
481
 
429
482
  def request(klass, url, options, &block)
430
483
  req, context = request_from_options(klass, url, options)
431
484
  async = options.delete(:async)
432
485
  background = options.delete(:async_background)
433
- create_executor_if_needed if (background || async)
434
486
  response = response_object_for(req, context, &block)
435
487
 
436
488
  if async
@@ -525,7 +577,6 @@ module Manticore
525
577
  end
526
578
 
527
579
  def get_proxy_host(opt)
528
- host = nil
529
580
  if opt.is_a? String
530
581
  uri = URI.parse(opt)
531
582
  if uri.host
@@ -610,23 +661,27 @@ module Manticore
610
661
 
611
662
  # Configure the SSL Context
612
663
  def ssl_socket_factory_from_options(ssl_options)
613
- trust_store = trust_strategy = nil
664
+ trust_strategy = nil
614
665
 
615
- case ssl_options.fetch(:verify, :strict)
616
- when false
617
- trust_store = nil
618
- trust_strategy = TrustSelfSignedStrategy::INSTANCE
619
- verifier = SSLConnectionSocketFactory::ALLOW_ALL_HOSTNAME_VERIFIER
620
- when :disable, :none
621
- trust_store = nil
666
+ case ssl_options.fetch(:verify, :default)
667
+ when :none, :disable
622
668
  trust_strategy = TrustAllStrategy::INSTANCE
669
+ verifier = NoopHostnameVerifier::INSTANCE
670
+ when false # compatibility
671
+ trust_strategy = TrustSelfSignedStrategy::INSTANCE
623
672
  verifier = SSLConnectionSocketFactory::ALLOW_ALL_HOSTNAME_VERIFIER
624
673
  when :browser
625
674
  verifier = SSLConnectionSocketFactory::BROWSER_COMPATIBLE_HOSTNAME_VERIFIER
626
- when true, :strict, :default
675
+ when :default, true
676
+ verifier = DefaultHostnameVerifier.new
677
+ when :strict # compatibility
627
678
  verifier = SSLConnectionSocketFactory::STRICT_HOSTNAME_VERIFIER
628
679
  else
629
- raise "Invalid value for :verify. Valid values are (:all, :browser, :default)"
680
+ raise "Invalid value for :verify. Valid values are (:default, :browser, :none)"
681
+ end
682
+
683
+ if ssl_options.include?(:trust_strategy)
684
+ trust_strategy = TrustStrategies.combine(trust_strategy, ssl_options.fetch(:trust_strategy))
630
685
  end
631
686
 
632
687
  context = SSLContextBuilder.new
@@ -650,7 +705,7 @@ module Manticore
650
705
  end
651
706
  end
652
707
 
653
- context.load_trust_material(trust_store, trust_strategy)
708
+ context.java_send :loadTrustMaterial, [KeyStore, TrustStrategy], trust_store, trust_strategy
654
709
  end
655
710
 
656
711
  KEY_EXTRACTION_REGEXP = /(?:^-----BEGIN(.* )PRIVATE KEY-----\n)(.*?)(?:-----END\1PRIVATE KEY.*$)/m
@@ -662,7 +717,6 @@ module Manticore
662
717
  # Support OpenSSL-style bare X.509 certs with an RSA key
663
718
  if ssl_options[:client_cert] && ssl_options[:client_key]
664
719
  key_store ||= blank_keystore
665
- certs, key = nil, nil
666
720
 
667
721
  cert_str = if ssl_options[:client_cert].is_a?(OpenSSL::X509::Certificate)
668
722
  ssl_options[:client_cert].to_s
@@ -701,7 +755,7 @@ module Manticore
701
755
  def get_store(prefix, options)
702
756
  KeyStore.get_instance(options[:"#{prefix}_type"] || guess_store_type(options[prefix])).tap do |store|
703
757
  instream = open(options[prefix], "rb").to_inputstream
704
- store.load(instream, options.fetch(:"#{prefix}_password", nil).to_java.toCharArray)
758
+ store.load(instream, options.fetch(:"#{prefix}_password", nil).to_java&.toCharArray)
705
759
  end
706
760
  end
707
761
 
@@ -1,3 +1,3 @@
1
1
  module Manticore
2
- VERSION = "0.8.0"
2
+ VERSION = "0.9.0"
3
3
  end
data/lib/manticore.rb CHANGED
@@ -67,6 +67,7 @@ module Manticore
67
67
 
68
68
  require_relative "./manticore/java_extensions"
69
69
  require_relative "./manticore/client/proxies"
70
+ require_relative "./manticore/client/trust_strategies"
70
71
  require_relative "./manticore/client"
71
72
  require_relative "./manticore/response"
72
73
  require_relative "./manticore/stubbed_response"
Binary file
data/manticore.gemspec CHANGED
@@ -29,8 +29,6 @@ Gem::Specification.new do |spec|
29
29
 
30
30
  spec.add_dependency "openssl_pkcs8_pure"
31
31
 
32
- spec.add_development_dependency "bundler"
33
- spec.add_development_dependency "rake"
34
32
  spec.add_development_dependency "jar-dependencies", "~> 0.4.1"
35
33
 
36
34
  spec.requirements << "jar org.apache.httpcomponents:httpclient, '~> 4.5.13'"
@@ -1,12 +1,11 @@
1
1
  # encoding: utf-8
2
2
  require "spec_helper"
3
3
 
4
- java_import "org.apache.http.entity.mime.MultipartEntityBuilder"
5
- java_import "org.apache.http.entity.ContentType"
6
-
7
4
  describe Manticore::Client do
8
5
  let(:client) { Manticore::Client.new }
9
6
 
7
+ after { client.close }
8
+
10
9
  it "fetches a URL and return a response" do
11
10
  expect(client.get(local_server)).to be_a Manticore::Response
12
11
  end
@@ -96,7 +95,7 @@ describe Manticore::Client do
96
95
 
97
96
  describe "ignore_ssl_validation (deprecated option)" do
98
97
  context "when on" do
99
- let(:client) { Manticore::Client.new ssl: {verify: false} }
98
+ let(:client) { Manticore::Client.new ssl: { verify: false } }
100
99
 
101
100
  it "does not break on SSL validation errors" do
102
101
  expect { client.get("https://localhost:55444/").body }.to_not raise_exception
@@ -104,7 +103,7 @@ describe Manticore::Client do
104
103
  end
105
104
 
106
105
  context "when off" do
107
- let(:client) { Manticore::Client.new ssl: {verify: true} }
106
+ let(:client) { Manticore::Client.new ssl: { verify: true } }
108
107
 
109
108
  it "breaks on SSL validation errors" do
110
109
  expect { client.get("https://localhost:55444/").call }.to raise_exception(Manticore::ClientProtocolException)
@@ -148,6 +147,29 @@ describe Manticore::Client do
148
147
  end
149
148
  end
150
149
 
150
+ context "when on and custom trust strategy is given" do
151
+ # let(:custom_trust_strategy) { Proc.new {|chain,type| true } }
152
+ let(:client) { Manticore::Client.new :ssl => {:verify => :strict, :trust_strategy => custom_trust_strategy} }
153
+ context 'and trust strategy approves the cert chain' do
154
+ let(:custom_trust_strategy) { Proc.new { |chain,type| true } }
155
+ it "verifies the request and succeed" do
156
+ expect { client.get("https://localhost:55444/").body }.to_not raise_exception
157
+ end
158
+ end
159
+ context 'and trust strategy does not approve the cert chain' do
160
+ let(:custom_trust_strategy) { Proc.new { |chain,type| false } }
161
+ it "breaks on SSL validation errors" do
162
+ begin
163
+ client.get("https://localhost:55445/").body
164
+ rescue Manticore::ClientProtocolException => e
165
+ expect( e.cause ).to be_a javax.net.ssl.SSLHandshakeException
166
+ else
167
+ fail "exception not raised"
168
+ end
169
+ end
170
+ end
171
+ end
172
+
151
173
  context "when the client specifies a protocol list" do
152
174
  let(:client) { Manticore::Client.new :ssl => {verify: :strict, truststore: File.expand_path("../../ssl/truststore.jks", __FILE__), truststore_password: "test123", protocols: ["TLSv1", "TLSv1.1", "TLSv1.2"]} }
153
175
 
@@ -157,10 +179,10 @@ describe Manticore::Client do
157
179
  end
158
180
 
159
181
  context "when on and custom trust store is given with the wrong password" do
160
- let(:client) { Manticore::Client.new :ssl => {verify: :strict, truststore: File.expand_path("../../ssl/truststore.jks", __FILE__), truststore_password: "wrongpass"} }
182
+ let(:ssl_opts) { { verify: :strict, truststore: File.expand_path("../../ssl/truststore.jks", __FILE__), truststore_password: "wrongpass" } }
161
183
 
162
184
  it "fails to load the keystore" do
163
- expect { client.get("https://localhost:55444/").body }.to raise_exception(Java::JavaIo::IOException)
185
+ expect { Manticore::Client.new(:ssl => ssl_opts) }.to raise_exception(Java::JavaIo::IOException)
164
186
  end
165
187
  end
166
188
 
@@ -193,7 +215,7 @@ describe Manticore::Client do
193
215
  let(:client) {
194
216
  Manticore::Client.new(
195
217
  :ssl => {
196
- verify: :strict,
218
+ verify: :default,
197
219
  ca_file: File.expand_path("../../ssl/root-ca.crt", __FILE__),
198
220
  client_cert: OpenSSL::X509::Certificate.new(File.read(File.expand_path("../../ssl/client.crt", __FILE__))),
199
221
  client_key: OpenSSL::PKey::RSA.new(File.read(File.expand_path("../../ssl/client.key", __FILE__))),
@@ -210,7 +232,7 @@ describe Manticore::Client do
210
232
  let(:client) {
211
233
  Manticore::Client.new(
212
234
  :ssl => {
213
- verify: :strict,
235
+ verify: :default,
214
236
  ca_file: File.expand_path("../../ssl/root-ca.crt", __FILE__),
215
237
  client_cert: File.read(File.expand_path("../../ssl/client.crt", __FILE__)),
216
238
  client_key: File.read(File.expand_path("../../ssl/client.key", __FILE__)),
@@ -237,6 +259,23 @@ describe Manticore::Client do
237
259
  it "does not break on untrusted certificates" do
238
260
  expect { client.get("https://localhost:55447/").body }.to_not raise_exception
239
261
  end
262
+
263
+ context "when custom trust strategy is given" do
264
+ # let(:custom_trust_strategy) { Proc.new {|chain,type| true } }
265
+ let(:client) { Manticore::Client.new :ssl => {:verify => :disable, :trust_strategy => custom_trust_strategy} }
266
+ context 'and trust strategy approves the cert chain' do
267
+ let(:custom_trust_strategy) { Proc.new { |chain,type| true } }
268
+ it "verifies the request and succeed" do
269
+ expect { client.get("https://localhost:55444/").body }.to_not raise_exception
270
+ end
271
+ end
272
+ context 'and trust strategy does not approve the cert chain' do
273
+ let(:custom_trust_strategy) { Proc.new { |chain,type| false } }
274
+ it "verifies the request and succeed" do
275
+ expect { client.get("https://localhost:55444/").body }.to_not raise_exception
276
+ end
277
+ end
278
+ end
240
279
  end
241
280
 
242
281
  context "against a server that verifies clients" do
@@ -276,11 +315,11 @@ describe Manticore::Client do
276
315
  end
277
316
 
278
317
  describe ":cipher_suites" do
279
- skip
318
+ skip 'TODO: someone should write the spec'
280
319
  end
281
320
 
282
321
  describe ":protocols" do
283
- skip
322
+ skip 'TODO: someone should write the spec'
284
323
  end
285
324
  end
286
325
 
@@ -535,7 +574,9 @@ describe Manticore::Client do
535
574
 
536
575
  it "sends an arbitrary entity" do
537
576
  f = open(File.expand_path(File.join(__FILE__, "..", "..", "spec_helper.rb")), "r").to_inputstream
538
- multipart_entity = MultipartEntityBuilder.create.add_text_body("foo", "bar").add_binary_body("whatever", f, ContentType::TEXT_PLAIN, __FILE__)
577
+ multipart_entity = org.apache.http.entity.mime.MultipartEntityBuilder.create.
578
+ add_text_body("foo", "bar").
579
+ add_binary_body("whatever", f, org.apache.http.entity.ContentType::TEXT_PLAIN, __FILE__)
539
580
  response = client.post(local_server, entity: multipart_entity.build)
540
581
  expect(response.body).to match "RSpec.configure"
541
582
  end
@@ -746,14 +787,13 @@ describe Manticore::Client do
746
787
  context "with a misbehaving endpoint" do
747
788
  let(:port) do
748
789
  p = 4000
749
- server = nil
750
790
  begin
751
791
  server = TCPServer.new p
752
792
  rescue Errno::EADDRINUSE
753
793
  p += 1
754
794
  retry
755
795
  ensure
756
- server.close
796
+ server&.close
757
797
  end
758
798
  p
759
799
  end
@@ -774,6 +814,7 @@ describe Manticore::Client do
774
814
  ].join("\n"))
775
815
  client.close
776
816
  rescue IOError => e
817
+ warn "caught an error: #{e.inspect}"
777
818
  break
778
819
  end
779
820
  end
@@ -0,0 +1,168 @@
1
+ # encoding: utf-8
2
+ require "spec_helper"
3
+ describe Manticore::Client::TrustStrategies do
4
+ describe '#coerce' do
5
+ subject(:coerced) { described_class.coerce(input) }
6
+ context 'with a nil value' do
7
+ let(:input) { nil }
8
+ it 'returns the value unchanged' do
9
+ expect(coerced).to be_nil
10
+ end
11
+ end
12
+ context 'with an implementation of org.apache.http.conn.ssl.TrustStrategy' do
13
+ let(:input) { org.apache.http.conn.ssl.TrustAllStrategy::INSTANCE }
14
+ it 'returns the value unchanged' do
15
+ expect(coerced).to be input
16
+ end
17
+ end
18
+ context 'with a Proc' do
19
+ let(:input) { ->(chain, type) { true } }
20
+ it 'wraps the proc in a `CustomTrustStrategy`' do
21
+ expect(Manticore::Client::CustomTrustStrategy).to receive(:new).with(input).and_call_original
22
+ expect(described_class.coerce(input)).to be_a_kind_of Manticore::Client::CustomTrustStrategy
23
+ end
24
+ end
25
+ end
26
+
27
+ describe '#combine' do
28
+ context 'when left-hand value is nil' do
29
+ let(:left_hand_strategy) { nil }
30
+ let(:right_hand_strategy) { described_class.coerce(->(chain,type){ true }) }
31
+ it 'returns the right-hand value coerced' do
32
+ expect(described_class).to receive(:coerce).with(right_hand_strategy).and_call_original
33
+ expect(described_class.combine(left_hand_strategy, right_hand_strategy)).to be right_hand_strategy
34
+ end
35
+ end
36
+ context 'when the right-hand value is nil' do
37
+ let(:left_hand_strategy) { described_class.coerce(->(chain,type){ true }) }
38
+ let(:right_hand_strategy) { nil }
39
+ it 'returns the left-hand value coerced' do
40
+ expect(described_class).to receive(:coerce).with(left_hand_strategy).and_call_original
41
+ expect(described_class.combine(left_hand_strategy, right_hand_strategy)).to be left_hand_strategy
42
+ end
43
+ end
44
+ context 'when neither value is nil' do
45
+ let(:left_hand_strategy) { described_class.coerce(->(chain,type){ true }) }
46
+ let(:right_hand_strategy) { described_class.coerce(->(chain,type){ true }) }
47
+
48
+ it 'returns a CombinedTrustStrategy' do
49
+ expect(Manticore::Client::CombinedTrustStrategy)
50
+ .to receive(:new).with(left_hand_strategy, right_hand_strategy).and_call_original
51
+
52
+ # ensures that the values are coerced.
53
+ expect(described_class).to receive(:coerce).with(left_hand_strategy).and_call_original
54
+ expect(described_class).to receive(:coerce).with(right_hand_strategy).and_call_original
55
+
56
+ combined = described_class.combine(left_hand_strategy, right_hand_strategy)
57
+ expect(combined).to be_a_kind_of Manticore::Client::CombinedTrustStrategy
58
+ end
59
+ end
60
+ context 'when both values are nil' do
61
+ let(:left_hand_strategy) { nil }
62
+ let(:right_hand_strategy) { nil }
63
+
64
+ it 'returns nil' do
65
+ expect(described_class.combine(left_hand_strategy, right_hand_strategy)).to be nil
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ describe Manticore::Client::CustomTrustStrategy do
72
+
73
+ subject(:custom_trust_strategy) { described_class.new(trust_strategy_proc) }
74
+
75
+ context 'when called via Java interface' do
76
+ def load_java_cert(file_path)
77
+ pem_contents = File.read(file_path)
78
+ cf = java.security.cert.CertificateFactory::getInstance("X.509")
79
+ is = java.io.ByteArrayInputStream.new(pem_contents.to_java_bytes)
80
+ cf.generateCertificate(is)
81
+ end
82
+
83
+ let(:java_host_cert) { load_java_cert(File.expand_path("../../ssl/host.crt", __FILE__)) }
84
+ let(:java_root_cert) { load_java_cert(File.expand_path("../../ssl/root-ca.crt", __FILE__)) }
85
+ let(:java_chain) { [java_host_cert, java_root_cert].to_java(java.security.cert.X509Certificate) }
86
+ let(:java_type) { java.lang.String.new("my_type".to_java_bytes) }
87
+
88
+ subject(:java_trust_strategy) { custom_trust_strategy.to_java(org.apache.http.conn.ssl.TrustStrategy) }
89
+
90
+ context 'when called with Java Certs and a Java String' do
91
+ let(:trust_strategy_proc) { ->(chain,type) { true } }
92
+ it 'yields an enum of equivalent Ruby certs and an equivalent Ruby String' do
93
+ expect(trust_strategy_proc).to receive(:call) do |chain, type|
94
+ expect(chain.to_a.length).to eq java_chain.length
95
+ chain.each_with_index do |cert, idx|
96
+ expect(cert).to be_a_kind_of OpenSSL::X509::Certificate
97
+ expect(cert.to_der).to eq String.from_java_bytes(java_chain[idx].encoded)
98
+ end
99
+ expect(type).to be_a_kind_of String
100
+ expect(type).to eq String.from_java_bytes(java_type.bytes)
101
+ end
102
+
103
+ expect(java_trust_strategy.isTrusted(java_chain, java_type)).to be true
104
+ end
105
+ end
106
+
107
+ context 'when the ruby block returns false' do
108
+ let(:trust_strategy_proc) { ->(chain,type) { false } }
109
+ it 'returns false' do
110
+ expect(java_trust_strategy.isTrusted(java_chain, java_type)).to be false
111
+ end
112
+ end
113
+
114
+ context 'when the ruby block returns true' do
115
+ let(:trust_strategy_proc) { ->(chain,type) { true } }
116
+ it 'returns true' do
117
+ expect(java_trust_strategy.isTrusted(java_chain, java_type)).to be true
118
+ end
119
+ end
120
+
121
+ context 'when the ruby block raises an exception' do
122
+ let(:trust_strategy_proc) { ->(chain, type) { fail(OpenSSL::X509::CertificateError, 'intentional') } }
123
+ it 'throws a CertificateException' do
124
+ expect {
125
+ java_trust_strategy.isTrusted(java_chain, java_type)
126
+ }.to raise_exception(java.security.cert.CertificateException)
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ describe Manticore::Client::CombinedTrustStrategy do
133
+ let(:always_trust_strategy) { ->(chain,type) { true } }
134
+ let(:never_trust_strategy) { ->(chain,type) { false } }
135
+
136
+ subject(:combined_trust_strategy) { Manticore::Client::TrustStrategies.combine(left_hand_strategy, right_hand_strategy) }
137
+
138
+ context 'when left-hand strategy trusts' do
139
+ let(:left_hand_strategy) { always_trust_strategy }
140
+ context 'when right-hand strategy trusts' do
141
+ let(:right_hand_strategy) { always_trust_strategy }
142
+ it 'trusts' do
143
+ expect(combined_trust_strategy.trusted?([],'ignored')).to be true
144
+ end
145
+ end
146
+ context 'when right-hand strategy does not trust' do
147
+ let(:right_hand_strategy) { never_trust_strategy }
148
+ it 'trusts' do
149
+ expect(combined_trust_strategy.trusted?([],'ignored')).to be true
150
+ end
151
+ end
152
+ end
153
+ context 'when left-hand strategy does not trust' do
154
+ let(:left_hand_strategy) { never_trust_strategy }
155
+ context 'when right-hand strategy trusts' do
156
+ let(:right_hand_strategy) { always_trust_strategy }
157
+ it 'trusts' do
158
+ expect(combined_trust_strategy.trusted?([],'ignored')).to be true
159
+ end
160
+ end
161
+ context 'when right-hand strategy does not trust' do
162
+ let(:right_hand_strategy) { never_trust_strategy }
163
+ it 'does not trust' do
164
+ expect(combined_trust_strategy.trusted?([],'ignored')).to be false
165
+ end
166
+ end
167
+ end
168
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: manticore
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: java
6
6
  authors:
7
7
  - Chris Heald
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-01-18 00:00:00.000000000 Z
11
+ date: 2022-06-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  requirement: !ruby/object:Gem::Requirement
@@ -24,34 +24,6 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
- - !ruby/object:Gem::Dependency
28
- requirement: !ruby/object:Gem::Requirement
29
- requirements:
30
- - - ">="
31
- - !ruby/object:Gem::Version
32
- version: '0'
33
- name: bundler
34
- prerelease: false
35
- type: :development
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
- - !ruby/object:Gem::Dependency
42
- requirement: !ruby/object:Gem::Requirement
43
- requirements:
44
- - - ">="
45
- - !ruby/object:Gem::Version
46
- version: '0'
47
- name: rake
48
- prerelease: false
49
- type: :development
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '0'
55
27
  - !ruby/object:Gem::Dependency
56
28
  requirement: !ruby/object:Gem::Requirement
57
29
  requirements:
@@ -92,6 +64,7 @@ files:
92
64
  - lib/manticore.rb
93
65
  - lib/manticore/client.rb
94
66
  - lib/manticore/client/proxies.rb
67
+ - lib/manticore/client/trust_strategies.rb
95
68
  - lib/manticore/cookie.rb
96
69
  - lib/manticore/facade.rb
97
70
  - lib/manticore/java_extensions.rb
@@ -106,6 +79,7 @@ files:
106
79
  - manticore.gemspec
107
80
  - spec/manticore/client_proxy_spec.rb
108
81
  - spec/manticore/client_spec.rb
82
+ - spec/manticore/client_trust_strategies_spec.rb
109
83
  - spec/manticore/cookie_spec.rb
110
84
  - spec/manticore/facade_spec.rb
111
85
  - spec/manticore/response_spec.rb
@@ -136,13 +110,14 @@ requirements:
136
110
  - jar commons-logging:commons-logging, '~> 1.2'
137
111
  - jar commons-codec:commons-codec, '~> 1.9'
138
112
  - jar org.apache.httpcomponents:httpcore, '~> 4.4.14'
139
- rubygems_version: 3.1.6
113
+ rubygems_version: 3.2.29
140
114
  signing_key:
141
115
  specification_version: 4
142
116
  summary: Manticore is an HTTP client built on the Apache HttpCore components
143
117
  test_files:
144
118
  - spec/manticore/client_proxy_spec.rb
145
119
  - spec/manticore/client_spec.rb
120
+ - spec/manticore/client_trust_strategies_spec.rb
146
121
  - spec/manticore/cookie_spec.rb
147
122
  - spec/manticore/facade_spec.rb
148
123
  - spec/manticore/response_spec.rb