rest-man 1.0.0

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