http 0.8.0.pre4 → 0.8.0.pre5

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b7920fa65897a4adb435be3f24272554a59bb8f0
4
- data.tar.gz: 9602b19d0e5f70eea417e69343a81159c356c3be
3
+ metadata.gz: cfd83e5db031e7c10a162574f2c700e879b66ea1
4
+ data.tar.gz: 3bcb6718bb90463c39ac0170482c86fe04f59da9
5
5
  SHA512:
6
- metadata.gz: e295f5c7d2b7c34ff6c4e0da1746361b777903d354910f0003eaded8c14cc180cd7e6e1edb11ce6980c63b851d0290c6e2dc0ba44c7bdee3cce036ee2ce17f3f
7
- data.tar.gz: 7956cd926d6060cdc7186e1546bf8b33192f82cb71ce288c98b19f467b9e8192a4d3cfa92fd2a789e9c2dcf5631a89c7bda9136f359b8f1bb0e15a16fc14a9f0
6
+ metadata.gz: f1b99fd432ba3b1cb7e0ec555796d86d75029543148d8f1a8f0e7fd3d49f898fef0b0dc2afee46fb7eb38add8aa803266b212cfc5b4941d2aa88ea220c938dae
7
+ data.tar.gz: 8594e4e915ea23e9646d0b3e335b733bf74be8af488a6fe15ff458c3edc1eb70ac3821abcab551afa4e419e4d301ddc13b1bee2c9623eb293683b694963bb106
data/.rubocop.yml CHANGED
@@ -5,6 +5,9 @@ Metrics/ClassLength:
5
5
  CountComments: false
6
6
  Max: 110
7
7
 
8
+ Metrics/PerceivedComplexity:
9
+ Max: 8
10
+
8
11
  Metrics/CyclomaticComplexity:
9
12
  Max: 8 # TODO: Lower to 6
10
13
 
data/CHANGES.md CHANGED
@@ -1,4 +1,4 @@
1
- ## 0.8.0.pre4 (2015-03-27)
1
+ ## 0.8.0.pre5 (2015-03-27)
2
2
 
3
3
  * Support for persistent HTTP connections (@zanker)
4
4
  * Add caching support. See #77 and #177. (@Asmod4n, @pezra)
data/Gemfile CHANGED
@@ -9,6 +9,7 @@ group :development do
9
9
  gem "celluloid-io"
10
10
  gem "guard"
11
11
  gem "guard-rspec", :require => false
12
+ gem "nokogiri", :require => false
12
13
  gem "pry"
13
14
 
14
15
  platforms :ruby_19, :ruby_20 do
data/README.md CHANGED
@@ -280,6 +280,7 @@ versions:
280
280
  * Ruby 2.0.0
281
281
  * Ruby 2.1.x
282
282
  * Ruby 2.2.x
283
+ * JRuby 1.7.x
283
284
 
284
285
  If something doesn't work on one of these versions, it's a bug.
285
286
 
data/Rakefile CHANGED
@@ -26,4 +26,46 @@ Yardstick::Rake::Verify.new do |verify|
26
26
  verify.threshold = 58
27
27
  end
28
28
 
29
+ task :generate_status_codes do
30
+ require "http"
31
+ require "nokogiri"
32
+
33
+ url = "http://www.iana.org/assignments/http-status-codes/http-status-codes.xml"
34
+ xml = Nokogiri::XML HTTP.get url
35
+ arr = xml.xpath("//xmlns:record").reduce [] do |a, e|
36
+ code = e.xpath("xmlns:value").text.to_s
37
+ desc = e.xpath("xmlns:description").text.to_s
38
+
39
+ next a if "Unassigned" == desc || "(Unused)" == desc
40
+
41
+ a << "#{code} => #{desc.inspect}"
42
+ end
43
+
44
+ File.open("./lib/http/response/status/reasons.rb", "w") do |io|
45
+ io.puts <<-TPL.gsub(/^[ ]{6}/, "")
46
+ # AUTO-GENERATED FILE, DO NOT CHANGE IT MANUALLY
47
+
48
+ require "delegate"
49
+
50
+ module HTTP
51
+ class Response
52
+ class Status < ::Delegator
53
+ # Code to Reason map
54
+ #
55
+ # @example Usage
56
+ #
57
+ # REASONS[400] # => "Bad Request"
58
+ # REASONS[414] # => "Request-URI Too Long"
59
+ #
60
+ # @return [Hash<Fixnum => String>]
61
+ REASONS = {
62
+ #{arr.join ",\n "}
63
+ }.each { |_, v| v.freeze }.freeze
64
+ end
65
+ end
66
+ end
67
+ TPL
68
+ end
69
+ end
70
+
29
71
  task :default => [:spec, :rubocop, :verify_measurements]
