httpclient 2.1.0

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,54 @@
1
+ # HTTPAccess2 - HTTP accessing library.
2
+ # Copyright (C) 2000-2007 NAKAMURA, Hiroshi <nakahiro@sarion.co.jp>.
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
+ # http-access2.rb is based on http-access.rb in http-access/0.0.4. Some part
9
+ # of code in http-access.rb was recycled in http-access2.rb. Those part is
10
+ # copyrighted by Maehashi-san.
11
+
12
+
13
+ require 'httpclient'
14
+
15
+
16
+ module HTTPAccess2
17
+ VERSION = ::HTTPClient::VERSION
18
+ RUBY_VERSION_STRING = ::HTTPClient::RUBY_VERSION_STRING
19
+ RCS_FILE, RCS_REVISION = ::HTTPClient::RCS_FILE, ::HTTPClient::RCS_REVISION
20
+ SSLEnabled = ::HTTPClient::SSLEnabled
21
+ SSPIEnabled = ::HTTPClient::SSPIEnabled
22
+ DEBUG_SSL = ::HTTPClient::DEBUG_SSL
23
+
24
+ Util = ::HTTPClient::Util
25
+
26
+ class Client < ::HTTPClient
27
+ class RetryableResponse < StandardError
28
+ end
29
+ end
30
+
31
+ SSLConfig = ::HTTPClient::SSLConfig
32
+ BasicAuth = ::HTTPClient::BasicAuth
33
+ DigestAuth = ::HTTPClient::DigestAuth
34
+ NegotiateAuth = ::HTTPClient::NegotiateAuth
35
+ AuthFilterBase = ::HTTPClient::AuthFilterBase
36
+ WWWAuth = ::HTTPClient::WWWAuth
37
+ ProxyAuth = ::HTTPClient::ProxyAuth
38
+ Site = ::HTTPClient::Site
39
+ Connection = ::HTTPClient::Connection
40
+ SessionManager = ::HTTPClient::SessionManager
41
+ SSLSocketWrap = ::HTTPClient::SSLSocketWrap
42
+ DebugSocket = ::HTTPClient::DebugSocket
43
+
44
+ class Session < ::HTTPClient::Session
45
+ class Error < StandardError
46
+ end
47
+ class InvalidState < Error
48
+ end
49
+ class BadResponse < Error
50
+ end
51
+ class KeepAliveDisconnected < Error
52
+ end
53
+ end
54
+ end
@@ -0,0 +1 @@
1
+ require 'httpclient/cookie'
@@ -0,0 +1 @@
1
+ require 'httpclient/http'
@@ -0,0 +1,2036 @@
1
+ # HTTPClient - HTTP client library.
2
+ # Copyright (C) 2000-2007 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
+ # httpclient.rb is based on http-access.rb in http-access/0.0.4. Some part
9
+ # of code in http-access.rb was recycled in httpclient.rb. Those part is
10
+ # copyrighted by Maehashi-san.
11
+
12
+
13
+ # Ruby standard library
14
+ require 'timeout'
15
+ require 'uri'
16
+ require 'socket'
17
+ require 'thread'
18
+ require 'digest/md5'
19
+
20
+ # Extra library
21
+ require 'httpclient/http'
22
+ require 'httpclient/cookie'
23
+
24
+
25
+ # DESCRIPTION
26
+ # HTTPClient -- Client to retrieve web resources via HTTP.
27
+ #
28
+ # How to create your client.
29
+ # 1. Create simple client.
30
+ # clnt = HTTPClient.new
31
+ #
32
+ # 2. Accessing resources through HTTP proxy.
33
+ # clnt = HTTPClient.new("http://myproxy:8080")
34
+ #
35
+ # 3. Set User-Agent and From in HTTP request header.(nil means "No proxy")
36
+ # clnt = HTTPClient.new(nil, "MyAgent", "nahi@keynauts.com")
37
+ #
38
+ # How to retrieve web resources.
39
+ # 1. Get content of specified URL.
40
+ # puts clnt.get_content("http://www.ruby-lang.org/en/")
41
+ #
42
+ # 2. Do HEAD request.
43
+ # res = clnt.head(uri)
44
+ #
45
+ # 3. Do GET request with query.
46
+ # res = clnt.get(uri)
47
+ #
48
+ # 4. Do POST request.
49
+ # res = clnt.post(uri)
50
+ # res = clnt.get|post|head(uri, proxy)
51
+ #
52
+ class HTTPClient
53
+
54
+ VERSION = '2.1.0'
55
+ RUBY_VERSION_STRING = "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"
56
+ s = %w$Id: httpclient.rb 172 2007-07-14 06:56:59Z nahi $
57
+ RCS_FILE, RCS_REVISION = s[1][/.*(?=,v$)/], s[2]
58
+
59
+ SSLEnabled = begin
60
+ require 'openssl'
61
+ true
62
+ rescue LoadError
63
+ false
64
+ end
65
+
66
+ SSPIEnabled = begin
67
+ require 'win32/sspi'
68
+ true
69
+ rescue LoadError
70
+ false
71
+ end
72
+
73
+ DEBUG_SSL = true
74
+
75
+
76
+ module Util
77
+ def urify(uri)
78
+ if uri.is_a?(URI)
79
+ uri
80
+ else
81
+ URI.parse(uri.to_s)
82
+ end
83
+ end
84
+
85
+ def uri_part_of(uri, part)
86
+ ((uri.scheme == part.scheme) and
87
+ (uri.host == part.host) and
88
+ (uri.port == part.port) and
89
+ uri.path.upcase.index(part.path.upcase) == 0)
90
+ end
91
+ module_function :uri_part_of
92
+
93
+ def uri_dirname(uri)
94
+ uri = uri.clone
95
+ uri.path = uri.path.sub(/\/[^\/]*\z/, '/')
96
+ uri
97
+ end
98
+ module_function :uri_dirname
99
+
100
+ def hash_find_value(hash)
101
+ hash.each do |k, v|
102
+ return v if yield(k, v)
103
+ end
104
+ nil
105
+ end
106
+ module_function :hash_find_value
107
+
108
+ def parse_challenge_param(param_str)
109
+ param = {}
110
+ param_str.scan(/\s*([^\,]+(?:\\.[^\,]*)*)/).each do |str|
111
+ key, value = str[0].scan(/\A([^=]+)=(.*)\z/)[0]
112
+ if /\A"(.*)"\z/ =~ value
113
+ value = $1.gsub(/\\(.)/, '\1')
114
+ end
115
+ param[key] = value
116
+ end
117
+ param
118
+ end
119
+ module_function :parse_challenge_param
120
+ end
121
+
122
+
123
+
124
+ # HTTPClient::SSLConfig -- SSL configuration of a client.
125
+ #
126
+ class SSLConfig # :nodoc:
127
+ attr_reader :client_cert
128
+ attr_reader :client_key
129
+ attr_reader :client_ca
130
+
131
+ attr_reader :verify_mode
132
+ attr_reader :verify_depth
133
+ attr_reader :verify_callback
134
+
135
+ attr_reader :timeout
136
+ attr_reader :options
137
+ attr_reader :ciphers
138
+
139
+ attr_reader :cert_store # don't use if you don't know what it is.
140
+
141
+ def initialize(client)
142
+ return unless SSLEnabled
143
+ @client = client
144
+ @cert_store = OpenSSL::X509::Store.new
145
+ @client_cert = @client_key = @client_ca = nil
146
+ @verify_mode = OpenSSL::SSL::VERIFY_PEER |
147
+ OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
148
+ @verify_depth = nil
149
+ @verify_callback = nil
150
+ @dest = nil
151
+ @timeout = nil
152
+ @options = defined?(OpenSSL::SSL::OP_ALL) ?
153
+ OpenSSL::SSL::OP_ALL | OpenSSL::SSL::OP_NO_SSLv2 : nil
154
+ @ciphers = "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH"
155
+ load_cacerts
156
+ end
157
+
158
+ def set_client_cert_file(cert_file, key_file)
159
+ @client_cert = OpenSSL::X509::Certificate.new(File.open(cert_file).read)
160
+ @client_key = OpenSSL::PKey::RSA.new(File.open(key_file).read)
161
+ change_notify
162
+ end
163
+
164
+ def clear_cert_store
165
+ @cert_store = OpenSSL::X509::Store.new
166
+ change_notify
167
+ end
168
+
169
+ def set_trust_ca(trust_ca_file_or_hashed_dir)
170
+ if FileTest.directory?(trust_ca_file_or_hashed_dir)
171
+ @cert_store.add_path(trust_ca_file_or_hashed_dir)
172
+ else
173
+ @cert_store.add_file(trust_ca_file_or_hashed_dir)
174
+ end
175
+ change_notify
176
+ end
177
+
178
+ def set_crl(crl_file)
179
+ crl = OpenSSL::X509::CRL.new(File.open(crl_file).read)
180
+ @cert_store.add_crl(crl)
181
+ @cert_store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK | OpenSSL::X509::V_FLAG_CRL_CHECK_ALL
182
+ change_notify
183
+ end
184
+
185
+ def client_cert=(client_cert)
186
+ @client_cert = client_cert
187
+ change_notify
188
+ end
189
+
190
+ def client_key=(client_key)
191
+ @client_key = client_key
192
+ change_notify
193
+ end
194
+
195
+ def client_ca=(client_ca)
196
+ @client_ca = client_ca
197
+ change_notify
198
+ end
199
+
200
+ def verify_mode=(verify_mode)
201
+ @verify_mode = verify_mode
202
+ change_notify
203
+ end
204
+
205
+ def verify_depth=(verify_depth)
206
+ @verify_depth = verify_depth
207
+ change_notify
208
+ end
209
+
210
+ def verify_callback=(verify_callback)
211
+ @verify_callback = verify_callback
212
+ change_notify
213
+ end
214
+
215
+ def timeout=(timeout)
216
+ @timeout = timeout
217
+ change_notify
218
+ end
219
+
220
+ def options=(options)
221
+ @options = options
222
+ change_notify
223
+ end
224
+
225
+ def ciphers=(ciphers)
226
+ @ciphers = ciphers
227
+ change_notify
228
+ end
229
+
230
+ # don't use if you don't know what it is.
231
+ def cert_store=(cert_store)
232
+ @cert_store = cert_store
233
+ change_notify
234
+ end
235
+
236
+ # interfaces for SSLSocketWrap.
237
+
238
+ def set_context(ctx)
239
+ # Verification: Use Store#verify_callback instead of SSLContext#verify*?
240
+ ctx.cert_store = @cert_store
241
+ ctx.verify_mode = @verify_mode
242
+ ctx.verify_depth = @verify_depth if @verify_depth
243
+ ctx.verify_callback = @verify_callback || method(:default_verify_callback)
244
+ # SSL config
245
+ ctx.cert = @client_cert
246
+ ctx.key = @client_key
247
+ ctx.client_ca = @client_ca
248
+ ctx.timeout = @timeout
249
+ ctx.options = @options
250
+ ctx.ciphers = @ciphers
251
+ end
252
+
253
+ # this definition must match with the one in ext/openssl/lib/openssl/ssl.rb
254
+ def post_connection_check(peer_cert, hostname)
255
+ check_common_name = true
256
+ cert = peer_cert
257
+ cert.extensions.each{|ext|
258
+ next if ext.oid != "subjectAltName"
259
+ ext.value.split(/,\s+/).each{|general_name|
260
+ if /\ADNS:(.*)/ =~ general_name
261
+ check_common_name = false
262
+ reg = Regexp.escape($1).gsub(/\\\*/, "[^.]+")
263
+ return true if /\A#{reg}\z/i =~ hostname
264
+ elsif /\AIP Address:(.*)/ =~ general_name
265
+ check_common_name = false
266
+ return true if $1 == hostname
267
+ end
268
+ }
269
+ }
270
+ if check_common_name
271
+ cert.subject.to_a.each{|oid, value|
272
+ if oid == "CN"
273
+ reg = Regexp.escape(value).gsub(/\\\*/, "[^.]+")
274
+ return true if /\A#{reg}\z/i =~ hostname
275
+ end
276
+ }
277
+ end
278
+ raise OpenSSL::SSL::SSLError, "hostname not match"
279
+ end
280
+
281
+ # Default callback for verification: only dumps error.
282
+ def default_verify_callback(is_ok, ctx)
283
+ if $DEBUG
284
+ puts "#{ is_ok ? 'ok' : 'ng' }: #{ctx.current_cert.subject}"
285
+ end
286
+ if !is_ok
287
+ depth = ctx.error_depth
288
+ code = ctx.error
289
+ msg = ctx.error_string
290
+ STDERR.puts "at depth #{depth} - #{code}: #{msg}"
291
+ end
292
+ is_ok
293
+ end
294
+
295
+ # Sample callback method: CAUTION: does not check CRL/ARL.
296
+ def sample_verify_callback(is_ok, ctx)
297
+ unless is_ok
298
+ depth = ctx.error_depth
299
+ code = ctx.error
300
+ msg = ctx.error_string
301
+ STDERR.puts "at depth #{depth} - #{code}: #{msg}" if $DEBUG
302
+ return false
303
+ end
304
+
305
+ cert = ctx.current_cert
306
+ self_signed = false
307
+ ca = false
308
+ pathlen = nil
309
+ server_auth = true
310
+ self_signed = (cert.subject.cmp(cert.issuer) == 0)
311
+
312
+ # Check extensions whatever its criticality is. (sample)
313
+ cert.extensions.each do |ex|
314
+ case ex.oid
315
+ when 'basicConstraints'
316
+ /CA:(TRUE|FALSE), pathlen:(\d+)/ =~ ex.value
317
+ ca = ($1 == 'TRUE')
318
+ pathlen = $2.to_i
319
+ when 'keyUsage'
320
+ usage = ex.value.split(/\s*,\s*/)
321
+ ca = usage.include?('Certificate Sign')
322
+ server_auth = usage.include?('Key Encipherment')
323
+ when 'extendedKeyUsage'
324
+ usage = ex.value.split(/\s*,\s*/)
325
+ server_auth = usage.include?('Netscape Server Gated Crypto')
326
+ when 'nsCertType'
327
+ usage = ex.value.split(/\s*,\s*/)
328
+ ca = usage.include?('SSL CA')
329
+ server_auth = usage.include?('SSL Server')
330
+ end
331
+ end
332
+
333
+ if self_signed
334
+ STDERR.puts 'self signing CA' if $DEBUG
335
+ return true
336
+ elsif ca
337
+ STDERR.puts 'middle level CA' if $DEBUG
338
+ return true
339
+ elsif server_auth
340
+ STDERR.puts 'for server authentication' if $DEBUG
341
+ return true
342
+ end
343
+
344
+ return false
345
+ end
346
+
347
+ private
348
+
349
+ def change_notify
350
+ @client.reset_all
351
+ end
352
+
353
+ def load_cacerts
354
+ file = File.join(File.dirname(__FILE__), 'httpclient', 'cacert.p7s')
355
+ if File.exist?(file)
356
+ require 'openssl'
357
+ dist_cert =<<__DIST_CERT__
358
+ -----BEGIN CERTIFICATE-----
359
+ MIIC/jCCAmegAwIBAgIBADANBgkqhkiG9w0BAQUFADBNMQswCQYDVQQGEwJKUDER
360
+ MA8GA1UECgwIY3Rvci5vcmcxFDASBgNVBAsMC0RldmVsb3BtZW50MRUwEwYDVQQD
361
+ DAxodHRwLWFjY2VzczIwHhcNMDcwNDI4MjM1NTI0WhcNMDkwNDI3MjM1NTI0WjBN
362
+ MQswCQYDVQQGEwJKUDERMA8GA1UECgwIY3Rvci5vcmcxFDASBgNVBAsMC0RldmVs
363
+ b3BtZW50MRUwEwYDVQQDDAxodHRwLWFjY2VzczIwgZ8wDQYJKoZIhvcNAQEBBQAD
364
+ gY0AMIGJAoGBALi66ujWtUCQm5HpMSyr/AAIFYVXC/dmn7C8TR/HMiUuW3waY4uX
365
+ LFqCDAGOX4gf177pX+b99t3mpaiAjJuqc858D9xEECzhDWgXdLbhRqWhUOble4RY
366
+ c1yWYC990IgXJDMKx7VAuZ3cBhdBxtlE9sb1ZCzmHQsvTy/OoRzcJCrTAgMBAAGj
367
+ ge0wgeowDwYDVR0TAQH/BAUwAwEB/zAxBglghkgBhvhCAQ0EJBYiUnVieS9PcGVu
368
+ U1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUJNE0GGaRKmN2qhnO
369
+ FyBWVl4Qj6owDgYDVR0PAQH/BAQDAgEGMHUGA1UdIwRuMGyAFCTRNBhmkSpjdqoZ
370
+ zhcgVlZeEI+qoVGkTzBNMQswCQYDVQQGEwJKUDERMA8GA1UECgwIY3Rvci5vcmcx
371
+ FDASBgNVBAsMC0RldmVsb3BtZW50MRUwEwYDVQQDDAxodHRwLWFjY2VzczKCAQAw
372
+ DQYJKoZIhvcNAQEFBQADgYEAcBAe+z1vn9CnrN8zZkb+mN1Uw3S0vUeebHtlXNUa
373
+ orzaGDx5uOiisUyAgTAlnDYQJ0i5M2tQVCAazicAo1dbEM+F2pvhJfRrDJic+rR+
374
+ tmZsdMCfuxCsy8gnYUT9PXbO/RMz7nvXZqxAcW5Ks1Stv0cTVjxL5IjCkfvAr/fj
375
+ XQ0=
376
+ -----END CERTIFICATE-----
377
+ __DIST_CERT__
378
+ p7 = OpenSSL::PKCS7.read_smime(File.open(file) { |f| f.read })
379
+ selfcert = OpenSSL::X509::Certificate.new(dist_cert)
380
+ store = OpenSSL::X509::Store.new
381
+ store.add_cert(selfcert)
382
+ if (p7.verify(nil, store, p7.data, 0))
383
+ set_trust_ca(file)
384
+ else
385
+ STDERR.puts("cacerts: #{file} loading failed")
386
+ end
387
+ end
388
+ end
389
+ end
390
+
391
+
392
+ # HTTPClient::BasicAuth -- BasicAuth repository.
393
+ #
394
+ class BasicAuth # :nodoc:
395
+ attr_reader :scheme
396
+
397
+ def initialize
398
+ @cred = nil
399
+ @auth = {}
400
+ @challengeable = {}
401
+ @scheme = "Basic"
402
+ end
403
+
404
+ def reset_challenge
405
+ @challengeable.clear
406
+ end
407
+
408
+ # uri == nil for generic purpose
409
+ def set(uri, user, passwd)
410
+ if uri.nil?
411
+ @cred = ["#{user}:#{passwd}"].pack('m').tr("\n", '')
412
+ else
413
+ uri = Util.uri_dirname(uri)
414
+ @auth[uri] = ["#{user}:#{passwd}"].pack('m').tr("\n", '')
415
+ end
416
+ end
417
+
418
+ # send cred only when a given uri is;
419
+ # - child page of challengeable(got WWW-Authenticate before) uri and,
420
+ # - child page of defined credential
421
+ def get(req)
422
+ target_uri = req.header.request_uri
423
+ return nil unless @challengeable.find { |uri, ok|
424
+ Util.uri_part_of(target_uri, uri) and ok
425
+ }
426
+ return @cred if @cred
427
+ Util.hash_find_value(@auth) { |uri, cred|
428
+ Util.uri_part_of(target_uri, uri)
429
+ }
430
+ end
431
+
432
+ def challenge(uri, param_str)
433
+ @challengeable[uri] = true
434
+ true
435
+ end
436
+ end
437
+
438
+
439
+ # HTTPClient::DigestAuth
440
+ #
441
+ class DigestAuth # :nodoc:
442
+ attr_reader :scheme
443
+
444
+ def initialize
445
+ @auth = {}
446
+ @challenge = {}
447
+ @nonce_count = 0
448
+ @scheme = "Digest"
449
+ end
450
+
451
+ def reset_challenge
452
+ @challenge.clear
453
+ end
454
+
455
+ def set(uri, user, passwd)
456
+ uri = Util.uri_dirname(uri)
457
+ @auth[uri] = [user, passwd]
458
+ end
459
+
460
+ # send cred only when a given uri is;
461
+ # - child page of challengeable(got WWW-Authenticate before) uri and,
462
+ # - child page of defined credential
463
+ def get(req)
464
+ target_uri = req.header.request_uri
465
+ param = Util.hash_find_value(@challenge) { |uri, param|
466
+ Util.uri_part_of(target_uri, uri)
467
+ }
468
+ return nil unless param
469
+ user, passwd = Util.hash_find_value(@auth) { |uri, auth_data|
470
+ Util.uri_part_of(target_uri, uri)
471
+ }
472
+ return nil unless user
473
+ uri = req.header.request_uri
474
+ calc_cred(req.header.request_method, uri, user, passwd, param)
475
+ end
476
+
477
+ def challenge(uri, param_str)
478
+ @challenge[uri] = Util.parse_challenge_param(param_str)
479
+ true
480
+ end
481
+
482
+ private
483
+
484
+ # this method is implemented by sromano and posted to
485
+ # http://tools.assembla.com/breakout/wiki/DigestForSoap
486
+ # Thanks!
487
+ # supported algorithm: MD5 only for now
488
+ def calc_cred(method, uri, user, passwd, param)
489
+ a_1 = "#{user}:#{param['realm']}:#{passwd}"
490
+ a_2 = "#{method}:#{uri.path}"
491
+ @nonce_count += 1
492
+ message_digest = []
493
+ message_digest << Digest::MD5.hexdigest(a_1)
494
+ message_digest << param['nonce']
495
+ message_digest << ('%08x' % @nonce_count)
496
+ message_digest << param['nonce']
497
+ message_digest << param['qop']
498
+ message_digest << Digest::MD5.hexdigest(a_2)
499
+ header = []
500
+ header << "username=\"#{user}\""
501
+ header << "realm=\"#{param['realm']}\""
502
+ header << "nonce=\"#{param['nonce']}\""
503
+ header << "uri=\"#{uri.path}\""
504
+ header << "cnonce=\"#{param['nonce']}\""
505
+ header << "nc=#{'%08x' % @nonce_count}"
506
+ header << "qop=\"#{param['qop']}\""
507
+ header << "response=\"#{Digest::MD5.hexdigest(message_digest.join(":"))}\""
508
+ header << "algorithm=\"MD5\""
509
+ header << "opaque=\"#{param['opaque']}\"" if param.key?('opaque')
510
+ header.join(", ")
511
+ end
512
+ end
513
+
514
+
515
+ # HTTPClient::NegotiateAuth
516
+ #
517
+ class NegotiateAuth # :nodoc:
518
+ attr_reader :scheme
519
+
520
+ def initialize
521
+ @challenge = {}
522
+ @scheme = "Negotiate"
523
+ end
524
+
525
+ def reset_challenge
526
+ @challenge.clear
527
+ end
528
+
529
+ def get(req)
530
+ return nil unless SSPIEnabled
531
+ target_uri = req.header.request_uri
532
+ param = Util.hash_find_value(@challenge) { |uri, param|
533
+ Util.uri_part_of(target_uri, uri)
534
+ }
535
+ return nil unless param
536
+ state = param[:state]
537
+ authenticator = param[:authenticator]
538
+ authphrase = param[:authphrase]
539
+ case state
540
+ when :init
541
+ authenticator = param[:authenticator] = Win32::SSPI::NegotiateAuth.new
542
+ return authenticator.get_initial_token
543
+ when :response
544
+ return authenticator.complete_authentication(authphrase)
545
+ end
546
+ nil
547
+ end
548
+
549
+ def challenge(uri, param_str)
550
+ return false unless SSPIEnabled
551
+ if param_str.nil? or @challenge[uri].nil?
552
+ c = @challenge[uri] = {}
553
+ c[:state] = :init
554
+ c[:authenticator] = nil
555
+ c[:authphrase] = ""
556
+ else
557
+ c = @challenge[uri]
558
+ c[:state] = :response
559
+ c[:authphrase] = param_str
560
+ end
561
+ true
562
+ end
563
+ end
564
+
565
+
566
+ class AuthFilterBase # :nodoc:
567
+ private
568
+
569
+ def parse_authentication_header(res, tag)
570
+ challenge = res.header[tag]
571
+ unless challenge
572
+ raise RuntimeError.new("no #{tag} header exists: #{res}")
573
+ end
574
+ challenge.collect { |c| parse_challenge_header(c) }
575
+ end
576
+
577
+ def parse_challenge_header(challenge)
578
+ scheme, param_str = challenge.scan(/\A(\S+)(?:\s+(.*))?\z/)[0]
579
+ if scheme.nil?
580
+ raise RuntimeError.new("unsupported challenge: #{challenge}")
581
+ end
582
+ return scheme, param_str
583
+ end
584
+ end
585
+
586
+
587
+ class WWWAuth < AuthFilterBase # :nodoc:
588
+ attr_reader :basic_auth
589
+ attr_reader :digest_auth
590
+ attr_reader :negotiate_auth
591
+
592
+ def initialize
593
+ @basic_auth = BasicAuth.new
594
+ @digest_auth = DigestAuth.new
595
+ @negotiate_auth = NegotiateAuth.new
596
+ # sort authenticators by priority
597
+ @authenticator = [@negotiate_auth, @digest_auth, @basic_auth]
598
+ end
599
+
600
+ def reset_challenge
601
+ @authenticator.each do |auth|
602
+ auth.reset_challenge
603
+ end
604
+ end
605
+
606
+ def set_auth(uri, user, passwd)
607
+ @basic_auth.set(uri, user, passwd)
608
+ @digest_auth.set(uri, user, passwd)
609
+ end
610
+
611
+ def filter_request(req)
612
+ @authenticator.each do |auth|
613
+ if cred = auth.get(req)
614
+ req.header.set('Authorization', auth.scheme + " " + cred)
615
+ return
616
+ end
617
+ end
618
+ end
619
+
620
+ def filter_response(req, res)
621
+ command = nil
622
+ uri = req.header.request_uri
623
+ if res.status == HTTP::Status::UNAUTHORIZED
624
+ if challenge = parse_authentication_header(res, 'www-authenticate')
625
+ challenge.each do |scheme, param_str|
626
+ @authenticator.each do |auth|
627
+ if scheme.downcase == auth.scheme.downcase
628
+ challengeable = auth.challenge(uri, param_str)
629
+ command = :retry if challengeable
630
+ end
631
+ end
632
+ end
633
+ # ignore unknown authentication scheme
634
+ end
635
+ end
636
+ command
637
+ end
638
+ end
639
+
640
+
641
+ class ProxyAuth < AuthFilterBase # :nodoc:
642
+ attr_reader :basic_auth
643
+ attr_reader :negotiate_auth
644
+
645
+ def initialize
646
+ @basic_auth = BasicAuth.new
647
+ @negotiate_auth = NegotiateAuth.new
648
+ # sort authenticators by priority
649
+ @authenticator = [@negotiate_auth, @basic_auth]
650
+ end
651
+
652
+ def reset_challenge
653
+ @authenticator.each do |auth|
654
+ auth.reset_challenge
655
+ end
656
+ end
657
+
658
+ def set_auth(user, passwd)
659
+ @basic_auth.set(nil, user, passwd)
660
+ end
661
+
662
+ def filter_request(req)
663
+ @authenticator.each do |auth|
664
+ if cred = auth.get(req)
665
+ req.header.set('Proxy-Authorization', auth.scheme + " " + cred)
666
+ return
667
+ end
668
+ end
669
+ end
670
+
671
+ def filter_response(req, res)
672
+ command = nil
673
+ uri = req.header.request_uri
674
+ if res.status == HTTP::Status::PROXY_AUTHENTICATE_REQUIRED
675
+ if challenge = parse_authentication_header(res, 'proxy-authenticate')
676
+ challenge.each do |scheme, param_str|
677
+ @authenticator.each do |auth|
678
+ if scheme.downcase == auth.scheme.downcase
679
+ challengeable = auth.challenge(uri, param_str)
680
+ command = :retry if challengeable
681
+ end
682
+ end
683
+ end
684
+ # ignore unknown authentication scheme
685
+ end
686
+ end
687
+ command
688
+ end
689
+ end
690
+
691
+
692
+ # HTTPClient::Site -- manage a site(host and port)
693
+ #
694
+ class Site # :nodoc:
695
+ attr_accessor :scheme
696
+ attr_accessor :host
697
+ attr_reader :port
698
+
699
+ def initialize(uri = nil)
700
+ if uri
701
+ @uri = uri
702
+ @scheme = uri.scheme
703
+ @host = uri.host
704
+ @port = uri.port.to_i
705
+ else
706
+ @uri = nil
707
+ @scheme = 'tcp'
708
+ @host = '0.0.0.0'
709
+ @port = 0
710
+ end
711
+ end
712
+
713
+ def addr
714
+ "#{@scheme}://#{@host}:#{@port.to_s}"
715
+ end
716
+
717
+ def port=(port)
718
+ @port = port.to_i
719
+ end
720
+
721
+ def ==(rhs)
722
+ if rhs.is_a?(Site)
723
+ ((@scheme == rhs.scheme) and (@host == rhs.host) and (@port == rhs.port))
724
+ else
725
+ false
726
+ end
727
+ end
728
+
729
+ def to_s
730
+ addr
731
+ end
732
+
733
+ def inspect
734
+ sprintf("#<%s:0x%x %s>", self.class.name, __id__, @uri || addr)
735
+ end
736
+ end
737
+
738
+
739
+ # HTTPClient::Connection -- magage a connection(one request and response to it).
740
+ #
741
+ class Connection # :nodoc:
742
+ attr_accessor :async_thread
743
+
744
+ def initialize(header_queue = [], body_queue = [])
745
+ @headers = header_queue
746
+ @body = body_queue
747
+ @async_thread = nil
748
+ @queue = Queue.new
749
+ end
750
+
751
+ def finished?
752
+ if !@async_thread
753
+ # Not in async mode.
754
+ true
755
+ elsif @async_thread.alive?
756
+ # Working...
757
+ false
758
+ else
759
+ # Async thread have been finished.
760
+ @async_thread.join
761
+ true
762
+ end
763
+ end
764
+
765
+ def pop
766
+ @queue.pop
767
+ end
768
+
769
+ def push(result)
770
+ @queue.push(result)
771
+ end
772
+
773
+ def join
774
+ unless @async_thread
775
+ false
776
+ else
777
+ @async_thread.join
778
+ end
779
+ end
780
+ end
781
+
782
+
783
+ # HTTPClient::SessionManager -- manage several sessions.
784
+ #
785
+ class SessionManager # :nodoc:
786
+ attr_accessor :agent_name # Name of this client.
787
+ attr_accessor :from # Owner of this client.
788
+
789
+ attr_accessor :protocol_version # Requested protocol version
790
+ attr_accessor :chunk_size # Chunk size for chunked request
791
+ attr_accessor :debug_dev # Device for dumping log for debugging
792
+ attr_accessor :socket_sync # Boolean value for Socket#sync
793
+
794
+ # These parameters are not used now...
795
+ attr_accessor :connect_timeout
796
+ attr_accessor :connect_retry # Maximum retry count. 0 for infinite.
797
+ attr_accessor :send_timeout
798
+ attr_accessor :receive_timeout
799
+ attr_accessor :read_block_size
800
+
801
+ attr_accessor :ssl_config
802
+
803
+ def initialize
804
+ @proxy = nil
805
+
806
+ @agent_name = nil
807
+ @from = nil
808
+
809
+ @protocol_version = nil
810
+ @debug_dev = nil
811
+ @socket_sync = true
812
+ @chunk_size = 4096
813
+
814
+ @connect_timeout = 60
815
+ @connect_retry = 1
816
+ @send_timeout = 120
817
+ @receive_timeout = 60 # For each read_block_size bytes
818
+ @read_block_size = 8192
819
+
820
+ @ssl_config = nil
821
+
822
+ @sess_pool = []
823
+ @sess_pool_mutex = Mutex.new
824
+ end
825
+
826
+ def proxy=(proxy)
827
+ if proxy.nil?
828
+ @proxy = nil
829
+ else
830
+ @proxy = Site.new(proxy)
831
+ end
832
+ end
833
+
834
+ def query(req, proxy)
835
+ req.body.chunk_size = @chunk_size
836
+ dest_site = Site.new(req.header.request_uri)
837
+ proxy_site = if proxy
838
+ Site.new(proxy)
839
+ else
840
+ @proxy
841
+ end
842
+ sess = open(dest_site, proxy_site)
843
+ begin
844
+ sess.query(req)
845
+ rescue
846
+ sess.close
847
+ raise
848
+ end
849
+ sess
850
+ end
851
+
852
+ def reset(uri)
853
+ site = Site.new(uri)
854
+ close(site)
855
+ end
856
+
857
+ def reset_all
858
+ close_all
859
+ end
860
+
861
+ def keep(sess)
862
+ add_cached_session(sess)
863
+ end
864
+
865
+ private
866
+
867
+ def open(dest, proxy = nil)
868
+ sess = nil
869
+ if cached = get_cached_session(dest)
870
+ sess = cached
871
+ else
872
+ sess = Session.new(dest, @agent_name, @from)
873
+ sess.proxy = proxy
874
+ sess.socket_sync = @socket_sync
875
+ sess.requested_version = @protocol_version if @protocol_version
876
+ sess.connect_timeout = @connect_timeout
877
+ sess.connect_retry = @connect_retry
878
+ sess.send_timeout = @send_timeout
879
+ sess.receive_timeout = @receive_timeout
880
+ sess.read_block_size = @read_block_size
881
+ sess.ssl_config = @ssl_config
882
+ sess.debug_dev = @debug_dev
883
+ end
884
+ sess
885
+ end
886
+
887
+ def close_all
888
+ each_sess do |sess|
889
+ sess.close
890
+ end
891
+ @sess_pool.clear
892
+ end
893
+
894
+ def close(dest)
895
+ if cached = get_cached_session(dest)
896
+ cached.close
897
+ true
898
+ else
899
+ false
900
+ end
901
+ end
902
+
903
+ def get_cached_session(dest)
904
+ cached = nil
905
+ @sess_pool_mutex.synchronize do
906
+ new_pool = []
907
+ @sess_pool.each do |s|
908
+ if s.dest == dest
909
+ cached = s
910
+ else
911
+ new_pool << s
912
+ end
913
+ end
914
+ @sess_pool = new_pool
915
+ end
916
+ cached
917
+ end
918
+
919
+ def add_cached_session(sess)
920
+ @sess_pool_mutex.synchronize do
921
+ @sess_pool << sess
922
+ end
923
+ end
924
+
925
+ def each_sess
926
+ @sess_pool_mutex.synchronize do
927
+ @sess_pool.each do |sess|
928
+ yield(sess)
929
+ end
930
+ end
931
+ end
932
+ end
933
+
934
+
935
+ # HTTPClient::SSLSocketWrap
936
+ #
937
+ class SSLSocketWrap
938
+ def initialize(socket, context, debug_dev = nil)
939
+ unless SSLEnabled
940
+ raise RuntimeError.new(
941
+ "Ruby/OpenSSL module is required for https access.")
942
+ end
943
+ @context = context
944
+ @socket = socket
945
+ @ssl_socket = create_ssl_socket(@socket)
946
+ @debug_dev = debug_dev
947
+ end
948
+
949
+ def ssl_connect
950
+ @ssl_socket.connect
951
+ end
952
+
953
+ def post_connection_check(host)
954
+ verify_mode = @context.verify_mode || OpenSSL::SSL::VERIFY_NONE
955
+ if verify_mode == OpenSSL::SSL::VERIFY_NONE
956
+ return
957
+ elsif @ssl_socket.peer_cert.nil? and
958
+ check_mask(verify_mode, OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT)
959
+ raise OpenSSL::SSL::SSLError, "no peer cert"
960
+ end
961
+ hostname = host.host
962
+ if @ssl_socket.respond_to?(:post_connection_check)
963
+ @ssl_socket.post_connection_check(hostname)
964
+ else
965
+ @context.post_connection_check(@ssl_socket.peer_cert, hostname)
966
+ end
967
+ end
968
+
969
+ def peer_cert
970
+ @ssl_socket.peer_cert
971
+ end
972
+
973
+ def addr
974
+ @socket.addr
975
+ end
976
+
977
+ def close
978
+ @ssl_socket.close
979
+ @socket.close
980
+ end
981
+
982
+ def closed?
983
+ @socket.closed?
984
+ end
985
+
986
+ def eof?
987
+ @ssl_socket.eof?
988
+ end
989
+
990
+ def gets(*args)
991
+ str = @ssl_socket.gets(*args)
992
+ @debug_dev << str if @debug_dev
993
+ str
994
+ end
995
+
996
+ def read(*args)
997
+ str = @ssl_socket.read(*args)
998
+ @debug_dev << str if @debug_dev
999
+ str
1000
+ end
1001
+
1002
+ def <<(str)
1003
+ rv = @ssl_socket.write(str)
1004
+ @debug_dev << str if @debug_dev
1005
+ rv
1006
+ end
1007
+
1008
+ def flush
1009
+ @ssl_socket.flush
1010
+ end
1011
+
1012
+ def sync
1013
+ @ssl_socket.sync
1014
+ end
1015
+
1016
+ def sync=(sync)
1017
+ @ssl_socket.sync = sync
1018
+ end
1019
+
1020
+ private
1021
+
1022
+ def check_mask(value, mask)
1023
+ value & mask == mask
1024
+ end
1025
+
1026
+ def create_ssl_socket(socket)
1027
+ ssl_socket = nil
1028
+ if OpenSSL::SSL.const_defined?("SSLContext")
1029
+ ctx = OpenSSL::SSL::SSLContext.new
1030
+ @context.set_context(ctx)
1031
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ctx)
1032
+ else
1033
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(socket)
1034
+ @context.set_context(ssl_socket)
1035
+ end
1036
+ ssl_socket
1037
+ end
1038
+ end
1039
+
1040
+
1041
+ # HTTPClient::DebugSocket -- debugging support
1042
+ #
1043
+ class DebugSocket < TCPSocket
1044
+ attr_accessor :debug_dev # Device for logging.
1045
+
1046
+ class << self
1047
+ def create_socket(host, port, debug_dev)
1048
+ debug_dev << "! CONNECT TO #{host}:#{port}\n"
1049
+ socket = new(host, port)
1050
+ socket.debug_dev = debug_dev
1051
+ socket.log_connect
1052
+ socket
1053
+ end
1054
+
1055
+ private :new
1056
+ end
1057
+
1058
+ def initialize(*args)
1059
+ super
1060
+ @debug_dev = nil
1061
+ end
1062
+
1063
+ def log_connect
1064
+ @debug_dev << '! CONNECTION ESTABLISHED' << "\n"
1065
+ end
1066
+
1067
+ def close
1068
+ super
1069
+ @debug_dev << '! CONNECTION CLOSED' << "\n"
1070
+ end
1071
+
1072
+ def gets(*args)
1073
+ str = super
1074
+ @debug_dev << str if str
1075
+ str
1076
+ end
1077
+
1078
+ def read(*args)
1079
+ str = super
1080
+ @debug_dev << str if str
1081
+ str
1082
+ end
1083
+
1084
+ def <<(str)
1085
+ super
1086
+ @debug_dev << str
1087
+ end
1088
+ end
1089
+
1090
+
1091
+ # HTTPClient::Session -- manage http session with one site.
1092
+ # One or more TCP sessions with the site may be created.
1093
+ # Only 1 TCP session is live at the same time.
1094
+ #
1095
+ class Session # :nodoc:
1096
+
1097
+ class Error < StandardError # :nodoc:
1098
+ end
1099
+
1100
+ class InvalidState < Error # :nodoc:
1101
+ end
1102
+
1103
+ class BadResponse < Error # :nodoc:
1104
+ end
1105
+
1106
+ class KeepAliveDisconnected < Error # :nodoc:
1107
+ end
1108
+
1109
+ attr_reader :dest # Destination site
1110
+ attr_reader :src # Source site
1111
+ attr_accessor :proxy # Proxy site
1112
+ attr_accessor :socket_sync # Boolean value for Socket#sync
1113
+
1114
+ attr_accessor :requested_version # Requested protocol version
1115
+
1116
+ attr_accessor :debug_dev # Device for dumping log for debugging
1117
+
1118
+ # These session parameters are not used now...
1119
+ attr_accessor :connect_timeout
1120
+ attr_accessor :connect_retry
1121
+ attr_accessor :send_timeout
1122
+ attr_accessor :receive_timeout
1123
+ attr_accessor :read_block_size
1124
+
1125
+ attr_accessor :ssl_config
1126
+ attr_reader :ssl_peer_cert
1127
+
1128
+ def initialize(dest, user_agent, from)
1129
+ @dest = dest
1130
+ @src = Site.new
1131
+ @proxy = nil
1132
+ @socket_sync = true
1133
+ @requested_version = nil
1134
+
1135
+ @debug_dev = nil
1136
+
1137
+ @connect_timeout = nil
1138
+ @connect_retry = 1
1139
+ @send_timeout = nil
1140
+ @receive_timeout = nil
1141
+ @read_block_size = nil
1142
+
1143
+ @ssl_config = nil
1144
+ @ssl_peer_cert = nil
1145
+
1146
+ @user_agent = user_agent
1147
+ @from = from
1148
+ @state = :INIT
1149
+
1150
+ @requests = []
1151
+
1152
+ @status = nil
1153
+ @reason = nil
1154
+ @headers = []
1155
+
1156
+ @socket = nil
1157
+ end
1158
+
1159
+ # Send a request to the server
1160
+ def query(req)
1161
+ connect() if @state == :INIT
1162
+ begin
1163
+ timeout(@send_timeout) do
1164
+ set_header(req)
1165
+ req.dump(@socket)
1166
+ # flush the IO stream as IO::sync mode is false
1167
+ @socket.flush unless @socket_sync
1168
+ end
1169
+ rescue Errno::ECONNABORTED, Errno::ECONNRESET, Errno::EPIPE
1170
+ close
1171
+ raise KeepAliveDisconnected.new
1172
+ rescue
1173
+ if SSLEnabled and $!.is_a?(OpenSSL::SSL::SSLError)
1174
+ raise KeepAliveDisconnected.new
1175
+ elsif $!.is_a?(TimeoutError)
1176
+ close
1177
+ raise
1178
+ else
1179
+ raise
1180
+ end
1181
+ end
1182
+
1183
+ @state = :META if @state == :WAIT
1184
+ @next_connection = nil
1185
+ @requests.push(req)
1186
+ end
1187
+
1188
+ def close
1189
+ if !@socket.nil? and !@socket.closed?
1190
+ @socket.flush rescue nil # try to rescue OpenSSL::SSL::SSLError: cf. #120
1191
+ @socket.close
1192
+ end
1193
+ @state = :INIT
1194
+ end
1195
+
1196
+ def closed?
1197
+ @state == :INIT
1198
+ end
1199
+
1200
+ def get_status
1201
+ version = status = reason = nil
1202
+ begin
1203
+ if @state != :META
1204
+ raise RuntimeError.new("get_status must be called at the beginning of a session.")
1205
+ end
1206
+ version, status, reason = read_header()
1207
+ rescue
1208
+ close
1209
+ raise
1210
+ end
1211
+ return version, status, reason
1212
+ end
1213
+
1214
+ def get_header(&block)
1215
+ begin
1216
+ read_header() if @state == :META
1217
+ rescue
1218
+ close
1219
+ raise
1220
+ end
1221
+ if block
1222
+ @headers.each do |line|
1223
+ block.call(line)
1224
+ end
1225
+ else
1226
+ @headers
1227
+ end
1228
+ end
1229
+
1230
+ def eof?
1231
+ if !@content_length.nil?
1232
+ @content_length == 0
1233
+ elsif @readbuf.length > 0
1234
+ false
1235
+ else
1236
+ @socket.closed? or @socket.eof?
1237
+ end
1238
+ end
1239
+
1240
+ def get_data(&block)
1241
+ begin
1242
+ read_header() if @state == :META
1243
+ return nil if @state != :DATA
1244
+ unless @state == :DATA
1245
+ raise InvalidState.new('state != DATA')
1246
+ end
1247
+ data = nil
1248
+ if block
1249
+ while true
1250
+ begin
1251
+ timeout(@receive_timeout) do
1252
+ data = read_body()
1253
+ end
1254
+ rescue TimeoutError
1255
+ raise
1256
+ end
1257
+ block.call(data) if data
1258
+ break if eof?
1259
+ end
1260
+ data = nil # Calling with block returns nil.
1261
+ else
1262
+ begin
1263
+ timeout(@receive_timeout) do
1264
+ data = read_body()
1265
+ end
1266
+ rescue TimeoutError
1267
+ raise
1268
+ end
1269
+ end
1270
+ rescue
1271
+ close
1272
+ raise
1273
+ end
1274
+ if eof?
1275
+ if @next_connection
1276
+ @state = :WAIT
1277
+ else
1278
+ close
1279
+ end
1280
+ end
1281
+ data
1282
+ end
1283
+
1284
+ private
1285
+
1286
+ LibNames = "(#{RCS_FILE}/#{RCS_REVISION}, #{RUBY_VERSION_STRING})"
1287
+
1288
+ def set_header(req)
1289
+ req.version = @requested_version if @requested_version
1290
+ if @user_agent
1291
+ req.header.set('User-Agent', "#{@user_agent} #{LibNames}")
1292
+ end
1293
+ if @from
1294
+ req.header.set('From', @from)
1295
+ end
1296
+ req.header.set('Date', HTTP.http_date(Time.now))
1297
+ end
1298
+
1299
+ # Connect to the server
1300
+ def connect
1301
+ site = @proxy || @dest
1302
+ begin
1303
+ retry_number = 0
1304
+ timeout(@connect_timeout) do
1305
+ @socket = create_socket(site)
1306
+ begin
1307
+ @src.host = @socket.addr[3]
1308
+ @src.port = @socket.addr[1]
1309
+ rescue SocketError
1310
+ # to avoid IPSocket#addr problem on Mac OS X 10.3 + ruby-1.8.1.
1311
+ # cf. [ruby-talk:84909], [ruby-talk:95827]
1312
+ end
1313
+ if @dest.scheme == 'https'
1314
+ @socket = create_ssl_socket(@socket)
1315
+ connect_ssl_proxy(@socket) if @proxy
1316
+ @socket.ssl_connect
1317
+ @socket.post_connection_check(@dest)
1318
+ @ssl_peer_cert = @socket.peer_cert
1319
+ end
1320
+ # Use Ruby internal buffering instead of passing data immediatly
1321
+ # to the underlying layer
1322
+ # => we need to to call explicitely flush on the socket
1323
+ @socket.sync = @socket_sync
1324
+ end
1325
+ rescue TimeoutError
1326
+ if @connect_retry == 0
1327
+ retry
1328
+ else
1329
+ retry_number += 1
1330
+ retry if retry_number < @connect_retry
1331
+ end
1332
+ close
1333
+ raise
1334
+ end
1335
+
1336
+ @state = :WAIT
1337
+ @readbuf = ''
1338
+ end
1339
+
1340
+ def create_socket(site)
1341
+ begin
1342
+ if @debug_dev
1343
+ DebugSocket.create_socket(site.host, site.port, @debug_dev)
1344
+ else
1345
+ TCPSocket.new(site.host, site.port)
1346
+ end
1347
+ rescue SystemCallError => e
1348
+ e.message << " (#{site})"
1349
+ raise
1350
+ rescue SocketError => e
1351
+ e.message << " (#{site})"
1352
+ raise
1353
+ end
1354
+ end
1355
+
1356
+ # wrap socket with OpenSSL.
1357
+ def create_ssl_socket(raw_socket)
1358
+ SSLSocketWrap.new(raw_socket, @ssl_config, (DEBUG_SSL ? @debug_dev : nil))
1359
+ end
1360
+
1361
+ def connect_ssl_proxy(socket)
1362
+ socket << sprintf("CONNECT %s:%s HTTP/1.1\r\n\r\n", @dest.host, @dest.port)
1363
+ parse_header(socket)
1364
+ unless @status == 200
1365
+ raise BadResponse.new(
1366
+ "connect to ssl proxy failed with status #{@status} #{@reason}")
1367
+ end
1368
+ end
1369
+
1370
+ # Read status block.
1371
+ def read_header
1372
+ if @state == :DATA
1373
+ get_data {}
1374
+ check_state()
1375
+ end
1376
+ unless @state == :META
1377
+ raise InvalidState, 'state != :META'
1378
+ end
1379
+ parse_header(@socket)
1380
+ @content_length = nil
1381
+ @chunked = false
1382
+ @headers.each do |line|
1383
+ case line
1384
+ when /^Content-Length:\s+(\d+)/i
1385
+ @content_length = $1.to_i
1386
+ when /^Transfer-Encoding:\s+chunked/i
1387
+ @chunked = true
1388
+ @content_length = true # how?
1389
+ @chunk_length = 0
1390
+ when /^Connection:\s+([\-\w]+)/i, /^Proxy-Connection:\s+([\-\w]+)/i
1391
+ case $1
1392
+ when /^Keep-Alive$/i
1393
+ @next_connection = true
1394
+ when /^close$/i
1395
+ @next_connection = false
1396
+ end
1397
+ else
1398
+ # Nothing to parse.
1399
+ end
1400
+ end
1401
+
1402
+ # Head of the request has been parsed.
1403
+ @state = :DATA
1404
+ req = @requests.shift
1405
+
1406
+ if req.header.request_method == 'HEAD'
1407
+ @content_length = 0
1408
+ if @next_connection
1409
+ @state = :WAIT
1410
+ else
1411
+ close
1412
+ end
1413
+ end
1414
+ @next_connection = false unless @content_length
1415
+ return [@version, @status, @reason]
1416
+ end
1417
+
1418
+ StatusParseRegexp = %r(\AHTTP/(\d+\.\d+)\s+(\d+)(?:\s+([^\r\n]+))?\r?\n\z)
1419
+ def parse_header(socket)
1420
+ begin
1421
+ timeout(@receive_timeout) do
1422
+ begin
1423
+ initial_line = socket.gets("\n")
1424
+ if initial_line.nil?
1425
+ raise KeepAliveDisconnected.new
1426
+ end
1427
+ if StatusParseRegexp =~ initial_line
1428
+ @version, @status, @reason = $1, $2.to_i, $3
1429
+ @next_connection = HTTP.keep_alive_enabled?(@version)
1430
+ else
1431
+ @version = '0.9'
1432
+ @status = nil
1433
+ @reason = nil
1434
+ @next_connection = false
1435
+ @readbuf = initial_line
1436
+ break
1437
+ end
1438
+ @headers = []
1439
+ while true
1440
+ line = socket.gets("\n")
1441
+ unless line
1442
+ raise BadResponse.new('Unexpected EOF.')
1443
+ end
1444
+ line.sub!(/\r?\n\z/, '')
1445
+ break if line.empty?
1446
+ if line.sub!(/^\t/, '')
1447
+ @headers[-1] << line
1448
+ else
1449
+ @headers.push(line)
1450
+ end
1451
+ end
1452
+ end while (@version == '1.1' && @status == 100)
1453
+ end
1454
+ rescue TimeoutError
1455
+ raise
1456
+ end
1457
+ end
1458
+
1459
+ def read_body
1460
+ if @chunked
1461
+ return read_body_chunked()
1462
+ elsif @content_length == 0
1463
+ return nil
1464
+ elsif @content_length
1465
+ return read_body_length()
1466
+ else
1467
+ if @readbuf.length > 0
1468
+ data = @readbuf
1469
+ @readbuf = ''
1470
+ return data
1471
+ else
1472
+ data = @socket.read(@read_block_size)
1473
+ data = nil if data and data.empty? # Absorbing interface mismatch.
1474
+ return data
1475
+ end
1476
+ end
1477
+ end
1478
+
1479
+ def read_body_length
1480
+ maxbytes = @read_block_size
1481
+ if @readbuf.length > 0
1482
+ data = @readbuf[0, @content_length]
1483
+ @readbuf[0, @content_length] = ''
1484
+ @content_length -= data.length
1485
+ return data
1486
+ end
1487
+ maxbytes = @content_length if maxbytes > @content_length
1488
+ data = @socket.read(maxbytes)
1489
+ if data
1490
+ @content_length -= data.length
1491
+ else
1492
+ @content_length = 0
1493
+ end
1494
+ return data
1495
+ end
1496
+
1497
+ RS = "\r\n"
1498
+ def read_body_chunked
1499
+ if @chunk_length == 0
1500
+ until (i = @readbuf.index(RS))
1501
+ @readbuf << @socket.gets(RS)
1502
+ end
1503
+ i += 2
1504
+ @chunk_length = @readbuf[0, i].hex
1505
+ @readbuf[0, i] = ''
1506
+ if @chunk_length == 0
1507
+ @content_length = 0
1508
+ @socket.gets(RS)
1509
+ return nil
1510
+ end
1511
+ end
1512
+ while @readbuf.length < @chunk_length + 2
1513
+ @readbuf << @socket.read(@chunk_length + 2 - @readbuf.length)
1514
+ end
1515
+ data = @readbuf[0, @chunk_length]
1516
+ @readbuf[0, @chunk_length + 2] = ''
1517
+ @chunk_length = 0
1518
+ return data
1519
+ end
1520
+
1521
+ def check_state
1522
+ if @state == :DATA
1523
+ if eof?
1524
+ if @next_connection
1525
+ if @requests.empty?
1526
+ @state = :WAIT
1527
+ else
1528
+ @state = :META
1529
+ end
1530
+ end
1531
+ end
1532
+ end
1533
+ end
1534
+ end
1535
+
1536
+ include Util
1537
+
1538
+ attr_reader :agent_name
1539
+ attr_reader :from
1540
+ attr_reader :ssl_config
1541
+ attr_accessor :cookie_manager
1542
+ attr_reader :test_loopback_response
1543
+ attr_reader :request_filter
1544
+ attr_reader :proxy_auth
1545
+ attr_reader :www_auth
1546
+
1547
+ class << self
1548
+ %w(get_content head get post put delete options trace).each do |name|
1549
+ eval <<-EOD
1550
+ def #{name}(*arg)
1551
+ new.#{name}(*arg)
1552
+ end
1553
+ EOD
1554
+ end
1555
+ end
1556
+
1557
+ class RetryableResponse < StandardError # :nodoc:
1558
+ end
1559
+
1560
+ # SYNOPSIS
1561
+ # Client.new(proxy = nil, agent_name = nil, from = nil)
1562
+ #
1563
+ # ARGS
1564
+ # proxy A String of HTTP proxy URL. ex. "http://proxy:8080".
1565
+ # agent_name A String for "User-Agent" HTTP request header.
1566
+ # from A String for "From" HTTP request header.
1567
+ #
1568
+ # DESCRIPTION
1569
+ # Create an instance.
1570
+ # SSLConfig cannot be re-initialized. Create new client.
1571
+ #
1572
+ def initialize(proxy = nil, agent_name = nil, from = nil)
1573
+ @proxy = nil # assigned later.
1574
+ @no_proxy = nil
1575
+ @agent_name = agent_name
1576
+ @from = from
1577
+ @www_auth = WWWAuth.new
1578
+ @proxy_auth = ProxyAuth.new
1579
+ @request_filter = [@proxy_auth, @www_auth]
1580
+ @debug_dev = nil
1581
+ @redirect_uri_callback = method(:default_redirect_uri_callback)
1582
+ @test_loopback_response = []
1583
+ @session_manager = SessionManager.new
1584
+ @session_manager.agent_name = @agent_name
1585
+ @session_manager.from = @from
1586
+ @session_manager.ssl_config = @ssl_config = SSLConfig.new(self)
1587
+ @cookie_manager = WebAgent::CookieManager.new
1588
+ load_environment
1589
+ self.proxy = proxy if proxy
1590
+ end
1591
+
1592
+ def debug_dev
1593
+ @debug_dev
1594
+ end
1595
+
1596
+ def debug_dev=(dev)
1597
+ @debug_dev = dev
1598
+ reset_all
1599
+ @session_manager.debug_dev = dev
1600
+ end
1601
+
1602
+ def protocol_version
1603
+ @session_manager.protocol_version
1604
+ end
1605
+
1606
+ def protocol_version=(protocol_version)
1607
+ reset_all
1608
+ @session_manager.protocol_version = protocol_version
1609
+ end
1610
+
1611
+ def connect_timeout
1612
+ @session_manager.connect_timeout
1613
+ end
1614
+
1615
+ def connect_timeout=(connect_timeout)
1616
+ reset_all
1617
+ @session_manager.connect_timeout = connect_timeout
1618
+ end
1619
+
1620
+ def send_timeout
1621
+ @session_manager.send_timeout
1622
+ end
1623
+
1624
+ def send_timeout=(send_timeout)
1625
+ reset_all
1626
+ @session_manager.send_timeout = send_timeout
1627
+ end
1628
+
1629
+ def receive_timeout
1630
+ @session_manager.receive_timeout
1631
+ end
1632
+
1633
+ def receive_timeout=(receive_timeout)
1634
+ reset_all
1635
+ @session_manager.receive_timeout = receive_timeout
1636
+ end
1637
+
1638
+ def proxy
1639
+ @proxy
1640
+ end
1641
+
1642
+ def proxy=(proxy)
1643
+ if proxy.nil?
1644
+ @proxy = nil
1645
+ @proxy_auth.reset_challenge
1646
+ else
1647
+ @proxy = urify(proxy)
1648
+ if @proxy.scheme == nil or @proxy.scheme.downcase != 'http' or
1649
+ @proxy.host == nil or @proxy.port == nil
1650
+ raise ArgumentError.new("unsupported proxy `#{proxy}'")
1651
+ end
1652
+ @proxy_auth.reset_challenge
1653
+ if @proxy.user || @proxy.password
1654
+ @proxy_auth.set_auth(@proxy.user, @proxy.password)
1655
+ end
1656
+ end
1657
+ reset_all
1658
+ @proxy
1659
+ end
1660
+
1661
+ def no_proxy
1662
+ @no_proxy
1663
+ end
1664
+
1665
+ def no_proxy=(no_proxy)
1666
+ @no_proxy = no_proxy
1667
+ reset_all
1668
+ end
1669
+
1670
+ # if your ruby is older than 2005-09-06, do not set socket_sync = false to
1671
+ # avoid an SSL socket blocking bug in openssl/buffering.rb.
1672
+ def socket_sync=(socket_sync)
1673
+ @session_manager.socket_sync = socket_sync
1674
+ end
1675
+
1676
+ def set_auth(uri, user, passwd)
1677
+ uri = urify(uri)
1678
+ @www_auth.set_auth(uri, user, passwd)
1679
+ reset_all
1680
+ end
1681
+
1682
+ # for backward compatibility
1683
+ def set_basic_auth(uri, user, passwd)
1684
+ uri = urify(uri)
1685
+ @www_auth.basic_auth.set(uri, user, passwd)
1686
+ reset_all
1687
+ end
1688
+
1689
+ def set_proxy_auth(user, passwd)
1690
+ uri = urify(uri)
1691
+ @proxy_auth.set_auth(user, passwd)
1692
+ reset_all
1693
+ end
1694
+
1695
+ def set_cookie_store(filename)
1696
+ if @cookie_manager.cookies_file
1697
+ raise RuntimeError.new("overriding cookie file location")
1698
+ end
1699
+ @cookie_manager.cookies_file = filename
1700
+ @cookie_manager.load_cookies if filename
1701
+ end
1702
+
1703
+ def save_cookie_store
1704
+ @cookie_manager.save_cookies
1705
+ end
1706
+
1707
+ def redirect_uri_callback=(redirect_uri_callback)
1708
+ @redirect_uri_callback = redirect_uri_callback
1709
+ end
1710
+
1711
+ # SYNOPSIS
1712
+ # Client#get_content(uri, query = nil, extheader = {}, &block = nil)
1713
+ #
1714
+ # ARGS
1715
+ # uri an_URI or a_string of uri to connect.
1716
+ # query a_hash or an_array of query part. e.g. { "a" => "b" }.
1717
+ # Give an array to pass multiple value like
1718
+ # [["a" => "b"], ["a" => "c"]].
1719
+ # extheader a_hash of extra headers like { "SOAPAction" => "urn:foo" }.
1720
+ # &block Give a block to get chunked message-body of response like
1721
+ # get_content(uri) { |chunked_body| ... }
1722
+ # Size of each chunk may not be the same.
1723
+ #
1724
+ # DESCRIPTION
1725
+ # Get a_sring of message-body of response.
1726
+ #
1727
+ def get_content(uri, query = nil, extheader = {}, &block)
1728
+ follow_redirect(uri, query) { |uri, query|
1729
+ get(uri, query, extheader, &block)
1730
+ }.content
1731
+ end
1732
+
1733
+ def post_content(uri, body = nil, extheader = {}, &block)
1734
+ follow_redirect(uri, nil) { |uri, query|
1735
+ post(uri, body, extheader, &block)
1736
+ }.content
1737
+ end
1738
+
1739
+ def strict_redirect_uri_callback(uri, res)
1740
+ newuri = URI.parse(res.header['location'][0])
1741
+ puts "Redirect to: #{newuri}" if $DEBUG
1742
+ newuri
1743
+ end
1744
+
1745
+ def default_redirect_uri_callback(uri, res)
1746
+ newuri = URI.parse(res.header['location'][0])
1747
+ unless newuri.is_a?(URI::HTTP)
1748
+ newuri = uri + newuri
1749
+ STDERR.puts(
1750
+ "could be a relative URI in location header which is not recommended")
1751
+ STDERR.puts(
1752
+ "'The field value consists of a single absolute URI' in HTTP spec")
1753
+ end
1754
+ puts "Redirect to: #{newuri}" if $DEBUG
1755
+ newuri
1756
+ end
1757
+
1758
+ def head(uri, query = nil, extheader = {})
1759
+ request('HEAD', uri, query, nil, extheader)
1760
+ end
1761
+
1762
+ def get(uri, query = nil, extheader = {}, &block)
1763
+ request('GET', uri, query, nil, extheader, &block)
1764
+ end
1765
+
1766
+ def post(uri, body = nil, extheader = {}, &block)
1767
+ request('POST', uri, nil, body, extheader, &block)
1768
+ end
1769
+
1770
+ def put(uri, body = nil, extheader = {}, &block)
1771
+ request('PUT', uri, nil, body, extheader, &block)
1772
+ end
1773
+
1774
+ def delete(uri, extheader = {}, &block)
1775
+ request('DELETE', uri, nil, nil, extheader, &block)
1776
+ end
1777
+
1778
+ def options(uri, extheader = {}, &block)
1779
+ request('OPTIONS', uri, nil, nil, extheader, &block)
1780
+ end
1781
+
1782
+ def trace(uri, query = nil, body = nil, extheader = {}, &block)
1783
+ request('TRACE', uri, query, body, extheader, &block)
1784
+ end
1785
+
1786
+ def request(method, uri, query = nil, body = nil, extheader = {}, &block)
1787
+ uri = urify(uri)
1788
+ conn = Connection.new
1789
+ res = nil
1790
+ retry_count = 5
1791
+ while retry_count > 0
1792
+ begin
1793
+ prepare_request(method, uri, query, body, extheader) do |req, proxy|
1794
+ do_get_block(req, proxy, conn, &block)
1795
+ end
1796
+ res = conn.pop
1797
+ break
1798
+ rescue RetryableResponse
1799
+ res = conn.pop
1800
+ retry_count -= 1
1801
+ end
1802
+ end
1803
+ res
1804
+ end
1805
+
1806
+ # Async interface.
1807
+
1808
+ def head_async(uri, query = nil, extheader = {})
1809
+ request_async('HEAD', uri, query, nil, extheader)
1810
+ end
1811
+
1812
+ def get_async(uri, query = nil, extheader = {})
1813
+ request_async('GET', uri, query, nil, extheader)
1814
+ end
1815
+
1816
+ def post_async(uri, body = nil, extheader = {})
1817
+ request_async('POST', uri, nil, body, extheader)
1818
+ end
1819
+
1820
+ def put_async(uri, body = nil, extheader = {})
1821
+ request_async('PUT', uri, nil, body, extheader)
1822
+ end
1823
+
1824
+ def delete_async(uri, extheader = {})
1825
+ request_async('DELETE', uri, nil, nil, extheader)
1826
+ end
1827
+
1828
+ def options_async(uri, extheader = {})
1829
+ request_async('OPTIONS', uri, nil, nil, extheader)
1830
+ end
1831
+
1832
+ def trace_async(uri, query = nil, body = nil, extheader = {})
1833
+ request_async('TRACE', uri, query, body, extheader)
1834
+ end
1835
+
1836
+ def request_async(method, uri, query = nil, body = nil, extheader = {})
1837
+ uri = urify(uri)
1838
+ conn = Connection.new
1839
+ t = Thread.new(conn) { |tconn|
1840
+ prepare_request(method, uri, query, body, extheader) do |req, proxy|
1841
+ do_get_stream(req, proxy, tconn)
1842
+ end
1843
+ }
1844
+ conn.async_thread = t
1845
+ conn
1846
+ end
1847
+
1848
+ ##
1849
+ # Multiple call interface.
1850
+
1851
+ # ???
1852
+
1853
+ ##
1854
+ # Management interface.
1855
+
1856
+ def reset(uri)
1857
+ uri = urify(uri)
1858
+ @session_manager.reset(uri)
1859
+ end
1860
+
1861
+ def reset_all
1862
+ @session_manager.reset_all
1863
+ end
1864
+
1865
+ private
1866
+
1867
+ def load_environment
1868
+ # http_proxy
1869
+ if getenv('REQUEST_METHOD')
1870
+ # HTTP_PROXY conflicts with the environment variable usage in CGI where
1871
+ # HTTP_* is used for HTTP header information. Unlike open-uri, we
1872
+ # simpley ignore http_proxy in CGI env and use cgi_http_proxy instead.
1873
+ self.proxy = getenv('cgi_http_proxy')
1874
+ else
1875
+ self.proxy = getenv('http_proxy')
1876
+ end
1877
+ # no_proxy
1878
+ self.no_proxy = getenv('no_proxy')
1879
+ end
1880
+
1881
+ def getenv(name)
1882
+ ENV[name.downcase] || ENV[name.upcase]
1883
+ end
1884
+
1885
+ def follow_redirect(uri, query = nil)
1886
+ retry_number = 0
1887
+ while retry_number < 10
1888
+ res = yield(uri, query)
1889
+ if HTTP::Status.successful?(res.status)
1890
+ return res
1891
+ elsif HTTP::Status.redirect?(res.status)
1892
+ uri = @redirect_uri_callback.call(uri, res)
1893
+ query = nil
1894
+ retry_number += 1
1895
+ else
1896
+ raise RuntimeError.new("Unexpected response: #{res.header.inspect}")
1897
+ end
1898
+ end
1899
+ raise RuntimeError.new("Retry count exceeded.")
1900
+ end
1901
+
1902
+ def prepare_request(method, uri, query, body, extheader)
1903
+ proxy = no_proxy?(uri) ? nil : @proxy
1904
+ begin
1905
+ req = create_request(method, uri, query, body, extheader, !proxy.nil?)
1906
+ yield(req, proxy)
1907
+ rescue Session::KeepAliveDisconnected
1908
+ req = create_request(method, uri, query, body, extheader, !proxy.nil?)
1909
+ yield(req, proxy)
1910
+ end
1911
+ end
1912
+
1913
+ def create_request(method, uri, query, body, extheader, proxy)
1914
+ if extheader.is_a?(Hash)
1915
+ extheader = extheader.to_a
1916
+ end
1917
+ if cookies = @cookie_manager.find(uri)
1918
+ extheader << ['Cookie', cookies]
1919
+ end
1920
+ boundary = nil
1921
+ content_type = extheader.find { |key, value|
1922
+ key.downcase == 'content-type'
1923
+ }
1924
+ if content_type && content_type[1] =~ /boundary=(.+)\z/
1925
+ boundary = $1
1926
+ end
1927
+ req = HTTP::Message.new_request(method, uri, query, body, proxy, boundary)
1928
+ extheader.each do |key, value|
1929
+ req.header.set(key, value)
1930
+ end
1931
+ if content_type.nil? and !body.nil?
1932
+ req.header.set('content-type', 'application/x-www-form-urlencoded')
1933
+ end
1934
+ req
1935
+ end
1936
+
1937
+ NO_PROXY_HOSTS = ['localhost']
1938
+
1939
+ def no_proxy?(uri)
1940
+ if !@proxy or NO_PROXY_HOSTS.include?(uri.host)
1941
+ return true
1942
+ end
1943
+ unless @no_proxy
1944
+ return false
1945
+ end
1946
+ @no_proxy.scan(/([^:,]+)(?::(\d+))?/) do |host, port|
1947
+ if /(\A|\.)#{Regexp.quote(host)}\z/i =~ uri.host &&
1948
+ (!port || uri.port == port.to_i)
1949
+ return true
1950
+ end
1951
+ end
1952
+ false
1953
+ end
1954
+
1955
+ # !! CAUTION !!
1956
+ # Method 'do_get*' runs under MT conditon. Be careful to change.
1957
+ def do_get_block(req, proxy, conn, &block)
1958
+ @request_filter.each do |filter|
1959
+ filter.filter_request(req)
1960
+ end
1961
+ if str = @test_loopback_response.shift
1962
+ dump_dummy_request_response(req.body.dump, str) if @debug_dev
1963
+ conn.push(HTTP::Message.new_response(str))
1964
+ return
1965
+ end
1966
+ content = ''
1967
+ res = HTTP::Message.new_response(content)
1968
+ @debug_dev << "= Request\n\n" if @debug_dev
1969
+ sess = @session_manager.query(req, proxy)
1970
+ res.peer_cert = sess.ssl_peer_cert
1971
+ @debug_dev << "\n\n= Response\n\n" if @debug_dev
1972
+ do_get_header(req, res, sess)
1973
+ conn.push(res)
1974
+ sess.get_data() do |str|
1975
+ block.call(str) if block
1976
+ content << str
1977
+ end
1978
+ @session_manager.keep(sess) unless sess.closed?
1979
+ commands = @request_filter.collect { |filter|
1980
+ filter.filter_response(req, res)
1981
+ }
1982
+ if commands.find { |command| command == :retry }
1983
+ raise RetryableResponse.new
1984
+ end
1985
+ end
1986
+
1987
+ def do_get_stream(req, proxy, conn)
1988
+ @request_filter.each do |filter|
1989
+ filter.filter_request(req)
1990
+ end
1991
+ if str = @test_loopback_response.shift
1992
+ dump_dummy_request_response(req.body.dump, str) if @debug_dev
1993
+ conn.push(HTTP::Message.new_response(str))
1994
+ return
1995
+ end
1996
+ piper, pipew = IO.pipe
1997
+ res = HTTP::Message.new_response(piper)
1998
+ @debug_dev << "= Request\n\n" if @debug_dev
1999
+ sess = @session_manager.query(req, proxy)
2000
+ res.peer_cert = sess.ssl_peer_cert
2001
+ @debug_dev << "\n\n= Response\n\n" if @debug_dev
2002
+ do_get_header(req, res, sess)
2003
+ conn.push(res)
2004
+ sess.get_data() do |str|
2005
+ pipew.syswrite(str)
2006
+ end
2007
+ pipew.close
2008
+ @session_manager.keep(sess) unless sess.closed?
2009
+ commands = @request_filter.collect { |filter|
2010
+ filter.filter_response(req, res)
2011
+ }
2012
+ # ignore commands (not retryable in async mode)
2013
+ end
2014
+
2015
+ def do_get_header(req, res, sess)
2016
+ res.version, res.status, res.reason = sess.get_status
2017
+ sess.get_header().each do |line|
2018
+ unless /^([^:]+)\s*:\s*(.*)$/ =~ line
2019
+ raise RuntimeError.new("Unparsable header: '#{line}'.") if $DEBUG
2020
+ end
2021
+ res.header.set($1, $2)
2022
+ end
2023
+ if res.header['set-cookie']
2024
+ res.header['set-cookie'].each do |cookie|
2025
+ @cookie_manager.parse(cookie, req.header.request_uri)
2026
+ end
2027
+ end
2028
+ end
2029
+
2030
+ def dump_dummy_request_response(req, res)
2031
+ @debug_dev << "= Dummy Request\n\n"
2032
+ @debug_dev << req
2033
+ @debug_dev << "\n\n= Dummy Response\n\n"
2034
+ @debug_dev << res
2035
+ end
2036
+ end