stella 0.7.0.004 → 0.7.0.005

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.
Files changed (35) hide show
  1. data/bin/stella +16 -20
  2. data/examples/essentials/logo.png +0 -0
  3. data/examples/{basic → essentials}/plan.rb +7 -3
  4. data/examples/{basic → essentials}/search_terms.csv +0 -0
  5. data/examples/example_webapp.rb +7 -4
  6. data/examples/example_webapp.ru +3 -0
  7. data/lib/stella.rb +18 -26
  8. data/lib/stella/cli.rb +4 -1
  9. data/lib/stella/client.rb +49 -26
  10. data/lib/stella/data.rb +35 -9
  11. data/lib/stella/data/http.rb +1 -1
  12. data/lib/stella/data/http/request.rb +3 -14
  13. data/lib/stella/engine.rb +10 -4
  14. data/lib/stella/engine/functional.rb +2 -4
  15. data/lib/stella/engine/load.rb +24 -21
  16. data/lib/stella/mixins.rb +1 -1
  17. data/lib/stella/stats.rb +17 -4
  18. data/lib/stella/testplan/usecase.rb +2 -2
  19. data/lib/stella/utils.rb +16 -1
  20. data/lib/stella/version.rb +1 -1
  21. data/stella.gemspec +17 -4
  22. data/vendor/httpclient-2.1.5.2/httpclient.rb +1025 -0
  23. data/vendor/httpclient-2.1.5.2/httpclient/auth.rb +522 -0
  24. data/vendor/httpclient-2.1.5.2/httpclient/cacert.p7s +1579 -0
  25. data/vendor/httpclient-2.1.5.2/httpclient/cacert_sha1.p7s +1579 -0
  26. data/vendor/httpclient-2.1.5.2/httpclient/connection.rb +84 -0
  27. data/vendor/httpclient-2.1.5.2/httpclient/cookie.rb +562 -0
  28. data/vendor/httpclient-2.1.5.2/httpclient/http.rb +867 -0
  29. data/vendor/httpclient-2.1.5.2/httpclient/session.rb +864 -0
  30. data/vendor/httpclient-2.1.5.2/httpclient/ssl_config.rb +417 -0
  31. data/vendor/httpclient-2.1.5.2/httpclient/stats.rb +90 -0
  32. data/vendor/httpclient-2.1.5.2/httpclient/timeout.rb +136 -0
  33. data/vendor/httpclient-2.1.5.2/httpclient/util.rb +86 -0
  34. metadata +18 -5
  35. data/lib/stella/dsl.rb +0 -5
@@ -1,4 +1,4 @@
1
1
 
2
2
  module Stella::Data::HTTP; end
3
3
 
4
- Stella::Utils.require_glob(STELLA_LIB_HOME, 'stella', 'data', 'http', '*.rb')
4
+ Stella::Utils.require_glob(Stella::LIB_HOME, 'stella', 'data', 'http', '*.rb')
@@ -25,7 +25,7 @@ module Stella::Data::HTTP
25
25
  field :content_type
26
26
 
27
27
  def has_body?
28
- !@body.nil? && !@body.empty?
28
+ !@body.nil?
29
29
  end
30
30
 
31
31
  def initialize (method, uri_str, version="1.1", &definition)
@@ -70,7 +70,7 @@ module Stella::Data::HTTP
70
70
  if definition.nil?
71
71
  @response_handler
72
72
  else
73
- args << 200 if args.empty?
73
+ args << /.+/ if args.empty?
74
74
  args.each do |status|
75
75
  @response_handler[status] = definition
76
76
  end
@@ -80,18 +80,7 @@ module Stella::Data::HTTP
80
80
  # +content+ can be literal content or a file path
81
81
  def body(*args)
82
82
  return @body if args.empty?
83
- content, form_param, content_type = *args
84
-
85
- @body.form_param = form_param if form_param
86
- @body.content_type = content_type if content_type
87
-
88
- if File.exists?(content)
89
- @body.content = File.new(content)
90
- @body.content_type ||= "application/x-www-form-urlencoded"
91
- else
92
- @body.content = content
93
- end
94
-
83
+ @body = args.first
95
84
  end
96
85
 
97
86
  def inspect
data/lib/stella/engine.rb CHANGED
@@ -19,12 +19,16 @@ module Stella::Engine
19
19
  end
20
20
  end
21
21
 
22
- def update_send_request(client_id, usecase, meth, uri, req, params, counter)
22
+ def update_prepare_request(client_id, usecase, req, counter)
23
23
  notice = "repeat: #{counter-1}" if counter > 1
24
24
  Stella.li2 ' ' << " %-46s %16s ".att(:reverse) % [req.desc, notice]
25
25
  end
26
26
 
27
- def update_receive_response(client_id, usecase, meth, uri, req, params, container)
27
+ def update_send_request(client_id, usecase, uri, req, params, counter)
28
+
29
+ end
30
+
31
+ def update_receive_response(client_id, usecase, uri, req, params, container)
28
32
  Stella.li ' %-59s %3d' % [uri, container.status]
29
33
  Stella.li2 " Method: " << req.http_method
30
34
  Stella.li2 " Params: " << params.inspect
@@ -42,14 +46,16 @@ module Stella::Engine
42
46
 
43
47
  def update_error_execute_response_handler(client_id, ex, req, container)
44
48
  Stella.le ex.message
49
+ Stella.ld ex.backtrace
45
50
  end
46
51
 
47
- def update_request_error(client_id, usecase, meth, uri, req, params, ex)
52
+ def update_request_error(client_id, usecase, uri, req, params, ex)
48
53
  desc = "#{usecase.desc} > #{req.desc}"
49
54
  Stella.le ' Client%-3s %-45s %s' % [client_id, desc, ex.message]
55
+ Stella.ld ex.backtrace
50
56
  end
51
57
 
52
58
  end
53
59
  end
54
60
 
55
- Stella::Utils.require_glob(STELLA_LIB_HOME, 'stella', 'engine', '*.rb')
61
+ Stella::Utils.require_glob(Stella::LIB_HOME, 'stella', 'engine', '*.rb')
@@ -19,8 +19,8 @@ module Stella::Engine
19
19
  client.enable_benchmark_mode if opts[:benchmark]
20
20
 
21
21
  Stella.li $/, "Starting test...", $/
22
- Drydock::Screen.flush
23
- sleep 0.2
22
+ Stella.lflush
23
+ sleep 0.3
24
24
 
25
25
  plan.usecases.each_with_index do |uc,i|
26
26
  desc = (uc.desc || "Usecase ##{i+1}")
@@ -28,8 +28,6 @@ module Stella::Engine
28
28
  Stella.rescue { client.execute uc }
29
29
  end
30
30
 
31
- Drydock::Screen.flush
32
-
33
31
  !plan.errors?
34
32
  end
35
33
 
@@ -7,40 +7,37 @@ module Stella::Engine
7
7
  def run(plan, opts={})
8
8
  opts = {
9
9
  :hosts => [],
10
- :users => 1,
10
+ :clients => 1,
11
11
  :time => nil,
12
12
  :benchmark => false,
13
13
  :repetitions => 1
14
14
  }.merge! opts
15
- opts[:users] = plan.usecases.size if opts[:users] < plan.usecases.size
16
- opts[:users] = 1000 if opts[:users] > 1000
15
+ opts[:clients] = plan.usecases.size if opts[:clients] < plan.usecases.size
16
+ opts[:clients] = 1000 if opts[:clients] > 1000
17
17
 
18
18
  Stella.ld "OPTIONS: #{opts.inspect}"
19
19
  Stella.li3 "Hosts: " << opts[:hosts].join(', ')
