alinta-rest-client 2.2.0-x64-mingw32

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.mailmap +10 -0
  4. data/.rspec +2 -0
  5. data/.rubocop +2 -0
  6. data/.rubocop-disables.yml +393 -0
  7. data/.rubocop.yml +8 -0
  8. data/.travis.yml +49 -0
  9. data/AUTHORS +106 -0
  10. data/Gemfile +11 -0
  11. data/LICENSE +21 -0
  12. data/README.md +896 -0
  13. data/Rakefile +140 -0
  14. data/bin/restclient +92 -0
  15. data/history.md +357 -0
  16. data/lib/rest-client.rb +2 -0
  17. data/lib/rest_client.rb +2 -0
  18. data/lib/restclient.rb +183 -0
  19. data/lib/restclient/abstract_response.rb +252 -0
  20. data/lib/restclient/exceptions.rb +244 -0
  21. data/lib/restclient/params_array.rb +72 -0
  22. data/lib/restclient/payload.rb +234 -0
  23. data/lib/restclient/platform.rb +49 -0
  24. data/lib/restclient/raw_response.rb +49 -0
  25. data/lib/restclient/request.rb +875 -0
  26. data/lib/restclient/resource.rb +178 -0
  27. data/lib/restclient/response.rb +90 -0
  28. data/lib/restclient/utils.rb +274 -0
  29. data/lib/restclient/version.rb +8 -0
  30. data/lib/restclient/windows.rb +8 -0
  31. data/lib/restclient/windows/root_certs.rb +105 -0
  32. data/rest-client.gemspec +32 -0
  33. data/rest-client.windows.gemspec +19 -0
  34. data/spec/ISS.jpg +0 -0
  35. data/spec/helpers.rb +54 -0
  36. data/spec/integration/_lib.rb +1 -0
  37. data/spec/integration/capath_digicert/244b5494.0 +19 -0
  38. data/spec/integration/capath_digicert/81b9768f.0 +19 -0
  39. data/spec/integration/capath_digicert/README +8 -0
  40. data/spec/integration/capath_digicert/digicert.crt +19 -0
  41. data/spec/integration/capath_verisign/415660c1.0 +14 -0
  42. data/spec/integration/capath_verisign/7651b327.0 +14 -0
  43. data/spec/integration/capath_verisign/README +8 -0
  44. data/spec/integration/capath_verisign/verisign.crt +14 -0
  45. data/spec/integration/certs/digicert.crt +19 -0
  46. data/spec/integration/certs/verisign.crt +14 -0
  47. data/spec/integration/httpbin_spec.rb +128 -0
  48. data/spec/integration/integration_spec.rb +118 -0
  49. data/spec/integration/request_spec.rb +127 -0
  50. data/spec/spec_helper.rb +29 -0
  51. data/spec/unit/_lib.rb +1 -0
  52. data/spec/unit/abstract_response_spec.rb +145 -0
  53. data/spec/unit/exceptions_spec.rb +108 -0
  54. data/spec/unit/params_array_spec.rb +36 -0
  55. data/spec/unit/payload_spec.rb +295 -0
  56. data/spec/unit/raw_response_spec.rb +22 -0
  57. data/spec/unit/request2_spec.rb +54 -0
  58. data/spec/unit/request_spec.rb +1238 -0
  59. data/spec/unit/resource_spec.rb +134 -0
  60. data/spec/unit/response_spec.rb +252 -0
  61. data/spec/unit/restclient_spec.rb +80 -0
  62. data/spec/unit/utils_spec.rb +147 -0
  63. data/spec/unit/windows/root_certs_spec.rb +22 -0
  64. metadata +318 -0
