diff options
Diffstat (limited to 'trunk/Echolot')
-rw-r--r-- | trunk/Echolot/Chain.pm | 266 | ||||
-rw-r--r-- | trunk/Echolot/Commands.pm | 131 | ||||
-rw-r--r-- | trunk/Echolot/Conf.pm | 531 | ||||
-rw-r--r-- | trunk/Echolot/Config.pm | 344 | ||||
-rw-r--r-- | trunk/Echolot/Fromlines.pm | 126 | ||||
-rw-r--r-- | trunk/Echolot/Globals.pm | 60 | ||||
-rw-r--r-- | trunk/Echolot/Log.pm | 163 | ||||
-rw-r--r-- | trunk/Echolot/Mailin.pm | 252 | ||||
-rw-r--r-- | trunk/Echolot/Pinger.pm | 211 | ||||
-rw-r--r-- | trunk/Echolot/Pinger/CPunk.pm | 205 | ||||
-rw-r--r-- | trunk/Echolot/Pinger/Mix.pm | 139 | ||||
-rw-r--r-- | trunk/Echolot/Report.pm | 70 | ||||
-rw-r--r-- | trunk/Echolot/Scheduler.pm | 196 | ||||
-rw-r--r-- | trunk/Echolot/Stats.pm | 983 | ||||
-rw-r--r-- | trunk/Echolot/Storage/File.pm | 1880 | ||||
-rw-r--r-- | trunk/Echolot/Thesaurus.pm | 144 | ||||
-rw-r--r-- | trunk/Echolot/Tools.pm | 476 |
17 files changed, 6177 insertions, 0 deletions
diff --git a/trunk/Echolot/Chain.pm b/trunk/Echolot/Chain.pm new file mode 100644 index 0000000..463adeb --- /dev/null +++ b/trunk/Echolot/Chain.pm @@ -0,0 +1,266 @@ +package Echolot::Chain; + +# +# $Id$ +# +# This file is part of Echolot - a Pinger for anonymous remailers. +# +# Copyright (c) 2002, 2003, 2004 Peter Palfrader <peter@palfrader.org> +# +# This program is free software. you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +=pod + +=head1 Name + +Echolot::Chain - actual sending and receiving of Chain-Pings. + +=head1 DESCRIPTION + +This package provides functions for sending out and receiving chain-pings. + +=cut + +use strict; +use English; +use Echolot::Log; +use Echolot::Pinger::Mix; +use Echolot::Pinger::CPunk; + +my %INTENSIVE_CARE; + +sub do_mix_chainping($$$$$$$$) { + my ($addr1, $type1, $keyid1, $addr2, $type2, $keyid2, $to, $body) = @_; + + ($type1 eq 'mix' && $type2 eq 'mix') or + Echolot::Log::warn("both types should really be mix ($type1, $type2)."), + return 0; + + my %key1 = Echolot::Globals::get()->{'storage'}->get_key($addr1, $type1, $keyid1); + my %key2 = Echolot::Globals::get()->{'storage'}->get_key($addr2, $type2, $keyid2); + Echolot::Pinger::Mix::ping( + $body, + $to, + 0, + [ $key1{'nick'} , $key2{'nick'} ], + { $keyid1 => \%key1, $keyid2 => \%key2 } ) or + return 0; + + return 1; +}; + +sub do_cpunk_chainping($$$$$$$$) { + my ($addr1, $type1, $keyid1, $addr2, $type2, $keyid2, $to, $body) = @_; + + my $keyhash = {}; + if ($type1 ne 'cpunk-clear') { + my %key = Echolot::Globals::get()->{'storage'}->get_key($addr1, $type1, $keyid1); + $keyhash->{$keyid1} = \%key; + }; + if ($type2 ne 'cpunk-clear') { + my %key = Echolot::Globals::get()->{'storage'}->get_key($addr2, $type2, $keyid2); + $keyhash->{$keyid2} = \%key; + }; + Echolot::Pinger::CPunk::ping( + $body, + $to, + 0, + [ { address => $addr1, + keyid => $keyid1, + encrypt => ($type1 ne 'cpunk-clear'), + pgp2compat => ($type1 eq 'cpunk-rsa') }, + { address => $addr2, + keyid => $keyid2, + encrypt => ($type2 ne 'cpunk-clear'), + pgp2compat => ($type2 eq 'cpunk-rsa') } ], + $keyhash ) or + return 0; + + return 1; +}; + +sub do_chainping($$$$$$$) { + my ($chaintype, $addr1, $type1, $key1, $addr2, $type2, $key2) = @_; + + my $now = time(); + my $token = join(':', $chaintype, $addr1, $type1, $key1, $addr2, $type2, $key2, $now); + my $mac = Echolot::Tools::make_mac($token); + my $body = "chaintype: $chaintype\n". + "remailer1: $addr1\n". + "type1: $type1\n". + "key1: $key1\n". + "remailer2: $addr2\n". + "type2: $type2\n". + "key2: $key2\n". + "sent: $now\n". + "mac: $mac\n". + Echolot::Tools::make_garbage(); + $body = Echolot::Tools::crypt_symmetrically($body, 'encrypt'); + + my $to = Echolot::Tools::make_address('chainping'); + if ($chaintype eq 'mix') { + do_mix_chainping($addr1, $type1, $key1, $addr2, $type2, $key2, $to, $body); + } elsif ($chaintype eq 'cpunk') { + do_cpunk_chainping($addr1, $type1, $key1, $addr2, $type2, $key2, $to, $body); + } else { + Echolot::Log::warn("Don't know how to handle chain ping type $chaintype."); + return 0; + }; + + Echolot::Globals::get()->{'storage'}->register_chainpingout($chaintype, $addr1, $type1, $key1, $addr2, $type2, $key2, $now); + return 1; +}; + +sub send_pings($;$$) { + return 1 unless Echolot::Config::get()->{'do_chainpings'}; + + my ($scheduled_for, $which1, $which2) = @_; + + $which1 = '' unless defined $which1; + $which2 = '' unless defined $which2; + + my $call_intervall = Echolot::Config::get()->{'chainpinger_interval'}; + my $send_every_n_calls = Echolot::Config::get()->{'chainping_every_nth_time'}; + + my $timemod = int ($scheduled_for / $call_intervall); + my $this_call_id = $timemod % $send_every_n_calls; + my $session_id = int ($scheduled_for / ($call_intervall * $send_every_n_calls)); + + # Same thing for Intensive Care -- yet unknown or already broken chains + my $send_every_n_calls_ic = Echolot::Config::get()->{'chainping_ic_every_nth_time'}; + + my $timemod_ic = int ($scheduled_for / $call_intervall); + my $this_call_id_ic = $timemod_ic % $send_every_n_calls_ic; + my $session_id_ic = int ($scheduled_for / ($call_intervall * $send_every_n_calls_ic)); + + my @remailers = Echolot::Globals::get()->{'storage'}->get_addresses(); + for my $chaintype (keys %{Echolot::Config::get()->{'which_chainpings'}}) { + + my @thisrems; + for my $rem (@remailers) { + next unless $rem->{'pingit'}; + my $addr = $rem->{'address'}; + my $type; + my %supports = map { $_ => 1 } Echolot::Globals::get()->{'storage'}->get_types($addr); + for my $thistype (@{Echolot::Config::get()->{'which_chainpings'}->{$chaintype}}) { + $type = $thistype, last if $supports{$thistype}; + }; + next unless $type; + my $key; + my $latest = 0; + for my $keyid (Echolot::Globals::get()->{'storage'}->get_keys($addr, $type)) { + my %key = Echolot::Globals::get()->{'storage'}->get_key($addr, $type, $keyid); + $key = $keyid, $latest = $key{'last_update'} if $latest < $key{'last_update'}; + }; + push @thisrems, { addr => $addr, type => $type, key => $key }; + }; + + for my $rem1 (@thisrems) { + my $addr1 = $rem1->{'addr'}; + + next unless ( + $which1 eq 'all' || + $which1 eq $addr1 || + $which1 eq ''); + + my $type1 = $rem1->{'type'}; + my $key1 = $rem1->{'key'}; + + for my $rem2 (@thisrems) { + my $addr2 = $rem2->{'addr'}; + next if $rem1 eq $rem2 && (! ($which1 eq $addr2 && $which2 eq $addr2)); + + next unless ( + $which2 eq 'all' || + $which2 eq $addr2 || + $which2 eq ''); + + my $type2 = $rem2->{'type'}; + my $key2 = $rem2->{'key'}; + + my $call_id = Echolot::Tools::makeShortNumHash($addr1.$addr2.$chaintype.$session_id ) % $send_every_n_calls; + my $call_id_ic = Echolot::Tools::makeShortNumHash($addr1.$addr2.$chaintype.$session_id_ic) % $send_every_n_calls_ic; + next unless ( + (($which1 eq $addr1 || $which1 eq 'all' ) && ($which2 eq $addr2 || $which2 eq 'all')) || + (($which1 eq '' && $which2 eq '') && ( + $this_call_id eq $call_id || + (defined $INTENSIVE_CARE{$chaintype}->{$addr1.' '.$addr2} && $this_call_id_ic eq $call_id_ic)))); + + Echolot::Log::debug("chainping calling $chaintype, $addr1 ($type1, $key1) - $addr2 ($type2, $key2)"); + do_chainping($chaintype, $addr1, $type1, $key1, $addr2, $type2, $key2); + }; + }; + }; + return 1; +}; + +sub set_intensive_care($@) { + my ($chaintype, $intensive_care) = @_; + + %{$INTENSIVE_CARE{$chaintype}} = map { ($_->{'addr1'}.' '.$_->{'addr2'}) => $_->{'reason'} } @$intensive_care; + if (scalar @$intensive_care) { + Echolot::Log::debug("intensive care $chaintype:\n" . join("\n", sort { $a cmp $b } map { "$_: $INTENSIVE_CARE{$chaintype}->{$_}" } keys %{$INTENSIVE_CARE{$chaintype}} )); + } else { + Echolot::Log::debug("intensive care $chaintype: (none)"); + }; +}; + +sub receive($$$$) { + my ($header, $msg, $token, $timestamp) = @_; + + my $now = time(); + + my $body; + if ($msg =~ /^-----BEGIN PGP MESSAGE-----/m) { + # work around borken middleman remailers that have a problem with some + # sort of end of line characters and randhopping them through reliable + # remailers.. + # they add an empty line between each usefull line + $msg =~ s/(\r?\n)\r?\n/$1/g if ($msg =~ /^-----BEGIN PGP MESSAGE-----\r?\n\r?\n/m); + $body = Echolot::Tools::crypt_symmetrically($msg, 'decrypt'); + }; + $body = $msg unless defined $body; + + my ($chaintype) = $body =~ /^chaintype: (.*)$/m; + my ($addr1) = $body =~ /^remailer1: (.*)$/m; + my ($type1) = $body =~ /^type1: (.*)$/m; + my ($key1) = $body =~ /^key1: (.*)$/m; + my ($addr2) = $body =~ /^remailer2: (.*)$/m; + my ($type2) = $body =~ /^type2: (.*)$/m; + my ($key2) = $body =~ /^key2: (.*)$/m; + my ($sent) = $body =~ /^sent: (.*)$/m; + my ($mac) = $body =~ /^mac: (.*)$/m; + + my @values = ($chaintype, $addr1, $type1, $key1, $addr2, $type2, $key2, $sent, $mac); + my $cleanstring = join ":", map { defined() ? $_ : "undef" } @values; + + (grep { ! defined() } @values) and + Echolot::Log::warn("Received chainping at $timestamp has undefined values: $cleanstring."), + return 0; + + pop @values; + Echolot::Tools::verify_mac(join(':', @values), $mac) or + Echolot::Log::warn("Received chainping at $timestamp has wrong mac; $cleanstring."), + return 0; + + Echolot::Globals::get()->{'storage'}->register_chainpingdone($chaintype, $addr1, $type1, $key1, $addr2, $type2, $key2, $sent, $now - $sent) or + return 0; + + return 1; +}; + +1; +# vim: set ts=4 shiftwidth=4: diff --git a/trunk/Echolot/Commands.pm b/trunk/Echolot/Commands.pm new file mode 100644 index 0000000..0602bf2 --- /dev/null +++ b/trunk/Echolot/Commands.pm @@ -0,0 +1,131 @@ +package Echolot::Commands; + +# +# $Id$ +# +# This file is part of Echolot - a Pinger for anonymous remailers. +# +# Copyright (c) 2002, 2003, 2004 Peter Palfrader <peter@palfrader.org> +# +# This program is free software. you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +=pod + +=head1 Name + +Echolot::Commands - manage commands like add key, set ttl etc. + +=head1 DESCRIPTION + +This package provides functions for sending out and receiving pings. + +=cut + +use strict; +use Echolot::Log; +use Fcntl ':flock'; # import LOCK_* constants +#use Fcntl ':seek'; # import SEEK_* constants +use POSIX; # import SEEK_* constants (older perls don't have SEEK_ in Fcntl) +use English; + +sub addCommand($) { + my ($command) = @_; + + my $filename = Echolot::Config::get()->{'commands_file'}; + open(FH, ">>$filename" ) or + Echolot::Log::warn("Cannot open $filename for appending $!."), + return 0; + flock(FH, LOCK_EX) or + Echolot::Log::warn("Cannot get exclusive lock on $filename: $!."), + return 0; + + print FH $command,"\n"; + + flock(FH, LOCK_UN) or + Echolot::Log::warn("Cannot unlock $filename: $!."); + close(FH) or + Echolot::Log::warn("Cannot close $filename: $!."); +}; + +sub processCommands($) { + my $filename = Echolot::Config::get()->{'commands_file'}; + + (-e $filename) or + return 1; + my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size, $atime,$mtime,$ctime,$blksize,$blocks)= stat $filename; + ($size > 0) or + return 1; + + open(FH, "+<$filename" ) or + Echolot::Log::warn("Cannot open $filename for reading: $!."), + return 0; + flock(FH, LOCK_EX) or + Echolot::Log::warn("Cannot get exclusive lock on $filename: $!."), + return 0; + + + while (<FH>) { + chomp; + my ($command, @args) = split; + + if ($command eq 'add') { + Echolot::Globals::get()->{'storage'}->add_address(@args); + } elsif ($command eq 'set') { + Echolot::Globals::get()->{'storage'}->set_stuff(@args); + } elsif ($command eq 'getkeyconf') { + Echolot::Globals::get()->{'scheduler'}->schedule('getkeyconf', 0, time(), \@args ); + } elsif ($command eq 'sendpings') { + Echolot::Globals::get()->{'scheduler'}->schedule('ping', 0, time(), \@args ); + } elsif ($command eq 'sendchainpings') { + Echolot::Globals::get()->{'scheduler'}->schedule('chainping', 0, time(), \@args ); + } elsif ($command eq 'buildstats') { + Echolot::Globals::get()->{'scheduler'}->schedule('buildstats', 0, time() ); + } elsif ($command eq 'buildkeys') { + Echolot::Globals::get()->{'scheduler'}->schedule('buildkeys', 0, time() ); + } elsif ($command eq 'buildthesaurus') { + Echolot::Globals::get()->{'scheduler'}->schedule('buildthesaurus', 0, time() ); + } elsif ($command eq 'buildfromlines') { + Echolot::Globals::get()->{'scheduler'}->schedule('buildfromlines', 0, time() ); + } elsif ($command eq 'summary') { + @args = ('manual'); + Echolot::Globals::get()->{'scheduler'}->schedule('summary', 0, time(), \@args ); + } elsif ($command eq 'delete') { + Echolot::Globals::get()->{'storage'}->delete_remailer(@args); + } elsif ($command eq 'setremailercaps') { + my $addr = shift @args; + my $conf = join(' ', @args); + Echolot::Conf::set_caps_manually($addr, $conf); + } elsif ($command eq 'deleteremailercaps') { + Echolot::Globals::get()->{'storage'}->delete_remailercaps(@args); + } else { + Echolot::Log::warn("Unkown command: '$_'."); + }; + }; + + seek(FH, 0, SEEK_SET) or + Echolot::Log::warn("Cannot seek to start '$filename': $!."), + return 0; + truncate(FH, 0) or + Echolot::Log::warn("Cannot truncate '$filename' to zero length: $!."), + return 0; + flock(FH, LOCK_UN) or + Echolot::Log::warn("Cannot unlock '$filename': $!."); + close(FH) or + Echolot::Log::warn("Cannot close '$filename': $!."); +}; + +1; +# vim: set ts=4 shiftwidth=4: diff --git a/trunk/Echolot/Conf.pm b/trunk/Echolot/Conf.pm new file mode 100644 index 0000000..2bfa582 --- /dev/null +++ b/trunk/Echolot/Conf.pm @@ -0,0 +1,531 @@ +package Echolot::Conf; + +# +# $Id$ +# +# This file is part of Echolot - a Pinger for anonymous remailers. +# +# Copyright (c) 2002, 2003, 2004 Peter Palfrader <peter@palfrader.org> +# +# This program is free software. you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +=pod + +=head1 Name + +Echolot::Conf - remailer Configuration/Capabilities + +=head1 DESCRIPTION + +This package provides functions for requesting, parsing, and analyzing +remailer-conf and remailer-key replies. + +=head1 CAVEATS + +When parsing OpenPGP keys only the address of the primary user id is taken into +account (This is the one with the latest self signature I think). + +=cut + +use strict; +use Echolot::Log; +use GnuPG::Interface; + + +sub is_not_a_remailer($) { + my ($reply) = @_; + if ($reply =~ /^\s* not \s+ a \s+ remailer\b/xi) { + return 1; + } else { + return 0; + }; +}; + +sub send_requests($;$) { + my ($scheduled_for, $which) = @_; + + $which = '' unless defined $which; + + my $call_intervall = Echolot::Config::get()->{'getkeyconf_interval'}; + my $send_every_n_calls = Echolot::Config::get()->{'getkeyconf_every_nth_time'}; + + my $timemod = int ($scheduled_for / $call_intervall); + my $this_call_id = $timemod % $send_every_n_calls; + my $session_id = int ($scheduled_for / ($call_intervall * $send_every_n_calls)); + + Echolot::Globals::get()->{'storage'}->delay_commit(); + + for my $remailer (Echolot::Globals::get()->{'storage'}->get_addresses()) { + next unless ($remailer->{'status'} eq 'active'); + next unless ($remailer->{'fetch'}); + my $address = $remailer->{'address'}; + + next unless ( + $which eq 'all' || + $which eq $address || + $which eq ''); + + for my $type (qw{conf key help stats adminkey}) { + + next unless ( + $which eq $address || + $which eq 'all' || + (($which eq '') && ($this_call_id == (Echolot::Tools::makeShortNumHash($address.$type.$session_id) % $send_every_n_calls)))); + + Echolot::Log::debug("Sending $type request to ".$address."."); + + my $source_text = Echolot::Config::get()->{'remailerxxxtext'}; + my $template = HTML::Template->new( + scalarref => \$source_text, + strict => 0, + global_vars => 1 ); + $template->param ( address => $address ); + $template->param ( operator_address => Echolot::Config::get()->{'operator_address'} ); + my $body = $template->output(); + + Echolot::Tools::send_message( + 'To' => $address, + 'Subject' => 'remailer-'.$type, + 'Token' => $type.'.'.$remailer->{'id'}, + 'Body' => $body); + + Echolot::Globals::get()->{'storage'}->decrease_ttl($address) if (($type eq 'conf') && ($which eq '')); + }; + }; + Echolot::Globals::get()->{'storage'}->enable_commit(); +}; + +sub check_resurrection() { + Echolot::Globals::get()->{'storage'}->delay_commit(); + for my $remailer (Echolot::Globals::get()->{'storage'}->get_addresses()) { + next unless ($remailer->{'status'} eq 'ttl timeout'); + next unless ($remailer->{'fetch'}); + next unless ($remailer->{'resurrection_ttl'}); + Echolot::Log::debug("Sending request to ".$remailer->{'address'}." to check for resurrection."); + for my $type (qw{conf key help stats adminkey}) { + Echolot::Tools::send_message( + 'To' => $remailer->{'address'}, + 'Subject' => 'remailer-'.$type, + 'Token' => $type.'.'.$remailer->{'id'}) + }; + Echolot::Globals::get()->{'storage'}->decrease_resurrection_ttl($remailer->{'address'}); + }; + Echolot::Globals::get()->{'storage'}->enable_commit(); +}; + + +sub remailer_caps($$$;$) { + my ($conf, $token, $time, $dontexpire) = @_; + + my ($id) = $token =~ /^conf\.(\d+)$/; + (defined $id) or + Echolot::Log::info("Returned token '$token' has no id at all."), + return 0; + + Echolot::Log::info("Could not find id in token '$token'."), return 0 unless defined $id; + my ($remailer_type) = ($conf =~ /^\s*Remailer-Type:\s* (.*?) \s*$/imx); + Echolot::Log::info("No remailer type found in remailer_caps from '$token'."), return 0 unless defined $remailer_type; + my ($remailer_caps) = ($conf =~ /^\s*( \$remailer{".*"} \s*=\s* "<.*@.*>.*"; )\s*$/imx); + Echolot::Log::info("No remailer caps found in remailer_caps from '$token'."), return 0 unless defined $remailer_caps; + my ($remailer_nick, $remailer_address) = ($remailer_caps =~ /^\s* \$remailer{"(.*)"} \s*=\s* "<(.*@.*)>.*"; \s*$/ix); + Echolot::Log::info("No remailer nick found in remailer_caps from '$token': '$remailer_caps'."), return 0 unless defined $remailer_nick; + Echolot::Log::info("No remailer address found in remailer_caps from '$token': '$remailer_caps'."), return 0 unless defined $remailer_address; + + + my $remailer = Echolot::Globals::get()->{'storage'}->get_address_by_id($id); + Echolot::Log::info("No remailer found for id '$id'."), return 0 unless defined $remailer; + if ($remailer->{'address'} ne $remailer_address) { + # Address mismatch -> Ignore reply and add $remailer_address to prospective addresses + Echolot::Log::info("Remailer address mismatch $remailer->{'address'} vs $remailer_address. Adding latter to prospective remailers."); + Echolot::Globals::get()->{'storage'}->add_prospective_address($remailer_address, 'self-capsstring-conf', $remailer_address); + } else { + Echolot::Log::debug("Setting capabilities for $remailer_address"); + Echolot::Globals::get()->{'storage'}->restore_ttl( $remailer->{'address'} ); + Echolot::Globals::get()->{'storage'}->set_caps($remailer_type, $remailer_caps, $remailer_nick, $remailer_address, $time, $dontexpire); + + # if remailer is cpunk and not pgponly + if (($remailer_caps =~ /\bcpunk\b/) && !($remailer_caps =~ /\bpgponly\b/)) { + Echolot::Globals::get()->{'storage'}->set_key( + 'cpunk-clear', + $remailer_nick, + $remailer->{'address'}, + 'N/A', + 'none', + 'N/A', + 'N/A', + 'N/A', + $time); + } + } + + + # Fetch prospective remailers from reliable's remailer-conf reply: + my @lines = split /\r?\n/, $conf; + + while (1) { + my $head; + while (@lines) { + $head = $lines[0]; + chomp $head; + shift @lines; + last if ($head eq 'SUPPORTED CPUNK (TYPE I) REMAILERS' || + $head eq 'SUPPORTED MIXMASTER (TYPE II) REMAILERS'); + }; + last unless defined $head; + my $wanting = $head eq 'SUPPORTED CPUNK (TYPE I) REMAILERS' ? 1 : + $head eq 'SUPPORTED MIXMASTER (TYPE II) REMAILERS' ? 2 : + undef; + last unless defined $wanting; + + while (@lines) { + $head = $lines[0]; + chomp $head; + shift @lines; + if ($wanting == 1) { + last unless ($head =~ /<(.*?@.*?)>/); + Echolot::Globals::get()->{'storage'}->add_prospective_address($1, 'reliable-caps-reply-type1', $remailer_address); + } elsif ($wanting == 2) { + last unless ($head =~ /\s(.*?@.*?)\s/); + Echolot::Globals::get()->{'storage'}->add_prospective_address($1, 'reliable-caps-reply-type2', $remailer_address); + } else { + Echolot::Log::confess("Shouldn't be here. wanting == $wanting."); + }; + }; + }; + + return 1; +}; + +sub remailer_conf($$$) { + my ($reply, $token, $time) = @_; + + my ($id) = $token =~ /^conf\.(\d+)$/; + (defined $id) or + Echolot::Log::info ("Returned token '$token' has no id at all."), + return 0; + + my $remailer = Echolot::Globals::get()->{'storage'}->get_address_by_id($id); + Echolot::Log::info("No remailer found for id '$id'."), return 0 unless defined $remailer; + Echolot::Log::debug("Received remailer-conf reply for $remailer."), + + Echolot::Globals::get()->{'storage'}->not_a_remailer($id), return 1 + if (is_not_a_remailer($reply)); + Echolot::Thesaurus::save_thesaurus('conf', $id, $reply); + + + remailer_caps($reply, $token, $time); +}; + +sub set_caps_manually($$) { + my ($addr, $caps) = @_; + + defined $addr or + Echolot::Log::info("Address not defined."), + return 0; + defined $caps or + Echolot::Log::info("Caps not defined."), + return 0; + + Echolot::Log::info("Setting caps for $addr manually to $caps."); + + my $remailer = Echolot::Globals::get()->{'storage'}->get_address($addr); + defined $remailer or + Echolot::Log::info("Remailer address $addr did not give a valid remailer."), + return 0; + my $id = $remailer->{'id'}; + defined $id or + Echolot::Log::info("Remailer address $addr did not give a remailer with an id."), + return 0; + my $token = 'conf.'.$id; + + my $conf = "Remailer-Type: set-manually\n$caps"; + remailer_caps($conf, $token, time, 1); + + return 1; +}; + +sub parse_mix_key($$$) { + my ($reply, $time, $remailer) = @_; + +# -----Begin Mix Key----- +# 7f6d997678b19ccac110f6e669143126 +# 258 +# AASyedeKiP1/UKyfrBz2K6gIhv4jfXIaHo8dGmwD +# KqkG3DwytgSySSY3wYm0foT7KvEnkG2aTi/uJva/ +# gymE+tsuM8l8iY1FOiXwHWLDdyUBPbrLjRkgm7GD +# Y7ogSjPhVLeMpzkSyO/ryeUfLZskBUBL0LxjLInB +# YBR3o6p/RiT0EQAAAAAAAAAAAAAAAAAAAAAAAAAA +# AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +# AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +# AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +# AAAAAAAAAAAAAAAAAAAAAQAB +# -----End Mix Key----- + + my %mixmasters; + # rot26 rot26@mix.uucico.de 7f6d997678b19ccac110f6e669143126 2.9b33 MC + my @mix_confs = ($reply =~ /^ + [a-z0-9]+ + \s+ + \S+\@\S+ + \s+ + [0-9a-f]{32} + .*?$/xmg); + my @mix_keys = ($reply =~ /^-----Begin \s Mix \s Key-----\r?\n + [0-9a-f]{32}\r?\n + \d+\r?\n + (?:[a-zA-Z0-9+\/]*\r?\n)+ + -----End \s Mix \s Key-----$/xmg ); + for (@mix_confs) { + my ($nick, $address, $keyid, $version, $caps, $created, $expires) = /^ + ([a-z0-9]+) + \s+ + (\S+@\S+) + \s+ + ([0-9a-f]{32}) + (?: [ \t]+ + (\S+) + (?: [ \t]+ + (\S+) + (?: [ \t]+ + (\d{4}-\d{2}-\d{2}) + (?: [ \t]+ + (\d{4}-\d{2}-\d{2}) + )? + )? + )? + )? .*?/x; + $mixmasters{$keyid} = { + nick => $nick, + address => $address, + version => $version, + caps => $caps, + created => $created, + expires => $expires, + summary => $_ + }; + }; + for (@mix_keys) { + my ($keyid) = /^-----Begin \s Mix \s Key-----\r?\n + ([0-9a-f]{32})\r?\n + \d+\r?\n + (?:[a-zA-Z0-9+\/]*\r?\n)+ + -----End \s Mix \s Key-----$/xmg; + $mixmasters{$keyid}->{'key'} = $_; + }; + + for my $keyid (keys %mixmasters) { + my $remailer_address = $mixmasters{$keyid}->{'address'}; + (defined $mixmasters{$keyid}->{'nick'}) or + Echolot::Log::info("Could not parse a remailer-key reply."), + next; + (defined $mixmasters{$keyid}->{'nick'} && ! defined $mixmasters{$keyid}->{'key'}) and + Echolot::Log::info("Mixmaster key header without key in reply from $remailer_address."), + next; + (! defined $mixmasters{$keyid}->{'nick'} && defined $mixmasters{$keyid}->{'key'}) and + Echolot::Log::info("Mixmaster key without key header in reply from $remailer_address."), + next; + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) = gmtime(); + my $today = sprintf("%04d-%02d-%02d", $year+1900, $mon+1, $mday); + (defined $mixmasters{$keyid}->{'created'} && ($today lt $mixmasters{$keyid}->{'created'})) and + Echolot::Log::info("Mixmaster key for $remailer_address created in the future ($today < ".$mixmasters{$keyid}->{'created'}.")."), + next; + (defined $mixmasters{$keyid}->{'expires'} && ($mixmasters{$keyid}->{'expires'} lt $today)) and + Echolot::Log::info("Mixmaster key for $remailer_address expired (".$mixmasters{$keyid}->{'expires'}." < $today)."), + next; + + if ($remailer->{'address'} ne $remailer_address) { + # Address mismatch -> Ignore reply and add $remailer_address to prospective addresses + Echolot::Log::info("Remailer address mismatch $remailer->{'address'} vs $remailer_address. Adding latter to prospective remailers."); + Echolot::Globals::get()->{'storage'}->add_prospective_address($remailer_address, 'self-capsstring-key', $remailer_address); + } else { + Echolot::Log::debug("Setting mix key for $remailer_address: $keyid"); + Echolot::Globals::get()->{'storage'}->restore_ttl( $remailer->{'address'} ); + Echolot::Globals::get()->{'storage'}->set_key( + 'mix', + $mixmasters{$keyid}->{'nick'}, + $mixmasters{$keyid}->{'address'}, + $mixmasters{$keyid}->{'key'}, + $keyid, + $mixmasters{$keyid}->{'version'}, + $mixmasters{$keyid}->{'caps'}, + $mixmasters{$keyid}->{'summary'}, + $time); + } + }; + + return 1; +}; + +sub parse_cpunk_key($$$) { + my ($reply, $time, $remailer) = @_; + + my $GnuPG = new GnuPG::Interface; + $GnuPG->call( Echolot::Config::get()->{'gnupg'} ) if (Echolot::Config::get()->{'gnupg'}); + $GnuPG->options->hash_init( + homedir => Echolot::Config::get()->{'gnupghome'} ); + $GnuPG->options->meta_interactive( 0 ); + my %cypherpunk; + + my @pgp_keys = ($reply =~ /^-----BEGIN \s PGP \s PUBLIC \s KEY \s BLOCK-----\r?\n + (?:.+\r?\n)* + \r?\n + (?:[a-zA-Z0-9+\/=]*\r?\n)+ + -----END \s PGP \s PUBLIC \s KEY \s BLOCK-----$/xmg ); + for my $key (@pgp_keys) { + my ( $stdin_fh, $stdout_fh, $stderr_fh, $status_fh, $handles ) = Echolot::Tools::make_gpg_fds(); + my $pid = $GnuPG->wrap_call( + commands => [qw{--with-colons}], + command_args => [qw{--no-options --no-secmem-warning --no-default-keyring --fast-list-mode}], + handles => $handles ); + my ($stdout, $stderr, $status) = Echolot::Tools::readwrite_gpg($key, $stdin_fh, $stdout_fh, $stderr_fh, $status_fh); + waitpid $pid, 0; + + ($stderr eq '') or + Echolot::Log::info("GnuPG returned something in stderr: '$stderr' when checking key '$key'; So what?"); + ($status eq '') or + Echolot::Log::info("GnuPG returned something in status '$status' when checking key '$key': So what?"); + + my @included_keys = $stdout =~ /^pub:.*$/mg; + (scalar @included_keys >= 2) && + # FIXME handle more than one key per block nicely + Echolot::Log::debug ("Cannot handle more than one key per block nicely (correctly) yet. Found ".(scalar @included_keys)." in one block from ".$remailer->{'address'}."."); + for my $included_key (@included_keys) { + my ($type, $keyid, $uid) = $included_key =~ /pub::\d+:(\d+):([0-9A-F]+):[^:]+:[^:]*:::([^:]+):/; + (defined $uid) or + Echolot::Log::info ("Unexpected format of '$included_key' by ".$remailer->{'address'}."; Skipping."), + next; + my ($address) = $uid =~ /<(.*?)>/; + $cypherpunk{$keyid} = { + address => $address, + type => $type, + key => $key # FIXME handle more than one key per block correctly + }; + }; + }; + + for my $keyid (keys %cypherpunk) { + my $remailer_address = $cypherpunk{$keyid}->{'address'}; + + if ($remailer->{'address'} ne $remailer_address) { + # Address mismatch -> Ignore reply and add $remailer_address to prospective addresses + Echolot::Log::info("Remailer address mismatch $remailer->{'address'} vs $remailer_address id key $keyid. Adding latter to prospective remailers."); + Echolot::Globals::get()->{'storage'}->add_prospective_address($remailer_address, 'self-capsstring-key', $remailer_address); + } else { + Echolot::Globals::get()->{'storage'}->restore_ttl( $remailer->{'address'} ); + # 1 .. RSA + # 17 .. DSA + if ($cypherpunk{$keyid}->{'type'} == 1 || $cypherpunk{$keyid}->{'type'} == 17 ) { + Echolot::Log::debug("Setting cpunk key for $remailer_address: $keyid; type ".$cypherpunk{$keyid}->{'type'}); + Echolot::Globals::get()->{'storage'}->set_key( + (($cypherpunk{$keyid}->{'type'} == 1) ? 'cpunk-rsa' : + (($cypherpunk{$keyid}->{'type'} == 17) ? 'cpunk-dsa' : + 'ERROR')), + $keyid, # as nick + $cypherpunk{$keyid}->{'address'}, + $cypherpunk{$keyid}->{'key'}, + $keyid, + 'N/A', + 'N/A', + 'N/A', + $time); + } else { + Echolot::Log::info("$keyid from $remailer_address has algoid ".$cypherpunk{$keyid}->{'type'}.". Cannot handle those."); + }; + } + }; + + return 1; +}; + +sub remailer_key($$$) { + my ($reply, $token, $time) = @_; + + my $cp_reply = $reply; + $cp_reply =~ s/^- -/-/gm; # PGP Signed messages + + my ($id) = $token =~ /^key\.(\d+)$/; + (defined $id) or + Echolot::Log::info ("Returned token '$token' has no id at all."), + return 0; + + my $remailer = Echolot::Globals::get()->{'storage'}->get_address_by_id($id); + Echolot::Log::info("No remailer found for id '$id'."), return 0 unless defined $remailer; + Echolot::Log::debug("Received remailer-keys reply for $remailer."), + + Echolot::Globals::get()->{'storage'}->not_a_remailer($id), return 1 + if (is_not_a_remailer($reply)); + Echolot::Thesaurus::save_thesaurus('key', $id, $reply); + + parse_mix_key($cp_reply, $time, $remailer); + parse_cpunk_key($cp_reply, $time, $remailer); + + return 1; +}; + +sub remailer_stats($$$) { + my ($reply, $token, $time) = @_; + + my ($id) = $token =~ /^stats\.(\d+)$/; + (defined $id) or + Echolot::Log::info ("Returned token '$token' has no id at all."), + return 0; + + + my $remailer = Echolot::Globals::get()->{'storage'}->get_address_by_id($id); + Echolot::Log::info("No remailer found for id '$id'."), return 0 unless defined $remailer; + Echolot::Log::debug("Received remailer-stats reply for $remailer."), + + Echolot::Globals::get()->{'storage'}->not_a_remailer($id), return 1 + if (is_not_a_remailer($reply)); + Echolot::Thesaurus::save_thesaurus('stats', $id, $reply); +}; + +sub remailer_help($$$) { + my ($reply, $token, $time) = @_; + + my ($id) = $token =~ /^help\.(\d+)$/; + (defined $id) or + Echolot::Log::info ("Returned token '$token' has no id at all."), + return 0; + + my $remailer = Echolot::Globals::get()->{'storage'}->get_address_by_id($id); + Echolot::Log::info("No remailer found for id '$id'."), return 0 unless defined $remailer; + Echolot::Log::debug("Received remailer-help reply for $remailer."), + + Echolot::Globals::get()->{'storage'}->not_a_remailer($id), return 1 + if (is_not_a_remailer($reply)); + Echolot::Thesaurus::save_thesaurus('help', $id, $reply); +}; + +sub remailer_adminkey($$$) { + my ($reply, $token, $time) = @_; + + my ($id) = $token =~ /^adminkey\.(\d+)$/; + (defined $id) or + Echolot::Log::info ("Returned token '$token' has no id at all."), + return 0; + + my $remailer = Echolot::Globals::get()->{'storage'}->get_address_by_id($id); + Echolot::Log::info("No remailer found for id '$id'."), return 0 unless defined $remailer; + Echolot::Log::debug("Received remailer-adminkey reply for $remailer."), + + Echolot::Globals::get()->{'storage'}->not_a_remailer($id), return 1 + if (is_not_a_remailer($reply)); + Echolot::Thesaurus::save_thesaurus('adminkey', $id, $reply); +}; + +1; +# vim: set ts=4 shiftwidth=4: diff --git a/trunk/Echolot/Config.pm b/trunk/Echolot/Config.pm new file mode 100644 index 0000000..dd3d97e --- /dev/null +++ b/trunk/Echolot/Config.pm @@ -0,0 +1,344 @@ +package Echolot::Config; + +# +# $Id$ +# +# This file is part of Echolot - a Pinger for anonymous remailers. +# +# Copyright (c) 2002, 2003, 2004 Peter Palfrader <peter@palfrader.org> +# +# This program is free software. you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +=pod + +=head1 Name + +Echolot::Config - echolot configuration + +=head1 DESCRIPTION + +Sets default configuration options and +reads configuration from the config file. + +=head1 FILES + +The configuration file is searched in those places in that order: + +=over + +=item the file pointed to by the B<ECHOLOT_CONF> environment variable + +=item <basedir>/pingd.conf + +=item $HOME/echolot/pingd.conf + +=item $HOME/pingd.conf + +=item $HOME/.pingd.conf + +=item /etc/echolot/pingd.conf + +=item /etc/pingd.conf + +=back + +=cut + +use strict; +use Carp; +use English; + +my $CONFIG; + +sub init($) { + my ($params) = @_; + + die ("Basedir is not defined\n") unless defined $params->{'basedir'}; + + my @CONFIG_FILES = (); + push(@CONFIG_FILES, $ENV{'ECHOLOT_CONF'}) if defined $ENV{'ECHOLOT_CONF'}; + push(@CONFIG_FILES, $params->{'basedir'}.'/pingd.conf') if defined $params->{'basedir'}; + push(@CONFIG_FILES, $ENV{'HOME'}.'/echolot/pingd.conf') if defined $ENV{'HOME'}; + push(@CONFIG_FILES, $ENV{'HOME'}.'/pingd.conf') if defined $ENV{'HOME'}; + push(@CONFIG_FILES, $ENV{'HOME'}.'/.pingd.conf') if defined $ENV{'HOME'}; + push(@CONFIG_FILES, '/etc/echolot/pingd.conf'); + push(@CONFIG_FILES, '/etc/pingd.conf'); + + my $DEFAULT; + $DEFAULT = { + # System Specific Options + recipient_delimiter => '+', + dev_random => '/dev/random', + dev_urandom => '/dev/urandom', + sendmail => '/usr/sbin/sendmail', + + # Magic Numbers + hash_len => 8, + stats_days => 12, + seconds_per_day => 24 * 60 * 60, + + # New Remailers + fetch_new => 1, + ping_new => 1, + show_new => 1, + + # Statistics Generation + separate_rlists => 0, + combined_list => 0, + thesaurus => 1, + fromlines => 1, + stats_sort_by_latency => 0, + + # Timers and Counters + processmail => 60, # process incomng mail every minute + buildstats => 5*60, # build statistics every 5 minutes + buildkeys => 8*60*60, # build keyring every 8 hours + buildthesaurus => 60*60, # hourly + buildfromlines => 60*60, # hourly + commitprospectives => 8*60*60, # commit prospective addresses every 8 hours + expire => 24*60*60, # daily + getkeyconf_interval => 5*60, # send out requests every 5 minutes + getkeyconf_every_nth_time => 24*60/5, # send out the same request to the same remailer once a day + check_resurrection => 7*24*60*60, # weekly + summary => 24*60*60, # daily + + metadata_backup => 8*60*60, # make backups of metadata and rotate them every 8 hours + metadata_backup_count => 32, # keep 32 rotations of metadata + + pinger_interval => 5*60, # send out pings every 5 minutes + ping_every_nth_time => 24, # send out pings to the same remailer every 24 calls, i.e. every 2 hours + + chainpinger_interval => 5*60, # send out pings every 5 minutes + chainping_every_nth_time => 2016, # send out pings to the same chain every 2016 calls, i.e. week + chainping_ic_every_nth_time => 288, # send out pings to broken or unknown chains every 288 calls, i.e. every day + chainping_period => 10*24*60*60, # 12 days + chainping_fudge => 0.3, # if less than 0.3 * rel1 * rel2 make it, the chain is really broken + chainping_grace => 1.5, # don't count pings sent no longer than 1.5 * (lat1 + lat2) ago + chainping_update => 4*60*60, # chain stats should never be older than 4 hours + chainping_minsample => 3, # have at least sent 3 pings before judging any chain + chainping_allbad_factor => 0.5, # at least 50% of possible chains (A x) need to fail for (A *) to be listed in broken chains + + addresses_default_ttl => 5, # getkeyconf seconds (days) + check_resurrection_ttl => 8, # check_resurrection seconds (weeks) + prospective_addresses_ttl => 5*24*60*60, # 5 days + reliable_auto_add_min => 6, # 6 remailes need to list new address + + expire_keys => 5*24*60*60, # 5 days + expire_confs => 5*24*60*60, # 5 days + expire_pings => 12*24*60*60, # 12 days + expire_thesaurus => 21*24*60*60, # 21 days + expire_chainpings => 12*24*60*60, # 12 days + expire_fromlines => 5*24*60*60, # 5 days + cleanup_tmpdir => 24*60*60, # daily + + random_garbage => 8192, + + + # Directories and files + mailin => 'mail', + mailerrordir => 'mail-errors', + resultdir => 'results', + thesaurusdir => 'results/thesaurus', + thesaurusindexfile => 'results/thesaurus/index', + fromlinesindexfile => 'results/from', + private_resultdir => 'results.private', + indexfilebasename => 'echolot', + gnupghome => 'gnupghome', + gnupg => '', + mixhome => 'mixhome', + mixmaster => 'mix', + tmpdir => 'tmp', + broken1 => 'broken1.txt', + broken2 => 'broken2.txt', + sameop => 'sameop.txt', + gzip => 'gzip', + + commands_file => 'commands.txt', + pidfile => 'pingd.pid', + + save_errormails => 0, + write_meta_files => 1, + meta_extension => '.meta', + + storage => { + backend => 'File', + File => { + basedir => 'data' + } + }, + + # logging + logfile => 'pingd.log', + loglevel => 'info', + + + # ping types + do_pings => { + 'cpunk-dsa' => 1, + 'cpunk-rsa' => 1, + 'cpunk-clear' => 1, + 'mix' => 1 + }, + do_chainpings => 1, + show_chainpings => 1, + which_chainpings => { + 'cpunk' => [ qw{cpunk-dsa cpunk-rsa cpunk-clear} ], + 'mix' => [ qw{mix} ] + }, + pings_weight => [ qw{0.5 1.0 1.0 1.0 1.0 0.9 0.8 0.5 0.3 0.2 0.2 0.1 } ], + + # templates + templates => { + default => { + 'indexfile' => 'templates/echolot.html', + 'thesaurusindexfile' => 'templates/thesaurusindex.html', + 'fromlinesindexfile' => 'templates/fromlinesindex.html', + 'mlist' => 'templates/mlist.html', + 'mlist2' => 'templates/mlist2.html', + 'rlist' => 'templates/rlist.html', + 'rlist-rsa' => 'templates/rlist-rsa.html', + 'rlist-dsa' => 'templates/rlist-dsa.html', + 'rlist-clear' => 'templates/rlist-clear.html', + 'rlist2' => 'templates/rlist2.html', + 'rlist2-rsa' => 'templates/rlist2-rsa.html', + 'rlist2-dsa' => 'templates/rlist2-dsa.html', + 'rlist2-clear' => 'templates/rlist2-clear.html', + 'clist' => 'templates/clist.html', + }, + }, + + 'echolot_css' => 'templates/echolot.css', + + remailerxxxtext => "Hello,\n". + "\n". + "This message requests remailer configuration data. The pinging software thinks\n". + "<TMPL_VAR NAME=\"address\"> is a remailer. Either it has been told so by the\n". + "maintainer of the pinger or it found the address in a remailer-conf or\n". + "remailer-key reply of some other remailer.\n". + "\n". + "If this is _not_ a remailer, you can tell this pinger that and it will stop\n". + "sending you those requests immediately (otherwise it will try a few more times).\n". + "Just reply and make sure the following is the first line of your message:\n". + " not a remailer\n". + "\n". + "If you want to talk to a human please mail <TMPL_VAR NAME=\"operator_address\">.\n", + + homedir => undef, + my_localpart => undef, + my_domain => undef, + operator_address => undef, + sitename => undef, + verbose => 0 + }; + + + my $configfile = undef; + for my $filename ( @CONFIG_FILES ) { + if ( defined $filename && -e $filename ) { + $configfile = $filename; + print "Using config file $configfile\n" if ($params->{'verbose'}); + last; + }; + }; + + die ("no Configuration file found\n") unless defined $configfile; + + { + local $/ = undef; + open(CONFIGCODE, $configfile) or + confess("Could not open configfile '$configfile': $!"); + my $config_code = <CONFIGCODE>; + close (CONFIGCODE); + ($config_code) = $config_code =~ /^(.*)$/s; + eval ($config_code); + ($EVAL_ERROR) and + confess("Evaling config code from '$configfile' returned error: $EVAL_ERROR"); + } + + + for my $key (keys %$CONFIG) { + warn("Unkown option: $key\n") unless (exists $DEFAULT->{$key}); + }; + + # Work around spelling bug until 2.0rc3 + if (exists $CONFIG->{'seperate_rlists'}) { + if (exists $CONFIG->{'separate_rlists'}) { + warn ("seperate_rlists has been superseded by separate_rlists."); + } else { + warn ("seperate_rlists has been superseded by separate_rlists, please change it in your config file.\n"); + $CONFIG->{'separate_rlists'} = $CONFIG->{'seperate_rlists'}; + }; + delete $CONFIG->{'seperate_rlists'}; + } + + # In 2.0.6: thesaurusindexfile and indexfilebasename config values + # should not longer have the extension (.html) in them + # Handle this gracefully for now: + if (exists $CONFIG->{'thesaurusindexfile'}) { + $CONFIG->{'thesaurusindexfile'} =~ s/\.html?$// and + warn ("thesaurusindexfile no longer should have the .html extension.\n"); + } + if (exists $CONFIG->{'indexfilebasename'}) { + $CONFIG->{'indexfilebasename'} =~ s/\.html?$// and + warn ("indexfilebasename no longer should have the .html extension.\n"); + } + + for my $key (keys %$DEFAULT) { + $CONFIG->{$key} = $DEFAULT->{$key} unless exists $CONFIG->{$key}; + }; + $CONFIG->{'homedir'} = $params->{'basedir'} unless (defined $CONFIG->{'homedir'}); + $CONFIG->{'verbose'} = $params->{'verbose'} if ($params->{'verbose'}); + + for my $key (keys %$CONFIG) { + warn ("Config option $key is not defined\n") unless defined $CONFIG->{$key}; + }; +}; + +sub check_binaries() { + for my $bin (qw{mixmaster}) { + my $path = get()->{$bin}; + + if ($path =~ m#/#) { + Echolot::Log::warn ("$bin binary $path does not exist or is not executeable") + unless -x $path; + } else { + my $found = 0; + if (defined $ENV{'PATH'}) { + for my $pathelem (split /:/, $ENV{'PATH'}) { + $found = $pathelem, last + if -e $pathelem.'/'.$path; + }; + }; + if ($found) { + Echolot::Log::warn ("$bin binary $found/$path is not executeable") + unless -x $found.'/'.$path; + } else { + Echolot::Log::warn ("$bin binary $path not found"); + }; + }; + }; +}; + +sub get() { + return $CONFIG; +}; + +sub dump() { + print Data::Dumper->Dump( [ $CONFIG ], [ 'CONFIG' ] ); +}; + +1; +# vim: set ts=4 shiftwidth=4: diff --git a/trunk/Echolot/Fromlines.pm b/trunk/Echolot/Fromlines.pm new file mode 100644 index 0000000..0479a42 --- /dev/null +++ b/trunk/Echolot/Fromlines.pm @@ -0,0 +1,126 @@ +package Echolot::Fromlines; + +# +# $Id$ +# +# This file is part of Echolot - a Pinger for anonymous remailers. +# +# Copyright (c) 2002, 2003, 2004 Peter Palfrader <peter@palfrader.org> +# +# This program is free software. you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +=pod + +=head1 Name + +Echolot::Thesaurus - build from header page + +=head1 DESCRIPTION + +This package builds the from header page with the information we +received from pings. + +=cut + +use strict; +use English; +use Echolot::Log; + + +sub build_fromlines() { + return 1 unless Echolot::Config::get()->{'fromlines'}; + + my $data; + my @remailers = Echolot::Globals::get()->{'storage'}->get_addresses(); + + for my $remailer (@remailers) { + next unless $remailer->{'showit'}; + my $addr = $remailer->{'address'}; + my $nick = Echolot::Globals::get()->{'storage'}->get_nick($addr); + next unless defined $nick; + my $caps = Echolot::Globals::get()->{'storage'}->get_capabilities($addr); + next unless defined $caps; + next unless $caps !~ m/\btesting\b/i; + my $middleman = $caps =~ m/\bmiddle\b/; + next if $middleman; + + + for my $user_supplied (0, 1) { + $data->{$user_supplied}->{$addr}->{'nick'} = $nick; + $data->{$user_supplied}->{$addr}->{'address'} = $addr; + + my @types = Echolot::Globals::get()->{'storage'}->get_types($addr); + my $from_types; + for my $type (@types) { + my $from_info = Echolot::Globals::get()->{'storage'}->get_fromline($addr, $type, $user_supplied); + my $from = $from_info->{'from'}; + $from = 'Not Available' unless defined $from; + $from = 'Middleman Remailer' if $middleman; + my $disclaim_top = $from_info->{'disclaim_top'} && ! $middleman ? 1 : 0; + my $disclaim_bot = $from_info->{'disclaim_bot'} && ! $middleman ? 1 : 0; + #my $last_update = $from_info->{'last_update'}; + #my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) = gmtime($last_update); + my $frominfo = $disclaim_top.':'.$disclaim_bot.':'.$from; + #my $date = sprintf("%04d-%02d-%02d", $year+1900, $mon+1, $mday); + #my $value = $middleman ? $type : ($type." ($date)"); + my $value = $type; + push @{$from_types->{$frominfo}}, $value; + }; + my $types_from; + for my $frominfo (sort keys %$from_types) { + my $types = join ", ", sort { $a cmp $b } @{$from_types->{$frominfo}}; + $types_from->{$types} = $frominfo; + }; + my @types_from = map { + my ($disclaim_top, $disclaim_bot, $from) = split (/:/, $types_from->{$_}, 3); + { + nick => $nick, + address => $addr, + types => $_, + disclaim_top => $disclaim_top, + disclaim_bot => $disclaim_bot, + from => Echolot::Tools::escape_HTML_entities($from) + } + } sort { $a cmp $b } keys %$types_from; + $data->{$user_supplied}->{$addr}->{'data'} = \@types_from; + }; + + # Remove user supplied if identical + my $f0 = join ':', map { + $_->{'disclaim_top'}.':'.$_->{'disclaim_bot'}.$_->{'types'}.':'.$_->{'from'} + } @{$data->{0}->{$addr}->{'data'}}; + my $f1 = join ':', map { + $_->{'disclaim_top'}.':'.$_->{'disclaim_bot'}.$_->{'types'}.':'.$_->{'from'} + } @{$data->{1}->{$addr}->{'data'}}; + if ($f0 eq $f1) { + delete $data->{1}->{$addr}; + }; + }; + + my @data0 = map {$data->{0}->{$_}} (sort { $data->{0}->{$a}->{'nick'} cmp $data->{0}->{$b}->{'nick'} } keys (%{$data->{0}})); + my @data1 = map {$data->{1}->{$_}} (sort { $data->{1}->{$a}->{'nick'} cmp $data->{1}->{$b}->{'nick'} } keys (%{$data->{1}})); + + Echolot::Tools::write_HTML_file( + Echolot::Config::get()->{'fromlinesindexfile'}, + 'fromlinesindexfile', + Echolot::Config::get()->{'buildfromlines'}, + default => \@data0, + usersupplied => \@data1); +}; + + +1; +# vim: set ts=4 shiftwidth=4: diff --git a/trunk/Echolot/Globals.pm b/trunk/Echolot/Globals.pm new file mode 100644 index 0000000..898e5d6 --- /dev/null +++ b/trunk/Echolot/Globals.pm @@ -0,0 +1,60 @@ +package Echolot::Globals; + +# +# $Id$ +# +# This file is part of Echolot - a Pinger for anonymous remailers. +# +# Copyright (c) 2002, 2003, 2004 Peter Palfrader <peter@palfrader.org> +# +# This program is free software. you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +=pod + +=head1 Name + +Echolot::Globals - echolot global variables + +=head1 DESCRIPTION + +=cut + +use strict; +use Carp; + +my $GLOBALS; + +sub init(%) { + my (%args) = @_; + + my $hostname = `hostname`; + $hostname =~ /^([a-zA-Z0-9_.-]*)$/; + $hostname = $1 || 'unknown'; + $GLOBALS->{'hostname'} = $hostname; + $GLOBALS->{'internalcounter'} = 1; + $GLOBALS->{'version'} = $args{'version'}; +}; + +sub initStorage { + $GLOBALS->{'storage'} = new Echolot::Storage::File ( datadir => Echolot::Config::get()->{'storage'}->{'File'}->{'basedir'} ); +}; + +sub get() { + return $GLOBALS; +}; + +1; +# vim: set ts=4 shiftwidth=4: diff --git a/trunk/Echolot/Log.pm b/trunk/Echolot/Log.pm new file mode 100644 index 0000000..15b5b80 --- /dev/null +++ b/trunk/Echolot/Log.pm @@ -0,0 +1,163 @@ +package Echolot::Log; + +# +# $Id$ +# +# This file is part of Echolot - a Pinger for anonymous remailers. +# +# Copyright (c) 2002, 2003, 2004 Peter Palfrader <peter@palfrader.org> +# +# This program is free software. you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +=pod + +=head1 Name + +Echolot::Globals - echolot global variables + +=head1 DESCRIPTION + +=cut + +use strict; +use Carp qw{}; +#use Time::HiRes qw( gettimeofday ); + +my %LOGLEVELS = qw{ + trace 8 + debug 7 + info 6 + notice 5 + warn 4 + warning 4 + error 3 + critical 2 + alert 1 + emergency 0 +}; + +my $LOGLEVEL; +my $LOGFILE; +my $LOGFH; + +my @monnames = qw{Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec}; +sub header_log($$) { + my ($level, $msg) = @_; + + #my ($secs, $msecs) = gettimeofday(); + #my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime( $secs ); + #my $time = sprintf("%s %02d %02d:%02d:%02d.%06d", + # $monnames[$mon], + # $mday, + # $hour, $min, $sec, $msecs); + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(); + my $time = sprintf("%s %02d %02d:%02d:%02d", + $monnames[$mon], + $mday, + $hour, $min, $sec); + my $prefix = $time.' ['.uc($level).'] '; + my $logstring = $msg."\n"; + my $first = 0; + $logstring =~ s/^/ $prefix . ($first++ ? ' ' : '' ) /emg; + return $logstring; +}; + +sub reopen() { + $LOGFH->close() if ($LOGFH->opened()); + + open($LOGFH, ">>".$LOGFILE) or + warn("Cannot open logfile $LOGFILE: $!"); +}; + +sub init() { + $LOGFILE = Echolot::Config::get()->{'logfile'}; + $LOGLEVEL = Echolot::Config::get()->{'loglevel'}; + $LOGFH = new IO::Handle; + + die ("Logfile not defined") unless defined ($LOGFILE); + die ("Loglevel not defined") unless defined ($LOGLEVEL); + die ("Loglevel $LOGLEVEL unkown") unless defined ($LOGLEVELS{$LOGLEVEL}); + + $LOGLEVEL = $LOGLEVELS{$LOGLEVEL}; + + reopen(); +}; + +sub log_message($$) { + my ($level, $msg) = @_; + + die("Loglevel $level unkown.") unless defined $LOGLEVELS{$level}; + return if $LOGLEVELS{$level} > $LOGLEVEL; + + $msg = header_log($level, $msg); + print $LOGFH $msg; + $LOGFH->flush(); +}; + +sub trace($) { + log_message('trace', $_[0]); +}; +sub debug($) { + log_message('debug', $_[0]); +}; +sub info($) { + log_message('info', $_[0]); +}; +sub notice($) { + log_message('notice', $_[0]); +}; +sub warn($) { + log_message('warn', $_[0]); +}; +sub warning($) { + log_message('warning', $_[0]); +}; +sub error($) { + log_message('error', $_[0]); +}; +sub critical($) { + log_message('critical', $_[0]); +}; +sub alert($) { + log_message('alert', $_[0]); +}; +sub emergency($) { + log_message('emergency', $_[0]); +}; + +sub logdie($) { + my ($msg) = @_; + critical($msg); + die($msg); +}; +sub cluck($) { + my ($msg) = @_; + my $longmess = Carp::longmess(); + $longmess =~ s/^/ /mg; + $msg .= "\n".$longmess; + warning($msg); +}; +sub confess($) { + my ($msg) = @_; + my $longmess = Carp::longmess(); + $longmess =~ s/^/ /mg; + $msg .= "\n".$longmess; + error($msg); + die($msg); +}; + +1; +# vim: set ts=4 shiftwidth=4: diff --git a/trunk/Echolot/Mailin.pm b/trunk/Echolot/Mailin.pm new file mode 100644 index 0000000..a2f2abb --- /dev/null +++ b/trunk/Echolot/Mailin.pm @@ -0,0 +1,252 @@ +package Echolot::Mailin; + +# +# $Id$ +# +# This file is part of Echolot - a Pinger for anonymous remailers. +# +# Copyright (c) 2002, 2003, 2004 Peter Palfrader <peter@palfrader.org> +# +# This program is free software. you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +=pod + +=head1 Name + +Echolot::Mailin - Incoming Mail Dispatcher for Echolot + +=head1 DESCRIPTION + + +=cut + +use strict; +use English; +use Echolot::Globals; +use Echolot::Log; +use Fcntl ':flock'; # import LOCK_* constants +use POSIX; # import SEEK_* constants (older perls don't have SEEK_ in Fcntl) + + +sub make_sane_name() { + my $result = time().'.'.$PROCESS_ID.'_'.Echolot::Globals::get()->{'internal_counter'}++.'.'.Echolot::Globals::get()->{'hostname'}; + return $result; +}; + +sub sane_move($$) { + my ($from, $to) = @_; + + my $link_success = link($from, $to); + $link_success or + Echolot::Log::warn("Cannot link $from to $to: $!."), + return 0; + #- Trying move"), + #rename($from, $to) or + # cluck("Renaming $from to $to didn't work either: $!"), + # return 0; + + $link_success && (unlink($from) or + Echolot::Log::warn("Cannot unlink $from: $!.") ); + return 1; +}; + +sub handle($) { + my ($lines) = @_; + + my $i=0; + my $body = ''; + my $header = ''; + my $to; + for ( ; $i < scalar @$lines; $i++) { + my $line = $lines->[$i]; + chomp($line); + last if $line eq ''; + $header .= $line."\n"; + + if ($line =~ m/^To:\s*(.*?)\s*$/) { + $to = $1; + }; + }; + for ( ; $i < scalar @$lines; $i++) { + $body .= $lines->[$i]; + }; + + (defined $to) or + Echolot::Log::info("No To header found in mail."), + return 0; + + my $address_result = Echolot::Tools::verify_address_tokens($to) or + Echolot::Log::debug("Verifying '$to' failed."), + return 0; + + my $type = $address_result->{'token'}; + my $timestamp = $address_result->{'timestamp'}; + + Echolot::Conf::remailer_conf($body, $type, $timestamp), return 1 if ($type =~ /^conf\./); + Echolot::Conf::remailer_key($body, $type, $timestamp), return 1 if ($type =~ /^key\./); + Echolot::Conf::remailer_help($body, $type, $timestamp), return 1 if ($type =~ /^help\./); + Echolot::Conf::remailer_stats($body, $type, $timestamp), return 1 if ($type =~ /^stats\./); + Echolot::Conf::remailer_adminkey($body, $type, $timestamp), return 1 if ($type =~ /^adminkey\./); + + Echolot::Pinger::receive($header, $body, $type, $timestamp), return 1 if ($type eq 'ping'); + Echolot::Chain::receive($header, $body, $type, $timestamp), return 1 if ($type eq 'chainping'); + + Echolot::Log::warn("Didn't know what to do with '$to'."), + return 0; +}; + +sub handle_file($) { + my ($file) = @_; + + open (FH, $file) or + Echolot::Log::warn("Cannot open file $file: $!,"), + return 0; + my @lines = <FH>; + my $body = join('', <FH>); + close (FH) or + Echolot::Log::warn("Cannot close file $file: $!."); + + return handle(\@lines); +}; + +sub read_mbox($) { + my ($file) = @_; + + my @mail; + my $mail = []; + my $blank = 1; + + open(FH, '+<'. $file) or + Echolot::Log::warn("cannot open '$file': $!."), + return undef; + flock(FH, LOCK_EX) or + Echolot::Log::warn("cannot gain lock on '$file': $!."), + return undef; + + while(<FH>) { + if($blank && /\AFrom .*\d{4}/) { + push(@mail, $mail) if scalar(@{$mail}); + $mail = [ $_ ]; + $blank = 0; + } else { + $blank = m#\A\Z# ? 1 : 0; + push @$mail, $_; + } + } + push(@mail, $mail) if scalar(@{$mail}); + + seek(FH, 0, SEEK_SET) or + Echolot::Log::warn("cannot seek to start of '$file': $!."), + return undef; + truncate(FH, 0) or + Echolot::Log::warn("cannot truncate '$file' to zero size: $!."), + return undef; + flock(FH, LOCK_UN) or + Echolot::Log::warn("cannot release lock on '$file': $!."), + return undef; + close(FH); + + return \@mail; +} + +sub read_maildir($) { + my ($dir) = @_; + + my @mail; + + my @files; + for my $sub (qw{new cur}) { + opendir(DIR, $dir.'/'.$sub) or + Echolot::Log::warn("Cannot open direcotry '$dir/$sub': $!."), + return undef; + push @files, map { $sub.'/'.$_ } grep { ! /^\./ } readdir(DIR); + closedir(DIR) or + Echolot::Log::warn("Cannot close direcotry '$dir/$sub': $!."); + }; + + for my $file (@files) { + $file =~ /^(.*)$/s or + Echolot::Log::confess("I really should match here. ('$file')."); + $file = $1; + + my $mail = []; + open(FH, $dir.'/'.$file) or + Echolot::Log::warn("cannot open '$dir/$file': $!."), + return undef; + @$mail = <FH>; + close(FH); + + push @mail, $mail; + }; + + for my $file (@files) { + unlink $dir.'/'.$file or + Echolot::Log::warn("cannot unlink '$dir/$file': $!."); + }; + + + return \@mail; +} + +sub storemail($$) { + my ($path, $mail) = @_; + + my $tmpname = $path.'/tmp/'.make_sane_name(); + open (F, '>'.$tmpname) or + Echolot::Log::warn("Cannot open $tmpname: $!."), + return undef; + print F join ('', @$mail); + close F; + + my $i; + for ($i = 0; $i < 5; $i++ ) { + my $targetname = $path.'/cur/'.make_sane_name(); + sane_move($tmpname, $targetname) or + sleep 1, next; + last; + }; + + return undef if ($i == 5); + return 1; +}; + +sub process() { + my $inmail = Echolot::Config::get()->{'mailin'}; + my $mailerrordir = Echolot::Config::get()->{'mailerrordir'}; + + my $mails = (-d $inmail) ? + read_maildir($inmail) : + ( ( -e $inmail ) ? read_mbox($inmail) : [] ); + + Echolot::Globals::get()->{'storage'}->delay_commit(); + for my $mail (@$mails) { + unless (handle($mail)) { + if (Echolot::Config::get()->{'save_errormails'}) { + Echolot::Log::info("Saving mail with unknown destination (probably a bounce) to mail-errordir."); + my $name = make_sane_name(); + storemail($mailerrordir, $mail) or + Echolot::Log::warn("Could not store a mail."); + } else { + Echolot::Log::info("Trashing mail with unknown destination (probably a bounce)."); + }; + }; + }; + Echolot::Globals::get()->{'storage'}->enable_commit(); +}; + +1; + +# vim: set ts=4 shiftwidth=4: diff --git a/trunk/Echolot/Pinger.pm b/trunk/Echolot/Pinger.pm new file mode 100644 index 0000000..d782edd --- /dev/null +++ b/trunk/Echolot/Pinger.pm @@ -0,0 +1,211 @@ +package Echolot::Pinger; + +# +# $Id$ +# +# This file is part of Echolot - a Pinger for anonymous remailers. +# +# Copyright (c) 2002, 2003, 2004 Peter Palfrader <peter@palfrader.org> +# +# This program is free software. you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +=pod + +=head1 Name + +Echolot::Pinger - actual sending and receiving of Pings. + +=head1 DESCRIPTION + +This package provides functions for sending out and receiving pings. + +=cut + +use strict; +use English; +use Echolot::Log; +use Echolot::Pinger::Mix; +use Echolot::Pinger::CPunk; + +sub do_mix_ping($$$$$$) { + my ($address, $type, $keyid, $to, $with_from, $body) = @_; + + ($type eq 'mix') or + Echolot::Log::warn("types should really be mix ($type)."), + return 0; + + my %key = Echolot::Globals::get()->{'storage'}->get_key($address, $type, $keyid); + Echolot::Pinger::Mix::ping( + $body, + $to, + $with_from, + [ $key{'nick'} ], + { $keyid => \%key } ) or + return 0; + + return 1; +}; + +sub do_cpunk_ping($$$$$$) { + my ($address, $type, $keyid, $to, $with_from, $body) = @_; + + my $keyhash = {}; + if ($type ne 'cpunk-clear') { + my %key = Echolot::Globals::get()->{'storage'}->get_key($address, $type, $keyid); + $keyhash->{$keyid} = \%key; + }; + Echolot::Pinger::CPunk::ping( + $body, + $to, + $with_from, + [ { address => $address, + keyid => $keyid, + encrypt => ($type ne 'cpunk-clear'), + pgp2compat => ($type eq 'cpunk-rsa') } ], + $keyhash ) or + return 0; + + return 1; +}; + +sub do_ping($$$$) { + my ($type, $address, $key, $with_from) = @_; + + my $now = time(); + my $token = join(':', $address, $type, $key, $with_from, $now); + my $mac = Echolot::Tools::make_mac($token); + my $body = "remailer: $address\n". + "type: $type\n". + "key: $key\n". + "with_from: $with_from\n". + "sent: $now\n". + "mac: $mac\n". + Echolot::Tools::make_garbage(); + $body = Echolot::Tools::crypt_symmetrically($body, 'encrypt'); + + my $to = Echolot::Tools::make_address('ping'); + if ($type eq 'mix') { + do_mix_ping($address, $type, $key, $to, $with_from, $body); + } elsif ($type eq 'cpunk-rsa' || $type eq 'cpunk-dsa' || $type eq 'cpunk-clear') { + do_cpunk_ping($address, $type, $key, $to, $with_from, $body); + } else { + Echolot::Log::warn("Don't know how to handle ping type $type."); + return 0; + }; + + Echolot::Globals::get()->{'storage'}->register_pingout($address, $type, $key, $now); + return 1; +}; + +sub send_pings($;$) { + my ($scheduled_for, $which) = @_; + + $which = '' unless defined $which; + + my $call_intervall = Echolot::Config::get()->{'pinger_interval'}; + my $send_every_n_calls = Echolot::Config::get()->{'ping_every_nth_time'}; + + my $timemod = int ($scheduled_for / $call_intervall); + my $this_call_id = $timemod % $send_every_n_calls; + my $session_id = int ($scheduled_for / ($call_intervall * $send_every_n_calls)); + + my @remailers = Echolot::Globals::get()->{'storage'}->get_addresses(); + for my $remailer (@remailers) { + next unless $remailer->{'pingit'}; + my $address = $remailer->{'address'}; + + next unless ( + $which eq 'all' || + $which eq $address || + $which eq ''); + + for my $type (Echolot::Globals::get()->{'storage'}->get_types($address)) { + next unless Echolot::Config::get()->{'do_pings'}->{$type}; + for my $key (Echolot::Globals::get()->{'storage'}->get_keys($address, $type)) { + next unless ( + $which eq $address || + $which eq 'all' || + (($which eq '') && ($this_call_id eq (Echolot::Tools::makeShortNumHash($address.$type.$key.$session_id) % $send_every_n_calls)))); + + my $with_from = (int($timemod / $send_every_n_calls)) % 2; + Echolot::Log::debug("ping calling $type, $address, $key, $with_from."); + do_ping($type, $address, $key, $with_from); + } + }; + }; + return 1; +}; + + +sub receive($$$$) { + my ($header, $msg, $token, $timestamp) = @_; + + my $now = time(); + + my $body; + my $bot = 0; + my $top = 0; + # < 2.0beta34 didn't encrypt pings. + if ($msg =~ /^-----BEGIN PGP MESSAGE-----/m) { + # work around borken middleman remailers that have a problem with some + # sort of end of line characters and randhopping them through reliable + # remailers.. + # they add an empty line between each usefull line + $msg =~ s/(\r?\n)\r?\n/$1/g if ($msg =~ /^-----BEGIN PGP MESSAGE-----\r?\n\r?\n/m); + + $top = ($msg =~ m/^\S.*-----BEGIN PGP MESSAGE-----/ms) ? 1 : 0; + $bot = ($msg =~ m/^-----END PGP MESSAGE-----.*\S/ms) ? 1 : 0; + + $body = Echolot::Tools::crypt_symmetrically($msg, 'decrypt'); + }; + $body = $msg unless defined $body; + + my ($addr) = $body =~ /^remailer: (.*)$/m; + my ($type) = $body =~ /^type: (.*)$/m; + my ($key) = $body =~ /^key: (.*)$/m; + my ($sent) = $body =~ /^sent: (.*)$/m; + my ($with_from) = $body =~ /^with_from: (.*)$/m; + my ($mac) = $body =~ /^mac: (.*)$/m; + + my @values = ($addr, $type, $key, defined $with_from ? $with_from : 'undef', $sent, $mac); # undef was added after 2.0.10 + my $cleanstring = join ":", map { defined() ? $_ : "undef" } @values; + my @values_obsolete = ($addr, $type, $key, $sent, $mac); # <= 2.0.10 + + (grep { ! defined() } @values_obsolete) and + Echolot::Log::warn("Received ping at $timestamp has undefined values: $cleanstring."), + return 0; + + pop @values; + pop @values_obsolete; + Echolot::Tools::verify_mac(join(':', @values), $mac) or + Echolot::Tools::verify_mac(join(':', @values_obsolete), $mac) or # old style without with_from + Echolot::Log::warn("Received ping at $timestamp has wrong mac; $cleanstring."), + return 0; + + Echolot::Globals::get()->{'storage'}->register_pingdone($addr, $type, $key, $sent, $now - $sent) or + return 0; + + if (defined $with_from) { # <= 2.0.10 didn't have with_from + my ($from) = $header =~ /From: (.*)/i; + $from = 'undefined' unless defined $from; + Echolot::Globals::get()->{'storage'}->register_fromline($addr, $type, $with_from, $from, $top, $bot); + }; + + return 1; +}; + +1; +# vim: set ts=4 shiftwidth=4: diff --git a/trunk/Echolot/Pinger/CPunk.pm b/trunk/Echolot/Pinger/CPunk.pm new file mode 100644 index 0000000..6cac797 --- /dev/null +++ b/trunk/Echolot/Pinger/CPunk.pm @@ -0,0 +1,205 @@ +package Echolot::Pinger::CPunk; + +# +# $Id$ +# +# This file is part of Echolot - a Pinger for anonymous remailers. +# +# Copyright (c) 2002, 2003, 2004 Peter Palfrader <peter@palfrader.org> +# +# This program is free software. you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +=pod + +=head1 Name + +Echolot::Pinger::CPunk - send cypherpunk pings + +=head1 DESCRIPTION + +This package provides functions for sending cypherpunk (type I) pings. + +=cut + +use strict; +use English; +use GnuPG::Interface; +use Echolot::Log; + +sub encrypt_to($$$$) { + my ($msg, $recipient, $keys, $pgp2compat) = @_; + + (defined $keys->{$recipient}) or + Echolot::Log::warn("Key for recipient $recipient is not defined."), + return undef; + (defined $keys->{$recipient}->{'key'}) or + Echolot::Log::warn("Key->key for recipient $recipient is not defined."), + return undef; + my $keyring = Echolot::Config::get()->{'tmpdir'}.'/'. + Echolot::Globals::get()->{'hostname'}.".".time.'.'.$PROCESS_ID.'_'.Echolot::Globals::get()->{'internalcounter'}++.'.keyring'; + + my $GnuPG = new GnuPG::Interface; + $GnuPG->call( Echolot::Config::get()->{'gnupg'} ) if (Echolot::Config::get()->{'gnupg'}); + $GnuPG->options->hash_init( + homedir => Echolot::Config::get()->{'gnupghome'} ); + $GnuPG->options->meta_interactive( 0 ); + + my ( $stdin_fh, $stdout_fh, $stderr_fh, $status_fh, $handles ) = Echolot::Tools::make_gpg_fds(); + my $pid = $GnuPG->wrap_call( + commands => [ '--import' ], + command_args => [qw{--no-options --no-secmem-warning --no-default-keyring --fast-list-mode --keyring}, $keyring, '--', '-' ], + handles => $handles ); + my ($stdout, $stderr, $status) = Echolot::Tools::readwrite_gpg($keys->{$recipient}->{'key'}, $stdin_fh, $stdout_fh, $stderr_fh, $status_fh); + waitpid $pid, 0; + + ($stdout eq '') or + Echolot::Log::info("GnuPG returned something in stdout '$stdout' while adding key for '$recipient': So what?"); + #($stderr eq '') or + #Echolot::Log::warn("GnuPG returned something in stderr: '$stderr' while adding key for '$recipient'; returning."), + #return undef; + ($status =~ /^^\[GNUPG:\] IMPORTED $recipient /m) or + Echolot::Log::info("GnuPG status '$status' didn't indicate key for '$recipient' was imported correctly."), + return undef; + + + + + + + #$msg =~ s/\r?\n/\r\n/g; + + + + + $GnuPG->options->hash_init( + armor => 1 ); + + ( $stdin_fh, $stdout_fh, $stderr_fh, $status_fh, $handles ) = Echolot::Tools::make_gpg_fds(); + my $command_args = [qw{--no-options --no-secmem-warning --always-trust --no-default-keyring --textmode --cipher-algo 3DES --keyring}, $keyring, '--recipient', $recipient]; + my $plaintextfile; + + #if ($pgp2compat) { + # push @$command_args, qw{--pgp2}; + #}; + # Files are required for compaitibility with PGP 2.* + # we also use files in all other cases since there is a bug in either GnuPG or GnuPG::Interface + # that let Echolot die if in certain cases: + # If a key is unuseable because it expired and we want to encrypt something to it + # pingd dies if there is only enough time between calling encrypt() and printing the message + # to GnuPG. (a sleep 1 triggered that reproduceably) + $plaintextfile = Echolot::Config::get()->{'tmpdir'}.'/'. + Echolot::Globals::get()->{'hostname'}.".".time.'.'.$PROCESS_ID.'_'.Echolot::Globals::get()->{'internalcounter'}++.'.plaintext'; + open (F, '>'.$plaintextfile) or + Echolot::Log::warn("Cannot open $plaintextfile for writing: $!."), + return 0; + print (F $msg); + close (F) or + Echolot::Log::warn("Cannot close $plaintextfile."), + return 0; + push @$command_args, $plaintextfile; + + $pid = $GnuPG->encrypt( + command_args => $command_args, + handles => $handles ); + ($stdout, $stderr, $status) = Echolot::Tools::readwrite_gpg('', $stdin_fh, $stdout_fh, $stderr_fh, $status_fh); + waitpid $pid, 0; + + #($stderr eq '') or + #Echolot::Log::warn("GnuPG returned something in stderr: '$stderr' while encrypting to '$recipient'."), + #return undef; + ($status =~ /^\[GNUPG:\] KEYEXPIRED (\d+)/m) and + Echolot::Log::info("Key $recipient expired at ".scalar gmtime($1)." UTC"), + return undef; + (($status =~ /^\[GNUPG:\] BEGIN_ENCRYPTION\s/m) && + ($status =~ /^\[GNUPG:\] END_ENCRYPTION\s/m)) or + Echolot::Log::info("GnuPG status '$status' didn't indicate message to '$recipient' was encrypted correctly (stderr: $stderr; args: ".join(' ', @$command_args).")."), + return undef; + + unlink ($keyring) or + Echolot::Log::warn("Cannot unlink tmp keyring '$keyring'."), + return undef; + unlink ($keyring.'~'); # gnupg does those evil backups + + (defined $plaintextfile) and + (unlink ($plaintextfile) or + Echolot::Log::warn("Cannot unlink tmp plaintextfile '$plaintextfile'."), + return undef); + + + my $result; + + $plaintextfile .= '.asc'; + open (F, '<'.$plaintextfile) or + Echolot::Log::warn("Cannot open $plaintextfile for reading: $!."), + return 0; + $result = join '', <F>; + close (F) or + Echolot::Log::warn("Cannot close $plaintextfile."), + return 0; + + (defined $plaintextfile) and + (unlink ($plaintextfile) or + Echolot::Log::warn("Cannot unlink tmp plaintextfile '$plaintextfile'."), + return undef); + + $result =~ s,^Version: .*$,Version: N/A,m; + #$result =~ s/\r?\n/\r\n/g; + return $result; +}; + +sub ping($$$$$) { + my ($body, $to, $with_from, $chain, $keys) = @_; + + my $msg = $body; + + for my $hop (reverse @$chain) { + my $header = ''; + if ($with_from) { + my $address = Echolot::Config::get()->{'my_localpart'} . '@' . + Echolot::Config::get()->{'my_domain'}; + $header = "##\nFrom: Echolot Pinger <$address>\n\n"; + $with_from = 0; + }; + # "Latent-Time: +0\n". + $msg = "::\n". + "Anon-To: $to\n". + "\n". + $header. + $msg; + + if ($hop->{'encrypt'}) { + my $encrypted = encrypt_to($msg, $hop->{'keyid'}, $keys, $hop->{'pgp2compat'}); + (defined $encrypted) or + Echolot::Log::debug("Encrypted is undefined."), + return undef; + $msg = "::\n". + "Encrypted: PGP\n". + "\n". + $encrypted; + }; + $to = $hop->{'address'}; + } + + Echolot::Tools::send_message( + To => $to, + Body => $msg + ); + + return 1; +}; + +1; +# vim: set ts=4 shiftwidth=4: diff --git a/trunk/Echolot/Pinger/Mix.pm b/trunk/Echolot/Pinger/Mix.pm new file mode 100644 index 0000000..66eaf25 --- /dev/null +++ b/trunk/Echolot/Pinger/Mix.pm @@ -0,0 +1,139 @@ +package Echolot::Pinger::Mix; + +# +# $Id$ +# +# This file is part of Echolot - a Pinger for anonymous remailers. +# +# Copyright (c) 2002, 2003, 2004 Peter Palfrader <peter@palfrader.org> +# +# This program is free software. you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +=pod + +=head1 Name + +Echolot::Pinger::Mix - send mix pings + +=head1 DESCRIPTION + +This package provides functions for sending mixmaster (type II) pings. + +=cut + +use strict; +use English; +use IO::Handle; +use Echolot::Log; + +sub ping($$$$$) { + my ($body, $to, $with_from, $chain, $keys) = @_; + + my $chaincomma = join (',', @$chain); + + my $keyring = Echolot::Config::get()->{'mixhome'}.'/pubring.mix'; + open (F, '>'.$keyring) or + Echolot::Log::warn("Cannot open $keyring for writing: $!."), + return 0; + for my $keyid (keys %$keys) { + print (F $keys->{$keyid}->{'summary'}, "\n\n"); + print (F $keys->{$keyid}->{'key'},"\n\n"); + }; + close (F) or + Echolot::Log::warn("Cannot close $keyring: $!."), + return 0; + + my $type2list = Echolot::Config::get()->{'mixhome'}.'/type2.list'; + open (F, '>'.$type2list) or + Echolot::Log::warn("Cannot open $type2list for writing: $!."), + return 0; + for my $keyid (keys %$keys) { + print (F $keys->{$keyid}->{'summary'}, "\n"); + }; + close (F) or + Echolot::Log::warn("Cannot close $type2list: $!."), + return 0; + + my $mixcfg = Echolot::Config::get()->{'mixhome'}.'/mix.cfg'; + my $address = Echolot::Config::get()->{'my_localpart'} . '@' . + Echolot::Config::get()->{'my_domain'}; + my $sendmail = Echolot::Config::get()->{'sendmail'}; + open (F, ">$mixcfg") or + Echolot::Log::warn("Cannot open $mixcfg for writing: $!."), + return 0; + print (F "REMAIL n\n"); + print (F "NAME Echolot Pinger\n"); + print (F "ADDRESS $address\n"); + print (F "PUBRING pubring.mix\n"); + print (F "TYPE2LIST type2.list\n"); + print (F "SENDMAIL $sendmail -f $address -t\n"); + print (F "VERBOSE 0\n"); + print (F "INDUMMYP 0\n"); + print (F "OUTDUMMYP 0\n"); + close (F) or + Echolot::Log::warn("Cannot close $mixcfg: $!."), + return 0; + + my ($stdinR, $stdinW) = (IO::Handle->new(), IO::Handle->new()); + my ($stdoutR, $stdoutW) = (IO::Handle->new(), IO::Handle->new()); + my ($stderrR, $stderrW) = (IO::Handle->new(), IO::Handle->new()); + pipe $stdinR, $stdinW; + pipe $stdoutR, $stdoutW; + pipe $stderrR, $stderrW; + my $pid = fork(); + defined $pid or + Echolot::Log::warn("Cannot fork for calling mixmaster: $!."), + return 0; + unless ($pid) { # child + $stdinW->close; + $stdoutR->close; + $stderrR->close; + close STDIN; + close STDOUT; + close STDERR; + open (STDIN, "<&".$stdinR->fileno) or Echolot::Log::warn ("Cannot dup stdinR (fd ".$stdinR->fileno.") as STDIN: $!"); + open (STDOUT, ">&".$stdoutW->fileno) or Echolot::Log::warn ("Cannot dup stdoutW (fd ".$stdoutW->fileno.") as STDOUT: $!"); + open (STDERR, ">&".$stderrW->fileno) or Echolot::Log::warn ("Cannot dup stderrW (fd ".$stderrW->fileno.") as STDERE: $!"); + $ENV{'MIXPATH'} = Echolot::Config::get()->{'mixhome'}; + { exec(Echolot::Config::get()->{'mixmaster'}, qw{-m -S -l}, $chaincomma); }; + Echolot::Log::warn("Cannot exec mixpinger: $!."); + exit(1); + }; + $stdinR->close; + $stdoutW->close; + $stderrW->close; + + my $msg; + $msg .= "From: Echolot Pinger <$address>\n" if $with_from; + $msg .= "To: $to\n\n$body\n"; + + Echolot::Log::debug("mixping: fds: stdinW $stdinW; stdoutR $stdoutR; stderrR $stderrR."), + my ($stdout, $stderr, undef) = Echolot::Tools::readwrite_gpg($msg, $stdinW, $stdoutR, $stderrR, undef); + waitpid $pid, 0; + + $stderr =~ s/\n+$//; + Echolot::Log::debug("Mixmaster said on unfiltered stderr: $stderr") if ($stderr ne ''); + $stderr =~ s/^Chain: .*//mg; + $stderr =~ s/^Warning: The message has a From: line.*//mg; + $stderr =~ s/\n+$//; + Echolot::Log::info("Mixmaster said on stdout: $stdout") if ($stdout ne ''); + Echolot::Log::warn("Mixmaster said on stderr: $stderr") if ($stderr ne ''); + + return 1; +}; + +1; +# vim: set ts=4 shiftwidth=4: diff --git a/trunk/Echolot/Report.pm b/trunk/Echolot/Report.pm new file mode 100644 index 0000000..1c2a4ae --- /dev/null +++ b/trunk/Echolot/Report.pm @@ -0,0 +1,70 @@ +package Echolot::Report; + +# +# $Id$ +# +# This file is part of Echolot - a Pinger for anonymous remailers. +# +# Copyright (c) 2002, 2003, 2004 Peter Palfrader <peter@palfrader.org> +# +# This program is free software. you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +=pod + +=head1 Name + +Echolot::Report - Summarize status of remailers + +=head1 DESCRIPTION + +This package prints the summary of remailers/addresses. + +=cut + +use strict; +use English; +use Echolot::Log; + +sub print_summary(;$) { + my ($manual) = @_; + + my @addresses = sort { $a->{'address'} cmp $b->{'address'} } Echolot::Globals::get()->{'storage'}->get_addresses(); + my $report = "*** Status summary ***\n"; + + for my $remailer (@addresses) { + my $addr = $remailer->{'address'}; + $report .= "$addr (ID: $remailer->{'id'}): ".uc($remailer->{'status'})."; ". + "Fetch/Ping/Show: ". + ($remailer->{'fetch'} ? '1' : '0') . + ($remailer->{'pingit'} ? '1' : '0') . + ($remailer->{'showit'} ? '1' : '0') . + "; TTL: $remailer->{'ttl'}\n"; + $report .= " Resurection TTL: $remailer->{'resurrection_ttl'}\n" if (defined $remailer->{'resurrection_ttl'} && ($remailer->{'status'} eq 'ttl timeout')); + for my $type (Echolot::Globals::get()->{'storage'}->get_types($addr)) { + $report .= " Type: $type: ".join(', ', Echolot::Globals::get()->{'storage'}->get_keys($addr, $type))."\n"; + }; + }; + if (defined $manual) { + Echolot::Log::notice($report); + } else { + Echolot::Log::info($report); + } + + return 1; +}; + +1; +# vim: set ts=4 shiftwidth=4: diff --git a/trunk/Echolot/Scheduler.pm b/trunk/Echolot/Scheduler.pm new file mode 100644 index 0000000..32d859f --- /dev/null +++ b/trunk/Echolot/Scheduler.pm @@ -0,0 +1,196 @@ +package Echolot::Scheduler; + +# +# $Id$ +# +# This file is part of Echolot - a Pinger for anonymous remailers. +# +# Copyright (c) 2002, 2003, 2004 Peter Palfrader <peter@palfrader.org> +# +# This program is free software. you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +=pod + +=head1 Name + +Echolot::Scheduler - Task selector/scheduler for echolot + +=head1 DESCRIPTION + +This package provides several functions for scheduling tasks within +the ping daemon. + +=over + +=cut + +use strict; +use English; +use Echolot::Log; + +my $ORDER = 1; + +=item B<new> () + +Creates a new scheduler object. + +=cut +sub new { + my ($class, %params) = @_; + my $self = {}; + bless $self, $class; + return $self; +}; + +=item B<add> (I<name>, I<interval>, I<offset>, I<missok>, I<what>) + +Adds a task with I<name> to the list of tasks. Every I<interval> seconds +I<what> is called. If for example I<interval> is 3600 - meaning I<what> +should be executed hourly - setting I<offset> to 600 would mean that +it get's called 10 minutes after the hour. + +I<missok> indicates that it is ok to miss one run of this job. This can happen +if we run behind schedule for instance. + +=cut +sub add($$$$$$) { + my ($self, $name, $interval, $offset, $missok, $what) = @_; + + Echolot::Log::logdie("Must not add zero intervall for job $name.") + unless $interval; + + if (defined $self->{'tasks'}->{$name}) { + @{ $self->{'schedule'} } = grep { $_->{'name'} ne $name } @{ $self->{'schedule'} }; + }; + + $self->{'tasks'}->{$name} = + { + interval => $interval, + offset => $offset, + what => $what, + order => $ORDER++, + missok => $missok, + }; + + $self->schedule($name, 1); + + return 1; +}; + +=item B<schedule> (I<name>, I<reschedule>, [ I<for>, [I<arguments>]] ) + +Schedule execution of I<name> for I<for>. If I<for> is not given it is calculated +from I<interval> and I<offset> passed to B<new>. if I<reschedule> is set +the task will be rescheduled when it's done (according to its interval). +You may also give arguments to passed to the task. + +=cut +sub schedule($$$;$$) { + my ($self, $name, $reschedule, $for, $arguments) = @_; + + (defined $self->{'tasks'}->{$name}) or + Echolot::Log::warn("Task $name is not defined."), + return 0; + + my $interval = $self->{'tasks'}->{$name}->{'interval'}; + my $offset = $self->{'tasks'}->{$name}->{'offset'}; + + + unless (defined $for) { + ($interval < 0) and + return 1; + my $now = time(); + $for = $now - $now % $interval + $offset; + ($for <= $now) and $for += $interval; + my $cnt = 0; + while ($self->{'tasks'}->{$name}->{'missok'} && ($for <= $now)) { + $for += $interval; + $cnt ++; + }; + Echolot::Log::debug("Skipping $cnt runs of $name.") if $cnt; + }; + + $arguments = [] unless defined $arguments; + + push @{ $self->{'schedule'} }, + { + start => $for, + order => $self->{'tasks'}->{$name}->{'order'}, + name => $name, + arguments => $arguments, + reschedule => $reschedule + }; + + @{ $self->{'schedule'} } = sort { $a->{'start'} <=> $b->{'start'} or $a->{'order'} <=> $b->{'order'} } + @{ $self->{'schedule'} }; + + return 1; +}; + +=item B<run> () + +Start the scheduling run. + +It will run forever or until a task with I<what> == 'exit' is executed. + +=cut +sub run($) { + my ($self) = @_; + + (defined $self->{'schedule'}->[0]) or + Echolot::Log::warn("Scheduler is empty."), + return 0; + + while(1) { + my $now = time(); + my $task = $self->{'schedule'}->[0]; + if ($task->{'start'} < $now) { + Echolot::Log::warn("Task $task->{'name'} could not be started on time.") + unless ($task->{'start'} == 0); + } else { + Echolot::Log::debug("zZzZZzz."); + $PROGRAM_NAME = "pingd [sleeping]"; + sleep ($task->{'start'} - $now); + }; + + (time() < $task->{'start'}) and + next; + + $now = $task->{'start'}; + do { + $task = shift @{ $self->{'schedule'} }; + my $name = $task->{'name'}; + $PROGRAM_NAME = "pingd [executing $name]"; + (defined $self->{'tasks'}->{$name}) or + Echolot::Log::cluck("Task $task->{'name'} is not defined."); + + my $what = $self->{'tasks'}->{$name}->{'what'}; + Echolot::Log::debug("Running $name (was scheduled for ".(time()-$now)." seconds ago)."); + last if ($what eq 'exit'); + &$what( $now, @{ $task->{'arguments'} } ); + $self->schedule($name, 1, $now + $self->{'tasks'}->{$name}->{'interval'}) if + ($task->{'reschedule'} && $self->{'tasks'}->{$name}->{'interval'} > 0); + + (defined $self->{'schedule'}->[0]) or + Echolot::Log::warn("Scheduler is empty."), + return 0; + } while ($now >= $self->{'schedule'}->[0]->{'start'}); + }; + + return 1; +}; + +# vim: set ts=4 shiftwidth=4: diff --git a/trunk/Echolot/Stats.pm b/trunk/Echolot/Stats.pm new file mode 100644 index 0000000..852b11b --- /dev/null +++ b/trunk/Echolot/Stats.pm @@ -0,0 +1,983 @@ +package Echolot::Stats; + +# +# $Id$ +# +# This file is part of Echolot - a Pinger for anonymous remailers. +# +# Copyright (c) 2002, 2003, 2004 Peter Palfrader <peter@palfrader.org> +# +# This program is free software. you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +=pod + +=head1 Name + +Echolot::Stats - produce Stats, keyrings et al + +=head1 DESCRIPTION + +This package provides functions for generating remailer stats, +and keyrings. + +=cut + +use strict; +use English; +use Echolot::Log; + +my $STATS_DAYS; +my $SECONDS_PER_DAY; +my $WEIGHT; + +my %LAST_BROKENCHAIN_RUN; +my %BROKEN_CHAINS; + +sub make_date() { + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) = gmtime(); + sprintf("%s %02d %s %4d %02d:%02d:%02d GMT", + Echolot::Tools::make_dayname($wday), + $mday, + Echolot::Tools::make_monthname($mon), + $year + 1900, + $hour, + $min, + $sec); +}; + +sub make_min_hr($$) { + my ($sec, $includesec) = @_; + my ($s, $m, $h); + + if (defined $sec) { + $s = $sec % 60; + $m = $sec / 60 % 60; + $h = int ($sec / 60 / 60); + }; + if ((! defined $sec) || ($sec < 0) || ($h > 99)) { + $h = 99; + $m = 59; + $s = 59; + }; + + if ($includesec) { + if ($h) { return sprintf ("%2d:%02d:%02d", $h, $m, $s); } + elsif ($m) { return sprintf ( " %2d:%02d", $m, $s); } + else { return sprintf ( " %2d", $s); }; + } else { + if ($h) { return sprintf ("%2d:%02d", $h, $m); } + else { return sprintf ( " :%02d", $m); }; + }; +}; + +sub build_list1_latencystr($) { + my ($lat) = @_; + + my $str = '?' x $STATS_DAYS; + for my $day (0 .. $STATS_DAYS - 1) { + substr($str, $STATS_DAYS - 1 - $day, 1) = + (defined $lat->[$day]) ? + ($lat->[$day] < 300 ? '#' : + ($lat->[$day] < 3600 ? '*' : + ($lat->[$day] < 14400 ? '+' : + ($lat->[$day] < 86400 ? '-' : + ($lat->[$day] < 172800 ? '.' : + '_' + ))))) + : ' '; + }; + return $str; +} + +sub build_list2_latencystr($) { + my ($lat) = @_; + + my $str = '?' x $STATS_DAYS; + for my $day (0 .. $STATS_DAYS - 1) { + substr($str, $STATS_DAYS - 1 - $day, 1) = + (defined $lat->[$day]) ? + ($lat->[$day] < 20*60 ? '0' : + ($lat->[$day] < 1*3600 ? '1' : + ($lat->[$day] < 2*3600 ? '2' : + ($lat->[$day] < 3*3600 ? '3' : + ($lat->[$day] < 4*3600 ? '4' : + ($lat->[$day] < 5*3600 ? '5' : + ($lat->[$day] < 6*3600 ? '6' : + ($lat->[$day] < 7*3600 ? '7' : + ($lat->[$day] < 8*3600 ? '8' : + ($lat->[$day] < 9*3600 ? '9' : + ($lat->[$day] < 12*3600 ? 'A' : + ($lat->[$day] < 18*3600 ? 'B' : + ($lat->[$day] < 24*3600 ? 'C' : + ($lat->[$day] < 30*3600 ? 'D' : + ($lat->[$day] < 36*3600 ? 'E' : + ($lat->[$day] < 42*3600 ? 'F' : + ($lat->[$day] < 48*3600 ? 'G' : + 'H' + ))))))))))))))))) + : '?'; + }; + return $str; +} + +sub build_list2_reliabilitystr($) { + my ($rel) = @_; + + my $str = '?' x $STATS_DAYS; + for my $day (0 .. $STATS_DAYS - 1) { + substr($str, $STATS_DAYS - 1 - $day, 1) = + (defined $rel->[$day]) ? + (($rel->[$day] >= 0.9999) ? + #(($rel->[$day] == 1) ? + '+' : + (int ($rel->[$day]*10))) + : '?'; + }; + return $str; +} + +sub build_list2_capsstr($) { + my ($caps) = @_; + + my %caps; + $caps{'middle'} = ($caps =~ m/\bmiddle\b/i); + $caps{'post'} = ($caps =~ m/\bpost\b/i) || ($caps =~ m/\banon-post-to\b/i); + $caps{'mix'} = ($caps =~ m/\bmix\b/i); + $caps{'remix'} = ($caps =~ m/\bremix\b/i); + $caps{'remix2'} = ($caps =~ m/\bremix2\b/i); + $caps{'hybrid'} = ($caps =~ m/\bhybrid\b/i); + $caps{'repgp2'} = ($caps =~ m/\brepgp2\b/i); + $caps{'repgp'} = ($caps =~ m/\brepgp\b/i); + $caps{'pgponly'} = ($caps =~ m/\bpgponly\b/i); + $caps{'ext'} = ($caps =~ m/\bext\b/i); + $caps{'max'} = ($caps =~ m/\bmax\b/i); + $caps{'test'} = ($caps =~ m/\btest\b/i); + $caps{'latent'} = ($caps =~ m/\blatent\b/i); + $caps{'ek'} = ($caps =~ m/\bek\b/i); + $caps{'ekx'} = ($caps =~ m/\bekx\b/i); + $caps{'esub'} = ($caps =~ m/\besub\b/i); + $caps{'inflt'} = ($caps =~ m/\binflt\d+\b/i); + $caps{'rhop'} = ($caps =~ m/\brhop\d+\b/i); + ($caps{'klen'}) = ($caps =~ m/\bklen(\d+)\b/i); + + my $str = + ($caps{'middle'} ? 'D' : ' ') . + ($caps{'post'} ? 'P' : ' ') . + ($caps{'remix2'} ? '2' : ($caps{'remix'} ? 'R' : ($caps{'mix'} ? 'M' : ' ' ))) . + ($caps{'hybrid'} ? 'H' : ' ') . + ($caps{'repgp2'} ? '2' : ($caps{'repgp'} ? 'G' : ' ' )) . + ($caps{'pgponly'} ? 'O' : ' ') . + ($caps{'ext'} ? 'X' : ' ') . + ($caps{'max'} ? 'A' : ' ') . + ($caps{'test'} ? 'T' : ' ') . + ($caps{'latent'} ? 'L' : ' ') . + ($caps{'ekx'} ? 'E' : ($caps{'ek'} ? 'e' : ' ' )) . + ($caps{'esub'} ? 'U' : ' ') . + ($caps{'inflt'} ? 'I' : ' ') . + ($caps{'rhop'} ? 'N' : ' ') . + (defined $caps{'klen'} ? + ($caps{'klen'} >= 900 ? '9' : ( + $caps{'klen'} >= 800 ? '8' : ( + $caps{'klen'} >= 700 ? '7' : ( + $caps{'klen'} >= 600 ? '6' : ( + $caps{'klen'} >= 500 ? '5' : ( + $caps{'klen'} >= 400 ? '4' : ( + $caps{'klen'} >= 300 ? '3' : ( + $caps{'klen'} >= 200 ? '2' : ( + $caps{'klen'} >= 100 ? '1' : '0' + ))))))))) + : ' '); + return $str; +} + +sub median($) { + my ($arr) = @_; + + my $cnt = scalar @$arr; + if ($cnt == 0) { + return undef; + } elsif ($cnt % 2 == 0) { + return (($arr->[ int(($cnt - 1 ) / 2) ] + $arr->[ int($cnt / 2) ] ) / 2); + } else { + return $arr->[ int(($cnt - 1 ) / 2) ]; + }; +}; + +# how many % (0-1) values of @$lats are greater than $lat. +# $@lats needs to be sorted +sub percentile($$) { + my ($lat, $lats) = @_; + + my $num = scalar @$lats; + my $i; + for ($i=0; $i < $num; $i++) { + last if $lat < $lats->[$i]; + } + return ($num - $i) / $num; +} + +sub calculate($$) { + my ($addr, $types) = @_; + my $now = time(); + + my $SKEW_ABS = 15*60; + my $SKEW_PERCENT = 0.80; + + my @out; + my @done; + + for my $type (@$types) { + next unless Echolot::Globals::get()->{'storage'}->has_type($addr, $type); + my @keys = Echolot::Globals::get()->{'storage'}->get_keys($addr, $type); + for my $key (@keys) { + push @out, grep {$_ > $now - $STATS_DAYS * $SECONDS_PER_DAY} Echolot::Globals::get()->{'storage'}->get_pings($addr, $type, $key, 'out'); + push @done, grep {$_->[0] > $now - $STATS_DAYS * $SECONDS_PER_DAY} Echolot::Globals::get()->{'storage'}->get_pings($addr, $type, $key, 'done'); + }; + }; + + my @latency_total = map { $_->[1] } @done; + my @latency_day; + my $sent_total; + my $received_total = 0; + my @sent_day; + my @received_day; + for my $done (@done) { + push @{ $latency_day [int(($now - $done->[0]) / $SECONDS_PER_DAY)] }, $done->[1]; + my $day = int(($now - $done->[0]) / $SECONDS_PER_DAY); + my $weight = $WEIGHT->[$day]; + $sent_total += $weight; $sent_day [$day] ++; + $received_total += $weight; $received_day[$day] ++; + }; + + @latency_total = sort { $a <=> $b } @latency_total; + my $latency_median = median (\@latency_total); + my @latency_median_day; + for ( 0 .. $STATS_DAYS - 1 ) { + @{$latency_day[$_]} = defined $latency_day[$_] ? (sort { $a <=> $b } @{$latency_day[$_]}) : (); + $latency_median_day[$_] = median ( $latency_day[$_] ); + } + + if (scalar @out) { + my @p = ( scalar @latency_total ) ? + map { #printf(STDERR "($now - $_ - $SKEW_ABS)/$SKEW_PERCENT ". + #"%s in (%s): %s\n", ($now - $_ - $SKEW_ABS)/$SKEW_PERCENT, join(',', @latency_total), + #percentile( ($now - $_ - $SKEW_ABS)/$SKEW_PERCENT , \@latency_total )); + percentile( ($now - $_ - $SKEW_ABS)/$SKEW_PERCENT , \@latency_total ) } @out : + map { 0 } @out; + for (my $i=0; $i < scalar @out; $i++) { + my $day = int(($now - $out[$i]) / $SECONDS_PER_DAY); + my $weight = $WEIGHT->[$day]; + $sent_total += $weight; $sent_day [$day] ++; + $received_total += $weight * $p[$i]; $received_day[$day] += $p[$i]; + }; + }; + #printf STDERR "$received_total / %s\n", (defined $sent_total ? $sent_total : 'n/a'); + $received_total /= $sent_total if ($sent_total); + for ( 0 .. $STATS_DAYS - 1 ) { + $received_day[$_] /= $sent_day[$_] if ($sent_day[$_]); + }; + + + + return { + avr_latency => $latency_median, + avr_reliability => $received_total, + latency_day => \@latency_median_day, + reliability_day => \@received_day + }; +}; + +sub write_file($$$$) { + my ($filebasename, $html_template, $expires, $output) = @_; + + my $filename = $filebasename.'.txt'; + open(F, '>'.$filename) or + Echolot::Log::warn("Cannot open $filename: $!."), + return 0; + print F $output; + close (F); + if (defined $expires) { + Echolot::Tools::write_meta_information($filename, + Expires => time + $expires) or + Echolot::Log::debug ("Error while writing meta information for $filename."), + return 0; + }; + return 1 unless defined $html_template; + + if (defined $output) { + $output =~ s/&/&/g; + $output =~ s/"/"/g; + $output =~ s/</</g; + $output =~ s/>/>/g; + }; + Echolot::Tools::write_HTML_file($filebasename, $html_template, $expires, list => $output); + + return 1; +}; + +sub build_mlist1($$$$$;$) { + my ($rems, $broken1, $broken2, $sameop, $filebasename, $html_template) = @_; + + my $output = ''; + $output .= sprintf "\nGroups of remailers sharing a machine or operator:\n$sameop\n" if (defined $sameop); + $output .= sprintf "\nBroken type-I remailer chains:\n$broken1\n" if (defined $broken1); + $output .= sprintf "\nBroken type-II remailer chains:\n$broken2\n" if (defined $broken2); + + $output .= sprintf "Last update: %s\n", make_date(); + $output .= sprintf "mixmaster history latency uptime\n"; + $output .= sprintf "--------------------------------------------\n"; + + for my $remailer (@$rems) { + $output .= sprintf "%-14s %-12s %8s %6.2f%%\n", + substr($remailer->{'nick'},0,14), + build_list1_latencystr($remailer->{'stats'}->{'latency_day'}), + make_min_hr($remailer->{'stats'}->{'avr_latency'}, 1), + $remailer->{'stats'}->{'avr_reliability'} * 100; + }; + + write_file($filebasename, $html_template, Echolot::Config::get()->{'buildstats'}, $output) or + Echolot::Log::debug("writefile failed."), + return 0; + return 1; +}; + +sub build_rlist1($$$$$;$) { + my ($rems, $broken1, $broken2, $sameop, $filebasename, $html_template) = @_; + + my $output = ''; + for my $remailer (sort {$a->{'caps'} cmp $b->{'caps'}} @$rems) { + $output .= $remailer->{'caps'}."\n" + } + + $output .= sprintf "\nGroups of remailers sharing a machine or operator:\n$sameop\n" if (defined $sameop); + $output .= sprintf "\nBroken type-I remailer chains:\n$broken1\n" if (defined $broken1); + $output .= sprintf "\nBroken type-II remailer chains:\n$broken2\n" if (defined $broken2); + + $output .= sprintf "\n"; + $output .= sprintf "Last update: %s\n", make_date(); + $output .= sprintf "remailer email address history latency uptime\n"; + $output .= sprintf "-----------------------------------------------------------------------\n"; + + for my $remailer (@$rems) { + $output .= sprintf "%-8s %-32s %-12s %8s %6.2f%%\n", + substr($remailer->{'nick'},0,8), + substr($remailer->{'address'},0,32), + build_list1_latencystr($remailer->{'stats'}->{'latency_day'}), + make_min_hr($remailer->{'stats'}->{'avr_latency'}, 1), + $remailer->{'stats'}->{'avr_reliability'} * 100; + }; + + + write_file($filebasename, $html_template, Echolot::Config::get()->{'buildstats'}, $output) or + Echolot::Log::debug("writefile failed."), + return 0; + return 1; +}; + + +sub build_list2($$$$$$;$) { + my ($rems, $type, $broken1, $broken2, $sameop, $filebasename, $html_template) = @_; + + my $output = ''; + + $output .= sprintf "Stats-Version: 2.0\n"; + $output .= sprintf "Generated: %s\n", make_date(); + $output .= sprintf "%-12s Latent-Hist Latent Uptime-Hist Uptime Options\n", ($type == 1 ? 'Cypherpunk' : $type == 2 ? 'Mixmaster' : "Type $type"); + $output .= sprintf "------------------------------------------------------------------------\n"; + + for my $remailer (@$rems) { + $output .= sprintf "%-12s %-12s %6s %-12s %5.1f%% %s\n", + substr($remailer->{'nick'},0,12), + build_list2_latencystr($remailer->{'stats'}->{'latency_day'}), + make_min_hr($remailer->{'stats'}->{'avr_latency'}, 0), + build_list2_reliabilitystr($remailer->{'stats'}->{'reliability_day'}), + $remailer->{'stats'}->{'avr_reliability'} * 100, + build_list2_capsstr($remailer->{'caps'}); + }; + + $output .= sprintf "\nGroups of remailers sharing a machine or operator:\n$sameop\n" if (defined $sameop); + $output .= sprintf "\nBroken type-I remailer chains:\n$broken1\n" if (defined $broken1); + $output .= sprintf "\nBroken type-II remailer chains:\n$broken2\n" if (defined $broken2); + + $output .= sprintf "\n\n\nRemailer-Capabilities:\n\n"; + for my $remailer (sort {$a->{'caps'} cmp $b->{'caps'}} @$rems) { + $output .= $remailer->{'caps'}."\n" if defined $remailer->{'caps'}; + } + + write_file($filebasename, $html_template, Echolot::Config::get()->{'buildstats'}, $output) or + Echolot::Log::debug("writefile failed."), + return 0; + return 1; +}; + +sub build_clist($$$$$;$) { + my ($remhash, $broken1, $broken2, $sameop, $filebasename, $html_template) = @_; + + my $output = ''; + + $output .= sprintf "Stats-Version: 2.0.1\n"; + $output .= sprintf "Generated: %s\n", make_date(); + $output .= sprintf "Mixmaster Latent-Hist Latent Uptime-Hist Uptime Options Type\n"; + $output .= sprintf "------------------------------------------------------------------------------------\n"; + + my $all; + for my $type (keys %$remhash) { + for my $remailer (@{$remhash->{$type}}) { + $all->{ $remailer->{'nick'} }->{$type} = $remailer + }; + }; + + for my $nick (sort {$a cmp $b} keys %$all) { + for my $type (sort {$a cmp $b} keys %{$all->{$nick}}) { + $output .= sprintf "%-12s %-12s %6s %-12s %5.1f%% %s %s\n", + $nick, + build_list2_latencystr($all->{$nick}->{$type}->{'stats'}->{'latency_day'}), + make_min_hr($all->{$nick}->{$type}->{'stats'}->{'avr_latency'}, 0), + build_list2_reliabilitystr($all->{$nick}->{$type}->{'stats'}->{'reliability_day'}), + $all->{$nick}->{$type}->{'stats'}->{'avr_reliability'} * 100, + build_list2_capsstr($all->{$nick}->{$type}->{'caps'}), + $type; + }; + }; + + $output .= sprintf "\nGroups of remailers sharing a machine or operator:\n$sameop\n" if (defined $sameop); + $output .= sprintf "\nBroken type-I remailer chains:\n$broken1\n" if (defined $broken1); + $output .= sprintf "\nBroken type-II remailer chains:\n$broken2\n" if (defined $broken2); + + $output .= sprintf "\n\n\nRemailer-Capabilities:\n\n"; + for my $nick (sort {$a cmp $b} keys %$all) { + for my $type (keys %{$all->{$nick}}) { + $output .= $all->{$nick}->{$type}->{'caps'}."\n", last if defined $all->{$nick}->{$type}->{'caps'}; + }; + } + + write_file($filebasename, $html_template, Echolot::Config::get()->{'buildstats'}, $output) or + Echolot::Log::debug("writefile failed."), + return 0; + return 1; +}; + + +sub build_rems($) { + my ($types) = @_; + + my %rems; + for my $remailer (Echolot::Globals::get()->{'storage'}->get_addresses()) { + my $addr = $remailer->{'address'}; + my $has_type = 0; + for my $type (@$types) { + $has_type = 1, last if (Echolot::Globals::get()->{'storage'}->has_type($addr, $type)); + }; + next unless $has_type; + + my $rem = { + 'stats' => calculate($addr,$types), + 'nick' => Echolot::Globals::get()->{'storage'}->get_nick($addr), + 'caps' => Echolot::Globals::get()->{'storage'}->get_capabilities($addr), + 'address' => $addr, + }; + $rem->{'list-it'} = $remailer->{'showit'} && defined $rem->{'caps'} && ($rem->{'caps'} !~ m/\btesting\b/i); + $rem->{'latency'} = $rem->{'stats'}->{'avr_latency'}; # for sorting purposes only + $rem->{'latency'} = 9999 unless defined $rem->{'latency'}; + + $rems{$addr} = $rem if (defined $rem->{'stats'} && defined $rem->{'nick'} && defined $rem->{'address'} && defined $rem->{'caps'} ); + }; + + my $sort_by_latency = Echolot::Config::get()->{'stats_sort_by_latency'}; + my @rems = + sort { + - ($a->{'stats'}->{'avr_reliability'} <=> $b->{'stats'}->{'avr_reliability'}) || + (($a->{'latency'} <=> $b->{'latency'}) * $sort_by_latency) || + ($a->{'nick'} cmp $b->{'nick'}) + } map { $rems{$_} } keys %rems; + + return \@rems; +}; + +sub compress_broken_chain($@) { + my ($num, @list) = @_; + + my %unique = (); + @list = sort { $a cmp $b} grep { ! $unique{$_}++; } @list; + + my %bad_left; + my %bad_right; + for my $chain (@list) { + chomp $chain; + my ($left, $right) = $chain =~ m/\((\S+) \s (\S+)\)/x or + Echolot::Log::warn("Could not parse bad chain '$chain'."), + next; + $bad_right{$right}++; + $bad_right{$right} += $num if ($left eq '*'); + $bad_left {$left }++; + $bad_left {$left } += $num if ($right eq '*'); + }; + + + my $threshold = $num * Echolot::Config::get()->{'chainping_allbad_factor'}; + my @result = (); + for my $key (keys %bad_right) { + delete $bad_right{$key}, next if $bad_right{$key} < $threshold; + push @result, "(* $key)"; + }; + for my $key (keys %bad_left) { + delete $bad_left{$key}, next if $bad_left{$key} < $threshold; + push @result, "($key *)"; + }; + + for my $chain (@list) { + chomp $chain; + my ($left, $right) = $chain =~ m/\((\S+) \s (\S+)\)/x or + # Echolot::Log::warn("Could not parse bad chain '$chain'."), -- don't warn again + push(@result, $chain), + next; + next if defined $bad_right{$right}; + next if defined $bad_left {$left }; + push(@result, $chain), + }; + + %unique = (); + @result = sort { $a cmp $b} grep { ! $unique{$_}++; } @result; + + return @result; +}; + +sub find_broken_chains($$$) { + my ($chaintype, $rems, $hard) = @_; + + if (!defined $LAST_BROKENCHAIN_RUN{$chaintype} || + $LAST_BROKENCHAIN_RUN{$chaintype} < time() - Echolot::Config::get()->{'chainping_update'} || + ! defined $BROKEN_CHAINS{$chaintype} ) { + Echolot::Log::debug ("Broken Chains $chaintype need generating."), + $LAST_BROKENCHAIN_RUN{$chaintype} = time(); + + my $pings = Echolot::Globals::get()->{'storage'}->get_chainpings($chaintype); + my @intensive_care = (); + my %remailers = map { $_->{'address'} => $_ } @$rems; + + my $stats; + my %received; + my @broken_chains; + for my $status (qw{done out}) { + my $status_done = $status eq 'done'; + my $status_out = $status eq 'out'; + for my $ping (@{$pings->{$status}}) { + my $addr1 = $ping->{'addr1'}; + my $addr2 = $ping->{'addr2'}; + my $sent = $ping->{'sent'}; + next if $sent < (time() - Echolot::Config::get()->{'chainping_period'}); + next unless defined $remailers{$addr1}; + next unless defined $remailers{$addr2}; + + if ($status_done) { + $received{$addr1.':'.$addr2.':'.$sent} = 1; + }; + if ($status_out && !defined $received{$addr1.':'.$addr2.':'.$sent}) { + my $lat1 = $remailers{$addr1}->{'stats'}->{'avr_latency'}; + my $lat2 = $remailers{$addr2}->{'stats'}->{'avr_latency'}; + $lat1 = 0 unless defined $lat1; + $lat2 = 0 unless defined $lat2; + my $theoretical_lat = $lat1 + $lat2; + $theoretical_lat = 0 unless defined $theoretical_lat; + my $latency = time() - $ping->{'sent'}; + # print ("lat helps $latency < ".int($theoretical_lat * Echolot::Config::get()->{'chainping_grace'})." $addr1 $addr2\n"), + next if ($latency < $theoretical_lat * Echolot::Config::get()->{'chainping_grace'}); + }; + + # print "Having $addr1 $addr2 $status at $sent\n"; + $stats->{$addr1}->{$addr2}->{$status}++; + }; + }; + # require Data::Dumper; + # print Data::Dumper->Dump([$stats]); + for my $addr1 (keys %$stats) { + for my $addr2 (keys %{$stats->{$addr1}}) { + my $theoretical_rel = $remailers{$addr1}->{'stats'}->{'avr_reliability'} * + $remailers{$addr2}->{'stats'}->{'avr_reliability'}; + my $out = $stats->{$addr1}->{$addr2}->{'out'}; + my $done = $stats->{$addr1}->{$addr2}->{'done'}; + $done = 0 unless defined $done; + ($out < Echolot::Config::get()->{'chainping_minsample'} && $done == 0) and + push (@intensive_care, { addr1 => $addr1, addr2 => $addr2, reason => "only $out sample".($out>1?'s':'').", none returned so far" }), + next; + ($out > 0) or + Echolot::Log::debug("Should not devide through zero ($done/$out) for $addr1, $addr2."), + next; + my $real_rel = $done / $out; + # print "$addr1 $addr2 $done / $out == $real_rel ($theoretical_rel)\n"; + next if ($real_rel > $theoretical_rel * Echolot::Config::get()->{'chainping_fudge'}); + my $nick1 = $remailers{$addr1}->{'nick'}; + my $nick2 = $remailers{$addr2}->{'nick'}; + push @broken_chains, + { public => $remailers{$addr1}->{'list-it'} && $remailers{$addr2}->{'list-it'}, + chain => "($nick1 $nick2)" }; + push @intensive_care, { addr1 => $addr1, addr2 => $addr2, reason => "bad: $done/$out" }; + }; + }; + $BROKEN_CHAINS{$chaintype} = \@broken_chains; + Echolot::Chain::set_intensive_care($chaintype, \@intensive_care); + } else { + Echolot::Log::debug ("Broken Chains $chaintype are up to date."), + }; + + my @hard = defined $hard ? (split /\n/, $hard) : (); + my @pub = @hard; + my @priv = @hard; + push @pub, map { $_->{'chain'} } grep { $_->{'public'} } @{ $BROKEN_CHAINS{$chaintype} }; + push @priv, map { $_->{'chain'} } @{ $BROKEN_CHAINS{$chaintype} }; + + my $pub = join "\n", compress_broken_chain(scalar @$rems, @pub); + my $priv = join "\n", compress_broken_chain(scalar @$rems, @priv); + + return ($pub, $priv); +}; + +sub build_lists() { + + my $clist; + my $pubclist; + my $rems; + my $pubrems; + + my %stats; + my %addresses; + + my $hardbroken1 = Echolot::Tools::read_file( Echolot::Config::get()->{'broken1'}, 1); + my $hardbroken2 = Echolot::Tools::read_file( Echolot::Config::get()->{'broken2'}, 1); + my $sameop = Echolot::Tools::read_file( Echolot::Config::get()->{'sameop'}, 1); + my $pubbroken1; + my $pubbroken2; + my $privbroken1; + my $privbroken2; + + my $mixrems = build_rems(['mix']); + my $cpunkrems = build_rems(['cpunk-rsa', 'cpunk-dsa', 'cpunk-clear']); + + if (Echolot::Config::get()->{'do_chainpings'}) { + ($pubbroken1, $privbroken1) = find_broken_chains('cpunk', $cpunkrems, $hardbroken1); + ($pubbroken2, $privbroken2) = find_broken_chains('mix' , $mixrems , $hardbroken2); + } else { + $pubbroken1 = $privbroken1 = $hardbroken1; + $pubbroken2 = $privbroken2 = $hardbroken2; + }; + + unless (Echolot::Config::get()->{'show_chainpings'}) { + $pubbroken1 = $hardbroken1; + $pubbroken2 = $hardbroken2; + }; + + $rems = $mixrems; + $mixrems = undef; + @$pubrems = grep { $_->{'list-it'} } @$rems; + build_mlist1( $rems, $privbroken1, $privbroken2, $sameop, Echolot::Config::get()->{'private_resultdir'}.'/'.'mlist', 'mlist'); + build_list2( $rems, 2, $privbroken1, $privbroken2, $sameop, Echolot::Config::get()->{'private_resultdir'}.'/'.'mlist2', 'mlist2'); + build_mlist1( $pubrems, $pubbroken1, $pubbroken2, $sameop, Echolot::Config::get()->{'resultdir'}.'/'.'mlist', 'mlist'); + build_list2( $pubrems, 2, $pubbroken1, $pubbroken2, $sameop, Echolot::Config::get()->{'resultdir'}.'/'.'mlist2', 'mlist2'); + $stats{'mix_total'} = scalar @$pubrems; + $stats{'mix_98'} = scalar grep { $_->{'stats'}->{'avr_reliability'} >= 0.98 } @$pubrems; + $addresses{$_->{'address'}}=1 for @$pubrems; + if (Echolot::Config::get()->{'combined_list'}) { + $clist->{'mix'} = $rems; + $pubclist->{'mix'} = $pubrems; $pubrems = undef; + }; + + $rems = $cpunkrems; + $cpunkrems = undef; + @$pubrems = grep { $_->{'list-it'} } @$rems; + build_rlist1( $rems, $privbroken1, $privbroken2, $sameop, Echolot::Config::get()->{'private_resultdir'}.'/'.'rlist', 'rlist'); + build_list2( $rems, 1, $privbroken1, $privbroken2, $sameop, Echolot::Config::get()->{'private_resultdir'}.'/'.'rlist2', 'rlist2'); + build_rlist1( $pubrems, $pubbroken1, $pubbroken2, $sameop, Echolot::Config::get()->{'resultdir'}.'/'.'rlist', 'rlist'); + build_list2( $pubrems, 1, $pubbroken1, $pubbroken2, $sameop, Echolot::Config::get()->{'resultdir'}.'/'.'rlist2', 'rlist2'); + $stats{'cpunk_total'} = scalar @$pubrems; + $stats{'cpunk_98'} = scalar grep { $_->{'stats'}->{'avr_reliability'} >= 0.98 } @$pubrems; + $addresses{$_->{'address'}}=1 for @$pubrems; + if (Echolot::Config::get()->{'combined_list'} && ! Echolot::Config::get()->{'separate_rlists'}) { + $clist->{'cpunk'} = $rems; + $pubclist->{'cpunk'} = $pubrems; $pubrems = undef; + }; + + if (Echolot::Config::get()->{'separate_rlists'}) { + $rems = build_rems(['cpunk-rsa']); + @$pubrems = grep { $_->{'list-it'} } @$rems; + build_rlist1( $rems, $privbroken1, $privbroken2, $sameop, Echolot::Config::get()->{'private_resultdir'}.'/'.'rlist-rsa', 'rlist-rsa'); + build_list2( $rems, 1, $privbroken1, $privbroken2, $sameop, Echolot::Config::get()->{'private_resultdir'}.'/'.'rlist2-rsa', 'rlist2-rsa'); + build_rlist1( $pubrems, $pubbroken1, $pubbroken2, $sameop, Echolot::Config::get()->{'resultdir'}.'/'.'rlist-rsa', 'rlist-rsa'); + build_list2( $pubrems, 1, $pubbroken1, $pubbroken2, $sameop, Echolot::Config::get()->{'resultdir'}.'/'.'rlist2-rsa', 'rlist2-rsa'); + if (Echolot::Config::get()->{'combined_list'}) { + $clist->{'cpunk-rsa'} = $rems; + $pubclist->{'cpunk-rsa'} = $pubrems; $pubrems = undef; + }; + + $rems = build_rems(['cpunk-dsa']); + @$pubrems = grep { $_->{'list-it'} } @$rems; + build_rlist1( $rems, $privbroken1, $privbroken2, $sameop, Echolot::Config::get()->{'private_resultdir'}.'/'.'rlist-dsa', 'rlist-dsa'); + build_list2( $rems, 1, $privbroken1, $privbroken2, $sameop, Echolot::Config::get()->{'private_resultdir'}.'/'.'rlist2-dsa', 'rlist2-dsa'); + build_rlist1( $pubrems, $pubbroken1, $pubbroken2, $sameop, Echolot::Config::get()->{'resultdir'}.'/'.'rlist-dsa', 'rlist-dsa'); + build_list2( $pubrems, 1, $pubbroken1, $pubbroken2, $sameop, Echolot::Config::get()->{'resultdir'}.'/'.'rlist2-dsa', 'rlist2-dsa'); + if (Echolot::Config::get()->{'combined_list'}) { + $clist->{'cpunk-dsa'} = $rems; + $pubclist->{'cpunk-dsa'} = $pubrems; $pubrems = undef; + }; + + $rems = build_rems(['cpunk-clear']); + @$pubrems = grep { $_->{'list-it'} } @$rems; + build_rlist1( $rems, $privbroken1, $privbroken2, $sameop, Echolot::Config::get()->{'private_resultdir'}.'/'.'rlist-clear', 'rlist-clear'); + build_list2( $rems, 1, $privbroken1, $privbroken2, $sameop, Echolot::Config::get()->{'private_resultdir'}.'/'.'rlist2-clear', 'rlist2-clear'); + build_rlist1( $pubrems, $pubbroken1, $pubbroken2, $sameop, Echolot::Config::get()->{'resultdir'}.'/'.'rlist-clear', 'rlist-clear'); + build_list2( $pubrems, 1, $pubbroken1, $pubbroken2, $sameop, Echolot::Config::get()->{'resultdir'}.'/'.'rlist2-clear', 'rlist2-clear'); + if (Echolot::Config::get()->{'combined_list'}) { + $clist->{'cpunk-clear'} = $rems; + $pubclist->{'cpunk-clear'} = $pubrems; $pubrems = undef; + }; + }; + if (Echolot::Config::get()->{'combined_list'}) { + build_clist( $clist, $privbroken1, $privbroken2, $sameop, Echolot::Config::get()->{'private_resultdir'}.'/'.'clist', 'clist'); + build_clist( $pubclist, $pubbroken1, $pubbroken2, $sameop, Echolot::Config::get()->{'resultdir'}.'/'.'clist', 'clist'); + }; + + $stats{'unique_addresses'} = scalar keys %addresses; + Echolot::Tools::write_HTML_file( + Echolot::Config::get()->{'resultdir'}.'/'.Echolot::Config::get()->{'indexfilebasename'}, + 'indexfile', + Echolot::Config::get()->{'buildstats'}, + %stats ); + + my $file = Echolot::Config::get()->{'echolot_css'}, + my $css; + { + local $/ = undef; + open(F, $file) or + Echolot::Log::warn("Could not open $file: $!."), + return 0; + $css = <F>; + close (F) or + Echolot::Log::warn("Cannot close $file: $!."), + return 0; + } + $file = Echolot::Config::get()->{'resultdir'}.'/echolot.css'; + open(F, '>'.$file) or + Echolot::Log::warn("Cannot open $file: $!."), + return 0; + print F $css or + Echolot::Log::warn("Cannot print to $file: $!."), + return 0; + close (F) or + Echolot::Log::warn("Cannot close $file: $!."), + return 0; + +}; + + +sub build_mixring() { + my @filenames; + + my $filename = Echolot::Config::get()->{'resultdir'}.'/pubring.mix'; + push @filenames, $filename; + open(F, '>'.$filename) or + Echolot::Log::warn("Cannot open $filename: $!."), + return 0; + $filename = Echolot::Config::get()->{'resultdir'}.'/type2.list'; + push @filenames, $filename; + open(T2L, '>'.$filename) or + Echolot::Log::warn("Cannot open $filename: $!."), + return 0; + $filename = Echolot::Config::get()->{'private_resultdir'}.'/pubring.mix'; + push @filenames, $filename; + open(F_PRIV, '>'.$filename) or + Echolot::Log::warn("Cannot open $filename: $!."), + return 0; + $filename = Echolot::Config::get()->{'private_resultdir'}.'/type2.list'; + push @filenames, $filename; + open(T2L_PRIV, '>'.$filename) or + Echolot::Log::warn("Cannot open $filename: $!."), + return 0; + + my $data; + for my $remailer (Echolot::Globals::get()->{'storage'}->get_addresses()) { + my $addr = $remailer->{'address'}; + next unless Echolot::Globals::get()->{'storage'}->has_type($addr, 'mix'); + + my %key; + for my $keyid (Echolot::Globals::get()->{'storage'}->get_keys($addr, 'mix')) { + my %new_key = Echolot::Globals::get()->{'storage'}->get_key($addr, 'mix', $keyid); + + if (!defined $key{'last_update'} || $key{'last_update'} < $new_key{'last_update'} ) { + %key = %new_key; + }; + }; + + my $caps = Echolot::Globals::get()->{'storage'}->get_capabilities($addr); + $key{'list-it'} = $remailer->{'showit'} && defined $caps && ($caps !~ m/\btesting\b/i); + if ( defined Echolot::Globals::get()->{'storage'}->get_nick($addr) ) { + $data->{$key{'summary'}} = \%key; + $data->{$key{'summary'}} = \%key; + }; + }; + + for my $indx (sort {$a cmp $b} keys %$data) { + my $key = $data->{$indx}; + if ($key->{'list-it'}) { + print F $key->{'summary'}."\n\n"; + print F $key->{'key'},"\n\n"; + print T2L $key->{'summary'},"\n"; + }; + print F_PRIV $key->{'summary'}."\n\n"; + print F_PRIV $key->{'key'},"\n\n"; + print T2L_PRIV $key->{'summary'},"\n"; + }; + + close(F); + close(T2L); + close(F_PRIV); + close(T2L_PRIV); + + for my $filename (@filenames) { + Echolot::Tools::write_meta_information($filename, + Expires => time + Echolot::Config::get()->{'buildkeys'}) or + Echolot::Log::debug ("Error while writing meta information for $filename."), + return 0; + }; +}; + + + +sub build_pgpring_type($$$$) { + my ($type, $GnuPG, $keyring, $keyids) = @_; + + for my $remailer (Echolot::Globals::get()->{'storage'}->get_addresses()) { + my $addr = $remailer->{'address'}; + next unless Echolot::Globals::get()->{'storage'}->has_type($addr, $type); + + my %key; + my $final_keyid; + for my $keyid (Echolot::Globals::get()->{'storage'}->get_keys($addr, $type)) { + my %new_key = Echolot::Globals::get()->{'storage'}->get_key($addr, $type, $keyid); + + if (!defined $key{'last_update'} || $key{'last_update'} < $new_key{'last_update'} ) { + %key = %new_key; + $final_keyid = $keyid; + }; + }; + + # only if we have a conf + if ( defined Echolot::Globals::get()->{'storage'}->get_nick($addr) ) { + my ( $stdin_fh, $stdout_fh, $stderr_fh, $status_fh, $handles ) = Echolot::Tools::make_gpg_fds(); + my $pid = $GnuPG->wrap_call( + commands => [ '--import' ], + command_args => [qw{--no-options --no-secmem-warning --no-default-keyring --fast-list-mode --keyring}, $keyring, '--', '-' ], + handles => $handles ); + my ($stdout, $stderr, $status) = Echolot::Tools::readwrite_gpg($key{'key'}, $stdin_fh, $stdout_fh, $stderr_fh, $status_fh); + waitpid $pid, 0; + + ($stdout eq '') or + Echolot::Log::info("GnuPG returned something in stdout '$stdout' while adding key for '$addr': So what?"); + # See DETAIL.gz in GnuPG's doc directory for syntax of GnuPG status + my ($count, $count_imported) = $status =~ /^\[GNUPG:\] IMPORT_RES (\d+) \d+ (\d+)/m; + if ($count_imported > 1) { + Echolot::Log::info("GnuPG status '$status' indicates more than one key for '$addr' imported. Ignoring."); + } elsif ($count_imported < 1) { + Echolot::Log::info("GnuPG status '$status' didn't indicate key for '$addr' was imported correctly. Ignoring."); + }; + my $caps = Echolot::Globals::get()->{'storage'}->get_capabilities($addr); + $keyids->{$final_keyid} = $remailer->{'showit'} && defined $caps && ($caps !~ m/\btesting\b/i); + }; + }; + + return 1; +}; + +sub build_pgpring_export($$$$) { + my ($GnuPG, $keyring, $file, $keyids) = @_; + + my ( $stdin_fh, $stdout_fh, $stderr_fh, $status_fh, $handles ) = Echolot::Tools::make_gpg_fds(); + my $pid = $GnuPG->wrap_call( + commands => [ '--export' ], + command_args => [qw{--no-options --no-secmem-warning --no-default-keyring --keyring}, $keyring, @$keyids ], + handles => $handles ); + my ($stdout, $stderr, $status) = Echolot::Tools::readwrite_gpg('', $stdin_fh, $stdout_fh, $stderr_fh, $status_fh); + waitpid $pid, 0; + + open (F, ">$file") or + Echolot::Log::warn ("Cannot open '$file': $!."), + return 0; + print F $stdout; + close F; + + Echolot::Tools::write_meta_information($file, + Expires => time + Echolot::Config::get()->{'buildkeys'}) or + Echolot::Log::debug ("Error while writing meta information for $file."), + return 0; + + return 1; +}; + +sub build_pgpring() { + my $GnuPG = new GnuPG::Interface; + $GnuPG->call( Echolot::Config::get()->{'gnupg'} ) if (Echolot::Config::get()->{'gnupg'}); + $GnuPG->options->hash_init( + armor => 1, + homedir => Echolot::Config::get()->{'gnupghome'} ); + $GnuPG->options->meta_interactive( 0 ); + + my $keyring = Echolot::Config::get()->{'tmpdir'}.'/'. + Echolot::Globals::get()->{'hostname'}.".".time.'.'.$PROCESS_ID.'_'.Echolot::Globals::get()->{'internalcounter'}++.'.keyring'; + + + my $keyids = {}; + build_pgpring_type('cpunk-rsa', $GnuPG, $keyring, $keyids) or + Echolot::Log::debug("build_pgpring_type failed."), + return undef; + + build_pgpring_export($GnuPG, $keyring, Echolot::Config::get()->{'resultdir'}.'/pgp-rsa.asc', [ grep {$keyids->{$_}} keys %$keyids ]) or + Echolot::Log::debug("build_pgpring_export failed."), + return undef; + + build_pgpring_export($GnuPG, $keyring, Echolot::Config::get()->{'private_resultdir'}.'/pgp-rsa.asc', [ keys %$keyids ]) or + Echolot::Log::debug("build_pgpring_export failed."), + return undef; + + build_pgpring_type('cpunk-dsa', $GnuPG, $keyring, $keyids) or + Echolot::Log::debug("build_pgpring_type failed."), + return undef; + + build_pgpring_export($GnuPG, $keyring, Echolot::Config::get()->{'resultdir'}.'/pgp-all.asc', [ grep {$keyids->{$_}} keys %$keyids ]) or + Echolot::Log::debug("build_pgpring_export failed."), + return undef; + + build_pgpring_export($GnuPG, $keyring, Echolot::Config::get()->{'private_resultdir'}.'/pgp-all.asc', [ keys %$keyids ]) or + Echolot::Log::debug("build_pgpring_export failed."), + return undef; + + + unlink ($keyring) or + Echolot::Log::warn("Cannot unlink tmp keyring '$keyring'."), + return undef; + unlink ($keyring.'~'); # gnupg does those evil backups +}; + +sub build_stats() { + $STATS_DAYS = Echolot::Config::get()->{'stats_days'}; + $SECONDS_PER_DAY = Echolot::Config::get()->{'seconds_per_day'}; + $WEIGHT = Echolot::Config::get()->{'pings_weight'}; + build_lists(); +}; +sub build_keys() { + build_mixring(); + build_pgpring(); +}; + +1; +# vim: set ts=4 shiftwidth=4: diff --git a/trunk/Echolot/Storage/File.pm b/trunk/Echolot/Storage/File.pm new file mode 100644 index 0000000..1f6d21d --- /dev/null +++ b/trunk/Echolot/Storage/File.pm @@ -0,0 +1,1880 @@ +package Echolot::Storage::File; + +# +# $Id$ +# +# This file is part of Echolot - a Pinger for anonymous remailers. +# +# Copyright (c) 2002, 2003, 2004 Peter Palfrader <peter@palfrader.org> +# +# This program is free software. you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +=pod + +=head1 Name + +Echolot::Storage::File - Storage backend for echolot + +=head1 DESCRIPTION + +This package provides several functions for data storage for echolot. + +=over + +=cut + +use strict; +use Data::Dumper; +use IO::Handle; +use English; +use Carp; +use Fcntl ':flock'; # import LOCK_* constants +#use Fcntl ':seek'; # import SEEK_* constants +use POSIX; # import SEEK_* constants (older perls don't have SEEK_ in Fcntl) +use Echolot::Tools; +use Echolot::Log; + + + +my $CONSTANTS = { + 'metadatafile' => 'metadata' +}; + +delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'}; + +my $METADATA_VERSION = 1; + + +=item B<new> (I<%args>) + +Creates a new storage backend object. +args keys: + +=over + +=item I<datadir> + +The basedir where this module may store it's configuration and pinging +data. + +=back + +=cut +sub new { + my ($class, %params) = @_; + my $self = {}; + bless $self, $class; + + $self->{'METADATA_FILE_IS_NEW'} = 0; + + defined($params{'datadir'}) or + confess ('No datadir option passed to new'); + $self->{'datadir'} = $params{'datadir'}; + $self->{'DELAY_COMMIT'} = 0; + + $self->delay_commit(); + $self->metadata_open() or + confess ('Opening Metadata failed. Exiting'); + $self->metadata_read() or + confess ('Reading Metadata from Storage failed. Exiting'); + $self->pingdata_open() or + confess ('Opening Ping files failed. Exiting'); + $self->chainpingdata_open() or + confess ('Opening Ping files failed. Exiting'); + $self->enable_commit(); + + return $self; +}; + +=item $storage->B<commit>( ) + +Write metadata unless B<delay_commt> is set. + +=cut +sub commit($) { + my ($self) = @_; + + if ($self->{'DELAY_COMMIT'}) { + $self->{'COMMIT_PENDING'} = 1; + return; + }; + $self->metadata_write(); + $self->{'COMMIT_PENDING'} = 0; +}; + +=item $storage->B<delay_commit>( ) + +Increase B<delay_commit> by one. + +=cut +sub delay_commit($) { + my ($self) = @_; + + $self->{'DELAY_COMMIT'}++; +}; + +=item $storage->B<enable_commit>( I<$set_> ) + +Decrease B<delay_commit> by one and call C<commit> if B<delay_commit> is zero +and I<$set_pending> is true. + +=cut +sub enable_commit($;$) { + my ($self, $set_pending) = @_; + + $self->{'DELAY_COMMIT'}--; + $self->commit() if (($self->{'COMMIT_PENDING'} || (defined $set_pending && $set_pending)) && ! $self->{'DELAY_COMMIT'}); +}; + +=item $storage->B<finish>( ) + +Shut down cleanly. + +=cut +sub finish($) { + my ($self) = @_; + + $self->pingdata_close(); + $self->chainpingdata_close(); + $self->metadata_write(); + $self->metadata_close(); +}; + + + + +=item $storage->B<metadata_open>( ) + +Open metadata. + +Returns 1 on success, undef on errors. + +=cut +sub metadata_open($) { + my ($self) = @_; + + $self->{'METADATA_FH'} = new IO::Handle; + my $filename = $self->{'datadir'} .'/'. $CONSTANTS->{'metadatafile'}; + + if ( -e $filename ) { + open($self->{'METADATA_FH'}, '+<' . $filename) or + Echolot::Log::warn("Cannot open $filename for reading: $!."), + return undef; + } else { + $self->{'METADATA_FILE_IS_NEW'} = 1; + open($self->{'METADATA_FH'}, '+>' . $filename) or + Echolot::Log::warn("Cannot open $filename for reading: $!."), + return undef; + }; + flock($self->{'METADATA_FH'}, LOCK_EX) or + Echolot::Log::warn("Cannot get exclusive lock on $filename: $!."), + return undef; + return 1; +}; + +=item $storage->B<metadata_close>( ) + +Close metadata. + +Returns 1 on success, undef on errors. + +=cut +sub metadata_close($) { + my ($self) = @_; + + flock($self->{'METADATA_FH'}, LOCK_UN) or + Echolot::Log::warn("Error when releasing lock on metadata file: $!."), + return undef; + close($self->{'METADATA_FH'}) or + Echolot::Log::warn("Error when closing metadata file: $!."), + return undef; + return 1; +}; + + +=item $storage->B<metadata_read>( ) + +Write metadata. + +Returns 1 on success, undef on errors. + +=cut +sub metadata_read($) { + my ($self) = @_; + + if ($self->{'METADATA_FILE_IS_NEW'}) { + $self->{'METADATA'}->{'version'} = $METADATA_VERSION; + $self->{'METADATA'}->{'addresses'} = {}; + $self->{'METADATA'}->{'remailers'} = {}; + + $self->{'METADATA_FILE_IS_NEW'} = 0; + $self->commit(); + } else { + $self->{'METADATA'} = (); + seek($self->{'METADATA_FH'}, 0, SEEK_SET) or + Echolot::Log::warn("Cannot seek to start of metadata file: $!."), + return 0; + { + local $/ = undef; + my $fh = $self->{'METADATA_FH'}; + my $metadata_code = <$fh>; + ($metadata_code) = $metadata_code =~ /^(.*)$/s; + my $METADATA; + eval ($metadata_code); + $self->{'METADATA'} = $METADATA; + }; + $EVAL_ERROR and + confess("Error when reading from metadata file: $EVAL_ERROR"), + return undef; + + defined($self->{'METADATA'}->{'version'}) or + confess("Stored data lacks version header"), + return undef; + ($self->{'METADATA'}->{'version'} == ($METADATA_VERSION)) or + Echolot::Log::warn("Metadata version mismatch ($self->{'METADATA'}->{'version'} vs. $METADATA_VERSION)."), + return undef; + }; + + defined($self->{'METADATA'}->{'secret'}) or + $self->{'METADATA'}->{'secret'} = Echolot::Tools::make_random ( 16, armor => 1 ), + $self->commit(); + + return 1; +}; + +=item $storage->B<metadata_write>( ) + +Write metadata. + +Returns 1 on success, undef on errors. + +=cut +sub metadata_write($) { + my ($self) = @_; + + my $data = Data::Dumper->Dump( [ $self->{'METADATA'} ], [ 'METADATA' ] ); + my $fh = $self->{'METADATA_FH'}; + + seek($fh, 0, SEEK_SET) or + Echolot::Log::warn("Cannot seek to start of metadata file: $!."), + return undef; + truncate($fh, 0) or + Echolot::Log::warn("Cannot truncate metadata file to zero length: $!."), + return undef; + print($fh "# vim:set syntax=perl:\n") or + Echolot::Log::warn("Error when writing to metadata file: $!."), + return undef; + print($fh $data) or + Echolot::Log::warn("Error when writing to metadata file: $!."), + return undef; + $fh->flush(); + + return 1; +}; + +=item $storage->B<metadata_backup>( ) + +Rotate metadata files and create a backup. + +Returns 1 on success, undef on errors. + +=cut +sub metadata_backup($) { + my ($self) = @_; + + my $filename = $self->{'datadir'} .'/'. $CONSTANTS->{'metadatafile'}; + for (my $i=Echolot::Config::get()->{'metadata_backup_count'} - 1; $i>=0; $i--) { + rename ($filename.'.'.($i) , $filename.'.'.($i+1)); + rename ($filename.'.'.($i).'.gz', $filename.'.'.($i+1).'.gz'); + }; + $filename .= '.1'; + + + my $data = Data::Dumper->Dump( [ $self->{'METADATA'} ], [ 'METADATA' ] ); + my $fh = new IO::Handle; + open ($fh, '>'.$filename) or + Echolot::Log::warn("Cannot open $filename for writing: $!."), + return undef; + print($fh "# vim:set syntax=perl:\n") or + Echolot::Log::warn("Error when writing to metadata file: $!."), + return undef; + print($fh $data) or + Echolot::Log::warn("Error when writing to metadata file: $!."), + return undef; + $fh->flush(); + close($fh) or + Echolot::Log::warn("Error when closing metadata file: $!."), + return undef; + + if (Echolot::Config::get()->{'gzip'}) { + system(Echolot::Config::get()->{'gzip'}, $filename) and + Echolot::Log::warn("Gziping $filename failed."), + return undef; + }; + + return 1; +}; + + + + +=item $storage->B<pingdata_open_one>( I<$remailer_addr>, I<$type>, I<$key> ) + +Open the pingdata file for the I<$remailer_addr>, I<$type>, and I<$key>. + +Returns 1 on success, undef on errors. + +=cut +sub pingdata_open_one($$$$) { + my ($self, $remailer_addr, $type, $key) = @_; + + defined ($self->{'METADATA'}->{'remailers'}->{$remailer_addr}) or + Echolot::Log::cluck ("$remailer_addr does not exist in Metadata."), + return undef; + defined ($self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'keys'}) or + Echolot::Log::cluck ("$remailer_addr has no keys in Metadata."), + return undef; + defined ($self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'keys'}->{$type}) or + Echolot::Log::cluck ("$remailer_addr type $type does not exist in Metadata."), + return undef; + defined ($self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'keys'}->{$type}->{$key}) or + Echolot::Log::cluck ("$remailer_addr type $type key $key does not exist in Metadata."), + return undef; + + + my $basename = $self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'keys'}->{$type}->{$key}->{'stats'}; + defined($basename) or + $basename = $self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'keys'}->{$type}->{$key}->{'stats'} = $remailer_addr.'.'.$type.'.'.$key.'.'.time.'.'.$PROCESS_ID.'_'.Echolot::Globals::get()->{'internalcounter'}++, + $self->commit(); + + my $filename = $self->{'datadir'} .'/'. $basename; + + for my $direction ('out', 'done') { + my $fh = new IO::Handle; + if ( -e $filename.'.'.$direction ) { + open($fh, '+<' . $filename.'.'.$direction) or + Echolot::Log::warn("Cannot open $filename.$direction for reading: $!."), + return undef; + $self->{'PING_FHS'}->{$remailer_addr}->{$type}->{$key}->{$direction} = $fh; + } else { + open($fh, '+>' . $filename.'.'.$direction) or + Echolot::Log::warn("Cannot open $filename.$direction for reading: $!."), + return undef; + $self->{'PING_FHS'}->{$remailer_addr}->{$type}->{$key}->{$direction} = $fh; + }; + flock($fh, LOCK_EX) or + Echolot::Log::warn("Cannot get exclusive lock on $remailer_addr $type $key $direction pings: $!."), + return undef; + }; + + return 1; +}; + +=item $storage->B<pingdata_open>( ) + +Open all pingdata files. + +Returns 1. + +=cut +sub pingdata_open($) { + my ($self) = @_; + + for my $remailer_addr ( keys %{$self->{'METADATA'}->{'remailers'}} ) { + for my $type ( keys %{$self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'keys'}} ) { + for my $key ( keys %{$self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'keys'}->{$type}} ) { + $self->pingdata_open_one($remailer_addr, $type, $key); + }; + }; + }; + return 1; +}; + +=item $storage->B<get_ping_fh>( I<$remailer_addr>, I<$type>, I<$key>, I<$direction>, I<$oknodo> ) + +Return the FH for the pingdata file of I<$remailer_addr>, I<$type>, I<$key>, and I<$direction>. + +If $<oknodo> is set, the absense of a defined filehandle does not cause it to +be opened/created. Instead -1 is returned. + +Returns undef on error; + +=cut +sub get_ping_fh($$$$$;$) { + my ($self, $remailer_addr, $type, $key, $direction, $oknodo) = @_; + + defined ($self->{'METADATA'}->{'addresses'}->{$remailer_addr}) or + Echolot::Log::cluck("$remailer_addr does not exist in Metadata."), + return undef; + + my $fh = $self->{'PING_FHS'}->{$remailer_addr}->{$type}->{$key}->{$direction}; + + unless (defined $fh) { + return -1 if (defined $oknodo && $oknodo); + + $self->pingdata_open_one($remailer_addr, $type, $key), + $fh = $self->{'PING_FHS'}->{$remailer_addr}->{$type}->{$key}->{$direction}; + defined ($fh) or + Echolot::Log::warn ("$remailer_addr; type=$type; key=$key has no assigned filehandle for $direction pings."), + return undef; + } + + return $fh; +}; + +=item $storage->B<pingdata_close_one>( I<$remailer_addr>, I<$type>, I<$key> ) + +Close the pingdata file for the I<$remailer_addr>, I<$type>, and I<$key>. + +Returns 1 on success, undef on errors. + +=cut +sub pingdata_close_one($$$$;$) { + my ($self, $remailer_addr, $type, $key, $delete) = @_; + + for my $direction ( keys %{$self->{'PING_FHS'}->{$remailer_addr}->{$type}->{$key}} ) { + my $fh = $self->{'PING_FHS'}->{$remailer_addr}->{$type}->{$key}->{$direction}; + + flock($fh, LOCK_UN) or + Echolot::Log::warn("Error when releasing lock on $remailer_addr type $type key $key direction $direction pings: $!."), + return undef; + close ($fh) or + Echolot::Log::warn("Error when closing $remailer_addr type $type key $key direction $direction pings: $!."), + return undef; + + if ((defined $delete) && ($delete eq 'delete')) { + my $basename = $self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'keys'}->{$type}->{$key}->{'stats'}; + my $filename = $self->{'datadir'} .'/'. $basename; + unlink ($filename.'.'.$direction) or + carp ("Cannot unlink $filename.'.'.$direction: $!"); + }; + }; + + delete $self->{'PING_FHS'}->{$remailer_addr}->{$type}->{$key}; + + delete $self->{'PING_FHS'}->{$remailer_addr}->{$type} + unless (scalar keys %{$self->{'PING_FHS'}->{$remailer_addr}->{$type}}); + delete $self->{'PING_FHS'}->{$remailer_addr} + unless (scalar keys %{$self->{'PING_FHS'}->{$remailer_addr}}); + + + return 1; +}; + +=item $storage->B<pingdata_close>( ) + +Close all pingdata files. + +Returns 1 on success, undef on errors. + +=cut +sub pingdata_close($) { + my ($self) = @_; + + for my $remailer_addr ( keys %{$self->{'PING_FHS'}} ) { + for my $type ( keys %{$self->{'PING_FHS'}->{$remailer_addr}} ) { + for my $key ( keys %{$self->{'PING_FHS'}->{$remailer_addr}->{$type}} ) { + $self->pingdata_close_one($remailer_addr, $type, $key) or + Echolot::Log::debug("Error when calling pingdata_close_one with $remailer_addr type $type key $key."), + return undef; + }; + }; + }; + return 1; +}; + +=item $storage->B<get_pings>( I<$remailer_addr>, I<$type>, I<$key>, I<$direction> ) + +Return an array of ping data for I<$remailer_addr>, I<$type>, I<$key>, and I<$direction>. + +If direction is B<out> then it's an array of scalar (the send timestamps). + +If direction is B<done> then it's an array of array references each having two +items: the send time and the latency. + +Returns undef on error; + +=cut +sub get_pings($$$$$) { + my ($self, $remailer_addr, $type, $key, $direction) = @_; + + my @pings; + + my $fh = $self->get_ping_fh($remailer_addr, $type, $key, $direction, 1); + (defined $fh) or + Echolot::Log::cluck ("$remailer_addr; type=$type; key=$key has no assigned filehandle for out pings."); + ($fh == -1) and + Echolot::Log::info ("$remailer_addr; type=$type; key=$key has no assigned filehandle for $direction pings (key has expired, or not available yet)."), + return (); + + seek($fh, 0, SEEK_SET) or + Echolot::Log::warn("Cannot seek to start of $remailer_addr type $type key $key direction $direction pings: $! ($fh)."), + return undef; + + if ($direction eq 'out') { + @pings = map {chomp; $_; } <$fh>; + } elsif ($direction eq 'done') { + @pings = map {chomp; my @arr = split (/\s+/, $_, 2); \@arr; } <$fh>; + } else { + confess("What the hell am I doing here? $remailer_addr; $type; $key; $direction"), + return undef; + }; + return @pings; +}; + + + + + +=item $storage->B<register_pingout>( I<$remailer_addr>, I<$type>, I<$key>, I<$sent_time> ) + +Register a ping sent to I<$remailer_addr>, I<$type>, I<$key> and I$<sent_time>. + +Returns 1 on success, undef on errors. + +=cut +sub register_pingout($$$$$) { + my ($self, $remailer_addr, $type, $key, $sent_time) = @_; + + my $fh = $self->get_ping_fh($remailer_addr, $type, $key, 'out') or + Echolot::Log::cluck ("$remailer_addr; type=$type; key=$key has no assigned filehandle for out pings."), + return undef; + + seek($fh, 0, SEEK_END) or + Echolot::Log::warn("Cannot seek to end of $remailer_addr; type=$type; key=$key; out pings: $!."), + return undef; + print($fh $sent_time."\n") or + Echolot::Log::warn("Error when writing to $remailer_addr; type=$type; key=$key; out pings: $!."), + return undef; + $fh->flush(); + Echolot::Log::debug("registering pingout for $remailer_addr ($type; $key)."); + + return 1; +}; + +=item $storage->B<register_pingdone>( I<$remailer_addr>, I<$type>, I<$key>, I<$sent_time>, I<$latency> ) + +Register that the ping sent to I<$remailer_addr>, I<$type>, I<$key> at +I$<sent_time> has returned with latency I<$latency>. + +Returns 1 on success, undef on errors. + +=cut +sub register_pingdone($$$$$$) { + my ($self, $remailer_addr, $type, $key, $sent_time, $latency) = @_; + + defined ($self->{'METADATA'}->{'addresses'}->{$remailer_addr}) or + Echolot::Log::warn ("$remailer_addr does not exist in Metadata."), + return undef; + + my @outpings = $self->get_pings($remailer_addr, $type, $key, 'out'); + my $origlen = scalar (@outpings); + @outpings = grep { $_ != $sent_time } @outpings; + ($origlen == scalar (@outpings)) and + Echolot::Log::info("No ping outstanding for $remailer_addr, $key, ".(scalar localtime $sent_time)."."), + return 1; + + # write ping to done + my $fh = $self->get_ping_fh($remailer_addr, $type, $key, 'done') or + Echolot::Log::cluck ("$remailer_addr; type=$type; key=$key has no assigned filehandle for done pings."), + return undef; + seek($fh, 0, SEEK_END) or + Echolot::Log::warn("Cannot seek to end of $remailer_addr done pings: $!."), + return undef; + print($fh $sent_time." ".$latency."\n") or + Echolot::Log::warn("Error when writing to $remailer_addr done pings: $!."), + return undef; + $fh->flush(); + + # rewrite outstanding pings + $fh = $self->get_ping_fh($remailer_addr, $type, $key, 'out') or + Echolot::Log::cluck ("$remailer_addr; type=$type; key=$key has no assigned filehandle for out pings."), + return undef; + seek($fh, 0, SEEK_SET) or + Echolot::Log::warn("Cannot seek to start of outgoing pings file for remailer $remailer_addr; key=$key: $!."), + return undef; + truncate($fh, 0) or + Echolot::Log::warn("Cannot truncate outgoing pings file for remailer $remailer_addr; key=$key file to zero length: $!."), + return undef; + print($fh (join "\n", @outpings), (scalar @outpings ? "\n" : '') ) or + Echolot::Log::warn("Error when writing to outgoing pings file for remailer $remailer_addr; key=$key file: $!."), + return undef; + $fh->flush(); + Echolot::Log::debug("registering pingdone from ".(scalar localtime $sent_time)." with latency $latency for $remailer_addr ($type; $key)."); + + return 1; +}; + + + + + + +=item $storage->B<chainpingdata_open_one>( I<$chaintype> ) + +Open the pingdata file for I<$chaintype> type chain pings. + +Returns 1 on success, undef on errors. + +=cut +sub chainpingdata_open_one($$) { + my ($self, $type) = @_; + + my $filename = $self->{'datadir'} .'/chainpings.'.$type; + + for my $direction ('out', 'done') { + my $fh = new IO::Handle; + if ( -e $filename.'.'.$direction ) { + open($fh, '+<' . $filename.'.'.$direction) or + Echolot::Log::warn("Cannot open $filename.$direction for reading: $!."), + return undef; + $self->{'CHAINPING_FHS'}->{$type}->{$direction} = $fh; + } else { + open($fh, '+>' . $filename.'.'.$direction) or + Echolot::Log::warn("Cannot open $filename.$direction for reading: $!."), + return undef; + $self->{'CHAINPING_FHS'}->{$type}->{$direction} = $fh; + }; + flock($fh, LOCK_EX) or + Echolot::Log::warn("Cannot get exclusive lock on $filename.$direction pings: $!."), + return undef; + }; + + return 1; +}; + +=item $storage->B<chainpingdata_open>( ) + +Open all chainpingdata files. + +Returns 1. + +=cut +sub chainpingdata_open($) { + my ($self) = @_; + + for my $type ( keys %{Echolot::Config::get()->{'which_chainpings'}} ) { + $self->chainpingdata_open_one($type); + }; + + return 1; +}; + + +=item $storage->B<get_chainping_fh>( I<$type>, I<$direction> ) + +Return the FH for the chainpingdata file of I<$type>, and I<$direction>. + +Returns undef on error; + +=cut +sub get_chainping_fh($$$) { + my ($self, $type, $direction) = @_; + + my $fh = $self->{'CHAINPING_FHS'}->{$type}->{$direction}; + + defined ($fh) or + $self->chainpingdata_open_one($type), + $fh = $self->{'CHAINPING_FHS'}->{$type}->{$direction}; + defined ($fh) or + Echolot::Log::warn ("chainping $type has no assigned filehandle for $direction chainpings."), + return undef; + + return $fh; +}; + +=item $storage->B<chainpingdata_close_one>( I<$type> ) + +Close the chainpingdata file for I<$type>. + +Returns 1 on success, undef on errors. + +=cut +sub chainpingdata_close_one($) { + my ($self, $type) = @_; + + for my $direction ( keys %{$self->{'CHAINPING_FHS'}->{$type}} ) { + my $fh = $self->{'CHAINPING_FHS'}->{$type}->{$direction}; + + flock($fh, LOCK_UN) or + Echolot::Log::warn("Error when releasing lock on $type direction $direction chainpings: $!."), + return undef; + close ($fh) or + Echolot::Log::warn("Error when closing $type direction $direction chainpings: $!."), + return undef; + }; + + delete $self->{'CHAINPING_FHS'}->{$type}; + + return 1; +}; + +=item $storage->B<chainpingdata_close>( ) + +Close all chainpingdata files. + +Returns 1 on success, undef on errors. + +=cut +sub chainpingdata_close($) { + my ($self) = @_; + + for my $type ( keys %{$self->{'CHAINPING_FHS'}} ) { + $self->chainpingdata_close_one($type) or + Echolot::Log::debug("Error when calling chainpingdata_close_one with type $type."), + return undef; + }; + return 1; +}; + + + +=item $storage->B<get_chainpings>( I<$chaintype> ) + +Return chainping data for I<$chaintype>. + +The result is a reference to a hash having two entries: out and done. + +Each of them is a reference to an array of single pings. Each ping is a hash +reference with the hash having the keys B<sent>, B<addr1>, B<type1>, B<key1>, +B<addr2>, B<type2>, B<key2>, and in case of received pings B<lat>. + +Out currently includes all sent pings - also those that allready arrived. +This is different from the get_pings() function above. + +Returns undef on error. + +=cut +sub get_chainpings($$) { + my ($self, $chaintype) = @_; + + my $fh = $self->get_chainping_fh($chaintype, 'out') or + Echolot::Log::warn ("have no assigned filehandle for $chaintype out chainpings."), + return undef; + seek($fh, 0, SEEK_SET) or + Echolot::Log::warn("Cannot seek to start of $chaintype out chainpings $!."), + return undef; + my @out = + map { + chomp; + my @a = split; + Echolot::Log::warn("'$_' has not 7 fields") if (scalar @a < 7); + { sent => $a[0], + addr1 => $a[1], + type1 => $a[2], + key1 => $a[3], + addr2 => $a[4], + type2 => $a[5], + key2 => $a[6] + } + } <$fh>; + my %sent = map { + my $a = $_; + my $key = join (' ', map ({ $a->{$_} } qw{sent addr1 type1 key1 addr2 type2 key2})); + $key => 1 + } @out; + + $fh = $self->get_chainping_fh($chaintype, 'done') or + Echolot::Log::warn ("assigned filehandle for $chaintype done chainpings."), + return undef; + seek($fh, 0, SEEK_SET) or + Echolot::Log::warn("Cannot seek to start of $chaintype done chainpings $!."), + return undef; + my @done = + grep { + # Only list things that actually got sent - and only once + my $a = $_; + my $key = join (' ', map ({ $a->{$_} } qw{sent addr1 type1 key1 addr2 type2 key2})); + my $exists = exists $sent{$key}; + delete $sent{$key}; + $exists + } + map { + chomp; + my @a = split; + { sent => $a[0], + addr1 => $a[1], + type1 => $a[2], + key1 => $a[3], + addr2 => $a[4], + type2 => $a[5], + key2 => $a[6], + lat => $a[7] + } + } <$fh>; + + return { + out => \@out, + done => \@done + }; +}; + + +=item $storage->B<register_chainpingout>( I<$chaintype>, I<$addr1>, I<$type1>, I<$key1>, I<$addr2>, I<$type2>, I<$key2>, I<$sent_time> > + +Register a chain ping of type I<$chaintype> sent through I<$addr1> (I<$type1>, I<$key1>) +and I<$addr2> (I<$type2>, I<$key2>) at I$<sent_time>. + +Returns 1 on success, undef on errors. + +=cut +sub register_chainpingout($$$$$$$$$) { + my ($self, $chaintype, $addr1, $type1, $key1, $addr2, $type2, $key2, $sent_time) = @_; + + my $fh = $self->get_chainping_fh($chaintype, 'out') or + Echolot::Log::cluck ("chaintype $chaintype/out has no assigned filehandle."), + return undef; + + seek($fh, 0, SEEK_END) or + Echolot::Log::warn("Cannot seek to end of chaintype $chaintype out pings: $!."), + return undef; + print($fh join(' ', $sent_time, $addr1, $type1, $key1, $addr2, $type2, $key2)."\n") or + Echolot::Log::warn("Error when writing to chaintype $chaintype out pings: $!."), + return undef; + $fh->flush(); + Echolot::Log::debug("registering chainping $chaintype out through $addr1 ($type1; $key1) to $addr2 ($type2; $key2)."); + + return 1; +}; + +=item $storage->B<register_chainpingdone>( I<$chaintype>, I<$addr1>, I<$type1>, I<$key1>, I<$addr2>, I<$type2>, I<$key2>, I<$sent_time>, I<$latency> ) + +Register that the chain ping of type I<$chaintype> sent through I<$addr1> (I<$type1>, I<$key1>) +and I<$addr2> (I<$type2>, I<$key2>) at I$<sent_time> +has returned with latency I<$latency>. + +Returns 1 on success, undef on errors. + +=cut +sub register_chainpingdone($$$$$$$$$$) { + my ($self, $chaintype, $addr1, $type1, $key1, $addr2, $type2, $key2, $sent_time, $latency) = @_; + + # write ping to done + my $fh = $self->get_chainping_fh($chaintype, 'done') or + Echolot::Log::cluck ("chaintype $chaintype/done has no assigned filehandle."), + return undef; + seek($fh, 0, SEEK_END) or + Echolot::Log::warn("Cannot seek to end of $chaintype/done pings: $!."), + return undef; + print($fh join(' ', $sent_time, $addr1, $type1, $key1, $addr2, $type2, $key2, $latency)."\n") or + Echolot::Log::warn("Error when writing to $chaintype/done pings: $!."), + return undef; + $fh->flush(); + Echolot::Log::debug("registering chainpingdone from ".(scalar localtime $sent_time)." with latency $latency chainping $chaintype out through $addr1 ($type1; $key1) to $addr2 ($type2; $key2)."); + + return 1; +}; + +=item $storage->B<add_prospective_address>( I<$addr>, I<$reason>, I<$additional> ) + +Add I<$addr> to the list of prospective remailers with I<$reason> and +I<$additional> information. + +Returns 1. + +=cut +sub add_prospective_address($$$$) { + my ($self, $addr, $reason, $additional) = @_; + + return 1 if defined $self->{'METADATA'}->{'addresses'}->{$addr}; + push @{ $self->{'METADATA'}->{'prospective_addresses'}{$addr} }, time().'; '. $reason. '; '. $additional; + $self->commit(); + + return 1; +}; + +=item $storage->B<commit_prospective_address>( ) + +Commit prospective remailers to the list of remailers we know. + +Returns 1. + +=cut +sub commit_prospective_address($) { + my ($self) = @_; + + $self->delay_commit(); + for my $addr (keys %{$self->{'METADATA'}->{'prospective_addresses'}}) { + if (defined $self->{'METADATA'}->{'addresses'}->{$addr}) { + delete $self->{'METADATA'}->{'prospective_addresses'}->{$addr}; + next; + }; + + # expire old prospective addresses + while (@{ $self->{'METADATA'}->{'prospective_addresses'}->{$addr} }) { + my ($time, $reason, $additional) = split(/;\s*/, $self->{'METADATA'}->{'prospective_addresses'}->{$addr}->[0] ); + if ($time < time() - Echolot::Config::get()->{'prospective_addresses_ttl'} ) { + shift @{ $self->{'METADATA'}->{'prospective_addresses'}->{$addr} }; + } else { + last; + }; + }; + + unless (scalar @{ $self->{'METADATA'}->{'prospective_addresses'}->{$addr} }) { + delete $self->{'METADATA'}->{'prospective_addresses'}->{$addr}; + next; + }; + + my %reasons; + for my $line ( @{ $self->{'METADATA'}->{'prospective_addresses'}->{$addr} } ) { + my ($time, $reason, $additional) = split(/;\s*/, $line); + push @{ $reasons{$reason} }, $additional; + }; + + # got prospective by reply to own remailer-conf or remailer-key request + if ( defined $reasons{'self-capsstring-conf'} || defined $reasons{'self-capsstring-key'} ) { + Echolot::Log::notice("$addr is used because of direct conf or key reply"); + $self->add_address($addr); + delete $self->{'METADATA'}->{'prospective_addresses'}->{$addr}; + next; + } + + # was listed in reliable's remailer-conf reply; @adds holds suggestors + my @adds; + push @adds, @{ $reasons{'reliable-caps-reply-type1'} } if defined $reasons{'reliable-caps-reply-type1'}; + push @adds, @{ $reasons{'reliable-caps-reply-type2'} } if defined $reasons{'reliable-caps-reply-type2'}; + if (scalar @adds) { + my %unique; + @adds = grep { ! $unique{$_}++; } @adds; + if (scalar @adds >= Echolot::Config::get()->{'reliable_auto_add_min'} ) { + Echolot::Log::notice("$addr is recommended by ". join(', ', @adds) . "."); + $self->add_address($addr); + delete $self->{'METADATA'}->{'prospective_addresses'}->{$addr}; + next; + }; + }; + }; + + $self->enable_commit(1); + + return 1; +}; + +=item $storage->B<get_address>( I<$addr> ) + +Get a reference to a hash of information of the remailers with address +I<$addr>. + +The hash has the following keys: + +=over + +=item status + +=item id + +=item address + +=item fetch + +=item showit + +=item pingit + +=item ttl + +=item resurrection_ttl + +=back + +Returns undef on errors. + +=cut +sub get_address($$) { + my ($self, $addr) = @_; + + defined ($self->{'METADATA'}->{'addresses'}->{$addr}) or + Echolot::Log::cluck ("$addr does not exist in Metadata."), + return undef; + + my $result = { + status => $self->{'METADATA'}->{'addresses'}->{$addr}->{'status'}, + id => $self->{'METADATA'}->{'addresses'}->{$addr}->{'id'}, + address => $_, + fetch => $self->{'METADATA'}->{'addresses'}->{$addr}->{'fetch'}, + showit => $self->{'METADATA'}->{'addresses'}->{$addr}->{'showit'}, + pingit => $self->{'METADATA'}->{'addresses'}->{$addr}->{'pingit'}, + ttl => $self->{'METADATA'}->{'addresses'}->{$addr}->{'ttl'}, + resurrection_ttl => $self->{'METADATA'}->{'addresses'}->{$addr}->{'resurrection_ttl'}, + }; + + return $result; +}; + +=item $storage->B<get_addresses>( ) + +Get an array of all remailers we know about. Each element in this array is a +hash reference as returned by C<get_address>. + +=cut +sub get_addresses($) { + my ($self) = @_; + + my @addresses = keys %{$self->{'METADATA'}->{'addresses'}}; + my @return_data = map { $self->get_address($_); } @addresses; + return @return_data; +}; + +=item $storage->B<add_address>( I<$addr> ) + +Adds a remailer with address I<$addr>. B<fetch>, B<pingit>, and B<shoit> are +set to the values configured for new remailers. + +Assign the remailer status B<active> and a new unique ID. + +See L<pingd.conf(5)> for more information on this. + +Returns 1. + +=cut +sub add_address($$) { + my ($self, $addr) = @_; + + my @all_addresses = $self->get_addresses(); + my $maxid = $self->{'METADATA'}->{'addresses_maxid'}; + unless (defined $maxid) { + $maxid = 0; + for my $addr (@all_addresses) { + if ($addr->{'id'} > $maxid) { + $maxid = $addr->{'id'}; + }; + }; + }; + + + + Echolot::Log::notice("Adding address $addr."); + + my $remailer = { + id => $maxid + 1, + status => 'active', + ttl => Echolot::Config::get()->{'addresses_default_ttl'}, + fetch => Echolot::Config::get()->{'fetch_new'}, + pingit => Echolot::Config::get()->{'ping_new'}, + showit => Echolot::Config::get()->{'show_new'}, + }; + $self->{'METADATA'}->{'addresses'}->{$addr} = $remailer; + $self->{'METADATA'}->{'addresses_maxid'} = $maxid+1; + $self->commit(); + + return 1; +}; + +=item $storage->B<set_stuff>( I<@args> ) + +@args is supposed to have two elements: I<$address>, and I<$setting>. + +Set verious options for the remailer with address $I<$address>. + +I<$setting> has to be of the form C<key=value>. Recognised keys are B<pingit>, +B<fetch>, and B<showit>. Acceptable values are B<on> and B<off>. + +See L<pingd(1)> for the meaning of these settings. + +Returns 1, undef on error. + +=cut +sub set_stuff($@) { + my ($self, @args) = @_; + + my ($addr, $setting) = @args; + my $args = join(', ', @args); + + defined ($addr) or + Echolot::Log::cluck ("Could not get address for '$args'."), + return undef; + defined ($setting) or + Echolot::Log::cluck ("Could not get setting for '$args'."), + return undef; + + defined ($self->{'METADATA'}->{'addresses'}->{$addr}) or + Echolot::Log::warn ("Address $addr does not exist."), + return undef; + + + if ($setting =~ /^(pingit|fetch|showit)=(on|off)$/) { + my $option = $1; + my $value = $2; + Echolot::Log::info("Setting $option to $value for $addr"); + $self->{'METADATA'}->{'addresses'}->{$addr}->{$option} = ($value eq 'on'); + } else { + Echolot::Log::warn ("Don't know what to do with '$setting' for $addr."), + return undef; + } + + $self->commit(); + return 1; +}; + + +=item $storage->B<get_address_by_id>( I<$id> ) + +Return the address for the remailer with id I<$id>. + +Return undef if there is no remailer with that id. + +=cut +sub get_address_by_id($$) { + my ($self, $id) = @_; + + my @addresses = grep {$self->{'METADATA'}->{'addresses'}->{$_}->{'id'} == $id} + keys %{$self->{'METADATA'}->{'addresses'}}; + return undef unless (scalar @addresses); + if (scalar @addresses >= 2) { + Echolot::Log::cluck("Searching for address by id '$id' gives more than one result."); + }; + my %return_data = %{$self->{'METADATA'}->{'addresses'}->{$addresses[0]}}; + $return_data{'address'} = $addresses[0]; + return \%return_data; +}; + +=item $storage->B<decrease_resurrection_ttl>( I<$address> ) + +Decrease the TTL (Time To Live) for remailer with address I<$address> by one. + +If it hits zero the remailer's status is set to B<ttl timeout>. + +Returns 1, undef on error. + +=cut +sub decrease_ttl($$) { + my ($self, $address) = @_; + + defined ($self->{'METADATA'}->{'addresses'}->{$address}) or + Echolot::Log::cluck ("$address does not exist in Metadata address list."), + return undef; + $self->{'METADATA'}->{'addresses'}->{$address}->{'ttl'} --; + $self->{'METADATA'}->{'addresses'}->{$address}->{'status'} = 'ttl timeout', + Echolot::Log::info("Remailer $address disabled: ttl expired."), + $self->{'METADATA'}->{'addresses'}->{$address}->{'resurrection_ttl'} = Echolot::Config::get()->{'check_resurrection_ttl'} + if ($self->{'METADATA'}->{'addresses'}->{$address}->{'ttl'} <= 0); + $self->commit(); + return 1; +}; + +=item $storage->B<decrease_resurrection_ttl>( I<$address> ) + +Decrease the resurrection TTL (Time To Live) for remailer with address +I<$address> by one. + +If it hits zero the remailer's status is set to B<dead>. + +Returns 1, undef on error. + +=cut +sub decrease_resurrection_ttl($$) { + my ($self, $address) = @_; + + defined ($self->{'METADATA'}->{'addresses'}->{$address}) or + Echolot::Log::cluck ("$address does not exist in Metadata address list."), + return 0; + ($self->{'METADATA'}->{'addresses'}->{$address}->{'status'} eq 'ttl timeout') or + Echolot::Log::cluck ("$address is not in ttl timeout status."), + return 0; + $self->{'METADATA'}->{'addresses'}->{$address}->{'resurrection_ttl'} --; + $self->{'METADATA'}->{'addresses'}->{$address}->{'status'} = 'dead', + Echolot::Log::info("Remailer $address is dead."), + if ($self->{'METADATA'}->{'addresses'}->{$address}->{'resurrection_ttl'} <= 0); + $self->commit(); + return 1; +}; + +=item $storage->B<restore_ttl>( I<$address> ) + +Restore the TTL (Time To Live) for remailer with address I<$address> to the +value configured with I<addresses_default_ttl> + +See L<pingd.conf(5)> for more information on this settings. + +Returns 1, undef on error. + +=cut +sub restore_ttl($$) { + my ($self, $address) = @_; + + defined ($self->{'METADATA'}->{'addresses'}->{$address}) or + Echolot::Log::cluck ("$address does not exist in Metadata address list."), + return undef; + defined ($self->{'METADATA'}->{'addresses'}->{$address}->{'status'}) or + Echolot::Log::cluck ("$address does exist in Metadata address list but does not have status defined."), + return undef; + Echolot::Log::info("Remailer $address is alive and active again.") + unless ($self->{'METADATA'}->{'addresses'}->{$address}->{'status'} eq 'active'); + $self->{'METADATA'}->{'addresses'}->{$address}->{'ttl'} = Echolot::Config::get()->{'addresses_default_ttl'}; + delete $self->{'METADATA'}->{'addresses'}->{$address}->{'resurrection_ttl'}; + $self->{'METADATA'}->{'addresses'}->{$address}->{'status'} = 'active' if + ($self->{'METADATA'}->{'addresses'}->{$address}->{'status'} eq 'ttl timeout' || + $self->{'METADATA'}->{'addresses'}->{$address}->{'status'} eq 'dead'); + $self->commit(); + return 1; +}; + + +=item $storage->B<not_a_remaielr>( I<$id> ) + +Set the remailer whoise id is I<$id> to B<disabled by user reply: is not a +remailer>. + +Returns 1, undef on error. + +=cut +sub not_a_remailer($$) { + my ($self, $id) = @_; + + my $remailer = $self->get_address_by_id($id); + defined $remailer or + Echolot::Log::cluck("No remailer found for id '$id'."), + return undef; + my $address = $remailer->{'address'}; + defined ($self->{'METADATA'}->{'addresses'}->{$address}) or + Echolot::Log::cluck ("$address does not exist in Metadata address list."), + return undef; + $self->{'METADATA'}->{'addresses'}->{$address}->{'status'} = 'disabled by user reply: is not a remailer'; + + Echolot::Log::info("Setting $id, $address to disabled by user reply."); + + $self->commit(); + return 1; +}; + +=item $storage->B<set_caps>( I<$type>, I<$caps>, I<$nick>, I<$address>, I<$timestamp> [, I<$dont_expire> ]) + +Sets the capabilities for remailer with address I<$address> to the given +information (I<$nick>, I<$type>, I<$caps>, I<$timestamp>). + +Type here means the software used (Mixmaster, Reliable) as given by the +remailer-conf reply or something like B<set manually>. + +If there already is newer information about that key than I<$timestamp> the +update is disregarded. + +If I<$dont_expire> is defined the setting is copied to the remailers metadata +as well. + +Returns 1. + +=cut +sub set_caps($$$$$$;$) { + my ($self, $type, $caps, $nick, $address, $timestamp, $dont_expire) = @_; + + (defined $address) or + Echolot::Log::cluck ("$address not defined in set_key."); + + if (! defined $self->{'metadata'}->{'remailers'}->{$address} ) { + $self->{'metadata'}->{'remailers'}->{$address} = {}; + }; + + if (! defined $self->{'METADATA'}->{'remailers'}->{$address}->{'conf'}) { + $self->{'METADATA'}->{'remailers'}->{$address}->{'conf'} = + { + nick => $nick, + type => $type, + capabilities => $caps, + last_update => $timestamp + }; + } else { + my $conf = $self->{'METADATA'}->{'remailers'}->{$address}->{'conf'}; + if ($conf->{'last_update'} >= $timestamp) { + Echolot::Log::info("Stored data is already newer for remailer $nick."); + return 1; + }; + $conf->{'last_update'} = $timestamp; + if ($conf->{'nick'} ne $nick) { + Echolot::Log::info($conf->{'nick'}." was renamed to $nick."); + $conf->{'nick'} = $nick; + }; + if ($conf->{'capabilities'} ne $caps) { + Echolot::Log::info("$nick has a new caps string '$caps' old: '".$conf->{'capabilities'}."'."); + $conf->{'capabilities'} = $caps; + }; + if ($conf->{'type'} ne $type) { + Echolot::Log::info("$nick has a new type string '$type'."); + $conf->{'type'} = $type; + }; + }; + + if (defined $dont_expire) { + $self->{'METADATA'}->{'remailers'}->{$address}->{'conf'}->{'dont_expire'} = $dont_expire; + }; + + $self->commit(); + + return 1; +}; + +=item $storage->B<set_key>( I<$type>, I<$nick>, I<$address>, I<$key>, I<$keyid>, I<$version>, I<$caps>, I<$summary>, I<$timestamp>) + +Sets the I<$type> key I<$keyid> for remailer with address I<$address> to the +given information (I<$nick>, I<$key>, I<$caps>, I<$summary>, I<$timestamp>). + +If there already is newer information about that key than I<$timestamp> the +update is disregarded. + +Returns 1. + +=cut +sub set_key($$$$$$$$$) { + my ($self, $type, $nick, $address, $key, $keyid, $version, $caps, $summary, $timestamp) = @_; + + (defined $address) or + Echolot::Log::cluck ("$address not defined in set_key."); + + if (! defined $self->{'metadata'}->{'remailers'}->{$address} ) { + $self->{'metadata'}->{'remailers'}->{$address} = {}; + }; + + if (! defined $self->{'METADATA'}->{'remailers'}->{$address}->{'keys'}) { + $self->{'METADATA'}->{'remailers'}->{$address}->{'keys'} = {}; + }; + if (! defined $self->{'METADATA'}->{'remailers'}->{$address}->{'keys'}->{$type}) { + $self->{'METADATA'}->{'remailers'}->{$address}->{'keys'}->{$type} = {}; + }; + + if (! defined $self->{'METADATA'}->{'remailers'}->{$address}->{'keys'}->{$type}->{$keyid}) { + $self->{'METADATA'}->{'remailers'}->{$address}->{'keys'}->{$type}->{$keyid} = + { + key => $key, + summary => $summary, + nick => $nick, + last_update => $timestamp + }; + } else { + my $keyref = $self->{'METADATA'}->{'remailers'}->{$address}->{'keys'}->{$type}->{$keyid}; + if ($keyref->{'last_update'} >= $timestamp) { + Echolot::Log::info("Stored data is already newer for remailer $nick."); + return 1; + }; + $keyref->{'last_update'} = $timestamp; + if ($keyref->{'nick'} ne $nick) { + Echolot::Log::info("$nick has a new key nick string '$nick' old: '".$keyref->{'nick'}."'."); + $keyref->{'nick'} = $nick; + }; + if ($keyref->{'summary'} ne $summary) { + Echolot::Log::info("$nick has a new key summary string '$summary' old: '".$keyref->{'summary'}."'."); + $keyref->{'summary'} = $summary; + }; + if ($keyref->{'key'} ne $key) { + #Echolot::Log::info("$nick has a new key string '$key' old: '".$keyref->{'key'}."' - This probably should not happen."); + Echolot::Log::info("$nick has a new key string for same keyid $keyid."); + $keyref->{'key'} = $key; + }; + }; + $self->commit(); + + return 1; +}; + +=item $storage->B<get_secret>( ) + +Return our secret (Used in Message Authentication Codes). + +=cut +sub get_secret($) { + my ($self) = @_; + + return $self->{'METADATA'}->{'secret'}; +}; + +=item $storage->B<get_types>( I<$remailer> ) + +Get an array of types supported by remailer with address I<$remailer>. + +Returns undef on errors. + +¿ It may be possible that a type is returned but then has no keys. This may be +a bug, I'm not sure. + +=cut +sub get_types($$) { + my ($self, $remailer) = @_; + + defined ($self->{'METADATA'}->{'addresses'}->{$remailer}) or + Echolot::Log::cluck ("$remailer does not exist in Metadata remailer list."), + return undef; + + return () unless defined $self->{'METADATA'}->{'remailers'}->{$remailer}; + return () unless defined $self->{'METADATA'}->{'remailers'}->{$remailer}->{'keys'}; + my @types = keys %{$self->{'METADATA'}->{'remailers'}->{$remailer}->{'keys'}}; + return @types; +}; + + +=item $storage->B<has_type>( I<$remailer>, I<$type> ) + +Checks if the remailer with address I<$remailer> has type I<$type> keys. + +Returns 1 if it has, 0 if not, undef on errors. + +=cut +sub has_type($$$) { + my ($self, $remailer, $type) = @_; + + defined ($self->{'METADATA'}->{'addresses'}->{$remailer}) or + Echolot::Log::cluck ("$remailer does not exist in Metadata remailer list."), + return undef; + + return 0 unless defined $self->{'METADATA'}->{'remailers'}->{$remailer}; + return 0 unless defined $self->{'METADATA'}->{'remailers'}->{$remailer}->{'keys'}; + return 0 unless defined $self->{'METADATA'}->{'remailers'}->{$remailer}->{'keys'}->{$type}; + return 0 unless scalar keys %{$self->{'METADATA'}->{'remailers'}->{$remailer}->{'keys'}->{$type}}; + return 1; +}; + + +=item $storage->B<get_keys>( I<$remailer>, I<$type> ) + +Returns an array listing all keyids of type I<$type> of remailer with address +I<$remailer>. + +Returns undef on errors. + +=cut +sub get_keys($$$) { + my ($self, $remailer, $type) = @_; + + defined ($self->{'METADATA'}->{'addresses'}->{$remailer}) or + Echolot::Log::cluck ("$remailer does not exist in Metadata address list."), + return undef; + + defined ($self->{'METADATA'}->{'remailers'}->{$remailer}) or + Echolot::Log::cluck ("$remailer does not exist in Metadata remailer list."), + return undef; + + defined ($self->{'METADATA'}->{'remailers'}->{$remailer}->{'keys'}) or + Echolot::Log::cluck ("$remailer does not have keys in Metadata remailer list."), + return undef; + + defined ($self->{'METADATA'}->{'remailers'}->{$remailer}->{'keys'}->{$type}) or + Echolot::Log::cluck ("$remailer does not have type '$type' in Metadata remailer list."), + return undef; + + my @keys = keys %{$self->{'METADATA'}->{'remailers'}->{$remailer}->{'keys'}->{$type}}; + return @keys; +}; + + + +=item $storage->B<get_key>( I<$remailer>, I<$type>, I<$key> ) + +Returns a hash having they keys C<summary>, C<key>, C<nick>, and +C<last_updated> of the I<$type> key with id I<$key> of remailer with address +I<$remailer>. + +Returns undef on errors. + +=cut +sub get_key($$$$) { + my ($self, $remailer, $type, $key) = @_; + + defined ($self->{'METADATA'}->{'addresses'}->{$remailer}) or + Echolot::Log::cluck ("$remailer does not exist in Metadata address list."), + return undef; + + defined ($self->{'METADATA'}->{'remailers'}->{$remailer}) or + Echolot::Log::cluck ("$remailer does not exist in Metadata remailer list."), + return undef; + + defined ($self->{'METADATA'}->{'remailers'}->{$remailer}->{'keys'}) or + Echolot::Log::cluck ("$remailer does not have keys in Metadata remailer list."), + return undef; + + defined ($self->{'METADATA'}->{'remailers'}->{$remailer}->{'keys'}->{$type}) or + Echolot::Log::cluck ("$remailer does not have type '$type' in Metadata remailer list."), + return undef; + + defined ($self->{'METADATA'}->{'remailers'}->{$remailer}->{'keys'}->{$type}->{$key}) or + Echolot::Log::cluck ("$remailer does not have key '$key' in type '$type' in Metadata remailer list."), + return undef; + + my %result = ( + summary => $self->{'METADATA'}->{'remailers'}->{$remailer}->{'keys'}->{$type}->{$key}->{'summary'}, + key => $self->{'METADATA'}->{'remailers'}->{$remailer}->{'keys'}->{$type}->{$key}->{'key'}, + nick => $self->{'METADATA'}->{'remailers'}->{$remailer}->{'keys'}->{$type}->{$key}->{'nick'}, + last_update => $self->{'METADATA'}->{'remailers'}->{$remailer}->{'keys'}->{$type}->{$key}->{'last_update'} + ); + + return %result; +}; + + +=item $storage->B<get_capabilities>( I<$remailer> ) + +Return the capabilities on file for remailer with address I<$remailer>. This +is probably the one we got from remailer-conf or set manually. + +Returns undef on errors. + +=cut +sub get_capabilities($$) { + my ($self, $remailer) = @_; + + return undef unless defined $self->{'METADATA'}->{'remailers'}->{$remailer}; + return undef unless defined $self->{'METADATA'}->{'remailers'}->{$remailer}->{'conf'}; + return $self->{'METADATA'}->{'remailers'}->{$remailer}->{'conf'}->{'capabilities'}; +}; + + +=item $storage->B<get_capabilities>( I<$remailer> ) + +Return the capabilities on file for remailer with address I<$remailer>. This +is probably the one we got from remailer-conf or set manually. + +Returns undef on errors. + +=cut +sub get_nick($$) { + my ($self, $remailer) = @_; + + defined $remailer or + Echolot::Log::cluck ("Undefined remailer passed to get_nick()."), + return undef; + return undef unless defined $self->{'METADATA'}->{'remailers'}->{$remailer}; + return undef unless defined $self->{'METADATA'}->{'remailers'}->{$remailer}->{'conf'}; + return $self->{'METADATA'}->{'remailers'}->{$remailer}->{'conf'}->{'nick'}; +}; + + +=item $storage->B<expire>( ) + +Expires old keys, confs and pings from the Storage as configured by +I<expire_keys>, I<expire_confs>, and I<expire_pings>. + +See L<pingd.conf(5)> for more information on these settings. + +Returns 1 on success, undef on errors. + +=cut +sub expire($) { + my ($self) = @_; + + my $now = time(); + my $expire_keys = $now - Echolot::Config::get()->{'expire_keys'}; + my $expire_conf = $now - Echolot::Config::get()->{'expire_confs'}; + my $expire_pings = $now - Echolot::Config::get()->{'expire_pings'}; + my $expire_chainpings = $now - Echolot::Config::get()->{'expire_chainpings'}; + my $expire_fromlines = $now - Echolot::Config::get()->{'expire_fromlines'}; + + # Remailer Information and pings + for my $remailer_addr ( keys %{$self->{'METADATA'}->{'remailers'}} ) { + if (exists $self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'keys'}) { + for my $type ( keys %{$self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'keys'}} ) { + if (exists $self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'keys'}->{$type}) { + for my $key ( keys %{$self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'keys'}->{$type}} ) { + if ($self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'keys'}->{$type}->{$key}->{'last_update'} < $expire_keys) { + Echolot::Log::info("Expiring $remailer_addr, key, $type, $key."); + $self->pingdata_close_one($remailer_addr, $type, $key, 'delete'); + delete $self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'keys'}->{$type}->{$key}; + }; + }; + delete $self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'keys'}->{$type} + unless (scalar keys %{$self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'keys'}->{$type}}); + }; + }; + delete $self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'keys'} + unless (scalar keys %{$self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'keys'}}); + } + + if (exists $self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'conf'}) { + delete $self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'conf'} + if (defined $self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'conf'} && + ($self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'conf'}->{'last_update'} < $expire_conf) && + ! ($self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'conf'}->{'dont_expire'})); + } + + delete $self->{'METADATA'}->{'remailers'}->{$remailer_addr}, + next + unless ( defined ($self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'conf'}) || + defined ($self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'keys'})); + + + next unless exists $self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'keys'}; + for my $type ( keys %{$self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'keys'}} ) { + next unless exists $self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'keys'}->{$type}; + for my $key ( keys %{$self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'keys'}->{$type}} ) { + my @out = grep {$_ > $expire_pings} $self->get_pings($remailer_addr, $type, $key, 'out'); + my @done = grep {$_->[0] > $expire_pings} $self->get_pings($remailer_addr, $type, $key, 'done'); + + + # write ping to done + my $fh = $self->get_ping_fh($remailer_addr, $type, $key, 'done') or + Echolot::Log::cluck ("$remailer_addr; type=$type; key=$key has no assigned filehandle for done pings."), + return undef; + seek($fh, 0, SEEK_SET) or + Echolot::Log::warn("Cannot seek to start of $remailer_addr out pings: $!."), + return undef; + truncate($fh, 0) or + Echolot::Log::warn("Cannot truncate done pings file for remailer $remailer_addr; key=$key file to zero length: $!."), + return undef; + for my $done (@done) { + print($fh $done->[0]." ".$done->[1]."\n") or + Echolot::Log::warn("Error when writing to $remailer_addr out pings: $!."), + return undef; + }; + $fh->flush(); + + # rewrite outstanding pings + $fh = $self->get_ping_fh($remailer_addr, $type, $key, 'out') or + Echolot::Log::cluck ("$remailer_addr; type=$type; key=$key has no assigned filehandle for out pings."), + return undef; + seek($fh, 0, SEEK_SET) or + Echolot::Log::warn("Cannot seek to start of outgoing pings file for remailer $remailer_addr; key=$key: $!."), + return undef; + truncate($fh, 0) or + Echolot::Log::warn("Cannot truncate outgoing pings file for remailer $remailer_addr; key=$key file to zero length: $!."), + return undef; + print($fh (join "\n", @out), (scalar @out ? "\n" : '') ) or + Echolot::Log::warn("Error when writing to outgoing pings file for remailer $remailer_addr; key=$key file: $!."), + return undef; + $fh->flush(); + }; + }; + }; + + # Chainpings + for my $type ( keys %{$self->{'CHAINPING_FHS'}} ) { + my $pings = $self->get_chainpings($type); + + @{ $pings->{'out'} } = map { + my $a = $_; + join (' ', map ({ $a->{$_} } qw{sent addr1 type1 key1 addr2 type2 key2})) + } grep { + $_->{'sent'} > $expire_chainpings + } + @{ $pings->{'out'} }; + @{ $pings->{'done'} } = map { + my $a = $_; + join (' ', map ({ $a->{$_} } qw{sent addr1 type1 key1 addr2 type2 key2 lat})) + } grep { + $_->{'sent'} > $expire_chainpings + } + @{ $pings->{'done'} }; + + for my $dir (qw{out done}) { + my $fh = $self->get_chainping_fh($type, $dir) or + Echolot::Log::warn ("have no assigned filehandle for $type $dir chainpings."), + return undef; + seek($fh, 0, SEEK_SET) or + Echolot::Log::warn("Cannot seek to start of $dir chainpings $type $!."), + return undef; + truncate($fh, 0) or + Echolot::Log::warn("Cannot truncate $dir chainpings $type file to zero length: $!."), + return undef; + print($fh (join "\n", @{$pings->{$dir}}), (scalar @{$pings->{$dir}} ? "\n" : '') ) or + Echolot::Log::warn("Error when writing to $dir chainpings $type file: $!."), + return undef; + $fh->flush(); + }; + }; + + # From Header lines + for my $remailer_addr ( keys %{$self->{'METADATA'}->{'fromlines'}} ) { + for my $type ( keys %{$self->{'METADATA'}->{'fromlines'}->{$remailer_addr}} ) { + for my $user_supplied ( keys %{$self->{'METADATA'}->{'fromlines'}->{$remailer_addr}->{$type}} ) { + delete $self->{'METADATA'}->{'fromlines'}->{$remailer_addr}->{$type}->{$user_supplied} + if ($self->{'METADATA'}->{'fromlines'}->{$remailer_addr}->{$type}->{$user_supplied}->{'last_update'} < $expire_fromlines); + }; + delete $self->{'METADATA'}->{'fromlines'}->{$remailer_addr}->{$type} + unless (scalar keys %{$self->{'METADATA'}->{'fromlines'}->{$remailer_addr}->{$type}}); + }; + delete $self->{'METADATA'}->{'fromlines'}->{$remailer_addr} + unless (scalar keys %{$self->{'METADATA'}->{'fromlines'}->{$remailer_addr}}); + }; + + $self->commit(); + + return 1; +}; + +=item $storage->B<delete_remailer>( I<$address> ) + +Delete all data on the remailer with I<$address>. This includes stored conf +and key information, pings and the remailer's settings like I<pingit> et al. + +If this remailer is still referenced by other remailers' remailer-conf reply it +is likely to get picked up again. + +Returns 1. + +=cut +sub delete_remailer($$) { + my ($self, $address) = @_; + + Echolot::Log::notice("Deleting remailer $address."); + + if (defined $self->{'METADATA'}->{'addresses'}->{$address}) { + delete $self->{'METADATA'}->{'addresses'}->{$address} + } else { + Echolot::Log::cluck("Remailer $address does not exist in addresses.") + }; + + if (defined $self->{'METADATA'}->{'remailers'}->{$address}) { + for my $type ( keys %{$self->{'METADATA'}->{'remailers'}->{$address}->{'keys'}} ) { + for my $key ( keys %{$self->{'METADATA'}->{'remailers'}->{$address}->{'keys'}->{$type}} ) { + $self->pingdata_close_one($address, $type, $key, 'delete'); + }; + }; + + delete $self->{'METADATA'}->{'remailers'}->{$address} + }; + + delete $self->{'METADATA'}->{'fromlines'}->{$address} + if (defined $self->{'METADATA'}->{'fromlines'}->{$address}); + + $self->commit(); + + return 1; +}; + +=item $storage->B<delete_remailercaps>( I<$address> ) + +Delete conf data of the remailer with I<$address>. + +Returns 1. + +=cut +sub delete_remailercaps($$) { + my ($self, $address) = @_; + + Echolot::Log::info("Deleting conf for remailer $address."); + + if (defined $self->{'METADATA'}->{'remailers'}->{$address}) { + delete $self->{'METADATA'}->{'remailers'}->{$address}->{'conf'} + if defined $self->{'METADATA'}->{'remailers'}->{$address}->{'conf'}; + } else { + Echolot::Log::cluck("Remailer $address does not exist in remailers.") + }; + $self->commit(); + + return 1; +}; + + +=item $storage->B<register_fromline>( I<$address>, I<$with_from>, I<$from>, $I<disclaimer_top>, $I<disclaimer_bot> ) + +Register that the remailer I<$address> returned the From header +line I<$from>. If I<$with_from> is 1 we had tried to supply our own +From, otherwise not. + +$I<disclaimer_top> and $I<disclaimer_bot> are boolean variables indicating +presence or absense of any disclaimer. + +Returns 1, undef on error. + +=cut + +sub register_fromline($$$$$$$) { + my ($self, $address, $type, $with_from, $from, $top, $bot) = @_; + + defined ($self->{'METADATA'}->{'addresses'}->{$address}) or + Echolot::Log::cluck ("$address does not exist in Metadata address list."), + return undef; + defined ($from) or + Echolot::Log::cluck ("from is not defined in register_fromline."), + return undef; + defined ($with_from) or + Echolot::Log::cluck ("from is not defined in register_fromline."), + return undef; + ($with_from == 0 || $with_from == 1) or + Echolot::Log::cluck ("with_from has evil value $with_from in register_fromline."), + return undef; + + Echolot::Log::debug("registering fromline $address, $type, $with_from, $from, $top, $bot."); + + $self->{'METADATA'}->{'fromlines'}->{$address}->{$type}->{$with_from} = { + last_update => time(), + from => $from, + disclaim_top => $top, + disclaim_bot => $bot, + }; + $self->commit(); + + return 1; +}; + + +=item $storage->B<get_fromline>( I<$addr>, I<$type>, I<$user_supplied> ) + +Return a hash reference with header From line information. + +The hash has two keys, B<last_update> and B<from>, which holds the actual information. + +If there is no from line registered for the given combination, undef is returned. + +On Error, also undef is returned. + +=cut + +sub get_fromline($$$$) { + my ($self, $addr, $type, $user_supplied) = @_; + + defined $self->{'METADATA'}->{'fromlines'}->{$addr} or + return undef; + defined $self->{'METADATA'}->{'fromlines'}->{$addr}->{$type} or + return undef; + defined $self->{'METADATA'}->{'fromlines'}->{$addr}->{$type}->{$user_supplied} or + return undef; + + defined $self->{'METADATA'}->{'fromlines'}->{$addr}->{$type}->{$user_supplied}->{'last_update'} or + Echolot::Log::cluck ("last_update is undefined with $addr $type $user_supplied."), + return undef; + defined $self->{'METADATA'}->{'fromlines'}->{$addr}->{$type}->{$user_supplied}->{'from'} or + Echolot::Log::cluck ("from is undefined with $addr $type $user_supplied."), + return undef; + + return { last_update => $self->{'METADATA'}->{'fromlines'}->{$addr}->{$type}->{$user_supplied}->{'last_update'}, + from => $self->{'METADATA'}->{'fromlines'}->{$addr}->{$type}->{$user_supplied}->{'from'}, + disclaim_top => $self->{'METADATA'}->{'fromlines'}->{$addr}->{$type}->{$user_supplied}->{'disclaim_top'}, + disclaim_bot => $self->{'METADATA'}->{'fromlines'}->{$addr}->{$type}->{$user_supplied}->{'disclaim_bot'} }; +} + + +# sub convert($) { +# my ($self) = @_; +# +# for my $remailer_addr ( keys %{$self->{'METADATA'}->{'remailers'}} ) { +# for my $type ( keys %{$self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'keys'}} ) { +# for my $key ( keys %{$self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'keys'}->{$type}} ) { +# if (defined $self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'stats'}->{$type}->{$key}) { +# $self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'keys'}->{$type}->{$key}->{'stats'} = +# $self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'stats'}->{$type}->{$key}; +# }; +# }; +# }; +# delete $self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'stats'}; +# }; +# +# $self->commit(); +# }; +# +# sub convert($) { +# my ($self) = @_; +# +# for my $remailer_addr ( keys %{$self->{'METADATA'}->{'addresses'}} ) { +# $self->{'METADATA'}->{'addresses'}->{$remailer_addr}->{'fetch'} = 1; +# $self->{'METADATA'}->{'addresses'}->{$remailer_addr}->{'pingit'} = 1; +# $self->{'METADATA'}->{'addresses'}->{$remailer_addr}->{'showit'} = 0; +# delete $self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'pingit'}; +# delete $self->{'METADATA'}->{'remailers'}->{$remailer_addr}->{'showit'}; +# }; +# +# $self->commit(); +# }; + +=back + +=cut + +# vim: set ts=4 shiftwidth=4: diff --git a/trunk/Echolot/Thesaurus.pm b/trunk/Echolot/Thesaurus.pm new file mode 100644 index 0000000..0ff207a --- /dev/null +++ b/trunk/Echolot/Thesaurus.pm @@ -0,0 +1,144 @@ +package Echolot::Thesaurus; + +# +# $Id$ +# +# This file is part of Echolot - a Pinger for anonymous remailers. +# +# Copyright (c) 2002, 2003, 2004 Peter Palfrader <peter@palfrader.org> +# +# This program is free software. you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +=pod + +=head1 Name + +Echolot::Thesaurus - build thesaurus pages + +=head1 DESCRIPTION + +This package provides necessary functions for the thesaurus. + +=cut + +use strict; +use English; +use Echolot::Log; + + +sub save_thesaurus($$$) { + my ($otype, $oid, $data) = @_; + + return 1 unless Echolot::Config::get()->{'thesaurus'}; + + my ($type) = $otype =~ /^([a-z-]+)$/; + Echolot::Log::cluck("type '$otype' is not clean in save_thesaurus."), return 0 unless defined $type; + my ($id) = $oid =~ /^([0-9]+)$/; + Echolot::Log::cluck("id '$oid' is not clean in save_thesaurus."), return 0 unless defined $id; + + my $file = Echolot::Config::get()->{'thesaurusdir'}.'/'.$id.'.'.$type; + open (F, ">$file") or + Echolot::Log::warn ("Cannot open '$file': $!."), + return 0; + print F $data; + close (F); + + return 1; +}; + +sub build_thesaurus() { + return 1 unless Echolot::Config::get()->{'thesaurus'}; + + my $dir = Echolot::Config::get()->{'thesaurusdir'}; + opendir(DIR, $dir) or + Echolot::Log::warn ("Cannot open '$dir': $!."), + return 0; + my @files = grep { ! /^\./ } readdir(DIR); + closedir(DIR); + + + my $expire_date = time() - Echolot::Config::get()->{'expire_thesaurus'}; + + my $data; + for my $filename (@files) { + my ($id, $what) = $filename =~ /^(\d+)\.(adminkey|conf|help|key|stats)$/; + next unless (defined $id && defined $what); + + my $remailer = Echolot::Globals::get()->{'storage'}->get_address_by_id($id); + next unless defined $remailer; + next unless $remailer->{'showit'}; + my $caps = Echolot::Globals::get()->{'storage'}->get_capabilities($remailer->{'address'}); + next unless defined $caps; + next unless $caps !~ m/\btesting\b/i; + + my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size, + $atime,$mtime,$ctime,$blksize,$blocks) + = stat($dir.'/'.$filename); + + if ($mtime < $expire_date) { + unlink ($dir.'/'.$filename) or + Echolot::Log::warn("Cannot unlink expired $filename."); + Echolot::Log::info("Expired thesaurus file $filename."); + next; + }; + + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) + = gmtime($mtime); + + my $date = sprintf("%04d-%02d-%02d", $year+1900, $mon+1, $mday); + my $time = sprintf("%02d:%02d", $hour, $min); + + + $data->{$remailer->{'address'}}->{$what.'_href'} = $filename; + $data->{$remailer->{'address'}}->{$what.'_date'} = $date; + $data->{$remailer->{'address'}}->{$what.'_time'} = $time; + $data->{$remailer->{'address'}}->{'id'} = $id; + }; + + + for my $addr (keys (%$data)) { + my $nick = Echolot::Globals::get()->{'storage'}->get_nick($addr); + if (defined $nick) { + $data->{$addr}->{'nick'} = $nick; + $data->{$addr}->{'address'} = $addr; + } else { + delete $data->{$addr}; + }; + }; + + my @data = map {$data->{$_}} (sort { $data->{$a}->{'nick'} cmp $data->{$b}->{'nick'} } keys (%$data)); + + + Echolot::Tools::write_HTML_file( + Echolot::Config::get()->{'thesaurusindexfile'}, + 'thesaurusindexfile', + Echolot::Config::get()->{'buildthesaurus'}, + remailers => \@data); + + open(F, ">$dir/index.txt") or + Echolot::Log::warn ("Cannot open '$dir/index.txt': $!."), + return 0; + for my $remailer (@data) { + printf F "%s\t%s\t%s\n", $remailer->{'nick'}, $remailer->{'id'}, $remailer->{'address'}; + }; + close(F) or + Echolot::Log::warn ("Cannot close '$dir/index.txt': $!."), + return 0; +}; + + +1; +# vim: set ts=4 shiftwidth=4: diff --git a/trunk/Echolot/Tools.pm b/trunk/Echolot/Tools.pm new file mode 100644 index 0000000..59d887f --- /dev/null +++ b/trunk/Echolot/Tools.pm @@ -0,0 +1,476 @@ +package Echolot::Tools; + +# +# $Id$ +# +# This file is part of Echolot - a Pinger for anonymous remailers. +# +# Copyright (c) 2002, 2003, 2004 Peter Palfrader <peter@palfrader.org> +# +# This program is free software. you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +=pod + +=head1 Name + +Echolot::Tools - Tools for echolot + +=head1 DESCRIPTION + + +=cut + +use strict; +use HTML::Template; +use Digest::MD5 qw{}; +use IO::Select; +use IO::Handle; +use GnuPG::Interface; +use Echolot::Log; +use English; + +sub hash($) { + my ($data) = @_; + ($data) = $data =~ m/(.*)/s; # untaint + my $hash = Digest::MD5::md5_hex($data); + return $hash; +}; + +sub make_random($;%) { + my ($length, %args) = @_; + + my $random; + + open (FH, Echolot::Config::get()->{'dev_random'}) or + Echolot::Log::warn("Cannot open ".Echolot::Config::get()->{'dev_random'}." for reading: $!."), + return 0; + read(FH, $random, $length) or + Echolot::Log::warn("Cannot read from ".Echolot::Config::get()->{'dev_random'}.": $!."), + return 0; + close (FH) or + Echolot::Log::warn("Cannot close ".Echolot::Config::get()->{'dev_random'}.": $!."), + return 0; + + $random = unpack('H*', $random) + if ($args{'armor'} == 1); + + return $random; +}; + +sub make_mac($) { + my ($token) = @_; + + my $mac = hash($token . Echolot::Globals::get()->{'storage'}->get_secret() ); + return $mac; +}; + +sub makeShortNumHash($) { + my ($text) = @_; + + my $hash = Echolot::Tools::make_mac($text); + $hash = substr($hash, 0, 4); + my $sum = hex($hash); + return $sum; +}; + +sub verify_mac($$) { + my ($token, $mac) = @_; + + return (hash($token . Echolot::Globals::get()->{'storage'}->get_secret() ) eq $mac); +}; + +sub make_address($) { + my ($subsystem) = @_; + + my $token = $subsystem.'='.time(); + my $hash = hash($token . Echolot::Globals::get()->{'storage'}->get_secret() ); + my $cut_hash = substr($hash, 0, Echolot::Config::get()->{'hash_len'}); + my $complete_token = $token.'='.$cut_hash; + my $address = Echolot::Config::get()->{'recipient_delimiter'} ne ''? + Echolot::Config::get()->{'my_localpart'}. + Echolot::Config::get()->{'recipient_delimiter'}. + $complete_token. + '@'. + Echolot::Config::get()->{'my_domain'} + : + Echolot::Config::get()->{'my_localpart'}. + '@'. + Echolot::Config::get()->{'my_domain'}. + '('. + $complete_token. + ')'; + + return $address; +}; + +sub verify_address_tokens($) { + my ($address) = @_; + + my ($type, $timestamp, $received_hash); + if (Echolot::Config::get()->{'recipient_delimiter'} ne '') { + my $delimiter = quotemeta( Echolot::Config::get()->{'recipient_delimiter'}); + ($type, $timestamp, $received_hash) = $address =~ /$delimiter (.*) = (\d+) = ([0-9a-f]+) @/x or + ($type, $timestamp, $received_hash) = $address =~ /\( (.*) = (\d+) = ([0-9a-f]+) \)/x or + Echolot::Log::debug("Could not parse to header '$address'."), + return undef; + } else { + ($type, $timestamp, $received_hash) = $address =~ /\( (.*) = (\d+) = ([0-9a-f]+) \)/x or + Echolot::Log::debug("Could not parse to header '$address'."), + return undef; + }; + + my $token = $type.'='.$timestamp; + my $hash = Echolot::Tools::hash($token . Echolot::Globals::get()->{'storage'}->get_secret() ); + my $cut_hash = substr($hash, 0, Echolot::Config::get()->{'hash_len'}); + + ($cut_hash eq $received_hash) or + Echolot::Log::info("Hash mismatch in '$address'."), + return undef; + + return + { timestamp => $timestamp, + token => $type }; +}; + +sub send_message(%) { + my (%args) = @_; + + defined($args{'To'}) or + Echolot::Log::cluck ('No recipient address given.'), + return 0; + $args{'Subject'} = '(no subject)' unless (defined $args{'Subject'}); + $args{'Body'} = '' unless (defined $args{'Body'}); + $args{'From_'} = + Echolot::Config::get()->{'my_localpart'}. + '@'. + Echolot::Config::get()->{'my_domain'}; + if (defined $args{'Token'}) { + $args{'From'} = make_address( $args{'Token'} ); + } else { + $args{'From'} = $args{'From_'}; + }; + $args{'Subject'} = 'none' unless (defined $args{'Subject'}); + + my @lines = map { $_."\n" } split (/\r?\n/, $args{'Body'}); + + open(SENDMAIL, '|'.Echolot::Config::get()->{'sendmail'}.' -f '.$args{'From_'}.' -t') + or Echolot::Log::warn("Cannot run sendmail: $!."), + return 0; + printf SENDMAIL "From: %s\n", $args{'From'}; + printf SENDMAIL "To: %s\n", $args{'To'}; + printf SENDMAIL "Subject: %s\n", $args{'Subject'}; + printf SENDMAIL "\n"; + for my $line (@lines) { + print SENDMAIL $line; + }; + close SENDMAIL; + + return 1; +}; + +sub make_monthname($) { + my ($month) = @_; + my @MON = qw{Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec}; + return $MON[$month]; +}; + +sub make_dayname($) { + my ($day) = @_; + my @WDAY = qw{Sun Mon Tue Wed Thu Fri Sat}; + return $WDAY[$day]; +}; + +sub date822($) { + my ($date) = @_; + + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) = gmtime($date); + # 14 Aug 2002 17:11:12 +0100 + return sprintf("%s, %02d %s %d %02d:%02d:%02d +0000", + make_dayname($wday), + $mday, + make_monthname($mon), + $year + 1900, + $hour, + $min, + $sec); +}; + +sub write_meta_information($%) { + my ($file, %data) = @_; + + return 1 unless Echolot::Config::get()->{'write_meta_files'}; + + $file .= Echolot::Config::get()->{'meta_extension'}; + open (F, ">$file") or + Echolot::Log::warn ("Cannot open $file: $!."), + return 0; + if (defined $data{'Expires'}) { + my $date = date822($data{'Expires'}); + print F "Expires: $date\n"; + }; + close(F); + return 1; +}; + +sub escape_HTML_entities($) { + my ($in) = @_; + + $in =~ s/&/&/; + $in =~ s/"/"/; + $in =~ s/</</; + $in =~ s/>/>/; + + return $in; +}; + +sub write_HTML_file($$;$%) { + my ($origfile, $template_file, $expire, %templateparams) = @_; + + my $operator = Echolot::Config::get()->{'operator_address'}; + $operator =~ s/@/./; + + for my $lang ( keys %{Echolot::Config::get()->{'templates'}} ) { + my $template = HTML::Template->new( + filename => Echolot::Config::get()->{'templates'}->{$lang}->{$template_file}, + strict => 0, + die_on_bad_params => 0, + global_vars => 1 ); + $template->param ( %templateparams ); + $template->param ( CURRENT_TIMESTAMP => scalar gmtime() ); + $template->param ( SITE_NAME => Echolot::Config::get()->{'sitename'} ); + $template->param ( separate_rlist => Echolot::Config::get()->{'separate_rlists'} ); + $template->param ( combined_list => Echolot::Config::get()->{'combined_list'} ); + $template->param ( thesaurus => Echolot::Config::get()->{'thesaurus'} ); + $template->param ( fromlines => Echolot::Config::get()->{'fromlines'} ); + $template->param ( version => Echolot::Globals::get()->{'version'} ); + $template->param ( operator => $operator ); + $template->param ( expires => date822( time + $expire )); + + my $file = $origfile; + $file .= '.'.$lang unless ($lang eq 'default'); + $file .= '.html'; + + open(F, '>'.$file) or + Echolot::Log::warn("Cannot open $file: $!."), + return 0; + print F $template->output() or + Echolot::Log::warn("Cannot print to $file: $!."), + return 0; + close (F) or + Echolot::Log::warn("Cannot close $file: $!."), + return 0; + + if (defined $expire) { + write_meta_information($file, + Expires => time + $expire) or + Echolot::Log::debug ("Error while writing meta information for $file."), + return 0; + }; + }; + + return 1; +}; + +sub make_gpg_fds() { + my %fds = ( + stdin => IO::Handle->new(), + stdout => IO::Handle->new(), + stderr => IO::Handle->new(), + status => IO::Handle->new() ); + my $handles = GnuPG::Handles->new( %fds ); + return ($fds{'stdin'}, $fds{'stdout'}, $fds{'stderr'}, $fds{'status'}, $handles); +}; + +sub readwrite_gpg($$$$$) { + my ($in, $inputfd, $stdoutfd, $stderrfd, $statusfd) = @_; + + Echolot::Log::trace("Entering readwrite_gpg."); + + local $INPUT_RECORD_SEPARATOR = undef; + my $sout = IO::Select->new(); + my $sin = IO::Select->new(); + my $offset = 0; + + Echolot::Log::trace("input is $inputfd; output is $stdoutfd; err is $stderrfd; status is ".(defined $statusfd ? $statusfd : 'undef')."."); + + $inputfd->blocking(0); + $stdoutfd->blocking(0); + $statusfd->blocking(0) if defined $statusfd; + $stderrfd->blocking(0); + $sout->add($stdoutfd); + $sout->add($stderrfd); + $sout->add($statusfd) if defined $statusfd; + $sin->add($inputfd); + + my ($stdout, $stderr, $status) = ("", "", ""); + + my ($readyr, $readyw); + while ($sout->count() > 0 || (defined($sin) && ($sin->count() > 0))) { + Echolot::Log::trace("select waiting for ".($sout->count())." fds."); + ($readyr, $readyw, undef) = IO::Select::select($sout, $sin, undef, 42); + Echolot::Log::trace("ready: write: ".(defined $readyw ? scalar @$readyw : 'none')."; read: ".(defined $readyr ? scalar @$readyr : 'none')); + for my $wfd (@$readyw) { + Echolot::Log::trace("writing to $wfd."); + my $written = 0; + if ($offset != length($in)) { + $written = $wfd->syswrite($in, length($in) - $offset, $offset); + } + unless (defined ($written)) { + Echolot::Log::warn("Error while writing to GnuPG: $!"); + close $wfd; + $sin->remove($wfd); + $sin = undef; + } else { + $offset += $written; + if ($offset == length($in)) { + Echolot::Log::trace("writing to $wfd done."); + close $wfd; + $sin->remove($wfd); + $sin = undef; + } + } + } + + next unless (defined(@$readyr)); # Wait some more. + + for my $rfd (@$readyr) { + if ($rfd->eof) { + Echolot::Log::trace("reading from $rfd done."); + $sout->remove($rfd); + close($rfd); + next; + } + Echolot::Log::trace("reading from $rfd."); + if ($rfd == $stdoutfd) { + $stdout .= <$rfd>; + next; + } + if (defined $statusfd && $rfd == $statusfd) { + $status .= <$rfd>; + next; + } + if ($rfd == $stderrfd) { + $stderr .= <$rfd>; + next; + } + } + } + Echolot::Log::trace("readwrite_gpg done."); + return ($stdout, $stderr, $status); +}; + +sub crypt_symmetrically($$) { + my ($msg, $direction) = @_; + + ($direction eq 'encrypt' || $direction eq 'decrypt') or + Echolot::Log::cluck("Wrong argument direction '$direction' passed to crypt_symmetrically."), + return undef; + + my $GnuPG = new GnuPG::Interface; + $GnuPG->call( Echolot::Config::get()->{'gnupg'} ) if (Echolot::Config::get()->{'gnupg'}); + $GnuPG->options->hash_init( + armor => 1, + homedir => Echolot::Config::get()->{'gnupghome'} ); + $GnuPG->options->meta_interactive( 0 ); + $GnuPG->passphrase( Echolot::Globals::get()->{'storage'}->get_secret() ); + + my ( $stdin_fh, $stdout_fh, $stderr_fh, $status_fh, $handles ) = make_gpg_fds(); + my $pid = + $direction eq 'encrypt' ? + $GnuPG->encrypt_symmetrically( handles => $handles ) : + $GnuPG->decrypt( handles => $handles ); + my ($stdout, $stderr, $status) = readwrite_gpg($msg, $stdin_fh, $stdout_fh, $stderr_fh, $status_fh); + waitpid $pid, 0; + + if ($direction eq 'encrypt') { + (($status =~ /^\[GNUPG:\] BEGIN_ENCRYPTION\s/m) && + ($status =~ /^\[GNUPG:\] END_ENCRYPTION\s/m)) or + Echolot::Log::info("GnuPG status '$status' didn't indicate message was encrypted correctly (stderr: $stderr). Returning."), + return undef; + } elsif ($direction eq 'decrypt') { + (($status =~ /^\[GNUPG:\] BEGIN_DECRYPTION\s/m) && + ($status =~ /^\[GNUPG:\] DECRYPTION_OKAY\s/m) && + ($status =~ /^\[GNUPG:\] END_DECRYPTION\s/m)) or + Echolot::Log::info("GnuPG status '$status' didn't indicate message was decrypted correctly (stderr: $stderr). Returning."), + return undef; + }; + + my $result = $stdout; + $result =~ s,^Version: .*$,Version: N/A,m; + return $result; +}; + +sub make_garbage() { + + my $file = Echolot::Config::get()->{'dev_urandom'}; + open(FH, $file) or + Echolot::Log::warn("Cannot open $file: $!."), + return ""; + my $random = ''; + my $want = int(rand(int(Echolot::Config::get()->{'random_garbage'} / 2))); + my $i = 0; + while ($want > 0) { + my $buf; + $want -= read(FH, $buf, $want); + $random .= $buf; + ($i++ > 15 && $want > 0) and + Echolot::Log::warn("Could not get enough garbage (still missing $want."), + last; + }; + close (FH) or + Echolot::Log::warn("Cannot close $file: $!."); + + $random = unpack("H*", $random); + $random = join "\n", grep { $_ ne '' } (split /(.{64})/, $random); + $random = "-----BEGIN GARBAGE-----\n". + $random."\n". + "-----END GARBAGE-----\n"; + + return $random; +}; + +sub read_file($;$) { + my ($name, $fail_ok) = @_; + + unless (open (F, $name)) { + Echolot::Log::warn("Could not open '$name': $!.") unless ($fail_ok); + return undef; + }; + local $/ = undef; + my $result = <F>; + close (F); + + return $result; +}; + +sub cleanup_tmp() { + my $tmpdir = Echolot::Config::get()->{'tmpdir'}; + + opendir(DIR, $tmpdir) or + Echolot::Log::warn("Could not open '$tmpdir': $!."), + return undef; + my @files = grep { ! /^[.]/ } readdir(DIR); + closedir(DIR); + + for my $file (@files) { + unlink($tmpdir.'/'.$file) or + Echolot::Log::warn("Could not unlink '$tmpdir/$file': $!."); + }; +}; + +1; + +# vim: set ts=4 shiftwidth=4: |