20
20
  Stella.li2 plan.pretty
21
21
 
22
22
  packages = build_thread_package plan, opts
23
- Stella.li $/, "Prepared #{packages.size} virtual users..."
24
- Drydock::Screen.flush
23
+ Stella.li $/, "Prepared #{packages.size} virtual clients..."
24
+ Stella.lflush
25
25
 
26
26
 
27
27
  Stella.li $/, "Starting test...", $/
28
- Drydock::Screen.flush
29
- sleep 0.2
28
+ Stella.lflush
29
+ sleep 0.3
30
30
 
31
- Thread.ify packages, :threads => opts[:users] do |package|
31
+ Thread.ify packages, :threads => opts[:clients] do |package|
32
32
  # TEMPFIX. The fill in build_thread_package is creating nil elements
33
33
  next if package.nil?
34
34
  (1..opts[:repetitions]).to_a.each do |rep|
35
35
  # We store client specific data in the usecase
36
36
  # so we clone it here so each thread is unique.
37
37
  Stella.rescue { package.client.execute package.usecase }
38
- Drydock::Screen.flush
39
38
  end
40
39
  end
41
40
 
42
- Drydock::Screen.flush
43
-
44
41
  !plan.errors?
45
42
  end
46
43
 
@@ -55,21 +52,22 @@ module Stella::Engine
55
52
  end
56
53
 
57
54
  def build_thread_package(plan, opts)
58
- packages, pointer = Array.new(opts[:users]), 0
55
+ packages, pointer = Array.new(opts[:clients]), 0
59
56
  plan.usecases.each_with_index do |usecase,i|
60
57
 
61
- count = case opts[:users]
58
+ count = case opts[:clients]
62
59
  when 0..9
63
- if (opts[:users] % plan.usecases.size > 0)
64
- raise Stella::Testplan::WackyRatio, "User count does not match usecase count evenly"
60
+ if (opts[:clients] % plan.usecases.size > 0)
61
+ msg = "Client count does not evenly match usecase count"
62
+ raise Stella::Testplan::WackyRatio, msg
65
63
  else
66
- (opts[:users] / plan.usecases.size)
64
+ (opts[:clients] / plan.usecases.size)
67
65
  end
68
66
  else
69
- (opts[:users] * usecase.ratio).to_i
67
+ (opts[:clients] * usecase.ratio).to_i
70
68
  end
71
69
 
72
- Stella.ld "THREAD PACKAGE: #{usecase.desc} #{pointer} #{(pointer+count)} #{count}"
70
+ Stella.ld "THREAD PACKAGE: #{usecase.desc} (#{pointer} + #{count})"
73
71
  # Fill the thread_package with the contents of the block
74
72
  packages.fill(pointer, count) do |index|
75
73
  Stella.li2 "Creating client ##{index+1} "
@@ -83,11 +81,15 @@ module Stella::Engine
83
81
  packages
84
82
  end
85
83
 
86
- def update_send_request(client_id, usecase, meth, uri, req, params, counter)
84
+ def update_prepare_request(client_id, usecase, req, counter)
85
+
86
+ end
87
+
88
+ def update_send_request(client_id, usecase, uri, req, params, counter)
87
89
 
88
90
  end
89
91
 
90
- def update_receive_response(client_id, usecase, meth, uri, req, params, container)
92
+ def update_receive_response(client_id, usecase, uri, req, params, container)
91
93
  desc = "#{usecase.desc} > #{req.desc}"
92
94
  Stella.li2 ' Client%-3s %3d %-6s %-45s %s' % [client_id, container.status, req.http_method, desc, uri]
93
95
  end
@@ -98,9 +100,10 @@ module Stella::Engine
98
100
  def update_error_execute_response_handler(client_id, ex, req, container)
99
101
  end
100
102
 
101
- def update_request_error(client_id, usecase, meth, uri, req, params, ex)
103
+ def update_request_error(client_id, usecase, uri, req, params, ex)
102
104
  desc = "#{usecase.desc} > #{req.desc}"
103
105
  Stella.le ' Client%-3s %-45s %s' % [client_id, desc, ex.message]
106
+ Stella.ld ex.backtrace
104
107
  end
105
108
 
106
109
 
data/lib/stella/mixins.rb CHANGED
@@ -1,2 +1,2 @@
1
1
 
2
- Stella::Utils.require_glob(STELLA_LIB_HOME, 'stella', 'mixins', '*.rb')
2
+ Stella::Utils.require_glob(Stella::LIB_HOME, 'stella', 'mixins', '*.rb')
data/lib/stella/stats.rb CHANGED
@@ -36,15 +36,28 @@ class Stats
36
36
 
37
37
  # Dump this Stats object with an optional additional message.
38
38
  def dump(msg = "", out=STDERR)
39
- out.puts "#{msg}: #{self.to_s}"
39
+ out.puts "#{msg}: #{self.inspect}"
40
40
  end
41
41
 
42
42
  # Returns a common display (used by dump)
43
- def to_s
44
- "[#{@name}]: SUM=%0.4f, SUMSQ=%0.4f, N=%0.4f, MEAN=%0.4f, SD=%0.4f, MIN=%0.4f, MAX=%0.4f" % [@sum, @sumsq, @n, mean, sd, @min, @max]
43
+ def inspect
44
+ v = [mean, @n, @sum, @sumsq, sd, @min, @max]
45
+ t = %w"N=%0.4f SUM=%0.4f SUMSQ=%0.4f SD=%0.4f MIN=%0.4f MAX=%0.4f"
46
+ "%0.4f: " << t % v
45
47
  end
46
48
 
47
-
49
+ def to_s
50
+ mean.to_s
51
+ end
52
+
53
+ def to_f
54
+ mean.to_f
55
+ end
56
+
57
+ def to_i
58
+ mean.to_i
59
+ end
60
+
48
61
  # Calculates and returns the mean for the data passed so far.
49
62
  def mean
50
63
  @sum / @n
@@ -59,13 +59,13 @@ class Testplan
59
59
 
60
60
  # Reads the contents of the file <tt>path</tt> (the current working
61
61
  # directory is assumed to be the same directory containing the test plan).
62
- def file(path)
62
+ def read(path)
63
63
  path = File.join(@base_path, path) if @base_path
64
64
  File.read(path)
65
65
  end
66
66
 
67
67
  def list(path)
68
- file(path).split $/
68
+ read(path).split $/
69
69
  end
70
70
 
71
71
  def add_request(meth, *args, &blk)
data/lib/stella/utils.rb CHANGED
@@ -2,7 +2,6 @@
2
2
  require 'socket'
3
3
  require 'open-uri'
4
4
  require 'date'
5
-
6
5
  require 'timeout'
7
6
 
8
7
  module Stella
@@ -56,6 +55,22 @@ module Stella
56
55
  end
57
56
  end
58
57
 
58
+
59
+ # <tt>require</tt> a library from the vendor directory.
60
+ # The vendor directory should be organized such
61
+ # that +name+ and +version+ can be used to create
62
+ # the path to the library.
63
+ #
64
+ # e.g.
65
+ #
66
+ # vendor/httpclient-2.1.5.2/httpclient
67
+ #
68
+ def require_vendor(name, version)
69
+ $: << File.join(LIB_HOME, '..', 'vendor', "#{name}-#{version}")
70
+ require name
71
+ end
72
+
73
+
59
74
  # Checks whether something is listening to a socket.
60
75
  # * +host+ A hostname
61
76
  # * +port+ The port to check
@@ -6,7 +6,7 @@ module Stella
6
6
  MAJOR = 0.freeze
7
7
  MINOR = 7.freeze
