httpclient 2.5.3.3 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -198,6 +198,7 @@ module HTTP
198
198
  @request_uri = uri || NIL_URI
199
199
  @request_query = query
200
200
  @request_absolute_uri = false
201
+ self
201
202
  end
202
203
 
203
204
  # Initialize this instance as a response.
@@ -209,6 +210,7 @@ module HTTP
209
210
  @request_uri = req.request_uri
210
211
  @request_query = req.request_query
211
212
  end
213
+ self
212
214
  end
213
215
 
214
216
  # Sets status code and reason phrase.
@@ -441,6 +443,8 @@ module HTTP
441
443
  attr_reader :size
442
444
  # maxbytes of IO#read for streaming request. See DEFAULT_CHUNK_SIZE.
443
445
  attr_accessor :chunk_size
446
+ # Hash that keeps IO positions
447
+ attr_accessor :positions
444
448
 
445
449
  # Default value for chunk_size
446
450
  DEFAULT_CHUNK_SIZE = 1024 * 16
@@ -460,6 +464,7 @@ module HTTP
460
464
  @positions = {}
461
465
  set_content(body, boundary)
462
466
  @chunk_size = DEFAULT_CHUNK_SIZE
467
+ self
463
468
  end
464
469
 
465
470
  # Initialize this instance as a response.
@@ -472,6 +477,7 @@ module HTTP
472
477
  else
473
478
  @size = nil
474
479
  end
480
+ self
475
481
  end
476
482
 
477
483
  # Dumps message body to given dev.
@@ -481,13 +487,15 @@ module HTTP
481
487
  # reason. (header is dumped to dev, too)
482
488
  # If no dev (the second argument) given, this method returns a dumped
483
489
  # String.
490
+ #
491
+ # assert: @size is not nil
484
492
  def dump(header = '', dev = '')
485
493
  if @body.is_a?(Parts)
486
494
  dev << header
487
495
  @body.parts.each do |part|
488
496
  if Message.file?(part)
489
497
  reset_pos(part)
490
- dump_file(part, dev)
498
+ dump_file(part, dev, @body.sizes[part])
491
499
  else
492
500
  dev << part
493
501
  end
@@ -495,7 +503,7 @@ module HTTP
495
503
  elsif Message.file?(@body)
496
504
  dev << header
497
505
  reset_pos(@body)
498
- dump_file(@body, dev)
506
+ dump_file(@body, dev, @size)
499
507
  elsif @body
500
508
  dev << header + @body
501
509
  else
@@ -563,10 +571,14 @@ module HTTP
563
571
  io.pos = @positions[io] if @positions.key?(io)
564
572
  end
565
573
 
566
- def dump_file(io, dev)
574
+ def dump_file(io, dev, sz)
567
575
  buf = ''
568
- while !io.read(@chunk_size, buf).nil?
576
+ rest = sz
577
+ while rest > 0
578
+ n = io.read([rest, @chunk_size].min, buf)
579
+ raise ArgumentError.new("Illegal size value: #size returns #{sz} but cannot read") if n.nil?
569
580
  dev << buf
581
+ rest -= n.bytesize
570
582
  end
571
583
  end
572
584
 
@@ -591,10 +603,12 @@ module HTTP
591
603
 
592
604
  class Parts
593
605
  attr_reader :size
606
+ attr_reader :sizes
594
607
 
595
608
  def initialize
596
609
  @body = []
597
- @size = 0
610
+ @sizes = {}
611
+ @size = 0 # total
598
612
  @as_stream = false
599
613
  end
600
614
 
@@ -603,15 +617,18 @@ module HTTP
603
617
  @as_stream = true
604
618
  @body << part
605
619
  if part.respond_to?(:lstat)
606
- @size += part.lstat.size
620
+ sz = part.lstat.size
621
+ add_size(part, sz)
607
622
  elsif part.respond_to?(:size)
608
623
  if sz = part.size
609
- @size += sz
624
+ add_size(part, sz)
610
625
  else
