right_aws 1.9.0 → 1.10.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -219,3 +219,28 @@ the source key.
219
219
  S3Interface::retrieve_object_and_verify (thanks to numerous user reports)
220
220
  - Updates to caching for Ec2::describe_images_by methods
221
221
  - Ec2 now has Ec2::last_request_id
222
+
223
+ === 1.10.0
224
+
225
+ Release Notes:
226
+
227
+ The big new features are SDB's SQL-like query and query_with_attributes
228
+ support as well as signature v2 support for all services. There are also
229
+ numerous bug fixes, many of them reported and patched by users and
230
+ customers.
231
+
232
+ - AwsBase: signature v2 support added
233
+ - AwsBase: fix the regex matching for xmlpath which didn't work in Ruby
234
+ 1.8.7 (thanks to a customer report)
235
+ - Ec2: describe_availability_zones improved to support regions
236
+ - Ec2: Disabled retries when EC2 returns "ServiceUnavailable: Request limit
237
+ exceeded"
238
+ - Ec2: Use POST for large queries; this avoids truncation of large user data
239
+ when launching instances (thanks to Bob for the report)
240
+ - CloudFront: docs fixes
241
+ - SDB: added: SQL-like query, select and query_with_attributes support
242
+ - SDB: fixed no method error when searching for id that doesn't exist
243
+ (thanks to multiple users for reporting this)
244
+ - S3: Fixed overzealous URL-encoding when generating URLs for S3 keys
245
+ (thanks to a bug report on the RubyForge forum)
246
+
data/README.txt CHANGED
@@ -38,7 +38,7 @@ The RightScale AWS gems comprise:
38
38
  AWS accounts.
39
39
  - Support for both first- and second-generation SQS (API versions 2007-05-01
40
40
  and 2008-01-01). These versions of SQS are not compatible.
41
- - Support for signature versions 0 and 1 on SQS, SDB, and EC2.
41
+ - Support for signature versions 0, 1 and 2 on all services.
42
42
  - Interoperability with any cloud running Eucalyptus (http://eucalyptus.cs.ucsb.edu)
43
43
  - Test suite (requires AWS account to do "live" testing).
44
44
 
@@ -119,7 +119,9 @@ multithreaded mode.
119
119
  'incompatible Net::HTTP monkey-patch'
120
120
 
121
121
  This is due to a conflict between the right_http_connection gem and another
122
- gem required by attachment_fu.
122
+ gem required by attachment_fu. It may be possible to require right_aws (and
123
+ thus right_http_connection) in the .after_initialize method of the config object in
124
+ environment.rb (check the docs for Rails::Configuration.after_initialize).
123
125
 
124
126
  - 8/07: Amazon has changed the semantics of the SQS service. A
125
127
  new queue may not be created within 60 seconds of the destruction of any
@@ -142,7 +144,7 @@ sudo gem install right_aws
142
144
 
143
145
  == LICENSE:
144
146
 
145
- Copyright (c) 2007-2008 RightScale, Inc.
147
+ Copyright (c) 2007-2009 RightScale, Inc.
146
148
 
147
149
  Permission is hereby granted, free of charge, to any person obtaining
148
150
  a copy of this software and associated documentation files (the
data/Rakefile CHANGED
@@ -10,27 +10,36 @@ require 'lib/right_aws.rb'
10
10
  testglobs = ["test/ts_right_aws.rb"]
11
11
 
12
12
 
13
- # Suppress Hoe's self-inclusion as a dependency for our Gem. This also keeps
14
- # Rake & rubyforge out of the dependency list. Users must manually install
13
+ # Suppress Hoe's self-inclusion as a dependency for our Gem. This also keeps
14
+ # Rake & rubyforge out of the dependency list. Users must manually install
15
15
  # these gems to run tests, etc.
16
+ # TRB 2/24/09: also do this for the extra_dev_deps array present in newer hoes.
17
+ # Older versions of RubyGems will try to install developer-dependencies as
18
+ # required runtime dependencies. It would be great to take advantage of the extra dev deps, but not
19
+ # at the cost of requiring many additional dependencies if the user is running an older RubyGems.
16
20
  class Hoe
17
21
  def extra_deps
18
22
  @extra_deps.reject do |x|
19
23
  Array(x).first == 'hoe'
20
24
  end
21
25
  end
26
+ def extra_dev_deps
27
+ @extra_dev_deps.reject do |x|
28
+ Array(x).first == 'hoe'
29
+ end
30
+ end
22
31
  end
23
32
 
24
33
  Hoe.new('right_aws', RightAws::VERSION::STRING) do |p|
25
- p.rubyforge_name = 'rightaws'
34
+ p.rubyforge_name = 'rightscale'
26
35
  p.author = 'RightScale, Inc.'
27
- p.email = 'support@rightscale.com'
28
- p.summary = 'Interface classes for the Amazon EC2, SQS, and S3 Web Services'
36
+ p.email = 'rubygems@rightscale.com'
37
+ p.summary = 'Interface classes for the Amazon EC2/EBS, SQS, S3, SDB, and ACF Web Services'
29
38
  p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n")
30
39
  p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1]
31
40
  p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
32
41
  p.remote_rdoc_dir = "/right_aws_gem_doc"
33
- p.extra_deps = [['right_http_connection','>= 1.2.1']]
42
+ p.extra_deps = [['right_http_connection','>= 1.2.4']]
34
43
  p.test_globs = testglobs
35
44
  end
36
45
 
@@ -198,7 +198,7 @@ module RightAws
198
198
  # Create a new distribution.
199
199
  # Returns the just created distribution or RightAws::AwsError exception.
200
200
  #
201
- # acf.create_distribution('bucket-for-k-dzreyev.s3.amazonaws.com', 'Woo-Hoo!', ['web1.my-awesome-site.net'] ) #=>
201
+ # acf.create_distribution('bucket-for-k-dzreyev.s3.amazonaws.com', 'Woo-Hoo!', true, ['web1.my-awesome-site.net'] ) #=>
202
202
  # {:comment => "Woo-Hoo!",
203
203
  # :enabled => true,
204
204
  # :location => "https://cloudfront.amazonaws.com/2008-06-30/distribution/E2REJM3VUN5RSI",
@@ -27,9 +27,69 @@ module RightAws
27
27
  require 'pp'
28
28
 
29
29
  class AwsUtils #:nodoc:
30
- @@digest = OpenSSL::Digest::Digest.new("sha1")
30
+ @@digest1 = OpenSSL::Digest::Digest.new("sha1")
31
+ @@digest256 = nil
32
+ if OpenSSL::OPENSSL_VERSION_NUMBER > 0x00908000
33
+ @@digest256 = OpenSSL::Digest::Digest.new("sha256") rescue nil # Some installation may not support sha256
34
+ end
35
+
31
36
  def self.sign(aws_secret_access_key, auth_string)
32
- Base64.encode64(OpenSSL::HMAC.digest(@@digest, aws_secret_access_key, auth_string)).strip
37
+ Base64.encode64(OpenSSL::HMAC.digest(@@digest1, aws_secret_access_key, auth_string)).strip
38
+ end
39
+
40
+ # Escape a string accordingly Amazon rulles
41
+ # http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?REST_RESTAuth.html
42
+ def self.amz_escape(param)
43
+ param.to_s.gsub(/([^a-zA-Z0-9._~-]+)/n) do
44
+ '%' + $1.unpack('H2' * $1.size).join('%').upcase
45
+ end
46
+ end
47
+
48
+ # Set a timestamp and a signature version
49
+ def self.fix_service_params(service_hash, signature)
50
+ service_hash["Timestamp"] ||= Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.000Z") unless service_hash["Expires"]
51
+ service_hash["SignatureVersion"] = signature
52
+ service_hash
53
+ end
54
+
55
+ # Signature Version 0
56
+ # A deprecated guy (should work till septemper 2009)
57
+ def self.sign_request_v0(aws_secret_access_key, service_hash)
58
+ fix_service_params(service_hash, '0')
59
+ string_to_sign = "#{service_hash['Action']}#{service_hash['Timestamp'] || service_hash['Expires']}"
60
+ service_hash['Signature'] = AwsUtils::sign(aws_secret_access_key, string_to_sign)
61
+ service_hash.to_a.collect{|key,val| "#{amz_escape(key)}=#{amz_escape(val.to_s)}" }.join("&")
62
+ end
63
+
64
+ # Signature Version 1
65
+ # Another deprecated guy (should work till septemper 2009)
66
+ def self.sign_request_v1(aws_secret_access_key, service_hash)
67
+ fix_service_params(service_hash, '1')
68
+ string_to_sign = service_hash.sort{|a,b| (a[0].to_s.downcase)<=>(b[0].to_s.downcase)}.to_s
69
+ service_hash['Signature'] = AwsUtils::sign(aws_secret_access_key, string_to_sign)
70
+ service_hash.to_a.collect{|key,val| "#{amz_escape(key)}=#{amz_escape(val.to_s)}" }.join("&")
71
+ end
72
+
73
+ # Signature Version 2
74
+ # EC2, SQS and SDB requests must be signed by this guy.
75
+ # See: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?REST_RESTAuth.html
76
+ # http://developer.amazonwebservices.com/connect/entry.jspa?externalID=1928
77
+ def self.sign_request_v2(aws_secret_access_key, service_hash, http_verb, host, uri)
78
+ fix_service_params(service_hash, '2')
79
+ # select a signing method (make an old openssl working with sha1)
80
+ # make 'HmacSHA256' to be a default one
81
+ service_hash['SignatureMethod'] = 'HmacSHA256' unless ['HmacSHA256', 'HmacSHA1'].include?(service_hash['SignatureMethod'])
82
+ service_hash['SignatureMethod'] = 'HmacSHA1' unless @@digest256
83
+ # select a digest
84
+ digest = (service_hash['SignatureMethod'] == 'HmacSHA256' ? @@digest256 : @@digest1)
85
+ # form string to sign
86
+ canonical_string = service_hash.keys.sort.map do |key|
87
+ "#{amz_escape(key)}=#{amz_escape(service_hash[key])}"
88
+ end.join('&')
89
+ string_to_sign = "#{http_verb.to_s.upcase}\n#{host.downcase}\n#{uri}\n#{canonical_string}"
90
+ # sign the string
91
+ signature = amz_escape(Base64.encode64(OpenSSL::HMAC.digest(digest, aws_secret_access_key, string_to_sign)).strip)
92
+ "#{canonical_string}&Signature=#{signature}"
33
93
  end
34
94
 
35
95
  # From Amazon's SQS Dev Guide, a brief description of how to escape:
@@ -111,7 +171,7 @@ module RightAws
111
171
  end
112
172
 
113
173
  module RightAwsBaseInterface
114
- DEFAULT_SIGNATURE_VERSION = '1'
174
+ DEFAULT_SIGNATURE_VERSION = '2'
115
175
 
116
176
  @@caching = false
117
177
  def self.caching
@@ -148,10 +208,20 @@ module RightAws
148
208
  if aws_access_key_id.blank? || aws_secret_access_key.blank?
149
209
  @aws_access_key_id = aws_access_key_id
150
210
  @aws_secret_access_key = aws_secret_access_key
151
- @params[:server] ||= service_info[:default_host]
152
- @params[:port] ||= service_info[:default_port]
153
- @params[:service] ||= service_info[:default_service]
154
- @params[:protocol] ||= service_info[:default_protocol]
211
+ # if the endpoint was explicitly defined - then use it
212
+ if @params[:endpoint_url]
213
+ @params[:server] = URI.parse(@params[:endpoint_url]).host
214
+ @params[:port] = URI.parse(@params[:endpoint_url]).port
215
+ @params[:service] = URI.parse(@params[:endpoint_url]).path
216
+ @params[:protocol] = URI.parse(@params[:endpoint_url]).scheme
217
+ @params[:region] = nil
218
+ else
219
+ @params[:server] ||= service_info[:default_host]
220
+ @params[:server] = "#{@params[:region]}.#{@params[:server]}" if @params[:region]
221
+ @params[:port] ||= service_info[:default_port]
222
+ @params[:service] ||= service_info[:default_service]
223
+ @params[:protocol] ||= service_info[:default_protocol]
224
+ end
155
225
  @params[:multi_thread] ||= defined?(AWS_DAEMON)
156
226
  @logger = @params[:logger]
157
227
  @logger = RAILS_DEFAULT_LOGGER if !@logger && defined?(RAILS_DEFAULT_LOGGER)
@@ -162,6 +232,15 @@ module RightAws
162
232
  @signature_version = (params[:signature_version] || DEFAULT_SIGNATURE_VERSION).to_s
163
233
  end
164
234
 
235
+ def signed_service_params(aws_secret_access_key, service_hash, http_verb=nil, host=nil, service=nil )
236
+ case signature_version.to_s
237
+ when '0' then AwsUtils::sign_request_v0(aws_secret_access_key, service_hash)
238
+ when '1' then AwsUtils::sign_request_v1(aws_secret_access_key, service_hash)
239
+ when '2' then AwsUtils::sign_request_v2(aws_secret_access_key, service_hash, http_verb, host, service)
240
+ else raise AwsError.new("Unknown signature version (#{signature_version.to_s}) requested")
241
+ end
242
+ end
243
+
165
244
  # Returns +true+ if the describe_xxx responses are being cached
166
245
  def caching?
167
246
  @params.key?(:cache) ? @params[:cache] : @@caching
@@ -592,8 +671,9 @@ module RightAws
592
671
  @xmlpath += @xmlpath.empty? ? name : "/#{name}"
593
672
  end
594
673
  def tag_end(name)
595
- @xmlpath[/^(.*?)\/?#{name}$/]
596
- @xmlpath = $1
674
+ if @xmlpath =~ /^(.*?)\/?#{name}$/
675
+ @xmlpath = $1
676
+ end
597
677
  tagend(name)
598
678
  end
599
679
  def text(text)
data/lib/ec2/right_ec2.rb CHANGED
@@ -68,7 +68,7 @@ module RightAws
68
68
  include RightAwsBaseInterface
69
69
 
70
70
  # Amazon EC2 API version being used
71
- API_VERSION = "2008-08-08"
71
+ API_VERSION = "2008-12-01"
72
72
  DEFAULT_HOST = "ec2.amazonaws.com"
73
73
  DEFAULT_PATH = '/'
74
74
  DEFAULT_PROTOCOL = 'https'
@@ -100,7 +100,9 @@ module RightAws
100
100
  # Create a new handle to an EC2 account. All handles share the same per process or per thread
101
101
  # HTTP connection to Amazon EC2. Each handle is for a specific account. The params have the
102
102
  # following options:
103
+ # * <tt>:endpoint_url</tt> a fully qualified url to Amazon API endpoint (this overwrites: :server, :port, :service, :protocol and :region). Example: 'https://eu-west-1.ec2.amazonaws.com/'
103
104
  # * <tt>:server</tt>: EC2 service host, default: DEFAULT_HOST
105
+ # * <tt>:region</tt>: EC2 region (North America by default)
104
106
  # * <tt>:port</tt>: EC2 service port, default: DEFAULT_PORT
105
107
  # * <tt>:protocol</tt>: 'http' or 'https', default: DEFAULT_PROTOCOL
106
108
  # * <tt>:multi_thread</tt>: true=HTTP connection per thread, false=per process
@@ -120,24 +122,34 @@ module RightAws
120
122
  aws_access_key_id || ENV['AWS_ACCESS_KEY_ID'] ,
121
123
  aws_secret_access_key|| ENV['AWS_SECRET_ACCESS_KEY'],
122
124
  params)
125
+ # EC2 doesn't really define any transient errors to retry, and in fact,
126
+ # when they return a 503 it is usually for 'request limit exceeded' which
127
+ # we most certainly should not retry. So let's pare down the list of
128
+ # retryable errors to InternalError only (see RightAwsBase for the default
129
+ # list)
130
+ amazon_problems = ['InternalError']
123
131
  end
124
132
 
125
133
 
126
134
  def generate_request(action, params={}) #:nodoc:
127
- service_hash = {"Action" => action,
128
- "AWSAccessKeyId" => @aws_access_key_id,
129
- "Version" => @@api,
130
- "Timestamp" => Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.000Z"),
131
- "SignatureVersion" => signature_version }
135
+ service_hash = {"Action" => action,
136
+ "AWSAccessKeyId" => @aws_access_key_id,
137
+ "Version" => @@api }
132
138
  service_hash.update(params)
133
- # prepare string to sign
134
- string_to_sign = case signature_version
135
- when '0' then service_hash["Action"] + service_hash["Timestamp"]
136
- when '1' then service_hash.sort{|a,b| (a[0].to_s.downcase)<=>(b[0].to_s.downcase)}.to_s
137
- end
138
- service_hash.update('Signature' => AwsUtils::sign(@aws_secret_access_key, string_to_sign))
139
- request_params = service_hash.to_a.collect{|key,val| key + "=" + CGI::escape(val) }.join("&")
140
- request = Net::HTTP::Get.new("#{@params[:service]}?#{request_params}")
139
+ service_params = signed_service_params(@aws_secret_access_key, service_hash, :get, @params[:server], @params[:service])
140
+
141
+ # use POST method if the length of the query string is too large
142
+ if service_params.size > 2000
143
+ if signature_version == '2'
144
+ # resign the request because HTTP verb is included into signature
145
+ service_params = signed_service_params(@aws_secret_access_key, service_hash, :post, @params[:server], @params[:service])
146
+ end
147
+ request = Net::HTTP::Post.new(service)
148
+ request.body = service_params
149
+ request['Content-Type'] = 'application/x-www-form-urlencoded'
150
+ else
151
+ request = Net::HTTP::Get.new("#{@params[:service]}?#{service_params}")
152
+ end
141
153
  # prepare output hash
142
154
  { :request => request,
143
155
  :server => @params[:server],
@@ -521,7 +533,7 @@ module RightAws
521
533
  # Amazon 169.254.169.254 does not like escaped symbols!
522
534
  # And it doesn't like "\n" inside of encoded string! Grrr....
523
535
  # Otherwise, some of UserData symbols will be lost...
524
- params['UserData'] = Base64.encode64(lparams[:user_data]).delete("\n") unless lparams[:user_data].blank?
536
+ params['UserData'] = Base64.encode64(lparams[:user_data]).delete("\n").strip unless lparams[:user_data].blank?
525
537
  end
526
538
  link = generate_request("RunInstances", params)
527
539
  #debugger
@@ -969,11 +981,13 @@ module RightAws
969
981
  # Describes availability zones that are currently available to the account and their states.
970
982
  # Returns an array of 2 keys (:zone_name and :zone_state) hashes:
971
983
  #
972
- # ec2.describe_availability_zones #=> [{:zone_state=>"available", :zone_name=>"us-east-1a"},
973
- # {:zone_state=>"available", :zone_name=>"us-east-1b"},
974
- # {:zone_state=>"available", :zone_name=>"us-east-1c"}]
984
+ # ec2.describe_availability_zones #=> [{:region_name=>"us-east-1",
985
+ # :zone_name=>"us-east-1a",
986
+ # :zone_state=>"available"}, ... ]
975
987
  #
976
- # ec2.describe_availability_zones('us-east-1c') #=> [{:zone_state=>"available", :zone_name=>"us-east-1c"}]
988
+ # ec2.describe_availability_zones('us-east-1c') #=> [{:region_name=>"us-east-1",
989
+ # :zone_state=>"available",
990
+ # :zone_name=>"us-east-1c"}]
977
991
  #
978
992
  def describe_availability_zones(list=[])
979
993
  link = generate_request("DescribeAvailabilityZones",
@@ -983,6 +997,23 @@ module RightAws
983
997
  on_exception
984
998
  end
985
999
 
1000
+ #-----------------------------------------------------------------
1001
+ # Regions
1002
+ #-----------------------------------------------------------------
1003
+
1004
+ # Describe regions.
1005
+ #
1006
+ # ec2.describe_regions #=> ["eu-west-1", "us-east-1"]
1007
+ #
1008
+ def describe_regions(list=[])
1009
+ link = generate_request("DescribeRegions",
1010
+ hash_params('RegionName',list.to_a))
1011
+ request_cache_or_info :describe_regions, link, QEc2DescribeRegionsParser, @@bench, list.blank?
1012
+ rescue Exception
1013
+ on_exception
1014
+ end
1015
+
1016
+
986
1017
  #-----------------------------------------------------------------
987
1018
  # EBS: Volumes
988
1019
  #-----------------------------------------------------------------
@@ -1133,6 +1164,45 @@ module RightAws
1133
1164
  rescue Exception
1134
1165
  on_exception
1135
1166
  end
1167
+
1168
+ # Create a snapshot of specified volume, but with the normal retry algorithms disabled.
1169
+ # This method will return immediately upon error. The user can specify connect and read timeouts (in s)
1170
+ # for the connection to AWS. If the user does not specify timeouts, try_create_snapshot uses the default values
1171
+ # in Rightscale::HttpConnection.
1172
+ #
1173
+ # ec2.try_create_snapshot('vol-898a6fe0') #=>
1174
+ # {:aws_volume_id => "vol-fd9f7a94",
1175
+ # :aws_started_at => Tue Jun 24 18:40:40 UTC 2008,
1176
+ # :aws_progress => "",
1177
+ # :aws_status => "pending",
1178
+ # :aws_id => "snap-d56783bc"}
1179
+ #
1180
+ def try_create_snapshot(volume_id, connect_timeout = nil, read_timeout = nil)
1181
+ # For safety in the ensure block...we don't want to restore values
1182
+ # if we never read them in the first place
1183
+ orig_reiteration_time = nil
1184
+ orig_http_params = nil
1185
+
1186
+ orig_reiteration_time = RightAws::AWSErrorHandler::reiteration_time
1187
+ RightAws::AWSErrorHandler::reiteration_time = 0
1188
+
1189
+ orig_http_params = Rightscale::HttpConnection::params()
1190
+ new_http_params = orig_http_params.dup
1191
+ new_http_params[:http_connection_retry_count] = 0
1192
+ new_http_params[:http_connection_open_timeout] = connect_timeout if !connect_timeout.nil?
1193
+ new_http_params[:http_connection_read_timeout] = read_timeout if !read_timeout.nil?
1194
+ Rightscale::HttpConnection::params = new_http_params
1195
+
1196
+ link = generate_request("CreateSnapshot",
1197
+ "VolumeId" => volume_id.to_s)
1198
+ request_info(link, QEc2CreateSnapshotParser.new(:logger => @logger))
1199
+
1200
+ rescue Exception
1201
+ on_exception
1202
+ ensure
1203
+ RightAws::AWSErrorHandler::reiteration_time = orig_reiteration_time if orig_reiteration_time
1204
+ Rightscale::HttpConnection::params = orig_http_params if orig_http_params
1205
+ end
1136
1206
 
1137
1207
  # Delete the specified snapshot.
1138
1208
  #
@@ -1527,8 +1597,9 @@ module RightAws
1527
1597
  end
1528
1598
  def tagend(name)
1529
1599
  case name
1530
- when 'zoneName' then @zone[:zone_name] = @text
1531
- when 'zoneState' then @zone[:zone_state] = @text
1600
+ when 'regionName' then @zone[:region_name] = @text
1601
+ when 'zoneName' then @zone[:zone_name] = @text
1602
+ when 'zoneState' then @zone[:zone_state] = @text
1532
1603
  when 'item' then @result << @zone
1533
1604
  end
1534
1605
  end
@@ -1537,6 +1608,19 @@ module RightAws
1537
1608
  end
1538
1609
  end
1539
1610
 
1611
+ #-----------------------------------------------------------------
1612
+ # PARSERS: Regions
1613
+ #-----------------------------------------------------------------
1614
+
1615
+ class QEc2DescribeRegionsParser < RightAWSParser #:nodoc:
1616
+ def tagend(name)
1617
+ @result << @text if name == 'regionName'
1618
+ end
1619
+ def reset
1620
+ @result = []
1621
+ end
1622
+ end
1623
+
1540
1624
  #-----------------------------------------------------------------
1541
1625
  # PARSERS: EBS - Volumes
1542
1626
  #-----------------------------------------------------------------
data/lib/right_aws.rb CHANGED
@@ -52,7 +52,7 @@ require 'acf/right_acf_interface'
52
52
  module RightAws #:nodoc:
53
53
  module VERSION #:nodoc:
54
54
  MAJOR = 1
55
- MINOR = 9
55
+ MINOR = 10
56
56
  TINY = 0
57
57
 
58
58
  STRING = [MAJOR, MINOR, TINY].join('.')
@@ -32,6 +32,7 @@ module RightAws
32
32
  DEFAULT_HOST = 's3.amazonaws.com'
33
33
  DEFAULT_PORT = 443
34
34
  DEFAULT_PROTOCOL = 'https'
35
+ DEFAULT_SERVICE = '/'
35
36
  REQUEST_TTL = 30
36
37
  DEFAULT_EXPIRES_AFTER = 1 * 24 * 60 * 60 # One day's worth of seconds
37
38
  ONE_YEAR_IN_SECONDS = 365 * 24 * 60 * 60
@@ -62,7 +63,8 @@ module RightAws
62
63
  def initialize(aws_access_key_id=nil, aws_secret_access_key=nil, params={})
63
64
  init({ :name => 'S3',
64
65
  :default_host => ENV['S3_URL'] ? URI.parse(ENV['S3_URL']).host : DEFAULT_HOST,
65
- :default_port => ENV['S3_URL'] ? URI.parse(ENV['S3_URL']).port : DEFAULT_PORT,
66
+ :default_port => ENV['S3_URL'] ? URI.parse(ENV['S3_URL']).port : DEFAULT_PORT,
67
+ :default_service => ENV['S3_URL'] ? URI.parse(ENV['S3_URL']).path : DEFAULT_SERVICE,
66
68
  :default_protocol => ENV['S3_URL'] ? URI.parse(ENV['S3_URL']).scheme : DEFAULT_PROTOCOL },
67
69
  aws_access_key_id || ENV['AWS_ACCESS_KEY_ID'],
68
70
  aws_secret_access_key || ENV['AWS_SECRET_ACCESS_KEY'],
@@ -108,29 +110,34 @@ module RightAws
108
110
  end
109
111
  true
110
112
  end
111
-
112
- # Generates request hash for REST API.
113
- # Assumes that headers[:url] is URL encoded (use CGI::escape)
114
- def generate_rest_request(method, headers) # :nodoc:
113
+
114
+ def fetch_request_params(headers) #:nodoc:
115
115
  # default server to use
116
- server = @params[:server]
117
- # fix path
118
- path_to_sign = headers[:url]
119
- path_to_sign = "/#{path_to_sign}" unless path_to_sign[/^\//]
116
+ server = @params[:server]
117
+ service = @params[:service].to_s
118
+ service.chop! if service[%r{/$}] # remove trailing '/' from service
120
119
  # extract bucket name and check it's dns compartibility
121
- path_to_sign[%r{^/([a-z0-9._-]*)(/[^?]*)?(\?.+)?}i]
120
+ headers[:url].to_s[%r{^([a-z0-9._-]*)(/[^?]*)?(\?.+)?}i]
122
121
  bucket_name, key_path, params_list = $1, $2, $3
123
122
  # select request model
124
123
  if is_dns_bucket?(bucket_name)
125
- # add backet to a server name
124
+ # fix a path
126
125
  server = "#{bucket_name}.#{server}"
127
- # remove bucket from the path
128
- path = "#{key_path || '/'}#{params_list}"
129
- # refactor the path (add '/' before params_list if the key is empty)
130
- path_to_sign = "/#{bucket_name}#{path}"
126
+ key_path ||= '/'
127
+ path = "#{service}#{key_path}#{params_list}"
131
128
  else
132
- path = path_to_sign
129
+ path = "#{service}/#{bucket_name}#{key_path}#{params_list}"
133
130
  end
131
+ path_to_sign = "#{service}/#{bucket_name}#{key_path}#{params_list}"
132
+ # path_to_sign = "/#{bucket_name}#{key_path}#{params_list}"
133
+ [ server, path, path_to_sign ]
134
+ end
135
+
136
+ # Generates request hash for REST API.
137
+ # Assumes that headers[:url] is URL encoded (use CGI::escape)
138
+ def generate_rest_request(method, headers) # :nodoc:
139
+ # calculate request data
140
+ server, path, path_to_sign = fetch_request_params(headers)
134
141
  data = headers[:data]
135
142
  # remove unset(==optional) and symbolyc keys
136
143
  headers.each{ |key, value| headers.delete(key) if (value.nil? || key.is_a?(Symbol)) }
@@ -808,26 +815,9 @@ module RightAws
808
815
 
809
816
  # Generates link for QUERY API
810
817
  def generate_link(method, headers={}, expires=nil) #:nodoc:
811
- # default server to use
812
- server = @params[:server]
813
- # fix path
814
- path_to_sign = headers[:url]
815
- path_to_sign = "/#{path_to_sign}" unless path_to_sign[/^\//]
816
- # extract bucket name and check it's dns compartibility
817
- path_to_sign[%r{^/([a-z0-9._-]*)(/[^?]*)?(\?.+)?}i]
818
- bucket_name, key_path, params_list = $1, $2, $3
819
- # select request model
820
- if is_dns_bucket?(bucket_name)
821
- # add backet to a server name
822
- server = "#{bucket_name}.#{server}"
823
- # remove bucket from the path
824
- path = "#{key_path || '/'}#{params_list}"
825
- # refactor the path (add '/' before params_list if the key is empty)
826
- path_to_sign = "/#{bucket_name}#{path}"
827
- else
828
- path = path_to_sign
829
- end
830
- # expiration time
818
+ # calculate request data
819
+ server, path, path_to_sign = fetch_request_params(headers)
820
+ # expiration time
831
821
  expires ||= DEFAULT_EXPIRES_AFTER
832
822
  expires = Time.now.utc + expires if expires.is_a?(Fixnum) && (expires < ONE_YEAR_IN_SECONDS)
833
823
  expires = expires.to_i
@@ -890,7 +880,7 @@ module RightAws
890
880
  # s3.put_link('my_awesome_bucket',key, object) #=> url string
891
881
  #
892
882
  def put_link(bucket, key, data=nil, expires=nil, headers={})
893
- generate_link('PUT', headers.merge(:url=>"#{bucket}/#{CGI::escape key}", :data=>data), expires)
883
+ generate_link('PUT', headers.merge(:url=>"#{bucket}/#{AwsUtils::URLencode key}", :data=>data), expires)
894
884
  rescue
895
885
  on_exception
896
886
  end
@@ -908,7 +898,7 @@ module RightAws
908
898
  #
909
899
  # see http://docs.amazonwebservices.com/AmazonS3/2006-03-01/VirtualHosting.html
910
900
  def get_link(bucket, key, expires=nil, headers={})
911
- generate_link('GET', headers.merge(:url=>"#{bucket}/#{CGI::escape key}"), expires)
901
+ generate_link('GET', headers.merge(:url=>"#{bucket}/#{AwsUtils::URLencode key}"), expires)
912
902
  rescue
913
903
  on_exception
914
904
  end
@@ -918,7 +908,7 @@ module RightAws
918
908
  # s3.head_link('my_awesome_bucket',key) #=> url string
919
909
  #
920
910
  def head_link(bucket, key, expires=nil, headers={})
921
- generate_link('HEAD', headers.merge(:url=>"#{bucket}/#{CGI::escape key}"), expires)
911
+ generate_link('HEAD', headers.merge(:url=>"#{bucket}/#{AwsUtils::URLencode key}"), expires)
922
912
  rescue
923
913
  on_exception
924
914
  end
@@ -928,7 +918,7 @@ module RightAws
928
918
  # s3.delete_link('my_awesome_bucket',key) #=> url string
929
919
  #
930
920
  def delete_link(bucket, key, expires=nil, headers={})
931
- generate_link('DELETE', headers.merge(:url=>"#{bucket}/#{CGI::escape key}"), expires)
921
+ generate_link('DELETE', headers.merge(:url=>"#{bucket}/#{AwsUtils::URLencode key}"), expires)
932
922
  rescue
933
923
  on_exception
934
924
  end
@@ -939,7 +929,7 @@ module RightAws
939
929
  # s3.get_acl_link('my_awesome_bucket',key) #=> url string
940
930
  #
941
931
  def get_acl_link(bucket, key='', headers={})
942
- return generate_link('GET', headers.merge(:url=>"#{bucket}/#{CGI::escape key}?acl"))
932
+ return generate_link('GET', headers.merge(:url=>"#{bucket}/#{AwsUtils::URLencode key}?acl"))
943
933
  rescue
944
934
  on_exception
945
935
  end
@@ -949,7 +939,7 @@ module RightAws
949
939
  # s3.put_acl_link('my_awesome_bucket',key) #=> url string
950
940
  #
951
941
  def put_acl_link(bucket, key='', headers={})
952
- return generate_link('PUT', headers.merge(:url=>"#{bucket}/#{CGI::escape key}?acl"))
942
+ return generate_link('PUT', headers.merge(:url=>"#{bucket}/#{AwsUtils::URLencode key}?acl"))
953
943
  rescue
954
944
  on_exception
955
945
  end