8
8
  TINY = 0.freeze
9
- PATCH = '004'.freeze
9
+ PATCH = '005'.freeze
10
10
  end
11
11
  def self.to_s; [MAJOR, MINOR, TINY].join('.'); end
12
12
  def self.to_f; self.to_s.to_f; end
data/stella.gemspec CHANGED
@@ -1,7 +1,7 @@
1
1
  @spec = Gem::Specification.new do |s|
2
2
  s.name = "stella"
3
3
  s.rubyforge_project = 'stella'
4
- s.version = "0.7.0.004"
4
+ s.version = "0.7.0.005"
5
5
  s.summary = "Stella: Your friend in performance testing."
6
6
  s.description = s.summary
7
7
  s.author = "Delano Mandelbaum"
@@ -29,9 +29,11 @@
29
29
  README.rdoc
30
30
  Rakefile
31
31
  bin/stella
32
- examples/basic/plan.rb
33
- examples/basic/search_terms.csv
32
+ examples/essentials/logo.png
33
+ examples/essentials/plan.rb
34
+ examples/essentials/search_terms.csv
34
35
  examples/example_webapp.rb
36
+ examples/example_webapp.ru
35
37
  examples/exceptions/plan.rb
36
38
  lib/stella.rb
37
39
  lib/stella/cli.rb
@@ -42,7 +44,6 @@
42
44
  lib/stella/data/http/body.rb
43
45
  lib/stella/data/http/request.rb
44
46
  lib/stella/data/http/response.rb
45
- lib/stella/dsl.rb
46
47
  lib/stella/engine.rb
47
48
  lib/stella/engine/functional.rb
48
49
  lib/stella/engine/load.rb
@@ -59,6 +60,18 @@
59
60
  lib/threadify.rb
60
61
  stella.gemspec
61
62
  support/useragents.txt
63
+ vendor/httpclient-2.1.5.2/httpclient.rb
64
+ vendor/httpclient-2.1.5.2/httpclient/auth.rb
65
+ vendor/httpclient-2.1.5.2/httpclient/cacert.p7s
66
+ vendor/httpclient-2.1.5.2/httpclient/cacert_sha1.p7s
67
+ vendor/httpclient-2.1.5.2/httpclient/connection.rb
68
+ vendor/httpclient-2.1.5.2/httpclient/cookie.rb
69
+ vendor/httpclient-2.1.5.2/httpclient/http.rb
70
+ vendor/httpclient-2.1.5.2/httpclient/session.rb
71
+ vendor/httpclient-2.1.5.2/httpclient/ssl_config.rb
72
+ vendor/httpclient-2.1.5.2/httpclient/stats.rb
73
+ vendor/httpclient-2.1.5.2/httpclient/timeout.rb
74
+ vendor/httpclient-2.1.5.2/httpclient/util.rb
62
75
  )
63
76
 
64
77
 
