aws-sigv4 1.0.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: dc5dab371e96d81659b31a8f240fac3f0181e1b1
4
+ data.tar.gz: fa9c9cce2b176379585923c0345890756f930e78
5
+ SHA512:
6
+ metadata.gz: cdad04342ef2a0e2e8d1016af4d7e50daf13bc892981f7629c7fb444bf86c599ad6c9657e377176c66ebddaea8a73b211c13e05c63ae38da9cf7f4cec86d70c7
7
+ data.tar.gz: af5088ab2930b07050ef3ab12c7de87671f54f3d8bcecc38a1fdde49e1df69f43cb11a80c402caf7257f5156b2eec935d35bcab427578f979ee8e6185d5bb1c0
@@ -0,0 +1,4 @@
1
+ require_relative 'aws-sigv4/credentials'
2
+ require_relative 'aws-sigv4/errors'
3
+ require_relative 'aws-sigv4/signature'
4
+ require_relative 'aws-sigv4/signer'
@@ -0,0 +1,59 @@
1
+ module Aws
2
+ module Sigv4
3
+ # Users that wish to configure static credentials can use the
4
+ # `:access_key_id` and `:secret_access_key` constructor options.
5
+ # @api private
6
+ class Credentials
7
+
8
+ # @option options [required, String] :access_key_id
9
+ # @option options [required, String] :secret_access_key
10
+ # @option options [String, nil] :session_token (nil)
11
+ def initialize(options = {})
12
+ if options[:access_key_id] && options[:secret_access_key]
13
+ @access_key_id = options[:access_key_id]
14
+ @secret_access_key = options[:secret_access_key]
15
+ @session_token = options[:session_token]
16
+ else
17
+ msg = "expected both :access_key_id and :secret_access_key options"
18
+ raise ArgumentError, msg
19
+ end
20
+ end
21
+
22
+ # @return [String]
23
+ attr_reader :access_key_id
24
+
25
+ # @return [String]
26
+ attr_reader :secret_access_key
27
+
28
+ # @return [String, nil]
29
+ attr_reader :session_token
30
+
31
+ # @return [Boolean]
32
+ def set?
33
+ !!(access_key_id && secret_access_key)
34
+ end
35
+
36
+ end
37
+
38
+ # Users that wish to configure static credentials can use the
39
+ # `:access_key_id` and `:secret_access_key` constructor options.
40
+ # @api private
41
+ class StaticCredentialsProvider
42
+
43
+ # @option options [Credentials] :credentials
44
+ # @option options [String] :access_key_id
45
+ # @option options [String] :secret_access_key
46
+ # @option options [String] :session_token (nil)
47
+ def initialize(options = {})
48
+ @credentials = options[:credentials] ?
49
+ options[:credentials] :
50
+ Credentials.new(options)
51
+ end
52
+
53
+ # @return [Credentials]
54
+ attr_reader :credentials
55
+
56
+ end
57
+
58
+ end
59
+ end
@@ -0,0 +1,25 @@
1
+ module Aws
2
+ module Sigv4
3
+ module Errors
4
+
5
+ class MissingCredentialsError < ArgumentError
6
+ def initialize(msg = nil)
7
+ super(msg || <<-MSG.strip)
8
+ missing credentials, provide credentials with one of the following options:
9
+ - :access_key_id and :secret_access_key
10
+ - :credentials
11
+ - :credentials_provider
12
+ MSG
13
+ end
14
+ end
15
+
16
+ class MissingRegionError < ArgumentError
17
+ def initialize(*args)
18
+ super("missing required option :region")
19
+ end
20
+ end
21
+
22
+ end
23
+ end
24
+ end
25
+
@@ -0,0 +1,63 @@
1
+ require 'uri'
2
+
3
+ module Aws
4
+ module Sigv4
5
+ class Request
6
+
7
+ # @option options [required, String] :http_method
8
+ # @option options [required, HTTP::URI, HTTPS::URI, String] :endpoint
9
+ # @option options [Hash<String,String>] :headers ({})
10
+ # @option options [String, IO] :body ('')
11
+ def initialize(options = {})
12
+ @http_method = nil
13
+ @endpoint = nil
14
+ @headers = {}
15
+ @body = ''
16
+ options.each_pair do |attr_name, attr_value|
17
+ send("#{attr_name}=", attr_value)
18
+ end
19
+ end
20
+
21
+ # @param [String] http_method One of 'GET', 'PUT', 'POST', 'DELETE', 'HEAD', or 'PATCH'
22
+ def http_method=(http_method)
23
+ @http_method = http_method
24
+ end
25
+
26
+ # @return [String] One of 'GET', 'PUT', 'POST', 'DELETE', 'HEAD', or 'PATCH'
27
+ def http_method
28
+ @http_method
29
+ end
30
+
31
+ # @param [String, HTTP::URI, HTTPS::URI] endpoint
32
+ def endpoint=(endpoint)
33
+ @endpoint = URI.parse(endpoint.to_s)
34
+ end
35
+
36
+ # @return [HTTP::URI, HTTPS::URI]
37
+ def endpoint
38
+ @endpoint
39
+ end
40
+
41
+ # @param [Hash] headers
42
+ def headers=(headers)
43
+ @headers = headers
44
+ end
45
+
46
+ # @return [Hash<String,String>]
47
+ def headers
48
+ @headers
49
+ end
50
+
51
+ # @param [String, IO] body
52
+ def body=(body)
53
+ @body = body
54
+ end
55
+
56
+ # @return [String, IO]
57
+ def body
58
+ @body
59
+ end
60
+
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,35 @@
1
+ module Aws
2
+ module Sigv4
3
+ class Signature
4
+
5
+ # @api private
6
+ def initialize(options)
7
+ options.each_pair do |attr_name, attr_value|
8
+ send("#{attr_name}=", attr_value)
9
+ end
10
+ end
11
+
12
+ # @return [Hash<String,String>] A hash of headers that should
13
+ # be applied to the HTTP request. Header keys are lower
14
+ # cased strings and may include the following:
15
+ #
16
+ # * 'host'
17
+ # * 'x-amz-date'
18
+ # * 'x-amz-security-token'
19
+ # * 'x-amz-content-sha256'
20
+ # * 'authorization'
21
+ #
22
+ attr_accessor :headers
23
+
24
+ # @return [String] For debugging purposes.
25
+ attr_accessor :canonical_request
26
+
27
+ # @return [String] For debugging purposes.
28
+ attr_accessor :string_to_sign
29
+
30
+ # @return [String] For debugging purposes.
31
+ attr_accessor :content_sha256
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,596 @@
1
+ require 'openssl'
2
+ require 'tempfile'
3
+ require 'time'
4
+ require 'uri'
5
+ require 'set'
6
+ require 'cgi'
7
+
8
+ module Aws
9
+ module Sigv4
10
+
11
+ # Utility class for creating AWS signature version 4 signature. This class
12
+ # provides two methods for generating signatures:
13
+ #
14
+ # * {#sign_request} - Computes a signature of the given request, returning
15
+ # the hash of headers that should be applied to the request.
16
+ #
17
+ # * {#presign_url} - Computes a presigned request with an expiration.
18
+ # By default, the body of this request is not signed and the request
19
+ # expires in 15 minutes.
20
+ #
21
+ # ## Configuration
22
+ #
23
+ # To use the signer, you need to specify the service, region, and credentials.
24
+ # The service name is normally the endpoint prefix to an AWS service. For
25
+ # example:
26
+ #
27
+ # ec2.us-west-1.amazonaws.com => ec2
28
+ #
29
+ # The region is normally the second portion of the endpoint, following
30
+ # the service name.
31
+ #
32
+ # ec2.us-west-1.amazonaws.com => us-west-1
33
+ #
34
+ # It is important to have the correct service and region name, or the
35
+ # signature will be invalid.
36
+ #
37
+ # ## Credentials
38
+ #
39
+ # The signer requires credentials. You can configure the signer
40
+ # with static credentials:
41
+ #
42
+ # signer = Aws::Sigv4::Signer.new(
43
+ # service: 's3',
44
+ # region: 'us-east-1',
45
+ # # static credentials
46
+ # access_key_id: 'akid',
47
+ # secret_access_key: 'secret'
48
+ # )
49
+ #
50
+ # You can also provide refreshing credentials via the `:credentials_provider`.
51
+ # If you are using the AWS SDK for Ruby, you can use any of the credential
52
+ # classes:
53
+ #
54
+ # signer = Aws::Sigv4::Signer.new(
55
+ # service: 's3',
56
+ # region: 'us-east-1',
57
+ # credentials_provider: Aws::InstanceProfileCredentials.new
58
+ # )
59
+ #
60
+ # Other AWS SDK for Ruby classes that can be provided via `:credentials_provider`:
61
+ #
62
+ # * `Aws::Credentials`
63
+ # * `Aws::SharedCredentials`
64
+ # * `Aws::InstanceProfileCredentials`
65
+ # * `Aws::AssumeRoleCredentials`
66
+ # * `Aws::ECSCredentials`
67
+ #
68
+ # A credential provider is any object that responds to `#credentials`
69
+ # returning another object that responds to `#access_key_id`, `#secret_access_key`,
70
+ # and `#session_token`.
71
+ #
72
+ class Signer
73
+
74
+ # @overload initialize(service:, region:, access_key_id:, secret_access_key:, session_token:nil, **options)
75
+ # @param [String] :service The service signing name, e.g. 's3'.
76
+ # @param [String] :region The region name, e.g. 'us-east-1'.
77
+ # @param [String] :access_key_id
78
+ # @param [String] :secret_access_key
79
+ # @param [String] :session_token (nil)
80
+ #
81
+ # @overload initialize(service:, region:, credentials:, **options)
82
+ # @param [String] :service The service signing name, e.g. 's3'.
83
+ # @param [String] :region The region name, e.g. 'us-east-1'.
84
+ # @param [Credentials] :credentials Any object that responds to the following
85
+ # methods:
86
+ #
87
+ # * `#access_key_id` => String
88
+ # * `#secret_access_key` => String
89
+ # * `#session_token` => String, nil
90
+ # * `#set?` => Boolean
91
+ #
92
+ # @overload initialize(service:, region:, credentials_provider:, **options)
93
+ # @param [String] :service The service signing name, e.g. 's3'.
94
+ # @param [String] :region The region name, e.g. 'us-east-1'.
95
+ # @param [#credentials] :credentials_provider An object that responds
96
+ # to `#credentials`, returning an object that responds to the following
97
+ # methods:
98
+ #
99
+ # * `#access_key_id` => String
100
+ # * `#secret_access_key` => String
101
+ # * `#session_token` => String, nil
102
+ # * `#set?` => Boolean
103
+ #
104
+ # @option options [Array<String>] :unsigned_headers ([]) A list of
105
+ # headers that should not be signed. This is useful when a proxy
106
+ # modifies headers, such as 'User-Agent', invalidating a signature.
107
+ #
108
+ # @option options [Boolean] :uri_escape_path (true) When `true`,
109
+ # the request URI path is uri-escaped as part of computing the canonical
110
+ # request string. This is required for every service, except Amazon S3,
111
+ # as of late 2016.
112
+ #
113
+ # @option options [Boolean] :apply_checksum_header (true) When `true`,
114
+ # the computed content checksum is returned in the hash of signature
115
+ # headers. This is required for AWS Glacier, and optional for
116
+ # every other AWS service as of late 2016.
117
+ #
118
+ def initialize(options = {})
119
+ @service = extract_service(options)
120
+ @region = extract_region(options)
121
+ @credentials_provider = extract_credentials_provider(options)
122
+ @unsigned_headers = Set.new((options.fetch(:unsigned_headers, [])).map(&:downcase))
123
+ @unsigned_headers << 'authorization'
124
+ [:uri_escape_path, :apply_checksum_header].each do |opt|
125
+ instance_variable_set("@#{opt}", options.key?(opt) ? !!options[:opt] : true)
126
+ end
127
+ end
128
+
129
+ # @return [String]
130
+ attr_reader :service
131
+
132
+ # @return [String]
133
+ attr_reader :region
134
+
135
+ # @return [#credentials] Returns an object that responds to
136
+ # `#credentials`, returning an object that responds to the following
137
+ # methods:
138
+ #
139
+ # * `#access_key_id` => String
140
+ # * `#secret_access_key` => String
141
+ # * `#session_token` => String, nil
142
+ # * `#set?` => Boolean
143
+ #
144
+ attr_reader :credentials_provider
145
+
146
+ # @return [Set<String>] Returns a set of header names that should not be signed.
147
+ # All header names have been downcased.
148
+ attr_reader :unsigned_headers
149
+
150
+ # @return [Boolean] When `true` the `x-amz-content-sha256` header will be signed and
151
+ # returned in the signature headers.
152
+ attr_reader :apply_checksum_header
153
+
154
+ # Computes a version 4 signature signature. Returns the resultant
155
+ # signature as a hash of headers to apply to your HTTP request. The given
156
+ # request is not modified.
157
+ #
158
+ # signature = signer.sign_request(
159
+ # http_method: 'PUT',
160
+ # url: 'https://domain.com',
161
+ # headers: {
162
+ # 'Abc' => 'xyz',
163
+ # },
164
+ # body: 'body' # String or IO object
165
+ # )
166
+ #
167
+ # # Apply the following hash of headers to your HTTP request
168
+ # signature.headers['Host']
169
+ # signature.headers['X-Amz-Date']
170
+ # signature.headers['X-Amz-Security-Token']
171
+ # signature.headers['X-Amz-Content-Sha256']
172
+ # signature.headers['Authorization']
173
+ #
174
+ # In addition to computing the signature headers, the canonicalized
175
+ # request, string to sign and content sha256 checksum are also available.
176
+ # These values are useful for debugging signature errors returned by AWS.
177
+ #
178
+ # signature.canonical_request #=> "..."
179
+ # signature.string_to_sign #=> "..."
180
+ # signature.content_sha256 #=> "..."
181
+ #
182
+ # @param [Hash] request
183
+ #
184
+ # @option request [required, String] :http_method One of
185
+ # 'GET', 'HEAD', 'PUT', 'POST', 'PATCH', or 'DELETE'
186
+ #
187
+ # @option request [required, String, URI::HTTPS, URI::HTTP] :url
188
+ # The request URI. Must be a valid HTTP or HTTPS URI.
189
+ #
190
+ # @option request [optional, Hash] :headers ({}) A hash of headers
191
+ # to sign. If the 'X-Amz-Content-Sha256' header is set, the `:body`
192
+ # is optional and will not be read.
193
+ #
194
+ # @option request [otpional, String, IO] :body ('') The HTTP request body.
195
+ # A sha256 checksum is computed of the body unless the
196
+ # 'X-Amz-Content-Sha256' header is set.
197
+ #
198
+ # @return [Signature] Return an instance of {Signature} that has
199
+ # a `#headers` method. The headers must be applied to your request.
200
+ #
201
+ def sign_request(request)
202
+
203
+ creds = get_credentials
204
+
205
+ http_method = extract_http_method(request)
206
+ url = extract_url(request)
207
+ headers = downcase_headers(request[:headers])
208
+
209
+ datetime = headers['x-amz-date']
210
+ datetime ||= Time.now.utc.strftime("%Y%m%dT%H%M%SZ")
211
+ date = datetime[0,8]
212
+
213
+ content_sha256 = headers['x-amz-content-sha256']
214
+ content_sha256 ||= sha256_hexdigest(request[:body] || '')
215
+
216
+ sigv4_headers = {}
217
+ sigv4_headers['host'] = host(url)
218
+ sigv4_headers['x-amz-date'] = datetime
219
+ sigv4_headers['x-amz-security-token'] = creds.session_token if creds.session_token
220
+ sigv4_headers['x-amz-content-sha256'] ||= content_sha256 if @apply_checksum_header
221
+
222
+ headers = headers.merge(sigv4_headers) # merge so we do not modify given headers hash
223
+
224
+ # compute signature parts
225
+ creq = canonical_request(http_method, url, headers, content_sha256)
226
+ sts = string_to_sign(datetime, creq)
227
+ sig = signature(creds.secret_access_key, date, sts)
228
+
229
+ # apply signature
230
+ sigv4_headers['authorization'] = [
231
+ "AWS4-HMAC-SHA256 Credential=#{credential(creds, date)}",
232
+ "SignedHeaders=#{signed_headers(headers)}",
233
+ "Signature=#{sig}",
234
+ ].join(', ')
235
+
236
+ # Returning the signature components.
237
+ Signature.new(
238
+ headers: sigv4_headers,
239
+ string_to_sign: sts,
240
+ canonical_request: creq,
241
+ content_sha256: content_sha256
242
+ )
243
+ end
244
+
245
+ # Signs a URL with query authentication. Using query parameters
246
+ # to authenticate requests is useful when you want to express a
247
+ # request entirely in a URL. This method is also referred as
248
+ # presigning a URL.
249
+ #
250
+ # See [Authenticating Requests: Using Query Parameters (AWS Signature Version 4)](http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html) for more information.
251
+ #
252
+ # To generate a presigned URL, you must provide a HTTP URI and
253
+ # the http method.
254
+ #
255
+ # url = signer.presigned_url(
256
+ # http_method: 'GET',
257
+ # url: 'https://my-bucket.s3-us-east-1.amazonaws.com/key',
258
+ # expires_in: 60
259
+ # )
260
+ #
261
+ # By default, signatures are valid for 15 minutes. You can specify
262
+ # the number of seconds for the URL to expire in.
263
+ #
264
+ # url = signer.presigned_url(
265
+ # http_method: 'GET',
266
+ # url: 'https://my-bucket.s3-us-east-1.amazonaws.com/key',
267
+ # expires_in: 3600 # one hour
268
+ # )
269
+ #
270
+ # You can provide a hash of headers that you plan to send with the
271
+ # request. Every 'X-Amz-*' header you plan to send with the request
272
+ # **must** be provided, or the signature is invalid. Other headers
273
+ # are optional, but should be provided for security reasons.
274
+ #
275
+ # url = signer.presigned_url(
276
+ # http_method: 'PUT',
277
+ # url: 'https://my-bucket.s3-us-east-1.amazonaws.com/key',
278
+ # headers: {
279
+ # 'X-Amz-Meta-Custom' => 'metadata'
280
+ # }
281
+ # )
282
+ #
283
+ # @option options [required, String] :http_method The HTTP request method,
284
+ # e.g. 'GET', 'HEAD', 'PUT', 'POST', 'PATCH', or 'DELETE'.
285
+ #
286
+ # @option options [required, String, HTTPS::URI, HTTP::URI] :url
287
+ # The URI to sign.
288
+ #
289
+ # @option options [Hash] :headers ({}) Headers that should
290
+ # be signed and sent along with the request. All x-amz-*
291
+ # headers must be present during signing. Other
292
+ # headers are optional.
293
+ #
294
+ # @option options [Integer<Seconds>] :expires_in (900)
295
+ # How long the presigned URL should be valid for. Defaults
296
+ # to 15 minutes (900 seconds).
297
+ #
298
+ # @option options [optional, String, IO] :body
299
+ # If the `:body` is set, then a SHA256 hexdigest will be computed of the body.
300
+ # If `:body_digest` is set, this option is ignored. If neither are set, then
301
+ # the `:body_digest` will be computed of the empty string.
302
+ #
303
+ # @option options [optional, String] :body_digest
304
+ # The SHA256 hexdigest of the request body. If you wish to send the presigned
305
+ # request without signing the body, you can pass 'UNSIGNED-PAYLOAD' as the
306
+ # `:body_digest` in place of passing `:body`.
307
+ #
308
+ # @option options [Time] :time (Time.now) Time of the signature.
309
+ # You should only set this value for testing.
310
+ #
311
+ # @return [HTTPS::URI, HTTP::URI]
312
+ #
313
+ def presign_url(options)
314
+
315
+ creds = get_credentials
316
+
317
+ http_method = extract_http_method(options)
318
+ url = extract_url(options)
319
+
320
+ headers = downcase_headers(options[:headers])
321
+ headers['host'] = host(url)
322
+
323
+ datetime = headers['x-amz-date']
324
+ datetime ||= (options[:time] || Time.now).utc.strftime("%Y%m%dT%H%M%SZ")
325
+ date = datetime[0,8]
326
+
327
+ content_sha256 = headers['x-amz-content-sha256']
328
+ content_sha256 ||= options[:body_digest]
329
+ content_sha256 ||= sha256_hexdigest(options[:body] || '')
330
+
331
+ params = {}
332
+ params['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256'
333
+ params['X-Amz-Credential'] = credential(creds, date)
334
+ params['X-Amz-Date'] = datetime
335
+ params['X-Amz-Expires'] = extract_expires_in(options)
336
+ params['X-Amz-SignedHeaders'] = signed_headers(headers)
337
+ params['X-Amz-Security-Token'] = creds.session_token if creds.session_token
338
+
339
+ params = params.map do |key, value|
340
+ "#{uri_escape(key)}=#{uri_escape(value)}"
341
+ end.join('&')
342
+
343
+ if url.query
344
+ url.query += '&' + params
345
+ else
346
+ url.query = params
347
+ end
348
+
349
+ creq = canonical_request(http_method, url, headers, content_sha256)
350
+ sts = string_to_sign(datetime, creq)
351
+ url.query += '&X-Amz-Signature=' + signature(creds.secret_access_key, date, sts)
352
+ url
353
+ end
354
+
355
+ private
356
+
357
+ def canonical_request(http_method, url, headers, content_sha256)
358
+ [
359
+ http_method,
360
+ path(url),
361
+ normalized_querystring(url.query || ''),
362
+ canonical_headers(headers) + "\n",
363
+ signed_headers(headers),
364
+ content_sha256,
365
+ ].join("\n")
366
+ end
367
+
368
+ def string_to_sign(datetime, canonical_request)
369
+ [
370
+ 'AWS4-HMAC-SHA256',
371
+ datetime,
372
+ credential_scope(datetime[0,8]),
373
+ sha256_hexdigest(canonical_request),
374
+ ].join("\n")
375
+ end
376
+
377
+ def credential_scope(date)
378
+ [
379
+ date,
380
+ @region,
381
+ @service,
382
+ 'aws4_request',
383
+ ].join('/')
384
+ end
385
+
386
+ def credential(credentials, date)
387
+ "#{credentials.access_key_id}/#{credential_scope(date)}"
388
+ end
389
+
390
+ def signature(secret_access_key, date, string_to_sign)
391
+ k_date = hmac("AWS4" + secret_access_key, date)
392
+ k_region = hmac(k_date, @region)
393
+ k_service = hmac(k_region, @service)
394
+ k_credentials = hmac(k_service, 'aws4_request')
395
+ hexhmac(k_credentials, string_to_sign)
396
+ end
397
+
398
+ def path(url)
399
+ path = url.path
400
+ path = '/' if path == ''
401
+ if @uri_escape_path
402
+ uri_escape_path(path)
403
+ else
404
+ path
405
+ end
406
+ end
407
+
408
+ def normalized_querystring(querystring)
409
+ params = querystring.split('&')
410
+ params = params.map { |p| p.match(/=/) ? p : p + '=' }
411
+ # We have to sort by param name and preserve order of params that
412
+ # have the same name. Default sort <=> in JRuby will swap members
413
+ # occasionally when <=> is 0 (considered still sorted), but this
414
+ # causes our normalized query string to not match the sent querystring.
415
+ # When names match, we then sort by their original order
416
+ params = params.each.with_index.sort do |a, b|
417
+ a, a_offset = a
418
+ a_name = a.split('=')[0]
419
+ b, b_offset = b
420
+ b_name = b.split('=')[0]
421
+ if a_name == b_name
422
+ a_offset <=> b_offset
423
+ else
424
+ a_name <=> b_name
425
+ end
426
+ end.map(&:first).join('&')
427
+ end
428
+
429
+ def signed_headers(headers)
430
+ headers.inject([]) do |signed_headers, (header, _)|
431
+ if @unsigned_headers.include?(header)
432
+ signed_headers
433
+ else
434
+ signed_headers << header
435
+ end
436
+ end.sort.join(';')
437
+ end
438
+
439
+ def canonical_headers(headers)
440
+ headers = headers.inject([]) do |headers, (k,v)|
441
+ if @unsigned_headers.include?(k)
442
+ headers
443
+ else
444
+ headers << [k,v]
445
+ end
446
+ end
447
+ headers = headers.sort_by(&:first)
448
+ headers.map{|k,v| "#{k}:#{canonical_header_value(v.to_s)}" }.join("\n")
449
+ end
450
+
451
+ def canonical_header_value(value)
452
+ value.match(/^".*"$/) ? value : value.gsub(/\s+/, ' ').strip
453
+ end
454
+
455
+ def host(uri)
456
+ if standard_port?(uri)
457
+ uri.host
458
+ else
459
+ "#{uri.host}:#{uri.port}"
460
+ end
461
+ end
462
+
463
+ def standard_port?(uri)
464
+ (uri.scheme == 'http' && uri.port == 80) ||
465
+ (uri.scheme == 'https' && uri.port == 443)
466
+ end
467
+
468
+ # @param [File, Tempfile, IO#read, String] value
469
+ # @return [String<SHA256 Hexdigest>]
470
+ def sha256_hexdigest(value)
471
+ if (File === value || Tempfile === value) && !value.path.nil? && File.exist?(value.path)
472
+ OpenSSL::Digest::SHA256.file(value).hexdigest
473
+ elsif value.respond_to?(:read)
474
+ sha256 = OpenSSL::Digest::SHA256.new
475
+ while chunk = value.read(1024 * 1024) # 1MB
476
+ sha256.update(chunk)
477
+ end
478
+ value.rewind
479
+ sha256.hexdigest
480
+ else
481
+ OpenSSL::Digest::SHA256.hexdigest(value)
482
+ end
483
+ end
484
+
485
+ def hmac(key, value)
486
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), key, value)
487
+ end
488
+
489
+ def hexhmac(key, value)
490
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), key, value)
491
+ end
492
+
493
+ def extract_service(options)
494
+ if options[:service]
495
+ options[:service]
496
+ else
497
+ msg = "missing required option :service"
498
+ raise ArgumentError, msg
499
+ end
500
+ end
501
+
502
+ def extract_region(options)
503
+ if options[:region]
504
+ options[:region]
505
+ else
506
+ raise Errors::MissingRegionError
507
+ end
508
+ end
509
+
510
+ def extract_credentials_provider(options)
511
+ if options[:credentials_provider]
512
+ options[:credentials_provider]
513
+ elsif options.key?(:credentials) || options.key?(:access_key_id)
514
+ StaticCredentialsProvider.new(options)
515
+ else
516
+ raise Errors::MissingCredentialsError
517
+ end
518
+ end
519
+
520
+ def extract_http_method(request)
521
+ if request[:http_method]
522
+ request[:http_method].upcase
523
+ else
524
+ msg = "missing required option :http_method"
525
+ raise ArgumentError, msg
526
+ end
527
+ end
528
+
529
+ def extract_url(request)
530
+ if request[:url]
531
+ URI.parse(request[:url].to_s)
532
+ else
533
+ msg = "missing required option :url"
534
+ raise ArgumentError, msg
535
+ end
536
+ end
537
+
538
+ def downcase_headers(headers)
539
+ (headers || {}).to_hash.inject({}) do |hash, (key, value)|
540
+ hash[key.downcase] = value
541
+ hash
542
+ end
543
+ end
544
+
545
+ def extract_expires_in(options)
546
+ case options[:expires_in]
547
+ when nil then 900.to_s
548
+ when Integer then options[:expires_in].to_s
549
+ else
550
+ msg = "expected :expires_in to be a number of seconds"
551
+ raise ArgumentError, msg
552
+ end
553
+ end
554
+
555
+ def uri_escape(string)
556
+ self.class.uri_escape(string)
557
+ end
558
+
559
+ def uri_escape_path(string)
560
+ self.class.uri_escape_path(string)
561
+ end
562
+
563
+ def get_credentials
564
+ credentials = @credentials_provider.credentials
565
+ if credentials_set?(credentials)
566
+ credentials
567
+ else
568
+ msg = 'unable to sign request without credentials set'
569
+ raise Errors::MissingCredentialsError.new(msg)
570
+ end
571
+ end
572
+
573
+ def credentials_set?(credentials)
574
+ credentials.access_key_id && credentials.secret_access_key
575
+ end
576
+
577
+ class << self
578
+
579
+ # @api private
580
+ def uri_escape_path(path)
581
+ path.gsub(/[^\/]+/) { |part| uri_escape(part) }
582
+ end
583
+
584
+ # @api private
585
+ def uri_escape(string)
586
+ if string.nil?
587
+ nil
588
+ else
589
+ CGI.escape(string.encode('UTF-8')).gsub('+', '%20').gsub('%7E', '~')
590
+ end
591
+ end
592
+
593
+ end
594
+ end
595
+ end
596
+ end
metadata ADDED
@@ -0,0 +1,50 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: aws-sigv4
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Amazon Web Services
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-11-08 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Amazon Web Services Signature Version 4 signing ligrary. Generates sigv4
14
+ signature for HTTP requests.
15
+ email:
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/aws-sigv4.rb
21
+ - lib/aws-sigv4/credentials.rb
22
+ - lib/aws-sigv4/errors.rb
23
+ - lib/aws-sigv4/request.rb
24
+ - lib/aws-sigv4/signature.rb
25
+ - lib/aws-sigv4/signer.rb
26
+ homepage: http://github.com/aws/aws-sdk-ruby
27
+ licenses:
28
+ - Apache-2.0
29
+ metadata: {}
30
+ post_install_message:
31
+ rdoc_options: []
32
+ require_paths:
33
+ - lib
34
+ required_ruby_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ required_rubygems_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ requirements: []
45
+ rubyforge_project:
46
+ rubygems_version: 2.6.4
47
+ signing_key:
48
+ specification_version: 4
49
+ summary: AWS Signature Version 4 library.
50
+ test_files: []