httpclient 2.1.2 → 2.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,379 @@
1
+ # HTTPClient - HTTP client library.
2
+ # Copyright (C) 2000-2008 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
+ class HTTPClient
10
+
11
+ begin
12
+ require 'openssl'
13
+ SSLEnabled = true
14
+ rescue LoadError
15
+ SSLEnabled = false
16
+ end
17
+
18
+ # Represents SSL configuration for HTTPClient instance.
19
+ # The implementation depends on OpenSSL.
20
+ #
21
+ # == Trust Anchor Control
22
+ #
23
+ # SSLConfig loads 'httpclient/cacert.p7s' as a trust anchor
24
+ # (trusted certificate(s)) with set_trust_ca in initialization time.
25
+ # This means that HTTPClient instance trusts some CA certificates by default,
26
+ # like Web browsers. 'httpclient/cacert.p7s' is created by the author and
27
+ # included in released package.
28
+ #
29
+ # 'cacert.p7s' is automatically generated from JDK 1.6.
30
+ #
31
+ # You may want to change trust anchor by yourself. Call clear_cert_store
32
+ # then set_trust_ca for that purpose.
33
+ class SSLConfig
34
+ include OpenSSL if SSLEnabled
35
+
36
+ # OpenSSL::X509::Certificate:: certificate for SSL client authenticateion.
37
+ # nil by default. (no client authenticateion)
38
+ attr_reader :client_cert
39
+ # OpenSSL::PKey::PKey:: private key for SSL client authentication.
40
+ # nil by default. (no client authenticateion)
41
+ attr_reader :client_key
42
+
43
+ # A number which represents OpenSSL's verify mode. Default value is
44
+ # OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT.
45
+ attr_reader :verify_mode
46
+ # A number of verify depth. Certification path which length is longer than
47
+ # this depth is not allowed.
48
+ attr_reader :verify_depth
49
+ # A callback handler for custom certificate verification. nil by default.
50
+ # If the handler is set, handler.call is invoked just after general
51
+ # OpenSSL's verification. handler.call is invoked with 2 arguments,
52
+ # ok and ctx; ok is a result of general OpenSSL's verification. ctx is a
53
+ # OpenSSL::X509::StoreContext.
54
+ attr_reader :verify_callback
55
+ # SSL timeout in sec. nil by default.
56
+ attr_reader :timeout
57
+ # A number of OpenSSL's SSL options. Default value is
58
+ # OpenSSL::SSL::OP_ALL | OpenSSL::SSL::OP_NO_SSLv2
59
+ attr_reader :options
60
+ # A String of OpenSSL's cipher configuration. Default value is
61
+ # ALL:!ADH:!LOW:!EXP:!MD5:+SSLv2:@STRENGTH
62
+ # See ciphers(1) man in OpenSSL for more detail.
63
+ attr_reader :ciphers
64
+
65
+ # OpenSSL::X509::X509::Store used for verification. You can reset the
66
+ # store with clear_cert_store and set the new store with cert_store=.
67
+ attr_reader :cert_store # don't use if you don't know what it is.
68
+
69
+ # For server side configuration. Ignore this.
70
+ attr_reader :client_ca # :nodoc:
71
+
72
+ # Creates a SSLConfig.
73
+ def initialize(client)
74
+ return unless SSLEnabled
75
+ @client = client
76
+ @cert_store = X509::Store.new
77
+ @client_cert = @client_key = @client_ca = nil
78
+ @verify_mode = SSL::VERIFY_PEER | SSL::VERIFY_FAIL_IF_NO_PEER_CERT
79
+ @verify_depth = nil
80
+ @verify_callback = nil
81
+ @dest = nil
82
+ @timeout = nil
83
+ @options = defined?(SSL::OP_ALL) ? SSL::OP_ALL | SSL::OP_NO_SSLv2 : nil
84
+ @ciphers = "ALL:!ADH:!LOW:!EXP:!MD5:+SSLv2:@STRENGTH"
85
+ load_cacerts
86
+ end
87
+
88
+ # Sets certificate (OpenSSL::X509::Certificate) for SSL client
89
+ # authentication.
90
+ # client_key and client_cert must be a pair.
91
+ #
92
+ # Calling this method resets all existing sessions.
93
+ def client_cert=(client_cert)
94
+ @client_cert = client_cert
95
+ change_notify
96
+ end
97
+
98
+ # Sets private key (OpenSSL::PKey::PKey) for SSL client authentication.
99
+ # client_key and client_cert must be a pair.
100
+ #
101
+ # Calling this method resets all existing sessions.
102
+ def client_key=(client_key)
103
+ @client_key = client_key
104
+ change_notify
105
+ end
106
+
107
+ # Sets certificate and private key for SSL client authentication.
108
+ # cert_file:: must be a filename of PEM/DER formatted file.
109
+ # key_file:: must be a filename of PEM/DER formatted file. Key must be an
110
+ # RSA key. If you want to use other PKey algorithm,
111
+ # use client_key=.
112
+ #
113
+ # Calling this method resets all existing sessions.
114
+ def set_client_cert_file(cert_file, key_file)
115
+ @client_cert = X509::Certificate.new(File.open(cert_file).read)
116
+ @client_key = PKey::RSA.new(File.open(key_file).read)
117
+ change_notify
118
+ end
119
+
120
+ # Drops current certificate store (OpenSSL::X509::Store) for SSL and create
121
+ # new one for the next session.
122
+ #
123
+ # Calling this method resets all existing sessions.
124
+ def clear_cert_store
125
+ @cert_store = X509::Store.new
126
+ change_notify
127
+ end
128
+
129
+ # Sets new certificate store (OpenSSL::X509::Store).
130
+ # don't use if you don't know what it is.
131
+ #
132
+ # Calling this method resets all existing sessions.
133
+ def cert_store=(cert_store)
134
+ @cert_store = cert_store
135
+ change_notify
136
+ end
137
+
138
+ # Sets trust anchor certificate(s) for verification.
139
+ # trust_ca_file_or_hashed_dir:: a filename of a PEM/DER formatted
140
+ # OpenSSL::X509::Certificate or
141
+ # a 'c-rehash'eddirectory name which stores
142
+ # trusted certificate files.
143
+ #
144
+ # Calling this method resets all existing sessions.
145
+ def set_trust_ca(trust_ca_file_or_hashed_dir)
146
+ if FileTest.directory?(trust_ca_file_or_hashed_dir)
147
+ @cert_store.add_path(trust_ca_file_or_hashed_dir)
148
+ else
149
+ @cert_store.add_file(trust_ca_file_or_hashed_dir)
150
+ end
151
+ change_notify
152
+ end
153
+
154
+ # Adds CRL for verification.
155
+ # crl:: a OpenSSL::X509::CRL or a filename of a PEM/DER formatted
156
+ # OpenSSL::X509::CRL.
157
+ #
158
+ # Calling this method resets all existing sessions.
159
+ def set_crl(crl)
160
+ unless crl.is_a?(X509::CRL)
161
+ crl = X509::CRL.new(File.open(crl).read)
162
+ end
163
+ @cert_store.add_crl(crl)
164
+ @cert_store.flags = X509::V_FLAG_CRL_CHECK | X509::V_FLAG_CRL_CHECK_ALL
165
+ change_notify
166
+ end
167
+
168
+ # Sets verify mode of OpenSSL. New value must be a combination of
169
+ # constants OpenSSL::SSL::VERIFY_*
170
+ #
171
+ # Calling this method resets all existing sessions.
172
+ def verify_mode=(verify_mode)
173
+ @verify_mode = verify_mode
174
+ change_notify
175
+ end
176
+
177
+ # Sets verify depth. New value must be a number.
178
+ #
179
+ # Calling this method resets all existing sessions.
180
+ def verify_depth=(verify_depth)
181
+ @verify_depth = verify_depth
182
+ change_notify
183
+ end
184
+
185
+ # Sets callback handler for custom certificate verification.
186
+ # See verify_callback.
187
+ #
188
+ # Calling this method resets all existing sessions.
189
+ def verify_callback=(verify_callback)
190
+ @verify_callback = verify_callback
191
+ change_notify
192
+ end
193
+
194
+ # Sets SSL timeout in sec.
195
+ #
196
+ # Calling this method resets all existing sessions.
197
+ def timeout=(timeout)
198
+ @timeout = timeout
199
+ change_notify
200
+ end
201
+
202
+ # Sets SSL options. New value must be a combination of # constants
203
+ # OpenSSL::SSL::OP_*
204
+ #
205
+ # Calling this method resets all existing sessions.
206
+ def options=(options)
207
+ @options = options
208
+ change_notify
209
+ end
210
+
211
+ # Sets cipher configuration. New value must be a String.
212
+ #
213
+ # Calling this method resets all existing sessions.
214
+ def ciphers=(ciphers)
215
+ @ciphers = ciphers
216
+ change_notify
217
+ end
218
+
219
+ def client_ca=(client_ca) # :nodoc:
220
+ @client_ca = client_ca
221
+ change_notify
222
+ end
223
+
224
+ # interfaces for SSLSocketWrap.
225
+ def set_context(ctx) # :nodoc:
226
+ # Verification: Use Store#verify_callback instead of SSLContext#verify*?
227
+ ctx.cert_store = @cert_store
228
+ ctx.verify_mode = @verify_mode
229
+ ctx.verify_depth = @verify_depth if @verify_depth
230
+ ctx.verify_callback = @verify_callback || method(:default_verify_callback)
231
+ # SSL config
232
+ ctx.cert = @client_cert
233
+ ctx.key = @client_key
234
+ ctx.client_ca = @client_ca
235
+ ctx.timeout = @timeout
236
+ ctx.options = @options
237
+ ctx.ciphers = @ciphers
238
+ end
239
+
240
+ # post connection check proc for ruby < 1.8.5.
241
+ # this definition must match with the one in ext/openssl/lib/openssl/ssl.rb
242
+ def post_connection_check(peer_cert, hostname) # :nodoc:
243
+ check_common_name = true
244
+ cert = peer_cert
245
+ cert.extensions.each{|ext|
246
+ next if ext.oid != "subjectAltName"
247
+ ext.value.split(/,\s+/).each{|general_name|
248
+ if /\ADNS:(.*)/ =~ general_name
249
+ check_common_name = false
250
+ reg = Regexp.escape($1).gsub(/\\\*/, "[^.]+")
251
+ return true if /\A#{reg}\z/i =~ hostname
252
+ elsif /\AIP Address:(.*)/ =~ general_name
253
+ check_common_name = false
254
+ return true if $1 == hostname
255
+ end
256
+ }
257
+ }
258
+ if check_common_name
259
+ cert.subject.to_a.each{|oid, value|
260
+ if oid == "CN"
261
+ reg = Regexp.escape(value).gsub(/\\\*/, "[^.]+")
262
+ return true if /\A#{reg}\z/i =~ hostname
263
+ end
264
+ }
265
+ end
266
+ raise SSL::SSLError, "hostname was not match with the server certificate"
267
+ end
268
+
269
+ # Default callback for verification: only dumps error.
270
+ def default_verify_callback(is_ok, ctx)
271
+ if $DEBUG
272
+ puts "#{ is_ok ? 'ok' : 'ng' }: #{ctx.current_cert.subject}"
273
+ end
274
+ if !is_ok
275
+ depth = ctx.error_depth
276
+ code = ctx.error
277
+ msg = ctx.error_string
278
+ STDERR.puts "at depth #{depth} - #{code}: #{msg}"
279
+ end
280
+ is_ok
281
+ end
282
+
283
+ # Sample callback method: CAUTION: does not check CRL/ARL.
284
+ def sample_verify_callback(is_ok, ctx)
285
+ unless is_ok
286
+ depth = ctx.error_depth
287
+ code = ctx.error
288
+ msg = ctx.error_string
289
+ STDERR.puts "at depth #{depth} - #{code}: #{msg}" if $DEBUG
290
+ return false
291
+ end
292
+
293
+ cert = ctx.current_cert
294
+ self_signed = false
295
+ ca = false
296
+ pathlen = nil
297
+ server_auth = true
298
+ self_signed = (cert.subject.cmp(cert.issuer) == 0)
299
+
300
+ # Check extensions whatever its criticality is. (sample)
301
+ cert.extensions.each do |ex|
302
+ case ex.oid
303
+ when 'basicConstraints'
304
+ /CA:(TRUE|FALSE), pathlen:(\d+)/ =~ ex.value
305
+ ca = ($1 == 'TRUE')
306
+ pathlen = $2.to_i
307
+ when 'keyUsage'
308
+ usage = ex.value.split(/\s*,\s*/)
309
+ ca = usage.include?('Certificate Sign')
310
+ server_auth = usage.include?('Key Encipherment')
311
+ when 'extendedKeyUsage'
312
+ usage = ex.value.split(/\s*,\s*/)
313
+ server_auth = usage.include?('Netscape Server Gated Crypto')
314
+ when 'nsCertType'
315
+ usage = ex.value.split(/\s*,\s*/)
316
+ ca = usage.include?('SSL CA')
317
+ server_auth = usage.include?('SSL Server')
318
+ end
319
+ end
320
+
321
+ if self_signed
322
+ STDERR.puts 'self signing CA' if $DEBUG
323
+ return true
324
+ elsif ca
325
+ STDERR.puts 'middle level CA' if $DEBUG
326
+ return true
327
+ elsif server_auth
328
+ STDERR.puts 'for server authentication' if $DEBUG
329
+ return true
330
+ end
331
+
332
+ return false
333
+ end
334
+
335
+ private
336
+
337
+ def change_notify
338
+ @client.reset_all
339
+ end
340
+
341
+ def load_cacerts
342
+ file = File.join(File.dirname(__FILE__), 'cacert.p7s')
343
+ if File.exist?(file)
344
+ dist_cert =<<__DIST_CERT__
345
+ -----BEGIN CERTIFICATE-----
346
+ MIIC/jCCAmegAwIBAgIBATANBgkqhkiG9w0BAQUFADBNMQswCQYDVQQGEwJKUDER
347
+ MA8GA1UECgwIY3Rvci5vcmcxFDASBgNVBAsMC0RldmVsb3BtZW50MRUwEwYDVQQD
348
+ DAxodHRwLWFjY2VzczIwHhcNMDcwOTExMTM1ODMxWhcNMDkwOTEwMTM1ODMxWjBN
349
+ MQswCQYDVQQGEwJKUDERMA8GA1UECgwIY3Rvci5vcmcxFDASBgNVBAsMC0RldmVs
350
+ b3BtZW50MRUwEwYDVQQDDAxodHRwLWFjY2VzczIwgZ8wDQYJKoZIhvcNAQEBBQAD
351
+ gY0AMIGJAoGBALi66ujWtUCQm5HpMSyr/AAIFYVXC/dmn7C8TR/HMiUuW3waY4uX
352
+ LFqCDAGOX4gf177pX+b99t3mpaiAjJuqc858D9xEECzhDWgXdLbhRqWhUOble4RY
353
+ c1yWYC990IgXJDMKx7VAuZ3cBhdBxtlE9sb1ZCzmHQsvTy/OoRzcJCrTAgMBAAGj
354
+ ge0wgeowDwYDVR0TAQH/BAUwAwEB/zAxBglghkgBhvhCAQ0EJBYiUnVieS9PcGVu
355
+ U1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUJNE0GGaRKmN2qhnO
356
+ FyBWVl4Qj6owDgYDVR0PAQH/BAQDAgEGMHUGA1UdIwRuMGyAFCTRNBhmkSpjdqoZ
357
+ zhcgVlZeEI+qoVGkTzBNMQswCQYDVQQGEwJKUDERMA8GA1UECgwIY3Rvci5vcmcx
358
+ FDASBgNVBAsMC0RldmVsb3BtZW50MRUwEwYDVQQDDAxodHRwLWFjY2VzczKCAQEw
359
+ DQYJKoZIhvcNAQEFBQADgYEAH11tstSUuqFpMqoh/vM5l3Nqb8ygblbqEYQs/iG/
360
+ UeQkOZk/P1TxB6Ozn2htJ1srqDpUsncFVZ/ecP19GkeOZ6BmIhppcHhE5WyLBcPX
361
+ It5q1BW0PiAzT9LlEGoaiW0nw39so0Pr1whJDfc1t4fjdk+kSiMIzRHbTDvHWfpV
362
+ nTA=
363
+ -----END CERTIFICATE-----
364
+ __DIST_CERT__
365
+ p7 = PKCS7.read_smime(File.open(file) { |f| f.read })
366
+ selfcert = X509::Certificate.new(dist_cert)
367
+ store = X509::Store.new
368
+ store.add_cert(selfcert)
369
+ if (p7.verify(nil, store, p7.data, 0))
370
+ set_trust_ca(file)
371
+ else
372
+ STDERR.puts("cacerts: #{file} loading failed")
373
+ end
374
+ end
375
+ end
376
+ end
377
+
378
+
379
+ end
@@ -0,0 +1,122 @@
1
+ # HTTPClient - HTTP client library.
2
+ # Copyright (C) 2000-2008 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 'timeout'
10
+ require 'thread'
11
+
12
+
13
+ class HTTPClient
14
+
15
+
16
+ # Replaces timeout.rb to avoid Thread creation and scheduling overhead.
17
+ #
18
+ # You should check another timeout replace in WEBrick.
19
+ # See lib/webrick/utils.rb in ruby/1.9.
20
+ #
21
+ # About this implementation:
22
+ # * Do not create Thread for each timeout() call. Just create 1 Thread for
23
+ # timeout scheduler.
24
+ # * Do not wakeup the scheduler thread so often. Let scheduler thread sleep
25
+ # until the nearest period.
26
+ class TimeoutScheduler
27
+
28
+ # Represents timeout period.
29
+ class Period
30
+ attr_reader :thread, :time
31
+
32
+ # Creates new Period.
33
+ def initialize(thread, time, ex)
34
+ @thread, @time, @ex = thread, time, ex
35
+ @lock = Mutex.new
36
+ end
37
+
38
+ # Raises if thread exists and alive.
39
+ def raise(message)
40
+ @lock.synchronize do
41
+ if @thread and @thread.alive?
42
+ @thread.raise(@ex, message)
43
+ end
44
+ end
45
+ end
46
+
47
+ # Cancel this Period. Mutex is needed to avoid too-late exception.
48
+ def cancel
49
+ @lock.synchronize do
50
+ @thread = nil
51
+ end
52
+ end
53
+ end
54
+
55
+ # Creates new TimeoutScheduler.
56
+ def initialize
57
+ @pool = {}
58
+ @next = nil
59
+ @thread = start_timer_thread
60
+ Thread.pass while @thread.status != 'sleep'
61
+ end
62
+
63
+ # Registers new timeout period.
64
+ def register(thread, sec, ex)
65
+ period = Period.new(thread, Time.now + sec, ex || ::Timeout::Error)
66
+ @pool[period] = true
67
+ if @next.nil? or period.time < @next
68
+ @thread.wakeup
69
+ end
70
+ period
71
+ end
72
+
73
+ # Cancels the given period.
74
+ def cancel(period)
75
+ @pool.delete(period)
76
+ period.cancel
77
+ end
78
+
79
+ private
80
+
81
+ def start_timer_thread
82
+ Thread.new {
83
+ while true
84
+ if @pool.empty?
85
+ @next = nil
86
+ sleep
87
+ else
88
+ min, = @pool.min { |a, b| a[0].time <=> b[0].time }
89
+ @next = min.time
90
+ sec = @next - Time.now
91
+ if sec > 0
92
+ sleep(sec)
93
+ end
94
+ end
95
+ now = Time.now
96
+ @pool.keys.each do |period|
97
+ if period.time < now
98
+ period.raise('execution expired')
99
+ cancel(period)
100
+ end
101
+ end
102
+ end
103
+ }
104
+ end
105
+ end
106
+
107
+ TIMEOUT_SCHEDULER = TimeoutScheduler.new
108
+
109
+ module Timeout
110
+ def timeout(sec, ex = nil, &block)
111
+ return yield if sec == nil or sec.zero?
112
+ begin
113
+ period = TIMEOUT_SCHEDULER.register(Thread.current, sec, ex)
114
+ yield(sec)
115
+ ensure
116
+ TIMEOUT_SCHEDULER.cancel(period)
117
+ end
118
+ end
119
+ end
120
+
121
+
122
+ end