httpclient 2.3.0.1 → 2.8.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +85 -0
  3. data/bin/httpclient +18 -6
  4. data/bin/jsonclient +85 -0
  5. data/lib/http-access2.rb +1 -1
  6. data/lib/httpclient.rb +262 -88
  7. data/lib/httpclient/auth.rb +269 -244
  8. data/lib/httpclient/cacert.pem +3952 -0
  9. data/lib/httpclient/cacert1024.pem +3866 -0
  10. data/lib/httpclient/connection.rb +1 -1
  11. data/lib/httpclient/cookie.rb +161 -514
  12. data/lib/httpclient/http.rb +57 -21
  13. data/lib/httpclient/include_client.rb +2 -0
  14. data/lib/httpclient/jruby_ssl_socket.rb +588 -0
  15. data/lib/httpclient/session.rb +259 -317
  16. data/lib/httpclient/ssl_config.rb +141 -188
  17. data/lib/httpclient/ssl_socket.rb +150 -0
  18. data/lib/httpclient/timeout.rb +1 -1
  19. data/lib/httpclient/util.rb +62 -1
  20. data/lib/httpclient/version.rb +1 -1
  21. data/lib/httpclient/webagent-cookie.rb +459 -0
  22. data/lib/jsonclient.rb +63 -0
  23. data/lib/oauthclient.rb +2 -1
  24. data/sample/jsonclient.rb +67 -0
  25. data/sample/oauth_twitter.rb +4 -4
  26. data/test/{ca-chain.cert → ca-chain.pem} +0 -0
  27. data/test/client-pass.key +18 -0
  28. data/test/helper.rb +10 -8
  29. data/test/jruby_ssl_socket/test_pemutils.rb +32 -0
  30. data/test/test_auth.rb +175 -4
  31. data/test/test_cookie.rb +147 -243
  32. data/test/test_http-access2.rb +17 -16
  33. data/test/test_httpclient.rb +458 -77
  34. data/test/test_jsonclient.rb +80 -0
  35. data/test/test_ssl.rb +341 -17
  36. data/test/test_webagent-cookie.rb +465 -0
  37. metadata +57 -55
  38. data/README.txt +0 -721
  39. data/lib/httpclient/cacert.p7s +0 -1858
  40. data/lib/httpclient/cacert_sha1.p7s +0 -1858
  41. data/sample/oauth_salesforce_10.rb +0 -63
@@ -1,5 +1,7 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  # HTTPClient - HTTP client library.
2
- # Copyright (C) 2000-2009 NAKAMURA, Hiroshi <nahi@ruby-lang.org>.
4
+ # Copyright (C) 2000-2015 NAKAMURA, Hiroshi <nahi@ruby-lang.org>.
3
5
  #
4
6
  # This program is copyrighted free software by NAKAMURA, Hiroshi. You can
5
7
  # redistribute it and/or modify it under the same terms of Ruby's license;
@@ -10,6 +12,7 @@ require 'time'
10
12
  if defined?(Encoding::ASCII_8BIT)
11
13
  require 'open-uri' # for encoding
12
14
  end
15
+ require 'httpclient/util'
13
16
 
14
17
 
15
18
  # A namespace module for HTTP Message definitions used by HTTPClient.
@@ -93,6 +96,7 @@ module HTTP
93
96
  # p res.header['last-modified'].first
94
97
  #
95
98
  class Message
99
+ include HTTPClient::Util
96
100
 
97
101
  CRLF = "\r\n"
98
102
 
@@ -196,6 +200,7 @@ module HTTP
196
200
  @request_uri = uri || NIL_URI
197
201
  @request_query = query
198
202
  @request_absolute_uri = false
203
+ self
199
204
  end
200
205
 
201
206
  # Initialize this instance as a response.
@@ -207,6 +212,7 @@ module HTTP
207
212
  @request_uri = req.request_uri
208
213
  @request_query = req.request_query
209
214
  end
215
+ self
210
216
  end
211
217
 
212
218
  # Sets status code and reason phrase.
@@ -393,9 +399,9 @@ module HTTP
393
399
  if @http_version >= '1.1' and get('Host').empty?
