~awstools-dev/ubuntu/maverick/ec2-ami-tools/maverick

« back to all changes in this revision

Viewing changes to lib/ec2/common/http.rb

  • Committer: Bazaar Package Importer
  • Author(s): Chuck Short
  • Date: 2008-10-14 08:35:25 UTC
  • Revision ID: james.westby@ubuntu.com-20081014083525-c0n69wr7r7aqfb8w
Tags: 1.3-26357-0ubuntu2
* New upstream version.
* Update the debian copyright file.
* Added quilt patch system to make it easier to maintain. 

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2008 Amazon.com, Inc. or its affiliates.  All Rights
 
2
# Reserved.  Licensed under the Amazon Software License (the
 
3
# "License").  You may not use this file except in compliance with the
 
4
# License. A copy of the License is located at
 
5
# http://aws.amazon.com/asl or in the "license" file accompanying this
 
6
# file.  This file is distributed on an "AS IS" BASIS, WITHOUT
 
7
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See
 
8
# the License for the specific language governing permissions and
 
9
# limitations under the License.
 
10
 
 
11
# ---------------------------------------------------------------------------
 
12
# Module that provides http functionality.
 
13
# ---------------------------------------------------------------------------
 
14
require 'uri'
 
15
require 'set'
 
16
require 'time'
 
17
require 'base64'
 
18
require 'tmpdir'
 
19
require 'tempfile'
 
20
require 'fileutils'
 
21
 
 
22
require 'ec2/common/curl'
 
23
require 'ec2/amitools/crypto'
 
24
 
 
25
module EC2
 
26
  module Common
 
27
    module HTTP
 
28
    
 
29
      class Response < EC2::Common::Curl::Response
 
30
        attr_reader :body
 
31
        def initialize(code, type, body=nil)
 
32
          super code, type
 
33
          @body = body
 
34
        end
 
35
      end
 
36
      
 
37
      class Headers
 
38
        MANDATORY = Set.new( [ 'content-md5', 'content-type', 'date' ] )
 
39
        X_AMZ_PREFIX = 'x-amz'
 
40
        
 
41
        def initialize(verb)
 
42
          raise ArgumentError.new('invalid verb') if verb.to_s.empty?
 
43
          @headers = {}
 
44
          @verb = verb
 
45
        end
 
46
        
 
47
        
 
48
        #---------------------------------------------------------------------
 
49
        # Add a Header key-value pair.        
 
50
        def add(name, value)
 
51
          raise ArgumentError.new( "name must be a String" ) unless name.is_a? String
 
52
          raise ArgumentError.new( "value must be a String" ) unless value.is_a? String
 
53
          @headers[name.downcase.strip] = value.downcase.strip
 
54
        end
 
55
        
 
56
        
 
57
        #-----------------------------------------------------------------------
 
58
        # Sign the headers using HMAC SHA1.
 
59
        def sign( aws_access_key_id, aws_secret_access_key, url )
 
60
      
 
61
          @headers['date'] = Time.now.httpdate          # add HTTP Date header
 
62
          
 
63
          # Build the data to sign.
 
64
          data = @verb + "\n"
 
65
          
 
66
          # Add new lines for content-type and content-md5 if not present.
 
67
          data += "\n" unless @headers.include?( 'content-type' )
 
68
          data += "\n" unless @headers.include?( 'content-md5' )          
 
69
          
 
70
          # Add mandatory headers and those that start with the x-amz prefix.
 
71
          @headers.sort.each do |name, value|
 
72
            if MANDATORY.include?( name )
 
73
              data += value + "\n"
 
74
            end
 
75
            
 
76
            # Headers that start with x-amz must have both their name and value 
 
77
            # added.
 
78
            if name =~ /^#{X_AMZ_PREFIX}/
 
79
              data += name + ":" + value +"\n"
 
80
            end
 
81
          end
 
82
          
 
83
          
 
84
          # Ignore everything in the URL after the question mark unless, by the
 
85
          # S3 protocol, it signifies an acl or torrent or logging parameter
 
86
          data << URI.parse(url).path
 
87
          ['acl', 'logging', 'torrent'].each do |item|
 
88
            regex = Regexp.new("[&?]#{item}($|&|=)")
 
89
            data << '?' + item if regex.match(url)
 
90
          end
 
91
          
 
92
          # Sign headers and then put signature back into headers.
 
93
          signature = Base64.encode64( Crypto::hmac_sha1( aws_secret_access_key, data ) )
 
94
          signature.chomp!
 
95
          @headers['Authorization'] = "AWS #{aws_access_key_id}:#{signature}"
 
96
        end
 
97
        
 
98
        
 
99
        #-----------------------------------------------------------------------
 
100
        # Return the headers as a map from header name to header value.
 
101
        def get
 
102
          return @headers.clone
 
103
        end
 
104
        
 
105
        
 
106
        #-----------------------------------------------------------------------
 
107
        # Return the headers as curl arguments of the form "-H name:value".
 
