hackerdude-aws 2.3.25

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