Saturday, March 15, 2008

Using stunnel to wrap Ruby network operations on the fly

In my current project, we need to be able to connect to POP3 servers. Some POP3 servers, such as Gmail, only allow SSL connections. Unfortunately, the Ruby 1.8.x net/pop library doesn't support SSL (although the 1.9 library does, but that was not an option for us in this project).

The usual answer here is to wrap your connection using stunnel, which acts as an SSL proxy for whatever traffic you want to send over it. Usually you run stunnel as a separate service pointing at the server, but since we'll be connecting to many different POP servers, I needed to be able to set up and tear down stunnels on the fly. The first attempt looked something like this:
system("echo -e 'foreground = yes\npid =\n[mail]\nclient = yes\n \
accept = 127.0.0.1:2000\nconnect = #{server}:#{port}\n' \
| stunnel -fd 0")
Since stunnel doesn't accept command-line options, you have to pipe options to it. The "fd -0" tells stunnel to read its configuration from file descriptor 0, better known as STDIN.

Since I need to run that command in a child process, then have the parent resume and make use of the child service, I embarked on a fun foray of Ruby's forking and threading capabilities.

First, I tried forking, replacing the child process with a call to exec instead of system, then detaching the parent and killing the child process when the POP session was done. This partially worked, but I couldn't figure out how to kill the child process, so I'd end up with multiple copies of stunnel running after the script ran, or the parent process itself would hang.

Looking through the Pickaxe chapter on threads and processes, I discovered IO.popen, which works perfectly. I can pipe input to STDIN, avoiding the ugliness of the "echo -e" above, and I can more easily kill the child process when I'm done.

This is what the final method looks like:
def stunnel_wrap(server, port)
stunnel = IO.popen("stunnel -fd 0",'w+')
stunnel.puts("foreground = yes\npid =\n[mail]\nclient = yes\n \
accept = 127.0.0.1:2000\nconnect = #{server}:#{port}\n"
)
stunnel.close_write
Kernel.sleep(1)
yield
ensure
Process.kill(9,stunnel.pid)
end
I handle exceptions at a higher layer in this class, so here all I do is make sure the stunnel process gets killed no matter what. I'm not sure if the sleep call is needed, but when I was testing this with Gmail it seemed to help to wait one second for the tunnel to activate before trying to use it.

To make the above example work, you just need to point your POP client at the stunnel (in this case 127.0.0.1 port 2000) and you'll be talking SSL to the server.
stunnel_wrap('pop.gmail.com',995) do
Net::POP3.start('127.0.0.1', 2000, account, password) do |pop|
# pop securely
end
end

2 comments:

Anne and Brian said...

i know i'm not supposed to be able to understand this:

First, I tried forking, replacing the child process with a call to exec instead of system, then detaching the parent and killing the child process when the POP session was done.

but it sounds very violent... ;)
Happy almost birthday, bro!

Anonymous said...

During the rest of that day there was no other adventure to
mar the peace of their journey. Once, indeed, the Tin Woodman
stepped upon a beetle that was crawling along the road, and killed
the poor little thing. This made the Tin Woodman very unhappy,
for he was always careful not to hurt any living creature;


Nike shoes
MBT
supra footwear
famous footwear
shoe dept
nike air max