right_aws_api 0.1.0 → 0.2.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 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: