httpclient 2.6.0.1 → 2.7.0

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.
@@ -1,5 +1,5 @@
1
1
  # HTTPClient - HTTP client library.
2
- # Copyright (C) 2000-2009 NAKAMURA, Hiroshi <nahi@ruby-lang.org>.
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;
@@ -6,13 +6,13 @@ require 'http-cookie'
6
6
 
7
7
  class HTTPClient
8
8
  class CookieManager
9
- attr_reader :format
9
+ attr_reader :format, :jar
10
10
  attr_accessor :cookies_file
11
11
 
12
- def initialize(cookies_file = nil, format = WebAgentSaver)
12
+ def initialize(cookies_file = nil, format = WebAgentSaver, jar = HTTP::CookieJar.new)
13
13
  @cookies_file = cookies_file
14
14
  @format = format
15
- @jar = HTTP::CookieJar.new
15
+ @jar = jar
16
16
  load_cookies if @cookies_file
17
17
  end
18
18
 
@@ -174,6 +174,7 @@ class WebAgent
174
174
  CookieManager = ::HTTPClient::CookieManager
175
175
 
176
176
  class Cookie < HTTP::Cookie
177
+ @@domain_warned = false
177
178
  @@warned = false
178
179
 
179
180
  def url
@@ -194,7 +195,7 @@ class WebAgent
194
195
  alias original_domain domain
195
196
 
196
197
  def domain
197
- warn('Cookie#domain returns dot-less domain name now. Use Cookie#dot_domain if you need "." at the beginning.')
198
+ domain_warning
198
199
  self.original_domain
199
200
  end
200
201
 
@@ -205,6 +206,13 @@ class WebAgent
205
206
 
206
207
  private
207
208
 
209
+ def domain_warning
210
+ unless @@domain_warned
211
+ warn('Cookie#domain returns dot-less domain name now. Use Cookie#dot_domain if you need "." at the beginning.')
212
+ @@domain_warned = true
213
+ end
214
+ end
215
+
208
216
  def deprecated(old, new)
209
217
  unless @@warned
210
218
  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.")
@@ -1,7 +1,7 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
 
3
3
  # HTTPClient - HTTP client library.
