right_http_connection 1.2.3 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8ba2dac9f5194cfcb8a8e6dec24f973418d07f22
4
+ data.tar.gz: 59e6d3f82e45ca7679849320fc7fd89f6e776d20
5
+ SHA512:
6
+ metadata.gz: fd754b030589555f947f2a52bb64c4d3524c902f3e0d95c00fbecf5a340c06d7d6353360763b225b3e05464357610e2dae54d7e21df17b899a65c3bef5430c02
7
+ data.tar.gz: 2ee74c4fc7cf86908f8a06b96be6cd892aeb25e16a345ea959e0fbe7907712a126c73704c8a3da013dec9443cecd8cc6e1caace3ebd4737c7e0e4da4e5287488
@@ -49,4 +49,38 @@ Initial public release
49
49
 
50
50
  - Added support for setting retry & timeout parameters in the constructor
51
51
  - Improve handling of data streams during upload: if there is a failure and a retry, reset
52
- the seek pointer for the subsequent re-request
52
+ the seek pointer for the subsequent re-request
53
+
54
+ == 1.2.4
55
+
56
+ * r4984, konstantin, 2008-08-11 14:49:18 +0400
57
+ * fixed a bug: <NoMethodError: You have a nil object when you didn't expect it!
58
+ The error occurred while evaluating nil.body_stream>
59
+
60
+ == 1.2.5
61
+
62
+ - ActiveSupport dependency removal
63
+
64
+
65
+ == 1.3.0
66
+ - Added:
67
+ - support for using through proxies
68
+ - functional tests
69
+
70
+ == 1.3.1
71
+ - Added:
72
+ - SSL certificate handshake support
73
+ - more specs
74
+ - ability to give client side key and certificate inline
75
+ - Fixed: some minor glitches
76
+
77
+ == 1.4.0
78
+ - Added
79
+ - license
80
+ - HTTP_PROXY env variable support
81
+ - Fixed
82
+ - context-length issue (thx kristianm)
83
+ - exception handling
84
+ - connection is now closed on any exception
85
+ - connection is reestablished when credentials change
86
+ - some other minor bugs
@@ -4,4 +4,5 @@ README.txt
4
4
  Rakefile
5
5
  lib/net_fix.rb
6
6
  lib/right_http_connection.rb
7
+ lib/support.rb
7
8
  setup.rb
data/Rakefile CHANGED
@@ -1,59 +1,34 @@
1
1
  require 'rubygems'
2
+ require 'bundler'
3
+ Bundler::GemHelper.install_tasks
2
4
  require 'rake'
3
5
  require 'rake/clean'
4
6
  require 'rake/testtask'
5
7
  require 'rake/packagetask'
6
- require 'rake/gempackagetask'
7
- require 'rake/rdoctask'
8
- require 'rake/contrib/rubyforgepublisher'
8
+ #require 'rake/gempackagetask'
9
+ #require 'rake/rdoctask'
10
+ #require 'rake/contrib/rubyforgepublisher'
11
+ require 'rspec/core/rake_task'
12
+ require 'cucumber/rake/task'
9
13
  require 'fileutils'
10
- require 'hoe'
11
14
  include FileUtils
12
15
  require File.join(File.dirname(__FILE__), 'lib', 'right_http_connection')
13
16
 
14
- AUTHOR = 'RightScale' # can also be an array of Authors
15
- EMAIL = "support@rightscale.com"
16
- DESCRIPTION = "RightScale's robust HTTP/S connection module"
17
- GEM_NAME = 'right_http_connection' # what ppl will type to install your gem
18
- RUBYFORGE_PROJECT = 'rightaws' # The unix name for your project
19
- HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
20
- DOWNLOAD_PATH = "http://rubyforge.org/projects/#{RUBYFORGE_PROJECT}"
21
-
22
- NAME = "right_http_connection"
23
- REV = nil # UNCOMMENT IF REQUIRED: File.read(".svn/entries")[/committed-rev="(d+)"/, 1] rescue nil
24
- VERS = RightHttpConnection::VERSION::STRING + (REV ? ".#{REV}" : "")
25
- CLEAN.include ['**/.*.sw?', '*.gem', '.config', '**/.DS_Store']
26
- RDOC_OPTS = ['--quiet', '--title', 'right_http_connection documentation',
27
- "--opname", "index.html",
28
- "--line-numbers",
29
- "--main", "README",
30
- "--inline-source"]
31
-
32
- class Hoe
33
- def extra_deps
34
- @extra_deps.reject { |x| Array(x).first == 'hoe' }
35
- end
36
- end
17
+ Bundler::GemHelper.install_tasks
18
+
19
+ =begin
37
20
 
38
- # Generate all the Rake tasks
39
- # Run 'rake -T' to see list of generated tasks (from gem root directory)
40
- hoe = Hoe.new(GEM_NAME, VERS) do |p|
41
- p.author = AUTHOR
42
- p.description = DESCRIPTION
43
- p.email = EMAIL
44
- p.summary = DESCRIPTION
45
- p.url = HOMEPATH
46
- p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
47
- p.test_globs = ["test/**/test_*.rb"]
48
- p.clean_globs = CLEAN #An array of file patterns to delete on clean.
49
- p.remote_rdoc_dir = "right_http_gem_doc"
50
-
51
- # == Optional
52
- p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
53
- #p.extra_deps = [] # An array of rubygem dependencies [name, version], e.g. [ ['active_support', '>= 1.3.1'] ]
54
- #p.spec_extras = {} # A hash of extra values to set in the gemspec.
21
+ # == Gem == #
22
+
23
+ gemtask = Rake::GemPackageTask.new(Gem::Specification.load("right_http_connection.gemspec")) do |package|
24
+ package.package_dir = ENV['PACKAGE_DIR'] || 'pkg'
25
+ package.need_zip = true
26
+ package.need_tar = true
55
27
  end