@@ -0,0 +1,72 @@
1
+ module RestClient
2
+
3
+ # The ParamsArray class is used to represent an ordered list of [key, value]
4
+ # pairs. Use this when you need to include a key multiple times or want
5
+ # explicit control over parameter ordering.
6
+ #
7
+ # Most of the request payload & parameter functions normally accept a Hash of
8
+ # keys => values, which does not allow for duplicated keys.
9
+ #
10
+ # @see RestClient::Utils.encode_query_string
11
+ # @see RestClient::Utils.flatten_params
12
+ #
13
+ class ParamsArray
14
+ include Enumerable
15
+
16
+ # @param array [Array<Array>] An array of parameter key,value pairs. These
17
+ # pairs may be 2 element arrays [key, value] or single element hashes
18
+ # {key => value}. They may also be single element arrays to represent a
19
+ # key with no value.
20
+ #
21
+ # @example
22
+ # >> ParamsArray.new([[:foo, 123], [:foo, 456], [:bar, 789]])
23
+ # This will be encoded as "foo=123&foo=456&bar=789"
24
+ #
25
+ # @example
26
+ # >> ParamsArray.new({foo: 123, bar: 456})
27
+ # This is valid, but there's no reason not to just use the Hash directly
28
+ # instead of a ParamsArray.
29
+ #
30
+ #
31
+ def initialize(array)
32
+ @array = process_input(array)
33
+ end
34
+
35
+ def each(*args, &blk)
36
+ @array.each(*args, &blk)
37
+ end
38
+
39
+ def empty?
40
+ @array.empty?
41
+ end
42
+
43
+ private
44
+
45
+ def process_input(array)
46
+ array.map {|v| process_pair(v) }
47
+ end
48
+
49
+ # A pair may be:
50
+ # - A single element hash, e.g. {foo: 'bar'}
51
+ # - A two element array, e.g. ['foo', 'bar']
52
+ # - A one element array, e.g. ['foo']
53
+ #
54
+ def process_pair(pair)
55
+ case pair
56
+ when Hash
57
+ if pair.length != 1
58
+ raise ArgumentError.new("Bad # of fields for pair: #{pair.inspect}")
59
+ end
60
+ pair.to_a.fetch(0)
61
+ when Array
62
+ if pair.length > 2
63
+ raise ArgumentError.new("Bad # of fields for pair: #{pair.inspect}")
64
+ end
65
+ [pair.fetch(0), pair[1]]
66
+ else
67
+ # recurse, converting any non-array to an array
68
+ process_pair(pair.to_a)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,234 @@
1
+ require 'tempfile'
2
+ require 'securerandom'
3
+ require 'stringio'
4
+
5
+ begin
6
+ # Use mime/types/columnar if available, for reduced memory usage
7
+ require 'mime/types/columnar'
8
+ rescue LoadError
9
+ require 'mime/types'
10
+ end
11
+
12
+ module RestClient
13
+ module Payload
14
+ extend self
15
+
16
+ def generate(params)
17
+ if params.is_a?(RestClient::Payload::Base)
18
+ # pass through Payload objects unchanged
19
+ params
20
+ elsif params.is_a?(String)
21
+ Base.new(params)
22
+ elsif params.is_a?(Hash)
23
+ if params.delete(:multipart) == true || has_file?(params)
24
+ Multipart.new(params)
25
+ else
26
+ UrlEncoded.new(params)
27
+ end
28
+ elsif params.is_a?(ParamsArray)
29
+ if _has_file?(params)
30
+ Multipart.new(params)
31
+ else
32
+ UrlEncoded.new(params)
33
+ end
34
+ elsif params.respond_to?(:read)
35
+ Streamed.new(params)
36
+ else
37
+ nil
38
+ end
39
+ end
40
+
41
+ def has_file?(params)
42
+ unless params.is_a?(Hash)
43
+ raise ArgumentError.new("Must pass Hash, not #{params.inspect}")
44
+ end
45
+ _has_file?(params)
46
+ end
47
+
48
+ def _has_file?(obj)
49
+ case obj
50
+ when Hash, ParamsArray
51
+ obj.any? {|_, v| _has_file?(v) }
52
+ when Array
53
+ obj.any? {|v| _has_file?(v) }
54
+ else
55
+ obj.respond_to?(:path) && obj.respond_to?(:read)
56
+ end
57
+ end
58
+
59
+ class Base
60
+ def initialize(params)
61
+ build_stream(params)
62
+ end
63
+
64
+ def build_stream(params)
65
+ @stream = StringIO.new(params)
66
+ @stream.seek(0)
67
+ end
68
+
69
+ def read(*args)
70
+ @stream.read(*args)
71
+ end
72
+
73
+ def to_s
74
+ result = read
75
+ @stream.seek(0)
76
+ result
77
+ end
78
+
79
+ def headers
80
+ {'Content-Length' => size.to_s}
81
+ end
82
+
83
+ def size
84
+ @stream.size
85
+ end
86
+
87
+ alias :length :size
88
+
89
+ def close
90
+ @stream.close unless @stream.closed?
91
+ end
92
+
93
+ def closed?
94
+ @stream.closed?
95
+ end
96
+
97
+ def to_s_inspect
98
+ to_s.inspect
99
+ end
100
+
101
+ def short_inspect
102
+ if size && size > 500
103
+ "#{size} byte(s) length"
104
+ else
105
+ to_s_inspect
106
+ end
107
+ end
108
+
109
+ end
110
+
111
+ class Streamed < Base
112
+ def build_stream(params = nil)
113
+ @stream = params
114
+ end
115
+
116
+ def size
117
+ if @stream.respond_to?(:size)
118
+ @stream.size
119
+ elsif @stream.is_a?(IO)
120
+ @stream.stat.size
121
+ end
122
+ end
123
+
124
+ # TODO (breaks compatibility): ought to use mime_for() to autodetect the
125
+ # Content-Type for stream objects that have a filename.
126
+
127
+ alias :length :size
128
+ end
129
+
130
+ class UrlEncoded < Base
131
+ def build_stream(params = nil)
132
+ @stream = StringIO.new(Utils.encode_query_string(params))
133
+ @stream.seek(0)
134
+ end
135
+
136
+ def headers
137
+ super.merge({'Content-Type' => 'application/x-www-form-urlencoded'})
138
+ end
139
+ end
140
+
141
+ class Multipart < Base
142
+ EOL = "\r\n"
143
+
144
+ def build_stream(params)
145
+ b = '--' + boundary
146
+
147
+ @stream = Tempfile.new("RESTClient.Stream.#{rand(1000)}")
148
+ @stream.binmode
149
+ @stream.write(b + EOL)
150
+
151
+ case params
152
+ when Hash, ParamsArray
153
+ x = Utils.flatten_params(params)
154
+ else
155
+ x = params
156
+ end
157
+
158
+ last_index = x.length - 1
159
+ x.each_with_index do |a, index|
160
+ k, v = * a
161
+ if v.respond_to?(:read) && v.respond_to?(:path)
162
+ create_file_field(@stream, k, v)
163
+ else
164
+ create_regular_field(@stream, k, v)
165
+ end
166
+ @stream.write(EOL + b)
167
+ @stream.write(EOL) unless last_index == index
168
+ end
169
+ @stream.write('--')
170
+ @stream.write(EOL)
171
+ @stream.seek(0)
172
+ end
173
+
174
+ def create_regular_field(s, k, v)
175
+ s.write("Content-Disposition: form-data; name=\"#{k}\"")
176
+ s.write(EOL)
177
+ s.write(EOL)
178
+ s.write(v)
179
+ end
180
+
181
+ def create_file_field(s, k, v)
182
+ begin
183
+ s.write("Content-Disposition: form-data;")
184
+ s.write(" name=\"#{k}\";") unless (k.nil? || k=='')
185
+ s.write(" filename=\"#{v.respond_to?(:original_filename) ? v.original_filename : File.basename(v.path)}\"#{EOL}")
186
+ s.write("Content-Type: #{v.respond_to?(:content_type) ? v.content_type : mime_for(v.path)}#{EOL}")
187
+ s.write(EOL)
188
+ while (data = v.read(8124))
189
+ s.write(data)
190
+ end
191
+ ensure
192
+ v.close if v.respond_to?(:close)
193
+ end
194
+ end
195
+
196
+ def mime_for(path)
197
+ mime = MIME::Types.type_for path
198
+ mime.empty? ? 'text/plain' : mime[0].content_type
199
+ end
200
+
201
+ def boundary
202
+ return @boundary if defined?(@boundary) && @boundary
203
+
204
+ # Use the same algorithm used by WebKit: generate 16 random
205
+ # alphanumeric characters, replacing `+` `/` with `A` `B` (included in
206
+ # the list twice) to round out the set of 64.
207
+ s = SecureRandom.base64(12)
208
+ s.tr!('+/', 'AB')
209
+
210
+ @boundary = '----RubyFormBoundary' + s
211
+ end
212
+
213
+ # for Multipart do not escape the keys
214
+ #
215
+ # Ostensibly multipart keys MAY be percent encoded per RFC 7578, but in
216
+ # practice no major browser that I'm aware of uses percent encoding.
217
+ #
218
+ # Further discussion of multipart encoding:
219
+ # https://github.com/rest-client/rest-client/pull/403#issuecomment-156976930
220
+ #
221
+ def handle_key key
222
+ key
223
+ end
224
+
225
+ def headers
226
+ super.merge({'Content-Type' => %Q{multipart/form-data; boundary=#{boundary}}})
227
+ end
228
+
229
+ def close
230
+ @stream.close!
231
+ end
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,49 @@
1
+ require 'rbconfig'
2
+
3
+ module RestClient
4
+ module Platform
5
+ # Return true if we are running on a darwin-based Ruby platform. This will
6
+ # be false for jruby even on OS X.
7
+ #
8
+ # @return [Boolean]
9
+ def self.mac_mri?
10
+ RUBY_PLATFORM.include?('darwin')
11
+ end
12
+
13
+ # Return true if we are running on Windows.
14
+ #
15
+ # @return [Boolean]
16
+ #
17
+ def self.windows?
18
+ # Ruby only sets File::ALT_SEPARATOR on Windows, and the Ruby standard
19
+ # library uses that to test what platform it's on.
20
+ !!File::ALT_SEPARATOR
21
+ end
22
+
23
+ # Return true if we are running on jruby.
24
+ #
25
+ # @return [Boolean]
26
+ #
27
+ def self.jruby?
28
+ # defined on mri >= 1.9
29
+ RUBY_ENGINE == 'jruby'
30
+ end
31
+
32
+ def self.architecture
33
+ "#{RbConfig::CONFIG['host_os']} #{RbConfig::CONFIG['host_cpu']}"
34
+ end
35
+
36
+ def self.ruby_agent_version
37
+ case RUBY_ENGINE
38
+ when 'jruby'
39
+ "jruby/#{JRUBY_VERSION} (#{RUBY_VERSION}p#{RUBY_PATCHLEVEL})"
40
+ else
41
+ "#{RUBY_ENGINE}/#{RUBY_VERSION}p#{RUBY_PATCHLEVEL}"
42
+ end
43
+ end
44
+
45
+ def self.default_user_agent
46
+ "rest-client/#{VERSION} (#{architecture}) #{ruby_agent_version}"
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,49 @@
1
+ module RestClient
2
+ # The response from RestClient on a raw request looks like a string, but is
3
+ # actually one of these. 99% of the time you're making a rest call all you
4
+ # care about is the body, but on the occassion you want to fetch the
5
+ # headers you can:
6
+ #
7
+ # RestClient.get('http://example.com').headers[:content_type]
8
+ #
9
+ # In addition, if you do not use the response as a string, you can access
10
+ # a Tempfile object at res.file, which contains the path to the raw
11
+ # downloaded request body.
12
+ class RawResponse
13
+
14
+ include AbstractResponse
15
+
16
+ attr_reader :file, :request, :start_time, :end_time
17
+
18
+ def inspect
19
+ "<RestClient::RawResponse @code=#{code.inspect}, @file=#{file.inspect}, @request=#{request.inspect}>"
20
+ end
21
+
22
+ # @param [Tempfile] tempfile The temporary file containing the body
23
+ # @param [Net::HTTPResponse] net_http_res
24
+ # @param [RestClient::Request] request
25
+ # @param [Time] start_time
26
+ def initialize(tempfile, net_http_res, request, start_time=nil)
27
+ @file = tempfile
28
+
29
+ # reopen the tempfile so we can read it
30
+ @file.open
31
+
32
+ response_set_vars(net_http_res, request, start_time)
33
+ end
34
+
35
+ def to_s
36
+ body
37
+ end
38
+
39
+ def body
40
+ @file.rewind
41
+ @file.read
42
+ end
43
+
44
+ def size
45
+ file.size
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,875 @@
1
+ require 'tempfile'
2
+ require 'cgi'
3
+ require 'netrc'
4
+ require 'set'
5
+
6
+ begin
7
+ # Use mime/types/columnar if available, for reduced memory usage
8
+ require 'mime/types/columnar'
9
+ rescue LoadError
10
+ require 'mime/types'
11
+ end
12
+
13
+ module RestClient
14
+ # This class is used internally by RestClient to send the request, but you can also
15
+ # call it directly if you'd like to use a method not supported by the
16
+ # main API. For example:
17
+ #
18
+ # RestClient::Request.execute(:method => :head, :url => 'http://example.com')
19
+ #
20
+ # Mandatory parameters:
21
+ # * :method
22
+ # * :url
23
+ # Optional parameters (have a look at ssl and/or uri for some explanations):
24
+ # * :headers a hash containing the request headers
25
+ # * :cookies may be a Hash{String/Symbol => String} of cookie values, an
26
+ # Array<HTTP::Cookie>, or an HTTP::CookieJar containing cookies. These
27
+ # will be added to a cookie jar before the request is sent.
28
+ # * :user and :password for basic auth, will be replaced by a user/password available in the :url
29
+ # * :block_response call the provided block with the HTTPResponse as parameter
30
+ # * :raw_response return a low-level RawResponse instead of a Response
31
+ # * :max_redirects maximum number of redirections (default to 10)
32
+ # * :proxy An HTTP proxy URI to use for this request. Any value here
33
+ # (including nil) will override RestClient.proxy.
34
+ # * :verify_ssl enable ssl verification, possible values are constants from
35
+ # OpenSSL::SSL::VERIFY_*, defaults to OpenSSL::SSL::VERIFY_PEER
36
+ # * :read_timeout and :open_timeout are how long to wait for a response and
37
+ # to open a connection, in seconds. Pass nil to disable the timeout.
38
+ # * :timeout can be used to set both timeouts
39
+ # * :ssl_client_cert, :ssl_client_key, :ssl_ca_file, :ssl_ca_path,
40
+ # :ssl_cert_store, :ssl_verify_callback, :ssl_verify_callback_warnings
41
+ # * :ssl_version specifies the SSL version for the underlying Net::HTTP connection
42
+ # * :ssl_ciphers sets SSL ciphers for the connection. See
43
+ # OpenSSL::SSL::SSLContext#ciphers=
44
+ # * :before_execution_proc a Proc to call before executing the request. This
45
+ # proc, like procs from RestClient.before_execution_procs, will be
46
+ # called with the HTTP request and request params.
47
+ class Request
48
+
49
+ attr_reader :method, :uri, :url, :headers, :payload, :proxy,
50
+ :user, :password, :read_timeout, :max_redirects,
51
+ :open_timeout, :raw_response, :processed_headers, :args,
52
+ :ssl_opts
53
+
54
+ # An array of previous redirection responses
55
+ attr_accessor :redirection_history
56
+
57
+ def self.execute(args, & block)
58
+ new(args).execute(& block)
59
+ end
60
+
61
+ SSLOptionList = %w{client_cert client_key ca_file ca_path cert_store
62
+ version ciphers verify_callback verify_callback_warnings}
63
+
64
+ def inspect
65
+ "<RestClient::Request @method=#{@method.inspect}, @url=#{@url.inspect}>"
66
+ end
67
+
68
+ def initialize args
69
+ @method = normalize_method(args[:method])
70
+ @headers = (args[:headers] || {}).dup
71
+ if args[:url]
72
+ @url = process_url_params(normalize_url(args[:url]), headers)
73
+ else
74
+ raise ArgumentError, "must pass :url"
75
+ end
76
+
77
+ @user = @password = nil
78
+ parse_url_with_auth!(url)
79
+
80
+ # process cookie arguments found in headers or args
81
+ @cookie_jar = process_cookie_args!(@uri, @headers, args)
82
+
83
+ @payload = Payload.generate(args[:payload])
84
+
85
+ @user = args[:user] if args.include?(:user)
86
+ @password = args[:password] if args.include?(:password)
87
+
88
+ if args.include?(:timeout)
89
+ @read_timeout = args[:timeout]
90
+ @open_timeout = args[:timeout]
91
+ end
92
+ if args.include?(:read_timeout)
93
+ @read_timeout = args[:read_timeout]
94
+ end
95
+ if args.include?(:open_timeout)
96
+ @open_timeout = args[:open_timeout]
97
+ end
98
+ @block_response = args[:block_response]
99
+ @raw_response = args[:raw_response] || false
100
+
101
+ @stream_log_percent = args[:stream_log_percent] || 10
102
+ if @stream_log_percent <= 0 || @stream_log_percent > 100
103
+ raise ArgumentError.new(
104
+ "Invalid :stream_log_percent #{@stream_log_percent.inspect}")
105
+ end
106
+
107
+ @proxy = args.fetch(:proxy) if args.include?(:proxy)
108
+
109
+ @ssl_opts = {}
110
+
111
+ if args.include?(:verify_ssl)
112
+ v_ssl = args.fetch(:verify_ssl)
113
+ if v_ssl
114
+ if v_ssl == true
115
+ # interpret :verify_ssl => true as VERIFY_PEER
116
+ @ssl_opts[:verify_ssl] = OpenSSL::SSL::VERIFY_PEER
117
+ else
118
+ # otherwise pass through any truthy values
119
+ @ssl_opts[:verify_ssl] = v_ssl
120
+ end
121
+ else
122
+ # interpret all falsy :verify_ssl values as VERIFY_NONE
123
+ @ssl_opts[:verify_ssl] = OpenSSL::SSL::VERIFY_NONE
124
+ end
125
+ else
126
+ # if :verify_ssl was not passed, default to VERIFY_PEER
127
+ @ssl_opts[:verify_ssl] = OpenSSL::SSL::VERIFY_PEER
128
+ end
129
+
130
+ SSLOptionList.each do |key|
131
+ source_key = ('ssl_' + key).to_sym
132
+ if args.has_key?(source_key)
133
+ @ssl_opts[key.to_sym] = args.fetch(source_key)
134
+ end
135
+ end
136
+
137
+ # Set some other default SSL options, but only if we have an HTTPS URI.
138
+ if use_ssl?
139
+
140
+ # If there's no CA file, CA path, or cert store provided, use default
141
+ if !ssl_ca_file && !ssl_ca_path && !@ssl_opts.include?(:cert_store)
142
+ @ssl_opts[:cert_store] = self.class.default_ssl_cert_store
143
+ end
144
+ end
145
+
146
+ @log = args[:log]
147
+ @max_redirects = args[:max_redirects] || 10
148
+ @processed_headers = make_headers headers
149
+ @processed_headers_lowercase = Hash[@processed_headers.map {|k, v| [k.downcase, v]}]
150
+ @args = args
151
+
152
+ @before_execution_proc = args[:before_execution_proc]
153
+ end
154
+
155
+ def execute & block
156
+ # With 2.0.0+, net/http accepts URI objects in requests and handles wrapping
157
+ # IPv6 addresses in [] for use in the Host request header.
158
+ transmit uri, net_http_request_class(method).new(uri, processed_headers), payload, & block
159
+ ensure
160
+ payload.close if payload
161
+ end
162
+
163
+ # SSL-related options
164
+ def verify_ssl
165
+ @ssl_opts.fetch(:verify_ssl)
166
+ end
167
+ SSLOptionList.each do |key|
168
+ define_method('ssl_' + key) do
169
+ @ssl_opts[key.to_sym]
170
+ end
171
+ end
172
+
173
+ # Return true if the request URI will use HTTPS.
174
+ #
175
+ # @return [Boolean]
176
+ #
177
+ def use_ssl?
178
+ uri.is_a?(URI::HTTPS)
179
+ end
180
+
181
+ # Extract the query parameters and append them to the url
182
+ #
183
+ # Look through the headers hash for a :params option (case-insensitive,
184
+ # may be string or symbol). If present and the value is a Hash or
185
+ # RestClient::ParamsArray, *delete* the key/value pair from the headers
186
+ # hash and encode the value into a query string. Append this query string
187
+ # to the URL and return the resulting URL.
188
+ #
189
+ # @param [String] url
190
+ # @param [Hash] headers An options/headers hash to process. Mutation
191
+ # warning: the params key may be removed if present!
192
+ #
193
+ # @return [String] resulting url with query string
194
+ #
195
+ def process_url_params(url, headers)
196
+ url_params = nil
197
+
198
+ # find and extract/remove "params" key if the value is a Hash/ParamsArray
199
+ headers.delete_if do |key, value|
200
+ if key.to_s.downcase == 'params' &&
201
+ (value.is_a?(Hash) || value.is_a?(RestClient::ParamsArray))
202
+ if url_params
203
+ raise ArgumentError.new("Multiple 'params' options passed")
204
+ end
205
+ url_params = value
206
+ true
207
+ else
208
+ false
209
+ end
210
+ end
211
+
212
+ # build resulting URL with query string
213
+ if url_params && !url_params.empty?
214
+ query_string = RestClient::Utils.encode_query_string(url_params)
215
+
216
+ if url.include?('?')
217
+ url + '&' + query_string
218
+ else
219
+ url + '?' + query_string
220
+ end
221
+ else
222
+ url
223
+ end
224
+ end
225
+
226
+ # Render a hash of key => value pairs for cookies in the Request#cookie_jar
227
+ # that are valid for the Request#uri. This will not necessarily include all
228
+ # cookies if there are duplicate keys. It's safer to use the cookie_jar
229
+ # directly if that's a concern.
230
+ #
231
+ # @see Request#cookie_jar
232
+ #
233
+ # @return [Hash]
234
+ #
235
+ def cookies
236
+ hash = {}
237
+
238
+ @cookie_jar.cookies(uri).each do |c|
239
+ hash[c.name] = c.value
240
+ end
241
+
242
+ hash
243
+ end
244
+
245
+ # @return [HTTP::CookieJar]
246
+ def cookie_jar
247
+ @cookie_jar
248
+ end
249
+
250
+ # Render a Cookie HTTP request header from the contents of the @cookie_jar,
251
+ # or nil if the jar is empty.
252
+ #
253
+ # @see Request#cookie_jar
254
+ #
255
+ # @return [String, nil]
256
+ #
257
+ def make_cookie_header
258
+ return nil if cookie_jar.nil?
259
+
260
+ arr = cookie_jar.cookies(url)
261
+ return nil if arr.empty?
262
+
263
+ return HTTP::Cookie.cookie_value(arr)
264
+ end
265
+
266
+ # Process cookies passed as hash or as HTTP::CookieJar. For backwards
267
+ # compatibility, these may be passed as a :cookies option masquerading
268
+ # inside the headers hash. To avoid confusion, if :cookies is passed in
269
+ # both headers and Request#initialize, raise an error.
270
+ #
271
+ # :cookies may be a:
272
+ # - Hash{String/Symbol => String}
273
+ # - Array<HTTP::Cookie>
274
+ # - HTTP::CookieJar
275
+ #
276
+ # Passing as a hash:
277
+ # Keys may be symbols or strings. Values must be strings.
278
+ # Infer the domain name from the request URI and allow subdomains (as
279
+ # though '.example.com' had been set in a Set-Cookie header). Assume a
280
+ # path of '/'.
281
+ #
282
+ # RestClient::Request.new(url: 'http://example.com', method: :get,
283
+ # :cookies => {:foo => 'Value', 'bar' => '123'}
284
+ # )
285
+ #
286
+ # results in cookies as though set from the server by:
287
+ # Set-Cookie: foo=Value; Domain=.example.com; Path=/
288
+ # Set-Cookie: bar=123; Domain=.example.com; Path=/
289
+ #
290
+ # which yields a client cookie header of:
291
+ # Cookie: foo=Value; bar=123
292
+ #
293
+ # Passing as HTTP::CookieJar, which will be passed through directly:
294
+ #
295
+ # jar = HTTP::CookieJar.new
296
+ # jar.add(HTTP::Cookie.new('foo', 'Value', domain: 'example.com',
297
+ # path: '/', for_domain: false))
298
+ #
299
+ # RestClient::Request.new(..., :cookies => jar)
300
+ #
301
+ # @param [URI::HTTP] uri The URI for the request. This will be used to
302
+ # infer the domain name for cookies passed as strings in a hash. To avoid
303
+ # this implicit behavior, pass a full cookie jar or use HTTP::Cookie hash
304
+ # values.
305
+ # @param [Hash] headers The headers hash from which to pull the :cookies
306
+ # option. MUTATION NOTE: This key will be deleted from the hash if
307
+ # present.
308
+ # @param [Hash] args The options passed to Request#initialize. This hash
309
+ # will be used as another potential source for the :cookies key.
310
+ # These args will not be mutated.
311
+ #
312
+ # @return [HTTP::CookieJar] A cookie jar containing the parsed cookies.
313
+ #
314
+ def process_cookie_args!(uri, headers, args)
315
+
316
+ # Avoid ambiguity in whether options from headers or options from
317
+ # Request#initialize should take precedence by raising ArgumentError when
318
+ # both are present. Prior versions of rest-client claimed to give
319
+ # precedence to init options, but actually gave precedence to headers.
320
+ # Avoid that mess by erroring out instead.
321
+ if headers[:cookies] && args[:cookies]
322
+ raise ArgumentError.new(
323
+ "Cannot pass :cookies in Request.new() and in headers hash")
324
+ end
325
+
326
+ cookies_data = headers.delete(:cookies) || args[:cookies]
327
+
328
+ # return copy of cookie jar as is
329
+ if cookies_data.is_a?(HTTP::CookieJar)
330
+ return cookies_data.dup
331
+ end
332
+
333
+ # convert cookies hash into a CookieJar
334
+ jar = HTTP::CookieJar.new
335
+
336
+ (cookies_data || []).each do |key, val|
337
+
338
+ # Support for Array<HTTP::Cookie> mode:
339
+ # If key is a cookie object, add it to the jar directly and assert that
340
+ # there is no separate val.
341
+ if key.is_a?(HTTP::Cookie)
342
+ if val
343
+ raise ArgumentError.new("extra cookie val: #{val.inspect}")
344
+ end
345
+
346
+ jar.add(key)
347
+ next
348
+ end
349
+
350
+ if key.is_a?(Symbol)
351
+ key = key.to_s
352
+ end
353
+
354
+ # assume implicit domain from the request URI, and set for_domain to
355
+ # permit subdomains
356
+ jar.add(HTTP::Cookie.new(key, val, domain: uri.hostname.downcase,
357
+ path: '/', for_domain: true))
358
+ end
359
+
360
+ jar
361
+ end
362
+
363
+ # Generate headers for use by a request. Header keys will be stringified
364
+ # using `#stringify_headers` to normalize them as capitalized strings.
365
+ #
366
+ # The final headers consist of:
367
+ # - default headers from #default_headers
368
+ # - user_headers provided here
369
+ # - headers from the payload object (e.g. Content-Type, Content-Lenth)
370
+ # - cookie headers from #make_cookie_header
371
+ #
372
+ # BUG: stringify_headers does not alter the capitalization of headers that
373
+ # are passed as strings, it only normalizes those passed as symbols. This
374
+ # behavior will probably remain for a while for compatibility, but it means
375
+ # that the warnings that attempt to detect accidental header overrides may
376
+ # not always work.
377
+ # https://github.com/rest-client/rest-client/issues/599
378
+ #
379
+ # @param [Hash] user_headers User-provided headers to include
380
+ #
381
+ # @return [Hash<String, String>] A hash of HTTP headers => values
382
+ #
383
+ def make_headers(user_headers)
384
+ headers = stringify_headers(default_headers).merge(stringify_headers(user_headers))
385
+
386
+ # override headers from the payload (e.g. Content-Type, Content-Length)
387
+ if @payload
388
+ payload_headers = @payload.headers
389
+
390
+ # Warn the user if we override any headers that were previously
391
+ # present. This usually indicates that rest-client was passed
392
+ # conflicting information, e.g. if it was asked to render a payload as
393
+ # x-www-form-urlencoded but a Content-Type application/json was
394
+ # also supplied by the user.
395
+ payload_headers.each_pair do |key, val|
396
+ if headers.include?(key) && headers[key] != val
397
+ warn("warning: Overriding #{key.inspect} header " +
398
+ "#{headers.fetch(key).inspect} with #{val.inspect} " +
399
+ "due to payload")
400
+ end
401
+ end
402
+
403
+ headers.merge!(payload_headers)
404
+ end
405
+
406
+ # merge in cookies
407
+ cookies = make_cookie_header
408
+ if cookies && !cookies.empty?
409
+ if headers['Cookie']
410
+ warn('warning: overriding "Cookie" header with :cookies option')
411
+ end
412
+ headers['Cookie'] = cookies
413
+ end
414
+
415
+ headers
416
+ end
417
+
418
+ # The proxy URI for this request. If `:proxy` was provided on this request,
419
+ # use it over `RestClient.proxy`.
420
+ #
421
+ # Return false if a proxy was explicitly set and is falsy.
422
+ #
423
+ # @return [URI, false, nil]
424
+ #
425
+ def proxy_uri
426
+ if defined?(@proxy)
427
+ if @proxy
428
+ URI.parse(@proxy)
429
+ else
430
+ false
431
+ end
432
+ elsif RestClient.proxy_set?
433
+ if RestClient.proxy
434
+ URI.parse(RestClient.proxy)
435
+ else
436
+ false
437
+ end
438
+ else
439
+ nil
440
+ end
441
+ end
442
+
443
+ def net_http_object(hostname, port)
444
+ p_uri = proxy_uri
445
+
446
+ if p_uri.nil?
447
+ # no proxy set
448
+ Net::HTTP.new(hostname, port)
449
+ elsif !p_uri
450
+ # proxy explicitly set to none
451
+ Net::HTTP.new(hostname, port, nil, nil, nil, nil)
452
+ else
453
+ Net::HTTP.new(hostname, port,
454
+ p_uri.hostname, p_uri.port, p_uri.user, p_uri.password)
455
+
456
+ end
457
+ end
458
+
459
+ def net_http_request_class(method)
460
+ Net::HTTP.const_get(method.capitalize, false)
461
+ end
462
+
463
+ def net_http_do_request(http, req, body=nil, &block)
464
+ if body && body.respond_to?(:read)
465
+ req.body_stream = body
466
+ return http.request(req, nil, &block)
467
+ else
468
+ return http.request(req, body, &block)
469
+ end
470
+ end
471
+
472
+ # Normalize a URL by adding a protocol if none is present.
473
+ #
474
+ # If the string has no HTTP-like scheme (i.e. scheme followed by '//'), a
475
+ # scheme of 'http' will be added. This mimics the behavior of browsers and
476
+ # user agents like cURL.
477
+ #
478
+ # @param [String] url A URL string.
479
+ #
480
+ # @return [String]
481
+ #
482
+ def normalize_url(url)
483
+ url = 'http://' + url unless url.match(%r{\A[a-z][a-z0-9+.-]*://}i)
484
+ url
485
+ end
486
+
487
+ # Return a certificate store that can be used to validate certificates with
488
+ # the system certificate authorities. This will probably not do anything on
489
+ # OS X, which monkey patches OpenSSL in terrible ways to insert its own
490
+ # validation. On most *nix platforms, this will add the system certifcates
491
+ # using OpenSSL::X509::Store#set_default_paths. On Windows, this will use
492
+ # RestClient::Windows::RootCerts to look up the CAs trusted by the system.
493
+ #
494
+ # @return [OpenSSL::X509::Store]
495
+ #
496
+ def self.default_ssl_cert_store
497
+ cert_store = OpenSSL::X509::Store.new
498
+ cert_store.set_default_paths
499
+
500
+ # set_default_paths() doesn't do anything on Windows, so look up
501
+ # certificates using the win32 API.
502
+ if RestClient::Platform.windows?
503
+ RestClient::Windows::RootCerts.instance.to_a.uniq.each do |cert|
504
+ begin
505
+ cert_store.add_cert(cert)
506
+ rescue OpenSSL::X509::StoreError => err
507
+ # ignore duplicate certs
508
+ raise unless err.message == 'cert already in hash table'
509
+ end
510
+ end
511
+ end
512
+
513
+ cert_store
514
+ end
515
+
516
+ def redacted_uri
517
+ if uri.password
518
+ sanitized_uri = uri.dup
519
+ sanitized_uri.password = 'REDACTED'
520
+ sanitized_uri
521
+ else
522
+ uri
523
+ end
524
+ end
525
+
526
+ def redacted_url
527
+ redacted_uri.to_s
528
+ end
529
+
530
+ # Default to the global logger if there's not a request-specific one
531
+ def log
532
+ @log || RestClient.log
533
+ end
534
+
535
+ def log_request
536
+ return unless log
537
+
538
+ out = []
539
+
540
+ out << "RestClient.#{method} #{redacted_url.inspect}"
541
+ out << payload.short_inspect if payload
542
+ out << processed_headers.to_a.sort.map { |(k, v)| [k.inspect, v.inspect].join("=>") }.join(", ")
543
+ log << out.join(', ') + "\n"
544
+ end
545
+
546
+ # Return a hash of headers whose keys are capitalized strings
547
+ #
548
+ # BUG: stringify_headers does not fix the capitalization of headers that
549
+ # are already Strings. Leaving this behavior as is for now for
550
+ # backwards compatibility.
551
+ # https://github.com/rest-client/rest-client/issues/599
552
+ #
553
+ def stringify_headers headers
554
+ headers.inject({}) do |result, (key, value)|
555
+ if key.is_a? Symbol
556
+ key = key.to_s.split(/_/).map(&:capitalize).join('-')
557
+ end
558
+ if 'CONTENT-TYPE' == key.upcase
559
+ result[key] = maybe_convert_extension(value.to_s)
560
+ elsif 'ACCEPT' == key.upcase
561
+ # Accept can be composed of several comma-separated values
562
+ if value.is_a? Array
563
+ target_values = value
564
+ else
565
+ target_values = value.to_s.split ','
566
+ end
567
+ result[key] = target_values.map { |ext|
568
+ maybe_convert_extension(ext.to_s.strip)
569
+ }.join(', ')
570
+ else
571
+ result[key] = value.to_s
572
+ end
573
+ result
574
+ end
575
+ end
576
+
577
+ # Default headers set by RestClient. In addition to these headers, servers
578
+ # will receive headers set by Net::HTTP, such as Accept-Encoding and Host.
579
+ #
580
+ # @return [Hash<Symbol, String>]
581
+ def default_headers
582
+ {
583
+ :accept => '*/*',
584
+ :user_agent => RestClient::Platform.default_user_agent,
585
+ }
586
+ end
587
+
588
+ private
589
+
590
+ # Parse the `@url` string into a URI object and save it as
591
+ # `@uri`. Also save any basic auth user or password as @user and @password.
592
+ # If no auth info was passed, check for credentials in a Netrc file.
593
+ #
594
+ # @param [String] url A URL string.
595
+ #
596
+ # @return [URI]
597
+ #
598
+ # @raise URI::InvalidURIError on invalid URIs
599
+ #
600
+ def parse_url_with_auth!(url)
601
+ uri = URI.parse(url)
602
+
603
+ if uri.hostname.nil?
604
+ raise URI::InvalidURIError.new("bad URI(no host provided): #{url}")
605
+ end
606
+
607
+ @user = CGI.unescape(uri.user) if uri.user
608
+ @password = CGI.unescape(uri.password) if uri.password
609
+ if !@user && !@password
610
+ @user, @password = Netrc.read[uri.hostname]
611
+ end
612
+
613
+ @uri = uri
614
+ end
615
+
616
+ def print_verify_callback_warnings
617
+ warned = false
618
+ if RestClient::Platform.mac_mri?
619
+ warn('warning: ssl_verify_callback return code is ignored on OS X')
620
+ warned = true
621
+ end
622
+ if RestClient::Platform.jruby?
623
+ warn('warning: SSL verify_callback may not work correctly in jruby')
624
+ warn('see https://github.com/jruby/jruby/issues/597')
625
+ warned = true
626
+ end
627
+ warned
628
+ end
629
+
630
+ # Parse a method and return a normalized string version.
631
+ #
632
+ # Raise ArgumentError if the method is falsy, but otherwise do no
633
+ # validation.
634
+ #
635
+ # @param method [String, Symbol]
636
+ #
637
+ # @return [String]
638
+ #
639
+ # @see net_http_request_class
640
+ #
641
+ def normalize_method(method)
642
+ raise ArgumentError.new('must pass :method') unless method
643
+ method.to_s.downcase
644
+ end
645
+
646
+ def transmit uri, req, payload, & block
647
+
648
+ # We set this to true in the net/http block so that we can distinguish
649
+ # read_timeout from open_timeout. Now that we only support Ruby 2.0+,
650
+ # this is only needed for Timeout exceptions thrown outside of Net::HTTP.
651
+ established_connection = false
652
+
653
+ setup_credentials req
654
+
655
+ net = net_http_object(uri.hostname, uri.port)
656
+ net.use_ssl = uri.is_a?(URI::HTTPS)
657
+ net.ssl_version = ssl_version if ssl_version
658
+ net.ciphers = ssl_ciphers if ssl_ciphers
659
+
660
+ net.verify_mode = verify_ssl
661
+
662
+ net.cert = ssl_client_cert if ssl_client_cert
663
+ net.key = ssl_client_key if ssl_client_key
664
+ net.ca_file = ssl_ca_file if ssl_ca_file
665
+ net.ca_path = ssl_ca_path if ssl_ca_path
666
+ net.cert_store = ssl_cert_store if ssl_cert_store
667
+
668
+ # We no longer rely on net.verify_callback for the main SSL verification
669
+ # because it's not well supported on all platforms (see comments below).
670
+ # But do allow users to set one if they want.
671
+ if ssl_verify_callback
672
+ net.verify_callback = ssl_verify_callback
673
+
674
+ # Hilariously, jruby only calls the callback when cert_store is set to
675
+ # something, so make sure to set one.
676
+ # https://github.com/jruby/jruby/issues/597
677
+ if RestClient::Platform.jruby?
678
+ net.cert_store ||= OpenSSL::X509::Store.new
679
+ end
680
+
681
+ if ssl_verify_callback_warnings != false
682
+ if print_verify_callback_warnings
683
+ warn('pass :ssl_verify_callback_warnings => false to silence this')
684
+ end
685
+ end
686
+ end
687
+
688
+ if OpenSSL::SSL::VERIFY_PEER == OpenSSL::SSL::VERIFY_NONE
689
+ warn('WARNING: OpenSSL::SSL::VERIFY_PEER == OpenSSL::SSL::VERIFY_NONE')
690
+ warn('This dangerous monkey patch leaves you open to MITM attacks!')
691
+ warn('Try passing :verify_ssl => false instead.')
692
+ end
693
+
694
+ if defined? @read_timeout
695
+ if @read_timeout == -1
696
+ warn 'Deprecated: to disable timeouts, please use nil instead of -1'
697
+ @read_timeout = nil
698
+ end
699
+ net.read_timeout = @read_timeout
700
+ end
701
+ if defined? @open_timeout
702
+ if @open_timeout == -1
703
+ warn 'Deprecated: to disable timeouts, please use nil instead of -1'
704
+ @open_timeout = nil
705
+ end
706
+ net.open_timeout = @open_timeout
707
+ end
708
+
709
+ RestClient.before_execution_procs.each do |before_proc|
710
+ before_proc.call(req, args)
711
+ end
712
+
713
+ if @before_execution_proc
714
+ @before_execution_proc.call(req, args)
715
+ end
716
+
717
+ log_request
718
+
719
+ start_time = Time.now
720
+ tempfile = nil
721
+
722
+ net.start do |http|
723
+ established_connection = true
724
+
725
+ if @block_response
726
+ net_http_do_request(http, req, payload, &@block_response)
727
+ else
728
+ res = net_http_do_request(http, req, payload) { |http_response|
729
+ if @raw_response
730
+ # fetch body into tempfile
731
+ tempfile = fetch_body_to_tempfile(http_response)
732
+ else
733
+ # fetch body
734
+ http_response.read_body
735
+ end
736
+ http_response
737
+ }
738
+ process_result(res, start_time, tempfile, &block)
739
+ end
740
+ end
741
+ rescue EOFError
742
+ raise RestClient::ServerBrokeConnection
743
+ rescue Net::OpenTimeout => err
744
+ raise RestClient::Exceptions::OpenTimeout.new(nil, err)
745
+ rescue Net::ReadTimeout => err
746
+ raise RestClient::Exceptions::ReadTimeout.new(nil, err)
747
+ rescue Timeout::Error, Errno::ETIMEDOUT => err
748
+ # handling for non-Net::HTTP timeouts
749
+ if established_connection
750
+ raise RestClient::Exceptions::ReadTimeout.new(nil, err)
751
+ else
752
+ raise RestClient::Exceptions::OpenTimeout.new(nil, err)
753
+ end
754
+
755
+ rescue OpenSSL::SSL::SSLError => error
756
+ # TODO: deprecate and remove RestClient::SSLCertificateNotVerified and just
757
+ # pass through OpenSSL::SSL::SSLError directly.
758
+ #
759
+ # Exceptions in verify_callback are ignored [1], and jruby doesn't support
760
+ # it at all [2]. RestClient has to catch OpenSSL::SSL::SSLError and either
761
+ # re-throw it as is, or throw SSLCertificateNotVerified based on the
762
+ # contents of the message field of the original exception.
763
+ #
764
+ # The client has to handle OpenSSL::SSL::SSLError exceptions anyway, so
765
+ # we shouldn't make them handle both OpenSSL and RestClient exceptions.
766
+ #
767
+ # [1] https://github.com/ruby/ruby/blob/89e70fe8e7/ext/openssl/ossl.c#L238
768
+ # [2] https://github.com/jruby/jruby/issues/597
769
+
770
+ if error.message.include?("certificate verify failed")
771
+ raise SSLCertificateNotVerified.new(error.message)
772
+ else
773
+ raise error
774
+ end
775
+ end
776
+
777
+ def setup_credentials(req)
778
+ if user && !@processed_headers_lowercase.include?('authorization')
779
+ req.basic_auth(user, password)
780
+ end
781
+ end
782
+
783
+ def fetch_body_to_tempfile(http_response)
784
+ # Taken from Chef, which as in turn...
785
+ # Stolen from http://www.ruby-forum.com/topic/166423
786
+ # Kudos to _why!
787
+ tf = Tempfile.new('rest-client.')
788
+ tf.binmode
789
+
790
+ size = 0
791
+ total = http_response['Content-Length'].to_i
792
+ stream_log_bucket = nil
793
+
794
+ http_response.read_body do |chunk|
795
+ tf.write chunk
796
+ size += chunk.size
797
+ if log
798
+ if total == 0
799
+ log << "streaming %s %s (%d of unknown) [0 Content-Length]\n" % [@method.upcase, @url, size]
800
+ else
801
+ percent = (size * 100) / total
802
+ current_log_bucket, _ = percent.divmod(@stream_log_percent)
803
+ if current_log_bucket != stream_log_bucket
804
+ stream_log_bucket = current_log_bucket
805
+ log << "streaming %s %s %d%% done (%d of %d)\n" % [@method.upcase, @url, (size * 100) / total, size, total]
806
+ end
807
+ end
808
+ end
809
+ end
810
+ tf.close
811
+ tf
812
+ end
813
+
814
+ # @param res The Net::HTTP response object
815
+ # @param start_time [Time] Time of request start
816
+ def process_result(res, start_time, tempfile=nil, &block)
817
+ if @raw_response
818
+ unless tempfile
819
+ raise ArgumentError.new('tempfile is required')
820
+ end
821
+ response = RawResponse.new(tempfile, res, self, start_time)
822
+ else
823
+ response = Response.create(res.body, res, self, start_time)
824
+ end
825
+
826
+ response.log_response
827
+
828
+ if block_given?
829
+ block.call(response, self, res, & block)
830
+ else
831
+ response.return!(&block)
832
+ end
833
+
834
+ end
835
+
836
+ def parser
837
+ URI.const_defined?(:Parser) ? URI::Parser.new : URI
838
+ end
839
+
840
+ # Given a MIME type or file extension, return either a MIME type or, if
841
+ # none is found, the input unchanged.
842
+ #
843
+ # >> maybe_convert_extension('json')
844
+ # => 'application/json'
845
+ #
846
+ # >> maybe_convert_extension('unknown')
847
+ # => 'unknown'
848
+ #
849
+ # >> maybe_convert_extension('application/xml')
850
+ # => 'application/xml'
851
+ #
852
+ # @param ext [String]
853
+ #
854
+ # @return [String]
855
+ #
856
+ def maybe_convert_extension(ext)
857
+ unless ext =~ /\A[a-zA-Z0-9_@-]+\z/
858
+ # Don't look up strings unless they look like they could be a file
859
+ # extension known to mime-types.
860
+ #
861
+ # There currently isn't any API public way to look up extensions
862
+ # directly out of MIME::Types, but the type_for() method only strips
863
+ # off after a period anyway.
864
+ return ext
865
+ end
866
+
867
+ types = MIME::Types.type_for(ext)
868
+ if types.empty?
869
+ ext
870
+ else
871
+ types.first.content_type
872
+ end
873
+ end
874
+ end
875
+ end