solutious-stella 0.7.0.004 → 0.7.0.005

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