old-www/LDP/LG/issue86/okopnik.html

463 lines
25 KiB
HTML

<!--startcut ==============================================-->
<!-- *** BEGIN HTML header *** -->
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN">
<HTML><HEAD>
<title>Perl One-Liner of the Month: The Case of the Evil Spambots LG #86</title>
</HEAD>
<BODY BGCOLOR="#FFFFFF" TEXT="#000000" LINK="#0000FF" VLINK="#0000AF"
ALINK="#FF0000">
<!-- *** END HTML header *** -->
<!-- *** BEGIN navbar *** -->
<IMG ALT="" SRC="../gx/navbar/left.jpg" WIDTH="14" HEIGHT="45" BORDER="0" ALIGN="bottom"><A HREF="lechnyr.html"><IMG ALT="[ Prev ]" SRC="../gx/navbar/prev.jpg" WIDTH="16" HEIGHT="45" BORDER="0" ALIGN="bottom"></A><A HREF="index.html"><IMG ALT="[ Table of Contents ]" SRC="../gx/navbar/toc.jpg" WIDTH="220" HEIGHT="45" BORDER="0" ALIGN="bottom" ></A><A HREF="../index.html"><IMG ALT="[ Front Page ]" SRC="../gx/navbar/frontpage.jpg" WIDTH="137" HEIGHT="45" BORDER="0" ALIGN="bottom"></A><A HREF="http://www.linuxgazette.com/cgi-bin/talkback/all.py?site=LG&article=http://www.linuxgazette.com/issue86/okopnik.html"><IMG ALT="[ Talkback ]" SRC="../gx/navbar/talkback.jpg" WIDTH="121" HEIGHT="45" BORDER="0" ALIGN="bottom" ></A><A HREF="../lg_faq.html"><IMG ALT="[ FAQ ]" SRC="./../gx/navbar/faq.jpg"WIDTH="62" HEIGHT="45" BORDER="0" ALIGN="bottom"></A><A HREF="qubism.html"><IMG ALT="[ Next ]" SRC="../gx/navbar/next.jpg" WIDTH="15" HEIGHT="45" BORDER="0" ALIGN="bottom" ></A><IMG ALT="" SRC="../gx/navbar/right.jpg" WIDTH="15" HEIGHT="45" ALIGN="bottom">
<!-- *** END navbar *** -->
<!--endcut ============================================================-->
<TABLE BORDER><TR><TD WIDTH="200">
<A HREF="http://www.linuxgazette.com/">
<IMG ALT="LINUX GAZETTE" SRC="../gx/2002/lglogo_200x41.png"
WIDTH="200" HEIGHT="41" border="0"></A>
<BR CLEAR="all">
<SMALL>...<I>making Linux just a little more fun!</I></SMALL>
</TD><TD WIDTH="380">
<CENTER>
<BIG><BIG><STRONG><FONT COLOR="maroon">Perl One-Liner of the Month: The Case of the Evil Spambots</FONT></STRONG></BIG></BIG>
<BR>
<STRONG>By <A HREF="../authors/okopnik.html">Ben Okopnik</A></STRONG>
</CENTER>
</TD></TR>
</TABLE>
<P>
<!-- END header -->
<p><i>A REPORTER'S NOTE</i>
<p><i>To forestall some sure-to-happen complaints, I'd like to underscore
the necessity of having the current version of Perl (at least 5.8.0, as
of this writing) in order to play with the scripts presented in these articles.
One-liners, to a far greater degree than proper scripts, rely on new and
unusual language features, and languages tend to "grow" new features and
drop old, outdated ones as version numbers rise. Perl, heading for its
17th year of growth and development, is no exception.</i>
<p><i>One of a number of possible problems with one-liners is fragility,
especially in those (many of them) which are dependent on cryptocontext,
side effects, and undocumented features, which are likely - in fact, are
</i>certain<i>
- to change without notice. One-liners are hacks which often demonstrate
some clever twist or feature, which encourages the use of all of the above.
Remember - these are fun toys which (hopefully) lead to a better understanding
of Perl; trying to use them as you would robust, solid code would be a
serious error. If you don't understand the </i><a href="../issue61/okopnik.html">basics</a><i>
of Perl, this is <b>not</b> the place to start.</i>
<br>&nbsp;
<pre>Debugging is twice as hard as writing the code in the first place.&nbsp;
Therefore, if you write the code as cleverly as possible, you are,
by definition, not smart enough to debug it.
&nbsp;-- Brian W. Kernighan</pre>
<p><br><i><b>Caveat Lector</b> (Let the reader beware).</i>
<p><b><i>Ben Okopnik</i></b>
<br><i>On board S/V "Ulysses", Saint Augustine, Florida</i>
<hr>
<p>Frink Ooblick had fallen asleep at the keyboard. He had been alternately
playing and trying to puzzle out the number-guessing game that Woomert
had written (the first had proven easy, but the second still eluded him);
in fact, his last unfinished game was still visible on the screen:
<pre>
<hr WIDTH="100%">perl -wlne'BEGIN{$b=rand$=}$a=qw/Up exit Down/[($_&lt;=&gt;int$b)+1];print eval$a'
50
Down
25
Up
37
Up
44
Up
<hr WIDTH="100%"></pre>
What was the secret? How did it work? <a href="#1">[1]</a> Frink's dreams
were full of floating bits of code which spiraled off into the distance
or mutated into monstrous shapes, threatening to consume the world. The
hand shaking his shoulder, waking him, was therefore a welcome relief.
Woomert stood at his side, looking impatient.
<p>&nbsp;- "Wake up, Frink, wake up! The game's afoot, you slug-a-bed;
let's go!"
<p>&nbsp;- "Uh... Erm... I'm, uh, awake. What's up?"
<p>&nbsp;- "In the living room. Come on, come on, there's not a moment
to lose!"
<p>Frink's first sight of their visitor brought him to a stop. Used to
dealing with the working crowd - sysadmins, techs, etc. - he had expected
the usual scruffy-and-competent look, perhaps complete with hiking boots;
what greeted his eyes was a fellow in a pinstripe suit, crisp white shirt,
a red "power" tie, and lacquered black shoes. He had been impatiently pacing
the floor, and brightened up considerably at the sight of Frink.
<p>&nbsp;- "Ah, this must be the second team member in your organizational
hierarchy! Excellent; now, we can get into actualizing the power strategies
that will reorganize this, erm, unpredicted opportunity into the profit
slot on the balance sheet. All right, here's how we wind-tunnel this: the
securitization of the computing resources is predicated on leveraging..."
<p>Keeping a cautious eye on their visitor, Frink prison-whispered to Woomert:
"What's he <i>saying?</i> And what language is it in?"
<p>&nbsp;- "It's Marketroid. You need to learn at least the basics of it;
not that it's spoken by the people who sign the checks - they don't have
much time for that sort of thing - but you're going to run into it in the
business world, and it's best to be prepared. Usually, though, most of
these people can still speak English; let's see if this fellow remembers
how. Oh, Mr. Wibbley!"
<p>Their visitor had just finished what he obviously considered an explanation
of the problem, had switched off the overhead LCD projector, put away his
laser pointer, and was looking at them in an expectant manner. Clearly,
he had heard of Woomert's reputation and was relying on the famous Hard-Nosed
Computer Detective to deal with... well, whatever it was.
<p>&nbsp;- "Mr. Wibbley - that was an excellent presentation, but I wonder
if you could restate the problem in more basic terms for my assistant here.
I'm afraid he's not up on proper business terminology, and has missed the
more subtle points."
<P>
<br>Their visitor heaved a sigh, and dropped into the nearby easy chair.
<p>&nbsp;- "Oh, sure. You know, they were going to send one of the system
administrators to talk to you, but of course I insisted on doing the presentation
myself as soon as I heard about it. After all, one of <i>them</i> wouldn't
have even thought of using that textured salmon-and-peach background on
the slides, and that's all the rage these days! Anyway, I <i>did</i> get
a note from him that explains it in his own words; it's crude and unsophisticated,
not at all proper marketing technique, but I suppose you fellows will understand
it..."
<p>The crumpled and coffee-stained napkin, most of which was covered with
calculations, reminders, and something that looked like firewall rules,
contained a short note framed with a red marker pen:
<br>&nbsp;
<center><table BORDER=4 COLS=1 WIDTH="100%" NOSAVE >
<tr>
<td>Woomert, spambots are harvesting the e-mail addresses on our website
(we've tagged them with the "plus hack", <a href="#2">[2]</a> so we know
where it's coming from); the amount of spam we're getting is growing by
leaps and bounds. We need to have the addresses out there - it's our contact
info, site problem reports, etc. - but we've got to stop the 'bots somehow!
I've already written the CGI to handle the hot links, but we need to have
the actual addresses displayed on the pages, and the 'bots are getting
those. Any ideas? The page is at <font color="#3333FF">http://xxxxxxxxxxxx.xxx</font>.
I've created an account for you; just go to <font color="#3333FF">ssh://xxxxxxxxx.xxx/xxx</font>,
password 'xxxxxxxxxx'. Thanks!&nbsp;
<br>&nbsp;- Int Main</td>
</tr>
</table></center>
<p>After Woomert had ushered out their visitor (and reassured him that,
indeed, the salmon-and-peach background was delightful), he returned to
the living room where Frink awaited him.
<p>&nbsp;- "What are you going to do, Woomert? Any plans?"
<p>&nbsp;- "Yes; let's take a peek at their website, then get out there and
look around. It's a mistake to make decisions ahead of your facts, and we
have few facts at hand."
<p>...
<p>Once again, Woomert and Frink found themselves surrounded by the familiar
sights and sounds of a working web site. They could see the Web server
easily spawning off threads without significantly affecting CPU load; clearly,
the local sysadmin had installed mod_perl <a href="#3">[3]</a>. Here and
there, data streams whisked by, and everything moved like a smoothly-oiled
machine.
<p>A sudden shadow made Frink look up. "What the..." Before he could go
any further, a horrifying creature, all tentacles, lenses, and evil intent
<a href="#4">[4]</a>
leaped upon the scene, sucked up a copy of every HTML file at once, and
was gone in a blink.
<p>&nbsp;- "What <i>was</i> that, Woomert - a spambot?"
<p>&nbsp;- "Yep. These things traverse the Net, collecting e-mail addresses
and reporting them to their scummy spammer masters. Given the nature of
the Net, you can't stop them - but you <i>can</i> make them much less effective.
Spammers are stupid, their bots even more so, and that's what we're going
to rely on. Mind you, whatever we do is only going to be a temporary solution;
eventually, spammers (or at least their hired techie help) will catch on
to this particular method - but by then, we'll implement other solutions."
<p>Walking up to a convenient terminal, Woomert slipped on his favorite
typing gloves and fired off a rapid volley.
<pre>
<hr WIDTH="100%">perl -MRFC::RFC822::Address=valid -wne'/[\w-]+@[\w.-]+/||next;print valid$&amp;' *html
<hr WIDTH="100%"></pre>
A line of '1's appeared on the screen; Woomert smiled and his fingers again
flew over the keyboard.
<pre>
<hr WIDTH="100%">perl -i -wlpe's=[\w-]+@[\w.-]+=join"",map{sprintf"&amp;#%s;",ord}split//,$&amp;=e' *html
<hr WIDTH="100%"></pre>
This time, there was no output; however, Woomert looked satisfied. He quickly
shot off an email to the local sysadmin that contained some instructions
and included a shorter version of the last one-liner -
<pre>
<hr WIDTH="100%">perl -we'map{printf"&amp;#%s;",ord}split//,pop' user@host.com
<hr WIDTH="100%"></pre>
- "All right-o, Frink; our work here is done. Home, here we come!"
<p>...
<p>The old-fashioned coal-fired samovar <a href="#6">[6]</a> was gently
perking; the <i>zavarka</i> (tea concentrate), made with excellent Georgian
tea, gave off a marvelous smell. A plate of canap&#233;s, ranging from the
best Russian butter and wild blackberry jam on freshly-baked fluffy white
bread to beluga caviar on a heavy, dark rye rubbed with just a touch of
garlic, was set close at hand, and both Woomert and Frink were merrily
foraging in the gourmet field thus presented. Eventually they settled back,
replete with good food, and Frink's curiosity could be contained no longer.
<p>&nbsp;- "Woomert, when I try to puzzle out your one-liners, I can only
get so far; then I run out of steam. Can you tell me about what you did?"
<p>Lying back in his favorite armchair, Woomert smiled.
<p>&nbsp;- "Instead, why don't you start by telling me what part you understood?
I like to see how far you've advanced, Frink; it's been a pleasure to me
to see you picking up some of the finer points. I'll take it from there."
<p>&nbsp;- "All right, then... Let's start with the first one:
<pre>
<hr WIDTH="100%">perl -MRFC::RFC822::Address=valid -wne'/[\w-]+@[\w.-]+/||next;print valid$&amp;' *html
<hr WIDTH="100%"></pre>
I recognized all the command-line switches:
<p><tt>-Mmodule Use the specified module</tt>
<br><tt>-w Enable warnings</tt>
<br><tt>-n Non-printing loop</tt>
<br><tt>-e Execute the following commands</tt>
<p>However, I couldn't quite puzzle out the '<tt>-MRFC::RFC822::Address=valid'</tt>syntax
- what was that?"
<p>&nbsp;- "Ah. As 'perldoc perlvar' tells us, in the entry for '-M', it's
a bit of syntactic sugar; '<tt>-MBar=foo</tt>' is a shortcut for '<tt>use
Bar qw/foo/</tt>', which imports the specified function 'foo' from module
'Bar'. Go on, you're doing well."
<p>Frink cleared his throat.
<p>&nbsp;- "In that case, I think I have it figured out... almost. Let
me take a quick look at 'perldoc perlvar' and 'perldoc RFC::RFC822::Address'...
Yes, that's what I thought - I've got it! The regex at the beginning -
<p><tt>/[\w-]+@[\w.-]+/</tt>
<p>tries to match e-mail addresses - it's not perfect, but should do reasonably
well. What it says is "match any character in <tt>[a-zA-Z0-9-]</tt> repeated
one or more times, followed by '<tt>@</tt>', followed by any character
in <tt>[a-zA-Z0-9.-]</tt> repeated one or more times". If the match does
not succeed - the '<tt>||</tt>' logical-or operator handles that - go to
the next line."
<p>&nbsp;- "Brilliant, Frink! What happens then?"
<p>&nbsp;- "If it does succeed, '<tt>next</tt>' is skipped over, and '<tt>print
valid$&amp;</tt>' is invoked. The module documentation tells me that the
'<tt>valid</tt>' function tests an e-mail address for RFC822 (e-mail specification)
conformance, and returns true or false based on validity. '<tt>$&amp;</tt>',
according to 'perldoc perlvar', is the last successful pattern match -
in other words, whatever was matched by the regex. Since you saw all '1's
and no errors - any matches that weren't RFC822-valid would have returned
something like "<tt>Use of uninitialized value in print at -e line 1</tt>"
- what you matched was all valid. What you were doing here is checking
to see that your regex only matched actual addresses. How did I do?"
<p>&nbsp;- "Excellent, my dear Frink; you're coming along well! As a side
note, it's generally best to avoid the use of&nbsp; <tt>$&amp;, $`,</tt>
and <tt>$'</tt> as well as '<tt>use English'</tt> in scripts; there's a
rather large performance penalty associated with them (see 'perldoc perlvar').
However, here we had a very small list of matches, and so I went ahead
with it. Go on, see what you can make of the next one."
<p>&nbsp;- "Um... the next one, right. Well, I've got part of it -
<pre>
<hr WIDTH="100%">perl -i -wpe's=[\w-]+@[\w.-]+=join"",map{sprintf"&amp;#%s;",ord}split//,$&amp;=e' *html
<hr WIDTH="100%"></pre>
<tt>-i In-place edit (modify the specified file[s])</tt>
<br><tt>-w Enable warnings</tt>
<br><tt>-p Printing loop</tt>
<br><tt>-e Execute the following commands</tt>
<p>Mmmm... I got sorta lost here, Woomert. I see that regex that you'd
used before, but what's that 's=' bit?"
<p>&nbsp;- "It's one of those convenient tweaks that Perl provides - although,
admittedly, the basic idea was stolen from 'sed'. It's simply an alternate
delimiter used with the 's' (substitute) operator; there are times when
using the default delimiter ("/") is highly inconvenient and leads to "toothpick
Hell" - as, for example, in matching a directory name:
<p><tt>s/\/path\/to\/my\/directory/my home directory/</tt>
<p>Far better to use an alternate delimiter, one that is not contained
in the text of either the pattern or the replacement:
<p><tt>s#/path/to/my/directory#my home directory#</tt>
<p>As long as it's non-alphanumeric and non-whitespace, it'll work fine.
There are some special cases, but they're all sensible ones; using a single
quote disables interpolation in both the pattern and the replacement (see
the rules in 'perldoc perlop'), and using braces or brackets as delimiters
requires rather obvious syntax:
<p><tt>s{a}{b}</tt>
<br><tt>s(a)(b)</tt>
<br><tt>s[a][b]</tt>
<p>Many people like '#' as a delimiter; I prefer '=', since '#' tends to
come up in HTML and comments. Can you make sense of any of the rest?"
<p>- "I'm afraid not. You're matching the email addresses as previously,
and replacing them with something, but I can't figure out what."
<p>- "All right; it <b>is</b> rather involved. The replacement part of
the substitution is actual Perl code; we can do that thanks to the 'e'
(evaluate) modifier on the end of the 's' operator. Let's parse the relevant
code from right to left:
<pre>join"",map{sprintf"&amp;#%s;",ord}split//,$&amp;</pre>
We know that '<tt>$&amp;</tt>' contains an email address; the next thing
we do is use the '<tt>split'</tt> function which converts a scalar to a
list, splitting it on whatever is specified between the delimiters. In
this case, however, the delimiter is empty, a null - so the returned list
has each character of the address as a separate element in the list. We
now pass this list to the '<tt>map</tt>' function, which will evaluate
the code specified in the <tt>{block}</tt> for each element of the supplied
list and return the result - as another list.
<p>Within the block itself, each character is used as an argument to the
'<tt>ord</tt>' function, which returns the ASCII value of that character;
this, in turn, is used as the argument for the '<tt>sprintf</tt>' function
which returns the following formatted string:
<p><tt>&amp;#&lt;ASCII_value&gt;;</tt>
<p>for each value so specified. After all the characters in the list have
been processed, we use the '<tt>join</tt>' function to convert the list
back to a scalar - which the substitute operator will now use as a replacement
string for the original email address. What used to be "<tt>foo@bar.com</tt>"
now looks like
<p><tt>&amp;#102;&amp;#111;&amp;#111;&amp;#64;&amp;#98;&amp;#97;&amp;#114;&amp;#46;&amp;#99;&amp;#111;&amp;#109;</tt>
<p>This, you must admit, looks nothing like an e-mail address - so spambots
will not be able to read it!"
<p>Frink looked troubled.
<p>&nbsp;- "Woomert, I hate to tell you... but human beings won't be able
to read it either!"
<p>Woomert took another sip of his tea and smiled.
<p>&nbsp;- "You're forgetting one thing, Frink. Humans aren't <i>going</i>
to be reading this; since it's part of the HTML files, it's going to be
read by
<i>browsers</i>. As it happens, the HTML specification for showing
ASCII characters by their value is
<p><tt>&amp;#&lt;ASCII_value&gt;;</tt>
<p>which is exactly what we've produced. Try this yourself: save the text
between the following lines as "text.html" and view it in a browser.
<pre>
<hr WIDTH="100%">&lt;html&gt;&lt;head&gt;&lt;title&gt;&lt;/title&gt;&lt;/head&gt;&lt;body&gt;
&amp;#87;&amp;#111;&amp;#111;&amp;#109;&amp;#101;&amp;#114;&amp;#116;&amp;#32;&amp;#70;&amp;#111;&amp;#111;&amp;#110;&amp;#108;&amp;#121;
&lt;/body&gt;&lt;/html&gt;
<hr WIDTH="100%"></pre>
Do you see what I mean?"
<p>A few moments later, Frink looked up from the keyboard.
<p>&nbsp;- "Woomert, what a great solution! Your client will be able to
display the addresses without them being harvested, and the Web page will
still look the same as it did before. I can tell by comparison that the
last bit of code:
<pre>
<hr WIDTH="100%">perl -we'map{printf"&amp;#%s;",ord}split//,pop' user@host.com
<hr WIDTH="100%"></pre>
simply enables the sysadmin to convert any new addresses before popping
them into the HTML. Wonderful!"
<p>&nbsp;- "A large part of the complete solution, of course, was the CGI
that the local admin had written - that takes a bit more than a one-liner,
although not very much more, given the power of the CGI module. Remember,
Frink: as your powers grow, make certain to align yourself with the side
of Good rather than Evil. Not only is it the right thing to do; the people
around you are far more likely to have brains!"
<br>&nbsp;
<br>&nbsp;
<p>
<hr WIDTH="100%">
<br><a NAME="1"></a>[1] Oddly enough, my mysterious correspondent did not
include the solution to this, perhaps deeming it simple enough (!) for
the public to figure out - or (and I suspect this to be the more likely
scenario) he has not yet figured it out himself. Readers are welcome to
write in with their ideas... but for now, the workings of Woomert's game
remain a puzzle.
<p><a NAME="2"></a>[2] A number of commonly-used Mail Transfer Agents will
ignore anything that follows a plus sign in the username part of the address,
e.g. &lt;smith+yahoo@joe.com&gt; will be routed exactly the same as &lt;smith@joe.com&gt;.
This can be a very useful mechanism for tracing and reducing spam: a "plus-hacked"
address that becomes too spam-loaded can be directed to "<tt>/dev/null</tt>"
and replaced by a newly generated one (say, &lt;smith+yahoo1@joe.com&gt; -
which would also go to &lt;smith@joe.com&gt;.)
<p><a NAME="3"></a>[3] A.K.A. "Apache On Steroids". From the mod_perl documentation:
<p><tt>The Apache/Perl integration project brings together the full power
of</tt>
<br><tt>the Perl programming language and the Apache HTTP server. This
is</tt>
<br><tt>achieved by linking the Perl runtime library into the server and</tt>
<br><tt>providing an object oriented Perl interface to the server's C language</tt>
<br><tt>API.</tt>
<p><tt>These pieces are seamlessly glued together by the `mod_perl' server</tt>
<br><tt>plugin, making it is possible to write Apache modules entirely
in</tt>
<br><tt>Perl. In addition, the persistent interpreter embedded in the server</tt>
<br><tt>avoids the overhead of starting an external interpreter program
and</tt>
<br><tt>the additional Perl start-up (compile) time.</tt>
<p>There are many major benefits to using mod_perl; if you use Apache in
any serious fashion without it, you're almost certainly throwing away some
of your time and effort.
<p><a NAME="4"></a>[4] If you've seen "The Matrix", just picture the Sentinels.
If you haven't seen it, hey, you've got only yourself to blame. :)
<p><a NAME="5"></a>[5] Gibberish is the written form of the Marketroid
language. It was formerly spoken by the Gibbers, who all died out as a
result of their complete inability to do anything (as opposed to talking
about it.) It is exactly as comprehensible as its spoken counterpart, although
many people confuse the two: "it's all marketroid gibberish!" is a highly
redundant statement.
<p><a NAME="6"></a>[6] See the "Russian Tea HOWTO", by D&aacute;niel Nagy,
for the proper way to make and serve Russian tea. The man <b><i>knows</i></b>
what he's talking about.
<!-- *** BEGIN author bio *** -->
<P>&nbsp;
<P>
<P> Ben is a Contributing Editor for Linux Gazette and a member of
The Answer Gang.
<!-- *** BEGIN bio *** -->
<P>
<IMG ALT="picture" SRC="../../gx/2002/tagbio/ben-okopnik.jpg" WIDTH="199"
HEIGHT="200" ALIGN="left" HSPACE="10" VSPACE="10">
<em>
Ben was born in Moscow, Russia in 1962. He became interested in
electricity at age six--promptly demonstrating it by sticking a fork into
a socket and starting a fire--and has been falling down technological mineshafts
ever since. He has been working with computers since the Elder Days, when
they had to be built by soldering parts onto printed circuit boards and
programs had to fit into 4k of memory. He would gladly pay good money to any
psychologist who can cure him of the resulting nightmares.
<p>Ben's subsequent experiences include creating software in nearly a dozen
languages, network and database maintenance during the approach of a hurricane,
and writing articles for publications ranging from sailing magazines to
technological journals. Having recently completed a seven-year
Atlantic/Caribbean cruise under sail, he is currently docked in Baltimore, MD,
where he works as a technical instructor for Sun Microsystems.
<p>Ben has been working with Linux since 1997, and credits it with his complete
loss of interest in waging nuclear warfare on parts of the Pacific Northwest.
</em>
<br CLEAR="all">
<!-- *** END bio *** -->
<!-- *** END author bio *** -->
<!-- *** BEGIN copyright *** -->
<hr>
<CENTER><SMALL><STRONG>
Copyright &copy; 2003, Ben Okopnik.
Copying license <A HREF="../copying.html">http://www.linuxgazette.com/copying.html</A><BR>
Published in Issue 86 of <i>Linux Gazette</i>, January 2003
</STRONG></SMALL></CENTER>
<!-- *** END copyright *** -->
<HR>
<!--startcut ==========================================================-->
<CENTER>
<!-- *** BEGIN navbar *** -->
<IMG ALT="" SRC="../gx/navbar/left.jpg" WIDTH="14" HEIGHT="45" BORDER="0" ALIGN="bottom"><A HREF="lechnyr.html"><IMG ALT="[ Prev ]" SRC="../gx/navbar/prev.jpg" WIDTH="16" HEIGHT="45" BORDER="0" ALIGN="bottom"></A><A HREF="index.html"><IMG ALT="[ Table of Contents ]" SRC="../gx/navbar/toc.jpg" WIDTH="220" HEIGHT="45" BORDER="0" ALIGN="bottom" ></A><A HREF="../index.html"><IMG ALT="[ Front Page ]" SRC="../gx/navbar/frontpage.jpg" WIDTH="137" HEIGHT="45" BORDER="0" ALIGN="bottom"></A><A HREF="http://www.linuxgazette.com/cgi-bin/talkback/all.py?site=LG&article=http://www.linuxgazette.com/issue86/okopnik.html"><IMG ALT="[ Talkback ]" SRC="../gx/navbar/talkback.jpg" WIDTH="121" HEIGHT="45" BORDER="0" ALIGN="bottom" ></A><A HREF="../lg_faq.html"><IMG ALT="[ FAQ ]" SRC="./../gx/navbar/faq.jpg"WIDTH="62" HEIGHT="45" BORDER="0" ALIGN="bottom"></A><A HREF="qubism.html"><IMG ALT="[ Next ]" SRC="../gx/navbar/next.jpg" WIDTH="15" HEIGHT="45" BORDER="0" ALIGN="bottom" ></A><IMG ALT="" SRC="../gx/navbar/right.jpg" WIDTH="15" HEIGHT="45" ALIGN="bottom">
<!-- *** END navbar *** -->
</CENTER>
</BODY></HTML>
<!--endcut ============================================================-->