#canonicalStringFor:path:headers:expires:
. This method builds the string that will be used to sign the HTTP request.According to the Amazon S3 Developer Guide, the canonical string is composed with the HTTP request method, the request path and some of the headers of the request. I refer you to either the S3 docs or to the RWS book for more information on how the string is built.
The code for
#canonicalStringFor:path:headers:expires:
is the following:S3Object>>canonicalStringFor: method path: path headers: headers expires: expires
| signHeaders canonical param |
"Turns the elements of an HTTP request into a string that can be
signed to prove a request comes from your web service account.
Code taken from RESTful Web Services. Copyright © 2007 O'Reilly Media, Inc.
All rights reserved. Used with permission."
"Start out with default values for all the interesting headers."
signHeaders := Dictionary new.
InterestingHeaders do: [:header | signHeaders at: header put: String new].
"Copy in any actual values, including values for custom S3 headers."
headers keysAndValuesDo:
[:header :value |
header isString ifTrue:
[header := header asLowercase.
"If it's a custom header, or one Amazon thinks is interesting..."
((InterestingHeaders includes: header) or:
[header beginsWith: AmazonHeaderPrefix]) ifTrue:
[signHeaders at: header put: value asString]]].
"This library eliminates the need for the x-amz-date header that Amazon defines,
but someone might set it anyway. If they do, we'll do without HTTP's standard Date header."
(signHeaders includesKey: 'x-amz-date') ifTrue: [signHeaders at: 'date 'put: String new].
"If an expiration time was provided, it overrides any Date header.
This signature will be valid until the expiration time,
not only during the single second designated by the Date header."
expires ifNotNil: [signHeaders at: 'date' put: expires].
"Now we start building the canonical string for this request. We start with the HTTP method."
canonical := WriteStream on: String new.
canonical
nextPutAll: method asUppercase;
nextPutAll: String cr.
"Sort the headers by name, and append them (or just their values) to the string to be signed."
signHeaders associations sort do:
[:assoc | | header value |
header := assoc key.
value := assoc value.
(header beginsWith: AmazonHeaderPrefix) ifTrue:
[canonical
nextPutAll: header;
nextPutAll: ':'].
canonical
nextPutAll: value;
nextPutAll: String cr].
"The final part of the string to be signed is the URI path.
We strip off the query string, and (if neccessary) tack one of the
special S3 query parameters back on: 'acl', 'torrent', or 'logging'."
canonical nextPutAll: (path copyUpTo: $?).
param := #('acl' 'torrent' 'logging')
detect: [:each | path includesSubString: '?', each]
ifNone: [nil].
param ifNotNil:
[canonical
nextPutAll: '?';
nextPutAll: param].
^ canonical contents
This is quite a long method according to Smalltalk’s standards, but it is a straightforward mapping of the Ruby one explained in the RWS book. Were I to built a real S3 library, I’d probably create something similar to Seaside’s and HV2’s Canvas systems.
The most glaring difference between this Smalltalk code and the Ruby one is how the path is handled. S3 requires you to strip the query string (that is, the part after the question mark) and to attach back one of the S3 query parameters (
acl
, torrent
, and logging
) in case it’s part of the query string. Richardson and Ruby use a regular expression to identify these parameters. I chose a simpler method on the account that, according to the S3 documentation, each request may have only one of the three special S3 parameters. So I just look for a string such as ?acl
and, if found, I attach it to the canonical string.Another difference is in how headers are handled. Ruby’s
Dictionary
class has a #sort_by
message that returns a sorted collection over which we can iterate. Smalltalk’s dictionaries don’t have this feature, so I have to send an #associations
message to obtain a sequential collections of associations. I iterate over this collection with an unary block that takes a single association as the argument of its #value:
message. In this example I split the association into its key and value and put them into two block temporary variables. This is done only for sake of clarity.In the next example in this series, we’ll see the other two messages understood by
S3Object
: the one we’ll use for signing the request and the one we’ll use for sending the HTTP request.