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.
11
# ---------------------------------------------------------------------------
12
# Module that provides http functionality.
13
# ---------------------------------------------------------------------------
22
require 'ec2/common/curl'
23
require 'ec2/amitools/crypto'
29
class Response < EC2::Common::Curl::Response
31
def initialize(code, type, body=nil)
38
MANDATORY = Set.new( [ 'content-md5', 'content-type', 'date' ] )
39
X_AMZ_PREFIX = 'x-amz'
42
raise ArgumentError.new('invalid verb') if verb.to_s.empty?
48
#---------------------------------------------------------------------
49
# Add a Header key-value pair.
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
57
#-----------------------------------------------------------------------
58
# Sign the headers using HMAC SHA1.
59
def sign( aws_access_key_id, aws_secret_access_key, url )
61
@headers['date'] = Time.now.httpdate # add HTTP Date header
63
# Build the data to sign.
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' )
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 )
76
# Headers that start with x-amz must have both their name and value
78
if name =~ /^#{X_AMZ_PREFIX}/
79
data += name + ":" + value +"\n"
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)
92
# Sign headers and then put signature back into headers.
93
signature = Base64.encode64( Crypto::hmac_sha1( aws_secret_access_key, data ) )
95
@headers['Authorization'] = "AWS #{aws_access_key_id}:#{signature}"
99
#-----------------------------------------------------------------------
100
# Return the headers as a map from header name to header value.
102
return @headers.clone
106
#-----------------------------------------------------------------------
107
# Return the headers as curl arguments of the form "-H name:value".
109
@headers.map { | name, value | "-H \"#{name}:#{value}\""}.join(' ')
114
#-----------------------------------------------------------------------
116
class Error < RuntimeError
118
def initialize(msg, code = nil)
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}")
129
class Transfer < Error; end
130
class Retrieve < Transfer; end
134
#-----------------------------------------------------------------------
135
# Invoke curl with arguments given and process results of output file
136
def HTTP::invoke( arguments, outfile, debug=false )
138
raise ArgumentError.new(outfile) unless File.exists? outfile
139
result = EC2::Common::Curl.execute(arguments, debug)
141
if result.response.success?
142
return result.response
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))
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 + '): '
155
content = REXML::XPath.first(doc, '/Error/Message')
156
message = content.text unless content.nil?
159
if result.response.type =~ /text/
160
message << IO.read(outfile)
163
raise Error::Transfer.new(synopsis + message, result.response.code)
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") ?
175
output = result.stdout.chomp
176
if debug and not output.empty?
177
message << "\nCurl.Output: " + output.gsub("\n", "\nCurl.Output: ")
179
raise Error::Transfer.new(synopsis + message.strip + '.', result.status)
181
rescue EC2::Common::Curl::Error => e
182
raise Error::Transfer.new(e.message, e.code)
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
191
output = Tempfile.new('ec2-delete-response')
193
arguments = ['-X DELETE']
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
200
arguments << "'#{url}'"
201
arguments << '-o ' + output.path
203
response = HTTP::invoke(arguments.join(' '), output.path, debug)
204
return EC2::Common::HTTP::Response.new(response.code, response.type)
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
221
output = Tempfile.new('ec2-put-response')
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
230
arguments << "'#{url}'"
231
arguments << '-T ' + path
232
arguments << '-o ' + output.path
234
response = HTTP::invoke(arguments.join(' '), output.path, debug)
235
return EC2::Common::HTTP::Response.new(response.code, response.type)
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
255
buffer = Tempfile.new('ec2-get-response')
259
directory = File.dirname(path)
260
FileUtils.mkdir_p(directory) unless File.exist?(directory)
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
268
arguments << "--max-filesize #{size}" if size
269
arguments << "'#{url}'"
270
arguments << '-o ' + path
274
response = HTTP::invoke(arguments.join(' '), path, debug)
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)
284
if buffer.is_a? Tempfile
289
File.delete( path ) if File.exist?( path ) and not buffer.is_a? Tempfile
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)
296
if buffer.is_a? Tempfile
298
buffer.unlink if File.exists? path
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
310
output = Tempfile.new('ec2-head-response')
312
arguments = ['--head']
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
319
arguments << "'#{url}'"
320
arguments << '-o ' + output.path
322
response = HTTP::invoke(arguments.join(' '), output.path, debug)
323
return EC2::Common::HTTP::Response.new(response.code, response.type)
325
rescue Error::Transfer => e
326
raise Error::Retrieve.new(e.message, e.code)