cmeiklejohn-aws 2.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+