56
28
 
29
+ directory gemtask.package_dir
30
+
31
+ CLEAN.include(gemtask.package_dir)
57
32
 
58
33
  desc 'Generate website files'
59
34
  task :website_generate do
@@ -88,3 +63,30 @@ task :check_version do
88
63
  exit
89
64
  end
90
65
  end
66
+ =end
67
+
68
+ task :default => 'spec'
69
+
70
+ # == Unit Tests == #
71
+
72
+ desc "Run unit tests"
73
+ RSpec::Core::RakeTask.new
74
+
75
+ namespace :spec do
76
+ desc "Run unit tests with RCov"
77
+ RSpec::Core::RakeTask.new(:rcov) do |t|
78
+ t.rcov = true
79
+ t.rcov_opts = %q[--exclude "spec"]
80
+ end
81
+
82
+ desc "Print Specdoc for unit tests"
83
+ RSpec::Core::RakeTask.new(:doc) do |t|
84
+ t.rspec_opts = ["--format", "documentation"]
85
+ end
86
+ end
87
+
88
+ # == Functional tests == #
89
+ desc "Run functional tests"
90
+ Cucumber::Rake::Task.new do |t|
91
+ t.cucumber_opts = %w{--color --format pretty}
92
+ end
@@ -25,6 +25,7 @@
25
25
  # Net::HTTP and Net::HTTPGenericRequest fixes to support 100-continue on
26
26
  # POST and PUT. The request must have 'expect' field set to '100-continue'.
27
27
 
28
+ require 'timeout'
28
29
 
29
30
  module Net
30
31
 
@@ -48,7 +49,7 @@ module Net
48
49
  end
49
50
 
50
51
  def rbuf_fill
