lunedì 24 settembre 2007

RESTful Web Services Example 3.18 - Building the canonical string

Today’s RESTful Web Services example is another utility method named #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.
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:
nextPutAll: header;
nextPutAll: ':'].
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:
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.

2 commenti:

Ramon Leon ha detto...

What Smalltalk are you using? Squeak dictionaries certainly have #asSortedCollection: with or without a sort block comparable to Ruby’s sort_by.

Also, wouldn’t

^String streamContents:[:canonical | ]

be more idiomatic than

canonical := WriteStream on: String new.

Just curious?

gcorriga ha detto...

Hi Ramon,

I couldn’t use #asSortedCollection because, when sent to a Dictionary instance, it returns a sorted collection of values only, while in this case I need both keys and values.

As for #streamContents:, I agree it would have been more idiomatic, but I’m avoiding using the most specific Smalltalk (and Squeak) idioms in order to make this examples clearer to Smalltalk newbies.

Anyway, the RWS repository on SqueakSource is set to Read and Write, so if you want you can contribute new, more idiomatic versions of the examples.