rest-client 1.6.14 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +6 -6
  3. data/.rspec +2 -1
  4. data/.rubocop-disables.yml +384 -0
  5. data/.rubocop.yml +3 -0
  6. data/.travis.yml +46 -1
  7. data/AUTHORS +28 -5
  8. data/Gemfile +5 -1
  9. data/LICENSE +21 -0
  10. data/README.md +784 -0
  11. data/Rakefile +95 -12
  12. data/bin/restclient +11 -12
  13. data/history.md +180 -16
  14. data/lib/restclient.rb +25 -11
  15. data/lib/restclient/abstract_response.rb +171 -51
  16. data/lib/restclient/exceptions.rb +102 -56
  17. data/lib/restclient/params_array.rb +72 -0
  18. data/lib/restclient/payload.rb +43 -74
  19. data/lib/restclient/platform.rb +22 -2
  20. data/lib/restclient/raw_response.rb +7 -3
  21. data/lib/restclient/request.rb +672 -179
  22. data/lib/restclient/resource.rb +6 -7
  23. data/lib/restclient/response.rb +64 -10
  24. data/lib/restclient/utils.rb +235 -0
  25. data/lib/restclient/version.rb +2 -1
  26. data/lib/restclient/windows.rb +8 -0
  27. data/lib/restclient/windows/root_certs.rb +105 -0
  28. data/rest-client.gemspec +16 -11
  29. data/rest-client.windows.gemspec +19 -0
  30. data/spec/helpers.rb +22 -0
  31. data/spec/integration/_lib.rb +1 -0
  32. data/spec/integration/capath_verisign/415660c1.0 +14 -0
  33. data/spec/integration/capath_verisign/7651b327.0 +14 -0
  34. data/spec/integration/capath_verisign/README +8 -0
  35. data/spec/integration/capath_verisign/verisign.crt +14 -0
  36. data/spec/integration/httpbin_spec.rb +87 -0
  37. data/spec/integration/integration_spec.rb +125 -0
  38. data/spec/integration/request_spec.rb +72 -20
  39. data/spec/spec_helper.rb +29 -0
  40. data/spec/unit/_lib.rb +1 -0
  41. data/spec/unit/abstract_response_spec.rb +145 -0
  42. data/spec/unit/exceptions_spec.rb +108 -0
  43. data/spec/{master_shake.jpg → unit/master_shake.jpg} +0 -0
  44. data/spec/unit/params_array_spec.rb +36 -0
  45. data/spec/{payload_spec.rb → unit/payload_spec.rb} +73 -54
  46. data/spec/{raw_response_spec.rb → unit/raw_response_spec.rb} +5 -4
  47. data/spec/unit/request2_spec.rb +54 -0
  48. data/spec/unit/request_spec.rb +1250 -0
  49. data/spec/unit/resource_spec.rb +134 -0
  50. data/spec/unit/response_spec.rb +241 -0
  51. data/spec/unit/restclient_spec.rb +79 -0
  52. data/spec/unit/utils_spec.rb +147 -0
  53. data/spec/unit/windows/root_certs_spec.rb +22 -0
  54. metadata +143 -53
  55. data/README.rdoc +0 -300
  56. data/lib/restclient/net_http_ext.rb +0 -55
  57. data/spec/abstract_response_spec.rb +0 -85
  58. data/spec/base.rb +0 -13
  59. data/spec/exceptions_spec.rb +0 -98
  60. data/spec/integration_spec.rb +0 -38
  61. data/spec/request2_spec.rb +0 -35
  62. data/spec/request_spec.rb +0 -528
  63. data/spec/resource_spec.rb +0 -136
  64. data/spec/response_spec.rb +0 -169
  65. data/spec/restclient_spec.rb +0 -73
@@ -1,16 +1,25 @@
1
1
  require 'cgi'
2
+ require 'http-cookie'
2
3
 
3
4
  module RestClient
4
5
 
5
6
  module AbstractResponse
6
7
 
7
- attr_reader :net_http_res, :args
8
+ attr_reader :net_http_res, :request
9
+
10
+ def inspect
11
+ raise NotImplementedError.new('must override in subclass')
12
+ end
8
13
 