626
+ @sizes.clear
611
627
  @size = nil
612
628
  end
613
629
  else
614
630
  # use chunked upload
631
+ @sizes.clear
615
632
  @size = nil
616
633
  end
617
634
  elsif @body[-1].is_a?(String)
@@ -630,6 +647,15 @@ module HTTP
630
647
  [@body.join]
631
648
  end
632
649
  end
650
+
651
+ private
652
+
653
+ def add_size(part, sz)
654
+ if @size
655
+ @sizes[part] = sz
656
+ @size += sz
657
+ end
658
+ end
633
659
  end
634
660
 
635
661
  def build_query_multipart_str(query, boundary)
@@ -908,12 +934,17 @@ module HTTP
908
934
  # used for retrieving the response.
909
935
  attr_accessor :peer_cert
910
936
 
937
+ # The other Message object when this Message is generated instead of
938
+ # the Message because of redirection, negotiation, or format conversion.
939
+ attr_accessor :previous
940
+
911
941
  # Creates a Message. This method should be used internally.
912
942
  # Use Message.new_connect_request, Message.new_request or
913
943
  # Message.new_response instead.
914
944
  def initialize # :nodoc:
915
945
  @http_header = Headers.new
916
946
  @http_body = @peer_cert = nil
947
+ @previous = nil
917
948
  end
918
949
 
919
950
  # Dumps message (header and body) to given dev.
@@ -1023,10 +1054,8 @@ module HTTP
1023
1054
  unless set_cookies.empty?
1024
1055
  uri = http_header.request_uri
1025
1056
  set_cookies.map { |str|
1026
- cookie = WebAgent::Cookie.new
1027
- cookie.parse(str, uri)
1028
- cookie
1029
- }
1057
+ WebAgent::Cookie.parse(str, uri)
1058
+ }.flatten
1030
1059
  end
1031
1060
  end
1032
1061
 
@@ -17,7 +17,7 @@ require 'thread'
17
17
  require 'stringio'
18
18
  require 'zlib'
19
19
 
20
- require 'httpclient/timeout'
20
+ require 'httpclient/timeout' # TODO: remove this once we drop 1.8 support
21
21
  require 'httpclient/ssl_config'
22
22
  require 'httpclient/http'
23
23
 
@@ -183,16 +183,6 @@ class HTTPClient
183
183
  add_cached_session(sess)
184
184
  end
185
185
 
186
- def invalidate(site)
187
- @sess_pool_mutex.synchronize do
188
- if pool = @sess_pool[site]
189
- pool.each do |sess|
190
- sess.invalidate
191
- end
192
- end
193
- end
194
- end
195
-
196
186
  private
197
187
 
198
188
  # TODO: create PR for webmock's httpclient adapter to use get_session
@@ -253,6 +243,9 @@ class HTTPClient
253
243
  end
254
244
 
255
245
  def get_cached_session(site)
246
+ if Thread.current[:HTTPClient_AcquireNewConnection]
247
+ return nil
248
+ end
256
249
  @sess_pool_mutex.synchronize do
257
250
  now = Time.now
258
251
  if now > @sess_pool_last_checked + @keep_alive_timeout
@@ -284,7 +277,7 @@ class HTTPClient
284
277
  end
285
278
 
286
279
  def valid_session?(sess, now)
287
- !sess.invalidated? and (now <= sess.last_used + @keep_alive_timeout)
280
+ (now <= sess.last_used + @keep_alive_timeout)
288
281
  end
289
282
 
290
283
  def add_cached_session(sess)
@@ -578,7 +571,6 @@ class HTTPClient
578
571
  def initialize(client, dest, agent_name, from)
579
572
  @client = client
580
573
  @dest = dest
581
- @invalidated = false
582
574
  @proxy = nil
583
575
  @socket_sync = true
584
576
  @requested_version = nil
@@ -662,14 +654,6 @@ class HTTPClient
662
654
  @state == :INIT
663
655
  end
664
656
 
