httpclient 2.2.1 → 2.2.2

Sign up to get free protection for your applications and to get access to all the features.
data/lib/httpclient.rb CHANGED
@@ -11,6 +11,7 @@ require 'stringio'
11
11
  require 'digest/sha1'
12
12
 
13
13
  # Extra library
14
+ require 'httpclient/version'
14
15
  require 'httpclient/util'
15
16
  require 'httpclient/ssl_config'
16
17
  require 'httpclient/connection'
@@ -229,7 +230,6 @@ require 'httpclient/cookie'
229
230
  # ruby -rhttpclient -e 'p HTTPClient.head(ARGV.shift).header["last-modified"]' http://dev.ctor.org/
230
231
  #
231
232
  class HTTPClient
232
- VERSION = '2.2.1'
233
233
  RUBY_VERSION_STRING = "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"
234
234
  /: (\S+) (\S+)/ =~ %q$Id$
235
235
  LIB_NAME = "(#{$1}/#{$2}, #{RUBY_VERSION_STRING})"
@@ -335,6 +335,8 @@ class HTTPClient
335
335
  attr_proxy(:send_timeout, true)
336
336
  # Response receiving timeout in sec.
337
337
  attr_proxy(:receive_timeout, true)
338
+ # Reuse the same connection within this timeout in sec. from last used.
339
+ attr_proxy(:keep_alive_timeout, true)
338
340
  # Negotiation retry count for authentication. 5 by default.
339
341
  attr_proxy(:protocol_retry_count, true)
340
342
  # if your ruby is older than 2005-09-06, do not set socket_sync = false to
@@ -1060,7 +1062,7 @@ private
1060
1062
  do_get_header(req, res, sess)
1061
1063
  conn.push(res)
1062
1064
  sess.get_body do |part|
1063
- force_binary(part)
1065
+ set_encoding(part, res.body_encoding)
1064
1066
  if block
1065
1067
  block.call(res, part)
1066
1068
  else
@@ -1096,7 +1098,7 @@ private
1096
1098
  do_get_header(req, res, sess)
1097
1099
  conn.push(res)
1098
1100
  sess.get_body do |part|
1099
- force_binary(part)
1101
+ set_encoding(part, res.body_encoding)
1100
1102
  pipew.write(part)
1101
1103
  end
1102
1104
  pipew.close
@@ -1109,9 +1111,7 @@ private
1109
1111
 
1110
1112
  def do_get_header(req, res, sess)
1111
1113
  res.http_version, res.status, res.reason, headers = sess.get_header
1112
- headers.each do |key, value|
1113
- res.header.add(key, value)
1114
- end
1114
+ res.header.set_headers(headers)
1115
1115
  if @cookie_manager
1116
1116
  res.header['set-cookie'].each do |cookie|
1117
1117
  @cookie_manager.parse(cookie, req.header.request_uri)
@@ -1125,4 +1125,8 @@ private
1125
1125
  @debug_dev << "\n\n= Dummy Response\n\n"
1126
1126
  @debug_dev << res
1127
1127
  end
1128
+
1129
+ def set_encoding(str, encoding)
1130
+ str.force_encoding(encoding) if encoding
1131
+ end
1128
1132
  end
@@ -7,6 +7,7 @@
7
7
 
8
8
 
9
9
  require 'time'
10
+ require 'open-uri' # for encoding
10
11
 
11
12
 
12
13
  # A namespace module for HTTP Message definitions used by HTTPClient.
@@ -122,6 +123,8 @@ module HTTP
122
123
  attr_accessor :body_charset # :nodoc:
123
124
  # Used for dumping response.
124
125
  attr_accessor :body_date # :nodoc:
126
+ # Used for keeping content encoding.
127
+ attr_reader :body_encoding # :nodoc:
125
128
 
126
129
  # HTTP response status code to reason phrase mapping definition.
