httpclient 2.1.2 → 2.1.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.
@@ -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