Posted in Development on Sunday, Sunday, April 26, 2009 by Anthony Burns
UPDATE: Part Three of this series is now available
Adding Basic Authentication to a url request is pretty straight forward, in fact it only requires a header to be added to the request with the NSURLRequest.addValue:forHTTPHeaderField method. Simple huh? Not quite. The HTTP spec requires that the username and password be Base64 encoded, which is usually a case of requiring the base64 library in standard Ruby, but that isn't available in MacRuby, and there doesn't appear to be Cocoa object designed for the purpose.
After spending five minutes googling turned up exactly bot all, I decided to change tack and find out how to base64 encode myself.
How to Base64 Encode
http://email.about.com/cs/standards/a/base64_encoding.htm
So with a combination of the above article, a trusty Ruby book, blood, sweat, tears and a lot of trial and error I constructed:
class Base64
ALPHA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-="
def encode(str)
dat = []
str.each_byte { |b| dat << b }
# Append 0s to the data to make it's length divisible by 3
missingChars = 3 - (dat.length % 3)
missingChars.times { dat << 0 }
result = ''
sections = dat.length / 3
sections.times do |i|
a = dat[(i * 3) + 0] >> 2
b = dat[(i * 3) + 0] << 4 & 63
b = b | dat[(i * 3) + 1] >> 4
c = dat[(i * 3) + 1] << 2 & 63
c = c | dat[(i * 3) + 2] >> 6
d = dat[(i * 3) + 2] & 63
if dat[(i * 3) + 1] == 0 then c = 64 end
if dat[(i * 3) + 2] == 0 then d = 64 end
result += ALPHA[a] + ALPHA[b] + ALPHA[c] + ALPHA[d]
end
return result
end
end
There's no point me stepping through it, you should be able to dissect it easy enough with the referenced article, or just use it if you don't care how it works - it's not perfect and probably won't work in all scenarios, but it's good enough for Base64 encoding a username/password combination.
We can then use the Base64 class to encode our credentials and add the Basic Authentication header, which I've chosen to do in a separate method of the TwitterApi class called addAuthentication:
def addAuthentication(req) enc = Base64.new data = @username + ':' + @password data = enc.encode(data) data = 'Basic ' + data req.addValue data, forHTTPHeaderField:'Authorization' end
I've also added a couple of attributes to the class that allow the username and password to be set:
attr_accessor :username, :password
And I've added a call to addAuthentication to the getUrl method as well as an authenticated parameter to the method signature, allowing you to specify if you want the request to be authenticated or not:
def getUrl(urlString, authenticated, delegate) callback = HttpRequestCallback.new callback.delegate = delegate callback.buf = NSMutableData.new callback.response = nil url = NSURL.URLWithString(urlString) request = NSMutableURLRequest.requestWithURL(url, cachePolicy:POLICY, timeoutInterval:TIMEOUT) addAuthentication(req) if authenticated # <-- Add Authentication if required callback.conn = NSURLConnection.alloc.initWithRequest(request, delegate:callback) end
Finally I've added a getTimeline method to the class that calls getUrl for the authenticated user's timeline:
def getTimeline(callback)
getUrl('http://twitter.com/statuses/friends_timeline.xml', true, lambda {|data| callback.call(data) })
end
We can consume this like so:
twitter = TwitterApi.new twitter.username = 'xxx' twitter.password = 'xxx' twitter.getTimeline(lambda do |data| result = NSString.alloc.initWithData data, encoding:NSUTF8StringEncoding # display the result in an alert box alert = NSAlert.new alert.setMessageText(result) alert.runModal() end)
Which should download the Xml of your authenticated Twitter timeline and display it in a message box.
So we can now obtain our own timeline authenticated using Basic Authentication; next we'll parse the Xml into an array of status objects and bind them to the table in our main window. Part Three Available Here.
Tagged as: cocoa, macruby, ruby
MacRuby doesn't have the base64 library because it is useless. You can encode any string in a single line by doing: [str].pack("m") For example: ["Hello World"].pack("m") = "SGVsbG8gV29ybGQ=\n"
Thanks for the pointer Luc, but it doesn't seem to work for me - pack("m") returns an empty string.
Quote: "I decided to change tack and find out how to base64 encode myself" ...sweet! Does that mean you can transmit yourself to anywhere that can decode base64? :-)
Hmm, maybe try .pack("m*") (with the *)?
[@username + ':' + @password].pack("m").chomp the chomp was eluding me. With it makes it work. printing the 2 out sometimes hides the new line.
@Luc + @Keenan I've tried both of those and neither seems to work. I have a method called alert(msg) that pops up an NSAlert box containing the msg, and I've tried: alert(['mystring'].pack("m*")) as well as the chomp one and the alert always comes up empty.