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,178 @@
1
+ module RestMan
2
+ # A class that can be instantiated for access to a RESTful resource,
3
+ # including authentication.
4
+ #
5
+ # Example:
6
+ #
7
+ # resource = RestMan::Resource.new('http://some/resource')
8
+ # jpg = resource.get(:accept => 'image/jpg')
9
+ #
10
+ # With HTTP basic authentication:
11
+ #
12
+ # resource = RestMan::Resource.new('http://protected/resource', :user => 'user', :password => 'password')
13
+ # resource.delete
14
+ #
15
+ # With a timeout (seconds):
16
+ #
17
+ # RestMan::Resource.new('http://slow', :read_timeout => 10)
18
+ #
19
+ # With an open timeout (seconds):
20
+ #
21
+ # RestMan::Resource.new('http://behindfirewall', :open_timeout => 10)
22
+ #
23
+ # You can also use resources to share common headers. For headers keys,
24
+ # symbols are converted to strings. Example:
25
+ #
26
+ # resource = RestMan::Resource.new('http://some/resource', :headers => { :client_version => 1 })
27
+ #
28
+ # This header will be transported as X-Client-Version (notice the X prefix,
29
+ # capitalization and hyphens)
30
+ #
31
+ # Use the [] syntax to allocate subresources:
32
+ #
33
+ # site = RestMan::Resource.new('http://example.com', :user => 'adam', :password => 'mypasswd')
34
+ # site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
35
+ #
36
+ class Resource
37
+ attr_reader :url, :options, :block
38
+
39
+ def initialize(url, options={}, backwards_compatibility=nil, &block)
40
+ @url = url
41
+ @block = block
42
+ if options.class == Hash
43
+ @options = options
44
+ else # compatibility with previous versions
45
+ @options = { :user => options, :password => backwards_compatibility }
46
+ end
47
+ end
48
+
49
+ def get(additional_headers={}, &block)
50
+ headers = (options[:headers] || {}).merge(additional_headers)
51
+ Request.execute(options.merge(
52
+ :method => :get,
53
+ :url => url,
54
+ :headers => headers,
55
+ :log => log), &(block || @block))
56
+ end
57
+
58
+ def head(additional_headers={}, &block)
59
+ headers = (options[:headers] || {}).merge(additional_headers)
60
+ Request.execute(options.merge(
61
+ :method => :head,
62
+ :url => url,
63
+ :headers => headers,
64
+ :log => log), &(block || @block))
65
+ end
66
+
67
+ def post(payload, additional_headers={}, &block)
68
+ headers = (options[:headers] || {}).merge(additional_headers)
69
+ Request.execute(options.merge(
70
+ :method => :post,
71
+ :url => url,
72
+ :payload => payload,
73
+ :headers => headers,
74
+ :log => log), &(block || @block))
75
+ end
76
+
77
+ def put(payload, additional_headers={}, &block)
78
+ headers = (options[:headers] || {}).merge(additional_headers)
79
+ Request.execute(options.merge(
80
+ :method => :put,
81
+ :url => url,
82
+ :payload => payload,
83
+ :headers => headers,
84
+ :log => log), &(block || @block))
85
+ end
86
+
87
+ def patch(payload, additional_headers={}, &block)
88
+ headers = (options[:headers] || {}).merge(additional_headers)
89
+ Request.execute(options.merge(
90
+ :method => :patch,
91
+ :url => url,
92
+ :payload => payload,
93
+ :headers => headers,
94
+ :log => log), &(block || @block))
95
+ end
96
+
97
+ def delete(additional_headers={}, &block)
98
+ headers = (options[:headers] || {}).merge(additional_headers)
99
+ Request.execute(options.merge(
100
+ :method => :delete,
101
+ :url => url,
102
+ :headers => headers,
103
+ :log => log), &(block || @block))
104
+ end
105
+
106
+ def to_s
107
+ url
108
+ end
109
+
110
+ def user
111
+ options[:user]
112
+ end
113
+
114
+ def password
115
+ options[:password]
116
+ end
117
+
118
+ def headers
119
+ options[:headers] || {}
120
+ end
121
+
122
+ def read_timeout
123
+ options[:read_timeout]
124
+ end
125
+
126
+ def open_timeout
127
+ options[:open_timeout]
128
+ end
129
+
130
+ def log
131
+ options[:log] || RestMan.log
132
+ end
133
+
134
+ # Construct a subresource, preserving authentication.
135
+ #
136
+ # Example:
137
+ #
138
+ # site = RestMan::Resource.new('http://example.com', 'adam', 'mypasswd')
139
+ # site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
140
+ #
141
+ # This is especially useful if you wish to define your site in one place and
142
+ # call it in multiple locations:
143
+ #
144
+ # def orders
145
+ # RestMan::Resource.new('http://example.com/orders', 'admin', 'mypasswd')
146
+ # end
147
+ #
148
+ # orders.get # GET http://example.com/orders
149
+ # orders['1'].get # GET http://example.com/orders/1
150
+ # orders['1/items'].delete # DELETE http://example.com/orders/1/items
151
+ #
152
+ # Nest resources as far as you want:
153
+ #
154
+ # site = RestMan::Resource.new('http://example.com')
155
+ # posts = site['posts']
156
+ # first_post = posts['1']
157
+ # comments = first_post['comments']
158
+ # comments.post 'Hello', :content_type => 'text/plain'
159
+ #
160
+ def [](suburl, &new_block)
161
+ case
162
+ when block_given? then self.class.new(concat_urls(url, suburl), options, &new_block)
163
+ when block then self.class.new(concat_urls(url, suburl), options, &block)
164
+ else self.class.new(concat_urls(url, suburl), options)
165
+ end
166
+ end
167
+
168
+ def concat_urls(url, suburl) # :nodoc:
169
+ url = url.to_s
170
+ suburl = suburl.to_s
171
+ if url.slice(-1, 1) == '/' or suburl.slice(0, 1) == '/'
172
+ url + suburl
173
+ else
174
+ "#{url}/#{suburl}"
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,90 @@
1
+ module RestMan
2
+
3
+ # A Response from RestMan, you can access the response body, the code or the headers.
4
+ #
5
+ class Response < String
6
+
7
+ include AbstractResponse
8
+
9
+ # Return the HTTP response body.
10
+ #
11
+ # Future versions of RestMan will deprecate treating response objects
12
+ # directly as strings, so it will be necessary to call `.body`.
13
+ #
14
+ # @return [String]
15
+ #
16
+ def body
17
+ # Benchmarking suggests that "#{self}" is fastest, and that caching the
18
+ # body string in an instance variable doesn't make it enough faster to be
19
+ # worth the extra memory storage.
20
+ String.new(self)
21
+ end
22
+
23
+ # Convert the HTTP response body to a pure String object.
24
+ #
25
+ # @return [String]
26
+ def to_s
27
+ body
28
+ end
29
+
30
+ # Convert the HTTP response body to a pure String object.
31
+ #
32
+ # @return [String]
33
+ def to_str
34
+ body
35
+ end
36
+
37
+ def inspect
38
+ "<RestMan::Response #{code.inspect} #{body_truncated(10).inspect}>"
39
+ end
40
+
41
+ # Initialize a Response object. Because RestMan::Response is
42
+ # (unfortunately) a subclass of String for historical reasons,
43
+ # Response.create is the preferred initializer.
44
+ #
45
+ # @param [String, nil] body The response body from the Net::HTTPResponse
46
+ # @param [Net::HTTPResponse] net_http_res
47
+ # @param [RestMan::Request] request
48
+ # @param [Time] start_time
49
+ def self.create(body, net_http_res, request, start_time=nil)
50
+ result = self.new(body || '')
51
+
52
+ result.response_set_vars(net_http_res, request, start_time)
53
+ fix_encoding(result)
54
+
55
+ result
56
+ end
57
+
58
+ # Set the String encoding according to the 'Content-Type: charset' header,
59
+ # if possible.
60
+ def self.fix_encoding(response)
61
+ charset = RestMan::Utils.get_encoding_from_headers(response.headers)
62
+ encoding = nil
63
+
64
+ begin
65
+ encoding = Encoding.find(charset) if charset
66
+ rescue ArgumentError
67
+ if response.log
68
+ response.log << "No such encoding: #{charset.inspect}"
69
+ end
70
+ end
71
+
72
+ return unless encoding
73
+
74
+ response.force_encoding(encoding)
75
+
76
+ response
77
+ end
78
+
79
+ private
80
+
81
+ def body_truncated(length)
82
+ b = body
83
+ if b.length > length
84
+ b[0..length] + '...'
85
+ else
86
+ b
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,274 @@
1
+ require 'http/accept'
2
+
3
+ module RestMan
4
+ # Various utility methods
5
+ module Utils
6
+
7
+ # Return encoding from an HTTP header hash.
8
+ #
9
+ # We use the RFC 7231 specification and do not impose a default encoding on
10
+ # text. This differs from the older RFC 2616 behavior, which specifies
11
+ # using ISO-8859-1 for text/* content types without a charset.
12
+ #
13
+ # Strings will use the default encoding when this method returns nil. This
14
+ # default is likely to be UTF-8 for Ruby >= 2.0
15
+ #
16
+ # @param headers [Hash<Symbol,String>]
17
+ #
18
+ # @return [String, nil] Return the string encoding or nil if no header is
19
+ # found.
20
+ #
21
+ # @example
22
+ # >> get_encoding_from_headers({:content_type => 'text/plain; charset=UTF-8'})
23
+ # => "UTF-8"
24
+ #
25
+ def self.get_encoding_from_headers(headers)
26
+ type_header = headers[:content_type]
27
+ return nil unless type_header
28
+
29
+ # TODO: remove this hack once we drop support for Ruby 2.0
30
+ if RUBY_VERSION.start_with?('2.0')
31
+ _content_type, params = deprecated_cgi_parse_header(type_header)
32
+
33
+ if params.include?('charset')
34
+ return params.fetch('charset').gsub(/(\A["']*)|(["']*\z)/, '')
35
+ end
36
+
37
+ else
38
+
39
+ begin
40
+ _content_type, params = cgi_parse_header(type_header)
41
+ rescue HTTP::Accept::ParseError
42
+ return nil
43
+ else
44
+ params['charset']
45
+ end
46
+ end
47
+ end
48
+
49
+ # Parse a Content-Type like header.
50
+ #
51
+ # Return the main content-type and a hash of params.
52
+ #
53
+ # @param [String] line
54
+ # @return [Array(String, Hash)]
55
+ #
56
+ def self.cgi_parse_header(line)
57
+ types = HTTP::Accept::MediaTypes.parse(line)
58
+
59
+ if types.empty?
60
+ raise HTTP::Accept::ParseError.new("Found no types in header line")
61
+ end
62
+
63
+ [types.first.mime_type, types.first.parameters]
64
+ end
65
+
66
+ # Parse semi-colon separated, potentially quoted header string iteratively.
67
+ #
68
+ # @private
69
+ #
70
+ # @deprecated This method is deprecated and only exists to support Ruby
71
+ # 2.0, which is not supported by HTTP::Accept.
72
+ #
73
+ # @todo remove this method when dropping support for Ruby 2.0
74
+ #
75
+ def self._cgi_parseparam(s)
76
+ return enum_for(__method__, s) unless block_given?
77
+
78
+ while s[0] == ';'
79
+ s = s[1..-1]
80
+ ends = s.index(';')
81
+ while ends && ends > 0 \
82
+ && (s[0...ends].count('"') -
83
+ s[0...ends].scan('\"').count) % 2 != 0
84
+ ends = s.index(';', ends + 1)
85
+ end
86
+ if ends.nil?
87
+ ends = s.length
88
+ end
89
+ f = s[0...ends]
90
+ yield f.strip
91
+ s = s[ends..-1]
92
+ end
93
+ nil
94
+ end
95
+
96
+ # Parse a Content-Type like header.
97
+ #
98
+ # Return the main content-type and a hash of options.
99
+ #
100
+ # This method was ported directly from Python's cgi.parse_header(). It
101
+ # probably doesn't read or perform particularly well in ruby.
102
+ # https://github.com/python/cpython/blob/3.4/Lib/cgi.py#L301-L331
103
+ #
104
+ # @param [String] line
105
+ # @return [Array(String, Hash)]
106
+ #
107
+ # @deprecated This method is deprecated and only exists to support Ruby
108
+ # 2.0, which is not supported by HTTP::Accept.
109
+ #
110
+ # @todo remove this method when dropping support for Ruby 2.0
111
+ #
112
+ def self.deprecated_cgi_parse_header(line)
113
+ parts = _cgi_parseparam(';' + line)
114
+ key = parts.next
115
+ pdict = {}
116
+
117
+ begin
118
+ while (p = parts.next)
119
+ i = p.index('=')
120
+ if i
121
+ name = p[0...i].strip.downcase
122
+ value = p[i+1..-1].strip
123
+ if value.length >= 2 && value[0] == '"' && value[-1] == '"'
124
+ value = value[1...-1]
125
+ value = value.gsub('\\\\', '\\').gsub('\\"', '"')
126
+ end
127
+ pdict[name] = value
128
+ end
129
+ end
130
+ rescue StopIteration
131
+ end
132
+
133
+ [key, pdict]
134
+ end
135
+
136
+ # Serialize a ruby object into HTTP query string parameters.
137
+ #
138
+ # There is no standard for doing this, so we choose our own slightly
139
+ # idiosyncratic format. The output closely matches the format understood by
140
+ # Rails, Rack, and PHP.
141
+ #
142
+ # If you don't want handling of complex objects and only want to handle
143
+ # simple flat hashes, you may want to use `URI.encode_www_form` instead,
144
+ # which implements HTML5-compliant URL encoded form data.
145
+ #
146
+ # @param [Hash,ParamsArray] object The object to serialize
147
+ #
148
+ # @return [String] A string appropriate for use as an HTTP query string
149
+ #
150
+ # @see {flatten_params}
151
+ #
152
+ # @see URI.encode_www_form
153
+ #
154
+ # @see See also Object#to_query in ActiveSupport
155
+ # @see http://php.net/manual/en/function.http-build-query.php
156
+ # http_build_query in PHP
157
+ # @see See also Rack::Utils.build_nested_query in Rack
158
+ #
159
+ # Notable differences from the ActiveSupport implementation:
160
+ #
161
+ # - Empty hash and empty array are treated the same as nil instead of being
162
+ # omitted entirely from the output. Rather than disappearing, they will
163
+ # appear to be nil instead.
164
+ #
165
+ # It's most common to pass a Hash as the object to serialize, but you can
166
+ # also use a ParamsArray if you want to be able to pass the same key with
167
+ # multiple values and not use the rack/rails array convention.
168
+ #
169
+ # @since 2.0.0
170
+ #
171
+ # @example Simple hashes
172
+ # >> encode_query_string({foo: 123, bar: 456})
173
+ # => 'foo=123&bar=456'
174
+ #
175
+ # @example Simple arrays
176
+ # >> encode_query_string({foo: [1,2,3]})
177
+ # => 'foo[]=1&foo[]=2&foo[]=3'
178
+ #
179
+ # @example Nested hashes
180
+ # >> encode_query_string({outer: {foo: 123, bar: 456}})
181
+ # => 'outer[foo]=123&outer[bar]=456'
182
+ #
183
+ # @example Deeply nesting
184
+ # >> encode_query_string({coords: [{x: 1, y: 0}, {x: 2}, {x: 3}]})
185
+ # => 'coords[][x]=1&coords[][y]=0&coords[][x]=2&coords[][x]=3'
186
+ #
187
+ # @example Null and empty values
188
+ # >> encode_query_string({string: '', empty: nil, list: [], hash: {}})
189
+ # => 'string=&empty&list&hash'
190
+ #
191
+ # @example Nested nulls
192
+ # >> encode_query_string({foo: {string: '', empty: nil}})
193
+ # => 'foo[string]=&foo[empty]'
194
+ #
195
+ # @example Multiple fields with the same name using ParamsArray
196
+ # >> encode_query_string(RestMan::ParamsArray.new([[:foo, 1], [:foo, 2], [:foo, 3]]))
197
+ # => 'foo=1&foo=2&foo=3'
198
+ #
199
+ # @example Nested ParamsArray
200
+ # >> encode_query_string({foo: RestMan::ParamsArray.new([[:a, 1], [:a, 2]])})
201
+ # => 'foo[a]=1&foo[a]=2'
202
+ #
203
+ # >> encode_query_string(RestMan::ParamsArray.new([[:foo, {a: 1}], [:foo, {a: 2}]]))
204
+ # => 'foo[a]=1&foo[a]=2'
205
+ #
206
+ def self.encode_query_string(object)
207
+ flatten_params(object, true).map {|k, v| v.nil? ? k : "#{k}=#{v}" }.join('&')
208
+ end
209
+
210
+ # Transform deeply nested param containers into a flat array of [key,
211
+ # value] pairs.
212
+ #
213
+ # @example
214
+ # >> flatten_params({key1: {key2: 123}})
215
+ # => [["key1[key2]", 123]]
216
+ #
217
+ # @example
218
+ # >> flatten_params({key1: {key2: 123, arr: [1,2,3]}})
219
+ # => [["key1[key2]", 123], ["key1[arr][]", 1], ["key1[arr][]", 2], ["key1[arr][]", 3]]
220
+ #
221
+ # @param object [Hash, ParamsArray] The container to flatten
222
+ # @param uri_escape [Boolean] Whether to URI escape keys and values
223
+ # @param parent_key [String] Should not be passed (used for recursion)
224
+ #
225
+ def self.flatten_params(object, uri_escape=false, parent_key=nil)
226
+ unless object.is_a?(Hash) || object.is_a?(ParamsArray) ||
227
+ (parent_key && object.is_a?(Array))
228
+ raise ArgumentError.new('expected Hash or ParamsArray, got: ' + object.inspect)
229
+ end
230
+
231
+ # transform empty collections into nil, where possible
232
+ if object.empty? && parent_key
233
+ return [[parent_key, nil]]
234
+ end
235
+
236
+ # This is essentially .map(), but we need to do += for nested containers
237
+ object.reduce([]) { |result, item|
238
+ if object.is_a?(Array)
239
+ # item is already the value
240
+ k = nil
241
+ v = item
242
+ else
243
+ # item is a key, value pair
244
+ k, v = item
245
+ k = escape(k.to_s) if uri_escape
246
+ end
247
+
248
+ processed_key = parent_key ? "#{parent_key}[#{k}]" : k
249
+
250
+ case v
251
+ when Array, Hash, ParamsArray
252
+ result.concat flatten_params(v, uri_escape, processed_key)
253
+ else
254
+ v = escape(v.to_s) if uri_escape && v
255
+ result << [processed_key, v]
256
+ end
257
+ }
258
+ end
259
+
260
+ # Encode string for safe transport by URI or form encoding. This uses a CGI
261
+ # style escape, which transforms ` ` into `+` and various special
262
+ # characters into percent encoded forms.
263
+ #
264
+ # This calls URI.encode_www_form_component for the implementation. The only
265
+ # difference between this and CGI.escape is that it does not escape `*`.
266
+ # http://stackoverflow.com/questions/25085992/
267
+ #
268
+ # @see URI.encode_www_form_component
269
+ #
270
+ def self.escape(string)
271
+ URI.encode_www_form_component(string)
272
+ end
273
+ end
274
+ end
@@ -0,0 +1,8 @@
1
+ module RestMan
2
+ VERSION_INFO = [1, 0, 0].freeze
3
+ VERSION = VERSION_INFO.map(&:to_s).join('.').freeze
4
+
5
+ def self.version
6
+ VERSION
7
+ end
8
+ end
@@ -0,0 +1,105 @@
1
+ require 'openssl'
2
+ require 'ffi'
3
+
4
+ # Adapted from Puppet, Copyright (c) Puppet Labs Inc,
5
+ # licensed under the Apache License, Version 2.0.
6
+ #
7
+ # https://github.com/puppetlabs/puppet/blob/bbe30e0a/lib/puppet/util/windows/root_certs.rb
8
+
9
+ # Represents a collection of trusted root certificates.
10
+ #
11
+ # @api public
12
+ class RestMan::Windows::RootCerts
13
+ include Enumerable
14
+ extend FFI::Library
15
+
16
+ typedef :ulong, :dword
17
+ typedef :uintptr_t, :handle
18
+
19
+ def initialize(roots)
20
+ @roots = roots
21
+ end
22
+
23
+ # Enumerates each root certificate.
24
+ # @yieldparam cert [OpenSSL::X509::Certificate] each root certificate
25
+ # @api public
26
+ def each
27
+ @roots.each {|cert| yield cert}
28
+ end
29
+
30
+ # Returns a new instance.
31
+ # @return [RestMan::Windows::RootCerts] object constructed from current root certificates
32
+ def self.instance
33
+ new(self.load_certs)
34
+ end
35
+
36
+ # Returns an array of root certificates.
37
+ #
38
+ # @return [Array<[OpenSSL::X509::Certificate]>] an array of root certificates
39
+ # @api private
40
+ def self.load_certs
41
+ certs = []
42
+
43
+ # This is based on a patch submitted to openssl:
44
+ # http://www.mail-archive.com/openssl-dev@openssl.org/msg26958.html
45
+ ptr = FFI::Pointer::NULL
46
+ store = CertOpenSystemStoreA(nil, "ROOT")
47
+ begin
48
+ while (ptr = CertEnumCertificatesInStore(store, ptr)) and not ptr.null?
49
+ context = CERT_CONTEXT.new(ptr)
50
+ cert_buf = context[:pbCertEncoded].read_bytes(context[:cbCertEncoded])
51
+ begin
52
+ certs << OpenSSL::X509::Certificate.new(cert_buf)
53
+ rescue => detail
54
+ warn("Failed to import root certificate: #{detail.inspect}")
55
+ end
56
+ end
57
+ ensure
58
+ CertCloseStore(store, 0)
59
+ end
60
+
61
+ certs
62
+ end
63
+
64
+ private
65
+
66
+ # typedef ULONG_PTR HCRYPTPROV_LEGACY;
67
+ # typedef void *HCERTSTORE;
68
+
69
+ class CERT_CONTEXT < FFI::Struct
70
+ layout(
71
+ :dwCertEncodingType, :dword,
72
+ :pbCertEncoded, :pointer,
73
+ :cbCertEncoded, :dword,
74
+ :pCertInfo, :pointer,
75
+ :hCertStore, :handle
76
+ )
77
+ end
78
+
79
+ # HCERTSTORE
80
+ # WINAPI
81
+ # CertOpenSystemStoreA(
82
+ # __in_opt HCRYPTPROV_LEGACY hProv,
83
+ # __in LPCSTR szSubsystemProtocol
84
+ # );
85
+ ffi_lib :crypt32
86
+ attach_function :CertOpenSystemStoreA, [:pointer, :string], :handle
87
+
88
+ # PCCERT_CONTEXT
89
+ # WINAPI
90
+ # CertEnumCertificatesInStore(
91
+ # __in HCERTSTORE hCertStore,
92
+ # __in_opt PCCERT_CONTEXT pPrevCertContext
93
+ # );
94
+ ffi_lib :crypt32
95
+ attach_function :CertEnumCertificatesInStore, [:handle, :pointer], :pointer
96
+
97
+ # BOOL
98
+ # WINAPI
99
+ # CertCloseStore(
100
+ # __in_opt HCERTSTORE hCertStore,
101
+ # __in DWORD dwFlags
102
+ # );
103
+ ffi_lib :crypt32
104
+ attach_function :CertCloseStore, [:handle, :dword], :bool
105
+ end
@@ -0,0 +1,8 @@
1
+ module RestMan
2
+ module Windows
3
+ end
4
+ end
5
+
6
+ if RestMan::Platform.windows?
7
+ require_relative './windows/root_certs'
8
+ end