665
- def invalidate
666
- @invalidated = true
667
- end
668
-
669
- def invalidated?
670
- @invalidated
671
- end
672
-
673
657
  def get_header
674
658
  begin
675
659
  if @state != :META
@@ -855,7 +839,7 @@ class HTTPClient
855
839
  filter.filter_response(req, res)
856
840
  }
857
841
  if commands.find { |command| command == :retry }
858
- raise RetryableResponse.new
842
+ raise RetryableResponse.new(res)
859
843
  end
860
844
  unless @status == 200
861
845
  raise BadResponseError.new("connect to ssl proxy failed with status #{@status} #{@reason}", res)
@@ -1,3 +1,3 @@
1
1
  class HTTPClient
2
- VERSION = '2.5.3.3'
2
+ VERSION = '2.6.0'
3
3
  end
@@ -0,0 +1,459 @@
1
+ # cookie.rb is redistributed file which is originally included in Webagent
2
+ # version 0.6.2 by TAKAHASHI `Maki' Masayoshi. And it contains some bug fixes.
3
+ # You can download the entire package of Webagent from
4
+ # http://www.rubycolor.org/arc/.
5
+
6
+
7
+ # Cookie class
8
+ #
9
+ # I refered to w3m's source to make these classes. Some comments
10
+ # are quoted from it. I'm thanksful for author(s) of it.
11
+ #
12
+ # w3m homepage: http://ei5nazha.yz.yamagata-u.ac.jp/~aito/w3m/eng/
13
+
14
+ require 'time'
15
+ require 'monitor'
16
+ require 'httpclient/util'
17
+
18
+ class WebAgent
19
+
20
+ module CookieUtils
21
+
22
+ def head_match?(str1, str2)
23
+ str1 == str2[0, str1.length]
24
+ end
25
+
26
+ def tail_match?(str1, str2)
27
+ if str1.length > 0
28
+ str1 == str2[-str1.length..-1].to_s
29
+ else
30
+ true
31
+ end
32
+ end
33
+
34
+ def domain_match(host, domain)
35
+ return false if domain.nil?
36
+ domainname = domain.sub(/\.\z/, '').downcase
37
+ hostname = host.sub(/\.\z/, '').downcase
38
+ case domain
39
+ when /\d+\.\d+\.\d+\.\d+/
40
+ return (hostname == domainname)
41
+ when '.'
42
+ return true
43
+ when /^\./
44
+ # allows; host == rubyforge.org, domain == .rubyforge.org
45
+ return tail_match?(domainname, '.' + hostname)
46
+ else
47
+ return (hostname == domainname)
48
+ end
49
+ end
50
+ end
51
+
52
+ class Cookie
53
+ include CookieUtils
54
+
55
+ attr_accessor :name, :value
56
+ attr_accessor :domain, :path
57
+ attr_accessor :expires ## for Netscape Cookie
58
+ attr_accessor :url
59
+ attr_writer :use, :secure, :http_only, :discard, :domain_orig, :path_orig, :override
60
+
61
+ USE = 1
62
+ SECURE = 2
63
+ DOMAIN = 4
64
+ PATH = 8
65
+ DISCARD = 16
66
+ OVERRIDE = 32
67
+ OVERRIDE_OK = 32
68
+ HTTP_ONLY = 64
69
+
70
+ def self.parse(str, url)
71
+ cookie = new
72
+ cookie.parse(str, url)
73
+ cookie
74
+ end
75
+
76
+ def initialize
77
+ @name = @value = @domain = @path = nil
78
+ @expires = nil
79
+ @url = nil
80
+ @use = @secure = @http_only = @discard = @domain_orig = @path_orig = @override = nil
81
+ end
82
+
83
+ def discard?
84
+ @discard
85
+ end
86
+
87
+ def use?
88
+ @use
89
+ end
90
+
91
+ def secure?
92
+ @secure
93
+ end
94
+
95
+ def http_only?
96
+ @http_only
97
+ end
98
+
99
+ def domain_orig?
100
+ @domain_orig
101
+ end
102
+
103
+ def path_orig?
104
+ @path_orig
105
+ end
106
+
107
+ def override?
108
+ @override
109
+ end
110
+
111
+ def flag
112
+ flg = 0
113
+ flg += USE if @use
114
+ flg += SECURE if @secure
115
+ flg += HTTP_ONLY if @http_only
116
+ flg += DOMAIN if @domain_orig
117
+ flg += PATH if @path_orig
118
+ flg += DISCARD if @discard
119
+ flg += OVERRIDE if @override
120
+ flg
121
+ end
122
+
123
+ def set_flag(flag)
124
+ flag = flag.to_i
125
+ @use = true if flag & USE > 0
126
+ @secure = true if flag & SECURE > 0
127
+ @http_only = true if flag & HTTP_ONLY > 0
128
+ @domain_orig = true if flag & DOMAIN > 0
129
+ @path_orig = true if flag & PATH > 0
130
+ @discard = true if flag & DISCARD > 0
131
+ @override = true if flag & OVERRIDE > 0
132
+ end
133
+
134
+ def match?(url)
135
+ domainname = url.host
136
+ if (!domainname ||
137
+ !domain_match(domainname, @domain) ||
138
+ (@path && !head_match?(@path, url.path.empty? ? '/' : url.path)) ||
139
+ (@secure && (url.scheme != 'https')) )
140
+ return false
141
+ else
142
+ return true
143
+ end
144
+ end
145
+
146
+ def join_quotedstr(array, sep)
147
+ ret = Array.new
148
+ old_elem = nil
149
+ array.each{|elem|
150
+ if (elem.scan(/"/).length % 2) == 0
151
+ if old_elem
152
+ old_elem << sep << elem
153
+ else
154
+ ret << elem
155
+ old_elem = nil
156
+ end
157
+ else
158
+ if old_elem
159
+ old_elem << sep << elem
160
+ ret << old_elem
161
+ old_elem = nil
162
+ else
163
+ old_elem = elem.dup
164
+ end
165
+ end
166
+ }
167
+ ret
168
+ end
169
+
170
+ def parse(str, url)
171
+ @url = url
172
+ # TODO: should not depend on join_quotedstr. scan with escape like CSV.
173
+ cookie_elem = str.split(/;/)
174
+ cookie_elem = join_quotedstr(cookie_elem, ';')
175
+ cookie_elem -= [""] # del empty elements, a cookie might included ";;"
176
+ first_elem = cookie_elem.shift
177
+ if first_elem !~ /([^=]*)(\=(.*))?/
178
+ return
179
+ ## raise ArgumentError 'invalid cookie value'
180
+ end
181
+ @name = $1.strip
182
+ @value = normalize_cookie_value($3)
183
+ cookie_elem.each{|pair|
184
+ key, value = pair.split(/=/, 2) ## value may nil
185
+ key.strip!
186
+ value = normalize_cookie_value(value)
187
+ case key.downcase
188
+ when 'domain'
189
+ @domain = value
190
+ when 'expires'
191
+ @expires = nil
192
+ begin
193
+ @expires = Time.parse(value).gmtime if value
194
+ rescue ArgumentError
195
+ end
196
+ when 'path'
197
+ @path = value
198
+ when 'secure'
199
+ @secure = true ## value may nil, but must 'true'.
200
+ when 'httponly'
201
+ @http_only = true ## value may nil, but must 'true'.
202
+ else
203
+ warn("Unknown key: #{key} = #{value}")
204
+ end
205
+ }
206
+ end
207
+
208
+ private
209
+
210
+ def normalize_cookie_value(value)
211
+ if value
212
+ value = value.strip.sub(/\A"(.*)"\z/) { $1 }
213
+ value = nil if value.empty?
214
+ end
215
+ value
216
+ end
217
+ end
218
+
219
+ ##
220
+ # An Array class that already includes the MonitorMixin module.
221
+ #
222
+ class SynchronizedArray < Array
223
+ include MonitorMixin
224
+ end
225
+
226
+ class CookieManager
227
+ include CookieUtils
228
+
229
+ ### errors
230
+ class Error < StandardError; end
231
+ class ErrorOverrideOK < Error; end
232
+ class SpecialError < Error; end
233
+
234
+ attr_reader :cookies
235
+ attr_accessor :cookies_file
236
+ attr_accessor :accept_domains, :reject_domains
237
+
238
+ def initialize(file=nil)
239
+ @cookies = SynchronizedArray.new
240
+ @cookies_file = file
241
+ @is_saved = true
242
+ @reject_domains = Array.new
243
+ @accept_domains = Array.new
244
+ @netscape_rule = false
245
+ end
246
+
247
+ def cookies=(cookies)
248
+ if cookies.is_a?(SynchronizedArray)
249
+ @cookies = cookies
250
+ else
251
+ @cookies = SynchronizedArray.new(cookies)
252
+ end
253
+ end
254
+
255
+ def save_all_cookies(force = nil, save_unused = true, save_discarded = true)
256
+ @cookies.synchronize do
257
+ check_expired_cookies
258
+ if @is_saved and !force
259
+ return
260
+ end
261
+ File.open(@cookies_file, 'w') do |f|
262
+ @cookies.each do |cookie|
263
+ if (cookie.use? or save_unused) and
264
+ (!cookie.discard? or save_discarded)
265
+ f.print(cookie.url.to_s,"\t",
266
+ cookie.name,"\t",
267
+ cookie.value,"\t",
268
+ cookie.expires.to_i,"\t",
269
+ cookie.domain,"\t",
270
+ cookie.path,"\t",
271
+ cookie.flag,"\n")
272
+ end
273
+ end
274
+ end
275
+ end
276
+ @is_saved = true
277
+ end
278
+
279
+ def save_cookies(force = nil)
280
+ save_all_cookies(force, false, false)
281
+ end
282
+
283
+ def check_expired_cookies
284
+ @cookies.reject!{|cookie|
285
+ is_expired = (cookie.expires && (cookie.expires < Time.now.gmtime))
286
+ if is_expired && !cookie.discard?
287
+ @is_saved = false
288
+ end
289
+ is_expired
290
+ }
291
+ end
292
+
293
+ def parse(str, url)
294
+ cookie = WebAgent::Cookie.new
295
+ cookie.parse(str, url)
296
+ add(cookie)
297
+ end
298
+
299
+ def find(url)
300
+ return nil if @cookies.empty?
301
+
302
+ cookie_list = Array.new
303
+ @cookies.each{|cookie|
304
+ is_expired = (cookie.expires && (cookie.expires < Time.now.gmtime))
305
+ if cookie.use? && !is_expired && cookie.match?(url)
306
+ if cookie_list.select{|c1| c1.name == cookie.name}.empty?
307
+ cookie_list << cookie
308
+ end
309
+ end
310
+ }
311
+ return make_cookie_str(cookie_list)
312
+ end
313
+ alias cookie_value find
314
+
315
+ def add(given)
316
+ check_domain(given.domain, given.url.host, given.override?)
317
+
318
+ domain = given.domain || given.url.host
319
+ path = given.path || given.url.path.sub(%r|/[^/]*\z|, '')
320
+
321
+ cookie = nil
322
+ @cookies.synchronize do
323
+ check_expired_cookies
324
+ cookie = @cookies.find { |c|
325
+ c.domain == domain && c.path == path && c.name == given.name
326
+ }
327
+ if !cookie
328
+ cookie = WebAgent::Cookie.new
329
+ cookie.use = true
330
+ @cookies << cookie
331
+ end
332
+ end
333
+
334
+ cookie.domain = domain
335
+ cookie.path = path
336
+ cookie.url = given.url
337
+ cookie.name = given.name
338
+ cookie.value = given.value
339
+ cookie.expires = given.expires
340
+ cookie.secure = given.secure?
341
+ cookie.http_only = given.http_only?
342
+ cookie.domain_orig = given.domain
343
+ cookie.path_orig = given.path
344
+
345
+ if cookie.discard? || cookie.expires == nil
346
+ cookie.discard = true
347
+ else
348
+ cookie.discard = false
349
+ @is_saved = false
350
+ end
351
+ end
352
+
353
+ def load_cookies
354
+ return if !File.readable?(@cookies_file)
355
+ @cookies.synchronize do
356
+ @cookies.clear
357
+ File.open(@cookies_file,'r'){|f|
358
+ while line = f.gets
359
+ cookie = WebAgent::Cookie.new
360
+ @cookies << cookie
361
+ col = line.chomp.split(/\t/)
362
+ cookie.url = HTTPClient::Util.urify(col[0])
363
+ cookie.name = col[1]
364
+ cookie.value = col[2]
365
+ if col[3].empty? or col[3] == '0'
366
+ cookie.expires = nil
367
+ else
368
+ cookie.expires = Time.at(col[3].to_i).gmtime
369
+ end
370
+ cookie.domain = col[4]
371
+ cookie.path = col[5]
372
+ cookie.set_flag(col[6])
373
+ end
374
+ }
375
+ end
376
+ end
377
+
378
+ # Who use it?
379
+ def check_cookie_accept_domain(domain)
380
+ unless domain
381
+ return false
382
+ end
383
+ @accept_domains.each{|dom|
384
+ if domain_match(domain, dom)
385
+ return true
386
+ end
387
+ }
388
+ @reject_domains.each{|dom|
389
+ if domain_match(domain, dom)
390
+ return false
391
+ end
392
+ }
393
+ return true
394
+ end
395
+
396
+ private
397
+
398
+ def make_cookie_str(cookie_list)
399
+ if cookie_list.empty?
400
+ return nil
401
+ end
402
+
403
+ ret = ''
404
+ c = cookie_list.shift
405
+ ret += "#{c.name}=#{c.value}"
406
+ cookie_list.each{|cookie|
407
+ ret += "; #{cookie.name}=#{cookie.value}"
408
+ }
409
+ return ret
410
+ end
411
+
412
+ # for conformance to http://wp.netscape.com/newsref/std/cookie_spec.html
413
+ attr_accessor :netscape_rule
414
+ SPECIAL_DOMAIN = [".com",".edu",".gov",".mil",".net",".org",".int"]
415
+
416
+ def check_domain(domain, hostname, override)
417
+ return unless domain
418
+
419
+ # [DRAFT 12] s. 4.2.2 (does not apply in the case that
420
+ # host name is the same as domain attribute for version 0
421
+ # cookie)
422
+ # I think that this rule has almost the same effect as the
423
+ # tail match of [NETSCAPE].
424
+ if domain !~ /^\./ && hostname != domain
425
+ domain = '.'+domain
426
+ end
427
+ # [NETSCAPE] rule
428
+ if @netscape_rule
429
+ n = domain.scan(/\./).length
430
+ if n < 2
431
+ cookie_error(SpecialError.new, override)
432
+ elsif n == 2
433
+ ## [NETSCAPE] rule
434
+ ok = SPECIAL_DOMAIN.select{|sdomain|
435
+ sdomain == domain[-(sdomain.length)..-1]
436
+ }
437
+ if ok.empty?
438
+ cookie_error(SpecialError.new, override)
439
+ end
440
+ end
441
+ end
442
+ # this implementation does not check RFC2109 4.3.2 case 2;
443
+ # the portion of host not in domain does not contain a dot.
444
+ # according to nsCookieService.cpp in Firefox 3.0.4, Firefox 3.0.4
445
+ # and IE does not check, too.
446
+ end
447
+
448
+ # not tested well; used only netscape_rule = true.
449
+ def cookie_error(err, override)
450
+ if !err.kind_of?(ErrorOverrideOK) || !override
451
+ raise err
452
+ end
453
+ end
454
+ end
455
+ end
456
+
457
+ class HTTPClient
458
+ CookieManager = WebAgent::CookieManager
459
+ end