51
- timeout(@read_timeout) {
52
+ Timeout.timeout(@read_timeout) {
52
53
  @rbuf << @io.sysread(@@socket_read_size)
53
54
  }
54
55
  end
@@ -91,18 +92,30 @@ module Net
91
92
  private
92
93
 
93
94
  def send_request_with_body(sock, ver, path, body, send_only=nil)
94
- self.content_length = body.length
95
+ self.content_length = body.respond_to?(:bytesize) ? body.bytesize : body.length
95
96
  delete 'Transfer-Encoding'
96
97
  supply_default_content_type
97
98
  write_header(sock, ver, path) unless send_only == :body
98
- sock.write(body) unless send_only == :header
99
+ sock.write(body && body.to_s) unless send_only == :header
99
100
  end
100
101
 
101
102
  def send_request_with_body_stream(sock, ver, path, f, send_only=nil)
103
+ # KD: Fix 'content-length': it must not be greater than a piece of file left to be read.
104
+ # Otherwise the connection may behave like crazy causing 4xx or 5xx responses
105
+ #
106
+ # Only do this helpful thing if the stream responds to :pos (it may be something
107
+ # that responds to :read and :size but not :pos).
108
+ if f.respond_to?(:pos)
109
+ file_size = f.respond_to?(:lstat) ? f.lstat.size : f.size
110
+ bytes_to_read = [ file_size - f.pos, self.content_length.to_i ].sort.first
111
+ self.content_length = bytes_to_read
112
+ end
113
+
102
114
  unless content_length() or chunked?
103
115
  raise ArgumentError,
104
116
  "Content-Length not given and Transfer-Encoding is not `chunked'"
105
117
  end
118
+ bytes_to_read ||= content_length()
106
119
  supply_default_content_type
107
120
  write_header(sock, ver, path) unless send_only == :body
108
121
  unless send_only == :header
@@ -112,8 +125,14 @@ module Net
112
125
  end
113
126
  sock.write "0\r\n\r\n"
114
127
  else
115
- while s = f.read(@@local_read_size)
128
+ # KD: When we read/write over file EOF it sometimes make the connection unstable
129
+ read_size = [ @@local_read_size, bytes_to_read ].sort.first
130
+ while s = f.read(read_size)
116
131
  sock.write s
132
+ # Make sure we do not read over EOF or more than expected content-length
133
+ bytes_to_read -= read_size
134
+ break if bytes_to_read <= 0
135
+ read_size = bytes_to_read if bytes_to_read < read_size
117
136
  end
118
137
  end
119
138
  end
@@ -144,6 +163,7 @@ module Net
144
163
  req.exec @socket, @curr_http_version, edit_path(req.path), send_only
145
164
  begin
146
165
  res = HTTPResponse.read_new(@socket)
166
+ res.decode_content = req.decode_content if RUBY_VERSION > '2.0'
147
167
  # if we expected 100-continue then send a body
148
168
  if res.is_a?(HTTPContinue) && send_only && req['content-length'].to_i > 0
149
169
  req.exec @socket, @curr_http_version, edit_path(req.path), :body
@@ -0,0 +1,109 @@
1
+ # These are ActiveSupport-;like extensions to do a few handy things in the gems
2
+ # Derived from ActiveSupport, so the AS copyright notice applies:
3
+ #
4
+ #
5
+ #
6
+ # Copyright (c) 2005 David Heinemeier Hansson
7
+ #
8
+ # Permission is hereby granted, free of charge, to any person obtaining
9
+ # a copy of this software and associated documentation files (the
10
+ # "Software"), to deal in the Software without restriction, including
11
+ # without limitation the rights to use, copy, modify, merge, publish,
12
+ # distribute, sublicense, and/or sell copies of the Software, and to
13
+ # permit persons to whom the Software is furnished to do so, subject to
14
+ # the following conditions:
15
+ #
16
+ # The above copyright notice and this permission notice shall be
17
+ # included in all copies or substantial portions of the Software.
18
+ #
19
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
21
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
23
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
24
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
25
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26
+ #++
27
+ #
28
+ #
29
+ class String #:nodoc:
30
+
31
+ # Constantize tries to find a declared constant with the name specified
32
+ # in the string. It raises a NameError when the name is not in CamelCase
33
+ # or is not initialized.
34
+ #
35
+ # Examples
36
+ # "Module".constantize #=> Module
37
+ # "Class".constantize #=> Class
38
+ def right_constantize()
39
+ unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ self
40
+ raise NameError, "#{self.inspect} is not a valid constant name!"
41
+ end
42
+ Object.module_eval("::#{$1}", __FILE__, __LINE__)
43
+ end
44
+
45
+ def right_camelize()
46
+ self.dup.split(/_/).map{ |word| word.capitalize }.join('')
47
+ end
48
+
49
+ end
50
+
51
+
52
+ class Object #:nodoc:
53
+ # "", " ", nil, [], and {} are blank
54
+ def right_blank?
55
+ if respond_to?(:empty?) && respond_to?(:strip)
56
+ empty? or strip.empty?
57
+ elsif respond_to?(:empty?)
58
+ empty?
59
+ else
60
+ !self
61
+ end
62
+ end
63
+ end
64
+
65
+ class NilClass #:nodoc:
66
+ def right_blank?
67
+ true
68
+ end
69
+ end
70
+
71
+ class FalseClass #:nodoc:
72
+ def right_blank?
73
+ true
74
+ end
75
+ end
76
+
77
+ class TrueClass #:nodoc:
78
+ def right_blank?
79
+ false
80
+ end
81
+ end
82
+
83
+ class Array #:nodoc:
84
+ alias_method :right_blank?, :empty?
85
+ end
86
+
87
+ class Hash #:nodoc:
88
+ alias_method :right_blank?, :empty?
89
+
90
+ # Return a new hash with all keys converted to symbols.
91
+ def right_symbolize_keys
92
+ inject({}) do |options, (key, value)|
93
+ options[key.to_sym] = value
94
+ options
95
+ end
96
+ end
97
+ end
98
+
99
+ class String #:nodoc:
100
+ def right_blank?
101
+ empty? || strip.empty?
102
+ end
103
+ end
104
+
105
+ class Numeric #:nodoc:
106
+ def right_blank?
107
+ false
108
+ end
109
+ end
@@ -0,0 +1,32 @@
1
+ #-- -*- mode: ruby; encoding: utf-8 -*-
2
+ # Copyright: Copyright (c) 2010 RightScale, Inc.
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # 'Software'), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ module RightHttpConnection #:nodoc:
25
+ module VERSION #:nodoc:
26
+ MAJOR = 1 unless defined?(MAJOR)
27
+ MINOR = 5 unless defined?(MINOR)
28
+ TINY = 1 unless defined?(TINY)
29
+
30
+ STRING = [MAJOR, MINOR, TINY].join('.') unless defined?(STRING)
31
+ end
32
+ end
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright (c) 2007-2008 RightScale Inc
2
+ # Copyright (c) 2007-2011 RightScale Inc
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining
5
5
  # a copy of this software and associated documentation files (the
@@ -27,22 +27,12 @@ require "time"
27
27
  require "logger"
28
28
 
29
29
  $:.unshift(File.dirname(__FILE__))
30
- require "net_fix"
31
-
32
-
33
- module RightHttpConnection #:nodoc:
34
- module VERSION #:nodoc:
35
- MAJOR = 1
36
- MINOR = 2
37
- TINY = 3
38
-
39
- STRING = [MAJOR, MINOR, TINY].join('.')
40
- end
41
- end
42
-
30
+ require 'base/version'
31
+ require 'base/support'
32
+ require 'base/net_fix'
43
33
 
44
34
  module Rightscale
45
-
35
+
46
36
  =begin rdoc
47
37
  HttpConnection maintains a persistent HTTP connection to a remote
48
38
  server. Each instance maintains its own unique connection to the
@@ -75,16 +65,16 @@ the full number of potential reconnects and retries available to
75
65
  them.
76
66
  =end
77
67
 
78
- class HttpConnection
68
+ class HttpConnection
79
69
 
80
70
  # Number of times to retry the request after encountering the first error
81
- HTTP_CONNECTION_RETRY_COUNT = 3
71
+ HTTP_CONNECTION_RETRY_COUNT = 3 unless defined?(HTTP_CONNECTION_RETRY_COUNT)
82
72
  # Throw a Timeout::Error if a connection isn't established within this number of seconds
83
- HTTP_CONNECTION_OPEN_TIMEOUT = 5
73
+ HTTP_CONNECTION_OPEN_TIMEOUT = 5 unless defined?(HTTP_CONNECTION_OPEN_TIMEOUT)
84
74
  # Throw a Timeout::Error if no data have been read on this connnection within this number of seconds
85
- HTTP_CONNECTION_READ_TIMEOUT = 120
86
- # Length of the post-error probationary period during which all requests will fail
87
- HTTP_CONNECTION_RETRY_DELAY = 15
75
+ HTTP_CONNECTION_READ_TIMEOUT = 120 unless defined?(HTTP_CONNECTION_READ_TIMEOUT)
76
+ # Length of the post-error probationary period during which all requests will fail
77
+ HTTP_CONNECTION_RETRY_DELAY = 15 unless defined?(HTTP_CONNECTION_RETRY_DELAY)
88
78
 
89
79
  #--------------------
90
80
  # class methods
@@ -95,22 +85,28 @@ them.
95
85
  @@params[:http_connection_open_timeout] = HTTP_CONNECTION_OPEN_TIMEOUT
96
86
  @@params[:http_connection_read_timeout] = HTTP_CONNECTION_READ_TIMEOUT
97
87
  @@params[:http_connection_retry_delay] = HTTP_CONNECTION_RETRY_DELAY
98
-
88
+
99
89
  # Query the global (class-level) parameters:
100
- #
101
- # :user_agent => 'www.HostName.com' # String to report as HTTP User agent
90
+ #
91
+ # :user_agent => 'www.HostName.com' # String to report as HTTP User agent
102
92
  # :ca_file => 'path_to_file' # Path to a CA certification file in PEM format. The file can contain several CA certificates. If this parameter isn't set, HTTPS certs won't be verified.
93
+ # :fail_if_ca_mismatch => Boolean # If ca_file is set and the server certificate doesn't verify, a log line is generated regardless, but normally right_http_connection continues on past the failure. If this is set, fail to connect in that case. Defaults to false.
103
94
  # :logger => Logger object # If omitted, HttpConnection logs to STDOUT
104
95
  # :exception => Exception to raise # The type of exception to raise
105
96
  # # if a request repeatedly fails. RuntimeError is raised if this parameter is omitted.
97
+ # :proxy_host => 'hostname' # hostname of HTTP proxy host to use, default none.
98
+ # :proxy_port => port # port of HTTP proxy host to use, default none.
99
+ # :proxy_username => 'username' # username to use for proxy authentication, default none.
100
+ # :proxy_password => 'password' # password to use for proxy authentication, default none.
106
101
  # :http_connection_retry_count # by default == Rightscale::HttpConnection::HTTP_CONNECTION_RETRY_COUNT
107
102
  # :http_connection_open_timeout # by default == Rightscale::HttpConnection::HTTP_CONNECTION_OPEN_TIMEOUT
108
103
  # :http_connection_read_timeout # by default == Rightscale::HttpConnection::HTTP_CONNECTION_READ_TIMEOUT
109
104
  # :http_connection_retry_delay # by default == Rightscale::HttpConnection::HTTP_CONNECTION_RETRY_DELAY
105
+ # :raise_on_timeout # do not perform a retry if timeout is received (false by default)
110
106
  def self.params
111
107
  @@params
112
108
  end
113
-
109
+
114
110
  # Set the global (class-level) parameters
115
111
  def self.params=(params)
116
112
  @@params = params
@@ -125,30 +121,65 @@ them.
125
121
  attr_accessor :logger
126
122
 
127
123
  # Params hash:
128
- # :user_agent => 'www.HostName.com' # String to report as HTTP User agent
124
+ # :user_agent => 'www.HostName.com' # String to report as HTTP User agent
129
125
  # :ca_file => 'path_to_file' # A path of a CA certification file in PEM format. The file can contain several CA certificates.
126
+ # :fail_if_ca_mismatch => Boolean # If ca_file is set and the server certificate doesn't verify, a log line is generated regardless, but normally right_http_connection continues on past the failure. If this is set, fail to connect in that case. Defaults to false.
130
127
  # :logger => Logger object # If omitted, HttpConnection logs to STDOUT
131
128
  # :exception => Exception to raise # The type of exception to raise if a request repeatedly fails. RuntimeError is raised if this parameter is omitted.
129
+ # :proxy_host => 'hostname' # hostname of HTTP proxy host to use, default none.
130
+ # :proxy_port => port # port of HTTP proxy host to use, default none.
131
+ # :proxy_username => 'username' # username to use for proxy authentication, default none.
132
+ # :proxy_password => 'password' # password to use for proxy authentication, default none.
132
133
  # :http_connection_retry_count # by default == Rightscale::HttpConnection.params[:http_connection_retry_count]
133
134
  # :http_connection_open_timeout # by default == Rightscale::HttpConnection.params[:http_connection_open_timeout]
134
135
  # :http_connection_read_timeout # by default == Rightscale::HttpConnection.params[:http_connection_read_timeout]
135
136
  # :http_connection_retry_delay # by default == Rightscale::HttpConnection.params[:http_connection_retry_delay]
136
- #
137
+ # :raise_on_timeout # do not perform a retry if timeout is received (false by default)
137
138
  def initialize(params={})
138
139
  @params = params
140
+
141
+ #set up logging first
142
+ @logger = get_param(:logger) ||
143
+ (RAILS_DEFAULT_LOGGER if defined?(RAILS_DEFAULT_LOGGER)) ||
144
+ Logger.new(STDOUT)
145
+
146
+ env_proxy_host, env_proxy_port, env_proxy_username, env_proxy_password = get_proxy_info_for_env if ENV['HTTP_PROXY']
147
+
139
148
  @params[:http_connection_retry_count] ||= @@params[:http_connection_retry_count]
140
149
  @params[:http_connection_open_timeout] ||= @@params[:http_connection_open_timeout]
141
150
  @params[:http_connection_read_timeout] ||= @@params[:http_connection_read_timeout]
142
151
  @params[:http_connection_retry_delay] ||= @@params[:http_connection_retry_delay]
152
+ @params[:proxy_host] ||= @@params[:proxy_host] || env_proxy_host
153
+ @params[:proxy_port] ||= @@params[:proxy_port] || env_proxy_port
154
+ @params[:proxy_username] ||= @@params[:proxy_username] || env_proxy_username
155
+ @params[:proxy_password] ||= @@params[:proxy_password] || env_proxy_password
156
+
143
157
  @http = nil
144
158
  @server = nil
145
- @logger = get_param(:logger) ||
146
- (RAILS_DEFAULT_LOGGER if defined?(RAILS_DEFAULT_LOGGER)) ||
147
- Logger.new(STDOUT)
159
+ #--------------
160
+ # Retry state - Keep track of errors on a per-server basis
161
+ #--------------
162
+ @state = {} # retry state indexed by server: consecutive error count, error time, and error
163
+
164
+ @eof = {}
165
+ end
166
+
167
+ def get_proxy_info_for_env
168
+ parsed_uri = URI.parse(ENV['HTTP_PROXY'])
169
+ if parsed_uri.scheme.to_s.downcase == 'http'
170
+ return parsed_uri.host, parsed_uri.port, parsed_uri.user, parsed_uri.password
171
+ else
172
+ @logger.warn "Invalid protocol in ENV['HTTP_PROXY'] URI = #{ENV['HTTP_PROXY'].inspect} expecting 'http' got #{parsed_uri.scheme.inspect}"
173
+ return
174
+ end
175
+ rescue Exception => e
176
+ @logger.warn "Error parsing ENV['HTTP_PROXY'] with exception: #{e.message}"
177
+ return
148
178
  end
179
+ private :get_proxy_info_for_env
149
180
 
150
- def get_param(name)
151
- @params[name] || @@params[name]
181
+ def get_param(name, custom_options={})
182
+ custom_options [name] || @params[name] || @@params[name]
152
183
  end
153
184
 
154
185
  # Query for the maximum size (in bytes) of a single read from the underlying
@@ -164,7 +195,7 @@ them.
164
195
  def socket_read_size=(newsize)
165
196
  Net::BufferedIO.socket_read_size=(newsize)
166
197
  end
167
-
198
+
168
199
  # Query for the maximum size (in bytes) of a single read from local data
169
200
  # sources like files. This is important, for example, in a streaming PUT of a
170
201
  # large buffer.
@@ -180,81 +211,78 @@ them.
180
211
  end
181
212
 
182
213
  private
183
- #--------------
184
- # Retry state - Keep track of errors on a per-server basis
185
- #--------------
186
- @@state = {} # retry state indexed by server: consecutive error count, error time, and error
187
- @@eof = {}
188
214
 
189
215
  # number of consecutive errors seen for server, 0 all is ok
190
216
  def error_count
191
- @@state[@server] ? @@state[@server][:count] : 0
217
+ @state[@server] ? @state[@server][:count] : 0
192
218
  end
193
-
219
+
194
220
  # time of last error for server, nil if all is ok
195
221
  def error_time
196
- @@state[@server] && @@state[@server][:time]
222
+ @state[@server] && @state[@server][:time]
197
223
  end
198
-
224
+
199
225
  # message for last error for server, "" if all is ok
200
226
  def error_message
201
- @@state[@server] ? @@state[@server][:message] : ""
227
+ @state[@server] ? @state[@server][:message] : ""
202
228
  end
203
-
229
+
204
230
  # add an error for a server
205
- def error_add(message)
206
- @@state[@server] = { :count => error_count+1, :time => Time.now, :message => message }
231
+ def error_add(error)
232
+ message = error
233
+ message = "#{error.class.name}: #{error.message}" if error.is_a?(Exception)
234
+ @state[@server] = { :count => error_count+1, :time => Time.now, :message => message }
207
235
  end
208
-
236
+
209
237
  # reset the error state for a server (i.e. a request succeeded)
210
238
  def error_reset
211
- @@state.delete(@server)
239
+ @state.delete(@server)
212
240
  end
213
-
241
+
214
242
  # Error message stuff...
215
243
  def banana_message
216
- return "#{@server} temporarily unavailable: (#{error_message})"
244
+ return "#{@protocol}://#{@server}:#{@port} temporarily unavailable: (#{error_message})"
217
245
  end
218
246
 
219
247
  def err_header
220
248
  return "#{self.class.name} :"
221
249
  end
222
-
250
+
223
251
  # Adds new EOF timestamp.
224
252
  # Returns the number of seconds to wait before new conection retry:
225
253
  # 0.5, 1, 2, 4, 8
226
254
  def add_eof
227
- (@@eof[@server] ||= []).unshift Time.now
228
- 0.25 * 2 ** @@eof[@server].size
255
+ (@eof[@server] ||= []).unshift Time.now
256
+ 0.25 * 2 ** @eof[@server].size
229
257
  end
230
258
 
231
259
  # Returns first EOF timestamp or nul if have no EOFs being tracked.
232
260
  def eof_time
233
- @@eof[@server] && @@eof[@server].last
261
+ @eof[@server] && @eof[@server].last
234
262
  end
235
-
263
+
236
264
  # Returns true if we are receiving EOFs during last @params[:http_connection_retry_delay] seconds
237
265
  # and there were no successful response from server
238
266
  def raise_on_eof_exception?
239
- @@eof[@server].blank? ? false : ( (Time.now.to_i-@params[:http_connection_retry_delay]) > @@eof[@server].last.to_i )
240
- end
241
-
267
+ @eof[@server].nil? ? false : ( (Time.now.to_i-@params[:http_connection_retry_delay]) > @eof[@server].last.to_i )
268
+ end
269
+
242
270
  # Reset a list of EOFs for this server.
243
271
  # This is being called when we have got an successful response from server.
244
272
  def eof_reset
245
- @@eof.delete(@server)
273
+ @eof.delete(@server)
246
274
  end
247
-
248
- # Detects if an object is 'streamable' - can we read from it, and can we know the size?
275
+
276
+ # Detects if an object is 'streamable' - can we read from it, and can we know the size?
249
277
  def setup_streaming(request)
250
278
  if(request.body && request.body.respond_to?(:read))
251
279
  body = request.body
252
- request.content_length = body.respond_to?(:lstat) ? body.lstat.size : body.size
280
+ request.content_length = body.respond_to?(:lstat) ? body.lstat.size : body.size
253
281
  request.body_stream = request.body
254
282
  true
255
283
  end
256
284
  end
257
-
285
+
258
286
  def get_fileptr_offset(request_params)
259
287
  request_params[:request].body.pos
260
288
  rescue Exception => e
@@ -262,7 +290,7 @@ them.
262
290
  # Just return 0 and get on with life.
263
291
  0
264
292
  end
265
-
293
+
266
294
  def reset_fileptr_offset(request, offset = 0)
267
295
  if(request.body_stream && request.body_stream.respond_to?(:pos))
268
296
  begin
@@ -272,38 +300,88 @@ them.
272
300
  " -- #{err_header} #{e.inspect}")