108
        def curl_arguments
 
109
          @headers.map { | name, value | "-H \"#{name}:#{value}\""}.join(' ')
 
110
        end
 
111
      end
 
112
    
 
113
    
 
114
      #-----------------------------------------------------------------------      
 
115
      # Errors.
 
116
      class Error < RuntimeError
 
117
        attr_reader :code
 
118
        def initialize(msg, code = nil)          
 
119
          super(msg)
 
120
          @code = code || 1
 
121
        end
 
122
        class PathInvalid < Error; end
 
123
        class Write < Error;       end
 
124
        class BadDigest < Error
 
125
          def initialize(file, expected, obtained)
 
126
            super("Digest for file '#{file}' #{obtained} differs from expected digest #{digest}")
 
127
          end
 
128
        end
 
129
        class Transfer < Error;    end
 
130
        class Retrieve < Transfer; end
 
131
      end
 
132
      
 
133
      
 
134
      #-----------------------------------------------------------------------      
 
135
      # Invoke curl with arguments given and process results of output file
 
136
      def HTTP::invoke( arguments, outfile, debug=false )
 
137
        begin
 
138
          raise ArgumentError.new(outfile) unless File.exists? outfile
 
139
          result = EC2::Common::Curl.execute(arguments, debug)
 
140
          if result.success?
 
141
            if result.response.success?
 
142
              return result.response
 
143
            else
 
144
              synopsis= 'Server.Error(' + result.response.code.to_s + '): '
 
145
              message = result.stderr + ' '
 
146
              if result.response.type == 'application/xml'                
 
147
                require 'rexml/document'
 
148
                doc = REXML::Document.new(IO.read(outfile))
 
149
                if doc.root
 
150
                  content = REXML::XPath.first(doc, '/Error/Code')
 
151
                  unless content.nil? or content.text.empty?
 
152
                    synopsis= 'Server.'+ content.text + '(' + 
 
153
                      result.response.code.to_s + '): '
 
154
                  end
 
155
                  content = REXML::XPath.first(doc, '/Error/Message')
 
156
                  message = content.text unless content.nil?
 
157
                end
 
158
              else
 
159
                if result.response.type =~ /text/
 
160
                  message << IO.read(outfile)
 
161
                end
 
162
              end
 
163
              raise Error::Transfer.new(synopsis + message, result.response.code)
 
164
            end
 
165
          else
 
166
            synopsis= 'Curl.Error(' + result.status.to_s + '): '
 
167
            message = result.stderr.split("\n").map { |line|            
 
168
              if (m = /^curl:\s+(?:\(\d{1,2}\)\s+)*(.*)$/.match(line) )
 
169
                (m.captures[0] == "try 'curl --help' for more information") ? 
 
170
                  '' : m.captures[0]
 
171
              else
 
172
                line.strip
 
173
              end
 
174
            }.join("\n")
 
175
            output = result.stdout.chomp
 
176
            if debug and not output.empty?
 
177
              message << "\nCurl.Output: " + output.gsub("\n", "\nCurl.Output: ")
 
178
            end
 
179
            raise Error::Transfer.new(synopsis + message.strip + '.', result.status)
 
180
          end
 
181
        rescue EC2::Common::Curl::Error => e
 
182
          raise Error::Transfer.new(e.message, e.code)
 
183
        end
 
184
      end
 
185
      
 
186
      #-----------------------------------------------------------------------      
 
187
      # Delete the file at the specified url.
 
188
      def HTTP::delete( url, options={}, user = nil, pass = nil, debug = false )
 
189
        raise ArgumentError.new('options') unless options.is_a? Hash
 
190
        begin
 
191
          output = Tempfile.new('ec2-delete-response')
 
192
          output.close
 
193
          arguments = ['-X DELETE']
 
194
          
 
195
          headers = EC2::Common::HTTP::Headers.new( 'DELETE' )
 
196
          options.each do |name, value| headers.add( name, value ) end
 
197
          headers.sign( user, pass, url ) if user and pass
 
198
          arguments << headers.curl_arguments
 
199
          
 
200
          arguments << "'#{url}'"
 
201
          arguments << '-o ' + output.path
 
202
          
 
203
          response = HTTP::invoke(arguments.join(' '), output.path, debug)
 
204
          return EC2::Common::HTTP::Response.new(response.code, response.type)
 
205
        ensure
 
206
          output.close(true)
 
207
          GC.start
 
208
        end
 
209
      end
 
210
      
 
211
  
 
212
      #-----------------------------------------------------------------------      
 
213
      # Put the file at the specified path to the specified url. The content of
 
214
      # the options hash will be passed as HTTP headers. If the username and
 
215
      # password options are specified, then the headers will be signed.
 
216
      def HTTP::put( url, path, options={}, user = nil, pass = nil, debug = false )
 
217
        raise Error::PathInvalid.new( path ) unless path and File::exist?( path )
 
218
        raise ArgumentError.new('options') unless options.is_a? Hash
 
219
        
 
220
        begin
 
