miasma-aws 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a3f6e18c7aa052992e9ffd65f0ce75d61b428704
4
+ data.tar.gz: ed79d1ba6dd4b90239b86a526a68e1d8946c01d1
5
+ SHA512:
6
+ metadata.gz: 79dbeffe45430e68c1e8af17988428e96e01a0e49345ac0ab1bb259ae021e86bfc2f19d8a0ed05b509281d74a3398861ad3fc55236a097852fe8c129c5d880f3
7
+ data.tar.gz: ebdaebefc0b15e64cb0609f0da662e7ea56c49cb2d837c4a6ae2228ef1763244160a0b55b466ef6f403e475f60dde534fc860883d4fab45ce314be07520c63ff
data/CHANGELOG.md ADDED
@@ -0,0 +1,2 @@
1
+ ## v0.1.0
2
+ * Initial release
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2014 Chris Roberts
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # Miasma AWS
2
+
3
+ AWS API plugin for the miasma cloud library
4
+
5
+ ## Current support matrix
6
+
7
+ |Model |Create|Read|Update|Delete|
8
+ |--------------|------|----|------|------|
9
+ |AutoScale | X | X | | |
10
+ |BlockStorage | | | | |
11
+ |Compute | X | X | | X |
12
+ |DNS | | | | |
13
+ |LoadBalancer | X | X | X | X |
14
+ |Network | | | | |
15
+ |Orchestration | X | X | X | X |
16
+ |Queues | | | | |
17
+ |Storage | X | X | X | X |
18
+
19
+ ## Info
20
+ * Repository: https://github.com/miasma-rb/miasma-aws
data/lib/miasma-aws.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'miasma'
2
+ require 'miasma-aws/version'
@@ -0,0 +1,4 @@
1
+ module MiasmaAws
2
+ # Current library version
3
+ VERSION = Gem::Version.new('0.1.0')
4
+ end
@@ -0,0 +1,444 @@
1
+ require 'miasma'
2
+ require 'miasma/utils/smash'
3
+
4
+ require 'time'
5
+ require 'openssl'
6
+
7
+ module Miasma
8
+ module Contrib
9
+ # Core API for AWS access
10
+ class AwsApiCore
11
+
12
+ module RequestUtils
13
+
14
+ # Fetch all results when tokens are being used
15
+ # for paging results
16
+ #
17
+ # @param next_token [String]
18
+ # @param result_key [Array<String, Symbol>] path to result
19
+ # @yield block to perform request
20
+ # @yieldparam options [Hash] request parameters (token information)
21
+ # @return [Array]
22
+ def all_result_pages(next_token, *result_key, &block)
23
+ list = []
24
+ options = next_token ? Smash.new('NextToken' => next_token) : Smash.new
25
+ result = block.call(options)
26
+ content = result.get(*result_key.dup)
27
+ if(content.is_a?(Array))
28
+ list += content
29
+ else
30
+ list << content
31
+ end
32
+ set = result.get(*result_key.slice(0, 3))
33
+ if(set && set['NextToken'])
34
+ list += all_result_pages(set['NextToken'], *result_key, &block)
35
+ end
36
+ list.compact
37
+ end
38
+
39
+ end
40
+
41
+ # @return [String] current time ISO8601 format
42
+ def self.time_iso8601
43
+ Time.now.utc.strftime('%Y%m%dT%H%M%SZ')
44
+ end
45
+
46
+ # HMAC helper class
47
+ class Hmac
48
+
49
+ # @return [OpenSSL::Digest]
50
+ attr_reader :digest
51
+ # @return [String] secret key
52
+ attr_reader :key
53
+
54
+ # Create new HMAC helper
55
+ #
56
+ # @param kind [String] digest type (sha1, sha256, sha512, etc)
57
+ # @param key [String] secret key
58
+ # @return [self]
59
+ def initialize(kind, key)
60
+ @digest = OpenSSL::Digest.new(kind)
61
+ @key = key
62
+ end
63
+
64
+ # @return [String]
65
+ def to_s
66
+ "Hmac#{digest.name}"
67
+ end
68
+
69
+ # Generate the hexdigest of the content
70
+ #
71
+ # @param content [String] content to digest
72
+ # @return [String] hashed result
73
+ def hexdigest_of(content)
74
+ digest << content
75
+ hash = digest.hexdigest
76
+ digest.reset
77
+ hash
78
+ end
79
+
80
+ # Sign the given data
81
+ #
82
+ # @param data [String]
83
+ # @param key_override [Object]
84
+ # @return [Object] signature
85
+ def sign(data, key_override=nil)
86
+ result = OpenSSL::HMAC.digest(digest, key_override || key, data)
87
+ digest.reset
88
+ result
89
+ end
90
+
91
+ # Sign the given data and return hexdigest
92
+ #
93
+ # @param data [String]
94
+ # @param key_override [Object]
95
+ # @return [String] hex encoded signature
96
+ def hex_sign(data, key_override=nil)
97
+ result = OpenSSL::HMAC.hexdigest(digest, key_override || key, data)
98
+ digest.reset
99
+ result
100
+ end
101
+
102
+ end
103
+
104
+ # Base signature class
105
+ class Signature
106
+
107
+ # Create new instance
108
+ def initialize(*args)
109
+ raise NotImplementedError.new 'This class should not be used directly!'
110
+ end
111
+
112
+ # Generate the signature
113
+ #
114
+ # @param http_method [Symbol] HTTP request method
115
+ # @param path [String] request path
116
+ # @param opts [Hash] request options
117
+ # @return [String] signature
118
+ def generate(http_method, path, opts={})
119
+ raise NotImplementedError
120
+ end
121
+
122
+ # URL string escape compatible with AWS requirements
123
+ #
124
+ # @param string [String] string to escape
125
+ # @return [String] escaped string
126
+ def safe_escape(string)
127
+ string.to_s.gsub(/([^a-zA-Z0-9_.\-~])/) do
128
+ '%' << $1.unpack('H2' * $1.bytesize).join('%').upcase
129
+ end
130
+ end
131
+
132
+ end
133
+
134
+ # AWS signature version 4
135
+ class SignatureV4 < Signature
136
+
137
+ # @return [Hmac]
138
+ attr_reader :hmac
139
+ # @return [String] access key
140
+ attr_reader :access_key
141
+ # @return [String] region
142
+ attr_reader :region
143
+ # @return [String] service
144
+ attr_reader :service
145
+
146
+ # Create new signature generator
147
+ #
148
+ # @param access_key [String]
149
+ # @param secret_key [String]
150
+ # @param region [String]
151
+ # @param service [String]
152
+ # @return [self]
153
+ def initialize(access_key, secret_key, region, service)
154
+ @hmac = Hmac.new('sha256', secret_key)
155
+ @access_key = access_key
156
+ @region = region
157
+ @service = service
158
+ end
159
+
160
+ # Generate the signature string for AUTH
161
+ #
162
+ # @param http_method [Symbol] HTTP request method
163
+ # @param path [String] request path
164
+ # @param opts [Hash] request options
165
+ # @return [String] signature
166
+ def generate(http_method, path, opts)
167
+ signature = generate_signature(http_method, path, opts)
168
+ "#{algorithm} Credential=#{access_key}/#{credential_scope}, SignedHeaders=#{signed_headers(opts[:headers])}, Signature=#{signature}"
169
+ end
170
+
171
+ # Generate URL with signed params
172
+ #
173
+ # @param http_method [Symbol] HTTP request method
174
+ # @param path [String] request path
175
+ # @param opts [Hash] request options
176
+ # @return [String] signature
177
+ def generate_url(http_method, path, opts)
178
+ opts[:params].merge!(
179
+ Smash.new(
180
+ 'X-Amz-SignedHeaders' => signed_headers(opts[:headers]),
181
+ 'X-Amz-Algorithm' => algorithm,
182
+ 'X-Amz-Credential' => "#{access_key}/#{credential_scope}"
183
+ )
184
+ )
185
+ signature = generate_signature(http_method, path, opts.merge(:body => 'UNSIGNED-PAYLOAD'))
186
+ params = opts[:params].merge('X-Amz-Signature' => signature)
187
+ "https://#{opts[:headers]['Host']}/#{path}?#{canonical_query(params)}"
188
+ end
189
+
190
+ # Generate the signature
191
+ #
192
+ # @param http_method [Symbol] HTTP request method
193
+ # @param path [String] request path
194
+ # @param opts [Hash] request options
195
+ # @return [String] signature
196
+ def generate_signature(http_method, path, opts)
197
+ to_sign = [
198
+ algorithm,
199
+ AwsApiCore.time_iso8601,
200
+ credential_scope,
201
+ hashed_canonical_request(
202
+ can_req = build_canonical_request(http_method, path, opts)
203
+ )
204
+ ].join("\n")
205
+ signature = sign_request(to_sign)
206
+ end
207
+
208
+ # Sign the request
209
+ #
210
+ # @param request [String] request to sign
211
+ # @return [String] signature
212
+ def sign_request(request)
213
+ key = hmac.sign(
214
+ 'aws4_request',
215
+ hmac.sign(
216
+ service,
217
+ hmac.sign(
218
+ region,
219
+ hmac.sign(
220
+ Time.now.utc.strftime('%Y%m%d'),
221
+ "AWS4#{hmac.key}"
222
+ )
223
+ )
224
+ )
225
+ )
226
+ hmac.hex_sign(request, key)
227
+ end
228
+
229
+ # @return [String] signature algorithm
230
+ def algorithm
231
+ 'AWS4-HMAC-SHA256'
232
+ end
233
+
234
+ # @return [String] credential scope for request
235
+ def credential_scope
236
+ [
237
+ Time.now.utc.strftime('%Y%m%d'),
238
+ region,
239
+ service,
240
+ 'aws4_request'
241
+ ].join('/')
242
+ end
243
+
244
+ # Generate the hash of the canonical request
245
+ #
246
+ # @param request [String] canonical request string
247
+ # @return [String] hashed canonical request
248
+ def hashed_canonical_request(request)
249
+ hmac.hexdigest_of(request)
250
+ end
251
+
252
+ # Build the canonical request string used for signing
253
+ #
254
+ # @param http_method [Symbol] HTTP request method
255
+ # @param path [String] request path
256
+ # @param opts [Hash] request options
257
+ # @return [String] canonical request string
258
+ def build_canonical_request(http_method, path, opts)
259
+ unless(path.start_with?('/'))
260
+ path = "/#{path}"
261
+ end
262
+ [
263
+ http_method.to_s.upcase,
264
+ path,
265
+ canonical_query(opts[:params]),
266
+ canonical_headers(opts[:headers]),
267
+ signed_headers(opts[:headers]),
268
+ canonical_payload(opts)
269
+ ].join("\n")
270
+ end
271
+
272
+ # Build the canonical query string used for signing
273
+ #
274
+ # @param params [Hash] query params
275
+ # @return [String] canonical query string
276
+ def canonical_query(params)
277
+ params ||= {}
278
+ params = Hash[params.sort_by(&:first)]
279
+ query = params.map do |key, value|
280
+ "#{safe_escape(key)}=#{safe_escape(value)}"
281
+ end.join('&')
282
+ end
283
+
284
+ # Build the canonical header string used for signing
285
+ #
286
+ # @param headers [Hash] request headers
287
+ # @return [String] canonical headers string
288
+ def canonical_headers(headers)
289
+ headers ||= {}
290
+ headers = Hash[headers.sort_by(&:first)]
291
+ headers.map do |key, value|
292
+ [key.downcase, value.chomp].join(':')
293
+ end.join("\n") << "\n"
294
+ end
295
+
296
+ # List of headers included in signature
297
+ #
298
+ # @param headers [Hash] request headers
299
+ # @return [String] header list
300
+ def signed_headers(headers)
301
+ headers ||= {}
302
+ headers.sort_by(&:first).map(&:first).
303
+ map(&:downcase).join(';')
304
+ end
305
+
306
+ # Build the canonical payload string used for signing
307
+ #
308
+ # @param options [Hash] request options
309
+ # @return [String] body checksum
310
+ def canonical_payload(options)
311
+ body = options.fetch(:body, '')
312
+ if(options[:json])
313
+ body = MultiJson.dump(options[:json])
314
+ elsif(options[:form])
315
+ body = URI.encode_www_form(options[:form])
316
+ end
317
+ if(body == 'UNSIGNED-PAYLOAD')
318
+ body
319
+ else
320
+ hmac.hexdigest_of(body)
321
+ end
322
+ end
323
+
324
+ end
325
+
326
+ module ApiCommon
327
+
328
+ def self.included(klass)
329
+ klass.class_eval do
330
+ attribute :aws_access_key_id, String, :required => true
331
+ attribute :aws_secret_access_key, String, :required => true
332
+ attribute :aws_region, String, :required => true
333
+ attribute :aws_host, String
334
+ attribute :aws_bucket_region, String
335
+
336
+ # @return [Contrib::AwsApiCore::SignatureV4]
337
+ attr_reader :signer
338
+ end
339
+ end
340
+
341
+ # Build new API for specified type using current provider / creds
342
+ #
343
+ # @param type [Symbol] api type
344
+ # @return [Api]
345
+ def api_for(type)
346
+ memoize(type) do
347
+ creds = attributes.dup
348
+ creds.delete(:aws_host)
349
+ Miasma.api(
350
+ Smash.new(
351
+ :type => type,
352
+ :provider => provider,
353
+ :credentials => creds
354
+ )
355
+ )
356
+ end
357
+ end
358
+
359
+ # Setup for API connections
360
+ def connect
361
+ unless(aws_host)
362
+ self.aws_host = [
363
+ self.class::API_SERVICE.downcase,
364
+ aws_region,
365
+ 'amazonaws.com'
366
+ ].join('.')
367
+ end
368
+ @signer = Contrib::AwsApiCore::SignatureV4.new(
369
+ aws_access_key_id, aws_secret_access_key, aws_region, self.class::API_SERVICE
370
+ )
371
+ end
372
+
373
+ # @return [String] custom escape for aws compat
374
+ def uri_escape(string)
375
+ signer.safe_escape(string)
376
+ end
377
+
378
+ # @return [HTTP] connection for requests (forces headers)
379
+ def connection
380
+ super.with_headers(
381
+ 'Host' => aws_host,
382
+ 'X-Amz-Date' => Contrib::AwsApiCore.time_iso8601
383
+ )
384
+ end
385
+
386
+ # @return [String] endpoint for request
387
+ def endpoint
388
+ "https://#{aws_host}"
389
+ end
390
+
391
+ # Override to inject signature
392
+ #
393
+ # @param connection [HTTP]
394
+ # @param http_method [Symbol]
395
+ # @param request_args [Array]
396
+ # @return [HTTP::Response]
397
+ # @note if http_method is :post, params will be automatically
398
+ # removed and placed into :form
399
+ def make_request(connection, http_method, request_args)
400
+ dest, options = request_args
401
+ path = URI.parse(dest).path
402
+ options = options ? options.to_smash : Smash.new
403
+ options[:params] = options.fetch(:params, Smash.new).to_smash.deep_merge('Version' => self.class::API_VERSION)
404
+ if(http_method.to_sym == :post)
405
+ if(options[:form])
406
+ options[:form].merge(options.delete(:params))
407
+ else
408
+ options[:form] = options.delete(:params)
409
+ end
410
+ end
411
+ update_request(connection, options)
412
+ signature = signer.generate(
413
+ http_method, path, options.merge(
414
+ Smash.new(
415
+ :headers => Smash[
416
+ connection.default_headers.to_a
417
+ ]
418
+ )
419
+ )
420
+ )
421
+ options = Hash[options.map{|k,v|[k.to_sym,v]}]
422
+ connection.auth(signature).send(http_method, dest, options)
423
+ end
424
+
425
+ # Simple callback to allow request option adjustments prior to
426
+ # signature calculation
427
+ #
428
+ # @param opts [Smash] request options
429
+ # @return [TrueClass]
430
+ def update_request(con, opts)
431
+ true
432
+ end
433
+
434
+ end
435
+
436
+ end
437
+ end
438
+
439
+ Models::Compute.autoload :Aws, 'miasma/contrib/aws/compute'
440
+ Models::LoadBalancer.autoload :Aws, 'miasma/contrib/aws/load_balancer'
441
+ Models::AutoScale.autoload :Aws, 'miasma/contrib/aws/auto_scale'
442
+ Models::Orchestration.autoload :Aws, 'miasma/contrib/aws/orchestration'
443
+ Models::Storage.autoload :Aws, 'miasma/contrib/aws/storage'
444
+ end