right_aws 2.1.0 → 3.0.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.
@@ -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.