9
14
  # HTTP status code
10
15
  def code
11
16
  @code ||= @net_http_res.code.to_i
12
17
  end
13
18
 
19
+ def history
20
+ @history ||= request.redirection_history || []
21
+ end
22
+
14
23
  # A hash of the headers, beautified with symbols and underscores.
15
24
  # e.g. "Content-type" will become :content_type.
16
25
  def headers
@@ -22,85 +31,196 @@ module RestClient
22
31
  @raw_headers ||= @net_http_res.to_hash
23
32
  end
24
33
 
25
- # Hash of cookies extracted from response headers
34
+ def response_set_vars(net_http_res, request)
35
+ @net_http_res = net_http_res
36
+ @request = request
37
+
38
+ # prime redirection history
39
+ history
40
+ end
41
+
42
+ # Hash of cookies extracted from response headers.
43
+ #
44
+ # NB: This will return only cookies whose domain matches this request, and
45
+ # may not even return all of those cookies if there are duplicate names.
46
+ # Use the full cookie_jar for more nuanced access.
47
+ #
48
+ # @see #cookie_jar
49
+ #
50
+ # @return [Hash]
51
+ #
26
52
  def cookies
27
- @cookies ||= (self.headers[:set_cookie] || {}).inject({}) do |out, cookie_content|
28
- out.merge parse_cookie(cookie_content)
53
+ hash = {}
54
+
55
+ cookie_jar.cookies(@request.uri).each do |cookie|
56
+ hash[cookie.name] = cookie.value
29
57
  end
58
+
59
+ hash
60
+ end
61
+
62
+ # Cookie jar extracted from response headers.
63
+ #
64
+ # @return [HTTP::CookieJar]
65
+ #
66
+ def cookie_jar
67
+ return @cookie_jar if defined?(@cookie_jar) && @cookie_jar
68
+
69
+ jar = @request.cookie_jar.dup
70
+ headers.fetch(:set_cookie, []).each do |cookie|
71
+ jar.parse(cookie, @request.uri)
72
+ end
73
+
74
+ @cookie_jar = jar
30
75
  end
31
76
 
32
77
  # Return the default behavior corresponding to the response code:
33
- # the response itself for code in 200..206, redirection for 301, 302 and 307 in get and head cases, redirection for 303 and an exception in other cases
34
- def return! request = nil, result = nil, & block
35
- if (200..207).include? code
78
+ #
79
+ # For 20x status codes: return the response itself
80
+ #
81
+ # For 30x status codes:
82
+ # 301, 302, 307: redirect GET / HEAD if there is a Location header
83
+ # 303: redirect, changing method to GET, if there is a Location header
84
+ #
85
+ # For all other responses, raise a response exception
86
+ #
87
+ def return!(&block)
88
+ case code
89
+ when 200..207
36
90
  self
37
- elsif [301, 302, 307].include? code
38
- unless [:get, :head].include? args[:method]
39
- raise Exceptions::EXCEPTIONS_MAP[code].new(self, code)
91
+ when 301, 302, 307
92
+ case request.method
93
+ when 'get', 'head'
94
+ check_max_redirects
95
+ follow_redirection(&block)
40
96
  else
41
- follow_redirection(request, result, & block)
97
+ raise exception_with_response
42
98
  end
43
- elsif code == 303
44
- args[:method] = :get
45
- args.delete :payload
46
- follow_redirection(request, result, & block)
47
- elsif Exceptions::EXCEPTIONS_MAP[code]
48
- raise Exceptions::EXCEPTIONS_MAP[code].new(self, code)
99
+ when 303
100
+ check_max_redirects
101
+ follow_get_redirection(&block)
49
102
  else
50
- raise RequestFailed.new(self, code)
103
+ raise exception_with_response
51
104
  end
52
105
  end
53
106
 
54
107
  def to_i
55
- code
108
+ warn('warning: calling Response#to_i is not recommended')
109
+ super
56
110
  end
57
111
 
58
112
  def description
59
113
  "#{code} #{STATUSES[code]} | #{(headers[:content_type] || '').gsub(/;.*$/, '')} #{size} bytes\n"