273
301
  raise e
274
302
  end
275
- end
303
+ end
276
304
  end
277
305
 
306
+ SECURITY_PARAMS = [:cert, :key, :cert_file, :key_file, :ca_file]
307
+
278
308
  # Start a fresh connection. The object closes any existing connection and
279
309
  # opens a new one.
280
310
  def start(request_params)
281
311
  # close the previous if exists
282
312
  finish
283
313
  # create new connection
284
- @server = request_params[:server]
285
- @port = request_params[:port]
286
- @protocol = request_params[:protocol]
314
+ @server = request_params[:server]
315
+ @port = request_params[:port]
316
+ @protocol = request_params[:protocol]
317
+ @proxy_host = request_params[:proxy_host]
318
+ @proxy_port = request_params[:proxy_port]
319
+ @proxy_username = request_params[:proxy_username]
320
+ @proxy_password = request_params[:proxy_password]
287
321
 
322
+ SECURITY_PARAMS.each do |param_name|
323
+ @params[param_name] = request_params[param_name]
324
+ end
325
+
288
326
  @logger.info("Opening new #{@protocol.upcase} connection to #@server:#@port")
289
- @http = Net::HTTP.new(@server, @port)
290
- @http.open_timeout = @params[:http_connection_open_timeout]
291
- @http.read_timeout = @params[:http_connection_read_timeout]
292
-
327
+
328
+ @logger.info("Connecting to proxy #{@proxy_host}:#{@proxy_port} with username" +
329
+ " #{@proxy_username.inspect}") unless @proxy_host.nil?
330
+
331
+ @http = Net::HTTP.new(@server, @port, @proxy_host, @proxy_port, @proxy_username,
332
+ @proxy_password)
333
+ @http.open_timeout = get_param(:http_connection_open_timeout, request_params)
334
+ @http.read_timeout = get_param(:http_connection_read_timeout, request_params)
335
+
293
336
  if @protocol == 'https'
