right_aws_api 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 113d86af588e3e120100ac9a99ca962f716e8ee9
4
- data.tar.gz: 50cece7801a4993dff77168a339fed0bfff64a3d
3
+ metadata.gz: 5329a8cc0261eacfd9bf681412e9064185934f7e
4
+ data.tar.gz: f6d75ece8c71660a0e6c465827083c1e655ca385
5
5
  SHA512:
6
- metadata.gz: 2abec6e84d93498ea70d7299814becc689deab4a63b9b3f0f8b5324a1ed71a26c4bbf25b5b71e14177e541c74aebaa2752e4ae6afe08c93fc7737a096b073b20
7
- data.tar.gz: 1b661a9ed16de1e9ee55e5e39d95165856b5fef65450703accf71822e1c18759b58c65137cdad6aa5dd366d00d6b6f260244ded69959e2ef17727d24e5cd0c27
6
+ metadata.gz: 4b6aa4f90078247eae455daf5e90d71323ea0dcf74f224885b4fb89f4beac77da3c7448f9e849e5253ed6d8019c7d97afa58164916f3ed9038143acb74ab129c
7
+ data.tar.gz: fddd2af29aa08ce0f364b69a45fb177f08799c6f28dc20ef03c47329ee8273e881bad92dad36bc42b8f6cc7c05246fbc80fecb65adb12b8573e313acc78c67cc
@@ -0,0 +1,70 @@
1
+ #--
2
+ # Copyright (c) 2014 RightScale, Inc.
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # 'Software'), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ require "cloud/aws/s3/manager"
25
+ require "cloud/aws/s3/link/routines/request_signer"
26
+ require "cloud/aws/s3/link/wrappers/default"
27
+
28
+ module RightScale
29
+ module CloudApi
30
+ module AWS
31
+ module S3
32
+
33
+ # Simple Storage Service Query API link namespace
34
+ #
35
+ # @api public
36
+ #
37
+ module Link
38
+
39
+ # S3 Query API links manager
40
+ #
41
+ # @example
42
+ # link = RightScale::CloudApi::AWS::S3::Link::Manager::new(key, secret, endpoint)
43
+ # link.get(
44
+ # 'devs-us-east/kd/Константин',
45
+ # :params => { 'response-content-type' => 'image/peg'}
46
+ # ) #=>
47
+ # https://devs-us-east.s3.amazonaws.com/kd%2F%D0%9A%D0%BE%D0%BD%D1%81%D1%82%D0%B0%
48
+ # D0%BD%D1%82%D0%B8%D0%BD?AWSAccessKeyId=AK...TA&Expires=1436557118&
49
+ # Signature=hg...%3D&response-content-type=image%2Fpeg
50
+ #
51
+ # @example
52
+ # link.ListAllMyBuckets#=>
53
+ # https://s3.amazonaws.com/?AWSAccessKeyId=AK...TA&Expires=1436651780&
54
+ # Signature=XK...53s%3D
55
+ #
56
+ class Manager < S3::Manager
57
+ end
58
+
59
+ class ApiManager < S3::ApiManager
60
+ set_routine CloudApi::RequestInitializer
61
+ set_routine AWS::S3::Link::RequestSigner
62
+
63
+ include Mixin::QueryApiPatterns
64
+ end
65
+ end
66
+
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,109 @@
1
+ #--
2
+ # Copyright (c) 2013 RightScale, Inc.
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # 'Software'), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ module RightScale
25
+ module CloudApi
26
+ module AWS
27
+ module S3
28
+ module Link
29
+
30
+ # S3 Request signer
31
+ class RequestSigner < S3::RequestSigner
32
+
33
+
34
+ # Authenticates an S3 request
35
+ #
36
+ # @return [void]
37
+ #
38
+ # @example
39
+ # # no example
40
+ #
41
+ def process
42
+ fail Error::new("Only GET method is supported") unless @data[:request][:verb] == :get
43
+ fail Error::new("Body must be blank") unless @data[:request][:body]._blank?
44
+ fail Error::new("Headers must be blank") unless @data[:request][:headers]._blank?
45
+
46
+ uri = @data[:connection][:uri]
47
+ access_key = @data[:credentials][:aws_access_key_id]
48
+ secret_key = @data[:credentials][:aws_secret_access_key]
49
+ bucket = @data[:request][:bucket]
50
+ object = @data[:request][:relative_path]
51
+ params = @data[:request][:params]
52
+ verb = @data[:request][:verb]
53
+
54
+ bucket, object = compute_bucket_name_and_object_path(bucket, object)
55
+ uri = compute_host(bucket, uri)
56
+
57
+ compute_params!(params, access_key)
58
+
59
+ # Set Auth param
60
+ signature = compute_signature(secret_key, verb, bucket, object, params)
61
+ params['Signature'] = signature
62
+
63
+ # Compute href
64
+ path = compute_path(bucket, object, params)
65
+ uri.path, uri.query = path.split('?')
66
+ @data[:result] = uri.to_s
67
+
68
+ # Set completion flag
69
+ @data[:vars][:system][:done] = true
70
+ end
71
+
72
+
73
+ # Sets response params
74
+ #
75
+ # @param [Hash] params
76
+ #
77
+ # @return [Hash]
78
+ #
79
+ def compute_params!(params, access_key)
80
+ # Expires
81
+ expires = params['Expires']
82
+ expires ||= Time.now.utc.to_i + ONE_YEAR_OF_SECONDS
83
+ expires = expires.to_i unless expires.is_a?(Fixnum)
84
+ params['Expires'] = expires
85
+ params['AWSAccessKeyId'] = access_key
86
+ params
87
+ end
88
+
89
+
90
+ # Computes signature
91
+ #
92
+ # @param [String] secret_key
93
+ # @param [String] verb
94
+ # @param [String] bucket
95
+ # @param [Hash] params
96
+ #
97
+ # @return [String]
98
+ #
99
+ def compute_signature(secret_key, verb, bucket, object, params)
100
+ can_path = compute_canonicalized_path(bucket, object, params)
101
+ Utils::AWS::sign_s3_signature(secret_key, verb, can_path, { 'expires' => params['Expires'] })
102
+ end
103
+ end
104
+
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,40 @@
1
+ #--
2
+ # Copyright (c) 2014 RightScale, Inc.
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # 'Software'), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ require "cloud/aws/s3/wrappers/default"
25
+
26
+ module RightScale
27
+ module CloudApi
28
+ module AWS
29
+ module S3
30
+ module Link
31
+ # S3 Link Wrapper namespace
32
+ module Wrapper
33
+ # Default wrapper
34
+ DEFAULT = S3::Wrapper::DEFAULT
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -45,7 +45,7 @@ module RightScale
45
45
  #
46
46
  # @example
47
47
  # # -- Using HTTP verb methods --
48
- #
48
+ #
49
49
  # # List all buckets
50
50
  # s3.get
51
51
  #
@@ -142,7 +142,7 @@ module RightScale
142
142
  # :headers => {'content-type' => 'image/jpeg'})
143
143
  #
144
144
  # @example
145
- #
145
+ #
146
146
  # # List all buckets
147
147
  # s3.ListAllMyBuckets #=>
148
148
  # {"ListAllMyBucketsResult"=>
@@ -179,7 +179,7 @@ module RightScale
179
179
  # "Size"=>"3257230",
180
180
  # "Key"=>"kd/boot.jpg"},
181
181
  # "@xmlns"=>"http://s3.amazonaws.com/doc/2006-03-01/",
182
- # "Prefix"=>"kd"}}
182
+ # "Prefix"=>"kd"}}
183
183
  #
184
184
  #
185
185
  # @example
@@ -279,21 +279,19 @@ module RightScale
279
279
  #
280
280
  #
281
281
  # @example
282
- # # Get a link to ListAllMyBuckets action
283
- # s3.ListAllMyBuckets(:options=>{:cloud=>{:link=>true}}) #=>
284
- # {"verb" => "get",
285
- # "link" => "https://s3.amazonaws.com/?AWSAccessKeyId=0K4QH...G2&Expires=1426111071&Signature=vLMH...3D",
286
- # "headers"=> {"host"=>["s3.amazonaws.com"]}}
287
- #
288
- # # If you need to get a bunch of links you can use with_options helper method:
289
- # opts = {:headers => {'expires' => Time.now + 3600}}
290
- # s3.with_options(:cloud=>{:link => true}) do
291
- # pp s3.GetObject({'Bucket'=>'my-bucket', 'Object'=>'kd/kd0.test', 'versionId'=>"00eYZeb291o4"}, opts)
292
- # pp s3.GetObject({'Bucket'=>'my-bucket', 'Object'=>'kd/kd1.test', 'versionId'=>"fafaf1obp1W4"}, opts)
293
- # pp s3.GetObject({'Bucket'=>'my-bucket', 'Object'=>'kd/kd2.test', 'versionId'=>"00asdTebp1W4"}, opts)
294
- # pp s3.GetObject({'Bucket'=>'my-bucket', 'Object'=>'kd/kd3.test', 'versionId'=>"0lkjreobp1W4"}, opts)
295
- # end
282
+ # link = RightScale::CloudApi::AWS::S3::Link::Manager::new(key, secret, endpoint)
283
+ # link.get(
284
+ # 'devs-us-east/kd/Константин',
285
+ # :params => { 'response-content-type' => 'image/peg'}
286
+ # ) #=>
287
+ # 'https://devs-us-east.s3.amazonaws.com/kd%2F%D0%9A%D0%BE%D0%BD%D1%81%D1%82%D0%B0%
288
+ # D0%BD%D1%82%D0%B8%D0%BD?AWSAccessKeyId=AK...TA&Expires=1436557118&
289
+ # Signature=hg...%3D&response-content-type=image%2Fpeg'
296
290
  #
291
+ # @example
292
+ # link.ListAllMyBuckets #=>
293
+ # 'https://s3.amazonaws.com/?AWSAccessKeyId=AK...TA&Expires=1436651780&
294
+ # Signature=XK...53s%3D'
297
295
  #
298
296
  # @see ApiManager
299
297
  # @see Wrapper::DEFAULT.extended Wrapper::DEFAULT.extended (click [View source])
@@ -365,8 +363,8 @@ module RightScale
365
363
  raise Error::new("Opts must be Hash not #{opts.class.name}") unless opts.is_a?(Hash)
366
364
  process_api_request(verb, relative_path, opts, &block)
367
365
  end
368
-
369
366
  end
367
+
370
368
  end
371
369
  end
372
370
  end
@@ -55,17 +55,18 @@ module RightScale
55
55
 
56
56
 
57
57
  # Using Query String API Amazon allows to override some of response headers:
58
- #
58
+ #
59
59
  # response-content-type response-content-language response-expires
60
60
  # reponse-cache-control response-content-disposition response-content-encoding
61
- #
61
+ #
62
62
  # @see http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTObjectGET.html?r=2145
63
63
  #
64
- OVERRIDE_RESPONSE_HEADERS = /^response-/
64
+ OVERRIDE_RESPONSE_HEADERS = /^response-/
65
65
 
66
66
  # One year in seconds
67
67
  ONE_YEAR_OF_SECONDS = 365*60*60*24
68
-
68
+
69
+
69
70
  # Authenticates an S3 request
70
71
  #
71
72
  # @return [void]
@@ -74,169 +75,215 @@ module RightScale
74
75
  # # no example
75
76
  #
76
77
  def process
77
- # Extract sub-resource(s).
78
- # Sub-resources are acl, torrent, versioning, location etc.
79
- sub_resources = {}
80
- @data[:request][:params].each do |key, value|
81
- sub_resources[key] = (value._blank? ? nil : value) if SUB_RESOURCES.include?(key) || key[OVERRIDE_RESPONSE_HEADERS]
82
- end
78
+ uri = @data[:connection][:uri]
79
+ access_key = @data[:credentials][:aws_access_key_id]
80
+ secret_key = @data[:credentials][:aws_secret_access_key]
81
+ body = @data[:request][:body]
82
+ bucket = @data[:request][:bucket]
83
+ headers = @data[:request][:headers]
84
+ object = @data[:request][:relative_path]
85
+ params = @data[:request][:params]
86
+ verb = @data[:request][:verb]
87
+
88
+ bucket, object = compute_bucket_name_and_object_path(bucket, object)
89
+ body = compute_body(body, headers['content-type'])
90
+ uri = compute_host(bucket, uri)
91
+
92
+ compute_headers!(headers, body, uri.host)
93
+
94
+ # Set Authorization header
95
+ signature = compute_signature(access_key, secret_key, verb, bucket, object, params, headers)
96
+ headers['authorization'] = "AWS #{access_key}:#{signature}"
97
+
98
+ @data[:request][:body] = body
99
+ @data[:request][:bucket] = bucket
100
+ @data[:request][:headers] = headers
101
+ @data[:request][:params] = params
102
+ @data[:request][:path] = compute_path(bucket, object, params)
103
+ @data[:request][:relative_path] = object
104
+ @data[:connection][:uri] = uri
105
+ end
83
106
 
84
- # Extract bucket name and object path
85
- if @data[:request][:bucket]._blank?
86
- # This is a very first attempt:
87
- # 1. extract the bucket name from the path
88
- # 2. save the bucket into request data vars
89
- bucket_name, @data[:request][:relative_path] = @data[:request][:relative_path].to_s[/^([^\/]*)\/?(.*)$/] && [$1, $2]
90
- @data[:request][:bucket] = bucket_name
91
- # Static path is the path that the original URL has.
92
- # 1. For Amazon it is always ''.
93
- # 2. For Euca it is usually a non-blank string.
94
- static_path = @data[:connection][:uri].path
95
- # Add trailing '/' to the path unless it is.
96
- static_path = "#{static_path}/" unless static_path[/\/$/]
97
- # Store the path: we may need it for signing redirects later.
98
- @data[:request][:static_path] = static_path
99
- else
100
- # This is a retry or a redirect:
101
- # 1. Extract the bucket name from the request data;
102
- # 2. Get rid of the path the remote server sent in the location header. We are
103
- # re-signing the request and have to build everything from the scratch.
104
- # In the crazy case when the new location has path differs from the original one
105
- # we are screwed up and we will get "SignatureDoesNotMatch" error. But this does
106
- # not seem to be the case for Amazon or Euca.
107
- bucket_name = @data[:request][:bucket]
108
- # Revert static path back to the original value.
109
- static_path = @data[:request][:static_path]
110
- @data[:connection][:uri].path = static_path
107
+
108
+ # Returns a list of sub-resource(s)
109
+ #
110
+ # Sub-resources are acl, torrent, versioning, location, etc. See SUB_RESOURCES
111
+ #
112
+ # @return [Hash]
113
+ #
114
+ def get_subresources(params)
115
+ result = {}
116
+ params.each do |key, value|
117
+ next unless SUB_RESOURCES.include?(key) || key[OVERRIDE_RESPONSE_HEADERS]
118
+ result[key] = (value._blank? ? nil : value)
111
119
  end
120
+ result
121
+ end
122
+
123
+
124
+ # Returns canonicalized bucket
125
+ #
126
+ # @param [String] bucket
127
+ #
128
+ # @return [String]
129
+ #
130
+ # @example
131
+ # # DNS bucket
132
+ # compute_canonicalized_bucket('foo-bar') #=> 'foo-bar/'
133
+ #
134
+ # @example
135
+ # # non DNS bucket
136
+ # compute_canonicalized_bucket('foo_bar') #=> 'foo_bar'
137
+ #
138
+ def compute_canonicalized_bucket(bucket)
139
+ bucket += '/' if Utils::AWS::is_dns_bucket?(bucket)
140
+ bucket
141
+ end
142
+
112
143
 
