11module AWS #:nodoc:
22 # AWS::SES is a Ruby library for Amazon's Simple Email Service's REST API (http://aws.amazon.com/ses).
3- #
3+ #
44 # == Getting started
5- #
5+ #
66 # To get started you need to require 'aws/ses':
7- #
7+ #
88 # % irb -rubygems
99 # irb(main):001:0> require 'aws/ses'
1010 # # => true
11- #
11+ #
1212 # Before you can do anything, you must establish a connection using Base.new. A basic connection would look something like this:
13- #
13+ #
1414 # ses = AWS::SES::Base.new(
15- # :access_key_id => 'abc',
15+ # :access_key_id => 'abc',
1616 # :secret_access_key => '123'
1717 # )
18- #
18+ #
1919 # The minimum connection options that you must specify are your access key id and your secret access key.
2020 #
2121 # === Connecting to a server from another region
22- #
22+ #
2323 # The default server API endpoint is "email.us-east-1.amazonaws.com", corresponding to the US East 1 region.
2424 # To connect to a different one, just pass it as a parameter to the AWS::SES::Base initializer:
2525 #
2626 # ses = AWS::SES::Base.new(
27- # :access_key_id => 'abc',
27+ # :access_key_id => 'abc',
2828 # :secret_access_key => '123',
2929 # :server => 'email.eu-west-1.amazonaws.com',
3030 # :message_id_domain => 'eu-west-1.amazonses.com'
3131 # )
3232 #
3333
3434 module SES
35-
35+
36+ SERVICE = 'ses'
37+
3638 API_VERSION = '2010-12-01'
3739
3840 DEFAULT_REGION = 'us-east-1'
3941
40- SERVICE = 'ec2 '
42+ USER_AGENT = 'github-aws-ses-ruby-gem '
4143
4244 DEFAULT_HOST = 'email.us-east-1.amazonaws.com'
4345
4446 DEFAULT_MESSAGE_ID_DOMAIN = 'email.amazonses.com'
45-
46- USER_AGENT = 'github-aws-ses-ruby-gem'
47-
47+
48+ UNSIGNED_HEADERS = [ 'content-length' , 'user-agent' , 'authorization' ]
49+
4850 # Encodes the given string with the secret_access_key by taking the
4951 # hmac-sha1 sum, and then base64 encoding it. Optionally, it will also
5052 # url encode the result of that to protect the string if it's going to
@@ -66,9 +68,9 @@ def SES.encode(secret_access_key, str, urlencode=true)
6668 return b64_hmac
6769 end
6870 end
69-
71+
7072 # Generates the HTTP Header String that Amazon looks for
71- #
73+ #
7274 # @param [String] key the AWS Access Key ID
7375 # @param [String] alg the algorithm used for the signature
7476 # @param [String] sig the signature itself
@@ -81,11 +83,11 @@ def SES.authorization_header_v4(credential, signed_headers, signature)
8183 end
8284
8385 # AWS::SES::Base is the abstract super class of all classes who make requests against SES
84- class Base
86+ class Base
8587 include SendEmail
8688 include Info
87-
88- attr_reader :use_ssl , :server , :proxy_server , :port , :message_id_domain , :signature_version , :region
89+
90+ attr_reader :use_ssl , :server , :proxy_server , :port , :message_id_domain , :signature_version , :region , :datetime , :date , :headers , :query , :action
8991 attr_accessor :settings
9092
9193 # @option options [String] :access_key_id ("") The user's AWS Access Key ID
@@ -125,6 +127,7 @@ def initialize( options = {} )
125127 raise ArgumentError , "No :use_ssl value provided" if options [ :use_ssl ] . nil?
126128 raise ArgumentError , "Invalid :use_ssl value provided, only 'true' or 'false' allowed" unless options [ :use_ssl ] == true || options [ :use_ssl ] == false
127129 raise ArgumentError , "No :server provided" if options [ :server ] . nil? || options [ :server ] . empty?
130+ raise ArgumentError , "signature_version must be `2` or `4`" unless signature_version == 2 || signature_version == 4
128131
129132 if options [ :port ]
130133 # user-specified port
@@ -150,19 +153,22 @@ def initialize( options = {} )
150153
151154 @http . use_ssl = @use_ssl
152155 end
153-
156+
154157 def connection
155158 @http
156159 end
157-
158- # Make the connection to AWS passing in our request.
160+
161+ # Make the connection to AWS passing in our request.
159162 # allow us to have a one line call in each method which will do all of the work
160163 # in making the actual request to AWS.
161164 def request ( action , params = { } )
162165 # Use a copy so that we don't modify the caller's Hash, remove any keys that have nil or empty values
166+ @action = action
163167 params = params . reject { |key , value | value . nil? or value . empty? }
164-
165- timestamp = Time . now . getutc
168+
169+ timestamp = Time . now . utc
170+ @datetime = timestamp . strftime ( "%Y%m%dT%H%M%SZ" )
171+ @date = @datetime [ 0 , 8 ]
166172
167173 params . merge! ( { "Action" => action ,
168174 "SignatureVersion" => signature_version . to_s ,
@@ -171,100 +177,130 @@ def request(action, params = {})
171177 "Version" => API_VERSION ,
172178 "Timestamp" => timestamp . iso8601 } )
173179
174- query = params . sort . collect do |param |
180+ @ query = params . sort . collect do |param |
175181 CGI ::escape ( param [ 0 ] ) + "=" + CGI ::escape ( param [ 1 ] )
176182 end . join ( "&" )
177183
178- req = { }
184+ @headers = { }
185+ @headers [ 'host' ] = @server
186+ @headers [ 'x-amz-date' ] = @datetime
187+ @headers [ 'user-agent' ] = @user_agent
188+ @headers [ signature_version == 4 ? 'authorization' : 'x-amzn-authorization' ] = gen_authorization ( timestamp . httpdate )
179189
180- req [ 'X-Amzn-Authorization' ] = get_aws_auth_param ( timestamp . httpdate , @secret_access_key , action , signature_version )
181- req [ 'Date' ] = timestamp . httpdate
182- req [ 'User-Agent' ] = @user_agent
190+ response = connection . post ( @path , @query , @headers )
183191
184- response = connection . post ( @path , query , req )
185-
186192 response_class = AWS ::SES . const_get ( "#{ action } Response" )
187193 result = response_class . new ( action , response )
188-
194+
189195 if result . error?
190196 raise ResponseError . new ( result )
191197 end
192-
198+
193199 result
194200 end
195201
196202 # Set the Authorization header using AWS signed header authentication
197- def get_aws_auth_param ( timestamp , secret_access_key , action = '' , signature_version = 2 )
198- raise ( ArgumentError , "signature_version must be `2` or `4`" ) unless signature_version == 2 || signature_version == 4
199- encoded_canonical = SES . encode ( secret_access_key , timestamp , false )
200-
203+ def gen_authorization ( timestamp )
201204 if signature_version == 4
202- SES . authorization_header_v4 ( sig_v4_auth_credential , sig_v4_auth_signed_headers , sig_v4_auth_signature ( action ) )
205+ sigv4 = signature ( date , region , SERVICE , string_to_sign )
206+ SES . authorization_header_v4 ( credentials , signed_headers , sigv4 )
203207 else
208+ encoded_canonical = SES . encode ( @secret_access_key , timestamp , false )
204209 SES . authorization_header ( @access_key_id , 'HmacSHA256' , encoded_canonical )
205210 end
206211 end
207212
208213 private
209214
210- def sig_v4_auth_credential
211- @access_key_id + '/' + credential_scope
212- end
213-
214- def sig_v4_auth_signed_headers
215- 'host;x-amz-date'
216- end
217-
218- def credential_scope
219- datestamp + '/' + region + '/' + SERVICE + '/' + 'aws4_request'
215+ def canonical_uri
216+ '/'
220217 end
221218
222- def string_to_sign ( for_action )
223- "AWS4-HMAC-SHA256 \n " + amzdate + " \n " + credential_scope + " \n " + Digest :: SHA256 . hexdigest ( canonical_request ( for_action ) . encode ( 'utf-8' ) . b )
219+ def canonical_querystring
220+ signature_version == 2 ? "Action= #{ action } &Version=2013-10-15" : ''
224221 end
225222
226-
227- def amzdate
228- Time . now . utc . strftime ( '%Y%m%dT%H%M%SZ' )
223+ def canonical_request
224+ [
225+ 'POST' ,
226+ canonical_uri ,
227+ canonical_querystring ,
228+ canonical_headers + "\n " ,
229+ signed_headers ,
230+ sha256_hexdigest ( @query ) ,
231+ ] . join ( "\n " )
229232 end
230233
231- def datestamp
232- Time . now . utc . strftime ( '%Y%m%d' )
234+ def credential_scope
235+ date + '/' + region + '/' + SERVICE + '/' + 'aws4_request'
233236 end
234237
235- def canonical_request ( for_action )
236- "GET" + "\n " + "/" + "\n " + canonical_querystring ( for_action ) + "\n " + canonical_headers + "\n " + sig_v4_auth_signed_headers + "\n " + payload_hash
238+ def string_to_sign
239+ [
240+ 'AWS4-HMAC-SHA256' ,
241+ datetime ,
242+ credential_scope ,
243+ sha256_hexdigest ( canonical_request ) ,
244+ ] . join ( "\n " )
237245 end
238246
239- def canonical_querystring ( action )
240- "Action=#{ action } &Version=2013-10-15"
247+ def signed_headers
248+ @headers . inject ( [ ] ) do |signed_headers , ( header , _ ) |
249+ if UNSIGNED_HEADERS . include? ( header )
250+ signed_headers
251+ else
252+ signed_headers << header
253+ end
254+ end . sort . join ( ';' )
241255 end
242256
243257 def canonical_headers
244- 'host:' + server + "\n " + 'x-amz-date:' + amzdate + "\n "
258+ _headers = @headers . inject ( [ ] ) do |hdrs , ( k , v ) |
259+ if UNSIGNED_HEADERS . include? ( k )
260+ hdrs
261+ else
262+ hdrs << [ k , v ]
263+ end
264+ end
265+ _headers = _headers . sort_by ( &:first )
266+ _headers . map { |k , v | "#{ k } :#{ canonical_header_value ( v . to_s ) } " } . join ( "\n " )
245267 end
246268
247- def payload_hash
248- Digest :: SHA256 . hexdigest ( '' . encode ( 'utf-8' ) )
269+ def canonical_header_value ( value )
270+ value . match ( /^".*"$/ ) ? value : value . gsub ( / \s +/ , ' ' ) . strip
249271 end
250272
251- def sig_v4_auth_signature ( for_action )
252- signing_key = getSignatureKey ( @secret_access_key , datestamp , region , SERVICE )
253-
254- OpenSSL ::HMAC . hexdigest ( "SHA256" , signing_key , string_to_sign ( for_action ) . encode ( 'utf-8' ) )
273+ def credentials
274+ @access_key_id + '/' + credential_scope
255275 end
256276
257- def getSignatureKey ( key , dateStamp , regionName , serviceName )
258- kDate = sign ( ( 'AWS4' + key ) . encode ( 'utf-8' ) , dateStamp )
259- kRegion = sign ( kDate , regionName )
260- kService = sign ( kRegion , serviceName )
261- kSigning = sign ( kService , 'aws4_request' )
277+ def signature ( date , region , service , string_to_sign )
278+ k_date = hmac ( "AWS4" + @secret_access_key , date )
279+ k_region = hmac ( k_date , region )
280+ k_service = hmac ( k_region , service )
281+ k_credentials = hmac ( k_service , 'aws4_request' )
282+ OpenSSL ::HMAC . hexdigest ( OpenSSL ::Digest . new ( 'sha256' ) , k_credentials , string_to_sign )
283+ end
262284
263- kSigning
285+ def hmac ( key , value )
286+ OpenSSL ::HMAC . digest ( OpenSSL ::Digest . new ( 'sha256' ) , key , value )
264287 end
265288
266- def sign ( key , msg )
267- OpenSSL ::HMAC . digest ( "SHA256" , key , msg . encode ( 'utf-8' ) )
289+ def sha256_hexdigest ( value )
290+ if ( File === value || Tempfile === value ) && !value . path . nil? && File . exist? ( value . path )
291+ OpenSSL ::Digest ::SHA256 . file ( value ) . hexdigest
292+ elsif value . respond_to? ( :read )
293+ sha256 = OpenSSL ::Digest ::SHA256 . new
294+ loop do
295+ chunk = value . read ( 1024 * 1024 ) # 1MB
296+ break unless chunk
297+ sha256 . update ( chunk )
298+ end
299+ value . rewind
300+ sha256 . hexdigest
301+ else
302+ OpenSSL ::Digest ::SHA256 . hexdigest ( value )
303+ end
268304 end
269305 end # class Base
270306 end # Module SES
0 commit comments