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.
- 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
|