# # $Id: Dump.pm,v 1.3.2.17 2006/11/15 19:32:29 gomor Exp $ # package Net::Packet::Dump; use strict; use warnings; use Carp; require Class::Gomor::Array; our @ISA = qw(Class::Gomor::Array); use Net::Packet::Env qw($Env); require Net::Packet::Frame; use Net::Packet::Utils qw(getRandom32bitsInt); use Net::Packet::Consts qw(:dump :layer); use Net::Pcap; use Time::HiRes qw(gettimeofday); use Storable qw(lock_store lock_retrieve); our @AS = qw( dev env file filter overwrite timeoutOnNext timeout promisc link nextFrame isRunning unlinkOnClean noStore noLayerWipe mode keepTimestamp _pid _pcapd _dumper _stats _firstTime _sName _sDataAwaiting ); our @AA = qw( frames ); our @AO = qw( framesSorted ); __PACKAGE__->cgBuildIndices; __PACKAGE__->cgBuildAccessorsScalar(\@AS); __PACKAGE__->cgBuildAccessorsArray(\@AA); no strict 'vars'; BEGIN { my $osname = { cygwin => \&_killTcpdumpWin32, MSWin32 => \&_killTcpdumpWin32, }; *_killTcpdump = $osname->{$^O} || \&_killTcpdumpOther; } sub new { my $self = shift->SUPER::new( dev => $Env->dev, env => $Env, file => "netpacket-tmp-$$.@{[getRandom32bitsInt()]}.pcap", filter => '', overwrite => 0, timeout => 0, promisc => 0, timeoutOnNext => 3, isRunning => 0, unlinkOnClean => 1, noStore => 0, noLayerWipe => 0, framesSorted => {}, frames => [], mode => NP_DUMP_MODE_ONLINE, keepTimestamp => 0, _sDataAwaiting => 0, _sName => "netpacket-tmp-$$.@{[getRandom32bitsInt()]}.storable", @_, ); unless ($self->[$__file]) { confess("You MUST set `file' attribute\n"); } $Env->dump($self) unless $Env->noDumpAutoSet; $self; } sub isModeOnline { shift->[$__mode] eq NP_DUMP_MODE_ONLINE } sub isModeOffline { shift->[$__mode] eq NP_DUMP_MODE_OFFLINE } sub isModeWriter { shift->[$__mode] eq NP_DUMP_MODE_WRITER } sub start { my $self = shift; $self->cgDebugPrint(1, 'will run in mode: '.$self->mode); $self->[$__isRunning] = 1; if ($self->isModeOnline) { if (-f $self->[$__file] && ! $self->[$__overwrite]) { croak("We will not overwrite a file by default. Use `overwrite' ". "attribute to do it\n"); } $self->_sStore(0); $self->_waitFileSize($self->[$___sName]); $self->_startTcpdump; $self->_openFileOffline; } elsif ($self->isModeOffline) { if (! -f $self->[$__file]) { croak("File does not exists: ".$self->[$__file]."\n"); } $self->_openFileOffline; $self->_setFilter; } elsif ($self->isModeWriter) { if (-f $self->[$__file] && ! $self->[$__overwrite]) { croak("We will not overwrite a file by default. Use `overwrite' ". "attribute to do it\n"); } $self->_openFileWriter; } 1; } sub stop { my $self = shift; return unless $self->[$__isRunning]; return if $self->isSon; if ($self->isModeOnline) { $self->_killTcpdump; $self->[$___pid] = undef; if ($self->[$___sName] && -f $self->[$___sName]) { unlink($self->[$___sName]); } } elsif ($self->isModeWriter) { Net::Pcap::dump_close($self->[$___dumper]); } elsif ($self->isModeOffline) { # Nothing to do here } Net::Pcap::close($self->[$___pcapd]); $self->[$__isRunning] = 0; 1; } sub isFather { shift->[$___pid] ? 1 : 0 } sub isSon { shift->[$___pid] ? 0 : 1 } sub _sStore { lock_store(\$_[1], $_[0]->[$___sName]) or carp("@{[(caller(0))[3]]}: lock_store: @{[$_[0]->[$___sName]]}: $!\n"); } sub _sRetrieve { ${lock_retrieve(shift->[$___sName])} } sub _sonPrintStats { my $self = shift; my $stats = $self->getStats; Net::Pcap::breakloop($self->[$___pcapd]); Net::Pcap::close($self->[$___pcapd]); $self->cgDebugPrint(1, 'Frames received : '.$stats->{ps_recv}); $self->cgDebugPrint(1, 'Frames dropped : '.$stats->{ps_drop}); $self->cgDebugPrint(1, 'Frames if dropped: '.$stats->{ps_ifdrop}); exit(0); } sub _waitFile { my $self = shift; my ($file) = @_; my $startTime = gettimeofday(); my $thisTime = $startTime; while (! -f $file) { if ($thisTime - $startTime > 10) { croak("@{[(caller(0))[3]]}: too long for file creation: $file\n") } $thisTime = gettimeofday(); } } sub _waitFileSize { my $self = shift; my ($file) = @_; $self->_waitFile($file); my $startTime = gettimeofday(); my $thisTime = $startTime; while (! ((stat($file))[7] > 0)) { if ($thisTime - $startTime > 10) { $self->clean; croak("@{[(caller(0))[3]]}: too long for file creation2: $file\n") } $thisTime = gettimeofday(); } } sub _startTcpdump { my $self = shift; my $err; my $pd = Net::Pcap::open_live( $self->[$__dev], 1514, $self->[$__promisc], 1000, \$err, ); unless ($pd) { croak("@{[(caller(0))[3]]}: open_live: $err\n"); } my $net = 0; my $mask = 0; Net::Pcap::lookupnet($self->[$__dev], \$net, \$mask, \$err); if ($err) { carp("@{[(caller(0))[3]]}: lookupnet: $err\n"); } my $fcode; if (Net::Pcap::compile($pd, \$fcode, $self->[$__filter], 0, $mask) < 0) { croak("@{[(caller(0))[3]]}: compile: ". Net::Pcap::geterr($pd). "\n"); } if (Net::Pcap::setfilter($pd, $fcode) < 0) { croak("@{[(caller(0))[3]]}: setfilter: ". Net::Pcap::geterr($pd). "\n"); } my $p = Net::Pcap::dump_open($pd, $self->[$__file]); unless ($p) { croak("@{[(caller(0))[3]]}: dump_open: ". Net::Pcap::geterr($pd). "\n"); } Net::Pcap::dump_flush($p); $SIG{CHLD} = 'IGNORE'; my $pid = fork(); croak("@{[(caller(0))[3]]}: fork: $!\n") unless defined $pid; if ($pid) { $self->[$___pid] = $pid; return 1; } else { $self->[$___pcapd] = $pd; $SIG{INT} = sub { $self->_sonPrintStats }; $SIG{TERM} = sub { $self->_sonPrintStats }; $self->cgDebugPrint(1, "dev: [@{[$self->[$__dev]]}]\n". "file: [@{[$self->[$__file]]}]\n". "filter: [@{[$self->[$__filter]]}]"); Net::Pcap::loop($pd, -1, \&_tcpdumpCallback, [ $p, $self ]); Net::Pcap::close($pd); exit(0); } } sub _tcpdumpCallback { my ($data, $hdr, $pkt) = @_; my $p = $data->[0]; my $self = $data->[1]; Net::Pcap::dump($p, $hdr, $pkt); Net::Pcap::dump_flush($p); my $n = $self->_sRetrieve; $self->_sStore(++$n); } sub _killTcpdumpWin32 { my $self = shift; return unless $self->[$___pid]; kill('KILL', $self->[$___pid]); } sub _killTcpdumpOther { my $self = shift; return unless $self->[$___pid]; kill('TERM', $self->[$___pid]); } sub clean { my $self = shift; if ($self->isModeOnline) { if ($self->[$__unlinkOnClean] && $self->[$__file] && -f $self->[$__file]) { unlink($self->[$__file]); $self->cgDebugPrint(1, "@{[$self->[$__file]]} removed"); } } if ($self->[$___sName] && -f $self->[$___sName]) { unlink($self->[$___sName]); } 1; } sub getStats { my $self = shift; unless ($self->[$___pcapd]) { carp("@{[(caller(0))[3]]}: unable to get stats, no pcap descriptor ". "opened\n"); return undef; } my %stats; Net::Pcap::stats($self->[$___pcapd], \%stats); $self->[$___stats] = \%stats; \%stats; } sub flush { my $self = shift; $self->[$__frames] = []; $self->[$__framesSorted] = {}; } sub _setFilter { my $self = shift; my $str = $self->[$__filter]; return unless $str; my ($net, $mask, $err); Net::Pcap::lookupnet($self->[$__dev], \$net, \$mask, \$err); if ($err) { croak("@{[(caller(0))[3]]}: Net::Pcap::lookupnet: @{[$self->[$__dev]]}: ". "$err\n"); } my $filter; Net::Pcap::compile($self->[$___pcapd], \$filter, $str, 0, $mask); unless ($filter) { croak("@{[(caller(0))[3]]}: Net::Pcap::compile: error\n"); } Net::Pcap::setfilter($self->[$___pcapd], $filter); } sub _openFileOffline { my $self = shift; my $err; $self->[$___pcapd] = Net::Pcap::open_offline($self->[$__file], \$err); unless ($self->[$___pcapd]) { croak("@{[(caller(0))[3]]}: Net::Pcap::open_offline: ". "@{[$self->[$__file]]}: $err\n"); } $self->[$__link] = Net::Pcap::datalink($self->[$___pcapd]); } sub _getPcapHeader { my $self = shift; # 24 bytes header of a DLT_RAW pcap file "\xd4\xc3\xb2\xa1\x02\x00\x04\x00\x00\x00\x00\x00". "\x00\x00\x00\x00\xdc\x05\x00\x00\x0c\x00\x00\x00"; } sub _openFileWriter { my $self = shift; my $file = $self->[$__file]; my $hdr = $self->_getPcapHeader; open(my $fh, '>', $file) or croak("@{[(caller(0))[3]]}: open: $file: $!\n"); syswrite($fh, $hdr, length($hdr)); close($fh); my $err; my $pcapd = Net::Pcap::open_offline($file, \$err); unless ($pcapd) { croak("@{[(caller(0))[3]]}: Net::Pcap::open_offline: ". "$file: $err\n"); } $self->[$___pcapd] = $pcapd; $self->[$___dumper] = Net::Pcap::dump_open($pcapd, $file); unless ($self->[$___dumper]) { croak("@{[(caller(0))[3]]}: Net::Pcap::dump_open: ". Net::Pcap::geterr($pcapd)."\n"); } 1; } sub _addToFramesSorted { my $self = shift; my ($frame) = @_; if (! $self->[$__env]->doFrameReturnList) { $self->framesSorted($frame); push @{$self->[$__frames]}, $frame; } else { for my $f (@$frame) { $self->framesSorted($f); push @{$self->[$__frames]}, $f; } } } sub _getTimestamp { my $self = shift; my ($hdr) = @_; $hdr->{tv_sec}.'.'.sprintf("%06d", $hdr->{tv_usec}); } sub _setTimestamp { my $self = shift; my @time = Time::HiRes::gettimeofday(); $time[0].'.'.sprintf("%06d", $time[1]); } my $mapLinks = { NP_DUMP_LINK_NULL() => NP_LAYER_NULL(), NP_DUMP_LINK_EN10MB() => NP_LAYER_ETH(), NP_DUMP_LINK_RAW() => NP_LAYER_RAW(), NP_DUMP_LINK_SLL() => NP_LAYER_SLL(), NP_DUMP_LINK_PPP() => NP_LAYER_PPP(), }; sub _pcapNext { my $self = shift; my %hdr; if (my $raw = Net::Pcap::next($self->[$___pcapd], \%hdr)) { my $ts = $self->[$__keepTimestamp] ? $self->_getTimestamp(\%hdr) : $self->_setTimestamp; my $frame = Net::Packet::Frame->new( env => $self->env, raw => $raw, timestamp => $ts, encapsulate => $mapLinks->{$self->[$__link]} || NP_LAYER_UNKNOWN, ) or return undef; $self->_addToFramesSorted($frame) unless $self->[$__noStore]; return $frame; } undef; } sub _getNextAwaitingFrameOffline { my $self = shift; $self->_pcapNext; } sub _getNextAwaitingFrameOnline { my $self = shift; my $last = $self->[$___sDataAwaiting]; my $new = $self->_sRetrieve; # Return if nothing new is awaiting return undef if ($new <= $last); $self->[$___sDataAwaiting]++; $self->_pcapNext; } sub _getNextAwaitingFrame { my $self = shift; $self->isModeOnline ? $self->_getNextAwaitingFrameOnline : $self->_getNextAwaitingFrameOffline; } # XXX: need more work #sub _getNextAwaitingFrames { #my $self = shift; #my $last = $self->[$___sDataAwaiting]; #my $new = $self->_sRetrieve; #my $diff = $new - $last; #return [] if $diff <= 0; # Nothing awaiting #$self->[$___sDataAwaiting] += $diff; #my $frames = []; #while ($diff--) { #push @$frames, $self->_pcapNext; #} #$frames; #} sub _nextTimeoutHandle { my $self = shift; # Handle timeout my $thisTime = gettimeofday() if $self->[$__timeoutOnNext]; $self->[$___firstTime] = $thisTime unless $self->[$___firstTime]; if ($self->[$__timeoutOnNext] && $self->[$___firstTime]) { if (($thisTime - $self->[$___firstTime]) > $self->[$__timeoutOnNext]) { $self->[$__timeout] = 1; $self->[$___firstTime] = 0; $self->cgDebugPrint(1, "Timeout occured"); return undef; } } 1; } sub _nextTimeoutReset { shift->[$___firstTime] = 0 } sub next { my $self = shift; unless ($self->[$__isRunning]) { croak("You MUST call start() method before using next() method\n"); } $self->_nextTimeoutHandle or return undef; my $frame = $self->_getNextAwaitingFrame; $self->_nextTimeoutReset if $frame; $frame ? do { $self->[$__nextFrame] = $frame } : undef; } sub nextAll { my $self = shift; while ($self->next) {} } sub write { my $self = shift; my ($frame) = @_; unless ($self->isModeWriter) { croak("Dump is not in writer mode\n"); } # Rebuild the frame without possible layer 2 my $new; $new .= $frame->l3->raw if $frame->l3; $new .= $frame->l4->raw if $frame->l4; $new .= $frame->l7->raw if $frame->l7; # Create pcap header my ($sec, $usec) = split('\.', $frame->timestamp); my $hdr = { len => length($new), caplen => length($new), tv_sec => $sec, tv_usec => $usec, }; Net::Pcap::pcap_dump($self->[$___dumper], $hdr, $new); Net::Pcap::dump_flush($self->[$___dumper]); } # XXX: broken for now #sub nextAll { #my $self = shift; #$self->_nextTimeoutHandle or return []; #my $frames = $self->_getNextAwaitingFrames; #$self->_nextTimeoutReset if @$frames; #$frames; #} sub timeoutReset { shift->[$__timeout] = 0 } sub framesFor { my $self = shift; my ($f) = @_; my $l2Key = ($f->l2 && $f->l2->getKeyReverse($f)) || 'all'; my $l3Key = ($f->l3 && $f->l3->getKeyReverse($f)) || 'all'; my $l4Key = ($f->l4 && $f->l4->getKeyReverse($f)) || 'all'; my $aref = $self->[$__framesSorted]->{$l2Key}{$l3Key}{$l4Key}; $aref ? @$aref : (); } # # Other accessors # sub framesSorted { my $self = shift; my ($f) = @_; if ($f) { # Wipe headers, since if not, framesFor() will not be able to find them. # Because if you create a Frame from L3, no headers are set for L2, but # the Dump will have them and store them into the l2Key. if ($self->env->desc && ! $self->[$__noLayerWipe]) { $f->l2(undef) if ref($self->env->desc) =~ /L3|L4/; $f->l3(undef) if ref($self->env->desc) =~ /L4/; } my $l2Key = ($f->l2 && $f->l2->getKey($f)) || 'all'; my $l3Key = ($f->l3 && $f->l3->getKey($f)) || 'all'; my $l4Key = ($f->l4 && $f->l4->getKey($f)) || 'all'; push @{$self->[$__framesSorted]->{$l2Key}{$l3Key}{$l4Key}}, $f; # We store a second time for ICMP messages if ($f->isIcmp) { my $l3Key = ($f->l3 && $f->l3->is.':'.$f->l3->dst) || 'all'; push @{$self->[$__framesSorted]->{$l2Key}{$l3Key}{$l4Key}}, $f; } } $self->[$__framesSorted]; } 1; __END__ =head1 NAME Net::Packet::Dump - a tcpdump-like object providing frame capturing and more =head1 SYNOPSIS require Net::Packet::Dump; use Net::Packet::Consts qw(:dump); # # Example live capture (sniffer like) # # Instanciate object my $dump = Net::Packet::Dump->new( mode => NP_DUMP_MODE_ONLINE, file => 'live.pcap', filter => 'tcp', promisc => 1, noStore => 1, keepTimestamp => 1, unlinkOnClean => 0, overwrite => 1, ); # Start capture $dump->start; while (1) { if (my $frame = $dump->next) { print $frame->l2->print, "\n" if $frame->l2; print $frame->l3->print, "\n" if $frame->l3; print $frame->l4->print, "\n" if $frame->l4; print $frame->l7->print, "\n" if $frame->l7; } } # Cleanup $dump->stop; $dump->clean; # # Example offline analysis # my $dump2 = Net::Packet::Dump->new( mode => NP_DUMP_MODE_OFFLINE, file => 'existant-file.pcap', unlinkOnClean => 0, ); # Analyze the .pcap file, build an array of Net::Packet::Frame's $dump2->start; $dump2->nextAll; # Browses captured frames for ($dump2->frames) { # Do what you want print $_->l2->print, "\n" if $_->l2; print $_->l3->print, "\n" if $_->l3; print $_->l4->print, "\n" if $_->l4; print $_->l7->print, "\n" if $_->l7; } # Cleanup $dump2->stop; $dump2->clean; # # Example writing mode # my $dump3 = Net::Packet::Dump->new( mode => NP_DUMP_MODE_WRITER, file => 'write.pcap', overwrite => 1, ); $dump3->start; # Build or capture some frames here my $frame = Net::Packet::Frame->new; # Write them $dump3->write($frame); # Cleanup $dump3->stop; $dump3->clean; =head1 DESCRIPTION This module is the capturing part of Net::Packet framework. It is basically a tcpdump process. When a capture starts, the tcpdump process is forked, and saves all traffic to a .pcap file. The parent process can call B or B to convert captured frames from .pcap file to Bs. Then, you can call B method on your sent frames to see if a corresponding reply is waiting in the B array attribute of B. By default, if you use this module to analyze frames you've sent (very likely ;)), and you've sent those frames at layer 4 (using B) (for example), lower layers will be wiped on storing in B array. This behaviour can be disabled by using B attribute. Since B 3.00, it is also possible to create complete .pcap files, thanks to the writer mode (see B). =head1 ATTRIBUTES =over 4 =item B By default, this attribute is set to B found in default B<$Env> object. You can overwrite it by specifying another one in B constructor. =item B Stores a B object. It is used in B method, for example. The default is to use the global B<$Env> object created when using B. =item B Where to save captured frames. By default, a random name file is chosen, named like `netpacket-tmp-$$.@{[getRandom32bitsInt()]}.pcap'. =item B A pcap filter to restrain what to capture. It also works in offline mode, to analyze only what you want, and not all traffic. Default to capture all traffic. WARNING: every time a packet passes this filter, and the B method is called, the internal counter used by b is reset. So the B attribute can only be used if you know exactly that the filter will only catch what you want and not perturbating traffic. =item B If the B exists, setting this to 1 will overwrite it. Default to not overwrite it. =item B Each time B method is called, an internal counter is incremented if no frame has been captured. When a frame is captured (that is, a frame passed the pcap filter), the B attribute is reset to 0. When the counter reaches the value of B, the B attribute is set to 1, meaning no frames have been captured during the specified amount of time. Default to 3 seconds. =item B Is auto set to 1 when a timeout has occured. It is not reset to 0 automatically, you need to do it yourself. =item B If you want to capture in promiscuous mode, set it to 1. Default to 0. =item B This attribute tells which datalink type is used for .pcap files. =item B This one stores a pointer to the latest received frame after a call to B method. If a B call is done, and no frame is received, this attribute is set to undef. =item B When the capturing process is running (B has been called), this is set to 1. So, when B method has been called, it is set to 1, and when B method is called, set to 0. =item B When the B method is called, and this attribute is set to 1, the B is deleted from disk. Set it to 0 to avoid this behaviour. BEWARE: default to 1. =item B If you set this attribute to 1, frames will not be stored in B array. It is used in sniffer-like programs, in order to avoid memory exhaustion by keeping all captured B into memory. Default is to store frames. =item B As explained in DESCRIPTION, if you send packets at layer 4, layer 2 and 3 are not keeped when stored in B. The same is true when sending at layer 3 (layer 2 is not kept). Default to wipe those layers. WARNING: if you set it to 1, and you need the B method from B, it will fail. In fact, this is a speed improvements, that is in order to find matching frame for your request, they are stored in a hash, using layer as keys (B and B are used to get keys from each layer. So, if you do not wipe layers, a key will be used to store the frame, but another will be used to search for it, and no match will be found. This is a current limitation I'm working on to remove. =item B When you crate a B, you have 3 possible modes : online, offline and writer. You need to load constants from B to have access to that (see B). The three constants are: NP_DUMP_MODE_ONLINE NP_DUMP_MODE_OFFLINE NP_DUMP_MODE_WRITER Default behaviour is to use online mode. =item B Sometimes, when frames are captured and saved to a .pcap file, timestamps sucks. That is, you send a frame, and receive the reply, but your request appear to have been sent after the reply. So, to correct that, you can use B framework own timestamping system. The default is 0. Set it manually to 1 if you need original .pcap frames timestamps. =item B [is an arrayref] Stores all analyzed frames found in a pcap file in this arrayref. =item B [is an hashref] Stores all analyzed frames found in a pcap file in this hashref, using keys to store and search related frames request/replies. =back =head1 METHODS =over 4 =item B Object contructor. Default values for attributes: dev: $Env->dev env: $Env file: "netpacket-tmp-$$.@{[getRandom32bitsInt()]}.pcap" filter: '' overwrite: 0 timeout: 0 promisc: 0 timeoutOnNext: 3 isRunning: 0 unlinkOnClean: 1 noStore: 0 noLayerWipe: 0 mode: NP_DUMP_MODE_ONLINE keepTimestamp: 0 =item B =item B =item B Returns 1 if B object is respectively set to online, offline or writer mode. 0 otherwise. =item B You MUST manually call this method to start frame capture, whatever mode you are in. In online mode, it will fork a tcpdump-like process to save captured frames to a .pcap file. It will not overwrite an existing file by default, use B attribute for that. In offline mode, it will only provide analyzing methods. In writer mode, it will only provide writing methods for frames. It will set B attribute to 1 when called. =item B You MUST manually call this method to stop the process. In online mode, it will not remove the generated .pcap file, you MUST call B method. In offline mode, it will to nothing. In writer mode, it will call B method. Then, it will set B attribute to 0. =item B =item B These methods will tell you if your current process is respectively the father, or son process of B object. =item B You MUST call this method manually. It will never be called by B framework. This method will remove the generated .pcap file in online mode if the B attribute is set to 1. In other modes, it will do nothing. =item B Tries to get packet statistics on an open descriptor. It returns a reference to a hash that has to following fields: B, B, B. =item B Will removed all analyzed frames from B array and B hash. Use it with caution, because B from B relies on those. =item B Returns the next captured frame; undef if none found in .pcap file. In all cases, B attribute is set (either to the captured frame or undef). Each time this method is run, a comparison is done to see if no frame has been captured during B amount of seconds. If so, B attribute is set to 1 to reflect the pending timeout. When a frame is received, it is stored in B arrayref, and in B hashref, used to quickly B it (see B), and internal counter for time elapsed since last received packet is reset. =item B Calls B method until it returns undef (meaning no new frame waiting to be analyzed from pcap file). =item B (scalar) In writer mode, this method takes a B as a parameter, and writes it to the .pcap file. Works only in writer mode. =item B Used to reset manually the B attribute. This is a helper method. =item B (scalar) You pass a B has parameter, and it returns an array of all frames relating to the connection. For example, when you send a TCP SYN packet, this method will return TCP packets relating to the used source/destination IP, source/destination port, and also related ICMP packets. =item B (scalar) Method mostly used internally to store in a hashref a captured frame. This is used to retrieve it quickly on B call. =back =head1 CONSTANTS =over 4 =item B =item B =item B =item B Constants for first layers within the pcap file. =item B =item B =item B Constants to set the dump mode. =back =head1 AUTHOR Patrice EGomoRE Auffret =head1 COPYRIGHT AND LICENSE Copyright (c) 2004-2006, Patrice EGomoRE Auffret You may distribute this module under the terms of the Artistic license. See LICENSE.Artistic file in the source distribution archive. =head1 RELATED MODULES L, L, L =cut