httpclient 2.6.0.1 → 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 +4 -4
- data/README.md +1 -1
- data/bin/httpclient +7 -1
- data/lib/http-access2.rb +1 -1
- data/lib/httpclient/auth.rb +3 -3
- data/lib/httpclient/cacert.pem +3952 -0
- data/lib/httpclient/connection.rb +1 -1
- data/lib/httpclient/cookie.rb +10 -10
- data/lib/httpclient/http.rb +9 -4
- data/lib/httpclient/jruby_ssl_socket.rb +588 -0
- data/lib/httpclient/session.rb +199 -262
- data/lib/httpclient/ssl_config.rb +123 -114
- data/lib/httpclient/ssl_socket.rb +150 -0
- data/lib/httpclient/timeout.rb +1 -1
- data/lib/httpclient/util.rb +33 -1
- data/lib/httpclient/version.rb +1 -1
- data/lib/httpclient/webagent-cookie.rb +2 -2
- data/lib/httpclient.rb +72 -20
- data/lib/oauthclient.rb +2 -1
- data/test/helper.rb +7 -5
- data/test/jruby_ssl_socket/test_pemutils.rb +32 -0
- data/test/test_auth.rb +28 -9
- data/test/test_cookie.rb +2 -2
- data/test/test_http-access2.rb +2 -0
- data/test/test_httpclient.rb +143 -23
- data/test/test_ssl.rb +295 -17
- data/test/test_webagent-cookie.rb +2 -2
- metadata +14 -10
- /data/lib/httpclient/{cacert.p7s → cacert1024.pem} +0 -0
- /data/test/{ca-chain.cert → ca-chain.pem} +0 -0
@@ -1,5 +1,5 @@
|
|
1
1
|
# HTTPClient - HTTP client library.
|
2
|
-
# Copyright (C) 2000-
|
2
|
+
# Copyright (C) 2000-2015 NAKAMURA, Hiroshi <nahi@ruby-lang.org>.
|
3
3
|
#
|
4
4
|
# This program is copyrighted free software by NAKAMURA, Hiroshi. You can
|
5
5
|
# redistribute it and/or modify it under the same terms of Ruby's license;
|
data/lib/httpclient/cookie.rb
CHANGED
@@ -3,16 +3,19 @@ unless defined?(HTTPClient::CookieManager)
|
|
3
3
|
begin # for catching LoadError and load webagent-cookie instead
|
4
4
|
|
5
5
|
require 'http-cookie'
|
6
|
+
require 'httpclient/util'
|
6
7
|
|
7
8
|
class HTTPClient
|
8
9
|
class CookieManager
|
9
|
-
|
10
|
+
include HTTPClient::Util
|
11
|
+
|
12
|
+
attr_reader :format, :jar
|
10
13
|
attr_accessor :cookies_file
|
11
14
|
|
12
|
-
def initialize(cookies_file = nil, format = WebAgentSaver)
|
15
|
+
def initialize(cookies_file = nil, format = WebAgentSaver, jar = HTTP::CookieJar.new)
|
13
16
|
@cookies_file = cookies_file
|
14
17
|
@format = format
|
15
|
-
@jar =
|
18
|
+
@jar = jar
|
16
19
|
load_cookies if @cookies_file
|
17
20
|
end
|
18
21
|
|
@@ -68,7 +71,7 @@ class HTTPClient
|
|
68
71
|
end
|
69
72
|
|
70
73
|
def find(uri)
|
71
|
-
|
74
|
+
warning('CookieManager#find is deprecated and will be removed in near future. Use HTTP::Cookie.cookie_value(CookieManager#cookies) instead')
|
72
75
|
if cookie = cookies(uri)
|
73
76
|
HTTP::Cookie.cookie_value(cookie)
|
74
77
|
end
|
@@ -174,7 +177,7 @@ class WebAgent
|
|
174
177
|
CookieManager = ::HTTPClient::CookieManager
|
175
178
|
|
176
179
|
class Cookie < HTTP::Cookie
|
177
|
-
|
180
|
+
include HTTPClient::Util
|
178
181
|
|
179
182
|
def url
|
180
183
|
deprecated('url', 'origin')
|
@@ -194,7 +197,7 @@ class WebAgent
|
|
194
197
|
alias original_domain domain
|
195
198
|
|
196
199
|
def domain
|
197
|
-
|
200
|
+
warning('Cookie#domain returns dot-less domain name now. Use Cookie#dot_domain if you need "." at the beginning.')
|
198
201
|
self.original_domain
|
199
202
|
end
|
200
203
|
|
@@ -206,10 +209,7 @@ class WebAgent
|
|
206
209
|
private
|
207
210
|
|
208
211
|
def deprecated(old, new)
|
209
|
-
|
210
|
-
warn("WebAgent::Cookie is deprecated and will be replaced with HTTP::Cookie in the near future. Please use Cookie##{new} instead of Cookie##{old} for the replacement.")
|
211
|
-
@@warned = true
|
212
|
-
end
|
212
|
+
warning("WebAgent::Cookie is deprecated and will be replaced with HTTP::Cookie in the near future. Please use Cookie##{new} instead of Cookie##{old} for the replacement.")
|
213
213
|
end
|
214
214
|
end
|
215
215
|
end
|
data/lib/httpclient/http.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
2
|
|
3
3
|
# HTTPClient - HTTP client library.
|
4
|
-
# Copyright (C) 2000-
|
4
|
+
# Copyright (C) 2000-2015 NAKAMURA, Hiroshi <nahi@ruby-lang.org>.
|
5
5
|
#
|
6
6
|
# This program is copyrighted free software by NAKAMURA, Hiroshi. You can
|
7
7
|
# redistribute it and/or modify it under the same terms of Ruby's license;
|
@@ -12,6 +12,7 @@ require 'time'
|
|
12
12
|
if defined?(Encoding::ASCII_8BIT)
|
13
13
|
require 'open-uri' # for encoding
|
14
14
|
end
|
15
|
+
require 'httpclient/util'
|
15
16
|
|
16
17
|
|
17
18
|
# A namespace module for HTTP Message definitions used by HTTPClient.
|
@@ -95,6 +96,7 @@ module HTTP
|
|
95
96
|
# p res.header['last-modified'].first
|
96
97
|
#
|
97
98
|
class Message
|
99
|
+
include HTTPClient::Util
|
98
100
|
|
99
101
|
CRLF = "\r\n"
|
100
102
|
|
@@ -698,8 +700,9 @@ module HTTP
|
|
698
700
|
|
699
701
|
def params_from_file(value)
|
700
702
|
params = {}
|
703
|
+
original_filename = value.respond_to?(:original_filename) ? value.original_filename : nil
|
701
704
|
path = value.respond_to?(:path) ? value.path : nil
|
702
|
-
params['filename'] = File.basename(path || '')
|
705
|
+
params['filename'] = original_filename || File.basename(path || '')
|
703
706
|
# Creation time is not available from File::Stat
|
704
707
|
if value.respond_to?(:mtime)
|
705
708
|
params['modification-date'] = value.mtime.rfc822
|
@@ -806,6 +809,8 @@ module HTTP
|
|
806
809
|
case path
|
807
810
|
when /\.txt$/i
|
808
811
|
'text/plain'
|
812
|
+
when /\.xml$/i
|
813
|
+
'text/xml'
|
809
814
|
when /\.(htm|html)$/i
|
810
815
|
'text/html'
|
811
816
|
when /\.doc$/i
|
@@ -980,12 +985,12 @@ module HTTP
|
|
980
985
|
|
981
986
|
VERSION_WARNING = 'Message#version (Float) is deprecated. Use Message#http_version (String) instead.'
|
982
987
|
def version
|
983
|
-
|
988
|
+
warning(VERSION_WARNING)
|
984
989
|
@http_header.http_version.to_f
|
985
990
|
end
|
986
991
|
|
987
992
|
def version=(version)
|
988
|
-
|
993
|
+
warning(VERSION_WARNING)
|
989
994
|
@http_header.http_version = version
|
990
995
|
end
|
991
996
|
|
@@ -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
|