127
130
  STATUS_CODE_MAP = {
@@ -166,6 +169,7 @@ module HTTP
166
169
  @body_type = nil
167
170
  @body_charset = nil
168
171
  @body_date = nil
172
+ @body_encoding = nil
169
173
 
170
174
  @is_request = nil
171
175
  @header_item = []
@@ -205,14 +209,31 @@ module HTTP
205
209
  end
206
210
 
207
211
  # Returns 'Content-Type' header value.
208
- def contenttype
212
+ def content_type
209
213
  self['Content-Type'][0]
210
214
  end
211
215
 
212
216
  # Sets 'Content-Type' header value. Overrides if already exists.
213
- def contenttype=(contenttype)
217
+ def content_type=(content_type)
214
218
  delete('Content-Type')
215
- self['Content-Type'] = contenttype
219
+ self['Content-Type'] = content_type
220
+ end
221
+
222
+ alias contenttype content_type
223
+ alias contenttype= content_type=
224
+
225
+ if defined?(Encoding::ASCII_8BIT)
226
+ def set_body_encoding
227
+ if type = self.content_type
228
+ OpenURI::Meta.init(o = '')
229
+ o.meta_add_field('content-type', type)
230
+ @body_encoding = o.encoding
231
+ end
232
+ end
233
+ else
234
+ def set_body_encoding
235
+ @body_encoding = nil
236
+ end
216
237
  end
217
238
 
218
239
  # Sets byte size of message body.
@@ -285,6 +306,13 @@ module HTTP
285
306
  get(key).collect { |item| item[1] }
286
307
  end
287
308
 
309
+ def set_headers(headers)
310
+ headers.each do |key, value|
311
+ add(key, value)
312
+ end
313
+ set_body_encoding
314
+ end
315
+
288
316
  def create_query_uri()
289
317
  if @request_method == 'CONNECT'
290
318
  return "#{@request_uri.host}:#{@request_uri.port}"
@@ -787,7 +815,7 @@ module HTTP
787
815
  end
788
816
 
789
817
  def escape_query(query) # :nodoc:
790
- query.sort_by { |attr, value| attr.to_s }.collect { |attr, value|
818
+ query.collect { |attr, value|
791
819
  if value.respond_to?(:read)
792
820
  value = value.read
793
821
  end
@@ -796,12 +824,14 @@ module HTTP
796
824
  end
797
825
 
798
826
  # from CGI.escape
799
- def escape(str) # :nodoc:
800
- if defined?(Encoding::ASCII_8BIT)
827
+ if defined?(Encoding::ASCII_8BIT)
828
+ def escape(str) # :nodoc:
801
829
  str.dup.force_encoding(Encoding::ASCII_8BIT).gsub(/([^ a-zA-Z0-9_.-]+)/) {
802
830
  '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
803
831
  }.tr(' ', '+')
804
- else
832
+ end
833
+ else
834
+ def escape(str) # :nodoc:
805
835
  str.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
806
836
  '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
807
837
  }.tr(' ', '+')
@@ -915,14 +945,21 @@ module HTTP
915
945
  @http_header.reason_phrase = reason
916
946
  end
917
947
 
948
+ # Returns 'Content-Type' header value.
949
+ def content_type
950
+ @http_header.content_type
951
+ end
952
+
918
953
  # Sets 'Content-Type' header value. Overrides if already exists.
919
- def contenttype
920
- @http_header.contenttype
954
+ def content_type=(content_type)
955
+ @http_header.content_type = content_type
921
956
  end
957
+ alias contenttype content_type
958
+ alias contenttype= content_type=
922
959
 
923
- # Returns 'Content-Type' header value.
924
- def contenttype=(contenttype)
925
- @http_header.contenttype = contenttype
960
+ # Returns content encoding
961
+ def body_encoding
962
+ @http_header.body_encoding
926
963
  end
927
964
 
928
965
  # Returns a content of message body. A String or an IO.
@@ -102,6 +102,7 @@ class HTTPClient
102
102
  attr_accessor :connect_retry
103
103
  attr_accessor :send_timeout
104
104
  attr_accessor :receive_timeout
105
+ attr_accessor :keep_alive_timeout
105
106
  attr_accessor :read_block_size
106
107
  attr_accessor :protocol_retry_count
107
108
 
@@ -129,7 +130,8 @@ class HTTPClient
129
130
  @connect_timeout = 60
130
131
  @connect_retry = 1
131
132
  @send_timeout = 120
132
- @receive_timeout = 60 # For each read_block_size bytes
133
+ @receive_timeout = 60 # For each read_block_size bytes
134
+ @keep_alive_timeout = 15 # '15' is from Apache 2 default
133
135
  @read_block_size = 1024 * 16 # follows net/http change in 1.8.7
134
136
  @protocol_retry_count = 5
135
137
 
@@ -139,8 +141,9 @@ class HTTPClient
139
141
  @transparent_gzip_decompression = false
140
142
  @socket_local = Site.new
141
143
 
142
- @sess_pool = []
144
+ @sess_pool = {}
143
145
  @sess_pool_mutex = Mutex.new
146
+ @sess_pool_last_checked = Time.now
144
147
  end
145
148
 
146
149
  def proxy=(proxy)
@@ -172,14 +175,15 @@ class HTTPClient
172
175
  close_all
173
176
  end
174
177
 
178
+ # assert: sess.last_used must not be nil
175
179
  def keep(sess)
176
180
  add_cached_session(sess)
177
181
  end
178
182
 
179
183
  def invalidate(site)
180
184
  @sess_pool_mutex.synchronize do
181
- @sess_pool.each do |sess|
182
- if sess.dest == site
185
+ if pool = @sess_pool[site]
186
+ pool.each do |sess|
183
187
  sess.invalidate
184
188
  end
185
189
  end
@@ -189,11 +193,12 @@ class HTTPClient
189
193
  private
190
194
 
191
195
  def open(uri, via_proxy = false)
196
+ site = Site.new(uri)
192
197
  sess = nil
193
- if cached = get_cached_session(uri)
198
+ if cached = get_cached_session(site)
194
199
  sess = cached
195
200
  else
196
- sess = Session.new(@client, Site.new(uri), @agent_name, @from)
201
+ sess = Session.new(@client, site, @agent_name, @from)
197
202
  sess.proxy = via_proxy ? @proxy : nil
198
203
  sess.socket_sync = @socket_sync
199
204
  sess.requested_version = @protocol_version if @protocol_version
@@ -214,8 +219,10 @@ class HTTPClient
214
219
 
215
220
  def close_all
216
221
  @sess_pool_mutex.synchronize do
217
- @sess_pool.each do |sess|
218
- sess.close
222
+ @sess_pool.each do |site, pool|
223
+ pool.each do |sess|
224
+ sess.close
225
+ end
219
226
  end
220
227
  end
221
228
  @sess_pool.clear
@@ -223,7 +230,7 @@ class HTTPClient
223
230
 
224
231
  # This method might not work as you expected...
225
232
  def close(dest)
226
- if cached = get_cached_session(dest)
233
+ if cached = get_cached_session(Site.new(dest))
227
234
  cached.close
228
235
  true
229
236
  else
@@ -231,27 +238,44 @@ class HTTPClient
231
238
  end
232
239
  end
233
240
 
234
- def get_cached_session(uri)
235
- cached = nil
241
+ def get_cached_session(site)
236
242
  @sess_pool_mutex.synchronize do
237
- new_pool = []
238
- @sess_pool.each do |s|
239
- if s.invalidated?
240
- s.close # close & remove from the pool
241
- elsif !cached && s.dest.match(uri)
242
- cached = s
243
- else
244
- new_pool << s
243
+ now = Time.now
244
+ if now > @sess_pool_last_checked + @keep_alive_timeout
245
+ scrub_cached_session(now)
246
+ @sess_pool_last_checked = now
247
+ end
248
+ if pool = @sess_pool[site]
249
+ pool.each_with_index do |sess, idx|
250
+ if valid_session?(sess, now)
251
+ return pool.slice!(idx)
252
+ end
245
253
  end
246
254
  end
247
- @sess_pool = new_pool
248
255
  end
249
- cached
256
+ nil
257
+ end
258
+
259
+ def scrub_cached_session(now)
260
+ @sess_pool.each do |site, pool|
261
+ pool.replace(pool.select { |sess|
262
+ if valid_session?(sess, now)
263
+ true
264
+ else
265
+ sess.close # close & remove from the pool
266
+ false
267
+ end
268
+ })
269
+ end
270
+ end
271
+
272
+ def valid_session?(sess, now)
273
+ !sess.invalidated? and (now <= sess.last_used + @keep_alive_timeout)
250
274
  end
251
275
 
252
276
  def add_cached_session(sess)
253
277
  @sess_pool_mutex.synchronize do
254
- @sess_pool << sess
278
+ (@sess_pool[sess.dest] ||= []).unshift(sess)
255
279
  end
256
280
  end
257
281
  end
@@ -523,6 +547,7 @@ class HTTPClient
523
547
  attr_accessor :test_loopback_http_response
524
548
 
525
549
  attr_accessor :transparent_gzip_decompression
550
+ attr_reader :last_used
526
551
 
527
552
  def initialize(client, dest, agent_name, from)
528
553
  @client = client
@@ -561,6 +586,7 @@ class HTTPClient
561
586
  @readbuf = nil
562
587
 
563
588
  @transparent_gzip_decompression = false
589
+ @last_used = nil
564
590
  end
565
591
 
566
592
  # Send a request to the server
@@ -594,6 +620,7 @@ class HTTPClient
594
620
  @state = :META if @state == :WAIT
595
621
  @next_connection = nil
596
622
  @requests.push(req)
623
+ @last_used = Time.now
597
624
  end
598
625
 
599
626
  def close
@@ -116,11 +116,6 @@ class HTTPClient
116
116
  def https?(uri)
117
117
  uri.scheme.downcase == 'https'
118
118
  end
119
-
120
- def force_binary(str)
121
- str.force_encoding(Encoding::ASCII_8BIT) if defined?(Encoding::ASCII_8BIT)
122
- str
123
- end
124
119
  end
125
120
 
126
121
 
@@ -0,0 +1,3 @@
1
+ class HTTPClient
2
+ VERSION = '2.2.2'
3
+ end
@@ -1356,6 +1356,19 @@ EOS
1356
1356
  end
1357
1357
  end
1358
1358
 
1359
+ def test_body_param_order
1360
+ ary = ('b'..'d').map { |k| ['key2', k] } << ['key1', 'a'] << ['key3', 'z']
1361
+ assert_equal("key2=b&key2=c&key2=d&key1=a&key3=z", HTTP::Message.escape_query(ary))
1362
+ end
1363
+
1364
+ if RUBY_VERSION > "1.9"
1365
+ def test_charset
1366
+ body = @client.get(serverurl + 'charset').body
1367
+ assert_equal(Encoding::EUC_JP, body.encoding)
1368
+ assert_equal('あいうえお'.encode(Encoding::EUC_JP), body)
1369
+ end
1370
+ end
1371
+
1359
1372
  private
1360
1373
 
1361
1374
  def check_query_get(query)
@@ -1379,7 +1392,7 @@ private
1379
1392
  :DocumentRoot => File.dirname(File.expand_path(__FILE__))
1380
1393
  )
1381
1394
  @serverport = @server.config[:Port]
1382
- [:hello, :sleep, :servlet_redirect, :redirect1, :redirect2, :redirect3, :redirect_self, :relative_redirect, :chunked, :largebody, :status, :compressed].each do |sym|
1395
+ [:hello, :sleep, :servlet_redirect, :redirect1, :redirect2, :redirect3, :redirect_self, :relative_redirect, :chunked, :largebody, :status, :compressed, :charset].each do |sym|
1383
1396
  @server.mount(
1384
1397
  "/#{sym}",
1385
1398
  WEBrick::HTTPServlet::ProcHandler.new(method("do_#{sym}").to_proc)
@@ -1456,6 +1469,7 @@ private
1456
1469
  end
1457
1470
 
1458
1471
  def do_compressed(req, res)
1472
+ res['content-type'] = 'application/octet-stream'
1459
1473
  if req.query['enc'] == 'gzip'
1460
1474
  res['content-encoding'] = 'gzip'
1461
1475
  res.body = GZIP_CONTENT
@@ -1465,6 +1479,15 @@ private
1465
1479
  end
1466
1480
  end
1467
1481
 
1482
+ def do_charset(req, res)
1483
+ if RUBY_VERSION > "1.9"
1484
+ res.body = 'あいうえお'.encode("euc-jp")
1485
+ res['Content-Type'] = 'text/plain; charset=euc-jp'
1486
+ else
1487
+ res.body = 'this endpoint is for 1.9 or later'
1488
+ end
1489
+ end
1490
+
1468
1491
  def do_status(req, res)
1469
1492
  res.status = req.query['status'].to_i
1470
1493
  end
@@ -1485,6 +1508,7 @@ private
1485
1508
  end
1486
1509
 
1487
1510
  def do_POST(req, res)
1511
+ res["content-type"] = "text/plain" # iso-8859-1, not US-ASCII
1488
1512
  res.body = 'post,' + req.body.to_s
1489
1513
  res["x-query"] = body_response(req)
1490
1514
  end
data/test/test_ssl.rb CHANGED
@@ -99,16 +99,25 @@ class TestSSL < Test::Unit::TestCase
99
99
  assert_equal("hello", @client.get_content(@url))
100
100
  assert(@verify_callback_called)
101
101
  #
102
- cfg.verify_depth = 1
102
+ unless ENV['TRAVIS']
103
+ # On travis environment, verify_depth seems to not work properly.
104
+ # Ubuntu 10.04 + OpenSSL 0.9.8k issue?
105
+ cfg.verify_depth = 1 # 2 required: root-sub
103
106
  @verify_callback_called = false
104
107
  begin
105
108
  @client.get(@url)
106
- assert(false)
109
+ assert(false, "verify_depth is not supported? #{OpenSSL::OPENSSL_VERSION}")
107
110
  rescue OpenSSL::SSL::SSLError => ssle
108
111
  assert_match(/certificate verify failed/, ssle.message)
109
112
  assert(@verify_callback_called)
110
113
  end
111
114
  #
115
+ cfg.verify_depth = 2 # 2 required: root-sub
116
+ @verify_callback_called = false
117
+ @client.get(@url)
118
+ assert(@verify_callback_called)
119
+ #
120
+ end
112
121
  cfg.verify_depth = nil
113
122
  cfg.cert_store = OpenSSL::X509::Store.new
114
123
  cfg.verify_mode = OpenSSL::SSL::VERIFY_PEER
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: httpclient
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.1
4
+ version: 2.2.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-06-02 00:00:00.000000000 Z
12
+ date: 2011-10-17 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description:
15
15
  email: nahi@ruby-lang.org
@@ -17,56 +17,57 @@ executables: []
17
17
  extensions: []
18
18
  extra_rdoc_files: []
19
19
  files:
20
- - lib/oauthclient.rb
21
- - lib/http-access2.rb
22
- - lib/http-access2/cookie.rb
23
- - lib/http-access2/http.rb
24
- - lib/hexdump.rb
20
+ - lib/httpclient/auth.rb
21
+ - lib/httpclient/ssl_config.rb
22
+ - lib/httpclient/http.rb
25
23
  - lib/httpclient/cacert_sha1.p7s
26
- - lib/httpclient/cookie.rb
27
- - lib/httpclient/session.rb
28
24
  - lib/httpclient/timeout.rb
29
- - lib/httpclient/util.rb
30
- - lib/httpclient/cacert.p7s
31
- - lib/httpclient/ssl_config.rb
25
+ - lib/httpclient/version.rb
32
26
  - lib/httpclient/connection.rb
33
- - lib/httpclient/auth.rb
34
- - lib/httpclient/http.rb
27
+ - lib/httpclient/session.rb
28
+ - lib/httpclient/cacert.p7s
29
+ - lib/httpclient/cookie.rb
30
+ - lib/httpclient/util.rb
31
+ - lib/oauthclient.rb
35
32
  - lib/httpclient.rb
36
- - sample/async.rb
37
- - sample/howto.rb
38
- - sample/cookie.rb
39
- - sample/oauth_buzz.rb
40
- - sample/wcat.rb
41
- - sample/oauth_twitter.rb
33
+ - lib/http-access2.rb
34
+ - lib/http-access2/http.rb
35
+ - lib/http-access2/cookie.rb
36
+ - lib/hexdump.rb
37
+ - sample/auth.rb
42
38
  - sample/dav.rb
43
- - sample/thread.rb
44
39
  - sample/stream.rb
45
- - sample/ssl/0key.pem
40
+ - sample/async.rb
41
+ - sample/wcat.rb
42
+ - sample/ssl/1000cert.pem
43
+ - sample/ssl/1000key.pem
46
44
  - sample/ssl/ssl_client.rb
45
+ - sample/ssl/0key.pem
47
46
  - sample/ssl/htdocs/index.html
48
- - sample/ssl/webrick_httpsd.rb
49
47
  - sample/ssl/0cert.pem
50
- - sample/ssl/1000cert.pem
51
- - sample/ssl/1000key.pem
48
+ - sample/ssl/webrick_httpsd.rb
49
+ - sample/thread.rb
52
50
  - sample/oauth_friendfeed.rb
53
- - sample/auth.rb
51
+ - sample/oauth_twitter.rb
52
+ - sample/cookie.rb
53
+ - sample/howto.rb
54
+ - sample/oauth_buzz.rb
55
+ - test/client.cert
56
+ - test/test_http-access2.rb
57
+ - test/server.cert
58
+ - test/htdigest
59
+ - test/test_ssl.rb
54
60
  - test/test_cookie.rb
61
+ - test/runner.rb
55
62
  - test/htpasswd
56
- - test/test_httpclient.rb
57
63
  - test/test_auth.rb
58
- - test/runner.rb
59
- - test/helper.rb
60
- - test/subca.cert
61
- - test/server.key
62
64
  - test/client.key
65
+ - test/helper.rb
63
66
  - test/ca.cert
64
- - test/htdigest
65
- - test/test_http-access2.rb
66
- - test/server.cert
67
- - test/test_ssl.rb
68
- - test/client.cert
69
67
  - test/sslsvr.rb
68
+ - test/subca.cert
69
+ - test/server.key
70
+ - test/test_httpclient.rb
70
71
  homepage: http://github.com/nahi/httpclient
71
72
  licenses: []
72
73
  post_install_message:
@@ -87,7 +88,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
87
88
  version: '0'
88
89
  requirements: []
89
90
  rubyforge_project:
90
- rubygems_version: 1.8.4
91
+ rubygems_version: 1.8.10
91
92
  signing_key:
92
93
  specification_version: 3
93
94
  summary: gives something like the functionality of libwww-perl (LWP) in Ruby