@@ -106,7 +106,7 @@ module HTTP
106
106
  # @param opts
107
107
  # @return [HTTP::Client]
108
108
  # @see Redirector#initialize
109
- def follow(opts = true)
109
+ def follow(opts = {})
110
110
  branch default_options.with_follow opts
111
111
  end
112
112
 
data/lib/http/client.rb CHANGED
@@ -2,6 +2,7 @@ require "cgi"
2
2
  require "uri"
3
3
  require "http/form_data"
4
4
  require "http/options"
5
+ require "http/connection"
5
6
  require "http/redirector"
6
7
 
7
8
  module HTTP
@@ -18,6 +19,7 @@ module HTTP
18
19
  def initialize(default_options = {})
19
20
  @default_options = HTTP::Options.new(default_options)
20
21
  @connection = nil
22
+ @state = :clean
21
23
  end
22
24
 
23
25
  # Make an HTTP request
@@ -38,13 +40,11 @@ module HTTP
38
40
  req = HTTP::Request.new(verb, uri, headers, proxy, body)
39
41
  res = perform req, opts
40
42
 
41
- if opts.follow
42
- res = Redirector.new(opts.follow).perform req, res do |request|
43
- perform request, opts
44
- end
45
- end
43
+ return res unless opts.follow
46
44
 
47
- res
45
+ Redirector.new(opts.follow).perform req, res do |request|
46
+ perform request, opts
47
+ end
48
48
  end
49
49
 
50
50
  # Perform a single (no follow) HTTP request
@@ -57,19 +57,22 @@ module HTTP
57
57
  def make_request(req, options)
58
58
  verify_connection!(req.uri)
59
59
 
60
+ @state = :dirty
61
+
60
62
  @connection ||= HTTP::Connection.new(req, options)
61
63
  @connection.send_request(req)
62
64
  @connection.read_headers!
63
65
 
64
66
  res = Response.new(
65
- @connection.parser.status_code,
66
- @connection.parser.http_version,
67
- @connection.parser.headers,
67
+ @connection.status_code,
68
+ @connection.http_version,
69
+ @connection.headers,
68
70
  Response::Body.new(@connection),
69
71
  req.uri
70
72
  )
71
73
 
72
74
  @connection.finish_response if req.verb == :head
75
+ @state = :clean
73
76
 
74
77
  res
75
78
 
@@ -84,6 +87,7 @@ module HTTP
84
87
  def close
85
88
  @connection.close if @connection
86
89
  @connection = nil
90
+ @state = :clean
87
91
  end
88
92
 
89
93
  private
@@ -92,11 +96,14 @@ module HTTP
92
96
  def verify_connection!(uri)
93
97
  if default_options.persistent? && base_host(uri) != default_options.persistent
94
98
  fail StateError, "Persistence is enabled for #{default_options.persistent}, but we got #{base_host(uri)}"
95
-
96
99
  # We re-create the connection object because we want to let prior requests
97
100
  # lazily load the body as long as possible, and this mimics prior functionality.
98
101
  elsif @connection && (!@connection.keep_alive? || @connection.expired?)
99
102
  close
103
+ # If we get into a bad state (eg, Timeout.timeout ensure being killed)
104
+ # close the connection to prevent potential for mixed responses.
105
+ elsif @state == :dirty
106
+ close
100
107
  end
101
108
  end
102
109
 
@@ -1,67 +1,80 @@
1
+ require "forwardable"
2
+
1
3
  require "http/response/parser"
2
4
 
3
5
  module HTTP
4
6
  # A connection to the HTTP server
5
7
  class Connection
6
- attr_reader :socket, :parser, :persistent, :keep_alive_timeout,
7
- :pending_request, :pending_response
8
-
8
+ extend Forwardable
9
9
  # Attempt to read this much data
10
10
  BUFFER_SIZE = 16_384
11
11
 
12
- def initialize(req, options)
13
- @persistent = options.persistent?
12
+ # HTTP/1.0
13
+ HTTP_1_0 = "1.0".freeze
14
14
 
15
- @keep_alive_timeout = options[:keep_alive_timeout]
15
+ # HTTP/1.1
16
+ HTTP_1_1 = "1.1".freeze
17
+
18
+ # @param [HTTP::Request] req
19
+ # @param [HTTP::Options] options
20
+ def initialize(req, options)
21
+ @persistent = options.persistent?
22
+ @keep_alive_timeout = options[:keep_alive_timeout].to_f
23
+ @pending_request = false
24
+ @pending_response = false
16
25
 
17
26
  @parser = Response::Parser.new
18
27
 
19
28
  @socket = options[:timeout_class].new(options[:timeout_options])
20
29
  @socket.connect(options[:socket_class], req.socket_host, req.socket_port)
21
30
 
22
- @socket.start_tls(
23
- req.uri.host,
24
- options[:ssl_socket_class],
25
- options[:ssl_context]
26
- ) if req.uri.is_a?(URI::HTTPS) && !req.using_proxy?
27
-
31
+ start_tls(req, options)
28
32
  reset_timer
29
33
  end
30
34
 
35
+ # @see (HTTP::Response::Parser#status_code)
36
+ def_delegator :@parser, :status_code
37
+
38
+ # @see (HTTP::Response::Parser#http_version)
39
+ def_delegator :@parser, :http_version
40
+
41
+ # @see (HTTP::Response::Parser#headers)
42
+ def_delegator :@parser, :headers
43
+
31
44
  # Send a request to the server
32
45
  #
33
46
  # @param [Request] Request to send to the server
34
- # @return [Nil]
47
+ # @return [nil]
35
48
  def send_request(req)
36
- if pending_response
49
+ if @pending_response
37
50
  fail StateError, "Tried to send a request while one is pending already. Make sure you read off the body."
38
- elsif pending_request
51
+ elsif @pending_request
39
52
  fail StateError, "Tried to send a request while a response is pending. Make sure you've fully read the body from the request."
40
53
  end
41
54
 
42
55
  @pending_request = true
43
56
 
44
- req.stream socket
57
+ req.stream @socket
45
58
 
46
59
  @pending_response = true
47
- @pending_request = nil
60
+ @pending_request = false
48
61
  end
49
62
 
50
63
  # Read a chunk of the body
51
64
  #
52
65
  # @return [String] data chunk
53
- # @return [Nil] when no more data left
66
+ # @return [nil] when no more data left
54
67
  def readpartial(size = BUFFER_SIZE)
55
- return unless pending_response
68
+ return unless @pending_response
56
69
 
57
70
  begin
58
71
  read_more size
59
- finished = parser.finished?
72
+ finished = @parser.finished?
60
73
  rescue EOFError
61
74
  finished = true
62
75
  end
63
76
 
64
- chunk = parser.chunk
77
+ chunk = @parser.chunk
65
78
 
66
79
  finish_response if finished
67
80
 
@@ -69,75 +82,91 @@ module HTTP
69
82
  end
70
83
 
71
84
  # Reads data from socket up until headers are loaded
85
+ # @return [void]
72
86
  def read_headers!
73
- read_more BUFFER_SIZE until parser.headers
87
+ read_more BUFFER_SIZE until @parser.headers
74
88
  set_keep_alive
75
-
76
- rescue IOError, Errno::ECONNRESET, Errno::EPIPE => ex
77
- return if ex.is_a?(EOFError) && parser.headers
78
- raise IOError, "problem making HTTP request: #{ex}"
89
+ rescue IOError, Errno::ECONNRESET, Errno::EPIPE => e
90
+ return if e.is_a?(EOFError) && @parser.headers
91
+ raise IOError, "problem making HTTP request: #{e}"
79
92
  end
80
93
 
81
94
  # Callback for when we've reached the end of a response
95
+ # @return [void]
82
96
  def finish_response
83
97
  close unless keep_alive?
84
98
 
85
- parser.reset
99
+ @parser.reset
86
100
  reset_timer
87
101
 
88
- @pending_response = nil
102
+ @pending_response = false
89
103
  end
90
104
 
91
105
  # Close the connection
106
+ # @return [void]
92
107
  def close
93
- socket.close unless socket.closed?
108
+ @socket.close unless @socket.closed?
94
109
 
95
- @pending_response = nil
96
- @pending_request = nil
110
+ @pending_response = false
111
+ @pending_request = false
97
112
  end
98
113
 
99
114
  # Whether we're keeping the conn alive
115
+ # @return [Boolean]
100
116
  def keep_alive?
101
- !!@keep_alive && !socket.closed?
117
+ !!@keep_alive && !@socket.closed?
102
118
  end
103
119
 
104
120
  # Whether our connection has expired
121
+ # @return [Boolean]
105
122
  def expired?
