vault-kv 0.12.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/.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
|