rest-man 1.0.0

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