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 +4 -4
- data/.travis.yml +6 -6
- data/CHANGELOG.md +8 -0
- data/Gemfile +9 -7
- data/lib/manticore/client/trust_strategies.rb +119 -0
- data/lib/manticore/client.rb +93 -39
- data/lib/manticore/version.rb +1 -1
- data/lib/manticore.rb +1 -0
- data/lib/org/manticore/manticore-ext.jar +0 -0
- data/manticore.gemspec +0 -2
- data/spec/manticore/client_spec.rb +55 -14
- data/spec/manticore/client_trust_strategies_spec.rb +168 -0
- metadata +6 -31
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 281400646207bd8e4b34bed53610787e5b5d222c72fe5e82dd1d42d8f071f1be
|
4
|
+
data.tar.gz: 421a4a8f1c4f8e06a7d12377812da2613877877904c73d3d8375348fbb9e2103
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 "
|
7
|
+
gem "rake-compiler", require: false
|
8
|
+
gem "simplecov"
|
9
|
+
|
8
10
|
gem "rspec", "~> 3.0"
|
9
11
|
gem "rspec-its"
|
10
|
-
|
11
|
-
gem "rack", ">= 2.1.4"
|
12
|
-
gem "
|
13
|
-
gem "
|
14
|
-
gem "
|
15
|
-
gem "
|
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
|
data/lib/manticore/client.rb
CHANGED
@@ -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
|
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 =
|
104
|
-
thread.
|
105
|
-
|
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] (:
|
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 `:
|
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
|
-
@
|
182
|
-
self.class.shutdown_on_finalize self, @
|
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
|
-
#
|
349
|
-
|
350
|
-
|
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
|
-
|
356
|
-
@executor
|
409
|
+
@executor ||= create_executor
|
357
410
|
end
|
358
411
|
|
359
|
-
|
360
|
-
|
361
|
-
|
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
|
-
@
|
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
|
-
|
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
|
424
|
-
|
425
|
-
|
426
|
-
|
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
|
-
|
664
|
+
trust_strategy = nil
|
614
665
|
|
615
|
-
case ssl_options.fetch(:verify, :
|
616
|
-
when
|
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
|
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 (:
|
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.
|
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
|
758
|
+
store.load(instream, options.fetch(:"#{prefix}_password", nil).to_java&.toCharArray)
|
705
759
|
end
|
706
760
|
end
|
707
761
|
|
data/lib/manticore/version.rb
CHANGED
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(:
|
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 {
|
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: :
|
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: :
|
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.
|
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
|
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.
|
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-
|
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.
|
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
|