contractful 0.1.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.
- checksums.yaml +7 -0
- data/API.md +142 -0
- data/ARCHITECTURE.md +134 -0
- data/AUTHORS +106 -0
- data/CODE_OF_CONDUCT.md +128 -0
- data/LICENSE +21 -0
- data/README.md +1290 -0
- data/SECURITY.md +67 -0
- data/bin/contractful +128 -0
- data/history.md +801 -0
- data/lib/contractful/abstract_response.rb +301 -0
- data/lib/contractful/circuit_breaker.rb +221 -0
- data/lib/contractful/connection_pool.rb +166 -0
- data/lib/contractful/contract/auth.rb +257 -0
- data/lib/contractful/contract/config.rb +93 -0
- data/lib/contractful/contract/definition.rb +295 -0
- data/lib/contractful/contract/dsl.rb +501 -0
- data/lib/contractful/contract/executor.rb +492 -0
- data/lib/contractful/contract/openapi_import.rb +1135 -0
- data/lib/contractful/contract/pagination.rb +218 -0
- data/lib/contractful/contract/param.rb +505 -0
- data/lib/contractful/contract/postman_import/auth.rb +39 -0
- data/lib/contractful/contract/postman_import/body.rb +87 -0
- data/lib/contractful/contract/postman_import/operation.rb +107 -0
- data/lib/contractful/contract/postman_import/scope.rb +37 -0
- data/lib/contractful/contract/postman_import/url.rb +80 -0
- data/lib/contractful/contract/postman_import.rb +164 -0
- data/lib/contractful/contract/response_field.rb +167 -0
- data/lib/contractful/contract/sequence.rb +432 -0
- data/lib/contractful/contract/swagger2/components.rb +84 -0
- data/lib/contractful/contract/swagger2/operations.rb +81 -0
- data/lib/contractful/contract/swagger2/parameters.rb +93 -0
- data/lib/contractful/contract/swagger2/references.rb +55 -0
- data/lib/contractful/contract/swagger2.rb +55 -0
- data/lib/contractful/contract.rb +85 -0
- data/lib/contractful/exceptions.rb +328 -0
- data/lib/contractful/notifications/active_support.rb +40 -0
- data/lib/contractful/notifications.rb +138 -0
- data/lib/contractful/pacer.rb +161 -0
- data/lib/contractful/params_array.rb +85 -0
- data/lib/contractful/payload.rb +340 -0
- data/lib/contractful/platform.rb +52 -0
- data/lib/contractful/rate_limit.rb +133 -0
- data/lib/contractful/raw_response.rb +70 -0
- data/lib/contractful/request.rb +1483 -0
- data/lib/contractful/resource.rb +189 -0
- data/lib/contractful/response.rb +109 -0
- data/lib/contractful/utils.rb +202 -0
- data/lib/contractful/version.rb +11 -0
- data/lib/contractful/windows/root_certs.rb +107 -0
- data/lib/contractful/windows.rb +9 -0
- data/lib/contractful.rb +233 -0
- metadata +244 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Contractful
|
|
4
|
+
# A class that can be instantiated for access to a RESTful resource,
|
|
5
|
+
# including authentication.
|
|
6
|
+
#
|
|
7
|
+
# Example:
|
|
8
|
+
#
|
|
9
|
+
# resource = Contractful::Resource.new('http://some/resource')
|
|
10
|
+
# jpg = resource.get(:accept => 'image/jpg')
|
|
11
|
+
#
|
|
12
|
+
# With HTTP basic authentication:
|
|
13
|
+
#
|
|
14
|
+
# resource = Contractful::Resource.new('http://protected/resource', :user => 'user', :password => 'password')
|
|
15
|
+
# resource.delete
|
|
16
|
+
#
|
|
17
|
+
# With a timeout (seconds):
|
|
18
|
+
#
|
|
19
|
+
# Contractful::Resource.new('http://slow', :read_timeout => 10)
|
|
20
|
+
#
|
|
21
|
+
# With an open timeout (seconds):
|
|
22
|
+
#
|
|
23
|
+
# Contractful::Resource.new('http://behindfirewall', :open_timeout => 10)
|
|
24
|
+
#
|
|
25
|
+
# You can also use resources to share common headers. For headers keys,
|
|
26
|
+
# symbols are converted to strings. Example:
|
|
27
|
+
#
|
|
28
|
+
# resource = Contractful::Resource.new('http://some/resource', :headers => { :client_version => 1 })
|
|
29
|
+
#
|
|
30
|
+
# This header will be transported as Client-Version (symbol keys are split
|
|
31
|
+
# on underscores and capitalized; no X- prefix is added)
|
|
32
|
+
#
|
|
33
|
+
# Use the [] syntax to allocate subresources:
|
|
34
|
+
#
|
|
35
|
+
# site = Contractful::Resource.new('http://example.com', :user => 'adam', :password => 'mypasswd')
|
|
36
|
+
# site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
|
|
37
|
+
#
|
|
38
|
+
class Resource
|
|
39
|
+
attr_reader :url, :options, :block
|
|
40
|
+
|
|
41
|
+
def initialize(url, options = {}, backwards_compatibility = nil, &block)
|
|
42
|
+
@url = url
|
|
43
|
+
@block = block
|
|
44
|
+
# compatibility with previous versions
|
|
45
|
+
@options = if options.instance_of?(Hash)
|
|
46
|
+
options
|
|
47
|
+
else
|
|
48
|
+
{ user: options, password: backwards_compatibility }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def get(additional_headers = {}, &block)
|
|
53
|
+
headers = (options[:headers] || {}).merge(additional_headers)
|
|
54
|
+
Request.execute(options.merge(
|
|
55
|
+
method: :get,
|
|
56
|
+
url: url,
|
|
57
|
+
headers: headers,
|
|
58
|
+
log: log
|
|
59
|
+
), &block || @block)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def head(additional_headers = {}, &block)
|
|
63
|
+
headers = (options[:headers] || {}).merge(additional_headers)
|
|
64
|
+
Request.execute(options.merge(
|
|
65
|
+
method: :head,
|
|
66
|
+
url: url,
|
|
67
|
+
headers: headers,
|
|
68
|
+
log: log
|
|
69
|
+
), &block || @block)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def post(payload, additional_headers = {}, &block)
|
|
73
|
+
headers = (options[:headers] || {}).merge(additional_headers)
|
|
74
|
+
Request.execute(options.merge(
|
|
75
|
+
method: :post,
|
|
76
|
+
url: url,
|
|
77
|
+
payload: payload,
|
|
78
|
+
headers: headers,
|
|
79
|
+
log: log
|
|
80
|
+
), &block || @block)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def put(payload, additional_headers = {}, &block)
|
|
84
|
+
headers = (options[:headers] || {}).merge(additional_headers)
|
|
85
|
+
Request.execute(options.merge(
|
|
86
|
+
method: :put,
|
|
87
|
+
url: url,
|
|
88
|
+
payload: payload,
|
|
89
|
+
headers: headers,
|
|
90
|
+
log: log
|
|
91
|
+
), &block || @block)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def patch(payload, additional_headers = {}, &block)
|
|
95
|
+
headers = (options[:headers] || {}).merge(additional_headers)
|
|
96
|
+
Request.execute(options.merge(
|
|
97
|
+
method: :patch,
|
|
98
|
+
url: url,
|
|
99
|
+
payload: payload,
|
|
100
|
+
headers: headers,
|
|
101
|
+
log: log
|
|
102
|
+
), &block || @block)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def delete(additional_headers = {}, &block)
|
|
106
|
+
headers = (options[:headers] || {}).merge(additional_headers)
|
|
107
|
+
Request.execute(options.merge(
|
|
108
|
+
method: :delete,
|
|
109
|
+
url: url,
|
|
110
|
+
headers: headers,
|
|
111
|
+
log: log
|
|
112
|
+
), &block || @block)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def to_s
|
|
116
|
+
url
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def user
|
|
120
|
+
options[:user]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def password
|
|
124
|
+
options[:password]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def headers
|
|
128
|
+
options[:headers] || {}
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def read_timeout
|
|
132
|
+
options[:read_timeout]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def open_timeout
|
|
136
|
+
options[:open_timeout]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def log
|
|
140
|
+
options[:log] || Contractful.log
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Construct a subresource, preserving authentication.
|
|
144
|
+
#
|
|
145
|
+
# Example:
|
|
146
|
+
#
|
|
147
|
+
# site = Contractful::Resource.new('http://example.com', 'adam', 'mypasswd')
|
|
148
|
+
# site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
|
|
149
|
+
#
|
|
150
|
+
# This is especially useful if you wish to define your site in one place and
|
|
151
|
+
# call it in multiple locations:
|
|
152
|
+
#
|
|
153
|
+
# def orders
|
|
154
|
+
# Contractful::Resource.new('http://example.com/orders', 'admin', 'mypasswd')
|
|
155
|
+
# end
|
|
156
|
+
#
|
|
157
|
+
# orders.get # GET http://example.com/orders
|
|
158
|
+
# orders['1'].get # GET http://example.com/orders/1
|
|
159
|
+
# orders['1/items'].delete # DELETE http://example.com/orders/1/items
|
|
160
|
+
#
|
|
161
|
+
# Nest resources as far as you want:
|
|
162
|
+
#
|
|
163
|
+
# site = Contractful::Resource.new('http://example.com')
|
|
164
|
+
# posts = site['posts']
|
|
165
|
+
# first_post = posts['1']
|
|
166
|
+
# comments = first_post['comments']
|
|
167
|
+
# comments.post 'Hello', :content_type => 'text/plain'
|
|
168
|
+
#
|
|
169
|
+
def [](suburl, &new_block)
|
|
170
|
+
if block_given?
|
|
171
|
+
self.class.new(concat_urls(url, suburl), options, &new_block)
|
|
172
|
+
elsif block
|
|
173
|
+
self.class.new(concat_urls(url, suburl), options, &block)
|
|
174
|
+
else
|
|
175
|
+
self.class.new(concat_urls(url, suburl), options)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def concat_urls(url, suburl) # :nodoc:
|
|
180
|
+
url = url.to_s
|
|
181
|
+
suburl = suburl.to_s
|
|
182
|
+
if url.slice(-1, 1) == '/' or suburl.slice(0, 1) == '/'
|
|
183
|
+
url + suburl
|
|
184
|
+
else
|
|
185
|
+
"#{url}/#{suburl}"
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Contractful
|
|
4
|
+
# A Response from Contractful, you can access the response body, the code or the headers.
|
|
5
|
+
#
|
|
6
|
+
class Response < String
|
|
7
|
+
include AbstractResponse
|
|
8
|
+
|
|
9
|
+
# Return the HTTP response body.
|
|
10
|
+
#
|
|
11
|
+
# Future versions of Contractful 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
|
+
# Parse the response body as JSON and return the result.
|
|
38
|
+
#
|
|
39
|
+
# This is a convenience wrapper around `JSON.parse(body)`. Any keyword
|
|
40
|
+
# arguments are forwarded to `JSON.parse`, so for example:
|
|
41
|
+
#
|
|
42
|
+
# response.json # => {"a" => 1}
|
|
43
|
+
# response.json(symbolize_names: true) # => {a: 1}
|
|
44
|
+
#
|
|
45
|
+
# Raises `JSON::ParserError` if the body is not valid JSON. Note that the
|
|
46
|
+
# return value is a plain parsed data structure, not a Response object.
|
|
47
|
+
#
|
|
48
|
+
# @param kwargs [Hash] keyword arguments forwarded to JSON.parse
|
|
49
|
+
#
|
|
50
|
+
# @return [Object] the parsed JSON data structure (typically Hash or Array)
|
|
51
|
+
#
|
|
52
|
+
def json(**kwargs)
|
|
53
|
+
JSON.parse(to_s, **kwargs)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def inspect
|
|
57
|
+
"<Contractful::Response #{code.inspect} #{body_truncated(10).inspect}>"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Initialize a Response object. Because Contractful::Response is
|
|
61
|
+
# (unfortunately) a subclass of String for historical reasons,
|
|
62
|
+
# Response.create is the preferred initializer.
|
|
63
|
+
#
|
|
64
|
+
# @param [String, nil] body The response body from the Net::HTTPResponse
|
|
65
|
+
# @param [Net::HTTPResponse] net_http_res
|
|
66
|
+
# @param [Contractful::Request] request
|
|
67
|
+
# @param [Time] start_time
|
|
68
|
+
def self.create(body, net_http_res, request, start_time = nil)
|
|
69
|
+
result = new(body || '')
|
|
70
|
+
|
|
71
|
+
result.response_set_vars(net_http_res, request, start_time)
|
|
72
|
+
fix_encoding(result)
|
|
73
|
+
|
|
74
|
+
result
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Set the String encoding according to the 'Content-Type: charset' header,
|
|
78
|
+
# if possible.
|
|
79
|
+
def self.fix_encoding(response)
|
|
80
|
+
charset = Contractful::Utils.get_encoding_from_headers(response.headers)
|
|
81
|
+
encoding = nil
|
|
82
|
+
|
|
83
|
+
begin
|
|
84
|
+
encoding = Encoding.find(charset) if charset
|
|
85
|
+
rescue ArgumentError
|
|
86
|
+
response.log << "No such encoding: #{charset.inspect}" if response.log
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
return unless encoding
|
|
90
|
+
|
|
91
|
+
response.force_encoding(encoding)
|
|
92
|
+
|
|
93
|
+
response
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def body_truncated(length)
|
|
99
|
+
b = body
|
|
100
|
+
if b.length > length
|
|
101
|
+
# b[0..length] is inclusive, returning length+1 chars before the
|
|
102
|
+
# ellipsis — use b[0, length] for an exact cut.
|
|
103
|
+
"#{b[0, length]}..."
|
|
104
|
+
else
|
|
105
|
+
b
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'http/accept'
|
|
4
|
+
|
|
5
|
+
module Contractful
|
|
6
|
+
# Various utility methods
|
|
7
|
+
module Utils
|
|
8
|
+
# Return encoding from an HTTP header hash.
|
|
9
|
+
#
|
|
10
|
+
# We use the RFC 7231 specification and do not impose a default encoding on
|
|
11
|
+
# text. This differs from the older RFC 2616 behavior, which specifies
|
|
12
|
+
# using ISO-8859-1 for text/* content types without a charset.
|
|
13
|
+
#
|
|
14
|
+
# Strings will use the default encoding when this method returns nil. This
|
|
15
|
+
# default is likely to be UTF-8 for Ruby >= 2.0
|
|
16
|
+
#
|
|
17
|
+
# @param headers [Hash<Symbol,String>]
|
|
18
|
+
#
|
|
19
|
+
# @return [String, nil] Return the string encoding or nil if no header is
|
|
20
|
+
# found.
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# >> get_encoding_from_headers({:content_type => 'text/plain; charset=UTF-8'})
|
|
24
|
+
# => "UTF-8"
|
|
25
|
+
#
|
|
26
|
+
def self.get_encoding_from_headers(headers)
|
|
27
|
+
type_header = headers[:content_type]
|
|
28
|
+
return nil unless type_header
|
|
29
|
+
|
|
30
|
+
begin
|
|
31
|
+
_content_type, params = cgi_parse_header(type_header)
|
|
32
|
+
rescue HTTP::Accept::ParseError
|
|
33
|
+
nil
|
|
34
|
+
else
|
|
35
|
+
params['charset']
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Parse a Content-Type like header.
|
|
40
|
+
#
|
|
41
|
+
# Return the main content-type and a hash of params.
|
|
42
|
+
#
|
|
43
|
+
# @param [String] line
|
|
44
|
+
# @return [Array(String, Hash)]
|
|
45
|
+
#
|
|
46
|
+
def self.cgi_parse_header(line)
|
|
47
|
+
types = HTTP::Accept::MediaTypes.parse(line)
|
|
48
|
+
|
|
49
|
+
raise HTTP::Accept::ParseError.new('Found no types in header line') if types.empty?
|
|
50
|
+
|
|
51
|
+
[types.first.mime_type, types.first.parameters]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Serialize a ruby object into HTTP query string parameters.
|
|
55
|
+
#
|
|
56
|
+
# There is no standard for doing this, so we choose our own slightly
|
|
57
|
+
# idiosyncratic format. The output closely matches the format understood by
|
|
58
|
+
# Rails, Rack, and PHP.
|
|
59
|
+
#
|
|
60
|
+
# If you don't want handling of complex objects and only want to handle
|
|
61
|
+
# simple flat hashes, you may want to use `URI.encode_www_form` instead,
|
|
62
|
+
# which implements HTML5-compliant URL encoded form data.
|
|
63
|
+
#
|
|
64
|
+
# @param [Hash,ParamsArray] object The object to serialize
|
|
65
|
+
#
|
|
66
|
+
# @return [String] A string appropriate for use as an HTTP query string
|
|
67
|
+
#
|
|
68
|
+
# @see flatten_params
|
|
69
|
+
#
|
|
70
|
+
# @see URI.encode_www_form
|
|
71
|
+
#
|
|
72
|
+
# @see See also Object#to_query in ActiveSupport
|
|
73
|
+
# @see http://php.net/manual/en/function.http-build-query.php
|
|
74
|
+
# http_build_query in PHP
|
|
75
|
+
# @see See also Rack::Utils.build_nested_query in Rack
|
|
76
|
+
#
|
|
77
|
+
# Notable differences from the ActiveSupport implementation:
|
|
78
|
+
#
|
|
79
|
+
# - Empty hash and empty array are treated the same as nil instead of being
|
|
80
|
+
# omitted entirely from the output. Rather than disappearing, they will
|
|
81
|
+
# appear to be nil instead.
|
|
82
|
+
#
|
|
83
|
+
# It's most common to pass a Hash as the object to serialize, but you can
|
|
84
|
+
# also use a ParamsArray if you want to be able to pass the same key with
|
|
85
|
+
# multiple values and not use the rack/rails array convention.
|
|
86
|
+
#
|
|
87
|
+
# @since 2.0.0
|
|
88
|
+
#
|
|
89
|
+
# @example Simple hashes
|
|
90
|
+
# >> encode_query_string({foo: 123, bar: 456})
|
|
91
|
+
# => 'foo=123&bar=456'
|
|
92
|
+
#
|
|
93
|
+
# @example Simple arrays
|
|
94
|
+
# >> encode_query_string({foo: [1,2,3]})
|
|
95
|
+
# => 'foo[]=1&foo[]=2&foo[]=3'
|
|
96
|
+
#
|
|
97
|
+
# @example Nested hashes
|
|
98
|
+
# >> encode_query_string({outer: {foo: 123, bar: 456}})
|
|
99
|
+
# => 'outer[foo]=123&outer[bar]=456'
|
|
100
|
+
#
|
|
101
|
+
# @example Deeply nesting
|
|
102
|
+
# >> encode_query_string({coords: [{x: 1, y: 0}, {x: 2}, {x: 3}]})
|
|
103
|
+
# => 'coords[][x]=1&coords[][y]=0&coords[][x]=2&coords[][x]=3'
|
|
104
|
+
#
|
|
105
|
+
# @example Null and empty values
|
|
106
|
+
# >> encode_query_string({string: '', empty: nil, list: [], hash: {}})
|
|
107
|
+
# => 'string=&empty&list&hash'
|
|
108
|
+
#
|
|
109
|
+
# @example Nested nulls
|
|
110
|
+
# >> encode_query_string({foo: {string: '', empty: nil}})
|
|
111
|
+
# => 'foo[string]=&foo[empty]'
|
|
112
|
+
#
|
|
113
|
+
# @example Multiple fields with the same name using ParamsArray
|
|
114
|
+
# >> encode_query_string(Contractful::ParamsArray.new([[:foo, 1], [:foo, 2], [:foo, 3]]))
|
|
115
|
+
# => 'foo=1&foo=2&foo=3'
|
|
116
|
+
#
|
|
117
|
+
# @example Nested ParamsArray
|
|
118
|
+
# >> encode_query_string({foo: Contractful::ParamsArray.new([[:a, 1], [:a, 2]])})
|
|
119
|
+
# => 'foo[a]=1&foo[a]=2'
|
|
120
|
+
#
|
|
121
|
+
# >> encode_query_string(Contractful::ParamsArray.new([[:foo, {a: 1}], [:foo, {a: 2}]]))
|
|
122
|
+
# => 'foo[a]=1&foo[a]=2'
|
|
123
|
+
#
|
|
124
|
+
def self.encode_query_string(object)
|
|
125
|
+
flatten_params(object, true).map { |k, v| v.nil? ? k : "#{k}=#{v}" }.join('&')
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Transform deeply nested param containers into a flat array of [key,
|
|
129
|
+
# value] pairs.
|
|
130
|
+
#
|
|
131
|
+
# @example
|
|
132
|
+
# >> flatten_params({key1: {key2: 123}})
|
|
133
|
+
# => [["key1[key2]", 123]]
|
|
134
|
+
#
|
|
135
|
+
# @example
|
|
136
|
+
# >> flatten_params({key1: {key2: 123, arr: [1,2,3]}})
|
|
137
|
+
# => [["key1[key2]", 123], ["key1[arr][]", 1], ["key1[arr][]", 2], ["key1[arr][]", 3]]
|
|
138
|
+
#
|
|
139
|
+
# @param object [Hash, ParamsArray] The container to flatten
|
|
140
|
+
# @param uri_escape [Boolean] Whether to URI escape keys and values
|
|
141
|
+
# @param parent_key [String] Should not be passed (used for recursion)
|
|
142
|
+
# @param depth [Integer] Should not be passed (used for recursion).
|
|
143
|
+
# Hard cap at MAX_FLATTEN_DEPTH to prevent SystemStackError on
|
|
144
|
+
# cyclically-referenced or adversarially-nested input.
|
|
145
|
+
#
|
|
146
|
+
MAX_FLATTEN_DEPTH = 100
|
|
147
|
+
|
|
148
|
+
def self.flatten_params(object, uri_escape = false, parent_key = nil, depth = 0)
|
|
149
|
+
unless object.is_a?(Hash) || object.is_a?(ParamsArray) ||
|
|
150
|
+
(parent_key && object.is_a?(Array))
|
|
151
|
+
raise ArgumentError.new('expected Hash or ParamsArray, got: ' + object.inspect)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
if depth > MAX_FLATTEN_DEPTH
|
|
155
|
+
raise ArgumentError,
|
|
156
|
+
"flatten_params nested deeper than #{MAX_FLATTEN_DEPTH} levels " \
|
|
157
|
+
'(reject input, or raise MAX_FLATTEN_DEPTH if you have a ' \
|
|
158
|
+
'legitimate use case)'
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# transform empty collections into nil, where possible
|
|
162
|
+
return [[parent_key, nil]] if object.empty? && parent_key
|
|
163
|
+
|
|
164
|
+
# This is essentially .map(), but we need to do += for nested containers
|
|
165
|
+
object.reduce([]) do |result, item|
|
|
166
|
+
if object.is_a?(Array)
|
|
167
|
+
# item is already the value
|
|
168
|
+
k = nil
|
|
169
|
+
v = item
|
|
170
|
+
else
|
|
171
|
+
# item is a key, value pair
|
|
172
|
+
k, v = item
|
|
173
|
+
k = escape(k.to_s) if uri_escape
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
processed_key = parent_key ? "#{parent_key}[#{k}]" : k
|
|
177
|
+
|
|
178
|
+
case v
|
|
179
|
+
when Array, Hash, ParamsArray
|
|
180
|
+
result.concat flatten_params(v, uri_escape, processed_key, depth + 1)
|
|
181
|
+
else
|
|
182
|
+
v = escape(v.to_s) if uri_escape && v
|
|
183
|
+
result << [processed_key, v]
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Encode string for safe transport by URI or form encoding. This uses a CGI
|
|
189
|
+
# style escape, which transforms ` ` into `+` and various special
|
|
190
|
+
# characters into percent encoded forms.
|
|
191
|
+
#
|
|
192
|
+
# This calls URI.encode_www_form_component for the implementation. The only
|
|
193
|
+
# difference between this and CGI.escape is that it does not escape `*`.
|
|
194
|
+
# http://stackoverflow.com/questions/25085992/
|
|
195
|
+
#
|
|
196
|
+
# @see URI.encode_www_form_component
|
|
197
|
+
#
|
|
198
|
+
def self.escape(string)
|
|
199
|
+
URI.encode_www_form_component(string)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
require 'ffi'
|
|
5
|
+
|
|
6
|
+
# Adapted from Puppet, Copyright (c) Puppet Labs Inc,
|
|
7
|
+
# licensed under the Apache License, Version 2.0.
|
|
8
|
+
#
|
|
9
|
+
# https://github.com/puppetlabs/puppet/blob/bbe30e0a/lib/puppet/util/windows/root_certs.rb
|
|
10
|
+
|
|
11
|
+
# Represents a collection of trusted root certificates.
|
|
12
|
+
#
|
|
13
|
+
# @api public
|
|
14
|
+
class Contractful::Windows::RootCerts
|
|
15
|
+
include Enumerable
|
|
16
|
+
extend FFI::Library
|
|
17
|
+
|
|
18
|
+
typedef :ulong, :dword
|
|
19
|
+
typedef :uintptr_t, :handle
|
|
20
|
+
|
|
21
|
+
def initialize(roots)
|
|
22
|
+
@roots = roots
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Enumerates each root certificate.
|
|
26
|
+
# @yieldparam cert [OpenSSL::X509::Certificate] each root certificate
|
|
27
|
+
# @api public
|
|
28
|
+
def each(&block)
|
|
29
|
+
@roots.each(&block)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns a new instance.
|
|
33
|
+
# @return [Contractful::Windows::RootCerts] object constructed from current root certificates
|
|
34
|
+
def self.instance
|
|
35
|
+
new(load_certs)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Returns an array of root certificates.
|
|
39
|
+
#
|
|
40
|
+
# @return [Array<[OpenSSL::X509::Certificate]>] an array of root certificates
|
|
41
|
+
# @api private
|
|
42
|
+
def self.load_certs
|
|
43
|
+
certs = []
|
|
44
|
+
|
|
45
|
+
# This is based on a patch submitted to openssl:
|
|
46
|
+
# http://www.mail-archive.com/openssl-dev@openssl.org/msg26958.html
|
|
47
|
+
ptr = FFI::Pointer::NULL
|
|
48
|
+
store = CertOpenSystemStoreA(nil, 'ROOT')
|
|
49
|
+
begin
|
|
50
|
+
while (ptr = CertEnumCertificatesInStore(store, ptr)) and !ptr.null?
|
|
51
|
+
context = CertContext.new(ptr)
|
|
52
|
+
cert_buf = context[:pbCertEncoded].read_bytes(context[:cbCertEncoded])
|
|
53
|
+
begin
|
|
54
|
+
certs << OpenSSL::X509::Certificate.new(cert_buf)
|
|
55
|
+
rescue StandardError => e
|
|
56
|
+
warn("Failed to import root certificate: #{e.inspect}")
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
ensure
|
|
60
|
+
CertCloseStore(store, 0)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
certs
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# typedef ULONG_PTR HCRYPTPROV_LEGACY;
|
|
67
|
+
# typedef void *HCERTSTORE;
|
|
68
|
+
|
|
69
|
+
# FFI mapping of the Windows CERT_CONTEXT structure returned by
|
|
70
|
+
# CertEnumCertificatesInStore.
|
|
71
|
+
class CertContext < FFI::Struct
|
|
72
|
+
layout(
|
|
73
|
+
:dwCertEncodingType, :dword,
|
|
74
|
+
:pbCertEncoded, :pointer,
|
|
75
|
+
:cbCertEncoded, :dword,
|
|
76
|
+
:pCertInfo, :pointer,
|
|
77
|
+
:hCertStore, :handle
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# HCERTSTORE
|
|
82
|
+
# WINAPI
|
|
83
|
+
# CertOpenSystemStoreA(
|
|
84
|
+
# __in_opt HCRYPTPROV_LEGACY hProv,
|
|
85
|
+
# __in LPCSTR szSubsystemProtocol
|
|
86
|
+
# );
|
|
87
|
+
ffi_lib :crypt32
|
|
88
|
+
attach_function :CertOpenSystemStoreA, %i[pointer string], :handle
|
|
89
|
+
|
|
90
|
+
# PCCERT_CONTEXT
|
|
91
|
+
# WINAPI
|
|
92
|
+
# CertEnumCertificatesInStore(
|
|
93
|
+
# __in HCERTSTORE hCertStore,
|
|
94
|
+
# __in_opt PCCERT_CONTEXT pPrevCertContext
|
|
95
|
+
# );
|
|
96
|
+
ffi_lib :crypt32
|
|
97
|
+
attach_function :CertEnumCertificatesInStore, %i[handle pointer], :pointer
|
|
98
|
+
|
|
99
|
+
# BOOL
|
|
100
|
+
# WINAPI
|
|
101
|
+
# CertCloseStore(
|
|
102
|
+
# __in_opt HCERTSTORE hCertStore,
|
|
103
|
+
# __in DWORD dwFlags
|
|
104
|
+
# );
|
|
105
|
+
ffi_lib :crypt32
|
|
106
|
+
attach_function :CertCloseStore, %i[handle dword], :bool
|
|
107
|
+
end
|