Monday, June 19, 2006

Distributed Ruby (from the archive)

I originally wrote this material for IBM. It was previously published as part of an IBM DeveloperWorks Tutorial located here. IBM has been gracious enough to let me republish it here.


What better example to show you dRuby in action than an ASCII duck image server? Why_ wrote it to show off a number of ASCII art ducks created on the ruby-talk mailing list.

The server code is simple:



Listing 1. A duck image server


$SAFE = 1
require 'drb'
class Duck
    @@ducks = {}
    Dir["ruby-duck-*.asc"].each do |duckfile|
        owner = duckfile.gsub( /^ruby-duck-(\w+)\.asc$/, '\1' ).untaint
        @@ducks[owner] = File.read( duckfile.untaint )
        class_eval %{
            def self.#{owner}
                @@ducks['#{owner}']
            end
        }
    end
    def self.list
        @@ducks.keys
    end
end
DRb.start_service "druby://whytheluckystiff.net:6503", Duck
DRb.thread.join

Only a few things are going on here. First, Why_ uses $SAFE and untaint to help control access to just the files to distribute. Setting $SAFE to 1 turns off the ability to process tainted data (data is considered tainted when it comes from outside the program). Because the file names used in the next step are tainted, Why_ uses untaint to regain access to that data. This is a good example of an important concept for anyone writing code that will be executed by untrusted users.

The next couple of things occur in the Duck class (notice that this class doesn't need to know about dRuby). Why_ starts with meta-programming, creating a method for each duck image file that displays the contents of that file. Then a method prints a listing of all the available images.

Now, Why_ uses dRb (the dRuby library). It just takes one line to start a dRb server that serves up an instance of the Duck class (which creates a separate thread behind the scenes). Finally, Why_ joins the thread to ensure that dRb is finished before the process exits.

A client for this server is also simple. You can run it from the command line: ruby -rdrb -e "DRb.start_service;duck=DRbObject.new(nil, 'druby://whytheluckystiff.net:6503');puts duck.list".

Running Ruby from the command line is a different idiom from what many people are used to, but it's not too hard to grok. The -r switch requires the dRb library, and the -e switch says to run the command(s) that follow. In this case, the command does three things: It starts a dRuby service, instantiates a Duck object from the dRb server, then does a puts of the output of the list method.

Distributing your Ruby objects

If you want to use dRuby yourself, Why's duck image server has shown you the two things you need: DRb::start_service and DRb::thread.join. Using just these two methods, you can create a simple server of your own, as shown in Listing 2.


Listing 2. A DRb Morse code server


require 'drb'
require 'Morse'
include Morse

class DMorse
  def self.di_dah(string)
    string.morse
  end
end

DRb.start_service "druby://localhost:6503", DMorse
DRb.thread.join

You probably want to do a bit more than this, though. A good first step is to add some security. Again, dRuby makes it easy. DRb#start_service accepts ACL objects (which can also be defined separately), allowing you to lock your new service. The simplest ACLs (although not the most secure) are IP address-based. If you're running the Morse code service shown in Listing 11 on a machine in the 192.168.1.0/24 network, and you want to limit access to only those hosts on your local network, you can define an ACL as shown in Listing 3.


Listing 3. ACLs for DRb


  require 'drb/acl'

  acl = ACL.new(%{deny all
                  allow localhost
                  allow 192.168.1.*})
  DRb.install_acl(acl)

You should do all you can to write safe and secure code. Use $SAFE, and think about using undef to get rid of unsafe methods. Don't forget, you're potentially opening up your code and your server to unknown users.


No comments: