miasma-aws 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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