60
114
  end
61
115
 
62
- # Follow a redirection
63
- def follow_redirection request = nil, result = nil, & block
64
- url = headers[:location]
65
- if url !~ /^http/
66
- url = URI.parse(args[:url]).merge(url).to_s
67
- end
68
- args[:url] = url
69
- if request
70
- if request.max_redirects == 0
71
- raise MaxRedirectsReached
72
- end
73
- args[:password] = request.password
74
- args[:user] = request.user
75
- args[:headers] = request.headers
76
- args[:max_redirects] = request.max_redirects - 1
77
- # pass any cookie set in the result
78
- if result && result['set-cookie']
79
- args[:headers][:cookies] = (args[:headers][:cookies] || {}).merge(parse_cookie(result['set-cookie']))
80
- end
81
- end
82
- Request.execute args, &block
116
+ # Follow a redirection response by making a new HTTP request to the
117
+ # redirection target.
118
+ def follow_redirection(&block)
119
+ _follow_redirection(request.args.dup, &block)
120
+ end
121
+
122
+ # Follow a redirection response, but change the HTTP method to GET and drop
123
+ # the payload from the original request.
124
+ def follow_get_redirection(&block)
125
+ new_args = request.args.dup
126
+ new_args[:method] = :get
127
+ new_args.delete(:payload)
128
+
129
+ _follow_redirection(new_args, &block)
83
130
  end
84
131
 
85
- def AbstractResponse.beautify_headers(headers)
132
+ # Convert headers hash into canonical form.
133
+ #
134
+ # Header names will be converted to lowercase symbols with underscores
135
+ # instead of hyphens.
136
+ #
137
+ # Headers specified multiple times will be joined by comma and space,
138
+ # except for Set-Cookie, which will always be an array.
139
+ #
140
+ # Per RFC 2616, if a server sends multiple headers with the same key, they
141
+ # MUST be able to be joined into a single header by a comma. However,
142
+ # Set-Cookie (RFC 6265) cannot because commas are valid within cookie
143
+ # definitions. The newer RFC 7230 notes (3.2.2) that Set-Cookie should be
144
+ # handled as a special case.
145
+ #
146
+ # http://tools.ietf.org/html/rfc2616#section-4.2
147
+ # http://tools.ietf.org/html/rfc7230#section-3.2.2
148
+ # http://tools.ietf.org/html/rfc6265
149
+ #
150
+ # @param headers [Hash]
151
+ # @return [Hash]
152
+ #
153
+ def self.beautify_headers(headers)
86
154
  headers.inject({}) do |out, (key, value)|
87
- out[key.gsub(/-/, '_').downcase.to_sym] = %w{ set-cookie }.include?(key.downcase) ? value : value.first
155
+ key_sym = key.tr('-', '_').downcase.to_sym
156
+
157
+ # Handle Set-Cookie specially since it cannot be joined by comma.
158
+ if key.downcase == 'set-cookie'
159
+ out[key_sym] = value
160
+ else
161
+ out[key_sym] = value.join(', ')
162
+ end
163
+
88
164
  out
89
165
  end
90
166
  end
91
167
 
92
168
  private
93
169
 
94
- # Parse a cookie value and return its content in an Hash
95
- def parse_cookie cookie_content
96
- out = {}
97
- CGI::Cookie::parse(cookie_content).each do |key, cookie|
98
- unless ['expires', 'path'].include? key
99
- out[CGI::escape(key)] = cookie.value[0] ? (CGI::escape(cookie.value[0]) || '') : ''
100
- end
170
+ # Follow a redirection
171
+ #
172
+ # @param new_args [Hash] Start with this hash of arguments for the
173
+ # redirection request. The hash will be mutated, so be sure to dup any
174
+ # existing hash that should not be modified.
175
+ #
176
+ def _follow_redirection(new_args, &block)
177
+
178
+ # parse location header and merge into existing URL
179
+ url = headers[:location]
180
+
181
+ # cannot follow redirection if there is no location header
182
+ unless url
183
+ raise exception_with_response
184
+ end
185
+
186
+ # handle relative redirects
187
+ unless url.start_with?('http')
188
+ url = URI.parse(request.url).merge(url).to_s
189
+ end
190
+ new_args[:url] = url
191
+
192
+ new_args[:password] = request.password
193
+ new_args[:user] = request.user
194
+ new_args[:headers] = request.headers
195
+ new_args[:max_redirects] = request.max_redirects - 1
196
+
197
+ # pass through our new cookie jar
198
+ new_args[:cookies] = cookie_jar
199
+
200
+ # prepare new request
201
+ new_req = Request.new(new_args)
202
+
203
+ # append self to redirection history
204
+ new_req.redirection_history = history + [self]
205
+
206
+ # execute redirected request
207
+ new_req.execute(&block)
208
+ end
209
+
210
+ def check_max_redirects
211
+ if request.max_redirects <= 0
212
+ raise exception_with_response
101
213
  end