294
337
  verifyCallbackProc = Proc.new{ |ok, x509_store_ctx|
338
+ # List of error codes: http://www.openssl.org/docs/apps/verify.html
295
339
  code = x509_store_ctx.error
296
340
  msg = x509_store_ctx.error_string
297
- #debugger
298
- @logger.warn("##### #{@server} certificate verify failed: #{msg}") unless code == 0
299
- true
341
+ if request_params[:fail_if_ca_mismatch] && code != 0
342
+ false
343
+ else
344
+ true
345
+ end
300
346
  }
301
347
  @http.use_ssl = true
348
+
302
349
  ca_file = get_param(:ca_file)
303
- if ca_file
304
- @http.verify_mode = OpenSSL::SSL::VERIFY_PEER
350
+ if ca_file && File.exists?(ca_file)
351
+ # Documentation for 'http.rb':
352
+ # : verify_mode, verify_mode=((|mode|))
353
+ # Sets the flags for server the certification verification at
354
+ # beginning of SSL/TLS session.
355
+ # OpenSSL::SSL::VERIFY_NONE or OpenSSL::SSL::VERIFY_PEER is acceptable.
356
+ #
357
+ # KHRVI: looks like the constant VERIFY_FAIL_IF_NO_PEER_CERT is not acceptable
305
358
  @http.verify_callback = verifyCallbackProc