4
- # Copyright (C) 2000-2009 NAKAMURA, Hiroshi <nahi@ruby-lang.org>.
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;
@@ -0,0 +1,526 @@
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.io.BufferedInputStream'
19
+ BUF_SIZE = 1024 * 16
20
+
21
+ def initialize(socket, debug_dev = nil)
22
+ @socket = socket
23
+ @debug_dev = debug_dev
24
+ @outstr = @socket.getOutputStream
25
+ @instr = BufferedInputStream.new(@socket.getInputStream)
26
+ @buf = (' ' * BUF_SIZE).to_java_bytes
27
+ @bufstr = ''
28
+ end
29
+
30
+ def close
31
+ @socket.close
32
+ end
33
+
34
+ def closed?
35
+ @socket.isClosed
36
+ end
37
+
38
+ def eof?
39
+ @socket.isClosed
40
+ end
41
+
42
+
43
+ def gets(rs)
44
+ while (size = @bufstr.index(rs)).nil?
45
+ if fill() == -1
46
+ size = @bufstr.size
47
+ break
48
+ end
49
+ end
50
+ str = @bufstr.slice!(0, size + rs.size)
51
+ debug(str)
52
+ str
53
+ end
54
+
55
+ def read(size, buf = nil)
56
+ while @bufstr.size < size
57
+ if fill() == -1
58
+ break
59
+ end
60
+ end
61
+ str = @bufstr.slice!(0, size)
62
+ debug(str)
63
+ if buf
64
+ buf.replace(str)
65
+ else
66
+ str
67
+ end
68
+ end
69
+
70
+ def readpartial(size, buf = nil)
71
+ while @bufstr.size == 0
72
+ if fill() == -1
73
+ raise EOFError.new('end of file reached')
74
+ end
75
+ end
76
+ str = @bufstr.slice!(0, size)
77
+ debug(str)
78
+ if buf
79
+ buf.replace(str)
80
+ else
81
+ str
82
+ end
83
+ end
84
+
85
+ def <<(str)
86
+ rv = @outstr.write(str.to_java_bytes)
87
+ debug(str)
88
+ rv
89
+ end
90
+
91
+ def flush
92
+ @socket.flush
93
+ end
94
+
95
+ def sync
96
+ true
97
+ end
98
+
99
+ def sync=(sync)
100
+ unless sync
101
+ raise "sync = false is not supported. This option was introduced for backward compatibility just in case."
102
+ end
103
+ end
104
+
105
+ private
106
+
107
+ def fill
108
+ size = @instr.read(@buf)
109
+ if size > 0
110
+ @bufstr << String.from_java_bytes(@buf, Encoding::BINARY)[0, size]
111
+ end
112
+ size
113
+ end
114
+
115
+ def debug(str)
116
+ @debug_dev << str if @debug_dev && str
117
+ end
118
+ end
119
+
120
+ class JRubySSLSocket < JavaSocketWrap
121
+ java_import 'java.io.ByteArrayInputStream'
122
+ java_import 'java.io.InputStreamReader'
123
+ java_import 'java.net.Socket'
124
+ java_import 'java.security.KeyStore'
125
+ java_import 'java.security.cert.Certificate'
126
+ java_import 'java.security.cert.CertificateFactory'
127
+ java_import 'javax.net.ssl.KeyManagerFactory'
128
+ java_import 'javax.net.ssl.SSLContext'
129
+ java_import 'javax.net.ssl.SSLSocketFactory'
130
+ java_import 'javax.net.ssl.TrustManager'
131
+ java_import 'javax.net.ssl.TrustManagerFactory'
132
+ java_import 'javax.net.ssl.X509TrustManager'
133
+ java_import 'org.jruby.ext.openssl.x509store.PEMInputOutput'
134
+
135
+ class JavaCertificate
136
+ attr_reader :cert
137
+
138
+ def initialize(cert)
139
+ @cert = cert
140
+ end
141
+
142
+ def subject
143
+ @cert.getSubjectDN
144
+ end
145
+
146
+ def to_text
147
+ @cert.toString
148
+ end
149
+
150
+ def to_pem
151
+ '(not in PEM format)'
152
+ end
153
+ end
154
+
155
+ class SSLStoreContext
156
+ attr_reader :current_cert, :chain, :error_depth, :error, :error_string
157
+
158
+ def initialize(current_cert, chain, error_depth, error, error_string)
159
+ @current_cert, @chain, @error_depth, @error, @error_string =
160
+ current_cert, chain, error_depth, error, error_string
161
+ end
162
+ end
163
+
164
+ class JSSEVerifyCallback
165
+ def initialize(verify_callback)
166
+ @verify_callback = verify_callback
167
+ end
168
+
169
+ def call(is_ok, chain, error_depth = -1, error = -1, error_string = '(unknown)')
170
+ if @verify_callback
171
+ ruby_chain = chain.map { |cert|
172
+ JavaCertificate.new(cert)
173
+ }.reverse
174
+ # NOTE: The order depends on provider implementation
175
+ ruby_chain.each do |cert|
176
+ is_ok = @verify_callback.call(
177
+ is_ok,
178
+ SSLStoreContext.new(cert, ruby_chain, error_depth, error, error_string)
179
+ )
180
+ end
181
+ end
182
+ is_ok
183
+ end
184
+ end
185
+
186
+ class VerifyNoneTrustManagerFactory
187
+ class VerifyNoneTrustManager
188
+ include X509TrustManager
189
+
190
+ def initialize(verify_callback)
191
+ @verify_callback = JSSEVerifyCallback.new(verify_callback)
192
+ end
193
+
194
+ def checkServerTrusted(chain, authType)
195
+ @verify_callback.call(true, chain)
196
+ end
197
+
198
+ def checkClientTrusted(chain, authType); end
199
+ def getAcceptedIssuers; end
200
+ end
201
+
202
+ def initialize(verify_callback = nil)
203
+ @verify_callback = verify_callback
204
+ end
205
+
206
+ def init(trustStore)
207
+ @managers = [VerifyNoneTrustManager.new(@verify_callback)].to_java(X509TrustManager)
208
+ end
209
+
210
+ def getTrustManagers
211
+ @managers
212
+ end
213
+ end
214
+
215
+ class SystemTrustManagerFactory
216
+ class SystemTrustManager
217
+ include X509TrustManager
218
+
219
+ def initialize(original, verify_callback)
220
+ @original = original
221
+ @verify_callback = JSSEVerifyCallback.new(verify_callback)
222
+ end
223
+
224
+ def checkServerTrusted(chain, authType)
225
+ is_ok = false
226
+ excn = nil
227
+ # TODO can we detect the depth from excn?
228
+ error_depth = -1
229
+ error = nil
230
+ error_message = nil
231
+ begin
232
+ @original.checkServerTrusted(chain, authType)
233
+ is_ok = true
234
+ rescue java.security.cert.CertificateException => excn
235
+ is_ok = false
236
+ error = excn.class.name
237
+ error_message = excn.getMessage
238
+ end
239
+ is_ok = @verify_callback.call(is_ok, chain, error_depth, error, error_message)
240
+ unless is_ok
241
+ excn ||= OpenSSL::SSL::SSLError.new('verifycallback failed')
242
+ raise excn
243
+ end
244
+ end
245
+
246
+ def checkClientTrusted(chain, authType); end
247
+ def getAcceptedIssuers; end
248
+ end
249
+
250
+ def initialize(verify_callback = nil)
251
+ @verify_callback = verify_callback
252
+ end
253
+
254
+ def init(trust_store)
255
+ tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm)
256
+ tmf.java_method(:init, [KeyStore]).call(trust_store)
257
+ @original = tmf.getTrustManagers.find { |tm|
258
+ tm.is_a?(X509TrustManager)
259
+ }
260
+ @managers = [SystemTrustManager.new(@original, @verify_callback)].to_java(X509TrustManager)
261
+ end
262
+
263
+ def getTrustManagers
264
+ @managers
265
+ end
266
+ end
267
+
268
+ module PEMUtils
269
+ def self.read_certificate(pem)
270
+ pem = pem.sub(/-----BEGIN CERTIFICATE-----/, '').sub(/-----END CERTIFICATE-----/, '')
271
+ der = pem.unpack('m*').first
272
+ cf = CertificateFactory.getInstance('X.509')
273
+ cf.generateCertificate(ByteArrayInputStream.new(der.to_java_bytes))
274
+ end
275
+
276
+ def self.read_private_key(pem, password)
277
+ if password
278
+ password = password.unpack('C*').to_java(:char)
279
+ end
280
+ PEMInputOutput.read_private_key(InputStreamReader.new(ByteArrayInputStream.new(pem.to_java_bytes)), password)
281
+ end
282
+ end
283
+
284
+ class KeyStoreLoader
285
+ PASSWORD = 16.times.map { rand(256) }.to_java(:char)
286
+
287
+ def initialize
288
+ @keystore = KeyStore.getInstance('JKS')
289
+ @keystore.load(nil)
290
+ end
291
+
292
+ def add(cert_file, key_file, password)
293
+ cert_str = cert_file.respond_to?(:to_pem) ? cert_file.to_pem : File.read(cert_file.to_s)
294
+ cert = PEMUtils.read_certificate(cert_str)
295
+ @keystore.setCertificateEntry('client_cert', cert)
296
+ key_str = key_file.respond_to?(:to_pem) ? key_file.to_pem : File.read(key_file.to_s)
297
+ key_pair = PEMUtils.read_private_key(key_str, password)
298
+ @keystore.setKeyEntry('client_key', key_pair.getPrivate, PASSWORD, [cert].to_java(Certificate))
299
+ end
300
+
301
+ def keystore
302
+ @keystore
303
+ end
304
+ end
305
+
306
+ class TrustStoreLoader
307
+ attr_reader :size
308
+
309
+ def initialize
310
+ @trust_store = KeyStore.getInstance('JKS')
311
+ @trust_store.load(nil)
312
+ @size = 0
313
+ end
314
+
315
+ def add(file_or_dir)
316
+ return if file_or_dir == :default
317
+ if File.directory?(file_or_dir)
318
+ warn('directory not yet supported')
319
+ else
320
+ pem = nil
321
+ File.read(file_or_dir).each_line do |line|
322
+ case line
323
+ when /-----BEGIN CERTIFICATE-----/
324
+ pem = ''
325
+ when /-----END CERTIFICATE-----/
326
+ cert = PEMUtils.read_certificate(pem)
327
+ @size += 1
328
+ @trust_store.setCertificateEntry("cert_#{@size}", cert)
329
+ else
330
+ if pem
331
+ pem << line
332
+ end
333
+ end
334
+ end
335
+ end
336
+ end
337
+
338
+ def trust_store
339
+ if @size == 0
340
+ nil
341
+ else
342
+ @trust_store
343
+ end
344
+ end
345
+ end
346
+
347
+ # Ported from commons-httpclient 'BrowserCompatHostnameVerifier'
348
+ class BrowserCompatHostnameVerifier
349
+ BAD_COUNTRY_2LDS = %w(ac co com ed edu go gouv gov info lg ne net or org).sort
350
+ require 'ipaddr'
351
+
352
+ def extract_sans(cert, subject_type)
353
+ sans = cert.getSubjectAlternativeNames rescue nil
354
+ if sans.nil?
355
+ return nil
356
+ end
357
+ sans.find_all { |san|
358
+ san.first.to_i == subject_type
359
+ }.map { |san|
360
+ san[1]
361
+ }
362
+ end
363
+
364
+ def extract_cn(cert)
365
+ subject = cert.getSubjectX500Principal()
366
+ if subject
367
+ subject_dn = javax.naming.ldap.LdapName.new(subject.toString)
368
+ subject_dn.getRdns.to_a.reverse.each do |rdn|
369
+ attributes = rdn.toAttributes
370
+ cn = attributes.get('cn')
371
+ if cn
372
+ if value = cn.get
373
+ return value.to_s
374
+ end
375
+ end
376
+ end
377
+ end
378
+ end
379
+
380
+ def ipaddr?(addr)
381
+ !(IPAddr.new(addr) rescue nil).nil?
382
+ end
383
+
384
+ def verify(hostname, cert)
385
+ is_ipaddr = ipaddr?(hostname)
386
+ sans = extract_sans(cert, is_ipaddr ? 7 : 2)
387
+ cn = extract_cn(cert)
388
+ if sans
389
+ sans.each do |san|
390
+ return true if match_identify(hostname, san)
391
+ end
392
+ raise OpenSSL::SSL::SSLError.new("Certificate for <#{hostname}> doesn't match any of the subject alternative names: #{sans}")
393
+ elsif cn
394
+ return true if match_identify(hostname, cn)
395
+ raise OpenSSL::SSL::SSLError.new("Certificate for <#{hostname}> doesn't match common name of the certificate subject: #{cn}")
396
+ end
397
+ raise OpenSSL::SSL::SSLError.new("Certificate subject for for <#{hostname}> doesn't contain a common name and does not have alternative names")
398
+ end
399
+
400
+ def match_identify(hostname, identity)
401
+ if hostname.nil?
402
+ return false
403
+ end
404
+ hostname = hostname.downcase
405
+ identity = identity.downcase
406
+ parts = identity.split('.')
407
+ if parts.length >= 3 && parts.first.end_with?('*') && valid_country_wildcard(parts)
408
+ create_wildcard_regexp(identity) =~ hostname
409
+ else
410
+ hostname == identity
411
+ end
412
+ end
413
+
414
+ def create_wildcard_regexp(value)
415
+ # Escape first then search '\*' for meta-char interpolation
416
+ labels = value.split('.').map { |e| Regexp.escape(e) }
417
+ # Handle '*'s only at the left-most label, exclude A-label and U-label
418
+ labels[0].gsub!(/\\\*/, '[^.]+') if !labels[0].start_with?('xn\-\-') and labels[0].ascii_only?
419
+ /\A#{labels.join('\.')}\z/i
420
+ end
421
+
422
+ def valid_country_wildcard(parts)
423
+ if parts.length != 3 || parts[2].length != 2
424
+ true
425
+ else
426
+ !BAD_COUNTRY_2LDS.include?(parts[1])
427
+ end
428
+ end
429
+ end
430
+
431
+ def self.create_socket(session)
432
+ site = session.proxy || session.dest
433
+ socket = Socket.new(site.host, site.port)
434
+ begin
435
+ if session.proxy
436
+ session.connect_ssl_proxy(JavaSocketWrap.new(socket), Util.urify(session.dest.to_s))
437
+ end
438
+ rescue
439
+ socket.close
440
+ raise
441
+ end
442
+ new(socket, session.dest, session.ssl_config, session.debug_dev)
443
+ end
444
+
445
+ def initialize(socket, dest, config, debug_dev = nil)
446
+ if config.ssl_version == :auto
447
+ ssl_version = 'TLSv1'
448
+ else
449
+ ssl_version = config.to_s.gsub(/_/, '.')
450
+ end
451
+ unless config.cert_store_crl_items.empty?
452
+ raise NotImplementedError.new('Manual CRL configuration is not yet supported')
453
+ end
454
+
455
+ km = nil
456
+ if config.client_cert && config.client_key
457
+ loader = KeyStoreLoader.new
458
+ loader.add(config.client_cert, config.client_key, config.client_key_pass)
459
+ kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm)
460
+ kmf.init(loader.keystore, KeyStoreLoader::PASSWORD)
461
+ km = kmf.getKeyManagers
462
+ end
463
+
464
+ trust_store = nil
465
+ verify_callback = config.verify_callback || config.method(:default_verify_callback)
466
+ if config.verify_mode == nil
467
+ tmf = VerifyNoneTrustManagerFactory.new(verify_callback)
468
+ else
469
+ tmf = SystemTrustManagerFactory.new(verify_callback)
470
+ loader = TrustStoreLoader.new
471
+ config.cert_store_items.each do |item|
472
+ loader.add(item)
473
+ end
474
+ trust_store = loader.trust_store
475
+ end
476
+ tmf.init(trust_store)
477
+ tm = tmf.getTrustManagers
478
+
479
+ ctx = SSLContext.getInstance(ssl_version)
480
+ ctx.init(km, tm, nil)
481
+ if config.timeout
482
+ ctx.getClientSessionContext.setSessionTimeout(config.timeout)
483
+ end
484
+
485
+ factory = ctx.getSocketFactory
486
+ begin
487
+ ssl_socket = factory.createSocket(socket, dest.host, dest.port, true)
488
+ ssl_socket.setEnabledProtocols([ssl_version].to_java(java.lang.String))
489
+ if config.ciphers != SSLConfig::CIPHERS_DEFAULT
490
+ ssl_socket.setEnabledCipherSuites(config.ciphers.to_java(java.lang.String))
491
+ end
492
+ ssl_socket.startHandshake
493
+ @peer_cert = JavaCertificate.new(ssl_socket.getSession.getPeerCertificates.first)
494
+ @ciphersuite = ssl_socket.getSession.getCipherSuite
495
+ post_connection_check(dest.host, @peer_cert)
496
+ rescue java.security.GeneralSecurityException => e
497
+ raise OpenSSL::SSL::SSLError.new(e.getMessage)
498
+ rescue javax.net.ssl.SSLException => e
499
+ raise OpenSSL::SSL::SSLError.new(e.getMessage)
500
+ rescue java.net.SocketException => e
501
+ raise OpenSSL::SSL::SSLError.new(e.getMessage)
502
+ end
503
+
504
+ super(ssl_socket, debug_dev)
505
+ end
506
+
507
+ def peer_cert
508
+ @peer_cert
509
+ end
510
+
511
+ def ciphersuite
512
+ @ciphersuite
513
+ end
514
+
515
+ private
516
+
517
+ def post_connection_check(hostname, wrap_cert)
518
+ BrowserCompatHostnameVerifier.new.verify(hostname, wrap_cert.cert)
519
+ end
520
+ end
521
+
522
+ SSLSocket = JRubySSLSocket
523
+
524
+ end
525
+
526
+ end