221
          output = Tempfile.new('ec2-put-response')
 
222
          output.close
 
223
          arguments = []
 
224
          
 
225
          headers = EC2::Common::HTTP::Headers.new('PUT')
 
226
          options.each do |name, value| headers.add( name, value ) end
 
227
          headers.sign( user, pass, url ) if user and pass
 
228
          arguments << headers.curl_arguments
 
229
          
 
230
          arguments << "'#{url}'"
 
231
          arguments << '-T ' + path
 
232
          arguments << '-o ' + output.path
 
233
          
 
234
          response = HTTP::invoke(arguments.join(' '), output.path, debug)
 
235
          return EC2::Common::HTTP::Response.new(response.code, response.type)          
 
236
        ensure
 
237
          output.close(true)
 
238
          GC.start
 
239
        end
 
240
      end
 
241
  
 
242
      #-----------------------------------------------------------------------      
 
243
      # Save the file at specified url, to the local file at specified path.
 
244
      # The local file will be created if necessary, or overwritten already
 
245
      # existing. If specified, the expected digest is compare to that of the
 
246
      # retrieved file which gets deleted if the calculated digest does not meet
 
247
      # expectations. If no path is specified, and the response is a 200 OK, the
 
248
      # content of the response will be returned as a String
 
249
      def HTTP::get( url, path=nil, options={}, user = nil, pass = nil, 
 
250
                    size = nil, digest = nil, debug = false )
 
251
        raise ArgumentError.new('options') unless options.is_a? Hash
 
252
        arguments = []
 
253
        buffer = nil
 
254
        if path.nil?
 
255
          buffer = Tempfile.new('ec2-get-response')
 
256
          buffer.close
 
257
          path = buffer.path
 
258
        else
 
259
          directory = File.dirname(path)
 
260
          FileUtils.mkdir_p(directory) unless File.exist?(directory)
 
261
        end
 
262
       
 
263
        headers = EC2::Common::HTTP::Headers.new('GET')
 
264
        options.each do |name, value| headers.add( name, value ) end
 
265
        headers.sign( user, pass, url ) if user and pass
 
266
        arguments << headers.curl_arguments
 
267
        
 
268
        arguments << "--max-filesize #{size}" if size
 
269
        arguments << "'#{url}'"
 
270
        arguments << '-o ' + path
 
271
        
 
272
        begin
 
273
          FileUtils.touch path
 
274
          response = HTTP::invoke(arguments.join(' '), path, debug)
 
275
          body = nil
 
276
          if response.success?
 
277
            if digest
 
278
              obtained = IO.popen("cat #{path} | openssl sha1") { |io| io.readline.chomp }
 
279
              unless digest == obtained                
 
280
                File.delete(path) if File.exists?(path) and not buffer.is_a? Tempfile
 
281
                raise Error::BadDigest.new(path, expected, obtained)
 
282
              end
 
283
            end
 
284
            if buffer.is_a? Tempfile
 
285
              buffer.open; 
 
286
              body = buffer.read
 
287
            end
 
288
          else
 
289
            File.delete( path ) if File.exist?( path ) and not buffer.is_a? Tempfile
 
290
          end
 
291
          return EC2::Common::HTTP::Response.new(response.code, response.type, body)
 
292
        rescue Error::Transfer => e
 
293
          File::delete( path ) if File::exist?( path ) and not buffer.is_a? Tempfile
 
294
          raise Error::Retrieve.new(e.message, e.code)
 
295
        ensure
 
296
          if buffer.is_a? Tempfile
 
297
            buffer.close
 
298
            buffer.unlink if File.exists? path
 
299
            GC.start
 
300
          end
 
301
        end
 
302
      end
 
303
      
 
304
  
 
305
      #-----------------------------------------------------------------------      
 
306
      # Get the HEAD response for the specified url.
 
307
      def HTTP::head( url, options={}, user = nil, pass = nil, debug = false )
 
308
        raise ArgumentError.new('options') unless options.is_a? Hash
 
309
        begin
 
310
          output = Tempfile.new('ec2-head-response')
 
311
          output.close
 
312
          arguments = ['--head']
 
313
          
 
314
          headers = EC2::Common::HTTP::Headers.new('HEAD')
 
315
          options.each do |name, value| headers.add( name, value ) end
 
316
          headers.sign( user, pass, url ) if user and pass
 
317
          arguments << headers.curl_arguments
 
318
          
 
319
          arguments << "'#{url}'"
 
320
          arguments << '-o ' + output.path
 
321
          
 
322
          response = HTTP::invoke(arguments.join(' '), output.path, debug)
 
323
          return EC2::Common::HTTP::Response.new(response.code, response.type)
 
324
          
 
325
        rescue Error::Transfer => e
 
326
          raise Error::Retrieve.new(e.message, e.code)
 
327
        ensure
 
328
          output.close(true)
 
329
          GC.start
 
330
        end
 
331
      end
 
332
    end
 
333
  end
 
334
end