httpclient 2.1.5 → 2.8.3
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 +7 -0
- data/README.md +85 -0
- data/bin/httpclient +77 -0
- data/bin/jsonclient +85 -0
- data/lib/hexdump.rb +50 -0
- data/lib/http-access2.rb +6 -4
- data/lib/httpclient/auth.rb +575 -173
- data/lib/httpclient/cacert.pem +3952 -0
- data/lib/httpclient/cacert1024.pem +3866 -0
- data/lib/httpclient/connection.rb +6 -2
- data/lib/httpclient/cookie.rb +162 -504
- data/lib/httpclient/http.rb +334 -119
- data/lib/httpclient/include_client.rb +85 -0
- data/lib/httpclient/jruby_ssl_socket.rb +588 -0
- data/lib/httpclient/session.rb +385 -288
- data/lib/httpclient/ssl_config.rb +195 -155
- data/lib/httpclient/ssl_socket.rb +150 -0
- data/lib/httpclient/timeout.rb +14 -10
- data/lib/httpclient/util.rb +142 -6
- data/lib/httpclient/version.rb +3 -0
- data/lib/httpclient/webagent-cookie.rb +459 -0
- data/lib/httpclient.rb +509 -202
- data/lib/jsonclient.rb +63 -0
- data/lib/oauthclient.rb +111 -0
- data/sample/async.rb +8 -0
- data/sample/auth.rb +11 -0
- data/sample/cookie.rb +18 -0
- data/sample/dav.rb +103 -0
- data/sample/howto.rb +49 -0
- data/sample/jsonclient.rb +67 -0
- data/sample/oauth_buzz.rb +57 -0
- data/sample/oauth_friendfeed.rb +59 -0
- data/sample/oauth_twitter.rb +61 -0
- data/sample/ssl/0cert.pem +22 -0
- data/sample/ssl/0key.pem +30 -0
- data/sample/ssl/1000cert.pem +19 -0
- data/sample/ssl/1000key.pem +18 -0
- data/sample/ssl/htdocs/index.html +10 -0
- data/sample/ssl/ssl_client.rb +22 -0
- data/sample/ssl/webrick_httpsd.rb +29 -0
- data/sample/stream.rb +21 -0
- data/sample/thread.rb +27 -0
- data/sample/wcat.rb +21 -0
- data/test/ca-chain.pem +44 -0
- data/test/ca.cert +23 -0
- data/test/client-pass.key +18 -0
- data/test/client.cert +19 -0
- data/test/client.key +15 -0
- data/test/helper.rb +131 -0
- data/test/htdigest +1 -0
- data/test/htpasswd +2 -0
- data/test/jruby_ssl_socket/test_pemutils.rb +32 -0
- data/test/runner.rb +2 -0
- data/test/server.cert +19 -0
- data/test/server.key +15 -0
- data/test/sslsvr.rb +65 -0
- data/test/subca.cert +21 -0
- data/test/test_auth.rb +492 -0
- data/test/test_cookie.rb +309 -0
- data/test/test_hexdump.rb +14 -0
- data/test/test_http-access2.rb +508 -0
- data/test/test_httpclient.rb +2145 -0
- data/test/test_include_client.rb +52 -0
- data/test/test_jsonclient.rb +80 -0
- data/test/test_ssl.rb +559 -0
- data/test/test_webagent-cookie.rb +465 -0
- metadata +85 -44
- data/lib/httpclient/auth.rb.orig +0 -513
- data/lib/httpclient/cacert.p7s +0 -1579
- data/lib/httpclient.rb.orig +0 -1020
- data/lib/tags +0 -908
@@ -0,0 +1,85 @@
|
|
1
|
+
# It is useful to re-use a HTTPClient instance for multiple requests, to
|
2
|
+
# re-use HTTP 1.1 persistent connections.
|
3
|
+
#
|
4
|
+
# To do that, you sometimes want to store an HTTPClient instance in a global/
|
5
|
+
# class variable location, so it can be accessed and re-used.
|
6
|
+
#
|
7
|
+
# This mix-in makes it easy to create class-level access to one or more
|
8
|
+
# HTTPClient instances. The HTTPClient instances are lazily initialized
|
9
|
+
# on first use (to, for instance, avoid interfering with WebMock/VCR),
|
10
|
+
# and are initialized in a thread-safe manner. Note that a
|
11
|
+
# HTTPClient, once initialized, is safe for use in multiple threads.
|
12
|
+
#
|
13
|
+
# Note that you `extend` HTTPClient::IncludeClient, not `include.
|
14
|
+
#
|
15
|
+
# require 'httpclient/include_client'
|
16
|
+
# class Widget
|
17
|
+
# extend HTTPClient::IncludeClient
|
18
|
+
#
|
19
|
+
# include_http_client
|
20
|
+
# # and/or, specify more stuff
|
21
|
+
# include_http_client('http://myproxy:8080', :method_name => :my_client) do |client|
|
22
|
+
# # any init you want
|
23
|
+
# client.set_cookie_store nil
|
24
|
+
# client.
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# That creates two HTTPClient instances available at the class level.
|
29
|
+
# The first will be available from Widget.http_client (default method
|
30
|
+
# name for `include_http_client`), with default initialization.
|
31
|
+
#
|
32
|
+
# The second will be available at Widget.my_client, with the init arguments
|
33
|
+
# provided, further initialized by the block provided.
|
34
|
+
#
|
35
|
+
# In addition to a class-level method, for convenience instance-level methods
|
36
|
+
# are also provided. Widget.http_client is identical to Widget.new.http_client
|
37
|
+
#
|
38
|
+
#
|
39
|
+
require 'httpclient'
|
40
|
+
|
41
|
+
class HTTPClient
|
42
|
+
module IncludeClient
|
43
|
+
|
44
|
+
|
45
|
+
def include_http_client(*args, &block)
|
46
|
+
# We're going to dynamically define a class
|
47
|
+
# to hold our state, namespaced, as well as possibly dynamic
|
48
|
+
# name of cover method.
|
49
|
+
method_name = (args.last.delete(:method_name) if args.last.kind_of? Hash) || :http_client
|
50
|
+
args.pop if args.last == {} # if last arg was named methods now empty, remove it.
|
51
|
+
|
52
|
+
# By the amazingness of closures, we can create these things
|
53
|
+
# in local vars here and use em in our method, we don't even
|
54
|
+
# need iVars for state.
|
55
|
+
client_instance = nil
|
56
|
+
client_mutex = Mutex.new
|
57
|
+
client_args = args
|
58
|
+
client_block = block
|
59
|
+
|
60
|
+
# to define a _class method_ on the specific class that's currently
|
61
|
+
# `self`, we have to use this bit of metaprogramming, sorry.
|
62
|
+
(class << self; self ; end).instance_eval do
|
63
|
+
define_method(method_name) do
|
64
|
+
# implementation copied from ruby stdlib singleton
|
65
|
+
# to create this global obj thread-safely.
|
66
|
+
return client_instance if client_instance
|
67
|
+
client_mutex.synchronize do
|
68
|
+
return client_instance if client_instance
|
69
|
+
# init HTTPClient with specified args/block
|
70
|
+
client_instance = HTTPClient.new(*client_args)
|
71
|
+
client_block.call(client_instance) if client_block
|
72
|
+
end
|
73
|
+
return client_instance
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# And for convenience, an _instance method_ on the class that just
|
78
|
+
# delegates to the class method.
|
79
|
+
define_method(method_name) do
|
80
|
+
self.class.send(method_name)
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,588 @@
|
|
1
|
+
# HTTPClient - HTTP client library.
|
2
|
+
# Copyright (C) 2000-2015 NAKAMURA, Hiroshi <nahi@ruby-lang.org>.
|
3
|
+
#
|
4
|
+
# This program is copyrighted free software by NAKAMURA, Hiroshi. You can
|
5
|
+
# redistribute it and/or modify it under the same terms of Ruby's license;
|
6
|
+
# either the dual license version in 2003, or any later version.
|
7
|
+
|
8
|
+
|
9
|
+
require 'java'
|
10
|
+
require 'httpclient/ssl_config'
|
11
|
+
|
12
|
+
|
13
|
+
class HTTPClient
|
14
|
+
|
15
|
+
unless defined?(SSLSocket)
|
16
|
+
|
17
|
+
class JavaSocketWrap
|
18
|
+
java_import 'java.net.InetSocketAddress'
|
19
|
+
java_import 'java.io.BufferedInputStream'
|
20
|
+
|
21
|
+
BUF_SIZE = 1024 * 16
|
22
|
+
|
23
|
+
def self.connect(socket, site, opts = {})
|
24
|
+
socket_addr = InetSocketAddress.new(site.host, site.port)
|
25
|
+
if opts[:connect_timeout]
|
26
|
+
socket.connect(socket_addr, opts[:connect_timeout])
|
27
|
+
else
|
28
|
+
socket.connect(socket_addr)
|
29
|
+
end
|
30
|
+
socket.setSoTimeout(opts[:so_timeout]) if opts[:so_timeout]
|
31
|
+
socket.setKeepAlive(true) if opts[:tcp_keepalive]
|
32
|
+
socket
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize(socket, debug_dev = nil)
|
36
|
+
@socket = socket
|
37
|
+
@debug_dev = debug_dev
|
38
|
+
@outstr = @socket.getOutputStream
|
39
|
+
@instr = BufferedInputStream.new(@socket.getInputStream)
|
40
|
+
@buf = (' ' * BUF_SIZE).to_java_bytes
|
41
|
+
@bufstr = ''
|
42
|
+
end
|
43
|
+
|
44
|
+
def close
|
45
|
+
@socket.close
|
46
|
+
end
|
47
|
+
|
48
|
+
def closed?
|
49
|
+
@socket.isClosed
|
50
|
+
end
|
51
|
+
|
52
|
+
def eof?
|
53
|
+
@socket.isClosed
|
54
|
+
end
|
55
|
+
|
56
|
+
def gets(rs)
|
57
|
+
while (size = @bufstr.index(rs)).nil?
|
58
|
+
if fill() == -1
|
59
|
+
size = @bufstr.size
|
60
|
+
break
|
61
|
+
end
|
62
|
+
end
|
63
|
+
str = @bufstr.slice!(0, size + rs.size)
|
64
|
+
debug(str)
|
65
|
+
str
|
66
|
+
end
|
67
|
+
|
68
|
+
def read(size, buf = nil)
|
69
|
+
while @bufstr.size < size
|
70
|
+
if fill() == -1
|
71
|
+
break
|
72
|
+
end
|
73
|
+
end
|
74
|
+
str = @bufstr.slice!(0, size)
|
75
|
+
debug(str)
|
76
|
+
if buf
|
77
|
+
buf.replace(str)
|
78
|
+
else
|
79
|
+
str
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def readpartial(size, buf = nil)
|
84
|
+
while @bufstr.size == 0
|
85
|
+
if fill() == -1
|
86
|
+
raise EOFError.new('end of file reached')
|
87
|
+
end
|
88
|
+
end
|
89
|
+
str = @bufstr.slice!(0, size)
|
90
|
+
debug(str)
|
91
|
+
if buf
|
92
|
+
buf.replace(str)
|
93
|
+
else
|
94
|
+
str
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def <<(str)
|
99
|
+
rv = @outstr.write(str.to_java_bytes)
|
100
|
+
debug(str)
|
101
|
+
rv
|
102
|
+
end
|
103
|
+
|
104
|
+
def flush
|
105
|
+
@socket.flush
|
106
|
+
end
|
107
|
+
|
108
|
+
def sync
|
109
|
+
true
|
110
|
+
end
|
111
|
+
|
112
|
+
def sync=(sync)
|
113
|
+
unless sync
|
114
|
+
raise "sync = false is not supported. This option was introduced for backward compatibility just in case."
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def fill
|
121
|
+
begin
|
122
|
+
size = @instr.read(@buf)
|
123
|
+
if size > 0
|
124
|
+
@bufstr << String.from_java_bytes(@buf, Encoding::BINARY)[0, size]
|
125
|
+
end
|
126
|
+
size
|
127
|
+
rescue java.io.IOException => e
|
128
|
+
raise OpenSSL::SSL::SSLError.new("#{e.class}: #{e.getMessage}")
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def debug(str)
|
133
|
+
@debug_dev << str if @debug_dev && str
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
class JRubySSLSocket < JavaSocketWrap
|
138
|
+
java_import 'java.io.ByteArrayInputStream'
|
139
|
+
java_import 'java.io.InputStreamReader'
|
140
|
+
java_import 'java.net.Socket'
|
141
|
+
java_import 'java.security.KeyStore'
|
142
|
+
java_import 'java.security.cert.Certificate'
|
143
|
+
java_import 'java.security.cert.CertificateFactory'
|
144
|
+
java_import 'javax.net.ssl.KeyManagerFactory'
|
145
|
+
java_import 'javax.net.ssl.SSLContext'
|
146
|
+
java_import 'javax.net.ssl.SSLSocketFactory'
|
147
|
+
java_import 'javax.net.ssl.TrustManager'
|
148
|
+
java_import 'javax.net.ssl.TrustManagerFactory'
|
149
|
+
java_import 'javax.net.ssl.X509TrustManager'
|
150
|
+
java_import 'org.jruby.ext.openssl.x509store.PEMInputOutput'
|
151
|
+
|
152
|
+
class JavaCertificate
|
153
|
+
attr_reader :cert
|
154
|
+
|
155
|
+
def initialize(cert)
|
156
|
+
@cert = cert
|
157
|
+
end
|
158
|
+
|
159
|
+
def subject
|
160
|
+
@cert.getSubjectDN
|
161
|
+
end
|
162
|
+
|
163
|
+
def to_text
|
164
|
+
@cert.toString
|
165
|
+
end
|
166
|
+
|
167
|
+
def to_pem
|
168
|
+
'(not in PEM format)'
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
class SSLStoreContext
|
173
|
+
attr_reader :current_cert, :chain, :error_depth, :error, :error_string
|
174
|
+
|
175
|
+
def initialize(current_cert, chain, error_depth, error, error_string)
|
176
|
+
@current_cert, @chain, @error_depth, @error, @error_string =
|
177
|
+
current_cert, chain, error_depth, error, error_string
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
class JSSEVerifyCallback
|
182
|
+
def initialize(verify_callback)
|
183
|
+
@verify_callback = verify_callback
|
184
|
+
end
|
185
|
+
|
186
|
+
def call(is_ok, chain, error_depth = -1, error = -1, error_string = '(unknown)')
|
187
|
+
if @verify_callback
|
188
|
+
ruby_chain = chain.map { |cert|
|
189
|
+
JavaCertificate.new(cert)
|
190
|
+
}.reverse
|
191
|
+
# NOTE: The order depends on provider implementation
|
192
|
+
ruby_chain.each do |cert|
|
193
|
+
is_ok = @verify_callback.call(
|
194
|
+
is_ok,
|
195
|
+
SSLStoreContext.new(cert, ruby_chain, error_depth, error, error_string)
|
196
|
+
)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
is_ok
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
class VerifyNoneTrustManagerFactory
|
204
|
+
class VerifyNoneTrustManager
|
205
|
+
include X509TrustManager
|
206
|
+
|
207
|
+
def initialize(verify_callback)
|
208
|
+
@verify_callback = JSSEVerifyCallback.new(verify_callback)
|
209
|
+
end
|
210
|
+
|
211
|
+
def checkServerTrusted(chain, authType)
|
212
|
+
@verify_callback.call(true, chain)
|
213
|
+
end
|
214
|
+
|
215
|
+
def checkClientTrusted(chain, authType); end
|
216
|
+
def getAcceptedIssuers; end
|
217
|
+
end
|
218
|
+
|
219
|
+
def initialize(verify_callback = nil)
|
220
|
+
@verify_callback = verify_callback
|
221
|
+
end
|
222
|
+
|
223
|
+
def init(trustStore)
|
224
|
+
@managers = [VerifyNoneTrustManager.new(@verify_callback)].to_java(X509TrustManager)
|
225
|
+
end
|
226
|
+
|
227
|
+
def getTrustManagers
|
228
|
+
@managers
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
class SystemTrustManagerFactory
|
233
|
+
class SystemTrustManager
|
234
|
+
include X509TrustManager
|
235
|
+
|
236
|
+
def initialize(original, verify_callback)
|
237
|
+
@original = original
|
238
|
+
@verify_callback = JSSEVerifyCallback.new(verify_callback)
|
239
|
+
end
|
240
|
+
|
241
|
+
def checkServerTrusted(chain, authType)
|
242
|
+
is_ok = false
|
243
|
+
excn = nil
|
244
|
+
# TODO can we detect the depth from excn?
|
245
|
+
error_depth = -1
|
246
|
+
error = nil
|
247
|
+
error_message = nil
|
248
|
+
begin
|
249
|
+
@original.checkServerTrusted(chain, authType)
|
250
|
+
is_ok = true
|
251
|
+
rescue java.security.cert.CertificateException => excn
|
252
|
+
is_ok = false
|
253
|
+
error = excn.class.name
|
254
|
+
error_message = excn.getMessage
|
255
|
+
end
|
256
|
+
is_ok = @verify_callback.call(is_ok, chain, error_depth, error, error_message)
|
257
|
+
unless is_ok
|
258
|
+
excn ||= OpenSSL::SSL::SSLError.new('verifycallback failed')
|
259
|
+
raise excn
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
def checkClientTrusted(chain, authType); end
|
264
|
+
def getAcceptedIssuers; end
|
265
|
+
end
|
266
|
+
|
267
|
+
def initialize(verify_callback = nil)
|
268
|
+
@verify_callback = verify_callback
|
269
|
+
end
|
270
|
+
|
271
|
+
def init(trust_store)
|
272
|
+
tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm)
|
273
|
+
tmf.java_method(:init, [KeyStore]).call(trust_store)
|
274
|
+
@original = tmf.getTrustManagers.find { |tm|
|
275
|
+
tm.is_a?(X509TrustManager)
|
276
|
+
}
|
277
|
+
@managers = [SystemTrustManager.new(@original, @verify_callback)].to_java(X509TrustManager)
|
278
|
+
end
|
279
|
+
|
280
|
+
def getTrustManagers
|
281
|
+
@managers
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
module PEMUtils
|
286
|
+
def self.read_certificate(pem)
|
287
|
+
cert = pem.sub(/.*?-----BEGIN CERTIFICATE-----/m, '').sub(/-----END CERTIFICATE-----.*?/m, '')
|
288
|
+
der = cert.unpack('m*').first
|
289
|
+
cf = CertificateFactory.getInstance('X.509')
|
290
|
+
cf.generateCertificate(ByteArrayInputStream.new(der.to_java_bytes))
|
291
|
+
end
|
292
|
+
|
293
|
+
def self.read_private_key(pem, password)
|
294
|
+
if password
|
295
|
+
password = password.unpack('C*').to_java(:char)
|
296
|
+
end
|
297
|
+
PEMInputOutput.read_private_key(InputStreamReader.new(ByteArrayInputStream.new(pem.to_java_bytes)), password)
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
class KeyStoreLoader
|
302
|
+
PASSWORD = 16.times.map { rand(256) }.to_java(:char)
|
303
|
+
|
304
|
+
def initialize
|
305
|
+
@keystore = KeyStore.getInstance('JKS')
|
306
|
+
@keystore.load(nil)
|
307
|
+
end
|
308
|
+
|
309
|
+
def add(cert_source, key_source, password)
|
310
|
+
cert_str = cert_source.respond_to?(:to_pem) ? cert_source.to_pem : File.read(cert_source.to_s)
|
311
|
+
cert = PEMUtils.read_certificate(cert_str)
|
312
|
+
@keystore.setCertificateEntry('client_cert', cert)
|
313
|
+
key_str = key_source.respond_to?(:to_pem) ? key_source.to_pem : File.read(key_source.to_s)
|
314
|
+
key_pair = PEMUtils.read_private_key(key_str, password)
|
315
|
+
@keystore.setKeyEntry('client_key', key_pair.getPrivate, PASSWORD, [cert].to_java(Certificate))
|
316
|
+
end
|
317
|
+
|
318
|
+
def keystore
|
319
|
+
@keystore
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
class TrustStoreLoader
|
324
|
+
attr_reader :size
|
325
|
+
|
326
|
+
def initialize
|
327
|
+
@trust_store = KeyStore.getInstance('JKS')
|
328
|
+
@trust_store.load(nil)
|
329
|
+
@size = 0
|
330
|
+
end
|
331
|
+
|
332
|
+
def add(cert_source)
|
333
|
+
return if cert_source == :default
|
334
|
+
if cert_source.respond_to?(:to_pem)
|
335
|
+
pem = cert_source.to_pem
|
336
|
+
load_pem(pem)
|
337
|
+
elsif File.directory?(cert_source)
|
338
|
+
warn("#{cert_source}: directory not yet supported")
|
339
|
+
return
|
340
|
+
else
|
341
|
+
pem = nil
|
342
|
+
File.read(cert_source).each_line do |line|
|
343
|
+
case line
|
344
|
+
when /-----BEGIN CERTIFICATE-----/
|
345
|
+
pem = ''
|
346
|
+
when /-----END CERTIFICATE-----/
|
347
|
+
load_pem(pem)
|
348
|
+
# keep parsing in case where multiple certificates in a file
|
349
|
+
else
|
350
|
+
if pem
|
351
|
+
pem << line
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
def trust_store
|
359
|
+
if @size == 0
|
360
|
+
nil
|
361
|
+
else
|
362
|
+
@trust_store
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
private
|
367
|
+
|
368
|
+
def load_pem(pem)
|
369
|
+
cert = PEMUtils.read_certificate(pem)
|
370
|
+
@size += 1
|
371
|
+
@trust_store.setCertificateEntry("cert_#{@size}", cert)
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
# Ported from commons-httpclient 'BrowserCompatHostnameVerifier'
|
376
|
+
class BrowserCompatHostnameVerifier
|
377
|
+
BAD_COUNTRY_2LDS = %w(ac co com ed edu go gouv gov info lg ne net or org).sort
|
378
|
+
require 'ipaddr'
|
379
|
+
|
380
|
+
def extract_sans(cert, subject_type)
|
381
|
+
sans = cert.getSubjectAlternativeNames rescue nil
|
382
|
+
if sans.nil?
|
383
|
+
return nil
|
384
|
+
end
|
385
|
+
sans.find_all { |san|
|
386
|
+
san.first.to_i == subject_type
|
387
|
+
}.map { |san|
|
388
|
+
san[1]
|
389
|
+
}
|
390
|
+
end
|
391
|
+
|
392
|
+
def extract_cn(cert)
|
393
|
+
subject = cert.getSubjectX500Principal()
|
394
|
+
if subject
|
395
|
+
subject_dn = javax.naming.ldap.LdapName.new(subject.toString)
|
396
|
+
subject_dn.getRdns.to_a.reverse.each do |rdn|
|
397
|
+
attributes = rdn.toAttributes
|
398
|
+
cn = attributes.get('cn')
|
399
|
+
if cn
|
400
|
+
if value = cn.get
|
401
|
+
return value.to_s
|
402
|
+
end
|
403
|
+
end
|
404
|
+
end
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
def ipaddr?(addr)
|
409
|
+
!(IPAddr.new(addr) rescue nil).nil?
|
410
|
+
end
|
411
|
+
|
412
|
+
def verify(hostname, cert)
|
413
|
+
is_ipaddr = ipaddr?(hostname)
|
414
|
+
sans = extract_sans(cert, is_ipaddr ? 7 : 2)
|
415
|
+
cn = extract_cn(cert)
|
416
|
+
if sans
|
417
|
+
sans.each do |san|
|
418
|
+
return true if match_identify(hostname, san)
|
419
|
+
end
|
420
|
+
raise OpenSSL::SSL::SSLError.new("Certificate for <#{hostname}> doesn't match any of the subject alternative names: #{sans}")
|
421
|
+
elsif cn
|
422
|
+
return true if match_identify(hostname, cn)
|
423
|
+
raise OpenSSL::SSL::SSLError.new("Certificate for <#{hostname}> doesn't match common name of the certificate subject: #{cn}")
|
424
|
+
end
|
425
|
+
raise OpenSSL::SSL::SSLError.new("Certificate subject for for <#{hostname}> doesn't contain a common name and does not have alternative names")
|
426
|
+
end
|
427
|
+
|
428
|
+
def match_identify(hostname, identity)
|
429
|
+
if hostname.nil?
|
430
|
+
return false
|
431
|
+
end
|
432
|
+
hostname = hostname.downcase
|
433
|
+
identity = identity.downcase
|
434
|
+
parts = identity.split('.')
|
435
|
+
if parts.length >= 3 && parts.first.end_with?('*') && valid_country_wildcard(parts)
|
436
|
+
create_wildcard_regexp(identity) =~ hostname
|
437
|
+
else
|
438
|
+
hostname == identity
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
def create_wildcard_regexp(value)
|
443
|
+
# Escape first then search '\*' for meta-char interpolation
|
444
|
+
labels = value.split('.').map { |e| Regexp.escape(e) }
|
445
|
+
# Handle '*'s only at the left-most label, exclude A-label and U-label
|
446
|
+
labels[0].gsub!(/\\\*/, '[^.]+') if !labels[0].start_with?('xn\-\-') and labels[0].ascii_only?
|
447
|
+
/\A#{labels.join('\.')}\z/i
|
448
|
+
end
|
449
|
+
|
450
|
+
def valid_country_wildcard(parts)
|
451
|
+
if parts.length != 3 || parts[2].length != 2
|
452
|
+
true
|
453
|
+
else
|
454
|
+
!BAD_COUNTRY_2LDS.include?(parts[1])
|
455
|
+
end
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
def self.create_socket(session)
|
460
|
+
opts = {
|
461
|
+
:connect_timeout => session.connect_timeout * 1000,
|
462
|
+
# send_timeout is ignored in JRuby
|
463
|
+
:so_timeout => session.receive_timeout * 1000,
|
464
|
+
:tcp_keepalive => session.tcp_keepalive,
|
465
|
+
:debug_dev => session.debug_dev
|
466
|
+
}
|
467
|
+
socket = nil
|
468
|
+
begin
|
469
|
+
if session.proxy
|
470
|
+
site = session.proxy || session.dest
|
471
|
+
socket = JavaSocketWrap.connect(Socket.new, site, opts)
|
472
|
+
session.connect_ssl_proxy(JavaSocketWrap.new(socket), Util.urify(session.dest.to_s))
|
473
|
+
end
|
474
|
+
new(socket, session.dest, session.ssl_config, opts)
|
475
|
+
rescue
|
476
|
+
socket.close if socket
|
477
|
+
raise
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
DEFAULT_SSL_PROTOCOL = (java.lang.System.getProperty('java.specification.version') == '1.7') ? 'TLSv1.2' : 'TLS'
|
482
|
+
def initialize(socket, dest, config, opts = {})
|
483
|
+
@config = config
|
484
|
+
begin
|
485
|
+
@ssl_socket = create_ssl_socket(socket, dest, config, opts)
|
486
|
+
ssl_version = java_ssl_version(config)
|
487
|
+
@ssl_socket.setEnabledProtocols([ssl_version].to_java(java.lang.String)) if ssl_version != DEFAULT_SSL_PROTOCOL
|
488
|
+
if config.ciphers != SSLConfig::CIPHERS_DEFAULT
|
489
|
+
@ssl_socket.setEnabledCipherSuites(config.ciphers.to_java(java.lang.String))
|
490
|
+
end
|
491
|
+
ssl_connect(dest.host)
|
492
|
+
rescue java.security.GeneralSecurityException => e
|
493
|
+
raise OpenSSL::SSL::SSLError.new(e.getMessage)
|
494
|
+
rescue java.io.IOException => e
|
495
|
+
raise OpenSSL::SSL::SSLError.new("#{e.class}: #{e.getMessage}")
|
496
|
+
end
|
497
|
+
|
498
|
+
super(@ssl_socket, opts[:debug_dev])
|
499
|
+
end
|
500
|
+
|
501
|
+
def java_ssl_version(config)
|
502
|
+
if config.ssl_version == :auto
|
503
|
+
DEFAULT_SSL_PROTOCOL
|
504
|
+
else
|
505
|
+
config.ssl_version.to_s.tr('_', '.')
|
506
|
+
end
|
507
|
+
end
|
508
|
+
|
509
|
+
def create_ssl_context(config)
|
510
|
+
unless config.cert_store_crl_items.empty?
|
511
|
+
raise NotImplementedError.new('Manual CRL configuration is not yet supported')
|
512
|
+
end
|
513
|
+
|
514
|
+
km = nil
|
515
|
+
if config.client_cert && config.client_key
|
516
|
+
loader = KeyStoreLoader.new
|
517
|
+
loader.add(config.client_cert, config.client_key, config.client_key_pass)
|
518
|
+
kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm)
|
519
|
+
kmf.init(loader.keystore, KeyStoreLoader::PASSWORD)
|
520
|
+
km = kmf.getKeyManagers
|
521
|
+
end
|
522
|
+
|
523
|
+
trust_store = nil
|
524
|
+
verify_callback = config.verify_callback || config.method(:default_verify_callback)
|
525
|
+
if !config.verify?
|
526
|
+
tmf = VerifyNoneTrustManagerFactory.new(verify_callback)
|
527
|
+
else
|
528
|
+
tmf = SystemTrustManagerFactory.new(verify_callback)
|
529
|
+
loader = TrustStoreLoader.new
|
530
|
+
config.cert_store_items.each do |item|
|
531
|
+
loader.add(item)
|
532
|
+
end
|
533
|
+
trust_store = loader.trust_store
|
534
|
+
end
|
535
|
+
tmf.init(trust_store)
|
536
|
+
tm = tmf.getTrustManagers
|
537
|
+
|
538
|
+
ctx = SSLContext.getInstance(java_ssl_version(config))
|
539
|
+
ctx.init(km, tm, nil)
|
540
|
+
if config.timeout
|
541
|
+
ctx.getClientSessionContext.setSessionTimeout(config.timeout)
|
542
|
+
end
|
543
|
+
ctx
|
544
|
+
end
|
545
|
+
|
546
|
+
def create_ssl_socket(socket, dest, config, opts)
|
547
|
+
ctx = create_ssl_context(config)
|
548
|
+
factory = ctx.getSocketFactory
|
549
|
+
if socket
|
550
|
+
ssl_socket = factory.createSocket(socket, dest.host, dest.port, true)
|
551
|
+
else
|
552
|
+
ssl_socket = factory.createSocket
|
553
|
+
JavaSocketWrap.connect(ssl_socket, dest, opts)
|
554
|
+
end
|
555
|
+
ssl_socket
|
556
|
+
end
|
557
|
+
|
558
|
+
def peer_cert
|
559
|
+
@peer_cert
|
560
|
+
end
|
561
|
+
|
562
|
+
private
|
563
|
+
|
564
|
+
def ssl_connect(hostname)
|
565
|
+
@ssl_socket.startHandshake
|
566
|
+
ssl_session = @ssl_socket.getSession
|
567
|
+
@peer_cert = JavaCertificate.new(ssl_session.getPeerCertificates.first)
|
568
|
+
if $DEBUG
|
569
|
+
warn("Protocol version: #{ssl_session.getProtocol}")
|
570
|
+
warn("Cipher: #{@ssl_socket.getSession.getCipherSuite}")
|
571
|
+
end
|
572
|
+
post_connection_check(hostname)
|
573
|
+
end
|
574
|
+
|
575
|
+
def post_connection_check(hostname)
|
576
|
+
if !@config.verify?
|
577
|
+
return
|
578
|
+
else
|
579
|
+
BrowserCompatHostnameVerifier.new.verify(hostname, @peer_cert.cert)
|
580
|
+
end
|
581
|
+
end
|
582
|
+
end
|
583
|
+
|
584
|
+
SSLSocket = JRubySSLSocket
|
585
|
+
|
586
|
+
end
|
587
|
+
|
588
|
+
end
|