102
- out
103
214
  end
104
- end
105
215
 
216
+ def exception_with_response
217
+ begin
218
+ klass = Exceptions::EXCEPTIONS_MAP.fetch(code)
219
+ rescue KeyError
220
+ raise RequestFailed.new(self, code)
221
+ end
222
+
223
+ raise klass.new(self, code)
224
+ end
225
+ end
106
226
  end
@@ -1,5 +1,19 @@
1
1
  module RestClient
2
2
 
3
+ # Hash of HTTP status code => message.
4
+ #
5
+ # 1xx: Informational - Request received, continuing process
6
+ # 2xx: Success - The action was successfully received, understood, and
7
+ # accepted
8
+ # 3xx: Redirection - Further action must be taken in order to complete the
9
+ # request
10
+ # 4xx: Client Error - The request contains bad syntax or cannot be fulfilled
11
+ # 5xx: Server Error - The server failed to fulfill an apparently valid
12
+ # request
13
+ #
14
+ # @see
15
+ # http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
16
+ #
3
17
  STATUSES = {100 => 'Continue',
4
18
  101 => 'Switching Protocols',
5
19
  102 => 'Processing', #WebDAV
@@ -12,6 +26,8 @@ module RestClient
12
26
  205 => 'Reset Content',
13
27
  206 => 'Partial Content',
14
28
  207 => 'Multi-Status', #WebDAV
29
+ 208 => 'Already Reported', # RFC5842
30
+ 226 => 'IM Used', # RFC3229
15
31
 
16
32
  300 => 'Multiple Choices',
17
33
  301 => 'Moved Permanently',
@@ -21,12 +37,13 @@ module RestClient
21
37
  305 => 'Use Proxy', # http/1.1
22
38
  306 => 'Switch Proxy', # no longer used
23
39
  307 => 'Temporary Redirect', # http/1.1
40
+ 308 => 'Permanent Redirect', # RFC7538
24
41
 
25
42
  400 => 'Bad Request',
26
43
  401 => 'Unauthorized',
27
44
  402 => 'Payment Required',
28
45
  403 => 'Forbidden',
29
- 404 => 'Resource Not Found',
46
+ 404 => 'Not Found',
30
47
  405 => 'Method Not Allowed',
31
48
  406 => 'Not Acceptable',
32
49
  407 => 'Proxy Authentication Required',
@@ -35,18 +52,21 @@ module RestClient
35
52
  410 => 'Gone',
36
53
  411 => 'Length Required',
37
54
  412 => 'Precondition Failed',
38
- 413 => 'Request Entity Too Large',
39
- 414 => 'Request-URI Too Long',
55
+ 413 => 'Payload Too Large', # RFC7231 (renamed, see below)
56
+ 414 => 'URI Too Long', # RFC7231 (renamed, see below)
40
57
  415 => 'Unsupported Media Type',
41
- 416 => 'Requested Range Not Satisfiable',
58
+ 416 => 'Range Not Satisfiable', # RFC7233 (renamed, see below)
42
59
  417 => 'Expectation Failed',
43
- 418 => 'I\'m A Teapot',
60
+ 418 => 'I\'m A Teapot', #RFC2324
44
61
  421 => 'Too Many Connections From This IP',
45
62
  422 => 'Unprocessable Entity', #WebDAV
46
63
  423 => 'Locked', #WebDAV
47
64
  424 => 'Failed Dependency', #WebDAV
48
65
  425 => 'Unordered Collection', #WebDAV
49
66
  426 => 'Upgrade Required',
67
+ 428 => 'Precondition Required', #RFC6585
68
+ 429 => 'Too Many Requests', #RFC6585
69
+ 431 => 'Request Header Fields Too Large', #RFC6585
50
70
  449 => 'Retry With', #Microsoft
51
71
  450 => 'Blocked By Windows Parental Controls', #Microsoft
52
72
 
@@ -58,20 +78,27 @@ module RestClient
58
78
  505 => 'HTTP Version Not Supported',
59
79
  506 => 'Variant Also Negotiates',
60
80
  507 => 'Insufficient Storage', #WebDAV
81
+ 508 => 'Loop Detected', # RFC5842
61
82
  509 => 'Bandwidth Limit Exceeded', #Apache
62
- 510 => 'Not Extended'}
63
-
64
- # Compatibility : make the Response act like a Net::HTTPResponse when needed
65
- module ResponseForException
66
- def method_missing symbol, *args
67
- if net_http_res.respond_to? symbol
68
- warn "[warning] The response contained in an RestClient::Exception is now a RestClient::Response instead of a Net::HTTPResponse, please update your code"
69
- net_http_res.send symbol, *args
70
- else
71
- super
72
- end
73
- end
74
- end
83
+ 510 => 'Not Extended',
84
+ 511 => 'Network Authentication Required', # RFC6585
85
+ }
86
+
87
+ STATUSES_COMPATIBILITY = {
88
+ # The RFCs all specify "Not Found", but "Resource Not Found" was used in
89
+ # earlier RestClient releases.
90
+ 404 => ['ResourceNotFound'],
91
+
92
+ # HTTP 413 was renamed to "Payload Too Large" in RFC7231.
93
+ 413 => ['RequestEntityTooLarge'],
94
+
95
+ # HTTP 414 was renamed to "URI Too Long" in RFC7231.
96
+ 414 => ['RequestURITooLong'],
97
+
98
+ # HTTP 416 was renamed to "Range Not Satisfiable" in RFC7233.
99
+ 416 => ['RequestedRangeNotSatisfiable'],
100
+ }
101
+
75
102
 