306
- @http.ca_file = ca_file
359
+ @http.ca_file= ca_file
360
+ @http.verify_mode = get_param(:use_server_auth) ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
361
+ # The depth count is 'level 0:peer certificate', 'level 1: CA certificate', 'level 2: higher level CA certificate', and so on.
362
+ # Setting the maximum depth to 2 allows the levels 0, 1, and 2. The default depth limit is 9, allowing for the peer certificate and additional 9 CA certificates.
363
+ @http.verify_depth = 9
364
+ else
365
+ @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
366
+ end
367
+
368
+ # CERT
369
+ cert_file = get_param(:cert_file, request_params)
370
+ cert = File.read(cert_file) if cert_file && File.exists?(cert_file)
371
+ cert ||= get_param(:cert, request_params)
372
+ # KEY
373
+ key_file = get_param(:key_file, request_params)
374
+ key = File.read(key_file) if key_file && File.exists?(key_file)
375
+ key ||= get_param(:key, request_params)
376
+ if cert && key
377
+ begin
378
+ @http.verify_callback = verifyCallbackProc
379
+ @http.cert = OpenSSL::X509::Certificate.new(cert)
380
+ @http.key = OpenSSL::PKey::RSA.new(key)
381
+ rescue OpenSSL::PKey::RSAError, OpenSSL::X509::CertificateError => e
382
+ @logger.error "##### Error loading SSL client cert or key: #{e.message} :: backtrace #{e.backtrace}"
383
+ raise e
384
+ end
307
385
  end
