httpclient 2.1.2 → 2.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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