76
103
  # This is the base RestClient exception class. Rescue it if you want to
77
104
  # catch any exception that your request might raise
@@ -81,15 +108,13 @@ module RestClient
81
108
  # probably an HTML error page) is e.response.
82
109
  class Exception < RuntimeError
83
110
  attr_accessor :response
84
- attr_writer :message
111
+ attr_accessor :original_exception
112
+ attr_writer :message
85
113
 
86
114
  def initialize response = nil, initial_response_code = nil
87
115
  @response = response
88
116
  @message = nil
89
117
  @initial_response_code = initial_response_code
90
-
91
- # compatibility: this make the exception behave like a Net::HTTPResponse
92
- response.extend ResponseForException if response
93
118
  end
94
119
 
95
120
  def http_code
@@ -101,22 +126,25 @@ module RestClient
101
126
  end
102
127
  end
103
128
 
104
- def http_body
105
- @response.body if @response
129
+ def http_headers
130
+ @response.headers if @response
106
131
  end
107
132
 
108
- def inspect
109
- "#{message}: #{http_body}"
133
+ def http_body
134
+ @response.body if @response
110
135
  end
111
136
 
112
137
  def to_s
113
- inspect
138
+ message
114
139
  end
115
140
 
116
141
  def message
117
- @message || self.class.name
142
+ @message || default_message
118
143
  end
119
144
 
145
+ def default_message
146
+ self.class.name
147
+ end
120
148
  end
121
149
 
122
150
  # Compatibility
@@ -126,7 +154,7 @@ module RestClient
126
154
  # The request failed with an error code not managed by the code
127
155
  class RequestFailed < ExceptionWithResponse
128
156
 
129
- def message
157
+ def default_message
130
158
  "HTTP status code #{http_code}"