106
123
  !@conn_expires_at || @conn_expires_at < Time.now
107
124
  end
108
125
 
109
- def reset_timer
110
- @conn_expires_at = Time.now + keep_alive_timeout if persistent
126
+ private
127
+
128
+ # Sets up SSL context and starts TLS if needed.
129
+ # @param (see #initialize)
130
+ # @return [void]
131
+ def start_tls(req, options)
132
+ return unless req.uri.is_a?(URI::HTTPS) && !req.using_proxy?
133
+
134
+ ssl_context = options[:ssl_context]
135
+
136
+ unless ssl_context
137
+ ssl_context = OpenSSL::SSL::SSLContext.new
138
+ ssl_context.set_params(options[:ssl] || {})
139
+ end
140
+
141
+ @socket.start_tls(req.uri.host, options[:ssl_socket_class], ssl_context)
111
142
  end
112
143
 
113
- private :reset_timer
144
+ # Resets expiration of persistent connection.
145
+ # @return [void]
146
+ def reset_timer
147
+ @conn_expires_at = Time.now + @keep_alive_timeout if @persistent
148
+ end
114
149
 
115
150
  # Store whether the connection should be kept alive.
116
151
  # Once we reset the parser, we lose all of this state.
152
+ # @return [void]
117
153
  def set_keep_alive
118
- return @keep_alive = false unless persistent
119
-
120
- # HTTP/1.0 requires opt in for Keep Alive
121
- if parser.http_version == "1.0"
122
- @keep_alive = parser.headers["Connection"] == HTTP::Client::KEEP_ALIVE
123
-
124
- # HTTP/1.1 is opt-out
125
- elsif parser.http_version == "1.1"
126
- @keep_alive = parser.headers["Connection"] != HTTP::Client::CLOSE
127
-
128
- # Anything else we assume doesn't supportit
129
- else
154
+ return @keep_alive = false unless @persistent
155
+
156
+ case @parser.http_version
157
+ when HTTP_1_0 # HTTP/1.0 requires opt in for Keep Alive
158
+ @keep_alive = @parser.headers[Client::CONNECTION] == HTTP::Client::KEEP_ALIVE
159
+ when HTTP_1_1 # HTTP/1.1 is opt-out
160
+ @keep_alive = @parser.headers[Client::CONNECTION] != HTTP::Client::CLOSE
161
+ else # Anything else we assume doesn't supportit
130
162
  @keep_alive = false
131
163
  end
132
164
  end
133
165
 
134
- private :set_keep_alive
135
-
136
166
  # Feeds some more data into parser
167
+ # @return [void]
137
168
  def read_more(size)
138
- parser << socket.readpartial(size) unless parser.finished?
169
+ @parser << @socket.readpartial(size) unless @parser.finished?
139
170
  end
140
-
141
- private :read_more
142
171
  end
143
172
  end
data/lib/http/options.rb CHANGED
@@ -47,6 +47,7 @@ module HTTP
47
47
  :timeout_options => {},
48
48
  :socket_class => self.class.default_socket_class,
49
49
  :ssl_socket_class => self.class.default_ssl_socket_class,
50
+ :ssl => {},
50
51
  :cache => self.class.default_cache,
51
52
  :keep_alive_timeout => 5,
52
53
  :headers => {}}
@@ -65,12 +66,21 @@ module HTTP
65
66
 
66
67
  %w(
67
68
  proxy params form json body follow response
68
- socket_class ssl_socket_class ssl_context
69
+ socket_class ssl_socket_class ssl_context ssl
69
70
  persistent keep_alive_timeout timeout_class timeout_options
70
71
  ).each do |method_name|
71
72
  def_option method_name
72
73
  end
73
74
 
75
+ def follow=(value)
76
+ @follow = case
77
+ when !value then nil
78
+ when true == value then {}
79
+ when value.respond_to?(:fetch) then value
80
+ else argument_error! "Unsupported follow options: #{value}"
81
+ end
82
+ end
83
+
74
84
  def_option :cache do |cache_or_cache_options|
75
85
  if cache_or_cache_options.respond_to? :perform
76
86
  cache_or_cache_options
@@ -1,3 +1,5 @@
1
+ require "set"
2
+
1
3
  module HTTP
2
4
  class Redirector
3
5
  # Notifies that we reached max allowed redirect hops
@@ -7,60 +9,86 @@ module HTTP
7
9
  class EndlessRedirectError < TooManyRedirectsError; end
8
10
 
