httpclient 2.3.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 +7 -0
- data/README.md +85 -0
- data/bin/httpclient +18 -6
- data/bin/jsonclient +85 -0
- data/lib/http-access2.rb +1 -1
- data/lib/httpclient.rb +262 -88
- data/lib/httpclient/auth.rb +269 -244
- data/lib/httpclient/cacert.pem +3952 -0
- data/lib/httpclient/cacert1024.pem +3866 -0
- data/lib/httpclient/connection.rb +1 -1
- data/lib/httpclient/cookie.rb +161 -514
- data/lib/httpclient/http.rb +57 -21
- data/lib/httpclient/include_client.rb +2 -0
- data/lib/httpclient/jruby_ssl_socket.rb +588 -0
- data/lib/httpclient/session.rb +259 -317
- data/lib/httpclient/ssl_config.rb +141 -188
- data/lib/httpclient/ssl_socket.rb +150 -0
- data/lib/httpclient/timeout.rb +1 -1
- data/lib/httpclient/util.rb +62 -1
- data/lib/httpclient/version.rb +1 -1
- data/lib/httpclient/webagent-cookie.rb +459 -0
- data/lib/jsonclient.rb +63 -0
- data/lib/oauthclient.rb +2 -1
- data/sample/jsonclient.rb +67 -0
- data/sample/oauth_twitter.rb +4 -4
- data/test/{ca-chain.cert → ca-chain.pem} +0 -0
- data/test/client-pass.key +18 -0
- data/test/helper.rb +10 -8
- data/test/jruby_ssl_socket/test_pemutils.rb +32 -0
- data/test/test_auth.rb +175 -4
- data/test/test_cookie.rb +147 -243
- data/test/test_http-access2.rb +17 -16
- data/test/test_httpclient.rb +458 -77
- data/test/test_jsonclient.rb +80 -0
- data/test/test_ssl.rb +341 -17
- data/test/test_webagent-cookie.rb +465 -0
- metadata +57 -55
- data/README.txt +0 -721
- data/lib/httpclient/cacert.p7s +0 -1858
- data/lib/httpclient/cacert_sha1.p7s +0 -1858
- data/sample/oauth_salesforce_10.rb +0 -63
data/lib/httpclient/http.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
1
3
|
# HTTPClient - HTTP client library.
|
2
|
-
# Copyright (C) 2000-
|
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.
|
402
|
+
set('Host', "#{@request_uri.hostname}")
|
397
403
|
else
|
398
|
-
set('Host', "#{@request_uri.
|
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
|
-
|
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
|
-
@
|
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
|
-
|
622
|
+
sz = part.lstat.size
|
623
|
+
add_size(part, sz)
|
605
624
|
elsif part.respond_to?(:size)
|
606
625
|
if sz = part.size
|
607
|
-
|
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 { |
|
846
|
-
if
|
847
|
-
|
878
|
+
values.each { |v|
|
879
|
+
if v.respond_to?(:read)
|
880
|
+
v = v.read
|
848
881
|
end
|
849
|
-
pairs.push(left + escape(
|
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
|
-
|
988
|
+
warning(VERSION_WARNING)
|
951
989
|
@http_header.http_version.to_f
|
952
990
|
end
|
953
991
|
|
954
992
|
def version=(version)
|
955
|
-
|
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
|
-
|
1025
|
-
|
1026
|
-
cookie
|
1027
|
-
}
|
1062
|
+
WebAgent::Cookie.parse(str, uri)
|
1063
|
+
}.flatten
|
1028
1064
|
end
|
1029
1065
|
end
|
1030
1066
|
|
@@ -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
|