394
400
  if @request_uri.port == @request_uri.default_port
395
401
  # GFE/1.3 dislikes default port number (returns 404)
396
- set('Host', "#{@request_uri.host}")
402
+ set('Host', "#{@request_uri.hostname}")
397
403
  else
398
- set('Host', "#{@request_uri.host}:#{@request_uri.port}")
404
+ set('Host', "#{@request_uri.hostname}:#{@request_uri.port}")
399
405
  end
400
406
  end
401
407
  end
@@ -439,6 +445,8 @@ module HTTP
439
445
  attr_reader :size
440
446
  # maxbytes of IO#read for streaming request. See DEFAULT_CHUNK_SIZE.
441
447
  attr_accessor :chunk_size
448
+ # Hash that keeps IO positions
449
+ attr_accessor :positions
442
450
 
443
451
  # Default value for chunk_size
444
452
  DEFAULT_CHUNK_SIZE = 1024 * 16
@@ -458,6 +466,7 @@ module HTTP
458
466
  @positions = {}
459
467
  set_content(body, boundary)
460
468
  @chunk_size = DEFAULT_CHUNK_SIZE
469
+ self
461
470
  end
462
471
 
463
472
  # Initialize this instance as a response.
@@ -470,6 +479,7 @@ module HTTP
470
479
  else
471
480
  @size = nil
472
481
  end
482
+ self
473
483
  end
474
484
 
475
485
  # Dumps message body to given dev.
@@ -479,13 +489,15 @@ module HTTP
479
489
  # reason. (header is dumped to dev, too)
480
490
  # If no dev (the second argument) given, this method returns a dumped
481
491
  # String.
492
+ #
493
+ # assert: @size is not nil
482
494
  def dump(header = '', dev = '')
483
495
  if @body.is_a?(Parts)
484
496
  dev << header
485
497
  @body.parts.each do |part|
486
498
  if Message.file?(part)
487
499
  reset_pos(part)
488
- dump_file(part, dev)
500
+ dump_file(part, dev, @body.sizes[part])
489
501
  else
490
502
  dev << part
491
503
  end
@@ -493,7 +505,7 @@ module HTTP
493
505
  elsif Message.file?(@body)
494
506
  dev << header
495
507
  reset_pos(@body)
496
- dump_file(@body, dev)
508
+ dump_file(@body, dev, @size)
497
509
  elsif @body
498
510
  dev << header + @body
499
511
  else
@@ -561,10 +573,14 @@ module HTTP
561
573
  io.pos = @positions[io] if @positions.key?(io)
562
574
  end
563
575
 
564
- def dump_file(io, dev)
576
+ def dump_file(io, dev, sz)
565
577
  buf = ''
566
- while !io.read(@chunk_size, buf).nil?
578
+ rest = sz
579
+ while rest > 0
580
+ n = io.read([rest, @chunk_size].min, buf)
581
+ raise ArgumentError.new("Illegal size value: #size returns #{sz} but cannot read") if n.nil?
567
582
  dev << buf
583
+ rest -= n.bytesize
568
584
  end
569
585
  end
570
586
 
@@ -589,10 +605,12 @@ module HTTP
589
605
 
590
606
  class Parts
591
607
  attr_reader :size
608
+ attr_reader :sizes
592
609
 
593
610
  def initialize
594
611
  @body = []
595
- @size = 0
612
+ @sizes = {}
613
+ @size = 0 # total
596
614
  @as_stream = false
597
615
  end
598
616
 
@@ -601,15 +619,18 @@ module HTTP
601
619
  @as_stream = true
602
620
  @body << part
603
621
  if part.respond_to?(:lstat)
604
- @size += part.lstat.size
622
+ sz = part.lstat.size
623
+ add_size(part, sz)
605
624
  elsif part.respond_to?(:size)
606
625
  if sz = part.size
607
- @size += sz
626
+ add_size(part, sz)
608
627
  else
628
+ @sizes.clear
609
629
  @size = nil
610
630
  end
611
631
  else
612
632
  # use chunked upload
633
+ @sizes.clear
613
634
  @size = nil
614
635
  end
615
636
  elsif @body[-1].is_a?(String)
@@ -628,6 +649,15 @@ module HTTP
628
649
  [@body.join]
629
650
  end
630
651
  end
652
+
653
+ private
654
+
655
+ def add_size(part, sz)
656
+ if @size
657
+ @sizes[part] = sz
658
+ @size += sz
659
+ end
660
+ end
631
661
  end
632
662
 
633
663
  def build_query_multipart_str(query, boundary)
@@ -670,8 +700,9 @@ module HTTP
670
700
 
671
701
  def params_from_file(value)
672
702
  params = {}
703
+ original_filename = value.respond_to?(:original_filename) ? value.original_filename : nil
673
704
  path = value.respond_to?(:path) ? value.path : nil
674
- params['filename'] = File.basename(path || '')
705
+ params['filename'] = original_filename || File.basename(path || '')
675
706
  # Creation time is not available from File::Stat
676
707
  if value.respond_to?(:mtime)
677
708
  params['modification-date'] = value.mtime.rfc822
@@ -778,6 +809,8 @@ module HTTP
778
809
  case path
779
810
  when /\.txt$/i
780
811
  'text/plain'
812
+ when /\.xml$/i
813
+ 'text/xml'
781
814
  when /\.(htm|html)$/i
782
815
  'text/html'
783
816
  when /\.doc$/i
@@ -842,11 +875,11 @@ module HTTP
842
875
  query.each { |attr, value|
843
876
  left = escape(attr.to_s) << '='
844
877
  if values = Array.try_convert(value)
845
- values.each { |value|
846
- if value.respond_to?(:read)
847
- value = value.read
878
+ values.each { |v|
879
+ if v.respond_to?(:read)
880
+ v = v.read
848
881
  end
849
- pairs.push(left + escape(value.to_s))
882
+ pairs.push(left + escape(v.to_s))
850
883
  }
851
884
  else
852
885
  if value.respond_to?(:read)
@@ -906,12 +939,17 @@ module HTTP
906
939
  # used for retrieving the response.
907
940
  attr_accessor :peer_cert
908
941
 
942
+ # The other Message object when this Message is generated instead of
943
+ # the Message because of redirection, negotiation, or format conversion.
944
+ attr_accessor :previous
945
+
909
946
  # Creates a Message. This method should be used internally.
910
947
  # Use Message.new_connect_request, Message.new_request or
911
948
  # Message.new_response instead.
912
949
  def initialize # :nodoc:
913
950
  @http_header = Headers.new
914
951
  @http_body = @peer_cert = nil
952
+ @previous = nil
915
953
  end
916
954
 
917
955
  # Dumps message (header and body) to given dev.
@@ -947,12 +985,12 @@ module HTTP
947
985
 
948
986
  VERSION_WARNING = 'Message#version (Float) is deprecated. Use Message#http_version (String) instead.'
949
987
  def version
950
- warn(VERSION_WARNING)
988
+ warning(VERSION_WARNING)
951
989
  @http_header.http_version.to_f
952
990
  end
953
991
 
954
992
  def version=(version)
955
- warn(VERSION_WARNING)
993
+ warning(VERSION_WARNING)
956
994
  @http_header.http_version = version
957
995
  end
958
996
 
@@ -1021,10 +1059,8 @@ module HTTP
1021
1059
  unless set_cookies.empty?
1022
1060
  uri = http_header.request_uri
1023
1061
  set_cookies.map { |str|
1024
- cookie = WebAgent::Cookie.new
1025
- cookie.parse(str, uri)
1026
- cookie
1027
- }
1062
+ WebAgent::Cookie.parse(str, uri)
1063
+ }.flatten
1028
1064
  end
1029
1065
  end
1030
1066
 
@@ -36,6 +36,8 @@
36
36
  # are also provided. Widget.http_client is identical to Widget.new.http_client
37
37
  #
38
38
  #
39
+ require 'httpclient'
40
+
39
41
  class HTTPClient
40
42
  module IncludeClient
41
43
 
@@ -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