public
Description: A Ruby Gem that gives you full access to several of the Amazon Web Services API from your Ruby/Ruby on Rails apps
Homepage: http://github.com/grempe/amazon-ec2
Clone URL: git://github.com/grempe/amazon-ec2.git
Click here to lend your support to: amazon-ec2 and make a donation at www.pledgie.com !
amazon-ec2 / lib / EC2.rb
100644 271 lines (216 sloc) 10.318 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
#--
# Amazon Web Services EC2 Query API Ruby library
#
# Ruby Gem Name:: amazon-ec2
# Author:: Glenn Rempe (mailto:glenn@rempe.us)
# Copyright:: Copyright (c) 2007-2008 Glenn Rempe
# License:: Distributes under the same terms as Ruby
# Home:: http://github.com/grempe/amazon-ec2/tree/master
#++
 
%w[ base64 cgi openssl digest/sha1 net/https rexml/document time ostruct ].each { |f| require f }
 
# Require any lib files that we have bundled with this Ruby Gem in the lib/EC2 directory.
# Parts of the EC2 module and Base class are broken out into separate
# files for maintainability and are organized by the functional groupings defined
# in the EC2 API developers guide.
Dir[File.join(File.dirname(__FILE__), 'EC2/**/*.rb')].sort.each { |lib| require lib }
 
