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