308
386
  end
309
387
  # open connection
@@ -312,55 +390,82 @@ them.
312
390
 
313
391
  public
314
392
 
315
- =begin rdoc
393
+ =begin rdoc
316
394
  Send HTTP request to server
317
395
 
318
396
  request_params hash:
319
397
  :server => 'www.HostName.com' # Hostname or IP address of HTTP server
320
- :port => '80' # Port of HTTP server
321
- :protocol => 'https' # http and https are supported on any port
398
+ :port => '80' # Port of HTTP server
399
+ :protocol => 'https' # http and https are supported on any port
322
400
  :request => 'requeststring' # Fully-formed HTTP request to make
401
+ :proxy_host => 'hostname' # hostname of HTTP proxy host to use, default none.
402
+ :proxy_port => port # port of HTTP proxy host to use, default none.
403
+ :proxy_username => 'username' # username to use for proxy authentication, default none.
404
+ :proxy_password => 'password' # password to use for proxy authentication, default none.
405
+
406
+ :raise_on_timeout # do not perform a retry if timeout is received (false by default)
407
+ :http_connection_retry_count
408
+ :http_connection_open_timeout
409
+ :http_connection_read_timeout
410
+ :http_connection_retry_delay
411
+ :user_agent
412
+ :exception
323
413
 
324
414
  Raises RuntimeError, Interrupt, and params[:exception] (if specified in new).
325
-
415
+
326
416
  =end
327
417
  def request(request_params, &block)
418
+ current_params = @params.merge(request_params)
419
+ exception = get_param(:exception, current_params) || RuntimeError
420
+
421
+ # Re-establish the connection if any of auth params has changed
422
+ same_auth_params_as_before = SECURITY_PARAMS.select do |param|
423
+ request_params[param] != get_param(param)
424
+ end.empty?
425
+
328
426
  # We save the offset here so that if we need to retry, we can return the file pointer to its initial position
329
- mypos = get_fileptr_offset(request_params)
427
+ mypos = get_fileptr_offset(current_params)
330
428
  loop do
429
+
430
+ current_params[:protocol] ||= (current_params[:port] == 443 ? 'https' : 'http')
431
+ # (re)open connection to server if none exists or params has changed
432
+ same_server_as_before = @server == current_params[:server] &&
433
+ @port == current_params[:port] &&
434
+ @protocol == current_params[:protocol] &&
435
+ same_auth_params_as_before
436
+
331
437
  # if we are inside a delay between retries: no requests this time!
332
- if error_count > @params[:http_connection_retry_count] &&
333
- error_time + @params[:http_connection_retry_delay] > Time.now
438
+ # (skip this step if the endpoint has changed)
439
+ if error_count > current_params[:http_connection_retry_count] &&
440
+ error_time + current_params[:http_connection_retry_delay] > Time.now &&
441
+ same_server_as_before
442
+
334
443
  # store the message (otherwise it will be lost after error_reset and
335
444
  # we will raise an exception with an empty text)
336
445
  banana_message_text = banana_message
337
446
  @logger.warn("#{err_header} re-raising same error: #{banana_message_text} " +
338
- "-- error count: #{error_count}, error age: #{Time.now.to_i - error_time.to_i}")
339
- exception = get_param(:exception) || RuntimeError
447
+ "-- error count: #{error_count}, error age: #{Time.now.to_i - error_time.to_i}")
340
448
  raise exception.new(banana_message_text)
341
449
  end
342
-
450
+
343
451
  # try to connect server(if connection does not exist) and get response data
344
452
  begin
345
- request_params[:protocol] ||= (request_params[:port] == 443 ? 'https' : 'http')
346
- # (re)open connection to server if none exists or params has changed
347
- unless @http &&
453
+ request = current_params[:request]
454
+ request['User-Agent'] = get_param(:user_agent, current_params) || ''
455
+ unless @http &&
348
456
  @http.started? &&