module EC2
 
  # Which host FQDN will we connect to for all API calls to AWS?
  # If EC2_URL is defined in the users ENV we can use that. It is
  # expected that this var is set with something like:
  # export EC2_URL='https://ec2.amazonaws.com'
  #
  if ENV['EC2_URL']
    EC2_URL = ENV['EC2_URL']
    VALID_HOSTS = ['https://ec2.amazonaws.com', 'https://us-east-1.ec2.amazonaws.com', 'https://eu-west-1.ec2.amazonaws.com']
    raise ArgumentError, "Invalid EC2_URL environment variable : #{EC2_URL}" unless VALID_HOSTS.include?(EC2_URL)
    DEFAULT_HOST = URI.parse(EC2_URL).host
  else
    # default US host
    DEFAULT_HOST = 'ec2.amazonaws.com'
  end
 
  # This is the version of the API as defined by Amazon Web Services
  API_VERSION = '2008-12-01'
 
  # Builds the canonical string for signing. This strips out all '&', '?', and '='
  # from the query string to be signed.
  # Note: The parameters in the path passed in must already be sorted in
  # case-insensitive alphabetical order and must not be url encoded.
  def EC2.canonical_string(params, host = DEFAULT_HOST, method="POST", base="/")
    # Sort, and encode parameters into a canonical string.
    sorted_params = params.sort {|x,y| x[0] <=> y[0]}
    encoded_params = sorted_params.collect do |p|
      encoded = (CGI::escape(p[0].to_s) +
                 "=" + CGI::escape(p[1].to_s))
      # Ensure spaces are encoded as '%20', not '+'
      encoded.gsub('+', '%20')
    end
    sigquery = encoded_params.join("&")
 
    # Generate the request description string
    req_desc =
      method + "\n" +
      host + "\n" +
      base + "\n" +
      sigquery
 
  end
 
  # Encodes the given string with the secret_access_key, by taking the
  # hmac-sha1 sum, and then base64 encoding it. Optionally, it will also
  # url encode the result of that to protect the string if it's going to
  # be used as a query string parameter.
  def EC2.encode(secret_access_key, str, urlencode=true)
    digest = OpenSSL::Digest::Digest.new('sha1')
    b64_hmac =
      Base64.encode64(
        OpenSSL::HMAC.digest(digest, secret_access_key, str)).gsub("\n","")
 
    if urlencode
      return CGI::escape(b64_hmac)
    else
      return b64_hmac
    end
  end
 
 
  #Introduction:
  #
  # The library exposes one main interface class, 'EC2::Base'.
  # This class provides all the methods for using the EC2 service
  # including the handling of header signing and other security issues .
  # This class uses Net::HTTP to interface with the EC2 Query API interface.
  #
  #Required Arguments:
  #
  # :access_key_id => String (default : "")
  # :secret_access_key => String (default : "")
  #
  #Optional Arguments:
  #
  # :use_ssl => Boolean (default : true)
  # :server => String (default : 'ec2.amazonaws.com')
  # :proxy_server => String (default : nil)
  #
  class Base
 
    attr_reader :use_ssl, :server, :proxy_server, :port
 
    def initialize( options = {} )
 
      options = { :access_key_id => "",
                  :secret_access_key => "",
                  :use_ssl => true,
                  :server => DEFAULT_HOST,
                  :proxy_server => nil
                  }.merge(options)
 
      @server = options[:server]
      @proxy_server = options[:proxy_server]
      @use_ssl = options[:use_ssl]
 
      raise ArgumentError, "No :access_key_id provided" if options[:access_key_id].nil? || options[:access_key_id].empty?
      raise ArgumentError, "No :secret_access_key provided" if options[:secret_access_key].nil? || options[:secret_access_key].empty?
      raise ArgumentError, "No :use_ssl value provided" if options[:use_ssl].nil?
      raise ArgumentError, "Invalid :use_ssl value provided, only 'true' or 'false' allowed" unless options[:use_ssl] == true || options[:use_ssl] == false
      raise ArgumentError, "No :server provided" if options[:server].nil? || options[:server].empty?
      
      if options[:port]
        # user-specified port
        @port = options[:port]
      elsif @use_ssl
        # https
        @port = 443
      else
        # http
        @port = 80
      end
 
      @access_key_id = options[:access_key_id]
      @secret_access_key = options[:secret_access_key]
 
      # Use proxy server if defined
      # Based on patch by Mathias Dalheimer. 20070217
      proxy = @proxy_server ? URI.parse(@proxy_server) : OpenStruct.new
      @http = Net::HTTP::Proxy( proxy.host,
                                proxy.port,
                                proxy.user,
                                proxy.password).new(options[:server], @port)
 
      @http.use_ssl = @use_ssl
 
      # Don't verify the SSL certificates. Avoids SSL Cert warning in log on every GET.
      @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
 
    end
 
 
    private
 
      # pathlist is a utility method which takes a key string and and array as input.
      # It converts the array into a Hash with the hash key being 'Key.n' where
      # 'n' increments by 1 for each iteration. So if you pass in args
      # ("ImageId", ["123", "456"]) you should get
      # {"ImageId.1"=>"123", "ImageId.2"=>"456"} returned.
      def pathlist(key, arr)
        params = {}
        arr.each_with_index do |value, i|
          params["#{key}.#{i+1}"] = value
        end
        params
      end
 
 
      # Make the connection to AWS EC2 passing in our request. This is generally called from
      # within a 'Response' class object or one of its sub-classes so the response is interpreted
      # in its proper context. See lib/EC2/responses.rb
      def make_request(action, params, data='')
 
        @http.start do
 
          # remove any keys that have nil or empty values
          params.reject! { |key, value| value.nil? or value.empty?}
 
          params.merge!( {"Action" => action,
                          "SignatureVersion" => "2",
                          "SignatureMethod" => 'HmacSHA1',
                          "AWSAccessKeyId" => @access_key_id,
                          "Version" => API_VERSION,
                          "Timestamp"=>Time.now.getutc.iso8601} )
 
          sig = get_aws_auth_param(params, @secret_access_key, @server)
 
          query = params.sort.collect do |param|
            CGI::escape(param[0]) + "=" + CGI::escape(param[1])
          end.join("&") + "&Signature=" + sig
 
          req = Net::HTTP::Post.new("/")
          req.content_type = 'application/x-www-form-urlencoded'
          req['User-Agent'] = "github-amazon-ec2-ruby-gem"
 
          response = @http.request(req, query)
 
          # Make a call to see if we need to throw an error based on the response given by EC2
          # All error classes are defined in EC2/exceptions.rb
          ec2_error?(response)
 
          return response
 
        end
 
      end
 
      # Set the Authorization header using AWS signed header authentication
      def get_aws_auth_param(params, secret_access_key, server)
        canonical_string = EC2.canonical_string(params, server)
        encoded_canonical = EC2.encode(secret_access_key, canonical_string)
      end
 
      # allow us to have a one line call in each method which will do all of the work
      # in making the actual request to AWS.
      def response_generator( options = {} )
 
        options = {
          :action => "",
          :params => {}
        }.merge(options)
 
        raise ArgumentError, ":action must be provided to response_generator" if options[:action].nil? || options[:action].empty?
 
        http_response = make_request(options[:action], options[:params])
        http_xml = http_response.body
        return Response.parse(:xml => http_xml)
 
      end
 
      # Raises the appropriate error if the specified Net::HTTPResponse object
      # contains an Amazon EC2 error; returns +false+ otherwise.
      def ec2_error?(response)
 
        # return false if we got a HTTP 200 code,
        # otherwise there is some type of error (40x,50x) and
        # we should try to raise an appropriate exception
        # from one of our exception classes defined in
        # exceptions.rb
        return false if response.is_a?(Net::HTTPSuccess)
 
        # parse the XML document so we can walk through it
        doc = REXML::Document.new(response.body)
 
        # Check that the Error element is in the place we would expect.
        # and if not raise a generic error exception
        unless doc.root.elements['Errors'].elements['Error'].name == 'Error'
          raise Error, "Unexpected error format. response.body is: #{response.body}"
        end
 
        # An valid error response looks like this:
        # <?xml version="1.0"?><Response><Errors><Error><Code>InvalidParameterCombination</Code><Message>Unknown parameter: foo</Message></Error></Errors><RequestID>291cef62-3e86-414b-900e-17246eccfae8</RequestID></Response>
        # AWS EC2 throws some exception codes that look like Error.SubError. Since we can't name classes this way
        # we need to strip out the '.' in the error 'Code' and we name the error exceptions with this
        # non '.' name as well.
        error_code = doc.root.elements['Errors'].elements['Error'].elements['Code'].text.gsub('.', '')
        error_message = doc.root.elements['Errors'].elements['Error'].elements['Message'].text
 
        # Raise one of our specific error classes if it exists.
        # otherwise, throw a generic EC2 Error with a few details.
        if EC2.const_defined?(error_code)
          raise EC2.const_get(error_code), error_message
        else
          raise EC2::Error, error_message
        end
 
      end
 
  end
 
end