131
159
  end
132
160
 
@@ -135,43 +163,68 @@ module RestClient
135
163
  end
136
164
  end
137
165
 
138
- # We will a create an exception for each status code, see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
166
+ # RestClient exception classes. TODO: move all exceptions into this module.
167
+ #
168
+ # We will a create an exception for each status code, see
169
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
170
+ #
139
171
  module Exceptions
140
172
  # Map http status codes to the corresponding exception class
141
173
  EXCEPTIONS_MAP = {}
142
174
  end
143
175
 
176
+ # Create HTTP status exception classes
144
177
  STATUSES.each_pair do |code, message|
145
-
146
- # Compatibility
147
- superclass = ([304, 401, 404].include? code) ? ExceptionWithResponse : RequestFailed
148
- klass = Class.new(superclass) do
149
- send(:define_method, :message) {"#{http_code ? "#{http_code} " : ''}#{message}"}
178
+ klass = Class.new(RequestFailed) do
179
+ send(:define_method, :default_message) {"#{http_code ? "#{http_code} " : ''}#{message}"}
150
180
  end
151
- klass_constant = const_set message.delete(' \-\''), klass
181
+ klass_constant = const_set(message.delete(' \-\''), klass)
152
182
  Exceptions::EXCEPTIONS_MAP[code] = klass_constant
153
183
  end
154
184
 
155
- # A redirect was encountered; caught by execute to retry with the new url.
156
- class Redirect < Exception
157
-
158
- def message
159
- 'Redirect'
185
+ # Create HTTP status exception classes used for backwards compatibility
186
+ STATUSES_COMPATIBILITY.each_pair do |code, compat_list|
187
+ klass = Exceptions::EXCEPTIONS_MAP.fetch(code)
188
+ compat_list.each do |old_name|
189
+ const_set(old_name, klass)
160
190
  end
191
+ end
161
192
 
162
- attr_accessor :url
193
+ module Exceptions
194
+ # We have to split the Exceptions module like we do here because the
195
+ # EXCEPTIONS_MAP is under Exceptions, but we depend on
196
+ # RestClient::RequestTimeout below.
197
+
198
+ # Base class for request timeouts.
199
+ #
200
+ # NB: Previous releases of rest-client would raise RequestTimeout both for
201
+ # HTTP 408 responses and for actual connection timeouts.
202
+ class Timeout < RestClient::RequestTimeout
203
+ def initialize(message=nil, original_exception=nil)
204
+ super(nil, nil)
205
+ self.message = message if message
206
+ self.original_exception = original_exception if original_exception
207
+ end
208
+ end
163
209
 
164
- def initialize(url)
165
- @url = url
210
+ # Timeout when connecting to a server. Typically wraps Net::OpenTimeout (in
211
+ # ruby 2.0 or greater).
212
+ class OpenTimeout < Timeout
213
+ def default_message
214
+ 'Timed out connecting to server'
215
+ end
166
216
  end
167
- end
168
217
 
169
- class MaxRedirectsReached < Exception
170
- def message
171
- 'Maximum number of redirect reached'
218
+ # Timeout when reading from a server. Typically wraps Net::ReadTimeout (in
219
+ # ruby 2.0 or greater).
220
+ class ReadTimeout < Timeout
221
+ def default_message
222
+ 'Timed out reading data from server'
223
+ end
172
224
  end
173
225
  end
174
226
 
227
+
175
228
  # The server broke the connection prior to the request completing. Usually
176
229
  # this means it crashed, or sometimes that your network connection was
177
230
  # severed before it could complete.
@@ -183,16 +236,9 @@ module RestClient
183
236
  end
184
237
 
185
238
  class SSLCertificateNotVerified < Exception
186
- def initialize(message)
239
+ def initialize(message = 'SSL certificate not verified')
187
240
  super nil, nil
188
241
  self.message = message
189
242
  end
190
243
  end
191
244
  end
192
-
193
- # backwards compatibility
194
- class RestClient::Request
195
- Redirect = RestClient::Redirect
196
- Unauthorized = RestClient::Unauthorized
197
- RequestFailed = RestClient::RequestFailed
198
- end