Sunday, March 05, 2006

Using SSL with Ruby http-access2

Expanding on my Ruby scripts, I wanted to download information from my yahoo account. Using Ruby to get HTTP is super easy. For example, the following will print the contents of http://www.w3.org/
require 'http-access2'
client = HTTPAccess2::Client.new()
puts client.get('http://www.w3.org/').content


Sometime later, I may post some examples of parsing HTML which is also super easy. For now I will talk about getting SSL to work.

Ruby http-access2 is a great library for writting web servers and clients. In this instance I am writting a simple client to login to yahoo and download information like my bookmarks. To login to yahoo, Ruby must use SSL.

It turns out that calling SSL from ruby is pretty easy. Just add the SSL configuration information. [Also, I changed the URL in this example.]
require 'http-access2'
client = HTTPAccess2::Client.new()
client.ssl_config.set_trust_ca('ca.cert')
puts client.get('https://login.yahoo.com/config/login?').content


This works great, if you have the correct certificate. If you don't (which happened to me), you get the message:
at depth 0 - 20: unable to get local issuer certificate
http-access2.rb:1001:in `connect': certificate verify failed (OpenSSL::SSL::SSLError)
from c:/codetiger/ruby/tools/ruby/lib/ruby/site_ruby/1.8/http-access2.rb:1001:in `ssl_connect'
from c:/codetiger/ruby/tools/ruby/lib/ruby/site_ruby/1.8/http-access2.rb:1363:in `connect'
...

Figuring out what caused this error was a little confusing because of the "unable to get local issuer certificate" message. It really means "Unable to validate the certificate of the host because the trusted root certificate was not found locally." Unfortunately http-access2's docs are almost non-existent. I eventually found this information from the cURL faq! cURL and http-access2 both use OpenSSL, so the faq about this error is correct. The error message in the stack trace was from OpenSSL.

To solve this error, I had to get the correct root certificate. This is easily accomplished. First, check the web page properties in your browser. For the yahoo page, you will find yahoo is signed by "Equifax Secure Certificate Authority". I'm using Windows XP, so all I had to do was go into Control Panel -> Internet Options -> Content tab -> Certificates -> Trusted Root Certificates -> select "Equifax Secure Certificate Authority" -> Export -> select Base 64 Encoding -> save to file (EquifaxSecureCertificateAuthority.cer). [Firefox did not have an export option.]

Now that the trusted root cert is in a file, Ruby can use it...
require 'http-access2'
client = HTTPAccess2::Client.new()
client.ssl_config.set_trust_ca('EquifaxSecureCertificateAuthority.cer')
puts client.get('https://login.yahoo.com/config/login?').content


Yahoo!!!

Friday, March 03, 2006

.Net in Ruby

While creating a simple backup script (see previous post), I needed to copy more than the file's data. The backup copy needed to include the meta-data that Windows keeps for the file. Ruby (and Java or Ant) can easily copy the contents of a file, but they don't know how to copy the meta-data. The solution is to use .Net, but I wanted to write my script in Ruby.

I found the Ruby/.Net Bridge worked like magic. For example to copy a file, just use...
require 'dotnet' # Ruby / .Net bridge
System.IO.File.Copy( srcpath, destpath )


The install of Ruby/.Net Bridge was very simple. Just download, unzip, and run deploy.cmd. The ease of installation made me very happy.

After I created my backup script. I wondered if this Bridge worked with .Net v2.0. So I took a spare machine and removed .Net v1.1 to make sure the Bridge could only use v2.0. Everything worked perfect!

The one area that I had trouble was handling .Net's Enum. In .Net, the FileAttributes class is an Enum which is also a bit field (see FlagsAttribute). Using the Ruby/.Net Bridge, I could not perform bit-wise math operations on the Enum. I asked the authors, who gave me a good solution using the Parse method. For example:
require 'dotnet' # Ruby / .Net bridge

def attributeRemove( fileattribute, attribute )
Enum.parse( System.IO.FileAttributes, (fileattribute.ToString().split(', ') - [attribute]).join( ', ' ) )
end

def setReadWrite( dospath )
attrs = attributeRemove( System.IO.File.GetAttributes( dospath ), 'ReadOnly' )
System.IO.File.SetAttributes( dospath, attrs )
end

The last feature that I wanted, was to decrypt files and folders that were stored in encrypted file systems. Since Windows encryption used a key tied to the machine and user, the backup should not stay encrypted. If the machine failed it may be impossible to get the necessary key to decrypt. Unfortunately there wasn't a .Net method to decrypt the file or folder. Instead I had to call the win32api. This was pretty easy to do (however, I found the available documentation very lacking.) The following is a combination of some Utility methods, and the DecryptFile method I created for my script.
require 'Win32API'
require 'dotnet' # Ruby / .Net bridge http://www.saltypickle.com/rubydotnet/ just download it, uncompress and run deploy.cmd (per instructions on site)

class Utils
def Utils.attribute?( dospath, attribute )
if attribute.is_a?(String)
return System.IO.File.GetAttributes( dospath ).ToString().split(', ').include?( attribute )
else
raise "attribute parameter must be a String. attribute.class=#{attribute.class}"
end
end

def Utils.attributeAdd( fileattribute, attribute )
Enum.parse( System.IO.FileAttributes, fileattribute.ToString() + ', ' + attribute )
end

def Utils.attributeRemove( fileattribute, attribute )
Enum.parse( System.IO.FileAttributes, (fileattribute.ToString().split(', ') - [attribute]).join( ', ' ) )
end

def Utils.setReadWrite( dospath )
attrs = Utils.attributeRemove( System.IO.File.GetAttributes( dospath ), 'ReadOnly' )
System.IO.File.SetAttributes( dospath, attrs )
end

def Utils.setReadOnly( dospath )
attrs = Utils.attributeAdd( System.IO.File.GetAttributes( dospath ), 'ReadOnly' )
System.IO.File.SetAttributes( dospath, attrs )
end
end

class Win32API
def Win32API.DecryptFile( dospath )
Utils.setReadWrite( dospath ) if isReadOnly = Utils.attribute?( dospath, 'ReadOnly' )
result = Win32API.new('Advapi32', 'DecryptFile', %w(p i), 'i').call(dospath,0)
#todo better error checking
if result == 0
Utils.setReadOnly( dospath ) if isReadOnly
raise "DecryptFile call failed on path: \"#{dospath}\". see also: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/fileio/fs/decryptfile.asp"
end
Utils.setReadOnly( dospath ) if isReadOnly
return result
end
end


An example usage...
if Utils.attribute?( path, 'Encrypted' )
Win32API.DecryptFile( path )
end