@@ -0,0 +1,1025 @@
1
+ # HTTPClient - HTTP client library.
2
+ # Copyright (C) 2000-2009 NAKAMURA, Hiroshi <nahi@ruby-lang.org>.
3
+ #
4
+ # This program is copyrighted free software by NAKAMURA, Hiroshi. You can
5
+ # redistribute it and/or modify it under the same terms of Ruby's license;
6
+ # either the dual license version in 2003, or any later version.
7
+
8
+
9
+
10
+ require 'uri'
11
+ require 'stringio'
12
+ require 'digest/sha1'
13
+
14
+ # Extra library
15
+ require 'httpclient/util'
16
+ require 'httpclient/ssl_config'
17
+ require 'httpclient/connection'
18
+ require 'httpclient/session'
19
+ require 'httpclient/http'
20
+ require 'httpclient/auth'
21
+ require 'httpclient/cookie'
22
+ require 'httpclient/stats'
23
+
24
+
25
+ # The HTTPClient class provides several methods for accessing Web resources
26
+ # via HTTP.
27
+ #
28
+ # HTTPClient instance is designed to be MT-safe. You can call a HTTPClient
29
+ # instance from several threads without synchronization after setting up an
30
+ # instance.
31
+ #
32
+ # clnt = HTTPClient.new
33
+ # clnt.set_cookie_store('/home/nahi/cookie.dat')
34
+ # urls.each do |url|
35
+ # Thread.new(url) do |u|
36
+ # p clnt.head(u).status
37
+ # end
38
+ # end
39
+ #
40
+ # == How to use
41
+ #
42
+ # At first, how to create your client. See initialize for more detail.
43
+ #
44
+ # 1. Create simple client.
45
+ #
46
+ # clnt = HTTPClient.new
47
+ #
48
+ # 2. Accessing resources through HTTP proxy. You can use environment
49
+ # variable 'http_proxy' or 'HTTP_PROXY' instead.
50
+ #
51
+ # clnt = HTTPClient.new('http://myproxy:8080')
52
+ #
53
+ # === How to retrieve web resources
54
+ #
55
+ # See get_content.
56
+ #
57
+ # 1. Get content of specified URL. It returns a String of whole result.
58
+ #
59
+ # puts clnt.get_content('http://dev.ctor.org/')
60
+ #
61
+ # 2. Get content as chunks of String. It yields chunks of String.
62
+ #
63
+ # clnt.get_content('http://dev.ctor.org/') do |chunk|
64
+ # puts chunk
65
+ # end
66
+ #
67
+ # === Invoking other HTTP methods
68
+ #
69
+ # See head, get, post, put, delete, options, propfind, proppatch and trace.
70
+ # It returns a HTTP::Message instance as a response.
71
+ #
72
+ # 1. Do HEAD request.
73
+ #
74
+ # res = clnt.head(uri)
75
+ # p res.header['Last-Modified'][0]
76
+ #
77
+ # 2. Do GET request with query.
78
+ #
79
+ # query = { 'keyword' => 'ruby', 'lang' => 'en' }
80
+ # res = clnt.get(uri, query)
81
+ # p res.status
82
+ # p res.contenttype
83
+ # p res.header['X-Custom']
84
+ # puts res.content
85
+ #
86
+ # === How to POST
87
+ #
88
+ # See post.
89
+ #
90
+ # 1. Do POST a form data.
91
+ #
92
+ # body = { 'keyword' => 'ruby', 'lang' => 'en' }
93
+ # res = clnt.post(uri, body)
94
+ #
95
+ # 2. Do multipart file upload with POST. No need to set extra header by
96
+ # yourself from httpclient/2.1.4.
97
+ #
98
+ # File.open('/tmp/post_data') do |file|
99
+ # body = { 'upload' => file, 'user' => 'nahi' }
100
+ # res = clnt.post(uri, body)
101
+ # end
102
+ #
103
+ # === Accessing via SSL
104
+ #
105
+ # Ruby needs to be compiled with OpenSSL.
106
+ #
107
+ # 1. Get content of specified URL via SSL.
108
+ # Just pass an URL which starts with 'https://'.
109
+ #
110
+ # https_url = 'https://www.rsa.com'
111
+ # clnt.get_content(https_url)
112
+ #
113
+ # 2. Getting peer certificate from response.
114
+ #
115
+ # res = clnt.get(https_url)
116
+ # p res.peer_cert #=> returns OpenSSL::X509::Certificate
117
+ #
118
+ # 3. Configuring OpenSSL options. See HTTPClient::SSLConfig for more details.
119
+ #
120
+ # user_cert_file = 'cert.pem'
121
+ # user_key_file = 'privkey.pem'
122
+ # clnt.ssl_config.set_client_cert_file(user_cert_file, user_key_file)
123
+ # clnt.get_content(https_url)
124
+ #
125
+ # === Handling Cookies
126
+ #
127
+ # 1. Using volatile Cookies. Nothing to do. HTTPClient handles Cookies.
128
+ #
129
+ # clnt = HTTPClient.new
130
+ # clnt.get_content(url1) # receives Cookies.
131
+ # clnt.get_content(url2) # sends Cookies if needed.
132
+ #
133
+ # 2. Saving non volatile Cookies to a specified file. Need to set a file at
134
+ # first and invoke save method at last.
135
+ #
136
+ # clnt = HTTPClient.new
137
+ # clnt.set_cookie_store('/home/nahi/cookie.dat')
138
+ # clnt.get_content(url)
139
+ # ...
140
+ # clnt.save_cookie_store
141
+ #
142
+ # 3. Disabling Cookies.
143
+ #
144
+ # clnt = HTTPClient.new
145
+ # clnt.cookie_manager = nil
146
+ #
147
+ # === Configuring authentication credentials
148
+ #
149
+ # 1. Authentication with Web server. Supports BasicAuth, DigestAuth, and
150
+ # Negotiate/NTLM (requires ruby/ntlm module).
151
+ #
152
+ # clnt = HTTPClient.new
153
+ # domain = 'http://dev.ctor.org/http-access2/'
154
+ # user = 'user'
155
+ # password = 'user'
156
+ # clnt.set_auth(domain, user, password)
157
+ # p clnt.get_content('http://dev.ctor.org/http-access2/login').status
158
+ #
159
+ # 2. Authentication with Proxy server. Supports BasicAuth and NTLM
160
+ # (requires win32/sspi)
161
+ #
162
+ # clnt = HTTPClient.new(proxy)
163
+ # user = 'proxy'
164
+ # password = 'proxy'
165
+ # clnt.set_proxy_auth(user, password)
166
+ # p clnt.get_content(url)
167
+ #
168
+ # === Invoking HTTP methods with custom header
169
+ #
170
+ # Pass a Hash or an Array for extheader argument.
171
+ #
172
+ # extheader = { 'Accept' => '*/*' }
173
+ # clnt.get_content(uri, query, extheader)
174
+ #
175
+ # extheader = [['Accept', 'image/jpeg'], ['Accept', 'image/png']]
176
+ # clnt.get_content(uri, query, extheader)
177
+ #
178
+ # === Invoking HTTP methods asynchronously
179
+ #
180
+ # See head_async, get_async, post_async, put_async, delete_async,
181
+ # options_async, propfind_async, proppatch_async, and trace_async.
182
+ # It immediately returns a HTTPClient::Connection instance as a returning value.
183
+ #
184
+ # connection = clnt.post_async(url, body)
185
+ # print 'posting.'
186
+ # while true
187
+ # break if connection.finished?
188
+ # print '.'
189
+ # sleep 1
190
+ # end
191
+ # puts '.'
192
+ # res = connection.pop
193
+ # p res.status
194
+ # p res.content.read # res.content is an IO for the res of async method.
195
+ #
196
+ # === Shortcut methods
197
+ #
198
+ # You can invoke get_content, get, etc. without creating HTTPClient instance.
199
+ #
200
+ # ruby -rhttpclient -e 'puts HTTPClient.get_content(ARGV.shift)' http://dev.ctor.org/
201
+ # ruby -rhttpclient -e 'p HTTPClient.head(ARGV.shift).header["last-modified"]' http://dev.ctor.org/
202
+ #
203
+ class HTTPClient
204
+ VERSION = '2.1.5'
205
+ RUBY_VERSION_STRING = "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"
206
+ /: (\S+) (\S+)/ =~ %q$Id: httpclient.rb 280 2009-06-02 15:44:28Z nahi $
207
+ LIB_NAME = "(#{$1}/#{$2}, #{RUBY_VERSION_STRING})"
208
+
209
+ include Util
210
+
211
+ # Raised for indicating running environment configuration error for example
212
+ # accessing via SSL under the ruby which is not compiled with OpenSSL.
213
+ class ConfigurationError < StandardError
214
+ end
215
+
216
+ # Raised for indicating HTTP response error.
217
+ class BadResponseError < RuntimeError
218
+ # HTTP::Message:: a response
219
+ attr_reader :res
220
+
221
+ def initialize(msg, res = nil) # :nodoc:
222
+ super(msg)
223
+ @res = res
224
+ end
225
+ end
226
+
227
+ # Raised for indicating a timeout error.
228
+ class TimeoutError < RuntimeError
229
+ end
230
+
231
+ # Raised for indicating a connection timeout error.
232
+ # You can configure connection timeout via HTTPClient#connect_timeout=.
233
+ class ConnectTimeoutError < TimeoutError
234
+ end
235
+
236
+ # Raised for indicating a request sending timeout error.
237
+ # You can configure request sending timeout via HTTPClient#send_timeout=.
238
+ class SendTimeoutError < TimeoutError
239
+ end
240
+
241
+ # Raised for indicating a response receiving timeout error.
242
+ # You can configure response receiving timeout via
243
+ # HTTPClient#receive_timeout=.
244
+ class ReceiveTimeoutError < TimeoutError
245
+ end
246
+
247
+ # Deprecated. just for backward compatibility
248
+ class Session
249
+ BadResponse = ::HTTPClient::BadResponseError
250
+ end
251
+
252
+ class << self
253
+ %w(get_content post_content head get post put delete options propfind proppatch trace).each do |name|
254
+ eval <<-EOD
255
+ def #{name}(*arg, &block)
256
+ clnt = new
257
+ begin
258
+ clnt.#{name}(*arg, &block)
259
+ ensure
260
+ clnt.reset_all
261
+ end
262
+ end
263
+ EOD
264
+ end
265
+
266
+ private
267
+
268
+ def attr_proxy(symbol, assignable = false)
269
+ name = symbol.to_s
270
+ define_method(name) {
271
+ @session_manager.__send__(name)
272
+ }
273
+ if assignable
274
+ aname = name + '='
275
+ define_method(aname) { |rhs|
276
+ reset_all
277
+ @session_manager.__send__(aname, rhs)
278
+ }
279
+ end
280
+ end
281
+ end
282
+
283
+ # HTTPClient::SSLConfig:: SSL configurator.
284
+ attr_reader :ssl_config
285
+ # WebAgent::CookieManager:: Cookies configurator.
286
+ attr_accessor :cookie_manager
287
+ # An array of response HTTP message body String which is used for loop-back
288
+ # test. See test/* to see how to use it. If you want to do loop-back test
289
+ # of HTTP header, use test_loopback_http_response instead.
290
+ attr_reader :test_loopback_response
291
+ # An array of request filter which can trap HTTP request/response.
292
+ # See HTTPClient::WWWAuth to see how to use it.
293
+ attr_reader :request_filter
294
+ # HTTPClient::ProxyAuth:: Proxy authentication handler.
295
+ attr_reader :proxy_auth
296
+ # HTTPClient::WWWAuth:: WWW authentication handler.
297
+ attr_reader :www_auth
298
+ # How many times get_content and post_content follows HTTP redirect.
299
+ # 10 by default.
300
+ attr_accessor :follow_redirect_count
301
+ # A Timer object containing response times
302
+ attr_reader :timer
303
+
304
+ # Set HTTP version as a String:: 'HTTP/1.0' or 'HTTP/1.1'
305
+ attr_proxy(:protocol_version, true)
306
+ # Connect timeout in sec.
307
+ attr_proxy(:connect_timeout, true)
308
+ # Request sending timeout in sec.
309
+ attr_proxy(:send_timeout, true)
310
+ # Response receiving timeout in sec.
311
+ attr_proxy(:receive_timeout, true)
312
+ # Negotiation retry count for authentication. 5 by default.
313
+ attr_proxy(:protocol_retry_count, true)
314
+ # if your ruby is older than 2005-09-06, do not set socket_sync = false to
315
+ # avoid an SSL socket blocking bug in openssl/buffering.rb.
316
+ attr_proxy(:socket_sync, true)
317
+ # User-Agent header in HTTP request.
318
+ attr_proxy(:agent_name, true)
319
+ # From header in HTTP request.
320
+ attr_proxy(:from, true)
321
+ # An array of response HTTP String (not a HTTP message body) which is used
322
+ # for loopback test. See test/* to see how to use it.
323
+ attr_proxy(:test_loopback_http_response)
324
+
325
+ # Default extheader for PROPFIND request.
326
+ PROPFIND_DEFAULT_EXTHEADER = { 'Depth' => '0' }
327
+
328
+ # Creates a HTTPClient instance which manages sessions, cookies, etc.
329
+ #
330
+ # HTTPClient.new takes 3 optional arguments for proxy url string,
331
+ # User-Agent String and From header String. User-Agent and From are embedded
332
+ # in HTTP request Header if given. No User-Agent and From header added
333
+ # without setting it explicitly.
334
+ #
335
+ # proxy = 'http://myproxy:8080'
336
+ # agent_name = 'MyAgent/0.1'
337
+ # from = 'from@example.com'
338
+ # HTTPClient.new(proxy, agent_name, from)
339
+ #
340
+ # You can use a keyword argument style Hash. Keys are :proxy, :agent_name
341
+ # and :from.
342
+ #
343
+ # HTTPClient.new(:agent_name = 'MyAgent/0.1')
344
+ def initialize(*args)
345
+ proxy, agent_name, from = keyword_argument(args, :proxy, :agent_name, :from)
346
+ @proxy = nil # assigned later.
347
+ @no_proxy = nil
348
+ @www_auth = WWWAuth.new
349
+ @proxy_auth = ProxyAuth.new
350
+ @request_filter = [@proxy_auth, @www_auth]
351
+ @debug_dev = nil
352
+ @redirect_uri_callback = method(:default_redirect_uri_callback)
353
+ @test_loopback_response = []
354
+ @session_manager = SessionManager.new(self)
355
+ @session_manager.agent_name = agent_name
356
+ @session_manager.from = from
357
+ @session_manager.ssl_config = @ssl_config = SSLConfig.new(self)
358
+ @cookie_manager = WebAgent::CookieManager.new
359
+ @follow_redirect_count = 10
360
+ @timer = Timer.new
361
+ load_environment
362
+ self.proxy = proxy if proxy
363
+ end
364
+
365
+ # Returns debug device if exists. See debug_dev=.
366
+ def debug_dev
367
+ @debug_dev
368
+ end
369
+
370
+ # Sets debug device. Once debug device is set, all HTTP requests and
371
+ # responses are dumped to given device. dev must respond to << for dump.
372
+ #
373
+ # Calling this method resets all existing sessions.
374
+ def debug_dev=(dev)
375
+ @debug_dev = dev
376
+ reset_all
377
+ @session_manager.debug_dev = dev
378
+ end
379
+
380
+ # Returns URI object of HTTP proxy if exists.
381
+ def proxy
382
+ @proxy
383
+ end
384
+
385
+ # Sets HTTP proxy used for HTTP connection. Given proxy can be an URI,
386
+ # a String or nil. You can set user/password for proxy authentication like
387
+ # HTTPClient#proxy = 'http://user:passwd@myproxy:8080'
388
+ #
389
+ # You can use environment variable 'http_proxy' or 'HTTP_PROXY' for it.
390
+ # You need to use 'cgi_http_proxy' or 'CGI_HTTP_PROXY' instead if you run
391
+ # HTTPClient from CGI environment from security reason. (HTTPClient checks
392
+ # 'REQUEST_METHOD' environment variable whether it's CGI or not)
393
+ #
394
+ # Calling this method resets all existing sessions.
395
+ def proxy=(proxy)
396
+ if proxy.nil?
397
+ @proxy = nil
398
+ @proxy_auth.reset_challenge
399
+ else
400
+ @proxy = urify(proxy)
401
+ if @proxy.scheme == nil or @proxy.scheme.downcase != 'http' or
402
+ @proxy.host == nil or @proxy.port == nil
403
+ raise ArgumentError.new("unsupported proxy #{proxy}")
404
+ end
405
+ @proxy_auth.reset_challenge
406
+ if @proxy.user || @proxy.password
407
+ @proxy_auth.set_auth(@proxy.user, @proxy.password)
408
+ end
409
+ end
410
+ reset_all
411
+ @session_manager.proxy = @proxy
412
+ @proxy
413
+ end
414
+
415
+ # Returns NO_PROXY setting String if given.
416
+ def no_proxy
417
+ @no_proxy
418
+ end
419
+
420
+ # Sets NO_PROXY setting String. no_proxy must be a comma separated String.
421
+ # Each entry must be 'host' or 'host:port' such as;
422
+ # HTTPClient#no_proxy = 'example.com,example.co.jp:443'
423
+ #
424
+ # 'localhost' is treated as a no_proxy site regardless of explicitly listed.
425
+ # HTTPClient checks given URI objects before accessing it.
426
+ # 'host' is tail string match. No IP-addr conversion.
427
+ #
428
+ # You can use environment variable 'no_proxy' or 'NO_PROXY' for it.
429
+ #
430
+ # Calling this method resets all existing sessions.
431
+ def no_proxy=(no_proxy)
432
+ @no_proxy = no_proxy
433
+ reset_all
434
+ end
435
+
436
+ # Sets credential for Web server authentication.
437
+ # domain:: a String or an URI to specify where HTTPClient should use this
438
+ # credential. If you set uri to nil, HTTPClient uses this credential
439
+ # wherever a server requires it.
440
+ # user:: username String.
441
+ # passwd:: password String.
442
+ #
443
+ # You can set multiple credentials for each uri.
444
+ #
445
+ # clnt.set_auth('http://www.example.com/foo/', 'foo_user', 'passwd')
446
+ # clnt.set_auth('http://www.example.com/bar/', 'bar_user', 'passwd')
447
+ #
448
+ # Calling this method resets all existing sessions.
449
+ def set_auth(domain, user, passwd)
450
+ uri = urify(domain)
451
+ @www_auth.set_auth(uri, user, passwd)
452
+ reset_all
453
+ end
454
+
455
+ # Deprecated. Use set_auth instead.
456
+ def set_basic_auth(domain, user, passwd)
457
+ uri = urify(domain)
458
+ @www_auth.basic_auth.set(uri, user, passwd)
459
+ reset_all
460
+ end
461
+
462
+ # Sets credential for Proxy authentication.
463
+ # user:: username String.
464
+ # passwd:: password String.
465
+ #
466
+ # Calling this method resets all existing sessions.
467
+ def set_proxy_auth(user, passwd)
468
+ @proxy_auth.set_auth(user, passwd)
469
+ reset_all
470
+ end
471
+
472
+ # Sets the filename where non-volatile Cookies be saved by calling
473
+ # save_cookie_store.
474
+ # This method tries to load and managing Cookies from the specified file.
475
+ #
476
+ # Calling this method resets all existing sessions.
477
+ def set_cookie_store(filename)
478
+ @cookie_manager.cookies_file = filename
479
+ @cookie_manager.load_cookies if filename
480
+ reset_all
481
+ end
482
+
483
+ # Try to save Cookies to the file specified in set_cookie_store. Unexpected
484
+ # error will be raised if you don't call set_cookie_store first.
485
+ # (interface mismatch between WebAgent::CookieManager implementation)
486
+ def save_cookie_store
487
+ @cookie_manager.save_cookies
488
+ end
489
+
490
+ # Sets callback proc when HTTP redirect status is returned for get_content
491
+ # and post_content. default_redirect_uri_callback is used by default.
492
+ #
493
+ # If you need strict implementation which does not allow relative URI
494
+ # redirection, set strict_redirect_uri_callback instead.
495
+ #
496
+ # clnt.redirect_uri_callback = clnt.method(:strict_redirect_uri_callback)
497
+ #
498
+ def redirect_uri_callback=(redirect_uri_callback)
499
+ @redirect_uri_callback = redirect_uri_callback
500
+ end
501
+
502
+ # Retrieves a web resource.
503
+ #
504
+ # uri:: a String or an URI object which represents an URL of web resource.
505
+ # query:: a Hash or an Array of query part of URL.
506
+ # e.g. { "a" => "b" } => 'http://host/part?a=b'.
507
+ # Give an array to pass multiple value like
508
+ # [["a", "b"], ["a", "c"]] => 'http://host/part?a=b&a=c'.
509
+ # extheader:: a Hash or an Array of extra headers. e.g.
510
+ # { 'Accept' => '*/*' } or
511
+ # [['Accept', 'image/jpeg'], ['Accept', 'image/png']].
512
+ # &block:: Give a block to get chunked message-body of response like
513
+ # get_content(uri) { |chunked_body| ... }.
514
+ # Size of each chunk may not be the same.
515
+ #
516
+ # get_content follows HTTP redirect status (see HTTP::Status.redirect?)
517
+ # internally and try to retrieve content from redirected URL. See
518
+ # redirect_uri_callback= how HTTP redirection is handled.
519
+ #
520
+ # If you need to get full HTTP response including HTTP status and headers,
521
+ # use get method. get returns HTTP::Message as a response and you need to
522
+ # follow HTTP redirect by yourself if you need.
523
+ def get_content(uri, query = nil, extheader = {}, &block)
524
+ follow_redirect(:get, uri, query, nil, extheader, &block).content
525
+ end
526
+
527
+ # Posts a content.
528
+ #
529
+ # uri:: a String or an URI object which represents an URL of web resource.
530
+ # body:: a Hash or an Array of body part.
531
+ # e.g. { "a" => "b" } => 'a=b'.
532
+ # Give an array to pass multiple value like
533
+ # [["a", "b"], ["a", "c"]] => 'a=b&a=c'.
534
+ # When you pass a File as a value, it will be posted as a
535
+ # multipart/form-data. e.g. { 'upload' => file }
536
+ # extheader:: a Hash or an Array of extra headers. e.g.
537
+ # { 'Accept' => '*/*' } or
538
+ # [['Accept', 'image/jpeg'], ['Accept', 'image/png']].
539
+ # &block:: Give a block to get chunked message-body of response like
540
+ # post_content(uri) { |chunked_body| ... }.
541
+ # Size of each chunk may not be the same.
542
+ #
543
+ # post_content follows HTTP redirect status (see HTTP::Status.redirect?)
544
+ # internally and try to post the content to redirected URL. See
545
+ # redirect_uri_callback= how HTTP redirection is handled.
546
+ #
547
+ # If you need to get full HTTP response including HTTP status and headers,
548
+ # use post method.
549
+ def post_content(uri, body = nil, extheader = {}, &block)
550
+ follow_redirect(:post, uri, nil, body, extheader, &block).content
551
+ end
552
+
553
+ # A method for redirect uri callback. How to use:
554
+ # clnt.redirect_uri_callback = clnt.method(:strict_redirect_uri_callback)
555
+ # This callback does not allow relative redirect such as
556
+ # Location: ../foo/
557
+ # in HTTP header. (raises BadResponseError instead)
558
+ def strict_redirect_uri_callback(uri, res)
559
+ newuri = URI.parse(res.header['location'][0])
560
+ if https?(uri) && !https?(newuri)
561
+ raise BadResponseError.new("redirecting to non-https resource")
562
+ end
563
+ unless newuri.is_a?(URI::HTTP)
564
+ raise BadResponseError.new("unexpected location: #{newuri}", res)
565
+ end
566
+ puts "redirect to: #{newuri}" if $DEBUG
567
+ newuri
568
+ end
569
+
570
+ # A default method for redirect uri callback. This method is used by
571
+ # HTTPClient instance by default.
572
+ # This callback allows relative redirect such as
573
+ # Location: ../foo/
574
+ # in HTTP header.
575
+ def default_redirect_uri_callback(uri, res)
576
+ newuri = URI.parse(res.header['location'][0])
577
+ if https?(uri) && !https?(newuri)
578
+ raise BadResponseError.new("redirecting to non-https resource")
579
+ end
580
+ unless newuri.is_a?(URI::HTTP)
581
+ newuri = uri + newuri
582
+ STDERR.puts("could be a relative URI in location header which is not recommended")
583
+ STDERR.puts("'The field value consists of a single absolute URI' in HTTP spec")
584
+ end
585
+ puts "redirect to: #{newuri}" if $DEBUG
586
+ newuri
587
+ end
588
+
589
+ # Sends HEAD request to the specified URL. See request for arguments.
590
+ def head(uri, query = nil, extheader = {})
591
+ request(:head, uri, query, nil, extheader)
592
+ end
593
+
594
+ # Sends GET request to the specified URL. See request for arguments.
595
+ def get(uri, query = nil, extheader = {}, &block)
596
+ request(:get, uri, query, nil, extheader, &block)
597
+ end
598
+
599
+ # Sends POST request to the specified URL. See request for arguments.
600
+ def post(uri, body = '', extheader = {}, &block)
601
+ request(:post, uri, nil, body, extheader, &block)
602
+ end
603
+
604
+ # Sends PUT request to the specified URL. See request for arguments.
605
+ def put(uri, body = '', extheader = {}, &block)
606
+ request(:put, uri, nil, body, extheader, &block)
607
+ end
608
+
609
+ # Sends DELETE request to the specified URL. See request for arguments.
610
+ def delete(uri, extheader = {}, &block)
611
+ request(:delete, uri, nil, nil, extheader, &block)
612
+ end
613
+
614
+ # Sends OPTIONS request to the specified URL. See request for arguments.
615
+ def options(uri, extheader = {}, &block)
616
+ request(:options, uri, nil, nil, extheader, &block)
617
+ end
618
+
619
+ # Sends PROPFIND request to the specified URL. See request for arguments.
620
+ def propfind(uri, extheader = PROPFIND_DEFAULT_EXTHEADER, &block)
621
+ request(:propfind, uri, nil, nil, extheader, &block)
622
+ end
623
+
624
+ # Sends PROPPATCH request to the specified URL. See request for arguments.
625
+ def proppatch(uri, body = nil, extheader = {}, &block)
626
+ request(:proppatch, uri, nil, body, extheader, &block)
627
+ end
628
+
629
+ # Sends TRACE request to the specified URL. See request for arguments.
630
+ def trace(uri, query = nil, body = nil, extheader = {}, &block)
631
+ request('TRACE', uri, query, body, extheader, &block)
632
+ end
633
+
634
+ # Sends a request to the specified URL.
635
+ #
636
+ # method:: HTTP method to be sent. method.to_s.upcase is used.
637
+ # uri:: a String or an URI object which represents an URL of web resource.
638
+ # query:: a Hash or an Array of query part of URL.
639
+ # e.g. { "a" => "b" } => 'http://host/part?a=b'
640
+ # Give an array to pass multiple value like
641
+ # [["a", "b"], ["a", "c"]] => 'http://host/part?a=b&a=c'
642
+ # body:: a Hash or an Array of body part.
643
+ # e.g. { "a" => "b" } => 'a=b'.
644
+ # Give an array to pass multiple value like
645
+ # [["a", "b"], ["a", "c"]] => 'a=b&a=c'.
646
+ # When the given method is 'POST' and the given body contains a file
647
+ # as a value, it will be posted as a multipart/form-data.
648
+ # e.g. { 'upload' => file }
649
+ # See HTTP::Message.file? for actual condition of 'a file'.
650
+ # extheader:: a Hash or an Array of extra headers. e.g.
651
+ # { 'Accept' => '*/*' } or
652
+ # [['Accept', 'image/jpeg'], ['Accept', 'image/png']].
653
+ # &block:: Give a block to get chunked message-body of response like
654
+ # get(uri) { |chunked_body| ... }.
655
+ # Size of each chunk may not be the same.
656
+ #
657
+ # You can also pass a String as a body. HTTPClient just sends a String as
658
+ # a HTTP request message body.
659
+ #
660
+ # When you pass an IO as a body, HTTPClient sends it as a HTTP request with
661
+ # chunked encoding (Transfer-Encoding: chunked in HTTP header). Bear in mind
662
+ # that some server application does not support chunked request. At least
663
+ # cgi.rb does not support it.
664
+ def request(method, uri, query = nil, body = nil, extheader = {}, &block)
665
+ uri = urify(uri)
666
+ if block
667
+ filtered_block = proc { |res, str|
668
+ block.call(str)
669
+ }
670
+ end
671
+ do_request(method, uri, query, body, extheader, &filtered_block)
672
+ end
673
+
674
+ # Sends HEAD request in async style. See request_async for arguments.
675
+ # It immediately returns a HTTPClient::Connection instance as a result.
676
+ def head_async(uri, query = nil, extheader = {})
677
+ request_async(:head, uri, query, nil, extheader)
678
+ end
679
+
680
+ # Sends GET request in async style. See request_async for arguments.
681
+ # It immediately returns a HTTPClient::Connection instance as a result.
682
+ def get_async(uri, query = nil, extheader = {})
683
+ request_async(:get, uri, query, nil, extheader)
684
+ end
685
+
686
+ # Sends POST request in async style. See request_async for arguments.
687
+ # It immediately returns a HTTPClient::Connection instance as a result.
688
+ def post_async(uri, body = nil, extheader = {})
689
+ request_async(:post, uri, nil, body, extheader)
690
+ end
691
+
692
+ # Sends PUT request in async style. See request_async for arguments.
693
+ # It immediately returns a HTTPClient::Connection instance as a result.
694
+ def put_async(uri, body = nil, extheader = {})
695
+ request_async(:put, uri, nil, body, extheader)
696
+ end
697
+
698
+ # Sends DELETE request in async style. See request_async for arguments.
699
+ # It immediately returns a HTTPClient::Connection instance as a result.
700
+ def delete_async(uri, extheader = {})
701
+ request_async(:delete, uri, nil, nil, extheader)
702
+ end
703
+
704
+ # Sends OPTIONS request in async style. See request_async for arguments.
705
+ # It immediately returns a HTTPClient::Connection instance as a result.
706
+ def options_async(uri, extheader = {})
707
+ request_async(:options, uri, nil, nil, extheader)
708
+ end
709
+
710
+ # Sends PROPFIND request in async style. See request_async for arguments.
711
+ # It immediately returns a HTTPClient::Connection instance as a result.
712
+ def propfind_async(uri, extheader = PROPFIND_DEFAULT_EXTHEADER)
713
+ request_async(:propfind, uri, nil, nil, extheader)
714
+ end
715
+
716
+ # Sends PROPPATCH request in async style. See request_async for arguments.
717
+ # It immediately returns a HTTPClient::Connection instance as a result.
718
+ def proppatch_async(uri, body = nil, extheader = {})
719
+ request_async(:proppatch, uri, nil, body, extheader)
720
+ end
721
+
722
+ # Sends TRACE request in async style. See request_async for arguments.
723
+ # It immediately returns a HTTPClient::Connection instance as a result.
724
+ def trace_async(uri, query = nil, body = nil, extheader = {})
725
+ request_async(:trace, uri, query, body, extheader)
726
+ end
727
+
728
+ # Sends a request in async style. request method creates new Thread for
729
+ # HTTP connection and returns a HTTPClient::Connection instance immediately.
730
+ #
731
+ # Arguments definition is the same as request.
732
+ def request_async(method, uri, query = nil, body = nil, extheader = {})
733
+ uri = urify(uri)
734
+ do_request_async(method, uri, query, body, extheader)
735
+ end
736
+
737
+ # Resets internal session for the given URL. Keep-alive connection for the
738
+ # site (host-port pair) is disconnected if exists.
739
+ def reset(uri)
740
+ uri = urify(uri)
741
+ @session_manager.reset(uri)
742
+ end
743
+
744
+ # Resets all of internal sessions. Keep-alive connections are disconnected.
745
+ def reset_all
746
+ @session_manager.reset_all
747
+ end
748
+
749
+ private
750
+
751
+ class RetryableResponse < StandardError # :nodoc:
752
+ end
753
+
754
+ class KeepAliveDisconnected < StandardError # :nodoc:
755
+ end
756
+
757
+ def do_request(method, uri, query, body, extheader, &block)
758
+ conn = Connection.new
759
+ res = nil
760
+ if HTTP::Message.file?(body)
761
+ pos = body.pos rescue nil
762
+ end
763
+ retry_count = @session_manager.protocol_retry_count
764
+ proxy = no_proxy?(uri) ? nil : @proxy
765
+ while retry_count > 0
766
+ body.pos = pos if pos
767
+ req = create_request(method, uri, query, body, extheader)
768
+ begin
769
+ protect_keep_alive_disconnected do
770
+ do_get_block(req, proxy, conn, &block)
771
+ end
772
+ res = conn.pop
773
+ break
774
+ rescue RetryableResponse
775
+ res = conn.pop
776
+ retry_count -= 1
777
+ end
778
+ end
779
+ res
780
+ end
781
+
782
+ def do_request_async(method, uri, query, body, extheader)
783
+ conn = Connection.new
784
+ t = Thread.new(conn) { |tconn|
785
+ if HTTP::Message.file?(body)
786
+ pos = body.pos rescue nil
787
+ end
788
+ retry_count = @session_manager.protocol_retry_count
789
+ proxy = no_proxy?(uri) ? nil : @proxy
790
+ while retry_count > 0
791
+ body.pos = pos if pos
792
+ req = create_request(method, uri, query, body, extheader)
793
+ begin
794
+ protect_keep_alive_disconnected do
795
+ do_get_stream(req, proxy, tconn)
796
+ end
797
+ break
798
+ rescue RetryableResponse
799
+ retry_count -= 1
800
+ end
801
+ end
802
+ }
803
+ conn.async_thread = t
804
+ conn
805
+ end
806
+
807
+ def load_environment
808
+ # http_proxy
809
+ if getenv('REQUEST_METHOD')
810
+ # HTTP_PROXY conflicts with the environment variable usage in CGI where
811
+ # HTTP_* is used for HTTP header information. Unlike open-uri, we
812
+ # simply ignore http_proxy in CGI env and use cgi_http_proxy instead.
813
+ self.proxy = getenv('cgi_http_proxy')
814
+ else
815
+ self.proxy = getenv('http_proxy')
816
+ end
817
+ # no_proxy
818
+ self.no_proxy = getenv('no_proxy')
819
+ end
820
+
821
+ def getenv(name)
822
+ ENV[name.downcase] || ENV[name.upcase]
823
+ end
824
+
825
+ def follow_redirect(method, uri, query, body, extheader, &block)
826
+ uri = urify(uri)
827
+ if block
828
+ filtered_block = proc { |r, str|
829
+ block.call(str) if HTTP::Status.successful?(r.status)
830
+ }
831
+ end
832
+ if HTTP::Message.file?(body)
833
+ pos = body.pos rescue nil
834
+ end
835
+ retry_number = 0
836
+ while retry_number < @follow_redirect_count
837
+ body.pos = pos if pos
838
+ res = do_request(method, uri, query, body, extheader, &filtered_block)
839
+ if HTTP::Status.successful?(res.status)
840
+ return res
841
+ elsif HTTP::Status.redirect?(res.status)
842
+ uri = urify(@redirect_uri_callback.call(uri, res))
843
+ retry_number += 1
844
+ else
845
+ raise BadResponseError.new("unexpected response: #{res.header.inspect}", res)
846
+ end
847
+ end
848
+ raise BadResponseError.new("retry count exceeded", res)
849
+ end
850
+
851
+ def protect_keep_alive_disconnected
852
+ begin
853
+ yield
854
+ rescue KeepAliveDisconnected
855
+ yield
856
+ end
857
+ end
858
+
859
+ def create_request(method, uri, query, body, extheader)
860
+ method = method.to_s.upcase
861
+ if extheader.is_a?(Hash)
862
+ extheader = extheader.to_a
863
+ else
864
+ extheader = extheader.dup
865
+ end
866
+ boundary = nil
867
+ if body
868
+ dummy, content_type = extheader.find { |key, value|
869
+ key.downcase == 'content-type'
870
+ }
871
+ if content_type
872
+ if /\Amultipart/ =~ content_type
873
+ if content_type =~ /boundary=(.+)\z/
874
+ boundary = $1
875
+ else
876
+ boundary = create_boundary
877
+ content_type = "#{content_type}; boundary=#{boundary}"
878
+ extheader = override_header(extheader, 'Content-Type', content_type)
879
+ end
880
+ end
881
+ elsif method == 'POST'
882
+ if file_in_form_data?(body)
883
+ boundary = create_boundary
884
+ content_type = "multipart/form-data; boundary=#{boundary}"
885
+ else
886
+ content_type = 'application/x-www-form-urlencoded'
887
+ end
888
+ extheader << ['Content-Type', content_type]
889
+ end
890
+ end
891
+ req = HTTP::Message.new_request(method, uri, query, body, boundary)
892
+ extheader.each do |key, value|
893
+ req.header.add(key, value)
894
+ end
895
+ if @cookie_manager && cookie = @cookie_manager.find(uri)
896
+ req.header.add('Cookie', cookie)
897
+ end
898
+ req
899
+ end
900
+
901
+ def create_boundary
902
+ Digest::SHA1.hexdigest(Time.now.to_s)
903
+ end
904
+
905
+ def file_in_form_data?(body)
906
+ HTTP::Message.multiparam_query?(body) &&
907
+ body.any? { |k, v| HTTP::Message.file?(v) }
908
+ end
909
+
910
+ def override_header(extheader, key, value)
911
+ result = []
912
+ extheader.each do |k, v|
913
+ if k.downcase == key.downcase
914
+ result << [key, value]
915
+ else
916
+ result << [k, v]
917
+ end
918
+ end
919
+ result
920
+ end
921
+
922
+ NO_PROXY_HOSTS = ['localhost']
923
+
924
+ def no_proxy?(uri)
925
+ if !@proxy or NO_PROXY_HOSTS.include?(uri.host)
926
+ return true
927
+ end
928
+ unless @no_proxy
929
+ return false
930
+ end
931
+ @no_proxy.scan(/([^:,]+)(?::(\d+))?/) do |host, port|
932
+ if /(\A|\.)#{Regexp.quote(host)}\z/i =~ uri.host &&
933
+ (!port || uri.port == port.to_i)
934
+ return true
935
+ end
936
+ end
937
+ false
938
+ end
939
+
940
+ def https?(uri)
941
+ uri.scheme.downcase == 'https'
942
+ end
943
+
944
+ # !! CAUTION !!
945
+ # Method 'do_get*' runs under MT conditon. Be careful to change.
946
+ def do_get_block(req, proxy, conn, &block)
947
+ @request_filter.each do |filter|
948
+ filter.filter_request(req)
949
+ end
950
+ if str = @test_loopback_response.shift
951
+ dump_dummy_request_response(req.body.dump, str) if @debug_dev
952
+ conn.push(HTTP::Message.new_response(str))
953
+ return
954
+ end
955
+ content = block ? nil : ''
956
+ res = HTTP::Message.new_response(content)
957
+ @debug_dev << "= Request\n\n" if @debug_dev
958
+ sess = @session_manager.query(req, proxy)
959
+ res.peer_cert = sess.ssl_peer_cert
960
+ @debug_dev << "\n\n= Response\n\n" if @debug_dev
961
+ do_get_header(req, res, sess)
962
+ conn.push(res)
963
+ sess.get_body do |part|
964
+ if block
965
+ block.call(res, part)
966
+ else
967
+ content << part
968
+ end
969
+ end
970
+ @session_manager.keep(sess) unless sess.closed?
971
+ commands = @request_filter.collect { |filter|
972
+ filter.filter_response(req, res)
973
+ }
974
+ if commands.find { |command| command == :retry }
975
+ raise RetryableResponse.new
976
+ end
977
+ end
978
+
979
+ def do_get_stream(req, proxy, conn)
980
+ @request_filter.each do |filter|
981
+ filter.filter_request(req)
982
+ end
983
+ if str = @test_loopback_response.shift
984
+ dump_dummy_request_response(req.body.dump, str) if @debug_dev
985
+ conn.push(HTTP::Message.new_response(StringIO.new(str)))
986
+ return
987
+ end
988
+ piper, pipew = IO.pipe
989
+ res = HTTP::Message.new_response(piper)
990
+ @debug_dev << "= Request\n\n" if @debug_dev
991
+ sess = @session_manager.query(req, proxy)
992
+ res.peer_cert = sess.ssl_peer_cert
993
+ @debug_dev << "\n\n= Response\n\n" if @debug_dev
994
+ do_get_header(req, res, sess)
995
+ conn.push(res)
996
+ sess.get_body do |part|
997
+ pipew.syswrite(part)
998
+ end
999
+ pipew.close
1000
+ @session_manager.keep(sess) unless sess.closed?
1001
+ commands = @request_filter.collect { |filter|
1002
+ filter.filter_response(req, res)
1003
+ }
1004
+ # ignore commands (not retryable in async mode)
1005
+ end
1006
+
1007
+ def do_get_header(req, res, sess)
1008
+ res.version, res.status, res.reason, headers = sess.get_header
1009
+ headers.each do |key, value|
1010
+ res.header.add(key, value)
1011
+ end
1012
+ if @cookie_manager
1013
+ res.header['set-cookie'].each do |cookie|
1014
+ @cookie_manager.parse(cookie, req.header.request_uri)
1015
+ end
1016
+ end
1017
+ end
1018
+
1019
+ def dump_dummy_request_response(req, res)
1020
+ @debug_dev << "= Dummy Request\n\n"
1021
+ @debug_dev << req
1022
+ @debug_dev << "\n\n= Dummy Response\n\n"
1023
+ @debug_dev << res
1024
+ end
1025
+ end