#
# Copyright (c) 2007-2008 RightScale Inc
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Test
module RightAws
require 'digest/md5'
require 'pp'
require 'cgi'
class AwsUtils #:nodoc:
@@digest1 = OpenSSL::Digest::Digest.new("sha1")
@@digest256 = nil
if OpenSSL::OPENSSL_VERSION_NUMBER > 0x00908000
@@digest256 = OpenSSL::Digest::Digest.new("sha256") rescue nil # Some installation may not support sha256
end
def self.sign(aws_secret_access_key, auth_string)
Base64.encode64(OpenSSL::HMAC.digest(@@digest1, aws_secret_access_key, auth_string)).strip
end
# Escape a string accordingly Amazon rulles
# http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?REST_RESTAuth.html
def self.amz_escape(param)
return CGI.escape(param.to_s).gsub("%7E", "~").gsub("+", "%20") # from: http://umlaut.rubyforge.org/svn/trunk/lib/aws_product_sign.rb
#param.to_s.gsub(/([^a-zA-Z0-9._~-]+)/n) do
# '%' + $1.unpack('H2' * $1.size).join('%').upcase
#end
end
# Set a timestamp and a signature version
def self.fix_service_params(service_hash, signature)
service_hash["Timestamp"] ||= Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.000Z") unless service_hash["Expires"]
service_hash["SignatureVersion"] = signature
service_hash
end
# Signature Version 0
# A deprecated guy (should work till septemper 2009)
def self.sign_request_v0(aws_secret_access_key, service_hash)
fix_service_params(service_hash, '0')
string_to_sign = "#{service_hash['Action']}#{service_hash['Timestamp'] || service_hash['Expires']}"
service_hash['Signature'] = AwsUtils::sign(aws_secret_access_key, string_to_sign)
service_hash.to_a.collect{|key,val| "#{amz_escape(key)}=#{amz_escape(val.to_s)}" }.join("&")
end
# Signature Version 1
# Another deprecated guy (should work till septemper 2009)
def self.sign_request_v1(aws_secret_access_key, service_hash)
fix_service_params(service_hash, '1')
string_to_sign = service_hash.sort{|a,b| (a[0].to_s.downcase)<=>(b[0].to_s.downcase)}.to_s
service_hash['Signature'] = AwsUtils::sign(aws_secret_access_key, string_to_sign)
service_hash.to_a.collect{|key,val| "#{amz_escape(key)}=#{amz_escape(val.to_s)}" }.join("&")
end
# Signature Version 2
# EC2, SQS and SDB requests must be signed by this guy.
# See: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?REST_RESTAuth.html
# http://developer.amazonwebservices.com/connect/entry.jspa?externalID=1928
def self.sign_request_v2(aws_secret_access_key, service_hash, http_verb, host, uri)
fix_service_params(service_hash, '2')
# select a signing method (make an old openssl working with sha1)
# make 'HmacSHA256' to be a default one
service_hash['SignatureMethod'] = 'HmacSHA256' unless ['HmacSHA256', 'HmacSHA1'].include?(service_hash['SignatureMethod'])
service_hash['SignatureMethod'] = 'HmacSHA1' unless @@digest256
# select a digest
digest = (service_hash['SignatureMethod'] == 'HmacSHA256' ? @@digest256 : @@digest1)
# form string to sign
canonical_string = service_hash.keys.sort.map do |key|
"#{amz_escape(key)}=#{amz_escape(service_hash[key])}"
end.join('&')
string_to_sign = "#{http_verb.to_s.upcase}\n#{host.downcase}\n#{uri}\n#{canonical_string}"
# sign the string
signature = amz_escape(Base64.encode64(OpenSSL::HMAC.digest(digest, aws_secret_access_key, string_to_sign)).strip)
"#{canonical_string}&Signature=#{signature}"
end
# From Amazon's SQS Dev Guide, a brief description of how to escape:
# "URL encode the computed signature and other query parameters as specified in
# RFC1738, section 2.2. In addition, because the + character is interpreted as a blank space
# by Sun Java classes that perform URL decoding, make sure to encode the + character
# although it is not required by RFC1738."
# Avoid using CGI::escape to escape URIs.
# CGI::escape will escape characters in the protocol, host, and port
# sections of the URI. Only target chars in the query
# string should be escaped.
def self.URLencode(raw)
e = URI.escape(raw)
e.gsub(/\+/, "%2b")
end
def self.allow_only(allowed_keys, params)
bogus_args = []
params.keys.each {|p| bogus_args.push(p) unless allowed_keys.include?(p) }
raise AwsError.new("The following arguments were given but are not legal for the function call #{caller_method}: #{bogus_args.inspect}") if bogus_args.length > 0
end
def self.mandatory_arguments(required_args, params)
rargs = required_args.dup
params.keys.each {|p| rargs.delete(p)}
raise AwsError.new("The following mandatory arguments were not provided to #{caller_method}: #{rargs.inspect}") if rargs.length > 0
end
def self.caller_method
caller[1]=~/`(.*?)'/
$1
end
end
class AwsBenchmarkingBlock #:nodoc:
attr_accessor :xml, :service
def initialize
# Benchmark::Tms instance for service (Ec2, S3, or SQS) access benchmarking.
@service = Benchmark::Tms.new()
# Benchmark::Tms instance for XML parsing benchmarking.
@xml = Benchmark::Tms.new()
end
end
class AwsNoChange < RuntimeError
end
class RightAwsBase
# Amazon HTTP Error handling
# Text, if found in an error message returned by AWS, indicates that this may be a transient
# error. Transient errors are automatically retried with exponential back-off.
AMAZON_PROBLEMS = [ 'internal service error',
'is currently unavailable',
'no response from',
'Please try again',
'InternalError',
'ServiceUnavailable', #from SQS docs
'Unavailable',
'This application is not currently available',
'InsufficientInstanceCapacity'
]
@@amazon_problems = AMAZON_PROBLEMS
# Returns a list of Amazon service responses which are known to be transient problems.
# We have to re-request if we get any of them, because the problem will probably disappear.
# By default this method returns the same value as the AMAZON_PROBLEMS const.
def self.amazon_problems
@@amazon_problems
end
# Sets the list of Amazon side problems. Use in conjunction with the
# getter to append problems.
def self.amazon_problems=(problems_list)
@@amazon_problems = problems_list
end
end
module RightAwsBaseInterface
DEFAULT_SIGNATURE_VERSION = '2'
@@caching = false
def self.caching
@@caching
end
def self.caching=(caching)
@@caching = caching
end
# Current aws_access_key_id
attr_reader :aws_access_key_id
# Last HTTP request object
attr_reader :last_request
# Last HTTP response object
attr_reader :last_response
# Last AWS errors list (used by AWSErrorHandler)
attr_accessor :last_errors
# Last AWS request id (used by AWSErrorHandler)
attr_accessor :last_request_id
# Logger object
attr_accessor :logger
# Initial params hash
attr_accessor :params
# RightHttpConnection instance
attr_reader :connection
# Cache
attr_reader :cache
# Signature version (all services except s3)
attr_reader :signature_version
def init(service_info, aws_access_key_id, aws_secret_access_key, params={}) #:nodoc:
@params = params
raise AwsError.new("AWS access keys are required to operate on #{service_info[:name]}") \
if aws_access_key_id.blank? || aws_secret_access_key.blank?
@aws_access_key_id = aws_access_key_id
@aws_secret_access_key = aws_secret_access_key
# if the endpoint was explicitly defined - then use it
if @params[:endpoint_url]
@params[:server] = URI.parse(@params[:endpoint_url]).host
@params[:port] = URI.parse(@params[:endpoint_url]).port
@params[:service] = URI.parse(@params[:endpoint_url]).path
@params[:protocol] = URI.parse(@params[:endpoint_url]).scheme
@params[:region] = nil
else
@params[:server] ||= service_info[:default_host]
@params[:server] = "#{@params[:region]}.#{@params[:server]}" if @params[:region]
@params[:port] ||= service_info[:default_port]
@params[:service] ||= service_info[:default_service]
@params[:protocol] ||= service_info[:default_protocol]
end
if !@params[:multi_thread].nil? && @params[:connection_mode].nil? # user defined this
@params[:connection_mode] = @params[:multi_thread] ? :per_thread : :single
end
# @params[:multi_thread] ||= defined?(AWS_DAEMON)
@params[:connection_mode] ||= :default
@params[:connection_mode] = :per_request if @params[:connection_mode] == :default
@logger = @params[:logger]
@logger = RAILS_DEFAULT_LOGGER if !@logger && defined?(RAILS_DEFAULT_LOGGER)
@logger = Logger.new(STDOUT) if !@logger
@logger.info "New #{self.class.name} using #{@params[:connection_mode].to_s}-connection mode"
@error_handler = nil
@cache = {}
@signature_version = (params[:signature_version] || DEFAULT_SIGNATURE_VERSION).to_s
end
def signed_service_params(aws_secret_access_key, service_hash, http_verb=nil, host=nil, service=nil )
case signature_version.to_s
when '0' then AwsUtils::sign_request_v0(aws_secret_access_key, service_hash)
when '1' then AwsUtils::sign_request_v1(aws_secret_access_key, service_hash)
when '2' then AwsUtils::sign_request_v2(aws_secret_access_key, service_hash, http_verb, host, service)
else raise AwsError.new("Unknown signature version (#{signature_version.to_s}) requested")
end
end
# Returns +true+ if the describe_xxx responses are being cached
def caching?
@params.key?(:cache) ? @params[:cache] : @@caching
end
# Check if the aws function response hits the cache or not.
# If the cache hits:
# - raises an +AwsNoChange+ exception if +do_raise+ == +:raise+.
# - returnes parsed response from the cache if it exists or +true+ otherwise.
# If the cache miss or the caching is off then returns +false+.
def cache_hits?(function, response, do_raise=:raise)
result = false
if caching?
function = function.to_sym
# get rid of requestId (this bad boy was added for API 2008-08-08+ and it is uniq for every response)
response = response.sub(%r{.+?}, '')
response_md5 =Digest::MD5.hexdigest(response).to_s
# check for changes
unless @cache[function] && @cache[function][:response_md5] == response_md5
# well, the response is new, reset cache data
update_cache(function, {:response_md5 => response_md5,
:timestamp => Time.now,
:hits => 0,
:parsed => nil})
else
# aha, cache hits, update the data and throw an exception if needed
@cache[function][:hits] += 1
if do_raise == :raise
raise(AwsNoChange, "Cache hit: #{function} response has not changed since "+
"#{@cache[function][:timestamp].strftime('%Y-%m-%d %H:%M:%S')}, "+
"hits: #{@cache[function][:hits]}.")
else
result = @cache[function][:parsed] || true
end
end
end
result
end
def update_cache(function, hash)
(@cache[function.to_sym] ||= {}).merge!(hash) if caching?
end
def on_exception(options={:raise=>true, :log=>true}) # :nodoc:
raise if $!.is_a?(AwsNoChange)
AwsError::on_aws_exception(self, options)
end
# Return +true+ if this instance works in multi_thread mode and +false+ otherwise.
def multi_thread
@params[:multi_thread]
end
def request_info_impl(connection, benchblock, request, parser, &block) #:nodoc:
@connection = connection
@last_request = request[:request]
@last_response = nil
response=nil
blockexception = nil
if(block != nil)
# TRB 9/17/07 Careful - because we are passing in blocks, we get a situation where
# an exception may get thrown in the block body (which is high-level
# code either here or in the application) but gets caught in the
# low-level code of HttpConnection. The solution is not to let any
# exception escape the block that we pass to HttpConnection::request.
# Exceptions can originate from code directly in the block, or from user
# code called in the other block which is passed to response.read_body.
benchblock.service.add! do
responsehdr = @connection.request(request) do |response|
#########
begin
@last_response = response
if response.is_a?(Net::HTTPSuccess)
@error_handler = nil
response.read_body(&block)
else
@error_handler = AWSErrorHandler.new(self, parser, :errors_list => self.class.amazon_problems) unless @error_handler
check_result = @error_handler.check(request)
if check_result
@error_handler = nil
return check_result
end
request_text_data = "#{request[:server]}:#{request[:port]}#{request[:request].path}"
raise AwsError.new(@last_errors, @last_response.code, @last_request_id, request_text_data)
end
rescue Exception => e
blockexception = e
end
end
#########
#OK, now we are out of the block passed to the lower level
if(blockexception)
raise blockexception
end
benchblock.xml.add! do
parser.parse(responsehdr)
end
return parser.result
end
else
benchblock.service.add!{ response = @connection.request(request) }
# check response for errors...
@last_response = response
if response.is_a?(Net::HTTPSuccess)
@error_handler = nil
benchblock.xml.add! { parser.parse(response) }
return parser.result
else
@error_handler = AWSErrorHandler.new(self, parser, :errors_list => self.class.amazon_problems) unless @error_handler
check_result = @error_handler.check(request)
if check_result
@error_handler = nil
return check_result
end
request_text_data = "#{request[:server]}:#{request[:port]}#{request[:request].path}"
raise AwsError.new(@last_errors, @last_response.code, @last_request_id, request_text_data)
end
end
rescue
@error_handler = nil
raise
end
def request_cache_or_info(method, link, parser_class, benchblock, use_cache=true) #:nodoc:
# We do not want to break the logic of parsing hence will use a dummy parser to process all the standard
# steps (errors checking etc). The dummy parser does nothig - just returns back the params it received.
# If the caching is enabled and hit then throw AwsNoChange.
# P.S. caching works for the whole images list only! (when the list param is blank)
# check cache
response, params = request_info(link, RightDummyParser.new)
cache_hits?(method.to_sym, response.body) if use_cache
parser = parser_class.new(:logger => @logger)
benchblock.xml.add!{ parser.parse(response, params) }
result = block_given? ? yield(parser) : parser.result
# update parsed data
update_cache(method.to_sym, :parsed => result) if use_cache
result
end
# Returns Amazons request ID for the latest request
def last_request_id
@last_response && @last_response.body.to_s[%r{(.+?)}] && $1
end
end
# Exception class to signal any Amazon errors. All errors occuring during calls to Amazon's
# web services raise this type of error.
# Attribute inherited by RuntimeError:
# message - the text of the error, generally as returned by AWS in its XML response.
class AwsError < RuntimeError
# either an array of errors where each item is itself an array of [code, message]),
# or an error string if the error was raised manually, as in AwsError.new('err_text')
attr_reader :errors
# Request id (if exists)
attr_reader :request_id
# Response HTTP error code
attr_reader :http_code
# Raw request text data to AWS
attr_reader :request_data
def initialize(errors=nil, http_code=nil, request_id=nil, request_data=nil)
@errors = errors
@request_id = request_id
@http_code = http_code
@request_data = request_data
msg = @errors.is_a?(Array) ? @errors.map{|code, msg| "#{code}: #{msg}"}.join("; ") : @errors.to_s
msg += "\nREQUEST(#{@request_data})" unless @request_data.nil?
super(msg)
end
# Does any of the error messages include the regexp +pattern+?
# Used to determine whether to retry request.
def include?(pattern)
if @errors.is_a?(Array)
@errors.each{ |code, msg| return true if code =~ pattern }
else
return true if @errors_str =~ pattern
end
false
end
# Generic handler for AwsErrors. +aws+ is the RightAws::S3, RightAws::EC2, or RightAws::SQS
# object that caused the exception (it must provide last_request and last_response). Supported
# boolean options are:
# * :log print a message into the log using aws.logger to access the Logger
# * :puts do a "puts" of the error
# * :raise re-raise the error after logging
def self.on_aws_exception(aws, options={:raise=>true, :log=>true})
# Only log & notify if not user error
if !options[:raise] || system_error?($!)
error_text = "#{$!.inspect}\n#{$@}.join('\n')}"
puts error_text if options[:puts]
# Log the error
if options[:log]
request = aws.last_request ? aws.last_request.path : '-none-'
response = aws.last_response ? "#{aws.last_response.code} -- #{aws.last_response.message} -- #{aws.last_response.body}" : '-none-'
aws.logger.error error_text
aws.logger.error "Request was: #{request}"
aws.logger.error "Response was: #{response}"
end
end
raise if options[:raise] # re-raise an exception
return nil
end
# True if e is an AWS system error, i.e. something that is for sure not the caller's fault.
# Used to force logging.
def self.system_error?(e)
!e.is_a?(self) || e.message =~ /InternalError|InsufficientInstanceCapacity|Unavailable/
end
end
class AWSErrorHandler
# 0-100 (%)
DEFAULT_CLOSE_ON_4XX_PROBABILITY = 10
@@reiteration_start_delay = 0.2
def self.reiteration_start_delay
@@reiteration_start_delay
end
def self.reiteration_start_delay=(reiteration_start_delay)
@@reiteration_start_delay = reiteration_start_delay
end
@@reiteration_time = 5
def self.reiteration_time
@@reiteration_time
end
def self.reiteration_time=(reiteration_time)
@@reiteration_time = reiteration_time
end
@@close_on_error = true
def self.close_on_error
@@close_on_error
end
def self.close_on_error=(close_on_error)
@@close_on_error = close_on_error
end
@@close_on_4xx_probability = DEFAULT_CLOSE_ON_4XX_PROBABILITY
def self.close_on_4xx_probability
@@close_on_4xx_probability
end
def self.close_on_4xx_probability=(close_on_4xx_probability)
@@close_on_4xx_probability = close_on_4xx_probability
end
# params:
# :reiteration_time
# :errors_list
# :close_on_error = true | false
# :close_on_4xx_probability = 1-100
def initialize(aws, parser, params={}) #:nodoc:
@aws = aws # Link to RightEc2 | RightSqs | RightS3 instance
@parser = parser # parser to parse Amazon response
@started_at = Time.now
@stop_at = @started_at + (params[:reiteration_time] || @@reiteration_time)
@errors_list = params[:errors_list] || []
@reiteration_delay = @@reiteration_start_delay
@retries = 0
# close current HTTP(S) connection on 5xx, errors from list and 4xx errors
@close_on_error = params[:close_on_error].nil? ? @@close_on_error : params[:close_on_error]
@close_on_4xx_probability = params[:close_on_4xx_probability] || @@close_on_4xx_probability
end
# Returns false if
def check(request) #:nodoc:
result = false
error_found = false
redirect_detected= false
error_match = nil
last_errors_text = ''
response = @aws.last_response
# log error
request_text_data = "#{request[:server]}:#{request[:port]}#{request[:request].path}"
# is this a redirect?
# yes!
if response.is_a?(Net::HTTPRedirection)
redirect_detected = true
else
# no, it's an error ...
@aws.logger.warn("##### #{@aws.class.name} returned an error: #{response.code} #{response.message}\n#{response.body} #####")
@aws.logger.warn("##### #{@aws.class.name} request: #{request_text_data} ####")
end
# Check response body: if it is an Amazon XML document or not:
if redirect_detected || (response.body && response.body[/<\?xml/]) # ... it is a xml document
@aws.class.bench_xml.add! do
error_parser = RightErrorResponseParser.new
error_parser.parse(response)
@aws.last_errors = error_parser.errors
@aws.last_request_id = error_parser.requestID
last_errors_text = @aws.last_errors.flatten.join("\n")
# on redirect :
if redirect_detected
location = response['location']
# ... log information and ...
@aws.logger.info("##### #{@aws.class.name} redirect requested: #{response.code} #{response.message} #####")
@aws.logger.info("##### New location: #{location} #####")
# ... fix the connection data
request[:server] = URI.parse(location).host
request[:protocol] = URI.parse(location).scheme
request[:port] = URI.parse(location).port
end
end
else # ... it is not a xml document(probably just a html page?)
@aws.last_errors = [[response.code, "#{response.message} (#{request_text_data})"]]
@aws.last_request_id = '-undefined-'
last_errors_text = response.message
end
# now - check the error
unless redirect_detected
@errors_list.each do |error_to_find|
if last_errors_text[/#{error_to_find}/i]
error_found = true
error_match = error_to_find
@aws.logger.warn("##### Retry is needed, error pattern match: #{error_to_find} #####")
break
end
end
end
# check the time has gone from the first error come
if redirect_detected || error_found
# Close the connection to the server and recreate a new one.
# It may have a chance that one server is a semi-down and reconnection
# will help us to connect to the other server
if !redirect_detected && @close_on_error
@aws.connection.finish "#{self.class.name}: error match to pattern '#{error_match}'"
end
if (Time.now < @stop_at)
@retries += 1
unless redirect_detected
@aws.logger.warn("##### Retry ##{@retries} is being performed. Sleeping for #{@reiteration_delay} sec. Whole time: #{Time.now-@started_at} sec ####")
sleep @reiteration_delay
@reiteration_delay *= 2
# Always make sure that the fp is set to point to the beginning(?)
# of the File/IO. TODO: it assumes that offset is 0, which is bad.
if(request[:request].body_stream && request[:request].body_stream.respond_to?(:pos))
begin
request[:request].body_stream.pos = 0
rescue Exception => e
@logger.warn("Retry may fail due to unable to reset the file pointer" +
" -- #{self.class.name} : #{e.inspect}")
end
end
else
@aws.logger.info("##### Retry ##{@retries} is being performed due to a redirect. ####")
end
result = @aws.request_info(request, @parser)
else
@aws.logger.warn("##### Ooops, time is over... ####")
end
# aha, this is unhandled error:
elsif @close_on_error
# Is this a 5xx error ?
if @aws.last_response.code.to_s[/^5\d\d$/]
@aws.connection.finish "#{self.class.name}: code: #{@aws.last_response.code}: '#{@aws.last_response.message}'"
# Is this a 4xx error ?
elsif @aws.last_response.code.to_s[/^4\d\d$/] && @close_on_4xx_probability > rand(100)
@aws.connection.finish "#{self.class.name}: code: #{@aws.last_response.code}: '#{@aws.last_response.message}', " +
"probability: #{@close_on_4xx_probability}%"
end
end
result
end
end
#-----------------------------------------------------------------
class RightSaxParserCallback #:nodoc:
def self.include_callback
include XML::SaxParser::Callbacks
end
def initialize(right_aws_parser)
@right_aws_parser = right_aws_parser
end
def on_start_element(name, attr_hash)
@right_aws_parser.tag_start(name, attr_hash)
end
def on_characters(chars)
@right_aws_parser.text(chars)
end
def on_end_element(name)
@right_aws_parser.tag_end(name)
end
def on_start_document; end
def on_comment(msg); end
def on_processing_instruction(target, data); end
def on_cdata_block(cdata); end
def on_end_document; end
end
class RightAWSParser #:nodoc:
# default parsing library
DEFAULT_XML_LIBRARY = 'rexml'
# a list of supported parsers
@@supported_xml_libs = [DEFAULT_XML_LIBRARY, 'libxml']
@@xml_lib = DEFAULT_XML_LIBRARY # xml library name: 'rexml' | 'libxml'
def self.xml_lib
@@xml_lib
end
def self.xml_lib=(new_lib_name)
@@xml_lib = new_lib_name
end
attr_accessor :result
attr_reader :xmlpath
attr_accessor :xml_lib
def initialize(params={})
@xmlpath = ''
@result = false
@text = ''
@xml_lib = params[:xml_lib] || @@xml_lib
@logger = params[:logger]
reset
end
def tag_start(name, attributes)
@text = ''
tagstart(name, attributes)
@xmlpath += @xmlpath.empty? ? name : "/#{name}"
end
def tag_end(name)
if @xmlpath =~ /^(.*?)\/?#{name}$/
@xmlpath = $1
end
tagend(name)
end
def text(text)
@text += text
tagtext(text)
end
# Parser method.
# Params:
# xml_text - xml message text(String) or Net:HTTPxxx instance (response)
# params[:xml_lib] - library name: 'rexml' | 'libxml'
def parse(xml_text, params={})
# Get response body
unless xml_text.is_a?(String)
xml_text = xml_text.body.respond_to?(:force_encoding) ? xml_text.body.force_encoding("UTF-8") : xml_text.body
end
@xml_lib = params[:xml_lib] || @xml_lib
# check that we had no problems with this library otherwise use default
@xml_lib = DEFAULT_XML_LIBRARY unless @@supported_xml_libs.include?(@xml_lib)
# load xml library
if @xml_lib=='libxml' && !defined?(XML::SaxParser)
begin
require 'xml/libxml'
# is it new ? - Setup SaxParserCallback
if XML::Parser::VERSION >= '0.5.1.0'
RightSaxParserCallback.include_callback
end
rescue LoadError => e
@@supported_xml_libs.delete(@xml_lib)
@xml_lib = DEFAULT_XML_LIBRARY
if @logger
@logger.error e.inspect
@logger.error e.backtrace
@logger.info "Can not load 'libxml' library. '#{DEFAULT_XML_LIBRARY}' is used for parsing."
end
end
end
# Parse the xml text
case @xml_lib
when 'libxml'
xml = XML::SaxParser.new
xml.string = xml_text
# check libxml-ruby version
if XML::Parser::VERSION >= '0.5.1.0'
xml.callbacks = RightSaxParserCallback.new(self)
else
xml.on_start_element{|name, attr_hash| self.tag_start(name, attr_hash)}
xml.on_characters{ |text| self.text(text)}
xml.on_end_element{ |name| self.tag_end(name)}
end
xml.parse
else
REXML::Document.parse_stream(xml_text, self)
end
end
# Parser must have a lots of methods
# (see /usr/lib/ruby/1.8/rexml/parsers/streamparser.rb)
# We dont need most of them in RightAWSParser and method_missing helps us
# to skip their definition
def method_missing(method, *params)
# if the method is one of known - just skip it ...
return if [:comment, :attlistdecl, :notationdecl, :elementdecl,
:entitydecl, :cdata, :xmldecl, :attlistdecl, :instruction,
:doctype].include?(method)
# ... else - call super to raise an exception
super(method, params)
end
# the functions to be overriden by children (if nessesery)
def reset ; end
def tagstart(name, attributes); end
def tagend(name) ; end
def tagtext(text) ; end
end
#-----------------------------------------------------------------
# PARSERS: Errors
#-----------------------------------------------------------------
#
# TemporaryRedirect
# Please re-send this request to the specified temporary endpoint. Continue to use the original request endpoint for future requests.
# FD8D5026D1C5ABA3
# bucket-for-k.s3-external-3.amazonaws.com
# ItJy8xPFPli1fq/JR3DzQd3iDvFCRqi1LTRmunEdM1Uf6ZtW2r2kfGPWhRE1vtaU
# bucket-for-k
#
class RightErrorResponseParser < RightAWSParser #:nodoc:
attr_accessor :errors # array of hashes: error/message
attr_accessor :requestID
# attr_accessor :endpoint, :host_id, :bucket
def tagend(name)
case name
when 'RequestID' ; @requestID = @text
when 'Code' ; @code = @text
when 'Message' ; @message = @text
# when 'Endpoint' ; @endpoint = @text
# when 'HostId' ; @host_id = @text
# when 'Bucket' ; @bucket = @text
when 'Error' ; @errors << [ @code, @message ]
end
end
def reset
@errors = []
end
end
# Dummy parser - does nothing
# Returns the original params back
class RightDummyParser # :nodoc:
attr_accessor :result
def parse(response, params={})
@result = [response, params]
end
end
class RightHttp2xxParser < RightAWSParser # :nodoc:
def parse(response)
@result = response.is_a?(Net::HTTPSuccess)
end
end
end