cmeiklejohn-aws 2.3.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1237 @@
1
+ #
2
+ # Copyright (c) 2007-2008 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #
23
+
24
+ # Test
25
+ module Aws
26
+ require 'digest/md5'
27
+ require 'pp'
28
+ require 'cgi'
29
+ require 'uri'
30
+ require 'xmlsimple'
31
+
32
+ class AwsUtils #:nodoc:
33
+ @@digest1 = OpenSSL::Digest::Digest.new("sha1")
34
+ @@digest256 = nil
35
+ if OpenSSL::OPENSSL_VERSION_NUMBER > 0x00908000
36
+ @@digest256 = OpenSSL::Digest::Digest.new("sha256") rescue nil # Some installation may not support sha256
37
+ end
38
+
39
+ def self.sign(aws_secret_access_key, auth_string)
40
+ Base64.encode64(OpenSSL::HMAC.digest(@@digest1, aws_secret_access_key, auth_string)).strip
41
+ end
42
+
43
+
44
+ # Set a timestamp and a signature version
45
+ def self.fix_service_params(service_hash, signature)
46
+ service_hash["Timestamp"] ||= Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.000Z") unless service_hash["Expires"]
47
+ service_hash["SignatureVersion"] = signature
48
+ service_hash
49
+ end
50
+
51
+ # Signature Version 0
52
+ # A deprecated guy (should work till septemper 2009)
53
+ def self.sign_request_v0(aws_secret_access_key, service_hash)
54
+ fix_service_params(service_hash, '0')
55
+ string_to_sign = "#{service_hash['Action']}#{service_hash['Timestamp'] || service_hash['Expires']}"
56
+ service_hash['Signature'] = AwsUtils::sign(aws_secret_access_key, string_to_sign)
57
+ service_hash.to_a.collect{|key, val| "#{amz_escape(key)}=#{amz_escape(val.to_s)}" }.join("&")
58
+ end
59
+
60
+ # Signature Version 1
61
+ # Another deprecated guy (should work till septemper 2009)
62
+ def self.sign_request_v1(aws_secret_access_key, service_hash)
63
+ fix_service_params(service_hash, '1')
64
+ string_to_sign = service_hash.sort{|a, b| (a[0].to_s.downcase)<=>(b[0].to_s.downcase)}.to_s
65
+ service_hash['Signature'] = AwsUtils::sign(aws_secret_access_key, string_to_sign)
66
+ service_hash.to_a.collect{|key, val| "#{amz_escape(key)}=#{amz_escape(val.to_s)}" }.join("&")
67
+ end
68
+
69
+ # Signature Version 2
70
+ # EC2, SQS and SDB requests must be signed by this guy.
71
+ # See: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?REST_RESTAuth.html
72
+ # http://developer.amazonwebservices.com/connect/entry.jspa?externalID=1928
73
+ def self.sign_request_v2(aws_secret_access_key, service_hash, http_verb, host, uri)
74
+ fix_service_params(service_hash, '2')
75
+ # select a signing method (make an old openssl working with sha1)
76
+ # make 'HmacSHA256' to be a default one
77
+ service_hash['SignatureMethod'] = 'HmacSHA256' unless ['HmacSHA256', 'HmacSHA1'].include?(service_hash['SignatureMethod'])
78
+ service_hash['SignatureMethod'] = 'HmacSHA1' unless @@digest256
79
+ # select a digest
80
+ digest = (service_hash['SignatureMethod'] == 'HmacSHA256' ? @@digest256 : @@digest1)
81
+ # form string to sign
82
+ canonical_string = service_hash.keys.sort.map do |key|
83
+ "#{amz_escape(key)}=#{amz_escape(service_hash[key])}"
84
+ end.join('&')
85
+ string_to_sign = "#{http_verb.to_s.upcase}\n#{host.downcase}\n#{uri}\n#{canonical_string}"
86
+ # sign the string
87
+ signature = escape_sig(Base64.encode64(OpenSSL::HMAC.digest(digest, aws_secret_access_key, string_to_sign)).strip)
88
+ ret = "#{canonical_string}&Signature=#{signature}"
89
+ # puts 'full=' + ret.inspect
90
+ ret
91
+ end
92
+
93
+ HEX = [
94
+ "%00", "%01", "%02", "%03", "%04", "%05", "%06", "%07",
95
+ "%08", "%09", "%0A", "%0B", "%0C", "%0D", "%0E", "%0F",
96
+ "%10", "%11", "%12", "%13", "%14", "%15", "%16", "%17",
97
+ "%18", "%19", "%1A", "%1B", "%1C", "%1D", "%1E", "%1F",
98
+ "%20", "%21", "%22", "%23", "%24", "%25", "%26", "%27",
99
+ "%28", "%29", "%2A", "%2B", "%2C", "%2D", "%2E", "%2F",
100
+ "%30", "%31", "%32", "%33", "%34", "%35", "%36", "%37",
101
+ "%38", "%39", "%3A", "%3B", "%3C", "%3D", "%3E", "%3F",
102
+ "%40", "%41", "%42", "%43", "%44", "%45", "%46", "%47",
103
+ "%48", "%49", "%4A", "%4B", "%4C", "%4D", "%4E", "%4F",
104
+ "%50", "%51", "%52", "%53", "%54", "%55", "%56", "%57",
105
+ "%58", "%59", "%5A", "%5B", "%5C", "%5D", "%5E", "%5F",
106
+ "%60", "%61", "%62", "%63", "%64", "%65", "%66", "%67",
107
+ "%68", "%69", "%6A", "%6B", "%6C", "%6D", "%6E", "%6F",
108
+ "%70", "%71", "%72", "%73", "%74", "%75", "%76", "%77",
109
+ "%78", "%79", "%7A", "%7B", "%7C", "%7D", "%7E", "%7F",
110
+ "%80", "%81", "%82", "%83", "%84", "%85", "%86", "%87",
111
+ "%88", "%89", "%8A", "%8B", "%8C", "%8D", "%8E", "%8F",
112
+ "%90", "%91", "%92", "%93", "%94", "%95", "%96", "%97",
113
+ "%98", "%99", "%9A", "%9B", "%9C", "%9D", "%9E", "%9F",
114
+ "%A0", "%A1", "%A2", "%A3", "%A4", "%A5", "%A6", "%A7",
115
+ "%A8", "%A9", "%AA", "%AB", "%AC", "%AD", "%AE", "%AF",
116
+ "%B0", "%B1", "%B2", "%B3", "%B4", "%B5", "%B6", "%B7",
117
+ "%B8", "%B9", "%BA", "%BB", "%BC", "%BD", "%BE", "%BF",
118
+ "%C0", "%C1", "%C2", "%C3", "%C4", "%C5", "%C6", "%C7",
119
+ "%C8", "%C9", "%CA", "%CB", "%CC", "%CD", "%CE", "%CF",
120
+ "%D0", "%D1", "%D2", "%D3", "%D4", "%D5", "%D6", "%D7",
121
+ "%D8", "%D9", "%DA", "%DB", "%DC", "%DD", "%DE", "%DF",
122
+ "%E0", "%E1", "%E2", "%E3", "%E4", "%E5", "%E6", "%E7",
123
+ "%E8", "%E9", "%EA", "%EB", "%EC", "%ED", "%EE", "%EF",
124
+ "%F0", "%F1", "%F2", "%F3", "%F4", "%F5", "%F6", "%F7",
125
+ "%F8", "%F9", "%FA", "%FB", "%FC", "%FD", "%FE", "%FF"
126
+ ]
127
+ TO_REMEMBER = 'AZaz09 -_.!~*\'()'
128
+ ASCII = {} # {'A'=>65, 'Z'=>90, 'a'=>97, 'z'=>122, '0'=>48, '9'=>57, ' '=>32, '-'=>45, '_'=>95, '.'=>}
129
+ TO_REMEMBER.each_char do |c| #unpack("c*").each do |c|
130
+ ASCII[c] = c.unpack("c")[0]
131
+ end
132
+ # puts 'ascii=' + ASCII.inspect
133
+
134
+ # Escape a string accordingly Amazon rulles
135
+ # http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?REST_RESTAuth.html
136
+ def self.amz_escape(param)
137
+
138
+ param = param.to_s
139
+ # param = param.force_encoding("UTF-8")
140
+
141
+ e = "x" # escape2(param.to_s)
142
+ # puts 'ESCAPED=' + e.inspect
143
+
144
+
145
+ #return CGI.escape(param.to_s).gsub("%7E", "~").gsub("+", "%20") # from: http://umlaut.rubyforge.org/svn/trunk/lib/aws_product_sign.rb
146
+
147
+ #param.to_s.gsub(/([^a-zA-Z0-9._~-]+)/n) do
148
+ # '%' + $1.unpack('H2' * $1.size).join('%').upcase
149
+ #end
150
+
151
+ # puts 'e in=' + e.inspect
152
+ # converter = Iconv.new('ASCII', 'UTF-8')
153
+ # e = converter.iconv(e) #.unpack('U*').select{ |cp| cp < 127 }.pack('U*')
154
+ # puts 'e out=' + e.inspect
155
+
156
+ e2 = CGI.escape(param)
157
+ e2 = e2.gsub("%7E", "~")
158
+ e2 = e2.gsub("+", "%20")
159
+ e2 = e2.gsub("*", "%2A")
160
+
161
+ # puts 'E2=' + e2.inspect
162
+ # puts e == e2.to_s
163
+
164
+ e2
165
+
166
+ end
167
+
168
+ def self.escape2(s)
169
+ # home grown
170
+ ret = ""
171
+ s.unpack("U*") do |ch|
172
+ # puts 'ch=' + ch.inspect
173
+ if ASCII['A'] <= ch && ch <= ASCII['Z'] # A to Z
174
+ ret << ch
175
+ elsif ASCII['a'] <= ch && ch <= ASCII['z'] # a to z
176
+ ret << ch
177
+ elsif ASCII['0'] <= ch && ch <= ASCII['9'] # 0 to 9
178
+ ret << ch
179
+ elsif ch == ASCII[' '] # space
180
+ ret << "%20" # "+"
181
+ elsif ch == ASCII['-'] || ch == ASCII['_'] || ch == ASCII['.'] || ch == ASCII['~']
182
+ ret << ch
183
+ elsif ch <= 0x007f # other ascii
184
+ ret << HEX[ch]
185
+ elsif ch <= 0x07FF # non-ascii
186
+ ret << HEX[0xc0 | (ch >> 6)]
187
+ ret << HEX[0x80 | (ch & 0x3F)]
188
+ else
189
+ ret << HEX[0xe0 | (ch >> 12)]
190
+ ret << HEX[0x80 | ((ch >> 6) & 0x3F)]
191
+ ret << HEX[0x80 | (ch & 0x3F)]
192
+ end
193
+
194
+ end
195
+ ret
196
+
197
+ end
198
+
199
+ def self.escape_sig(raw)
200
+ e = CGI.escape(raw)
201
+ end
202
+
203
+ # From Amazon's SQS Dev Guide, a brief description of how to escape:
204
+ # "URL encode the computed signature and other query parameters as specified in
205
+ # RFC1738, section 2.2. In addition, because the + character is interpreted as a blank space
206
+ # by Sun Java classes that perform URL decoding, make sure to encode the + character
207
+ # although it is not required by RFC1738."
208
+ # Avoid using CGI::escape to escape URIs.
209
+ # CGI::escape will escape characters in the protocol, host, and port
210
+ # sections of the URI. Only target chars in the query
211
+ # string should be escaped.
212
+ def self.URLencode(raw)
213
+ e = URI.escape(raw)
214
+ e.gsub(/\+/, "%2b")
215
+ end
216
+
217
+
218
+ def self.allow_only(allowed_keys, params)
219
+ bogus_args = []
220
+ params.keys.each {|p| bogus_args.push(p) unless allowed_keys.include?(p) }
221
+ 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
222
+ end
223
+
224
+ def self.mandatory_arguments(required_args, params)
225
+ rargs = required_args.dup
226
+ params.keys.each {|p| rargs.delete(p)}
227
+ raise AwsError.new("The following mandatory arguments were not provided to #{caller_method}: #{rargs.inspect}") if rargs.length > 0
228
+ end
229
+
230
+ def self.caller_method
231
+ caller[1]=~/`(.*?)'/
232
+ $1
233
+ end
234
+
235
+ end
236
+
237
+ class AwsBenchmarkingBlock #:nodoc:
238
+ attr_accessor :xml, :service
239
+
240
+ def initialize
241
+ # Benchmark::Tms instance for service (Ec2, S3, or SQS) access benchmarking.
242
+ @service = Benchmark::Tms.new()
243
+ # Benchmark::Tms instance for XML parsing benchmarking.
244
+ @xml = Benchmark::Tms.new()
245
+ end
246
+ end
247
+
248
+ class AwsNoChange < RuntimeError
249
+ end
250
+
251
+ class AwsBase
252
+
253
+ # Amazon HTTP Error handling
254
+
255
+ # Text, if found in an error message returned by AWS, indicates that this may be a transient
256
+ # error. Transient errors are automatically retried with exponential back-off.
257
+ AMAZON_PROBLEMS = [ 'internal service error',
258
+ 'is currently unavailable',
259
+ 'no response from',
260
+ 'Please try again',
261
+ 'InternalError',
262
+ 'ServiceUnavailable', #from SQS docs
263
+ 'Unavailable',
264
+ 'This application is not currently available',
265
+ 'InsufficientInstanceCapacity'
266
+ ]
267
+ @@amazon_problems = AMAZON_PROBLEMS
268
+ # Returns a list of Amazon service responses which are known to be transient problems.
269
+ # We have to re-request if we get any of them, because the problem will probably disappear.
270
+ # By default this method returns the same value as the AMAZON_PROBLEMS const.
271
+ def self.amazon_problems
272
+ @@amazon_problems
273
+ end
274
+
275
+ # Sets the list of Amazon side problems. Use in conjunction with the
276
+ # getter to append problems.
277
+ def self.amazon_problems=(problems_list)
278
+ @@amazon_problems = problems_list
279
+ end
280
+
281
+ end
282
+
283
+ module AwsBaseInterface
284
+ DEFAULT_SIGNATURE_VERSION = '2'
285
+
286
+ @@caching = false
287
+
288
+ def self.caching
289
+ @@caching
290
+ end
291
+
292
+ def self.caching=(caching)
293
+ @@caching = caching
294
+ end
295
+
296
+ # Current aws_access_key_id
297
+ attr_reader :aws_access_key_id
298
+ # Last HTTP request object
299
+ attr_reader :last_request
300
+ # Last HTTP response object
301
+ attr_reader :last_response
302
+ # Last AWS errors list (used by AWSErrorHandler)
303
+ attr_accessor :last_errors
304
+ # Last AWS request id (used by AWSErrorHandler)
305
+ attr_accessor :last_request_id
306
+ # Logger object
307
+ attr_accessor :logger
308
+ # Initial params hash
309
+ attr_accessor :params
310
+ # RightHttpConnection instance
311
+ attr_reader :connection
312
+ # Cache
313
+ attr_reader :cache
314
+ # Signature version (all services except s3)
315
+ attr_reader :signature_version
316
+
317
+ def init(service_info, aws_access_key_id, aws_secret_access_key, params={}) #:nodoc:
318
+ @params = params
319
+ raise AwsError.new("AWS access keys are required to operate on #{service_info[:name]}") \
320
+ if aws_access_key_id.blank? || aws_secret_access_key.blank?
321
+ @aws_access_key_id = aws_access_key_id
322
+ @aws_secret_access_key = aws_secret_access_key
323
+ # if the endpoint was explicitly defined - then use it
324
+ if @params[:endpoint_url]
325
+ @params[:server] = URI.parse(@params[:endpoint_url]).host
326
+ @params[:port] = URI.parse(@params[:endpoint_url]).port
327
+ @params[:service] = URI.parse(@params[:endpoint_url]).path
328
+ @params[:protocol] = URI.parse(@params[:endpoint_url]).scheme
329
+ @params[:region] = nil
330
+ else
331
+ @params[:server] ||= service_info[:default_host]
332
+ @params[:server] = "#{@params[:region]}.#{@params[:server]}" if @params[:region]
333
+ @params[:port] ||= service_info[:default_port]
334
+ @params[:service] ||= service_info[:default_service]
335
+ @params[:protocol] ||= service_info[:default_protocol]
336
+ @params[:api_version] ||= service_info[:api_version]
337
+ end
338
+ if !@params[:multi_thread].nil? && @params[:connection_mode].nil? # user defined this
339
+ @params[:connection_mode] = @params[:multi_thread] ? :per_thread : :single
340
+ end
341
+ # @params[:multi_thread] ||= defined?(AWS_DAEMON)
342
+ @params[:connection_mode] ||= :default
343
+ @params[:connection_mode] = :per_request if @params[:connection_mode] == :default
344
+ @logger = @params[:logger]
345
+ @logger = RAILS_DEFAULT_LOGGER if !@logger && defined?(RAILS_DEFAULT_LOGGER)
346
+ @logger = Logger.new(STDOUT) if !@logger
347
+ @logger.info "New #{self.class.name} using #{@params[:connection_mode].to_s}-connection mode"
348
+ @error_handler = nil
349
+ @cache = {}
350
+ @signature_version = (params[:signature_version] || DEFAULT_SIGNATURE_VERSION).to_s
351
+ end
352
+
353
+ def signed_service_params(aws_secret_access_key, service_hash, http_verb=nil, host=nil, service=nil )
354
+ case signature_version.to_s
355
+ when '0' then
356
+ AwsUtils::sign_request_v0(aws_secret_access_key, service_hash)
357
+ when '1' then
358
+ AwsUtils::sign_request_v1(aws_secret_access_key, service_hash)
359
+ when '2' then
360
+ AwsUtils::sign_request_v2(aws_secret_access_key, service_hash, http_verb, host, service)
361
+ else
362
+ raise AwsError.new("Unknown signature version (#{signature_version.to_s}) requested")
363
+ end
364
+ end
365
+
366
+
367
+ def generate_request(action, params={})
368
+ generate_request2(@aws_access_key_id, @aws_secret_access_key, action, @params[:api_version], @params, params)
369
+ end
370
+
371
+ # FROM SDB
372
+ def generate_request2(aws_access_key, aws_secret_key, action, api_version, lib_params, user_params={}) #:nodoc:
373
+ # remove empty params from request
374
+ user_params.delete_if {|key, value| value.nil? }
375
+ # user_params.each_pair do |k,v|
376
+ # user_params[k] = v.force_encoding("UTF-8")
377
+ # end
378
+ #params_string = params.to_a.collect{|key,val| key + "=#{CGI::escape(val.to_s)}" }.join("&")
379
+ # prepare service data
380
+ service = lib_params[:service]
381
+ # puts 'service=' + service.to_s
382
+ service_hash = {"Action" => action,
383
+ "AWSAccessKeyId" => aws_access_key }
384
+ service_hash.update("Version" => api_version) if api_version
385
+ service_hash.update(user_params)
386
+ service_params = signed_service_params(aws_secret_key, service_hash, :get, lib_params[:server], lib_params[:service])
387
+ #
388
+ # use POST method if the length of the query string is too large
389
+ # see http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/MakingRESTRequests.html
390
+ if service_params.size > 2000
391
+ if signature_version == '2'
392
+ # resign the request because HTTP verb is included into signature
393
+ service_params = signed_service_params(aws_secret_key, service_hash, :post, lib_params[:server], service)
394
+ end
395
+ request = Net::HTTP::Post.new(service)
396
+ request.body = service_params
397
+ request['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8'
398
+ else
399
+ request = Net::HTTP::Get.new("#{service}?#{service_params}")
400
+ end
401
+
402
+ #puts "\n\n --------------- QUERY REQUEST TO AWS -------------- \n\n"
403
+ #puts "#{@params[:service]}?#{service_params}\n\n"
404
+
405
+ # prepare output hash
406
+ { :request => request,
407
+ :server => lib_params[:server],
408
+ :port => lib_params[:port],
409
+ :protocol => lib_params[:protocol] }
410
+ end
411
+
412
+ def get_conn(connection_name, lib_params, logger)
413
+ # thread = lib_params[:multi_thread] ? Thread.current : Thread.main
414
+ # thread[connection_name] ||= Rightscale::HttpConnection.new(:exception => Aws::AwsError, :logger => logger)
415
+ # conn = thread[connection_name]
416
+ # return conn
417
+ http_conn = nil
418
+ conn_mode = lib_params[:connection_mode]
419
+ if conn_mode == :per_request
420
+ http_conn = Rightscale::HttpConnection.new(:exception => AwsError, :logger => logger)
421
+
422
+ elsif conn_mode == :per_thread || conn_mode == :single
423
+ thread = conn_mode == :per_thread ? Thread.current : Thread.main
424
+ thread[connection_name] ||= Rightscale::HttpConnection.new(:exception => AwsError, :logger => logger)
425
+ http_conn = thread[connection_name]
426
+ # ret = request_info_impl(http_conn, bench, request, parser, &block)
427
+ end
428
+ return http_conn
429
+
430
+ end
431
+
432
+ def close_conn(conn_name)
433
+ conn_mode = @params[:connection_mode]
434
+ if conn_mode == :per_thread || conn_mode == :single
435
+ thread = conn_mode == :per_thread ? Thread.current : Thread.main
436
+ if !thread[conn_name].nil?
437
+ thread[conn_name].finish
438
+ thread[conn_name] = nil
439
+ end
440
+ end
441
+ end
442
+
443
+ #
444
+ # def request_info2(request, parser, lib_params, connection_name, logger, bench)
445
+ # t = get_conn(connection_name, lib_params, logger)
446
+ # request_info_impl(t, bench, request, parser)
447
+ # end
448
+
449
+ # Sends request to Amazon and parses the response
450
+ # Raises AwsError if any banana happened
451
+ def request_info2(request, parser, lib_params, connection_name, logger, bench, &block) #:nodoc:
452
+ ret = nil
453
+ http_conn = get_conn(connection_name, lib_params, logger)
454
+ begin
455
+ retry_count = 1
456
+ count = 0
457
+ while count <= retry_count
458
+ puts 'RETRYING QUERY due to QueryTimeout...' if count > 0
459
+ begin
460
+ ret = request_info_impl(http_conn, bench, request, parser, &block)
461
+ break
462
+ rescue Aws::AwsError => ex
463
+ if !ex.include?(/QueryTimeout/) || count == retry_count
464
+ raise ex
465
+ end
466
+ end
467
+ count += 1
468
+ end
469
+ ensure
470
+ http_conn.finish if http_conn && lib_params[:connection_mode] == :per_request
471
+ end
472
+ ret
473
+ end
474
+
475
+
476
+ # This is the direction we should head instead of writing our own parsers for everything, much simpler
477
+ # params:
478
+ # - :group_tags => hash of indirection to eliminate, see: http://xml-simple.rubyforge.org/
479
+ # - :force_array => true for all or an array of tag names to force
480
+ # - :pull_out_array => an array of levels to dig into when generating return value (see rds.rb for example)
481
+ def request_info_xml_simple(connection_name, lib_params, request, logger, params = {})
482
+
483
+ @connection = get_conn(connection_name, lib_params, logger)
484
+ begin
485
+ @last_request = request[:request]
486
+ @last_response = nil
487
+
488
+ response = @connection.request(request)
489
+ # puts "response=" + response.body
490
+ # benchblock.service.add!{ response = @connection.request(request) }
491
+ # check response for errors...
492
+ @last_response = response
493
+ if response.is_a?(Net::HTTPSuccess)
494
+ @error_handler = nil
495
+ # benchblock.xml.add! { parser.parse(response) }
496
+ # return parser.result
497
+ force_array = params[:force_array] || false
498
+ # Force_array and group_tags don't work nice together so going to force array manually
499
+ xml_simple_options = {"KeyToSymbol"=>false, 'ForceArray' => false}
500
+ xml_simple_options["GroupTags"] = params[:group_tags] if params[:group_tags]
501
+
502
+ # { 'GroupTags' => { 'searchpath' => 'dir' }
503
+ # 'ForceArray' => %r(_list$)
504
+ parsed = XmlSimple.xml_in(response.body, xml_simple_options)
505
+ # todo: we may want to consider stripping off a couple of layers when doing this, for instance:
506
+ # <DescribeDBInstancesResponse xmlns="http://rds.amazonaws.com/admin/2009-10-16/">
507
+ # <DescribeDBInstancesResult>
508
+ # <DBInstances>
509
+ # <DBInstance>....
510
+ # Strip it off and only return an array or hash of <DBInstance>'s (hash by identifier).
511
+ # would have to be able to make the RequestId available somehow though, perhaps some special array subclass which included that?
512
+ unless force_array.is_a? Array
513
+ force_array = []
514
+ end
515
+ parsed = symbolize(parsed, force_array)
516
+ # puts 'parsed=' + parsed.inspect
517
+ if params[:pull_out_array]
518
+ ret = Aws::AwsResponseArray.new(parsed[:response_metadata])
519
+ level_hash = parsed
520
+ params[:pull_out_array].each do |x|
521
+ level_hash = level_hash[x]
522
+ end
523
+ if level_hash.is_a? Hash # When there's only one
524
+ ret << level_hash
525
+ else # should be array
526
+ # puts 'level_hash=' + level_hash.inspect
527
+ level_hash.each do |x|
528
+ ret << x
529
+ end
530
+ end
531
+ elsif params[:pull_out_single]
532
+ # returns a single object
533
+ ret = AwsResponseObjectHash.new(parsed[:response_metadata])
534
+ level_hash = parsed
535
+ params[:pull_out_single].each do |x|
536
+ level_hash = level_hash[x]
537
+ end
538
+ ret.merge!(level_hash)
539
+ else
540
+ ret = parsed
541
+ end
542
+ return ret
543
+
544
+ else
545
+ @error_handler = AWSErrorHandler.new(self, nil, :errors_list => self.class.amazon_problems) unless @error_handler
546
+ check_result = @error_handler.check(request)
547
+ if check_result
548
+ @error_handler = nil
549
+ return check_result
550
+ end
551
+ request_text_data = "#{request[:server]}:#{request[:port]}#{request[:request].path}"
552
+ raise AwsError2.new(@last_response.code, @last_request_id, request_text_data, @last_response.body)
553
+ end
554
+ ensure
555
+ @connection.finish if @connection && lib_params[:connection_mode] == :per_request
556
+ end
557
+
558
+ end
559
+
560
+ def symbolize(hash, force_array)
561
+ ret = {}
562
+ hash.keys.each do |key|
563
+ val = hash[key]
564
+ if val.is_a? Hash
565
+ val = symbolize(val, force_array)
566
+ if force_array.include? key
567
+ val = [val]
568
+ end
569
+ elsif val.is_a? Array
570
+ val = val.collect { |x| symbolize(x, force_array) }
571
+ end
572
+ ret[key.underscore.to_sym] = val
573
+ end
574
+ ret
575
+ end
576
+
577
+ # Returns +true+ if the describe_xxx responses are being cached
578
+ def caching?
579
+ @params.key?(:cache) ? @params[:cache] : @@caching
580
+ end
581
+
582
+ # Check if the aws function response hits the cache or not.
583
+ # If the cache hits:
584
+ # - raises an +AwsNoChange+ exception if +do_raise+ == +:raise+.
585
+ # - returnes parsed response from the cache if it exists or +true+ otherwise.
586
+ # If the cache miss or the caching is off then returns +false+.
587
+ def cache_hits?(function, response, do_raise=:raise)
588
+ result = false
589
+ if caching?
590
+ function = function.to_sym
591
+ # get rid of requestId (this bad boy was added for API 2008-08-08+ and it is uniq for every response)
592
+ response = response.sub(%r{<requestId>.+?</requestId>}, '')
593
+ response_md5 =Digest::MD5.hexdigest(response).to_s
594
+ # check for changes
595
+ unless @cache[function] && @cache[function][:response_md5] == response_md5
596
+ # well, the response is new, reset cache data
597
+ update_cache(function, {:response_md5 => response_md5,
598
+ :timestamp => Time.now,
599
+ :hits => 0,
600
+ :parsed => nil})
601
+ else
602
+ # aha, cache hits, update the data and throw an exception if needed
603
+ @cache[function][:hits] += 1
604
+ if do_raise == :raise
605
+ raise(AwsNoChange, "Cache hit: #{function} response has not changed since "+
606
+ "#{@cache[function][:timestamp].strftime('%Y-%m-%d %H:%M:%S')}, "+
607
+ "hits: #{@cache[function][:hits]}.")
608
+ else
609
+ result = @cache[function][:parsed] || true
610
+ end
611
+ end
612
+ end
613
+ result
614
+ end
615
+
616
+ def update_cache(function, hash)
617
+ (@cache[function.to_sym] ||= {}).merge!(hash) if caching?
618
+ end
619
+
620
+ def on_exception(options={:raise=>true, :log=>true}) # :nodoc:
621
+ raise if $!.is_a?(AwsNoChange)
622
+ AwsError::on_aws_exception(self, options)
623
+ end
624
+
625
+ # Return +true+ if this instance works in multi_thread mode and +false+ otherwise.
626
+ def multi_thread
627
+ @params[:multi_thread]
628
+ end
629
+
630
+
631
+ def request_info_impl(connection, benchblock, request, parser, &block) #:nodoc:
632
+ @connection = connection
633
+ @last_request = request[:request]
634
+ @last_response = nil
635
+ response=nil
636
+ blockexception = nil
637
+
638
+ if (block != nil)
639
+ # TRB 9/17/07 Careful - because we are passing in blocks, we get a situation where
640
+ # an exception may get thrown in the block body (which is high-level
641
+ # code either here or in the application) but gets caught in the
642
+ # low-level code of HttpConnection. The solution is not to let any
643
+ # exception escape the block that we pass to HttpConnection::request.
644
+ # Exceptions can originate from code directly in the block, or from user
645
+ # code called in the other block which is passed to response.read_body.
646
+ benchblock.service.add! do
647
+ responsehdr = @connection.request(request) do |response|
648
+ #########
649
+ begin
650
+ @last_response = response
651
+ if response.is_a?(Net::HTTPSuccess)
652
+ @error_handler = nil
653
+ response.read_body(&block)
654
+ else
655
+ @error_handler = AWSErrorHandler.new(self, parser, :errors_list => self.class.amazon_problems) unless @error_handler
656
+ check_result = @error_handler.check(request)
657
+ if check_result
658
+ @error_handler = nil
659
+ return check_result
660
+ end
661
+ request_text_data = "#{request[:server]}:#{request[:port]}#{request[:request].path}"
662
+ raise AwsError.new(@last_errors, @last_response.code, @last_request_id, request_text_data)
663
+ end
664
+ rescue Exception => e
665
+ blockexception = e
666
+ end
667
+ end
668
+ #########
669
+
670
+ #OK, now we are out of the block passed to the lower level
671
+ if (blockexception)
672
+ raise blockexception
673
+ end
674
+ benchblock.xml.add! do
675
+ parser.parse(responsehdr)
676
+ end
677
+ return parser.result
678
+ end
679
+ else
680
+ benchblock.service.add!{ response = @connection.request(request) }
681
+ # check response for errors...
682
+ @last_response = response
683
+ if response.is_a?(Net::HTTPSuccess)
684
+ @error_handler = nil
685
+ benchblock.xml.add! { parser.parse(response) }
686
+ return parser.result
687
+ else
688
+ @error_handler = AWSErrorHandler.new(self, parser, :errors_list => self.class.amazon_problems) unless @error_handler
689
+ check_result = @error_handler.check(request)
690
+ if check_result
691
+ @error_handler = nil
692
+ return check_result
693
+ end
694
+ request_text_data = "#{request[:server]}:#{request[:port]}#{request[:request].path}"
695
+ raise AwsError.new(@last_errors, @last_response.code, @last_request_id, request_text_data)
696
+ end
697
+ end
698
+ rescue
699
+ @error_handler = nil
700
+ raise
701
+ end
702
+
703
+ def request_cache_or_info(method, link, parser_class, benchblock, use_cache=true) #:nodoc:
704
+ # We do not want to break the logic of parsing hence will use a dummy parser to process all the standard
705
+ # steps (errors checking etc). The dummy parser does nothig - just returns back the params it received.
706
+ # If the caching is enabled and hit then throw AwsNoChange.
707
+ # P.S. caching works for the whole images list only! (when the list param is blank)
708
+ # check cache
709
+ response, params = request_info(link, RightDummyParser.new)
710
+ cache_hits?(method.to_sym, response.body) if use_cache
711
+ parser = parser_class.new(:logger => @logger)
712
+ benchblock.xml.add!{ parser.parse(response, params) }
713
+ result = block_given? ? yield(parser) : parser.result
714
+ # update parsed data
715
+ update_cache(method.to_sym, :parsed => result) if use_cache
716
+ result
717
+ end
718
+
719
+ # Returns Amazons request ID for the latest request
720
+ def last_request_id
721
+ @last_response && @last_response.body.to_s[%r{<requestId>(.+?)</requestId>}] && $1
722
+ end
723
+
724
+ def hash_params(prefix, list) #:nodoc:
725
+ groups = {}
726
+ list.each_index{|i| groups.update("#{prefix}.#{i+1}"=>list[i])} if list
727
+ return groups
728
+ end
729
+
730
+ end
731
+
732
+
733
+ # Exception class to signal any Amazon errors. All errors occuring during calls to Amazon's
734
+ # web services raise this type of error.
735
+ # Attribute inherited by RuntimeError:
736
+ # message - the text of the error, generally as returned by AWS in its XML response.
737
+ class AwsError < RuntimeError
738
+
739
+ # either an array of errors where each item is itself an array of [code, message]),
740
+ # or an error string if the error was raised manually, as in <tt>AwsError.new('err_text')</tt>
741
+ attr_reader :errors
742
+
743
+ # Request id (if exists)
744
+ attr_reader :request_id
745
+
746
+ # Response HTTP error code
747
+ attr_reader :http_code
748
+
749
+ # Raw request text data to AWS
750
+ attr_reader :request_data
751
+
752
+ attr_reader :response
753
+
754
+ def initialize(errors=nil, http_code=nil, request_id=nil, request_data=nil, response=nil)
755
+ @errors = errors
756
+ @request_id = request_id
757
+ @http_code = http_code
758
+ @request_data = request_data
759
+ @response = response
760
+ msg = @errors.is_a?(Array) ? @errors.map{|code, msg| "#{code}: #{msg}"}.join("; ") : @errors.to_s
761
+ msg += "\nREQUEST=#{@request_data} " unless @request_data.nil?
762
+ msg += "\nREQUEST ID=#{@request_id} " unless @request_id.nil?
763
+ super(msg)
764
+ end
765
+
766
+ # Does any of the error messages include the regexp +pattern+?
767
+ # Used to determine whether to retry request.
768
+ def include?(pattern)
769
+ if @errors.is_a?(Array)
770
+ @errors.each{ |code, msg| return true if code =~ pattern }
771
+ else
772
+ return true if @errors_str =~ pattern
773
+ end
774
+ false
775
+ end
776
+
777
+ # Generic handler for AwsErrors. +aws+ is the Aws::S3, Aws::EC2, or Aws::SQS
778
+ # object that caused the exception (it must provide last_request and last_response). Supported
779
+ # boolean options are:
780
+ # * <tt>:log</tt> print a message into the log using aws.logger to access the Logger
781
+ # * <tt>:puts</tt> do a "puts" of the error
782
+ # * <tt>:raise</tt> re-raise the error after logging
783
+ def self.on_aws_exception(aws, options={:raise=>true, :log=>true})
784
+ # Only log & notify if not user error
785
+ if !options[:raise] || system_error?($!)
786
+ error_text = "#{$!.inspect}\n#{$@}.join('\n')}"
787
+ puts error_text if options[:puts]
788
+ # Log the error
789
+ if options[:log]
790
+ request = aws.last_request ? aws.last_request.path : '-none-'
791
+ response = aws.last_response ? "#{aws.last_response.code} -- #{aws.last_response.message} -- #{aws.last_response.body}" : '-none-'
792
+ @response = response
793
+ aws.logger.error error_text
794
+ aws.logger.error "Request was: #{request}"
795
+ aws.logger.error "Response was: #{response}"
796
+ end
797
+ end
798
+ raise if options[:raise] # re-raise an exception
799
+ return nil
800
+ end
801
+
802
+ # True if e is an AWS system error, i.e. something that is for sure not the caller's fault.
803
+ # Used to force logging.
804
+ def self.system_error?(e)
805
+ !e.is_a?(self) || e.message =~ /InternalError|InsufficientInstanceCapacity|Unavailable/
806
+ end
807
+
808
+ end
809
+
810
+ # Simplified version
811
+ class AwsError2 < RuntimeError
812
+ # Request id (if exists)
813
+ attr_reader :request_id
814
+
815
+ # Response HTTP error code
816
+ attr_reader :http_code
817
+
818
+ # Raw request text data to AWS
819
+ attr_reader :request_data
820
+
821
+ attr_reader :response
822
+
823
+ attr_reader :errors
824
+
825
+ def initialize(http_code=nil, request_id=nil, request_data=nil, response=nil)
826
+
827
+ @request_id = request_id
828
+ @http_code = http_code
829
+ @request_data = request_data
830
+ @response = response
831
+ # puts '@response=' + @response.inspect
832
+
833
+ if @response
834
+ ref = XmlSimple.xml_in(@response, { "ForceArray"=>false })
835
+ # puts "refxml=" + ref.inspect
836
+ msg = "#{ref['Error']['Code']}: #{ref['Error']['Message']}"
837
+ else
838
+ msg = "#{@http_code}: REQUEST(#{@request_data})"
839
+ end
840
+ msg += "\nREQUEST ID=#{@request_id} " unless @request_id.nil?
841
+ super(msg)
842
+ end
843
+
844
+
845
+ end
846
+
847
+
848
+ class AWSErrorHandler
849
+ # 0-100 (%)
850
+ DEFAULT_CLOSE_ON_4XX_PROBABILITY = 10
851
+
852
+ @@reiteration_start_delay = 0.2
853
+
854
+ def self.reiteration_start_delay
855
+ @@reiteration_start_delay
856
+ end
857
+
858
+ def self.reiteration_start_delay=(reiteration_start_delay)
859
+ @@reiteration_start_delay = reiteration_start_delay
860
+ end
861
+
862
+ @@reiteration_time = 5
863
+
864
+ def self.reiteration_time
865
+ @@reiteration_time
866
+ end
867
+
868
+ def self.reiteration_time=(reiteration_time)
869
+ @@reiteration_time = reiteration_time
870
+ end
871
+
872
+ @@close_on_error = true
873
+
874
+ def self.close_on_error
875
+ @@close_on_error
876
+ end
877
+
878
+ def self.close_on_error=(close_on_error)
879
+ @@close_on_error = close_on_error
880
+ end
881
+
882
+ @@close_on_4xx_probability = DEFAULT_CLOSE_ON_4XX_PROBABILITY
883
+
884
+ def self.close_on_4xx_probability
885
+ @@close_on_4xx_probability
886
+ end
887
+
888
+ def self.close_on_4xx_probability=(close_on_4xx_probability)
889
+ @@close_on_4xx_probability = close_on_4xx_probability
890
+ end
891
+
892
+ # params:
893
+ # :reiteration_time
894
+ # :errors_list
895
+ # :close_on_error = true | false
896
+ # :close_on_4xx_probability = 1-100
897
+ def initialize(aws, parser, params={}) #:nodoc:
898
+ @aws = aws # Link to RightEc2 | RightSqs | RightS3 instance
899
+ @parser = parser # parser to parse Amazon response
900
+ @started_at = Time.now
901
+ @stop_at = @started_at + (params[:reiteration_time] || @@reiteration_time)
902
+ @errors_list = params[:errors_list] || []
903
+ @reiteration_delay = @@reiteration_start_delay
904
+ @retries = 0
905
+ # close current HTTP(S) connection on 5xx, errors from list and 4xx errors
906
+ @close_on_error = params[:close_on_error].nil? ? @@close_on_error : params[:close_on_error]
907
+ @close_on_4xx_probability = params[:close_on_4xx_probability] || @@close_on_4xx_probability
908
+ end
909
+
910
+ # Returns false if
911
+ def check(request) #:nodoc:
912
+ result = false
913
+ error_found = false
914
+ redirect_detected= false
915
+ error_match = nil
916
+ last_errors_text = ''
917
+ response = @aws.last_response
918
+ # log error
919
+ request_text_data = "#{request[:server]}:#{request[:port]}#{request[:request].path}"
920
+ # is this a redirect?
921
+ # yes!
922
+ if response.is_a?(Net::HTTPRedirection)
923
+ redirect_detected = true
924
+ else
925
+ # no, it's an error ...
926
+ @aws.logger.warn("##### #{@aws.class.name} returned an error: #{response.code} #{response.message}\n#{response.body} #####")
927
+ @aws.logger.warn("##### #{@aws.class.name} request: #{request_text_data} ####")
928
+ end
929
+ # Check response body: if it is an Amazon XML document or not:
930
+ if redirect_detected || (response.body && response.body[/<\?xml/]) # ... it is a xml document
931
+ @aws.class.bench_xml.add! do
932
+ error_parser = RightErrorResponseParser.new
933
+ error_parser.parse(response)
934
+ @aws.last_errors = error_parser.errors
935
+ @aws.last_request_id = error_parser.requestID
936
+ last_errors_text = @aws.last_errors.flatten.join("\n")
937
+ # on redirect :
938
+ if redirect_detected
939
+ location = response['location']
940
+ # ... log information and ...
941
+ @aws.logger.info("##### #{@aws.class.name} redirect requested: #{response.code} #{response.message} #####")
942
+ @aws.logger.info("##### New location: #{location} #####")
943
+ # ... fix the connection data
944
+ request[:server] = URI.parse(location).host
945
+ request[:protocol] = URI.parse(location).scheme
946
+ request[:port] = URI.parse(location).port
947
+ end
948
+ end
949
+ else # ... it is not a xml document(probably just a html page?)
950
+ @aws.last_errors = [[response.code, "#{response.message} (#{request_text_data})"]]
951
+ @aws.last_request_id = '-undefined-'
952
+ last_errors_text = response.message
953
+ end
954
+ # now - check the error
955
+ unless redirect_detected
956
+ @errors_list.each do |error_to_find|
957
+ if last_errors_text[/#{error_to_find}/i]
958
+ error_found = true
959
+ error_match = error_to_find
960
+ @aws.logger.warn("##### Retry is needed, error pattern match: #{error_to_find} #####")
961
+ break
962
+ end
963
+ end
964
+ end
965
+ # check the time has gone from the first error come
966
+ if redirect_detected || error_found
967
+ # Close the connection to the server and recreate a new one.
968
+ # It may have a chance that one server is a semi-down and reconnection
969
+ # will help us to connect to the other server
970
+ if !redirect_detected && @close_on_error
971
+ @aws.connection.finish "#{self.class.name}: error match to pattern '#{error_match}'"
972
+ end
973
+
974
+ if (Time.now < @stop_at)
975
+ @retries += 1
976
+ unless redirect_detected
977
+ @aws.logger.warn("##### Retry ##{@retries} is being performed. Sleeping for #{@reiteration_delay} sec. Whole time: #{Time.now-@started_at} sec ####")
978
+ sleep @reiteration_delay
979
+ @reiteration_delay *= 2
980
+
981
+ # Always make sure that the fp is set to point to the beginning(?)
982
+ # of the File/IO. TODO: it assumes that offset is 0, which is bad.
983
+ if (request[:request].body_stream && request[:request].body_stream.respond_to?(:pos))
984
+ begin
985
+ request[:request].body_stream.pos = 0
986
+ rescue Exception => e
987
+ @logger.warn("Retry may fail due to unable to reset the file pointer" +
988
+ " -- #{self.class.name} : #{e.inspect}")
989
+ end
990
+ end
991
+ else
992
+ @aws.logger.info("##### Retry ##{@retries} is being performed due to a redirect. ####")
993
+ end
994
+ result = @aws.request_info(request, @parser)
995
+ else
996
+ @aws.logger.warn("##### Ooops, time is over... ####")
997
+ end
998
+ # aha, this is unhandled error:
999
+ elsif @close_on_error
1000
+ # Is this a 5xx error ?
1001
+ if @aws.last_response.code.to_s[/^5\d\d$/]
1002
+ @aws.connection.finish "#{self.class.name}: code: #{@aws.last_response.code}: '#{@aws.last_response.message}'"
1003
+ # Is this a 4xx error ?
1004
+ elsif @aws.last_response.code.to_s[/^4\d\d$/] && @close_on_4xx_probability > rand(100)
1005
+ @aws.connection.finish "#{self.class.name}: code: #{@aws.last_response.code}: '#{@aws.last_response.message}', " +
1006
+ "probability: #{@close_on_4xx_probability}%"
1007
+ end
1008
+ end
1009
+ result
1010
+ end
1011
+
1012
+ end
1013
+
1014
+
1015
+ #-----------------------------------------------------------------
1016
+
1017
+ class RightSaxParserCallback #:nodoc:
1018
+ def self.include_callback
1019
+ include XML::SaxParser::Callbacks
1020
+ end
1021
+
1022
+ def initialize(right_aws_parser)
1023
+ @right_aws_parser = right_aws_parser
1024
+ end
1025
+
1026
+ def on_start_element(name, attr_hash)
1027
+ @right_aws_parser.tag_start(name, attr_hash)
1028
+ end
1029
+
1030
+ def on_characters(chars)
1031
+ @right_aws_parser.text(chars)
1032
+ end
1033
+
1034
+ def on_end_element(name)
1035
+ @right_aws_parser.tag_end(name)
1036
+ end
1037
+
1038
+ def on_start_document;
1039
+ end
1040
+
1041
+ def on_comment(msg)
1042
+ ;
1043
+ end
1044
+
1045
+ def on_processing_instruction(target, data)
1046
+ ;
1047
+ end
1048
+
1049
+ def on_cdata_block(cdata)
1050
+ ;
1051
+ end
1052
+
1053
+ def on_end_document;
1054
+ end
1055
+ end
1056
+
1057
+ class AwsParser #:nodoc:
1058
+ # default parsing library
1059
+ DEFAULT_XML_LIBRARY = 'rexml'
1060
+ # a list of supported parsers
1061
+ @@supported_xml_libs = [DEFAULT_XML_LIBRARY, 'libxml']
1062
+
1063
+ @@xml_lib = DEFAULT_XML_LIBRARY # xml library name: 'rexml' | 'libxml'
1064
+ def self.xml_lib
1065
+ @@xml_lib
1066
+ end
1067
+
1068
+ def self.xml_lib=(new_lib_name)
1069
+ @@xml_lib = new_lib_name
1070
+ end
1071
+
1072
+ attr_accessor :result
1073
+ attr_reader :xmlpath
1074
+ attr_accessor :xml_lib
1075
+
1076
+ def initialize(params={})
1077
+ @xmlpath = ''
1078
+ @result = false
1079
+ @text = ''
1080
+ @xml_lib = params[:xml_lib] || @@xml_lib
1081
+ @logger = params[:logger]
1082
+ reset
1083
+ end
1084
+
1085
+ def tag_start(name, attributes)
1086
+ @text = ''
1087
+ tagstart(name, attributes)
1088
+ @xmlpath += @xmlpath.empty? ? name : "/#{name}"
1089
+ end
1090
+
1091
+ def tag_end(name)
1092
+ if @xmlpath =~ /^(.*?)\/?#{name}$/
1093
+ @xmlpath = $1
1094
+ end
1095
+ tagend(name)
1096
+ end
1097
+
1098
+ def text(text)
1099
+ @text += text
1100
+ tagtext(text)
1101
+ end
1102
+
1103
+ # Parser method.
1104
+ # Params:
1105
+ # xml_text - xml message text(String) or Net:HTTPxxx instance (response)
1106
+ # params[:xml_lib] - library name: 'rexml' | 'libxml'
1107
+ def parse(xml_text, params={})
1108
+ # Get response body
1109
+ unless xml_text.is_a?(String)
1110
+ xml_text = xml_text.body.respond_to?(:force_encoding) ? xml_text.body.force_encoding("UTF-8") : xml_text.body
1111
+ end
1112
+
1113
+ @xml_lib = params[:xml_lib] || @xml_lib
1114
+ # check that we had no problems with this library otherwise use default
1115
+ @xml_lib = DEFAULT_XML_LIBRARY unless @@supported_xml_libs.include?(@xml_lib)
1116
+ # load xml library
1117
+ if @xml_lib=='libxml' && !defined?(XML::SaxParser)
1118
+ begin
1119
+ require 'xml/libxml'
1120
+ # is it new ? - Setup SaxParserCallback
1121
+ if XML::Parser::VERSION >= '0.5.1.0'
1122
+ RightSaxParserCallback.include_callback
1123
+ end
1124
+ rescue LoadError => e
1125
+ @@supported_xml_libs.delete(@xml_lib)
1126
+ @xml_lib = DEFAULT_XML_LIBRARY
1127
+ if @logger
1128
+ @logger.error e.inspect
1129
+ @logger.error e.backtrace
1130
+ @logger.info "Can not load 'libxml' library. '#{DEFAULT_XML_LIBRARY}' is used for parsing."
1131
+ end
1132
+ end
1133
+ end
1134
+ # Parse the xml text
1135
+ case @xml_lib
1136
+ when 'libxml'
1137
+ xml = XML::SaxParser.string(xml_text)
1138
+ # check libxml-ruby version
1139
+ if XML::Parser::VERSION >= '0.5.1.0'
1140
+ xml.callbacks = RightSaxParserCallback.new(self)
1141
+ else
1142
+ xml.on_start_element{|name, attr_hash| self.tag_start(name, attr_hash)}
1143
+ xml.on_characters{ |text| self.text(text)}
1144
+ xml.on_end_element{ |name| self.tag_end(name)}
1145
+ end
1146
+ xml.parse
1147
+ else
1148
+ REXML::Document.parse_stream(xml_text, self)
1149
+ end
1150
+ end
1151
+
1152
+ # Parser must have a lots of methods
1153
+ # (see /usr/lib/ruby/1.8/rexml/parsers/streamparser.rb)
1154
+ # We dont need most of them in AwsParser and method_missing helps us
1155
+ # to skip their definition
1156
+ def method_missing(method, *params)
1157
+ # if the method is one of known - just skip it ...
1158
+ return if [:comment, :attlistdecl, :notationdecl, :elementdecl,
1159
+ :entitydecl, :cdata, :xmldecl, :attlistdecl, :instruction,
1160
+ :doctype].include?(method)
1161
+ # ... else - call super to raise an exception
1162
+ super(method, params)
1163
+ end
1164
+
1165
+ # the functions to be overriden by children (if nessesery)
1166
+ def reset;
1167
+ end
1168
+
1169
+ def tagstart(name, attributes)
1170
+ ;
1171
+ end
1172
+
1173
+ def tagend(name)
1174
+ ;
1175
+ end
1176
+
1177
+ def tagtext(text)
1178
+ ;
1179
+ end
1180
+ end
1181
+
1182
+ #-----------------------------------------------------------------
1183
+ # PARSERS: Errors
1184
+ #-----------------------------------------------------------------
1185
+
1186
+ #<Error>
1187
+ # <Code>TemporaryRedirect</Code>
1188
+ # <Message>Please re-send this request to the specified temporary endpoint. Continue to use the original request endpoint for future requests.</Message>
1189
+ # <RequestId>FD8D5026D1C5ABA3</RequestId>
1190
+ # <Endpoint>bucket-for-k.s3-external-3.amazonaws.com</Endpoint>
1191
+ # <HostId>ItJy8xPFPli1fq/JR3DzQd3iDvFCRqi1LTRmunEdM1Uf6ZtW2r2kfGPWhRE1vtaU</HostId>
1192
+ # <Bucket>bucket-for-k</Bucket>
1193
+ #</Error>
1194
+
1195
+ class RightErrorResponseParser < AwsParser #:nodoc:
1196
+ attr_accessor :errors # array of hashes: error/message
1197
+ attr_accessor :requestID
1198
+ # attr_accessor :endpoint, :host_id, :bucket
1199
+ def tagend(name)
1200
+ case name
1201
+ when 'RequestID';
1202
+ @requestID = @text
1203
+ when 'Code';
1204
+ @code = @text
1205
+ when 'Message';
1206
+ @message = @text
1207
+ # when 'Endpoint' ; @endpoint = @text
1208
+ # when 'HostId' ; @host_id = @text
1209
+ # when 'Bucket' ; @bucket = @text
1210
+ when 'Error';
1211
+ @errors << [ @code, @message ]
1212
+ end
1213
+ end
1214
+
1215
+ def reset
1216
+ @errors = []
1217
+ end
1218
+ end
1219
+
1220
+ # Dummy parser - does nothing
1221
+ # Returns the original params back
1222
+ class RightDummyParser # :nodoc:
1223
+ attr_accessor :result
1224
+
1225
+ def parse(response, params={})
1226
+ @result = [response, params]
1227
+ end
1228
+ end
1229
+
1230
+ class RightHttp2xxParser < AwsParser # :nodoc:
1231
+ def parse(response)
1232
+ @result = response.is_a?(Net::HTTPSuccess)
1233
+ end
1234
+ end
1235
+
1236
+ end
1237
+