9
11
  # HTTP status codes which indicate redirects
10
- REDIRECT_CODES = [300, 301, 302, 303, 307, 308].freeze
12
+ REDIRECT_CODES = [300, 301, 302, 303, 307, 308].to_set.freeze
11
13
 
12
- # :nodoc:
13
- def initialize(options = nil)
14
- options = {:max_hops => 5} unless options.respond_to?(:fetch)
15
- @max_hops = options.fetch(:max_hops, 5)
16
- @max_hops = false if @max_hops && 1 > @max_hops.to_i
17
- end
14
+ # Codes which which should raise StateError in strict mode if original
15
+ # request was any of {UNSAFE_VERBS}
16
+ STRICT_SENSITIVE_CODES = [300, 301, 302].to_set.freeze
18
17
 
19
- # Follows redirects until non-redirect response found
20
- def perform(request, response, &block)
21
- reset(request, response)
22
- follow(&block)
23
- end
18
+ # Insecure http verbs, which should trigger StateError in strict mode
19
+ # upon {STRICT_SENSITIVE_CODES}
20
+ UNSAFE_VERBS = [:put, :delete, :post].to_set.freeze
24
21
 
25
- private
22
+ # Verbs which will remain unchanged upon See Other response.
23
+ SEE_OTHER_ALLOWED_VERBS = [:get, :head].to_set.freeze
26
24
 
27
- # Reset redirector state
28
- def reset(request, response)
29
- @request, @response = request, response
30
- @visited = []
25
+ # @!attribute [r] strict
26
+ # Returns redirector policy.
27
+ # @return [Boolean]
28
+ attr_reader :strict
29
+
30
+ # @!attribute [r] max_hops
31
+ # Returns maximum allowed hops.
32
+ # @return [Fixnum]
33
+ attr_reader :max_hops
34
+
35
+ # @param [Hash] opts
36
+ # @option opts [Boolean] :strict (true) redirector hops policy
37
+ # @option opts [#to_i] :max_hops (5) maximum allowed amount of hops
38
+ def initialize(options = {})
39
+ @strict = options.fetch(:strict, true)
40
+ @max_hops = options.fetch(:max_hops, 5).to_i
31
41
  end
32
42
 
33
- # Follow redirects
34
- def follow
35
- while REDIRECT_CODES.include?(@response.code)
36
- @visited << @request.uri.to_s
43
+ # Follows redirects until non-redirect response found
44
+ def perform(request, response)
45
+ @request = request
46
+ @response = response
47
+ @visited = []
48
+
49
+ while REDIRECT_CODES.include? @response.status.code
50
+ @visited << "#{@request.verb} #{@request.uri}"
37
51
 
38
52
  fail TooManyRedirectsError if too_many_hops?
39
53
  fail EndlessRedirectError if endless_loop?
40
54
 
41
- uri = @response.headers["Location"]
42
- fail StateError, "no Location header in redirect" unless uri
43
-
44
- if 303 == @response.code
45
- @request = @request.redirect uri, :get
46
- else
47
- @request = @request.redirect uri
48
- end
49
-
55
+ @request = redirect_to @response.headers["Location"]
50
56
  @response = yield @request
51
57
  end
52
58
 
53
59
  @response
54
60
  end
55
61
 
62
+ private
63
+
56
64
  # Check if we reached max amount of redirect hops
65
+ # @return [Boolean]
57
66
  def too_many_hops?
58
- @max_hops < @visited.count if @max_hops
67
+ 1 <= @max_hops && @max_hops < @visited.count
59
68
  end
60
69
 
61
70
  # Check if we got into an endless loop
71
+ # @return [Boolean]
62
72
  def endless_loop?
63
- 2 < @visited.count(@visited.last)
73
+ 2 <= @visited.count(@visited.last)
74
+ end
75
+
76
+ # Redirect policy for follow
77
+ # @return [Request]
78
+ def redirect_to(uri)
79
+ fail StateError, "no Location header in redirect" unless uri
80
+
81
+ verb = @request.verb
82
+ code = @response.status.code
83
+
84
+ if UNSAFE_VERBS.include?(verb) && STRICT_SENSITIVE_CODES.include?(code)
85
+ fail StateError, "can't follow #{@response.status} redirect" if @strict
86
+ verb = :get
87
+ end
88
+
89
+ verb = :get if !SEE_OTHER_ALLOWED_VERBS.include?(verb) && 303 == code
90
+
91
+ @request.redirect(uri, verb)
64
92
  end
65
93
  end
66
94
  end