349
- @server == request_params[:server] &&
350
- @port == request_params[:port] &&
351
- @protocol == request_params[:protocol]
352
- start(request_params)
457
+ same_server_as_before
458
+ same_auth_params_as_before = true
459
+ start(current_params)
353
460
  end
354
-
355
- # get response and return it
356
- request = request_params[:request]
357
- request['User-Agent'] = get_param(:user_agent) || ''
358
461
 
359
462
  # Detect if the body is a streamable object like a file or socket. If so, stream that
360
463
  # bad boy.
361
464
  setup_streaming(request)
465
+ # update READ_TIMEOUT value (it can be passed with request_params hash)
466
+ @http.read_timeout = get_param(:http_connection_read_timeout, current_params)
362
467
  response = @http.request(request, &block)
363
-
468
+
364
469
  error_reset
365
470
  eof_reset
366
471
  return response
@@ -373,51 +478,64 @@ them.
373
478
  # 'slept'. It is still not clear which way we should treat errors
374
479
  # like RST and resolution failures. For now, there is no additional
375
480
  # delay for these errors although this may change in the future.
376
-
481
+
377
482
  # EOFError means the server closed the connection on us.
378
483
  rescue EOFError => e
379
- @logger.debug("#{err_header} server #{@server} closed connection")
380
- @http = nil
484
+ finish(e.message)
381
485
 
486
+ @logger.debug("#{err_header} server #{@server} closed connection")
487
+
382
488
  # if we have waited long enough - raise an exception...
383
489
  if raise_on_eof_exception?
384
- exception = get_param(:exception) || RuntimeError
385
- @logger.warn("#{err_header} raising #{exception} due to permanent EOF being received from #{@server}, error age: #{Time.now.to_i - eof_time.to_i}")
490
+ @logger.warn("#{err_header} raising #{exception} due to permanent EOF being received from #{@server}, error age: #{Time.now.to_i - eof_time.to_i}")
386
491
  raise exception.new("Permanent EOF is being received from #{@server}.")
387
492
  else
388
493
  # ... else just sleep a bit before new retry
389
494
  sleep(add_eof)
390
495
  # We will be retrying the request, so reset the file pointer
391
496
  reset_fileptr_offset(request, mypos)
392
- end
393
- rescue Exception => e # See comment at bottom for the list of errors seen...
394
- @http = nil
395
- # if ctrl+c is pressed - we have to reraise exception to terminate proggy
396
- if e.is_a?(Interrupt) && !( e.is_a?(Errno::ETIMEDOUT) || e.is_a?(Timeout::Error))
397
- @logger.debug( "#{err_header} request to server #{@server} interrupted by ctrl-c")
398
- raise
399
- elsif e.is_a?(ArgumentError) && e.message.include?('wrong number of arguments (5 for 4)')
497
+ end
498
+ rescue ArgumentError => e
499
+ finish(e.message)
500
+
501
+ if e.message.include?('wrong number of arguments (5 for 4)')
400
502
  # seems our net_fix patch was overriden...
401
- exception = get_param(:exception) || RuntimeError
402
503
  raise exception.new('incompatible Net::HTTP monkey-patch')
504
+ else
505
+ raise e
506
+ end
507
+
508
+ rescue Timeout::Error, SocketError, SystemCallError, Interrupt => e # See comment at bottom for the list of errors seen...
509
+ finish(e.message)
510
+ if e.is_a?(Errno::ETIMEDOUT) || e.is_a?(Timeout::Error)
511
+ # Omit retries if it was explicitly requested
512
+ # #6481:
513
+ # ... When creating a resource in EC2 (instance, volume, snapshot, etc) it is undetermined what happened if the call times out.
514
+ # The resource may or may not have been created in EC2. Retrying the call may cause multiple resources to be created...
515
+ raise exception.new("#{e.class.name}: #{e.message}") if current_params[:raise_on_timeout]
516
+ elsif e.is_a?(Interrupt)
517
+ # if ctrl+c is pressed - we have to reraise exception to terminate proggy
518
+ @logger.debug( "#{err_header} request to server #{@server} interrupted by ctrl-c")
519
+ raise e
403
520
  end
404
521
  # oops - we got a banana: log it
405
- error_add(e.message)
522
+ error_add(e)
406
523
  @logger.warn("#{err_header} request failure count: #{error_count}, exception: #{e.inspect}")
407
524
 
408
525
  # We will be retrying the request, so reset the file pointer
409
526
  reset_fileptr_offset(request, mypos)
410
-
411
527
  end
412
528
  end
413
529
  end
414
530
 
415
531
  def finish(reason = '')
416
532
  if @http && @http.started?
417
- reason = ", reason: '#{reason}'" unless reason.blank?
533
+ reason = ", reason: '#{reason}'" unless reason.empty?
418
534
  @logger.info("Closing #{@http.use_ssl? ? 'HTTPS' : 'HTTP'} connection to #{@http.address}:#{@http.port}#{reason}")
419
- @http.finish
535
+ @http.finish
420
536
  end
537
+ ensure
538
+ @http = nil
421
539
  end
422
540
 
423
541
  # Errors received during testing:
@@ -430,6 +548,6 @@ them.
430
548
  # #<Errno::ECONNRESET: Connection reset by peer>
431
549
  # #<OpenSSL::SSL::SSLError: SSL_write:: bad write retry>
432
550
  end
433
-
551
+
434
552
  end
435
553