right_aws 2.1.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -38,6 +38,17 @@ module RightAws
38
38
  ONE_YEAR_IN_SECONDS = 365 * 24 * 60 * 60
39
39
  AMAZON_HEADER_PREFIX = 'x-amz-'
40
40
  AMAZON_METADATA_PREFIX = 'x-amz-meta-'
41
+ S3_REQUEST_PARAMETERS = [ 'acl',
42
+ 'location',
43
+ 'logging', # this one is beta, no support for now
44
+ 'response-content-type',
45
+ 'response-content-language',
46
+ 'response-expires',
47
+ 'response-cache-control',
48
+ 'response-content-disposition',
49
+ 'response-content-encoding',
50
+ 'torrent' ].sort
51
+
41
52
 
42
53
  @@bench = AwsBenchmarkingBlock.new
43
54
  def self.bench_xml
@@ -107,13 +118,20 @@ module RightAws
107
118
  s3_headers.sort { |a, b| a[0] <=> b[0] }.each do |key, value|
108
119
  out_string << (key[/^#{AMAZON_HEADER_PREFIX}/o] ? "#{key}:#{value}\n" : "#{value}\n")
109
120
  end
110
- # ignore everything after the question mark...
121
+ # ignore everything after the question mark by default...
111
122
  out_string << path.gsub(/\?.*$/, '')
112
- # ...unless there is an acl or torrent parameter
113
- out_string << '?acl' if path[/[&?]acl($|&|=)/]
114
- out_string << '?torrent' if path[/[&?]torrent($|&|=)/]
115
- out_string << '?location' if path[/[&?]location($|&|=)/]
116
- out_string << '?logging' if path[/[&?]logging($|&|=)/] # this one is beta, no support for now
123
+ # ... unless there is a parameter that we care about.
124
+ S3_REQUEST_PARAMETERS.each do |parameter|
125
+ if path[/[&?]#{parameter}(=[^&]*)?($|&)/]
126
+ if $1
127
+ value = CGI::unescape($1)
128
+ else
129
+ value = ''
130
+ end
131
+ out_string << (out_string[/[?]/] ? "&#{parameter}#{value}" : "?#{parameter}#{value}")
132
+ end
133
+ end
134
+
117
135
  out_string
118
136
  end
119
137
 
@@ -135,6 +153,7 @@ module RightAws
135
153
  # extract bucket name and check it's dns compartibility
136
154
  headers[:url].to_s[%r{^([a-z0-9._-]*)(/[^?]*)?(\?.+)?}i]
137
155
  bucket_name, key_path, params_list = $1, $2, $3
156
+ key_path = key_path.gsub( '%2F', '/' ) if key_path
138
157
  # select request model
139
158
  if !param(:no_subdomains) && is_dns_bucket?(bucket_name)
140
159
  # fix a path
@@ -836,6 +855,7 @@ module RightAws
836
855
  def generate_link(method, headers={}, expires=nil) #:nodoc:
837
856
  # calculate request data
838
857
  server, path, path_to_sign = fetch_request_params(headers)
858
+ path_to_sign = CGI.unescape(path_to_sign)
839
859
  # expiration time
840
860
  expires ||= DEFAULT_EXPIRES_AFTER
841
861
  expires = Time.now.utc + expires if expires.is_a?(Fixnum) && (expires < ONE_YEAR_IN_SECONDS)
@@ -844,7 +864,7 @@ module RightAws
844
864
  headers.each{ |key, value| headers.delete(key) if (value.nil? || key.is_a?(Symbol)) }
845
865
  #generate auth strings
846
866
  auth_string = canonical_string(method, path_to_sign, headers, expires)
847
- signature = CGI::escape(Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new("sha1"), @aws_secret_access_key, auth_string)).strip)
867
+ signature = CGI::escape(AwsUtils::sign( @aws_secret_access_key, auth_string))
848
868
  # path building
849
869
  addon = "Signature=#{signature}&Expires=#{expires}&AWSAccessKeyId=#{@aws_access_key_id}"
850
870
  path += path[/\?/] ? "&#{addon}" : "?#{addon}"
@@ -916,8 +936,20 @@ module RightAws
916
936
  # s3.get_link('my_awesome_bucket',key) #=> https://s3.amazonaws.com:443/my_awesome_bucket/asia%2Fcustomers?Signature=QAO...
917
937
  #
918
938
  # see http://docs.amazonwebservices.com/AmazonS3/2006-03-01/VirtualHosting.html
919
- def get_link(bucket, key, expires=nil, headers={})
920
- generate_link('GET', headers.merge(:url=>"#{bucket}/#{CGI::escape key}"), expires)
939
+ #
940
+ # To specify +response+-* parameters, define them in the response_params hash:
941
+ #
942
+ # s3.get_link('my_awesome_bucket',key,nil,{},{ "response-content-disposition" => "attachment; filename=caf�.png", "response-content-type" => "image/png"})
943
+ #
944
+ # #=> https://s3.amazonaws.com:443/my_awesome_bucket/asia%2Fcustomers?response-content-disposition=attachment%3B%20filename%3Dcaf%25C3%25A9.png&response-content-type=image%2Fpng&Signature=wio...
945
+ #
946
+ def get_link(bucket, key, expires=nil, headers={}, response_params={})
947
+ if response_params.size > 0
948
+ response_params = '?' + response_params.map { |k, v| "#{k}=#{CGI::escape(v).gsub(/[+]/, '%20')}" }.join('&')
949
+ else
950
+ response_params = ''
951
+ end
952
+ generate_link('GET', headers.merge(:url=>"#{bucket}/#{CGI::escape key}#{response_params}"), expires)
921
953
  rescue
922
954
  on_exception
923
955
  end
@@ -1171,7 +1203,7 @@ module RightAws
1171
1203
  def headers_to_string(headers)
1172
1204
  result = {}
1173
1205
  headers.each do |key, value|
1174
- value = value.to_s if value.is_a?(Array) && value.size<2
1206
+ value = value.first if value.is_a?(Array) && value.size<2
1175
1207
  result[key] = value
1176
1208
  end
1177
1209
  result
@@ -33,7 +33,7 @@ module RightAws
33
33
  DEFAULT_PORT = 443
34
34
  DEFAULT_PROTOCOL = 'https'
35
35
  DEFAULT_PATH = '/'
36
- API_VERSION = '2007-11-07'
36
+ API_VERSION = '2009-04-15'
37
37
  DEFAULT_NIL_REPRESENTATION = 'nil'
38
38
 
39
39
  @@bench = AwsBenchmarkingBlock.new
@@ -399,12 +399,21 @@ module RightAws
399
399
  # :box_usage => "0.0000093222",
400
400
  # :request_id => "81273d21-001-1111-b3f9-512d91d29ac8" }
401
401
  #
402
+ # # request all attributes using a consistent read
403
+ # # see: http://docs.amazonwebservices.com/AmazonSimpleDB/latest/DeveloperGuide/index.html?ConsistencySummary.html
404
+ # sdb.get_attributes('family', 'toys', nil, true) # => { :attributes => {"cat" => ["clew", "Jons_socks", "mouse"] },
405
+ # "Silvia" => ["beetle", "rolling_pin", "kids"],
406
+ # "Jon" => ["vacuum_cleaner", "hammer", "spade"]},
407
+ # :box_usage => "0.0000093222",
408
+ # :request_id => "81273d21-000-1111-b3f9-512d91d29ac8" }
409
+ #
402
410
  # see: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/SDB_API_GetAttributes.html
403
411
  #
404
- def get_attributes(domain_name, item_name, attribute_name=nil)
405
- link = generate_request("GetAttributes", 'DomainName' => domain_name,
406
- 'ItemName' => item_name,
407
- 'AttributeName' => attribute_name )
412
+ def get_attributes(domain_name, item_name, attribute_name=nil, consistent_read=nil)
413
+ link = generate_request("GetAttributes", 'DomainName' => domain_name,
414
+ 'ItemName' => item_name,
415
+ 'AttributeName' => attribute_name,
416
+ 'ConsistentRead' => consistent_read )
408
417
  res = request_info(link, QSdbGetAttributesParser.new)
409
418
  res[:attributes].each_value do |values|
410
419
  values.collect! { |e| sdb_to_ruby(e) }
@@ -0,0 +1,286 @@
1
+ #
2
+ # Copyright (c) 2007-2008 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
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #
23
+
24
+ module RightAws
25
+ class SnsInterface < RightAwsBase
26
+ include RightAwsBaseInterface
27
+
28
+ DEFAULT_HOST = 'sns.us-east-1.amazonaws.com'
29
+ DEFAULT_PORT = 443
30
+ DEFAULT_PROTOCOL = 'https'
31
+ DEFAULT_SERVICE = '/'
32
+ REQUEST_TTL = 30
33
+
34
+ # Apparently boilerplate stuff
35
+ @@bench = AwsBenchmarkingBlock.new
36
+ def self.bench_xml
37
+ @@bench.xml
38
+ end
39
+ def self.bench_service
40
+ @@bench.service
41
+ end
42
+
43
+ def initialize(aws_access_key_id=nil, aws_secret_access_key=nil, params={})
44
+ if params[:region]
45
+ server = "sns.#{params[:region]}.amazonaws.com"
46
+ params.delete(:region)
47
+ else
48
+ server = DEFAULT_HOST
49
+ end
50
+ init({ :name => 'SNS',
51
+ :default_host => ENV['SNS_URL'] ? URI.parse(ENV['SNS_URL']).host : server,
52
+ :default_port => ENV['SNS_URL'] ? URI.parse(ENV['SNS_URL']).port : DEFAULT_PORT,
53
+ :default_service => ENV['SNS_URL'] ? URI.parse(ENV['SNS_URL']).path : DEFAULT_SERVICE,
54
+ :default_protocol => ENV['SNS_URL'] ? URI.parse(ENV['SNS_URL']).scheme : DEFAULT_PROTOCOL},
55
+ aws_access_key_id || ENV['AWS_ACCESS_KEY_ID'],
56
+ aws_secret_access_key || ENV['AWS_SECRET_ACCESS_KEY'],
57
+ params)
58
+ end
59
+
60
+ # TODO: RJG - Seems like generate_request and generate_rest_request could be in a sub class?
61
+ # Generates a request hash for the sns API
62
+ def generate_request(action, params={}) # :nodoc:
63
+ # Sometimes we need to use queue uri (delete queue etc)
64
+ # In that case we will use Symbol key: 'param[:queue_url]'
65
+ service = params[:sns_url] ? URI(params[:sns_url]).path : '/'
66
+ # remove unset(=optional) and symbolyc keys
67
+ params.each{ |key, value| params.delete(key) if (value.nil? || key.is_a?(Symbol)) }
68
+ # prepare output hash
69
+ service_hash = { "Action" => action,
70
+ "Expires" => (Time.now + REQUEST_TTL).utc.strftime("%Y-%m-%dT%H:%M:%SZ"),
71
+ "AWSAccessKeyId" => @aws_access_key_id }
72
+ #"Version" => API_VERSION }
73
+ service_hash.update(params)
74
+ service_params = signed_service_params(@aws_secret_access_key, service_hash, :get, @params[:server], service)
75
+ request = Net::HTTP::Get.new("#{AwsUtils::URLencode(service)}?#{service_params}")
76
+ # prepare output hash
77
+ { :request => request,
78
+ :server => @params[:server],
79
+ :port => @params[:port],
80
+ :protocol => @params[:protocol] }
81
+ end
82
+
83
+ # Generates a request hash for the REST API
84
+ def generate_rest_request(method, param) # :nodoc:
85
+ sns_uri = param[:sns_url] ? URI(param[:sns_url]).path : '/'
86
+ message = param[:message] # extract message body if nesessary
87
+ # remove unset(=optional) and symbolyc keys
88
+ param.each{ |key, value| param.delete(key) if (value.nil? || key.is_a?(Symbol)) }
89
+ # created request
90
+ param_to_str = param.to_a.collect{|key,val| key.to_s + "=" + CGI::escape(val.to_s) }.join("&")
91
+ param_to_str = "?#{param_to_str}" unless param_to_str.right_blank?
92
+ request = "Net::HTTP::#{method.capitalize}".right_constantize.new("#{sns_uri}#{param_to_str}")
93
+ request.body = message if message
94
+ # set main headers
95
+ request['content-md5'] = ''
96
+ request['Content-Type'] = 'text/plain'
97
+ request['Date'] = Time.now.httpdate
98
+ # generate authorization string
99
+ auth_string = "#{method.upcase}\n#{request['content-md5']}\n#{request['Content-Type']}\n#{request['Date']}\n#{CGI::unescape(sns_uri)}"
100
+ signature = AwsUtils::sign(@aws_secret_access_key, auth_string)
101
+ # set other headers
102
+ request['Authorization'] = "AWS #{@aws_access_key_id}:#{signature}"
103
+ #request['AWS-Version'] = API_VERSION
104
+ # prepare output hash
105
+ { :request => request,
106
+ :server => @params[:server],
107
+ :port => @params[:port],
108
+ :protocol => @params[:protocol] }
109
+ end
110
+
111
+ # Sends request to Amazon and parses the response
112
+ # Raises AwsError if any banana happened
113
+ def request_info(request, parser) # :nodoc:
114
+ request_info_impl(:sns_connection, @@bench, request, parser)
115
+ end
116
+
117
+ def create_topic(topic_name)
118
+ req_hash = generate_request('CreateTopic', 'Name' => topic_name)
119
+ request_info(req_hash, SnsCreateTopicParser.new)
120
+ end
121
+
122
+ def list_topics()
123
+ req_hash = generate_request('ListTopics')
124
+ request_info(req_hash, SnsListTopicsParser.new)
125
+ end
126
+
127
+ def delete_topic(topic_arn)
128
+ req_hash = generate_request('DeleteTopic', 'TopicArn' => topic_arn)
129
+ request_info(req_hash, RightHttp2xxParser.new)
130
+ end
131
+
132
+ def subscribe(topic_arn, protocol, endpoint)
133
+ req_hash = generate_request('Subscribe', 'TopicArn' => topic_arn, 'Protocol' => protocol, 'Endpoint' => endpoint)
134
+ request_info(req_hash, SnsSubscribeParser.new)
135
+ end
136
+
137
+ def unsubscribe(subscription_arn)
138
+ req_hash = generate_request('Unsubscribe', 'SubscriptionArn' => subscription_arn)
139
+ request_info(req_hash, RightHttp2xxParser.new)
140
+ end
141
+
142
+ def publish(topic_arn, message, subject)
143
+ req_hash = generate_request('Publish', 'TopicArn' => topic_arn, 'Message' => message, 'Subject' => subject)
144
+ request_info(req_hash, SnsPublishParser.new)
145
+ end
146
+
147
+ def set_topic_attribute(topic_arn, attribute_name, attribute_value)
148
+ if attribute_name != 'Policy' && attribute_name != 'DisplayName'
149
+ raise(ArgumentError, "The only values accepted for the attribute_name parameter are (Policy, DisplayName)")
150
+ end
151
+ req_hash = generate_request('SetTopicAttributes', 'TopicArn' => topic_arn, 'AttributeName' => attribute_name, 'AttributeValue' => attribute_value)
152
+ request_info(req_hash, RightHttp2xxParser.new)
153
+ end
154
+
155
+ def get_topic_attributes(topic_arn)
156
+ req_hash = generate_request('GetTopicAttributes', 'TopicArn' => topic_arn)
157
+ request_info(req_hash, SnsGetTopicAttributesParser.new)
158
+ end
159
+
160
+ # Calls either the ListSubscriptions or ListSubscriptionsByTopic depending on whether or not the topic_arn parameter is provided.
161
+ def list_subscriptions(topic_arn = nil)
162
+ req_hash = topic_arn ? generate_request('ListSubscriptionsByTopic', 'TopicArn' => topic_arn) : generate_request('ListSubscriptions')
163
+ request_info(req_hash, SnsListSubscriptionsParser.new)
164
+ end
165
+
166
+ def confirm_subscription(topic_arn, token, authenticate_on_unsubscribe=false)
167
+ req_hash = generate_request('ConfirmSubscription', 'AuthenticateOnUnsubscribe' => authenticate_on_unsubscribe.to_s, 'Token' => token, 'TopicArn' => topic_arn)
168
+ request_info(req_hash, SnsConfirmSubscriptionParser.new)
169
+ end
170
+
171
+ def add_permission(topic_arn, label, acct_action_hash_ary)
172
+ n_hash = {
173
+ 'TopicArn' => topic_arn,
174
+ 'Label' => label
175
+ }
176
+
177
+ acct_action_hash_ary.each_with_index do |hash_val, idx|
178
+ n_hash["AWSAccountId.member.#{idx+1}"] = hash_val[:aws_account_id]
179
+ n_hash["ActionName.member.#{idx+1}"] = hash_val[:action]
180
+ end
181
+
182
+ req_hash = generate_request('AddPermission', n_hash)
183
+ request_info(req_hash, RightHttp2xxParser.new)
184
+ end
185
+
186
+ def remove_permission(topic_arn, label)
187
+ req_hash = generate_request('RemovePermission', 'TopicArn' => topic_arn, 'Label' => label)
188
+ request_info(req_hash, RightHttp2xxParser.new)
189
+ end
190
+
191
+ class SnsCreateTopicParser < RightAWSParser # :nodoc:
192
+ def reset
193
+ @result = ''
194
+ @request_id = ''
195
+ end
196
+ def tagend(name)
197
+ case name
198
+ when 'RequestId' then @result_id = @text
199
+ when 'TopicArn' then @result = @text
200
+ end
201
+ end
202
+ end
203
+
204
+ class SnsListTopicsParser < RightAWSParser # :nodoc:
205
+ def reset
206
+ @result = []
207
+ @request_id = ''
208
+ end
209
+ def tagstart(name, attributes)
210
+ @current_key = {} if name == 'member'
211
+ end
212
+ def tagend(name)
213
+ case name
214
+ when 'RequestId' then @result_id = @text
215
+ when 'TopicArn' then @current_key[:arn] = @text
216
+ when 'member' then @result << @current_key
217
+ end
218
+ end
219
+ end
220
+
221
+ class SnsSubscribeParser < RightAWSParser # :nodoc:
222
+ def reset
223
+ @result = ''
224
+ end
225
+ def tagend(name)
226
+ case name
227
+ when 'SubscriptionArn' then @result = @text
228
+ end
229
+ end
230
+ end
231
+
232
+ class SnsPublishParser < RightAWSParser # :nodoc:
233
+ def reset
234
+ @result = ''
235
+ end
236
+ def tagend(name)
237
+ case name
238
+ when 'MessageId' then @result = @text
239
+ end
240
+ end
241
+ end
242
+
243
+ class SnsGetTopicAttributesParser < RightAWSParser # :nodoc:
244
+ def reset
245
+ @result = {}
246
+ end
247
+ def tagend(name)
248
+ case name
249
+ when 'key' then @current_attr = @text
250
+ when 'value' then @result[@current_attr] = @text
251
+ end
252
+ end
253
+ end
254
+
255
+ class SnsListSubscriptionsParser < RightAWSParser # :nodoc:
256
+ def reset
257
+ @result = []
258
+ end
259
+ def tagstart(name, attributes)
260
+ @current_key = {} if name == 'member'
261
+ end
262
+ def tagend(name)
263
+ case name
264
+ when 'TopicArn' then @current_key[:topic_arn] = @text
265
+ when 'Protocol' then @current_key[:protocol] = @text
266
+ when 'SubscriptionArn' then @current_key[:subscription_arn] = @text
267
+ when 'Owner' then @current_key[:owner] = @text
268
+ when 'Endpoint' then @current_key[:endpoint] = @text
269
+ when 'member' then @result << @current_key
270
+ end
271
+ end
272
+ end
273
+
274
+ class SnsConfirmSubscriptionParser < RightAWSParser # :nodoc:
275
+ def reset
276
+ @result = ''
277
+ end
278
+ def tagend(name)
279
+ case name
280
+ when 'SubscriptionArn' then @result = @text
281
+ end
282
+ end
283
+ end
284
+
285
+ end
286
+ end
@@ -0,0 +1,39 @@
1
+ # Notes and tips for developers
2
+
3
+ ## Setting up credentials for testing
4
+
5
+ Before you can run any tests, you need to set up credentials (API key and secret) that
6
+ will be used when talking to AWS. The credentials are loaded in `test/test_credentials.rb`
7
+ and are expected to be found in `~/.rightscale/testcredentials.rb` and look like this:
8
+
9
+ TestCredentials.aws_access_key_id= 'AAAAAAAAAAAAAAAAAAAA'
10
+ TestCredentials.aws_secret_access_key= 'asdfasdfsadf'
11
+ TestCredentials.account_number= '???'
12
+
13
+ If you prefer to store your secret key in the OS X keychain, you can do this:
14
+
15
+ def secret_access_key_from_keychain (key_id)
16
+ dump = `security -q find-generic-password -a "#{key_id}" -g 2>&1`
17
+ dump[/password: "(.*)"/, 1]
18
+ end
19
+
20
+ TestCredentials.aws_access_key_id= 'AAAAAAAAAAAAAAAAAAAA'
21
+ TestCredentials.aws_secret_access_key= secret_access_key_from_keychain(TestCredentials.aws_access_key_id)
22
+ TestCredentials.account_number= '???'
23
+
24
+ ## Running tests
25
+
26
+ There is no test suite that runs all tests. Each module is tested separately. E.g.,
27
+ to run the Load Balancer tests, run `rake testelb`. Run `rake -T` for a full list.
28
+
29
+ Some tests need to launch services on AWS to have something to test. This means two things:
30
+
31
+ 1. Running all the tests will cost you money.
32
+ 2. You will need to shut down some services separately once you are done, or things
33
+ will keep running and cost you money.
34
+
35
+ As an example, the ELB and Route 53 tests need a load balancer for testing. Starting a load balancer
36
+ for every test would make every test case cost as much as running the LB for one hour, so it makes
37
+ more sense to leave it running until it's no longer needed.
38
+
39
+ The ELB tests contain instructions for shutting down the load balancer.