113
- # Escape that part of the path that may have UTF-8 chars (in S3 Object name for instance).
144
+ # Returns canonicalized path
145
+ #
146
+ # @param [String] bucket
147
+ # @param [String] relative_path
148
+ # @param [Hash] params
149
+ #
150
+ # @return [String]
151
+ #
152
+ # @example
153
+ # params = { 'Foo' => 1, 'acl' => '2', 'response-content-type' => 'jpg' }
154
+ # compute_canonicalized_path('foo-bar_bucket', 'a/b/c/d.jpg', params)
155
+ # #=> '/foo-bar_bucket/a/b/c/d.jpg?acl=3&response-content-type=jpg'
156
+ #
157
+ def compute_canonicalized_path(bucket, relative_path, params)
158
+ can_bucket = compute_canonicalized_bucket(bucket)
159
+ sub_params = get_subresources(params)
160
+ # We use the block below to avoid escaping: Amazon does not like escaped bucket and '/'
161
+ # in canonicalized path (relative path has been escaped above already)
162
+ Utils::join_urn(can_bucket, relative_path, sub_params) { |value| value }
163
+ end
164
+
165
+
166
+ # Extracts S3 bucket name and escapes relative path
167
+ #
168
+ # @param [String] bucket
169
+ # @param [String] relative_path
170
+ #
171
+ # @return [Array] [bucket, escaped_relative_path]
172
+ #
173
+ # @example
174
+ # subject.compute_bucket_name_and_object_path(nil, 'my-test-bucket/foo/bar/банана.jpg') #=>
175
+ # ['my-test-bucket', 'foo%2Fbar%2F%D0%B1%D0%B0%D0%BD%D0%B0%D0%BD%D0%B0.jpg']
176
+ #
177
+ def compute_bucket_name_and_object_path(bucket, relative_path)
178
+ return [bucket, relative_path] if bucket
179
+ # This is a very first attempt:
180
+ relative_path.to_s[/^([^\/]*)\/?(.*)$/]
181
+ # Escape part of the path that may have UTF-8 chars (in S3 Object name for instance).
114
182
  # Amazon wants them to be escaped before we sign the request.
115
- #
116
- # P.S. Escape AFTER we extract bucket name.
117
- @data[:request][:relative_path] = Utils::AWS::amz_escape(@data[:request][:relative_path])
118
-
119
- # Calculate a canonical path (bucket part must end with '/')
120
- bucket_string = Utils::AWS::is_dns_bucket?(bucket_name) ? "#{bucket_name}/" : bucket_name.to_s
121
- canonicalized_path = Utils::join_urn(static_path,
122
- bucket_string,
123
- @data[:request][:relative_path],
124
- sub_resources ){ |value| value } # pass this block to avoid escaping: Amazon does not like escaped things in canonicalized_path
125
-
126
- # Make sure headers required for authentication are set
127
- unless @data[:options][:cloud][:link]
128
- # Make sure 'content-type' is set.
129
- # P.S. Ruby 2.1+ sets 'content-type' by default for POST and PUT requests.
130
- # So we need to include it into our signature to avoid the error below:
131
- # 'The request signature we calculated does not match the signature you provided.
132
- # Check your key and signing method.'
133
- @data[:request][:headers].set_if_blank('content-type', 'application/octet-stream')
134
- # REST Auth:
135
- unless @data[:request][:body]._blank?
136
- # Fix body if it is a Hash instance
137
- if @data[:request][:body].is_a?(Hash)
138
- @data[:request][:body] = Utils::contentify_body(@data[:request][:body], @data[:request][:headers]['content-type'])
139
- end
140
- # Calculate 'content-md5' when possible (some API calls wanna have it set)
141
- if @data[:request][:body].is_a?(String)
142
- @data[:request][:headers]['content-md5'] = Base64::encode64(Digest::MD5::digest(@data[:request][:body])).strip
143
- end
144
- end
145
- # Set date
146
- @data[:request][:headers].set_if_blank('date', Time::now.utc.httpdate)
147
- # Sign a request
148
- signature = Utils::AWS::sign_s3_signature( @data[:credentials][:aws_secret_access_key],
149
- @data[:request][:verb],
150
- canonicalized_path,
151
- @data[:request][:headers])
152
- @data[:request][:headers]['authorization'] = "AWS #{@data[:credentials][:aws_access_key_id]}:#{signature}"
153
- else
154
- # @see http://docs.amazonwebservices.com/AmazonS3/latest/dev/RESTAuthentication.html
155
- #
156
- # Amazon: ... We assume that when a browser makes the GET request, it won't provide a Content-MD5 or a Content-Type header,
157
- # nor will it set any x-amz- headers, so those parts of the StringToSign are left blank. ...
158
- #
159
- # Only GET requests!
160
- raise Error::new("Only GET requests are supported by S3 Query String API") unless @data[:request][:verb] == :get
161
- # Expires
162
- expires = Utils::dearrayify(@data[:request][:headers]['expires'].first || (Time.now.utc.to_i + ONE_YEAR_OF_SECONDS))
163
- expires = expires.to_i unless expires.is_a?(Fixnum)
164
- # QUERY STRING AUTH: ('expires' and 'x-amz-*' headers are not supported)
165
- @data[:request][:params]['Expires'] = expires
166
- @data[:request][:headers]['expires'] = expires # a hack to sign a record
167
- @data[:request][:headers].dup.each do |header, values|
168
- @data[:request][:headers].delete(header) unless header.to_s[/(^x-amz-)|(^expires$)/]
169
- end
170
- @data[:request][:params]['AWSAccessKeyId'] = @data[:credentials][:aws_access_key_id]
171
- # Sign a request
172
- signature = Utils::AWS::sign_s3_signature( @data[:credentials][:aws_secret_access_key],
173
- @data[:request][:verb],
174
- canonicalized_path,
175
- @data[:request][:headers] )
176
- @data[:request][:params]['Signature'] = signature
177
- # we dont need this header any more
178
- @data[:request][:headers].delete('expires')
179
- end
183
+ [ $1, Utils::AWS::amz_escape($2) ]
184
+ end
180
185
 
181
- # Sub-domain compatible buckets vs incompatible ones
182
- if !@data[:options][:cloud][:no_subdomains] && Utils::AWS::is_dns_bucket?(bucket_name)
183
- # DNS compatible bucket name:
184
- #
185
- # Figure out if we need to add bucket name into the host name. It is rediculous but
186
- # sometimes Amazon returns a redirect to a host with the bucket name already mixed in
187
- # but sometimes without.
188
- # The only thing we can do so far is to check if the host name starts with the bucket
189
- # and the name is at least 4th level DNS name.
190
- #
191
- # Examples:
192
- # * my-bucket.s3-ap-southeast-2.amazonaws.com
193
- # * my-bucket.s3.amazonaws.com
194
- # * s3.amazonaws.com
195
- #
196
- # P.S. This assumtion will not work for any other providers but we will figure it out later
197
- # if we support any. The only other provider we support is Eucalyptus but it always
198
- # expects that the bucket goes into path and never into the host therefore we are
199
- # OK with Euca (Euca is expected to be run with :no_subdomains => true).
200
- #
201
- unless @data[:connection][:uri].host[/^#{bucket_name}\..+\.[^.]+\.[^.]+$/]
202
- # If there was a redirect and it had 'location' header then there is nothing to do with the host
203
- # otherwise we have to add the bucket to the host.
204
- # P.S. When Amazon returns a redirect (usually 301) with the new host in the message body
205
- # the new host does not have the bucket name in it. But if it is 307 and the host is in the location
206
- # header then that host name already includes the bucket in it. Grrrr....
207
- @data[:connection][:uri].host = "#{bucket_name}.#{@data[:connection][:uri].host}"
208
- end
209
- @data[:request][:path] = Utils::join_urn( @data[:connection][:uri].path,
210
- @data[:request][:relative_path],
211
- @data[:request][:params] )
212
- else
213
- # Old incompatible or Eucalyptus
214
- @data[:request][:path] = Utils::join_urn( @data[:connection][:uri].path,
215
- "#{bucket_name}",
216
- @data[:request][:relative_path],
217
- @data[:request][:params] )
218
- end
219
186
 
220
- # Host should be set for REST requests (and should not affect on Query String ones)
221
- @data[:request][:headers]['host'] = @data[:connection][:uri].host
222
-
223
- # Finalize data
224
- if @data[:options][:cloud][:link]
225
- # Amazon supports only some GET requests without body and any headers:
226
- # Return the link
227
- uri = @data[:connection][:uri].clone
228
- uri.path, uri.query = @data[:request][:path].split('?')
229
- @data[:result] = {
230
- "verb" => @data[:request][:verb].to_s,
231
- "link" => uri.to_s,
232
- "headers" => @data[:request][:headers]
233
- }
234
- # Query Auth:we should stop here because we just generated a link for the third part usage
235
- @data[:vars][:system][:done] = true
236
- end
187
+ # Figure out if we need to add bucket name into the host name
188
+ #
189
+ # If there was a redirect and it had 'location' header then there is nothing to do
190
+ # with the host, otherwise we have to add the bucket to the host.
191
+ #
192
+ # P.S. When Amazon returns a redirect (usually 301) with the new host in the message
193
+ # body, the new host does not have the bucket name in it. But if it is 307 and the
194
+ # host is in the location header then that host name already includes the bucket in it.
195
+ # The only thing we can do so far is to check if the host name starts with the bucket
196
+ # and the name is at least 4th level DNS name.
197
+ #
198
+ # Examples:
199
+ # * my-bucket.s3-ap-southeast-2.amazonaws.com
200
+ # * my-bucket.s3.amazonaws.com
201
+ # * s3.amazonaws.com
202
+ #
203
+ # @param [String] bucket
204
+ # @param [URI] uri
205
+ #
206
+ # @return [URI]
207
+ #
208
+ def compute_host(bucket, uri)
209
+ return uri unless Utils::AWS::is_dns_bucket?(bucket)
210
+ return uri if uri.host[/^#{bucket}\..+\.[^.]+\.[^.]+$/]
211
+ uri.host = "#{bucket}.#{uri.host}"
212
+ uri
237
213
  end
238
- end
239
214
 
215
+
216
+ # Returns response body
217
+ #
218
+ # @param [Object] body
219
+ # @param [String] content_type
220
+ #
221
+ # @return [Object]
222
+ #
223
+ def compute_body(body, content_type)
224
+ return body if body._blank?
225
+ # Make sure it is a String instance
226
+ return body unless body.is_a?(Hash)
227
+ Utils::contentify_body(body, content_type)
228
+ end
229
+
230
+
231
+ # Sets response headers
232
+ #
233
+ # @param [Hash] headers
234
+ # @param [String] body
235
+ # @param [String] host
236
+ #
237
+ # @return [Hash]
238
+ #
239
+ def compute_headers!(headers, body, host)
240
+ # Make sure 'content-type' is set.
241
+ # P.S. Ruby 2.1+ sets 'content-type' by default for POST and PUT requests.
242
+ # So we need to include it into our signature to avoid the error below:
243
+ # 'The request signature we calculated does not match the signature you provided.
244
+ # Check your key and signing method.'
245
+ headers.set_if_blank('content-type', 'application/octet-stream')
246
+ headers.set_if_blank('date', Time::now.utc.httpdate)
247
+ headers['content-md5'] = Base64::encode64(Digest::MD5::digest(body)).strip if !body._blank?
248
+ headers['host'] = host
249
+ headers
250
+ end
251
+
252
+
253
+ # Computes signature
254
+ #
255
+ # @param [String] access_key
256
+ # @param [String] secret_key
257
+ # @param [String] verb
258
+ # @param [String] bucket
259
+ # @param [Hash] params
260
+ # @param [Hash] headers
261
+ #
262
+ # @return [String]
263
+ #
264
+ def compute_signature(access_key, secret_key, verb, bucket, object, params, headers)
265
+ can_path = compute_canonicalized_path(bucket, object, params)
266
+ Utils::AWS::sign_s3_signature(secret_key, verb, can_path, headers)
267
+ end
268
+
269
+
270
+ # Builds request path
271
+ #
272
+ # @param [String] bucket
273
+ # @param [String] object
274
+ # @param [Hash] params
275
+ #
276
+ # @return [String]
277
+ #
278
+ def compute_path(bucket, object, params)
279
+ data = []
280
+ data << bucket unless Utils::AWS::is_dns_bucket?(bucket)
281
+ data << object
282
+ data << params
283
+ Utils::join_urn(*data)
284
+ end
285
+
286
+ end
240
287
  end
241
288
  end
242
289
  end
data/lib/right_aws_api.rb CHANGED
@@ -40,6 +40,7 @@ require "cloud/aws/iam/manager"
40
40
  require "cloud/aws/rds/manager"
41
41
  require "cloud/aws/route53/manager"
42
42
  require "cloud/aws/s3/manager"
43
+ require "cloud/aws/s3/link/manager"
43
44
  require "cloud/aws/sdb/manager"
44
45
  require "cloud/aws/sns/manager"
45
46
  require "cloud/aws/sqs/manager"
@@ -33,7 +33,7 @@ module RightScale
33
33
  # Gem version namespace
34
34
  module VERSION
35
35
  # Current version
36
- STRING = '0.1.0'
36
+ STRING = '0.2.0'
37
37
  end
38
38
  end
39
39
  end
@@ -38,6 +38,7 @@ Gem::Specification.new do |spec|
38
38
  spec.add_dependency 'right_cloud_api_base', '>= 0.1.0'
39
39
 
40
40
  spec.add_development_dependency 'rake'
41
+ spec.add_development_dependency 'rspec', '>= 3.0.0'
41
42
 
42
43
  spec.description = <<-EOF
43
44
  == DESCRIPTION:
@@ -0,0 +1,53 @@
1
+ require 'right_aws_api'
2
+
3
+ require 'rspec'
4
+
5
+ describe RightScale::CloudApi::AWS::S3::Link::RequestSigner do
6
+
7
+
8
+ context '#compute_params!' do
9
+ before :each do
10
+ @access_key = 'access-key'
11
+ end
12
+
13
+ context 'Expires' do
14
+ it 'defaults to something in the future' do
15
+ result = subject.compute_params!({}, @access_key)
16
+ expect(result['Expires']).to be_an(Integer)
17
+ end
18
+
19
+ it 'sets the passed value' do
20
+ expectation = 123
21
+ result = subject.compute_params!({ 'Expires' => expectation }, @access_key)
22
+ expect(result['Expires']).to eq(expectation)
23
+ end
24
+ end
25
+
26
+
27
+ context 'AWSAccessKeyId' do
28
+ it 'sets the passed value' do
29
+ result = subject.compute_params!({}, @access_key)
30
+ expect(result['AWSAccessKeyId']).to eq(@access_key)
31
+ end
32
+ end
33
+
34
+ end
35
+
36
+
37
+ context '#compute_signature' do
38
+ before :each do
39
+ @secret_key = 'secret-key'
40
+ @verb = :get
41
+ @bucket = 'foo-bar'
42
+ @object = 'foo%2Fbar%2F%D0%B1%D0%B0%D0%BD%D0%B0%D0%BD%D0%B0.jpg'
43
+ @params = { 'Foo' => 1, 'Bar' => 2, 'Expires' => 1000000 }
44
+ @expectation = "EShMsLs2Bqak5YuIqOTJq15qcJE="
45
+ @result = subject.compute_signature(@secret_key, @verb, @bucket, @object, @params)
46
+ end
47
+
48
+ it 'properly calculates the signature' do
49
+ expect(@result).to eq(@expectation)
50
+ end
51
+ end
52
+
53
+ end
@@ -0,0 +1,230 @@
1
+ require 'right_aws_api'
2
+
3
+ require 'rspec'
4
+
5
+ describe RightScale::CloudApi::AWS::S3::RequestSigner do
6
+ context '#get_subresources' do
7
+ before :each do
8
+ @sub_resources = {
9
+ 'acl' => 0,
10
+ 'policy' => 0,
11
+ 'versions' => 0,
12
+ 'website' => 0,
13
+ 'response-content-type' => 0,
14
+ 'response-content-language' => 0,
15
+ 'response-foo-bar' => 0,
16
+ }
17
+ trash = {
18
+ 'foo' => 0,
19
+ 'bar' => 0,
20
+ }
21
+ params = @sub_resources.merge(trash)
22
+ @result = subject.get_subresources(params)
23
+ end
24
+
25
+ it "extracts SUB_RESOURCES and response- params" do
26
+ expect(@result).to eq(@sub_resources)
27
+ end
28
+ end
29
+
30
+
31
+ context '#compute_canonicalized_bucket' do
32
+ context 'DNS bucket' do
33
+ it 'adds a trailing slash' do
34
+ expect(subject.compute_canonicalized_bucket('foo-bar')).to eq('foo-bar/')
35
+ end
36
+ end
37
+
38
+ context 'non DNS bucket' do
39
+ it 'does nothing' do
40
+ expect(subject.compute_canonicalized_bucket('foo_bar')).to eq('foo_bar')
41
+ end
42
+ end
43
+ end
44
+
45
+
46
+ context '#compute_canonicalized_path' do
47
+ context 'with no sub-resources' do
48
+ before :each do
49
+ bucket = 'foo-bar_bucket'
50
+ relative_path = 'a/b/c/d.jpg'
51
+ params = { 'Foo' => 1, 'acl' => '2', 'response-content-type' => 'jpg' }
52
+ @result = subject.compute_canonicalized_path(bucket, relative_path, params)
53
+ @expectation = '/foo-bar_bucket/a/b/c/d.jpg?acl=2&response-content-type=jpg'
54
+ end
55
+
56
+ it 'works' do
57
+ expect(@result).to eq(@expectation)
58
+ end
59
+ end
60
+ end
61
+
62
+
63
+
64
+ context '#compute_bucket_name_and_object_path' do
65
+ before :each do
66
+ @original_path = 'my-test-bucket/foo/bar/банана.jpg'
67
+ @bucket = 'my-test-bucket'
68
+ @relative_path = 'foo%2Fbar%2F%D0%B1%D0%B0%D0%BD%D0%B0%D0%BD%D0%B0.jpg'
69
+ end
70
+
71
+ context 'when this is a first API call attempt' do
72
+ before :each do
73
+ @r_bucket, @r_path = subject.compute_bucket_name_and_object_path(nil, @original_path)
74
+ end
75
+
76
+ it "extracts the bucket and escapes the path" do
77
+ expect(@r_bucket).to eq(@bucket)
78
+ expect(@r_path).to eq(@relative_path)
79
+ end
80
+ end
81
+
82
+
83
+ context 'when there is a redirect and the bucket is already extracted' do
84
+ before :each do
85
+ @r_bucket, @r_path = subject.compute_bucket_name_and_object_path(@bucket, @relative_path)
86
+ end
87
+
88
+ it "does nothing" do
89
+ expect(@r_bucket).to eq(@bucket)
90
+ expect(@r_path).to eq(@relative_path)
91
+ end
92
+ end
93
+ end
94
+
95
+
96
+ context '#compute_host' do
97
+ context 'DNS bucket' do
98
+ before :each do
99
+ bucket = 'foo-bar-bucket'
100
+ uri = URI.parse('https://a.b.com')
101
+ @result = subject.compute_host(bucket, uri)
102
+ @expectation = 'https://foo-bar-bucket.a.b.com'
103
+ end
104
+
105
+ it 'prepends the host name with the bucket name' do
106
+ expect(@result.to_s).to eq(@expectation)
107
+ end
108
+ end
109
+
110
+
111
+ context 'non DNS bucket' do
112
+ before :each do
113
+ bucket = 'foo-bar_bucket'
114
+ uri = URI.parse('https://a.b.com')
115
+ @result = subject.compute_host(bucket, uri)
116
+ @expectation = 'https://a.b.com'
117
+ end
118
+
119
+ it 'has no effect on the host name' do
120
+ expect(@result.to_s).to eq(@expectation)
121
+ end
122
+ end
123
+ end
124
+
125
+
126
+ context '#compute_body' do
127
+ it 'has no effect for non Hash body' do
128
+ expect(subject.compute_body(nil, 'application/json')).to eq(nil)
129
+ expect(subject.compute_body(1, 'application/json')).to eq(1)
130
+ expect(subject.compute_body([1], 'application/json')).to eq([1])
131
+ end
132
+
133
+ it 'converts Hashes to the required content type' do
134
+ expect(subject.compute_body({1 => 2}, 'application/json')).to eq("{\"1\":2}")
135
+ expect(subject.compute_body({1 => 2}, 'application/xml')).to eq("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<1>2</1>")
136
+ end
137
+ end
138
+
139
+
140
+ context '#compute_headers!' do
141
+ before :each do
142
+ @headers = RightScale::CloudApi::HTTPHeaders.new
143
+ @body = nil
144
+ @host = 'foo_bar.host.com'
145
+ end
146
+
147
+ context 'content-type' do
148
+ it 'defaults content-type to application/octet-stream' do
149
+ result = subject.compute_headers!(@headers, @body, @host)
150
+ expectation = ['application/octet-stream']
151
+ expect(result['content-type']).to eq(expectation)
152
+ end
153
+ end
154
+
155
+
156
+ context 'date' do
157
+ it 'sets date' do
158
+ result = subject.compute_headers!(@headers, @body, @host)
159
+ expect(result['date']).to be_an(Array)
160
+ expect(result['date'].first).to be_a(String)
161
+ end
162
+ end
163
+
164
+
165
+ context 'content-md5' do
166
+ context 'body is blank' do
167
+ it 'does not set the header' do
168
+ result = subject.compute_headers!(@headers, @body, @host)
169
+ expect(result['content-md5']).to eq([])
170
+ end
171
+ end
172
+
173
+
174
+ context 'body is not blank' do
175
+ it 'does not set the header' do
176
+ result = subject.compute_headers!(@headers, 'woo-hoo', @host)
177
+ expect(result['content-md5']).to eq(['Ezs4dVuMkr7EgUDB41SEMg=='])
178
+ end
179
+ end
180
+ end
181
+
182
+
183
+ context 'host' do
184
+ it 'sets the host' do
185
+ result = subject.compute_headers!(@headers, @body, @host)
186
+ expect(result['host']).to eq([@host])
187
+ end
188
+ end
189
+ end
190
+
191
+
192
+ context '#compute_signature' do
193
+ before :each do
194
+ @secret_key = 'secret-key'
195
+ @verb = :get
196
+ @bucket = 'foo-bar'
197
+ @object = 'foo%2Fbar%2F%D0%B1%D0%B0%D0%BD%D0%B0%D0%BD%D0%B0.jpg'
198
+ @params = { 'Foo' => 1, 'Bar' => 2}
199
+ @headers = RightScale::CloudApi::HTTPHeaders.new(
200
+ 'x-amz-foo-bar' => '1',
201
+ 'x-amx-foo-boo' => '2',
202
+ 'date' => 'Fri, 11 Jul 2014 21:25:46 GMT',
203
+ 'other-header' => 'moo'
204
+ )
205
+ @expectation = "Z7hSptZVg7WytxFfM7K73henBpA="
206
+ @result = subject.compute_signature(@access_key, @secret_key, @verb, @bucket, @object, @params, @headers)
207
+ end
208
+
209
+ it 'properly calculates the signature' do
210
+ expect(@result).to eq(@expectation)
211
+ end
212
+ end
213
+
214
+
215
+ context '#compute_path' do
216
+ before :each do
217
+ @path = 'foo-bar'
218
+ @object = 'foo%2Fbar%2F%D0%B1%D0%B0%D0%BD%D0%B0%D0%BD%D0%B0.jpg'
219
+ @params = { 'Foo' => 1, 'Bar' => 2}
220
+ end
221
+
222
+ it 'works for DNS bucket' do
223
+ expect(subject.compute_path('foo-bar', @object, @params)).to eq('/foo%2Fbar%2F%D0%B1%D0%B0%D0%BD%D0%B0%D0%BD%D0%B0.jpg?Bar=2&Foo=1')
224
+ end
225
+
226
+ it 'works for non DNS bucket' do
227
+ expect(subject.compute_path('foo_bar', @object, @params)).to eq('/foo_bar/foo%2Fbar%2F%D0%B1%D0%B0%D0%BD%D0%B0%D0%BD%D0%B0.jpg?Bar=2&Foo=1')
228
+ end
229
+ end
230
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: right_aws_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - RightScale, Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-06-19 00:00:00.000000000 Z
11
+ date: 2014-07-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: right_cloud_api_base
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 3.0.0
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 3.0.0
41
55
  description: |+
42
56
  == DESCRIPTION:
43
57
 
@@ -75,6 +89,9 @@ files:
75
89
  - lib/cloud/aws/route53/manager.rb
76
90
  - lib/cloud/aws/route53/routines/request_signer.rb
77
91
  - lib/cloud/aws/route53/wrappers/default.rb
92
+ - lib/cloud/aws/s3/link/manager.rb
93
+ - lib/cloud/aws/s3/link/routines/request_signer.rb
94
+ - lib/cloud/aws/s3/link/wrappers/default.rb
78
95
  - lib/cloud/aws/s3/manager.rb
79
96
  - lib/cloud/aws/s3/parsers/response_error.rb
80
97
  - lib/cloud/aws/s3/routines/request_signer.rb
@@ -85,6 +102,8 @@ files:
85
102
  - lib/right_aws_api.rb
86
103
  - lib/right_aws_api_version.rb
87
104
  - right_aws_api.gemspec
105
+ - spec/cloud/aws/s3/link/routines/request_signer_spec.rb
106
+ - spec/cloud/aws/s3/routines/request_signer_spec.rb
88
107
  - spec/describe_calls.rb
89
108
  homepage:
90
109
  licenses: []
@@ -114,5 +133,7 @@ signing_key:
114
133
  specification_version: 4
115
134
  summary: The gem provides interface to AWS cloud services.
116
135
  test_files:
136
+ - spec/cloud/aws/s3/routines/request_signer_spec.rb
137
+ - spec/cloud/aws/s3/link/routines/request_signer_spec.rb
117
138
  - spec/describe_calls.rb
118
139
  has_rdoc: