vault-kv 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +42 -0
- data/.rspec +2 -0
- data/.travis.yml +29 -0
- data/CHANGELOG.md +228 -0
- data/Gemfile +3 -0
- data/LICENSE +362 -0
- data/README.md +212 -0
- data/Rakefile +6 -0
- data/lib/vault.rb +49 -0
- data/lib/vault/api.rb +13 -0
- data/lib/vault/api/approle.rb +218 -0
- data/lib/vault/api/auth.rb +316 -0
- data/lib/vault/api/auth_tls.rb +92 -0
- data/lib/vault/api/auth_token.rb +242 -0
- data/lib/vault/api/help.rb +33 -0
- data/lib/vault/api/kv.rb +207 -0
- data/lib/vault/api/logical.rb +150 -0
- data/lib/vault/api/secret.rb +168 -0
- data/lib/vault/api/sys.rb +25 -0
- data/lib/vault/api/sys/audit.rb +91 -0
- data/lib/vault/api/sys/auth.rb +116 -0
- data/lib/vault/api/sys/health.rb +63 -0
- data/lib/vault/api/sys/init.rb +83 -0
- data/lib/vault/api/sys/leader.rb +48 -0
- data/lib/vault/api/sys/lease.rb +49 -0
- data/lib/vault/api/sys/mount.rb +103 -0
- data/lib/vault/api/sys/policy.rb +92 -0
- data/lib/vault/api/sys/seal.rb +81 -0
- data/lib/vault/client.rb +447 -0
- data/lib/vault/configurable.rb +48 -0
- data/lib/vault/defaults.rb +197 -0
- data/lib/vault/encode.rb +19 -0
- data/lib/vault/errors.rb +72 -0
- data/lib/vault/persistent.rb +1158 -0
- data/lib/vault/persistent/connection.rb +42 -0
- data/lib/vault/persistent/pool.rb +48 -0
- data/lib/vault/persistent/timed_stack_multi.rb +70 -0
- data/lib/vault/request.rb +43 -0
- data/lib/vault/response.rb +89 -0
- data/lib/vault/vendor/connection_pool.rb +150 -0
- data/lib/vault/vendor/connection_pool/timed_stack.rb +178 -0
- data/lib/vault/vendor/connection_pool/version.rb +5 -0
- data/lib/vault/version.rb +3 -0
- data/vault.gemspec +30 -0
- metadata +186 -0
@@ -0,0 +1,103 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module Vault
|
4
|
+
class Mount < Response
|
5
|
+
# @!attribute [r] config
|
6
|
+
# Arbitrary configuration for the backend.
|
7
|
+
# @return [Hash<Symbol, Object>]
|
8
|
+
field :config
|
9
|
+
|
10
|
+
# @!attribute [r] description
|
11
|
+
# Description of the mount.
|
12
|
+
# @return [String]
|
13
|
+
field :description
|
14
|
+
|
15
|
+
# @!attribute [r] type
|
16
|
+
# Type of the mount.
|
17
|
+
# @return [String]
|
18
|
+
field :type
|
19
|
+
end
|
20
|
+
|
21
|
+
class Sys < Request
|
22
|
+
# List all mounts in the vault.
|
23
|
+
#
|
24
|
+
# @example
|
25
|
+
# Vault.sys.mounts #=> { :secret => #<struct Vault::Mount type="generic", description="generic secret storage"> }
|
26
|
+
#
|
27
|
+
# @return [Hash<Symbol, Mount>]
|
28
|
+
def mounts
|
29
|
+
json = client.get("/v1/sys/mounts")
|
30
|
+
json = json[:data] if json[:data]
|
31
|
+
return Hash[*json.map do |k,v|
|
32
|
+
[k.to_s.chomp("/").to_sym, Mount.decode(v)]
|
33
|
+
end.flatten]
|
34
|
+
end
|
35
|
+
|
36
|
+
# Create a mount at the given path.
|
37
|
+
#
|
38
|
+
# @example
|
39
|
+
# Vault.sys.mount("pg", "postgresql", "Postgres user management") #=> true
|
40
|
+
#
|
41
|
+
# @param [String] path
|
42
|
+
# the path to mount at
|
43
|
+
# @param [String] type
|
44
|
+
# the type of mount
|
45
|
+
# @param [String] description
|
46
|
+
# a human-friendly description (optional)
|
47
|
+
def mount(path, type, description = nil, options = {})
|
48
|
+
payload = options.merge type: type
|
49
|
+
payload[:description] = description if !description.nil?
|
50
|
+
|
51
|
+
client.post("/v1/sys/mounts/#{encode_path(path)}", JSON.fast_generate(payload))
|
52
|
+
return true
|
53
|
+
end
|
54
|
+
|
55
|
+
# Tune a mount at the given path.
|
56
|
+
#
|
57
|
+
# @example
|
58
|
+
# Vault.sys.mount_tune("pki", max_lease_ttl: '87600h') #=> true
|
59
|
+
#
|
60
|
+
# @param [String] path
|
61
|
+
# the path to write
|
62
|
+
# @param [Hash] data
|
63
|
+
# the data to write
|
64
|
+
def mount_tune(path, data = {})
|
65
|
+
json = client.post("/v1/sys/mounts/#{encode_path(path)}/tune", JSON.fast_generate(data))
|
66
|
+
return true
|
67
|
+
end
|
68
|
+
|
69
|
+
# Unmount the thing at the given path. If the mount does not exist, an error
|
70
|
+
# will be raised.
|
71
|
+
#
|
72
|
+
# @example
|
73
|
+
# Vault.sys.unmount("pg") #=> true
|
74
|
+
#
|
75
|
+
# @param [String] path
|
76
|
+
# the path to unmount
|
77
|
+
#
|
78
|
+
# @return [true]
|
79
|
+
def unmount(path)
|
80
|
+
client.delete("/v1/sys/mounts/#{encode_path(path)}")
|
81
|
+
return true
|
82
|
+
end
|
83
|
+
|
84
|
+
# Change the name of the mount
|
85
|
+
#
|
86
|
+
# @example
|
87
|
+
# Vault.sys.remount("pg", "postgres") #=> true
|
88
|
+
#
|
89
|
+
# @param [String] from
|
90
|
+
# the origin mount path
|
91
|
+
# @param [String] to
|
92
|
+
# the new mount path
|
93
|
+
#
|
94
|
+
# @return [true]
|
95
|
+
def remount(from, to)
|
96
|
+
client.post("/v1/sys/remount", JSON.fast_generate(
|
97
|
+
from: from,
|
98
|
+
to: to,
|
99
|
+
))
|
100
|
+
return true
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module Vault
|
4
|
+
class Policy < Response
|
5
|
+
# @!attribute [r] name
|
6
|
+
# Name of the policy.
|
7
|
+
#
|
8
|
+
# @example Get the name of the policy
|
9
|
+
# policy.name #=> "default"
|
10
|
+
#
|
11
|
+
# @return [String]
|
12
|
+
field :name
|
13
|
+
|
14
|
+
# @!attribute [r] rules
|
15
|
+
# Raw HCL policy.
|
16
|
+
#
|
17
|
+
# @example Display the list of rules
|
18
|
+
# policy.rules #=> "path \"secret/foo\" {}"
|
19
|
+
#
|
20
|
+
# @return [String]
|
21
|
+
field :rules
|
22
|
+
end
|
23
|
+
|
24
|
+
class Sys
|
25
|
+
# The list of policies in vault.
|
26
|
+
#
|
27
|
+
# @example
|
28
|
+
# Vault.sys.policies #=> ["root"]
|
29
|
+
#
|
30
|
+
# @return [Array<String>]
|
31
|
+
def policies
|
32
|
+
client.get("/v1/sys/policy")[:policies]
|
33
|
+
end
|
34
|
+
|
35
|
+
# Get the policy by the given name. If a policy does not exist by that name,
|
36
|
+
# +nil+ is returned.
|
37
|
+
#
|
38
|
+
# @example
|
39
|
+
# Vault.sys.policy("root") #=> #<Vault::Policy rules="">
|
40
|
+
#
|
41
|
+
# @return [Policy, nil]
|
42
|
+
def policy(name)
|
43
|
+
json = client.get("/v1/sys/policy/#{encode_path(name)}")
|
44
|
+
return Policy.decode(json)
|
45
|
+
rescue HTTPError => e
|
46
|
+
return nil if e.code == 404
|
47
|
+
raise
|
48
|
+
end
|
49
|
+
|
50
|
+
# Create a new policy with the given name and rules.
|
51
|
+
#
|
52
|
+
# @example
|
53
|
+
# policy = <<-EOH
|
54
|
+
# path "sys" {
|
55
|
+
# policy = "deny"
|
56
|
+
# }
|
57
|
+
# EOH
|
58
|
+
# Vault.sys.put_policy("dev", policy) #=> true
|
59
|
+
#
|
60
|
+
# It is recommend that you load policy rules from a file:
|
61
|
+
#
|
62
|
+
# @example
|
63
|
+
# policy = File.read("/path/to/my/policy.hcl")
|
64
|
+
# Vault.sys.put_policy("dev", policy)
|
65
|
+
#
|
66
|
+
# @param [String] name
|
67
|
+
# the name of the policy
|
68
|
+
# @param [String] rules
|
69
|
+
# the policy rules
|
70
|
+
#
|
71
|
+
# @return [true]
|
72
|
+
def put_policy(name, rules)
|
73
|
+
client.put("/v1/sys/policy/#{encode_path(name)}", JSON.fast_generate(
|
74
|
+
rules: rules,
|
75
|
+
))
|
76
|
+
return true
|
77
|
+
end
|
78
|
+
|
79
|
+
# Delete the policy with the given name. If a policy does not exist, vault
|
80
|
+
# will not return an error.
|
81
|
+
#
|
82
|
+
# @example
|
83
|
+
# Vault.sys.delete_policy("dev") #=> true
|
84
|
+
#
|
85
|
+
# @param [String] name
|
86
|
+
# the name of the policy
|
87
|
+
def delete_policy(name)
|
88
|
+
client.delete("/v1/sys/policy/#{encode_path(name)}")
|
89
|
+
return true
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module Vault
|
4
|
+
class SealStatus < Response
|
5
|
+
# @!method sealed?
|
6
|
+
# Returns if the Vault is sealed.
|
7
|
+
#
|
8
|
+
# @example Check if the Vault is sealed
|
9
|
+
# status.sealed? #=> true
|
10
|
+
#
|
11
|
+
# @return [Boolean]
|
12
|
+
field :sealed, as: :sealed?
|
13
|
+
|
14
|
+
# @!attribute t
|
15
|
+
# Threshold of keys required to unseal the Vault.
|
16
|
+
#
|
17
|
+
# @example Get the threshold of keys
|
18
|
+
# status.t #=> 3
|
19
|
+
#
|
20
|
+
# @return [Fixnum]
|
21
|
+
field :t
|
22
|
+
|
23
|
+
# @!attribute n
|
24
|
+
# Total number of unseal keys.
|
25
|
+
#
|
26
|
+
# @example Get the total number of keys
|
27
|
+
# status.n #=> 5
|
28
|
+
#
|
29
|
+
# @return [Fixnum]
|
30
|
+
field :n
|
31
|
+
|
32
|
+
# @!attribute progress
|
33
|
+
# Number of keys that have been entered.
|
34
|
+
#
|
35
|
+
# @example Get the current unseal progress
|
36
|
+
# status.progress #=> 2
|
37
|
+
#
|
38
|
+
# @return [Fixnum]
|
39
|
+
field :progress
|
40
|
+
end
|
41
|
+
|
42
|
+
class Sys
|
43
|
+
# Get the current seal status.
|
44
|
+
#
|
45
|
+
# @example
|
46
|
+
# Vault.sys.seal_status #=> #<Vault::SealStatus sealed=false, t=1, n=1, progress=0>
|
47
|
+
#
|
48
|
+
# @return [SealStatus]
|
49
|
+
def seal_status
|
50
|
+
json = client.get("/v1/sys/seal-status")
|
51
|
+
return SealStatus.decode(json)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Seal the vault. Warning: this will seal the vault!
|
55
|
+
#
|
56
|
+
# @example
|
57
|
+
# Vault.sys.seal #=> true
|
58
|
+
#
|
59
|
+
# @return [true]
|
60
|
+
def seal
|
61
|
+
client.put("/v1/sys/seal", nil)
|
62
|
+
return true
|
63
|
+
end
|
64
|
+
|
65
|
+
# Unseal the vault with the given shard.
|
66
|
+
#
|
67
|
+
# @example
|
68
|
+
# Vault.sys.unseal("abcd-1234") #=> #<Vault::SealStatus sealed=true, t=3, n=5, progress=1>
|
69
|
+
#
|
70
|
+
# @param [String] shard
|
71
|
+
# the key to use
|
72
|
+
#
|
73
|
+
# @return [SealStatus]
|
74
|
+
def unseal(shard)
|
75
|
+
json = client.put("/v1/sys/unseal", JSON.fast_generate(
|
76
|
+
key: shard,
|
77
|
+
))
|
78
|
+
return SealStatus.decode(json)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/lib/vault/client.rb
ADDED
@@ -0,0 +1,447 @@
|
|
1
|
+
require "cgi"
|
2
|
+
require "json"
|
3
|
+
require "uri"
|
4
|
+
|
5
|
+
require_relative "persistent"
|
6
|
+
require_relative "configurable"
|
7
|
+
require_relative "errors"
|
8
|
+
require_relative "version"
|
9
|
+
require_relative "encode"
|
10
|
+
|
11
|
+
module Vault
|
12
|
+
class Client
|
13
|
+
# The user agent for this client.
|
14
|
+
USER_AGENT = "VaultRuby/#{Vault::VERSION} (+github.com/hashicorp/vault-ruby)".freeze
|
15
|
+
|
16
|
+
# The name of the header used to hold the Vault token.
|
17
|
+
TOKEN_HEADER = "X-Vault-Token".freeze
|
18
|
+
|
19
|
+
# The name of the header used to hold the wrapped request ttl.
|
20
|
+
WRAP_TTL_HEADER = "X-Vault-Wrap-TTL".freeze
|
21
|
+
|
22
|
+
# The name of the header used for redirection.
|
23
|
+
LOCATION_HEADER = "location".freeze
|
24
|
+
|
25
|
+
# The default headers that are sent with every request.
|
26
|
+
DEFAULT_HEADERS = {
|
27
|
+
"Content-Type" => "application/json",
|
28
|
+
"Accept" => "application/json",
|
29
|
+
"User-Agent" => USER_AGENT,
|
30
|
+
}.freeze
|
31
|
+
|
32
|
+
# The default list of options to use when parsing JSON.
|
33
|
+
JSON_PARSE_OPTIONS = {
|
34
|
+
max_nesting: false,
|
35
|
+
create_additions: false,
|
36
|
+
symbolize_names: true,
|
37
|
+
}.freeze
|
38
|
+
|
39
|
+
RESCUED_EXCEPTIONS = [].tap do |a|
|
40
|
+
# Failure to even open the socket (usually permissions)
|
41
|
+
a << SocketError
|
42
|
+
|
43
|
+
# Failed to reach the server (aka bad URL)
|
44
|
+
a << Errno::ECONNREFUSED
|
45
|
+
|
46
|
+
# Failed to read body or no response body given
|
47
|
+
a << EOFError
|
48
|
+
|
49
|
+
# Timeout (Ruby 1.9-)
|
50
|
+
a << Timeout::Error
|
51
|
+
|
52
|
+
# Timeout (Ruby 1.9+) - Ruby 1.9 does not define these constants so we
|
53
|
+
# only add them if they are defiend
|
54
|
+
a << Net::ReadTimeout if defined?(Net::ReadTimeout)
|
55
|
+
a << Net::OpenTimeout if defined?(Net::OpenTimeout)
|
56
|
+
|
57
|
+
a << PersistentHTTP::Error
|
58
|
+
end.freeze
|
59
|
+
|
60
|
+
# Indicates a requested operation is not possible due to security
|
61
|
+
# concerns.
|
62
|
+
class SecurityError < RuntimeError
|
63
|
+
end
|
64
|
+
|
65
|
+
include Vault::Configurable
|
66
|
+
|
67
|
+
# Create a new Client with the given options. Any options given take
|
68
|
+
# precedence over the default options.
|
69
|
+
#
|
70
|
+
# @return [Vault::Client]
|
71
|
+
def initialize(options = {})
|
72
|
+
# Use any options given, but fall back to the defaults set on the module
|
73
|
+
Vault::Configurable.keys.each do |key|
|
74
|
+
value = options.key?(key) ? options[key] : Defaults.public_send(key)
|
75
|
+
instance_variable_set(:"@#{key}", value)
|
76
|
+
end
|
77
|
+
|
78
|
+
@lock = Mutex.new
|
79
|
+
@nhp = nil
|
80
|
+
end
|
81
|
+
|
82
|
+
def pool
|
83
|
+
@lock.synchronize do
|
84
|
+
return @nhp if @nhp
|
85
|
+
|
86
|
+
@nhp = PersistentHTTP.new("vault-ruby", nil, pool_size)
|
87
|
+
|
88
|
+
if proxy_address
|
89
|
+
proxy_uri = URI.parse "http://#{proxy_address}"
|
90
|
+
|
91
|
+
proxy_uri.port = proxy_port if proxy_port
|
92
|
+
|
93
|
+
if proxy_username
|
94
|
+
proxy_uri.user = proxy_username
|
95
|
+
proxy_uri.password = proxy_password
|
96
|
+
end
|
97
|
+
|
98
|
+
@nhp.proxy = proxy_uri
|
99
|
+
end
|
100
|
+
|
101
|
+
# Use a custom open timeout
|
102
|
+
if open_timeout || timeout
|
103
|
+
@nhp.open_timeout = (open_timeout || timeout).to_i
|
104
|
+
end
|
105
|
+
|
106
|
+
# Use a custom read timeout
|
107
|
+
if read_timeout || timeout
|
108
|
+
@nhp.read_timeout = (read_timeout || timeout).to_i
|
109
|
+
end
|
110
|
+
|
111
|
+
@nhp.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
112
|
+
|
113
|
+
# Vault requires TLS1.2
|
114
|
+
@nhp.ssl_version = "TLSv1_2"
|
115
|
+
|
116
|
+
# Only use secure ciphers
|
117
|
+
@nhp.ciphers = ssl_ciphers
|
118
|
+
|
119
|
+
# Custom pem files, no problem!
|
120
|
+
pem = ssl_pem_contents || (ssl_pem_file ? File.read(ssl_pem_file) : nil)
|
121
|
+
if pem
|
122
|
+
@nhp.cert = OpenSSL::X509::Certificate.new(pem)
|
123
|
+
@nhp.key = OpenSSL::PKey::RSA.new(pem, ssl_pem_passphrase)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Use custom CA cert for verification
|
127
|
+
if ssl_ca_cert
|
128
|
+
@nhp.ca_file = ssl_ca_cert
|
129
|
+
end
|
130
|
+
|
131
|
+
# Use custom CA path that contains CA certs
|
132
|
+
if ssl_ca_path
|
133
|
+
@nhp.ca_path = ssl_ca_path
|
134
|
+
end
|
135
|
+
|
136
|
+
if ssl_cert_store
|
137
|
+
@nhp.cert_store = ssl_cert_store
|
138
|
+
end
|
139
|
+
|
140
|
+
# Naughty, naughty, naughty! Don't blame me when someone hops in
|
141
|
+
# and executes a MITM attack!
|
142
|
+
if !ssl_verify
|
143
|
+
@nhp.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
144
|
+
end
|
145
|
+
|
146
|
+
# Use custom timeout for connecting and verifying via SSL
|
147
|
+
if ssl_timeout || timeout
|
148
|
+
@nhp.ssl_timeout = (ssl_timeout || timeout).to_i
|
149
|
+
end
|
150
|
+
|
151
|
+
@nhp
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
private :pool
|
156
|
+
|
157
|
+
# Shutdown any open pool connections. Pool will be recreated upon next request.
|
158
|
+
def shutdown
|
159
|
+
@nhp.shutdown()
|
160
|
+
@nhp = nil
|
161
|
+
end
|
162
|
+
|
163
|
+
# Creates and yields a new client object with the given token. This may be
|
164
|
+
# used safely in a threadsafe manner because the original client remains
|
165
|
+
# unchanged. The value of the block is returned.
|
166
|
+
#
|
167
|
+
# @yield [Vault::Client]
|
168
|
+
def with_token(token)
|
169
|
+
client = self.dup
|
170
|
+
client.token = token
|
171
|
+
return yield client if block_given?
|
172
|
+
return nil
|
173
|
+
end
|
174
|
+
|
175
|
+
# Determine if the given options are the same as ours.
|
176
|
+
# @return [true, false]
|
177
|
+
def same_options?(opts)
|
178
|
+
options.hash == opts.hash
|
179
|
+
end
|
180
|
+
|
181
|
+
# Perform a GET request.
|
182
|
+
# @see Client#request
|
183
|
+
def get(path, params = {}, headers = {})
|
184
|
+
request(:get, path, params, headers)
|
185
|
+
end
|
186
|
+
|
187
|
+
# Perform a LIST request.
|
188
|
+
# @see Client#request
|
189
|
+
def list(path, params = {}, headers = {})
|
190
|
+
params = params.merge(list: true)
|
191
|
+
request(:get, path, params, headers)
|
192
|
+
end
|
193
|
+
|
194
|
+
# Perform a POST request.
|
195
|
+
# @see Client#request
|
196
|
+
def post(path, data = {}, headers = {})
|
197
|
+
request(:post, path, data, headers)
|
198
|
+
end
|
199
|
+
|
200
|
+
# Perform a PUT request.
|
201
|
+
# @see Client#request
|
202
|
+
def put(path, data, headers = {})
|
203
|
+
request(:put, path, data, headers)
|
204
|
+
end
|
205
|
+
|
206
|
+
# Perform a PATCH request.
|
207
|
+
# @see Client#request
|
208
|
+
def patch(path, data, headers = {})
|
209
|
+
request(:patch, path, data, headers)
|
210
|
+
end
|
211
|
+
|
212
|
+
# Perform a DELETE request.
|
213
|
+
# @see Client#request
|
214
|
+
def delete(path, params = {}, headers = {})
|
215
|
+
request(:delete, path, params, headers)
|
216
|
+
end
|
217
|
+
|
218
|
+
# Make an HTTP request with the given verb, data, params, and headers. If
|
219
|
+
# the response has a return type of JSON, the JSON is automatically parsed
|
220
|
+
# and returned as a hash; otherwise it is returned as a string.
|
221
|
+
#
|
222
|
+
# @raise [HTTPError]
|
223
|
+
# if the request is not an HTTP 200 OK
|
224
|
+
#
|
225
|
+
# @param [Symbol] verb
|
226
|
+
# the lowercase symbol of the HTTP verb (e.g. :get, :delete)
|
227
|
+
# @param [String] path
|
228
|
+
# the absolute or relative path from {Defaults.address} to make the
|
229
|
+
# request against
|
230
|
+
# @param [#read, Hash, nil] data
|
231
|
+
# the data to use (varies based on the +verb+)
|
232
|
+
# @param [Hash] headers
|
233
|
+
# the list of headers to use
|
234
|
+
#
|
235
|
+
# @return [String, Hash]
|
236
|
+
# the response body
|
237
|
+
def request(verb, path, data = {}, headers = {})
|
238
|
+
# Build the URI and request object from the given information
|
239
|
+
uri = build_uri(verb, path, data)
|
240
|
+
request = class_for_request(verb).new(uri.request_uri)
|
241
|
+
if uri.userinfo()
|
242
|
+
request.basic_auth uri.user, uri.password
|
243
|
+
end
|
244
|
+
|
245
|
+
if proxy_address and uri.scheme.downcase == "https"
|
246
|
+
raise SecurityError, "no direct https connection to vault"
|
247
|
+
end
|
248
|
+
|
249
|
+
# Get a list of headers
|
250
|
+
headers = DEFAULT_HEADERS.merge(headers)
|
251
|
+
|
252
|
+
# Add the Vault token header - users could still override this on a
|
253
|
+
# per-request basis
|
254
|
+
if !token.nil?
|
255
|
+
headers[TOKEN_HEADER] ||= token
|
256
|
+
end
|
257
|
+
|
258
|
+
# Add headers
|
259
|
+
headers.each do |key, value|
|
260
|
+
request.add_field(key, value)
|
261
|
+
end
|
262
|
+
|
263
|
+
# Setup PATCH/POST/PUT
|
264
|
+
if [:patch, :post, :put].include?(verb)
|
265
|
+
if data.respond_to?(:read)
|
266
|
+
request.content_length = data.size
|
267
|
+
request.body_stream = data
|
268
|
+
elsif data.is_a?(Hash)
|
269
|
+
request.form_data = data
|
270
|
+
else
|
271
|
+
request.body = data
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
begin
|
276
|
+
# Create a connection using the block form, which will ensure the socket
|
277
|
+
# is properly closed in the event of an error.
|
278
|
+
response = pool.request(uri, request)
|
279
|
+
|
280
|
+
case response
|
281
|
+
when Net::HTTPRedirection
|
282
|
+
# On a redirect of a GET or HEAD request, the URL already contains
|
283
|
+
# the data as query string parameters.
|
284
|
+
if [:head, :get].include?(verb)
|
285
|
+
data = {}
|
286
|
+
end
|
287
|
+
request(verb, response[LOCATION_HEADER], data, headers)
|
288
|
+
when Net::HTTPSuccess
|
289
|
+
success(response)
|
290
|
+
else
|
291
|
+
error(response)
|
292
|
+
end
|
293
|
+
rescue *RESCUED_EXCEPTIONS => e
|
294
|
+
raise HTTPConnectionError.new(address, e)
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
# Construct a URL from the given verb and path. If the request is a GET or
|
299
|
+
# DELETE request, the params are assumed to be query params are are
|
300
|
+
# converted as such using {Client#to_query_string}.
|
301
|
+
#
|
302
|
+
# If the path is relative, it is merged with the {Defaults.address}
|
303
|
+
# attribute. If the path is absolute, it is converted to a URI object and
|
304
|
+
# returned.
|
305
|
+
#
|
306
|
+
# @param [Symbol] verb
|
307
|
+
# the lowercase HTTP verb (e.g. :+get+)
|
308
|
+
# @param [String] path
|
309
|
+
# the absolute or relative HTTP path (url) to get
|
310
|
+
# @param [Hash] params
|
311
|
+
# the list of params to build the URI with (for GET and DELETE requests)
|
312
|
+
#
|
313
|
+
# @return [URI]
|
314
|
+
def build_uri(verb, path, params = {})
|
315
|
+
# Add any query string parameters
|
316
|
+
if [:delete, :get].include?(verb)
|
317
|
+
path = [path, to_query_string(params)].compact.join("?")
|
318
|
+
end
|
319
|
+
|
320
|
+
# Parse the URI
|
321
|
+
uri = URI.parse(path)
|
322
|
+
|
323
|
+
# Don't merge absolute URLs
|
324
|
+
uri = URI.parse(File.join(address, path)) unless uri.absolute?
|
325
|
+
|
326
|
+
# Return the URI object
|
327
|
+
uri
|
328
|
+
end
|
329
|
+
|
330
|
+
# Helper method to get the corresponding {Net::HTTP} class from the given
|
331
|
+
# HTTP verb.
|
332
|
+
#
|
333
|
+
# @param [#to_s] verb
|
334
|
+
# the HTTP verb to create a class from
|
335
|
+
#
|
336
|
+
# @return [Class]
|
337
|
+
def class_for_request(verb)
|
338
|
+
Net::HTTP.const_get(verb.to_s.capitalize)
|
339
|
+
end
|
340
|
+
|
341
|
+
# Convert the given hash to a list of query string parameters. Each key and
|
342
|
+
# value in the hash is URI-escaped for safety.
|
343
|
+
#
|
344
|
+
# @param [Hash] hash
|
345
|
+
# the hash to create the query string from
|
346
|
+
#
|
347
|
+
# @return [String, nil]
|
348
|
+
# the query string as a string, or +nil+ if there are no params
|
349
|
+
def to_query_string(hash)
|
350
|
+
hash.map do |key, value|
|
351
|
+
"#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
|
352
|
+
end.join('&')[/.+/]
|
353
|
+
end
|
354
|
+
|
355
|
+
# Parse the response object and manipulate the result based on the given
|
356
|
+
# +Content-Type+ header. For now, this method only parses JSON, but it
|
357
|
+
# could be expanded in the future to accept other content types.
|
358
|
+
#
|
359
|
+
# @param [HTTP::Message] response
|
360
|
+
# the response object from the request
|
361
|
+
#
|
362
|
+
# @return [String, Hash]
|
363
|
+
# the parsed response, as an object
|
364
|
+
def success(response)
|
365
|
+
if response.body && (response.content_type || '').include?("json")
|
366
|
+
JSON.parse(response.body, JSON_PARSE_OPTIONS)
|
367
|
+
else
|
368
|
+
response.body
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
# Raise a response error, extracting as much information from the server's
|
373
|
+
# response as possible.
|
374
|
+
#
|
375
|
+
# @raise [HTTPError]
|
376
|
+
#
|
377
|
+
# @param [HTTP::Message] response
|
378
|
+
# the response object from the request
|
379
|
+
def error(response)
|
380
|
+
if response.body && response.body.match("missing client token")
|
381
|
+
raise MissingTokenError
|
382
|
+
end
|
383
|
+
|
384
|
+
# Use the correct exception class
|
385
|
+
case response
|
386
|
+
when Net::HTTPClientError
|
387
|
+
klass = HTTPClientError
|
388
|
+
when Net::HTTPServerError
|
389
|
+
klass = HTTPServerError
|
390
|
+
else
|
391
|
+
klass = HTTPError
|
392
|
+
end
|
393
|
+
|
394
|
+
if (response.content_type || '').include?("json")
|
395
|
+
# Attempt to parse the error as JSON
|
396
|
+
begin
|
397
|
+
json = JSON.parse(response.body, JSON_PARSE_OPTIONS)
|
398
|
+
|
399
|
+
if json[:errors]
|
400
|
+
raise klass.new(address, response, json[:errors])
|
401
|
+
end
|
402
|
+
rescue JSON::ParserError; end
|
403
|
+
end
|
404
|
+
|
405
|
+
raise klass.new(address, response, [response.body])
|
406
|
+
end
|
407
|
+
|
408
|
+
# Execute the given block with retries and exponential backoff.
|
409
|
+
#
|
410
|
+
# @param [Array<Exception>] rescued
|
411
|
+
# the list of exceptions to rescue
|
412
|
+
def with_retries(*rescued, &block)
|
413
|
+
options = rescued.last.is_a?(Hash) ? rescued.pop : {}
|
414
|
+
exception = nil
|
415
|
+
retries = 0
|
416
|
+
|
417
|
+
rescued = Defaults::RETRIED_EXCEPTIONS if rescued.empty?
|
418
|
+
|
419
|
+
max_attempts = options[:attempts] || Defaults::RETRY_ATTEMPTS
|
420
|
+
backoff_base = options[:base] || Defaults::RETRY_BASE
|
421
|
+
backoff_max = options[:max_wait] || Defaults::RETRY_MAX_WAIT
|
422
|
+
|
423
|
+
begin
|
424
|
+
return yield retries, exception
|
425
|
+
rescue *rescued => e
|
426
|
+
exception = e
|
427
|
+
|
428
|
+
retries += 1
|
429
|
+
raise if retries > max_attempts
|
430
|
+
|
431
|
+
# Calculate the exponential backoff combined with an element of
|
432
|
+
# randomness.
|
433
|
+
backoff = [backoff_base * (2 ** (retries - 1)), backoff_max].min
|
434
|
+
backoff = backoff * (0.5 * (1 + Kernel.rand))
|
435
|
+
|
436
|
+
# Ensure we are sleeping at least the minimum interval.
|
437
|
+
backoff = [backoff_base, backoff].max
|
438
|
+
|
439
|
+
# Exponential backoff.
|
440
|
+
Kernel.sleep(backoff)
|
441
|
+
|
442
|
+
# Now retry
|
443
|
+
retry
|
444
|
+
end
|
445
|
+
end
|
446
|
+
end
|
447
|
+
end
|