http 0.8.0.pre4 → 0.8.0.pre5

Sign up to get free protection for your applications and to get access to all the features.
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