#!/usr/bin/perl -sw
##
## Razor2::Client::Core - Vipul's Razor Client API
##
## Copyright (c) 2001, Vipul Ved Prakash. All rights reserved.
## This code is free software; you can redistribute it and/or modify
## it under the same terms as Perl itself.
##
## $Id: Core.pm,v 1.92 2006/05/27 00:00:53 rsoderberg Exp $
package Razor2::Client::Core;
use strict;
use IO::Socket;
use IO::Select;
use Errno qw(:POSIX);
use Razor2::Client::Version;
use Data::Dumper;
use base qw(Razor2::String);
use base qw(Razor2::Logger);
use base qw(Razor2::Client::Engine);
use base qw(Razor2::Errorhandler);
use Razor2::Client::Version;
use Razor2::String qw(hextobase64 makesis parsesis hmac_sha1 xor_key
prep_mail debugobj to_batched_query
from_batched_query hexbits2hash
fisher_yates_shuffle);
our ($VERSION) = do { my @r = ( q$Revision: 1.92 $ =~ /\d+/g ); sprintf "%d." . "%02d" x $#r, @r };
our $PROTOCOL = $Razor2::Client::Version::PROTOCOL;
sub new {
my ( $class, $conf, %params ) = @_;
my $self = {};
bless $self, $class;
$self->debug("Razor Agents $VERSION, protocol version $PROTOCOL.");
return $self;
}
#
# We store server-specific config info for each server we know about.
# All info about razor servers is stored in $self->{s}.
#
# Basically we get the server name/ip from {list},
# load that server's specific info from {allconfs} into {conf},
# and do stuff. If server is no good, we get nextserver from {list}
#
# $self->{s}->{list} ptr to {nomination} if report,revoke; or {catalogue} if check
# or the cmd-line server (-rs server)
# $self->{s}->{new_list} set to 1 when discover gets new lists
# $self->{s}->{catalogue} array ref containing catalogue servers
# $self->{s}->{nomination}array ref containing nomination servers
# $self->{s}->{discovery} array ref containing discovery servers
#
# $self->{s}->{modified} array ref containing servers whose .conf needs updating
# $self->{s}->{modified_lst} array ref containing which .lst files need updating
#
# $self->{s}->{ip} string containing ip (or dns name) of current server from {list})
# $self->{s}->{port} string containing port, taken from server:port from {list}
# $self->{s}->{engines} engines supported, derived from {conf}->{se}
# $self->{s}->{conf} hash ref containing current server's config params
# read from $razorhome/server.$ip.conf
#
# $self->{s}->{allconfs} hash ref of all servers' configs. key={ip}, val={conf}
# as read from server.*.conf file
#
# $self->{s}->{listfile} string containing path/file of server.lst, either
# nomination or catalogue depending $self->{breed}
# $self->{conf}->{listfile_discovery} string containing path/file of discovery server
#
# NOTE: if we are razor-check, server is Catalogue Server
# otherwise server is Nomination server.
#
# everytime we update our server list, $self->{s}->{list};
# we want to write that to disk - $self->{s}->{listfile}
#
sub nextserver {
my ($self) = @_;
$self->log( 16, "entered nextserver" );
# see if we need to discover (.lst files might be too old)
$self->discover() or return $self->errprefix("nextserver");
# first time we don't remove from list
# or if we've rediscovered.
shift @{ $self->{s}->{list} } unless ( $self->{s}->{new_list} || !$self->{s}->{ip} );
$self->{s}->{new_list} = 0;
my $next = ${ $self->{s}->{list} }[0];
# do we ever want to put current back on the end of list?
# push @{$self->{s}->{list}}, $self->{s}->{ip};
if ($next) {
( $self->{s}->{port} ) = $next =~ /:(.*)$/;
$next =~ s/:.*$//; # optional
$self->{s}->{ip} = $next; # ip can be IP or DNS name
$self->{s}->{port} ||= $self->{conf}->{port} || 2703;
$self->{s}->{conf} = $self->{s}->{allconfs}->{$next};
my $svrport = "$self->{s}->{ip}:$self->{s}->{port}";
# get rid of server specific stuff
delete $self->{s}->{greeting};
unless ( ref( $self->{s}->{conf} ) ) {
# never used this server before, no cached info. go get it!
$self->{s}->{conf} = {};
$self->connect; # calls parse_greeting which calls compute_server_conf
}
else {
$self->compute_server_conf(1); # computes supported engines, logs info
}
$self->writeservers();
my $srl = defined( $self->{s}->{conf}->{srl} ) ? $self->{s}->{conf}->{srl} : "<unknown>";
$self->log( 8, "Using next closest server $svrport, cached info srl $srl" );
#$self->logobj(11, "Using next closest server $svrport, cached info", $self->{s}->{conf});
return 1;
}
else {
return $self->error("Razor server $self->{opt}->{server} not available at this time")
if $self->{opt}->{server};
$self->{force_discovery} = 1;
if ( $self->{done_discovery} && !( $self->discover ) ) {
return $self->errprefix("No Razor servers available at this time");
}
return $self->nextserver;
}
}
sub load_at_runtime {
my ( $self, $class, $sub, $args ) = @_;
$sub = 'new' unless defined $sub;
$args = "" unless defined $args;
eval "use $class";
if ($@) {
$self->log( 2, "$class not found, please to fix." );
return $self->error("\n\n$@");
}
my $evalstr;
if ( $sub && $sub ne "new" ) {
$evalstr = $class . "::$sub($args);";
}
else {
$evalstr = $class . "->new($args)";
}
if ( my $dude = eval $evalstr ) {
$self->log( 12, "Found and evaled $evalstr ==> $dude" );
return $dude;
}
else {
$self->log( 5, "Found but problem (bad args?) with $evalstr" );
return $self->error("Problem with $evalstr");
}
}
#
# uses DNS to find Discovery servers
# puts discovery servers in $self->{s}->{discovery}
#
sub bootstrap_discovery {
my ($self) = @_;
$self->log( 16, "entered bootstrap_discovery" );
if ( $self->{conf}->{server} ) {
$self->log( 8, "no bootstap_discovery when cmd-line server specified" );
return 1;
}
unless ( $self->{force_bootstrap_discovery} ) {
if ( ref( $self->{s}->{discovery} ) && scalar( @{ $self->{s}->{discovery} } ) ) {
$self->log( 8, "already have " . scalar( @{ $self->{s}->{discovery} } ) . " discovery servers" );
return 1;
}
elsif ( $self->{done_bootstrap} ) {
# if we've done it before {s}->{discovery} should be set
$self->log( 8, "already have done bootstrap_discovery" );
return 1;
}
}
unless ( defined $self->{conf}->{listfile_discovery} ) {
$self->log( 6, "discovery listfile not defined!" );
}
elsif ( -s $self->{conf}->{listfile_discovery} ) {
my $wait = $self->{conf}->{rediscovery_wait_dns} || 604800; # 604800 secs == 7 days
my $randomize = int( rand( $wait / 7 ) );
my $timeleft = ( ( stat( $self->{conf}->{listfile_discovery} ) )[9] + $wait - $randomize ) - time;
if ( $timeleft > 0 ) {
$self->log( 7, "$timeleft seconds before soonest DNS discovery" );
return 1 unless $self->{force_bootstrap_discovery};
$self->log( 5, "forcing DNS discovery" );
}
else {
$self->log( 5, "DNS discovery overdue by " . ( 0 - $timeleft ) . " seconds" );
}
}
else {
if ( -e $self->{conf}->{listfile_discovery} ) {
$self->log( 6, "empty discovery listfile: $self->{conf}->{listfile_discovery}" );
}
else {
$self->log( 6, "no discovery listfile: $self->{conf}->{listfile_discovery}" );
}
}
$self->{s}->{discovery} = [ $self->{conf}->{razordiscovery} ];
push @{ $self->{s}->{modified_lst} }, "discovery";
return 1;
}
#
# uses Discovery Servers to find closest Nomination/Catalogue Servers.
# called every day or so of if .lst file is empty
#
# puts servers in $self->{s}->{list}
#
sub discover {
my ($self) = @_;
$self->log( 16, "entered discover" );
#
# do we need to discover?
#
# no discover if cmd-line server
return 1 if $self->{opt}->{server};
#
# don't discover if conf says turn_off_discovery (unless force_discovery)
#
return 1 if $self->{conf}->{turn_off_discovery} && ( !( $self->{force_discovery} ) );
return $self->error("No Razor servers available at this time")
if $self->{done_discovery};
# so if user has their own servers, and they are temporarily down, force_discovery.
# good: shit will work
# bad: it will erase their custom server*.lst file
#
unless ( defined $self->{s}->{listfile} ) {
$self->debug("listfile not defined!");
}
elsif ( -s $self->{s}->{listfile} ) {
my $randomize = int( rand( $self->{conf}->{rediscovery_wait} / 7 ) );
my $timeleft = ( ( stat( $self->{s}->{listfile} ) )[9] + $self->{conf}->{rediscovery_wait} - $randomize ) - time;
if ( $timeleft > 0 ) {
$self->debug("$timeleft seconds before closest server discovery");
return 1 unless $self->{force_discovery};
$self->debug("forcing discovery");
}
else {
$self->debug( "server discovery overdue by " . ( 0 - $timeleft ) . " seconds" );
}
}
else {
if ( -e $self->{s}->{listfile} ) {
$self->debug("empty listfile: $self->{s}->{listfile}");
}
else {
$self->debug("no listfile: $self->{s}->{listfile}");
}
}
#
# we need to discover.
#
return $self->errprefix("discover0") unless $self->bootstrap_discovery();
#
# Go ahead and do discovery for both csl and nsl.
#
my %stype = ( csl => 'catalogue', nsl => 'nomination' );
my $srvs = { csl => {}, nsl => {} };
my $list_orig = $self->{s}->{list};
$self->{s}->{list} = $self->{s}->{discovery};
foreach ( @{ $self->{s}->{discovery} } ) {
unless ( defined $_ ) {
$self->log( 5, "Razor Discovery Server not defined!" );
next;
}
$self->log( 8, "Checking with Razor Discovery Server $_" );
unless ( $self->connect( server => $_, discovery_server => 1 ) ) {
$self->log( 5, "Razor Discovery Server $_ is unreachable" );
next;
}
foreach my $querytype (qw(csl nsl)) {
my $query = "a=g&pm=$querytype\r\n";
my $resp = $self->_send( [$query] );
unless ($resp) {
$self->{s}->{list} = $list_orig;
return $self->errprefix("discover1");
}
# from_batched_query wants "-" in beginning, but not ".\r\n" at end
$resp->[0] =~ s/\.\r\n$//sg;
my $h = from_batched_query( $resp->[0], {} );
foreach my $href (@$h) {
next unless $href->{$querytype};
$self->log( 8, "Discovery Server $_ replying with $querytype=$href->{$querytype}" );
$srvs->{$querytype}->{ $href->{$querytype} } = 1;
}
unless ( keys %{ $srvs->{$querytype} } ) {
$self->log( 5, "Razor Discovery Server $_ had no valid $querytype servers" );
next;
}
}
}
$self->{s}->{list} = $list_orig;
foreach my $querytype (qw(csl nsl)) {
my @list = keys %{ $srvs->{$querytype} };
#return $self->error("Could not get valid info from Discovery Servers")
# unless @list;
unless (@list) {
if ( $self->{force_bootstrap_discovery} ) {
return $self->error("Bootstrap discovery failed. Giving up.");
}
$self->log( 5, "Couldn't talk to discovery servers. Will force a bootstrap..." );
$self->{force_bootstrap_discovery} = 1;
return $self->error("Bootstrap discovery failed. Giving up.") unless $self->bootstrap_discovery();
return $self->discover();
}
fisher_yates_shuffle( \@list ) if @list > 1;
$self->{s}->{ $stype{$querytype} } = \@list;
push @{ $self->{s}->{modified_lst} }, $stype{$querytype};
}
$self->disconnect();
unless ( $self->{opt}->{server} ) {
if ( $self->{breed} =~ /^check/ ) {
$self->{s}->{list} = $self->{s}->{catalogue};
$self->{s}->{listfile} = $self->{conf}->{listfile_catalogue}; # for discovery()
}
else {
$self->{s}->{list} = $self->{s}->{nomination};
$self->{s}->{listfile} = $self->{conf}->{listfile_nomination}; # for discovery()
}
}
$self->{s}->{new_list} = 1;
$self->{done_discovery} = 1;
$self->writeservers();
return $self;
}
# only for debugging and errorchecking
#
sub logobj {
my ( $self, $loglevel, $prefix, @objs ) = @_;
return unless $self->logll($loglevel);
foreach my $obj (@objs) {
my $line = debugobj($obj);
$self->log( $loglevel, "$prefix:\n $line" );
}
}
#
# Mail Object
#
# Main data type used by check and report is the Mail Object.
# an array of hash ref's, where array order matches mails in mbox (or stdin).
#
# key = value (not all defined)
#
# id = integer NOTE: only key guaranteed to exist
# orig_mail = ref to string containing orig email (headers+body)
# headers = headers of orig_email
# spam = 0, not spam, >1 spam
# skipme = 0|1 (not checked against server, usually whitelisted mail)
# p = array ref to mimeparts. see below
# e1 = similar to p, but special for engine 1
#
# e1: each mail obj contains a special part for engine 1
#
# skipme = 0|1 (ex: 1 if cleaned body goes to 0 len)
# spam = 0, not spam, >1 spam
# body = body of orig_mail
# cleaned = body sent thru razor 1 preproc
# e1 = hash using engine 1
# sent = hash ref sent to server
# resp = hash ref of server response
#
# p: each mail obj contains 1 or more mimeparts, which can contain:
#
# id = string - mailid.part
# skipme = 0|1 (ex: 1 if cleaned body goes to 0 len)
# spam = 0, not spam, >1 spam
# body = bodyparts (mimeparts) of orig_email, has X-Razor & Content-* headers
# cleaned = body sent through preprocessors (deHtml, deQP, etc..), debugging use only
# e2 = hash using engine 2
# e3 = hash using engine 2
# e4 = hash using engine 2
# sent = array ref of hash ref's sent to server
# resp = array ref of hash ref's, where hash is parsed sis of server response
#
#
sub prepare_objects {
my ( $self, $objs ) = @_;
my @objects;
unless ( $self->{s}->{engines}
|| ( $self->{s}->{engines} = $self->compute_supported_engines() ) ) {
$self->log( 1, "ALLBAD. supported engines not defined" );
}
my $i = 1;
if ( ref( $objs->[0] ) eq 'HASH' ) { # checking cmd-line signatures
foreach my $o (@$objs) {
my $obj = { id => $i++ };
$obj->{p}->[0]->{id} = "$obj->{id}.0";
$obj->{p}->[0]->{"e$o->{eng}"} = $o->{sig};
$obj->{ep4} = $o->{ep4} if $o->{ep4};
push @objects, $obj;
}
}
elsif ( ref( $objs->[0] ) eq 'SCALAR' ) { # checking/reporting mail
foreach my $o (@$objs) {
my $obj = { id => $i++ };
$obj->{orig_mail} = $o;
$self->log2file( 16, $o, "$obj->{id}.orig_mail" ); # includes headers and all
push @objects, $obj;
}
$self->prepare_parts( \@objects );
}
$self->logobj( 14, "prepared objs", \@objects );
return \@objects;
}
sub prepare_parts {
my ( $self, $objs ) = @_;
my $prep_mail_debug = 0; # debug print, 0=none, 1=split_mime stuff, 2=more verbose
$prep_mail_debug++ if $self->{conf}->{debuglevel} > 15;
$prep_mail_debug++ if $self->{conf}->{debuglevel} > 16;
foreach my $obj (@$objs) {
next if ( $obj->{skipme} || !$obj->{orig_mail} );
#
# now split up mime parts from orig mail
#
my ( $headers, @bodyparts ) = prep_mail(
$obj->{orig_mail},
$self->{conf}->{report_headers},
4 * 1024,
60 * 1024,
15 * 1024,
$self->{name_version},
$prep_mail_debug, # $debug,
);
my $lines = " prep_mail done: mail $obj->{id} headers=" . length($$headers);
foreach ( 0 .. $#bodyparts ) { $lines .= ", mime$_=" . length( ${ $bodyparts[$_] } ); }
$self->log( 8, $lines );
unless (@bodyparts) {
$self->log( 2, "empty body in mail $obj->{id}, skipping" );
next;
}
$$headers =~ s/\r\n/\n/gs;
$obj->{headers} = $headers;
# $obj->{e1} = {
# id => "$obj->{id}.e1",
# body => $obj->{orig_mail},
# };
$obj->{p} = [];
foreach ( 0 .. $#bodyparts ) {
$bodyparts[$_] =~ s/\r\n/\n/gs;
$obj->{p}->[$_] = {
id => "$obj->{id}.$_",
body => $bodyparts[$_],
};
}
}
return 1;
}
# given mail objects, fills out
#
# - e1
#
# and for each body part of mail object, fills out
#
# - cleaned
# - e2
# - e3
# - e4
#
# also returns array ref of sigs suitable for printing
#
sub compute_sigs {
my ( $self, $objects ) = @_;
my @printable_sigs;
foreach my $obj (@$objects) {
next if ( $obj->{skipme} || !$obj->{orig_mail} );
if ( ${ $obj->{orig_mail} } =~ /\n(Subject: [^\n]+)\n/ ) {
my $subj = substr $1, 0, 70;
$self->log( 8, "mail " . $obj->{id} . " $subj" );
}
else {
$self->log( 8, "mail " . $obj->{id} . " has no subject" );
}
#
# clean each bodypart, removing if new length is 0
#
next unless $obj->{p};
foreach my $objp ( @{ $obj->{p} } ) {
next if $objp->{skipme};
my $olen = length( ${ $objp->{body} } );
my $clnpart = ${ $objp->{body} };
# We'll do a VR8 preproc to determine emptiness
# of email, and store it so VR8 can use it.
my $clnpart_vr8 = $clnpart;
$self->{preproc_vr8}->preproc( \$clnpart_vr8 ); # in da future: $self->{s}->{conf}->{dre}
$objp->{cleaned_vr8} = \$clnpart_vr8;
# This for VR4 (the only other signature scheme
# supported at this time.
$self->{preproc}->preproc( \$clnpart );
$objp->{cleaned} = \$clnpart;
my $clen = length($clnpart_vr8);
$self->log2file( 15, $objp->{body}, "$objp->{id}.before_preproc.as_reported" );
$self->log2file( 15, $objp->{cleaned}, "$objp->{id}.after_preproc" );
if ( $clen eq 0 ) {
$self->log( 6, "preproc: mail $objp->{id} went from $olen bytes to 0, erasing" );
$objp->{skipme} = 1;
next;
}
elsif ( ( $clen < 128 ) and ( $clnpart =~ /^(Content\S*:[^\n]*\n\r?)+(Content\S*:[^\n]*)?\s*$/s ) ) {
$self->log( 6, "preproc: mail $objp->{id} seems empty, erasing" );
$objp->{skipme} = 1;
next;
}
elsif ( $clnpart_vr8 !~ /\S/ ) {
$self->log( 6, "preproc: mail $objp->{id} went to all whitespace, erasing" );
$objp->{skipme} = 1;
next;
}
elsif ( $clen eq $olen ) {
$self->log( 6, "preproc: mail $objp->{id} unchanged, bytes=$olen" );
}
else {
$self->log( 6, "preproc: mail $objp->{id} went from $olen bytes to $clen " );
}
}
#
# compute sig for bodyparts that are cleaned.
#
if ( $self->{s}->{conf}->{ep4} ) {
$obj->{ep4} = $self->{s}->{conf}->{ep4};
}
else {
$obj->{ep4} = '7542-10';
$self->log( 8, "warning: no ep4 for server $self->{s}->{ip}, using $obj->{ep4}" );
}
foreach my $objp ( @{ $obj->{p} } ) {
next if $objp->{skipme};
$self->log( 15, "mail part is [${$objp->{cleaned}}]" );
if ( ${ $objp->{cleaned} } =~ /^\s+$/ ) {
$self->log( 6, "mail $objp->{id} is whitespace only; skipping!" );
}
$self->log( 6, "computing sigs for mail $objp->{id}, len " . length( ${ $objp->{cleaned} } ) );
foreach ( sort keys %{ $self->{s}->{engines} } ) {
my $engine_no = $_;
my $sig;
if ( $engine_no == 4 ) {
$sig = $self->compute_engine(
$engine_no,
$objp->{cleaned},
$obj->{ep4}
);
}
elsif ( $engine_no == 8 ) {
$sig = $self->compute_engine(
$engine_no,
$objp->{cleaned_vr8}
);
}
else {
# Unsupported signature type, don't calculate.
next; # handled above
}
if ($sig) {
$objp->{"e$engine_no"} = $sig;
my @sigs;
if ( ref $sig eq 'ARRAY' ) {
@sigs = @$sig;
}
else {
push @sigs, $sig;
}
for (@sigs) {
my $line = "$objp->{id} e$engine_no: $_";
$line .= ", ep4: $obj->{ep4}" if ( $engine_no eq '4' );
push @printable_sigs, $line;
}
}
else {
$self->log( 6, "Engine ($engine_no) didn't produce a signature for mail $objp->{id}" );
}
}
}
$self->logobj( 14, "computed sigs for obj", $obj );
}
return \@printable_sigs;
}
#
# this function is the only one that has to be aware
# of razor protocol syntax. (not including random logging)
# the hashes generated here are eventually sent to to_batched_query.
#
sub make_query {
my ( $self, $params ) = @_;
if ( $params->{action} =~ /^check/ ) {
if ( ref $params->{sig} eq 'ARRAY' ) { # Multiple signature per part, VR8
my $sigs = $params->{sig};
my @queries;
for (@$sigs) {
my %query = ( a => 'c', e => $params->{eng}, s => $_ );
push @queries, \%query;
}
return \@queries;
}
else {
my %query = ( a => 'c', e => $params->{eng}, s => $params->{sig} );
$query{ep4} = $params->{ep4} if $query{e} eq '4';
return \%query;
}
}
elsif ( $params->{action} =~ /^rcheck/ ) {
my %query = (
a => 'r',
e => $params->{eng},
s => $params->{sig},
);
$query{ep4} = $params->{ep4} if $query{e} eq '4';
return \%query;
}
elsif ( $params->{action} =~ /(report)/ ) {
# prep_mail already truncated headers and body parts > 64K
my @dudes;
my $n = 0;
while ( $params->{obj}->{p}->[$n] ) {
my $line = ${ $params->{obj}->{headers} };
while (1) {
my $body = $params->{obj}->{p}->[$n]->{body};
last unless ( ( length($$body) + length($line) < $self->{s}->{conf}->{bqs} * 1024 ) );
$self->log( 11, "bqs=" . ( $self->{s}->{conf}->{bqs} * 1024 ) . " adding to line [len=" . length($line) . "] mail $params->{obj}->{p}->[$n]->{id}" . " [len=" . length($$body) . "], total len=" . ( length($$body) + length($line) ) );
$line .= "\r\n" . $$body;
$n++;
last unless $params->{obj}->{p}->[$n];
}
push @dudes, $line;
}
my @queries;
foreach (@dudes) {
push @queries, {
a => $params->{action} eq 'report' ? 'r' : 'revoke',
message => $_,
};
}
return @queries;
}
elsif ( $params->{action} =~ /revoke/ ) {
# Never send messages on revoke. Revoke all signature
# that we were able to compute.
my $n = 0;
my @queries;
while ( $params->{obj}->{p}->[$n] ) {
for my $engine ( keys %{ $self->{s}->{engines} } ) {
my $sigs;
if ( $sigs = $params->{obj}->{p}->[$n]->{"e$engine"} ) {
if ( ref $sigs eq 'ARRAY' ) {
for my $sig (@$sigs) {
push @queries, { a => 'revoke', e => $engine, s => $sig };
}
}
else {
push @queries, { a => 'revoke', e => $engine, s => $sigs };
}
}
}
$n++;
}
return @queries;
}
}
#
# prepare queries in correct syntax for sending over network
#
sub obj2queries {
my ( $self, $objects, $action ) = @_;
my @queries = ();
foreach my $obj (@$objects) {
next if $obj->{skipme};
push @queries, $obj->{e1}->{sent} if $obj->{e1}->{sent};
foreach my $objp ( @{ $obj->{p} } ) {
next if $objp->{skipme};
#$self->log(8,"not skipping mail part $objp->{id}, sent: ". scalar(@{$objp->{sent}}));
push @queries, @{ $objp->{sent} } if $objp->{sent};
}
}
if ( scalar(@queries) ) {
$self->log( 8, "preparing " . scalar(@queries) . " queries" );
}
else {
$self->log( 8, "objects yielded no valid queries" );
return [];
}
my $qbatched = to_batched_query(
\@queries,
$self->{s}->{conf}->{bql},
$self->{s}->{conf}->{bqs},
1
);
$self->log( 8, "sending " . scalar(@$qbatched) . " batches" );
return $qbatched;
}
#
# Parse response syntax, add info to appropriate object
#
sub queries2obj {
my ( $self, $objs, $responses, $action ) = @_;
my @resp;
foreach (@$responses) {
# from_batched_query wants "-" in beginning, but not ".\r\n" at end
s/\.\r\n$//sg;
my $arrayref = from_batched_query($_);
push @resp, @$arrayref;
}
$self->log( 12, "processing " . scalar(@resp) . " responses" );
$self->logobj( 14, "from_batched_query", \@resp );
my $j = 0;
while (@resp) {
my $obj = $objs->[ $j++ ];
return $self->error("more responses than mail objs!") unless $obj;
next if $obj->{skipme};
if ( $obj->{e1}->{sent} && !$obj->{e1}->{skipme} ) {
$obj->{e1}->{resp} = shift @resp;
$self->log( 12, "adding a resp to mail $obj->{e1}->{id}" );
}
foreach my $objp ( @{ $obj->{p} } ) {
next unless $objp->{sent};
# for each part, shift out as many responses as there were queries
foreach ( @{ $objp->{sent} } ) {
push @{ $objp->{resp} }, shift @resp;
$self->log( 12, "adding a resp to mail $objp->{id}" );
}
}
#$self->logobj(13,"end of queries2obj",$obj);
}
return 1;
}
sub check_resp {
my ( $self, $me, $sent, $resp, $objp ) = @_;
# default is no contention
$objp->{ct} = 0;
$objp->{ct} = $resp->{ct} if exists $resp->{ct};
if ( exists $resp->{err} ) {
$self->logobj( 4, "$me: got err $resp->{err} for query", $sent );
return 0;
}
if ( $resp->{p} eq '1' ) {
if ( exists $resp->{cf} ) {
if ( $resp->{cf} < $self->{s}->{min_cf} ) {
$self->log( 6, "$me: Not spam: cf $resp->{cf} < min_cf $self->{s}->{min_cf}" );
return 0;
}
else {
$self->log( 6, "$me: Is spam: cf $resp->{cf} >= min_cf $self->{s}->{min_cf}" );
return 1;
}
}
$self->log( 6, "$me: sig found, no cf, ok." );
return 1;
}
if ( $resp->{p} eq '0' ) {
$self->log( 6, "$me: sig not found." );
return 0;
}
# should never get here
$self->logobj(
2, "$me: got bad response from server - sent obj, resp obj",
[ $sent, $resp ]
);
return 0;
}
sub rcheck_resp {
my ( $self, $me, $sent, $resp ) = @_;
$self->log( 8, "$me: invalid $sent" ) unless ref($sent);
$self->log( 8, "$me: invalid $resp" ) unless ref($resp);
if ( exists $resp->{err} ) {
if ( $resp->{err} eq '230' ) {
$self->log( 8, "$me: err 230 - server wants mail" );
return 1;
}
$self->logobj( 4, "$me: got err $resp->{err} for query", $sent );
return 0;
}
if ( $resp->{res} eq '1' ) {
$self->log( 5, "$me: Server accepted report." );
return 0;
}
if ( $resp->{res} eq '0' ) {
$self->log( 1, "$me: Server did not accept report. Shame on the server." );
return 0;
}
# should never get here
$self->logobj(
2, "$me: got bad response from server - sent obj, resp obj",
[ $sent, $resp ]
);
return 0;
}
sub check {
my ( $self, $objects ) = @_;
my $valid = 0;
foreach my $obj (@$objects) {
next if $obj->{skipme};
#
# Logic used in ordering of check queries
#
# queries should go like this: (e=engine, p=part)
# e1, p0e2, p0e3, p0e4, p1e2, p1e3, p1e4, etc..
# unless cmd-line sigs are passed.
#
# engine 1 is for entire mail, not parts
if (
$obj->{e1} # cmd-line sig checks don't have this
&& $self->{s}->{engines}->{1}
) {
$obj->{e1}->{sent} = $self->make_query(
{
action => 'check',
sig => $obj->{e1}->{e1},
eng => 1
}
);
}
# rest of engines and mime parts
foreach my $objp ( @{ $obj->{p} } ) {
if ( $objp->{skipme} ) {
$self->log( 8, "mail $objp->{id} skipped in check" );
next;
}
$objp->{sent} = [];
foreach ( sort keys %{ $self->{s}->{engines} } ) {
my $engine_save = $_;
next if $_ eq 1; # engine 1 done above
my $sig = $objp->{"e$_"};
unless ($sig) {
$self->log( 5, "mail $objp->{id} e$_ got no sig" );
next;
}
unless ( $self->{s}->{engines}->{$_} ) {
# warn if cmd-lig sig check is not supported
$self->log( 5, "mail $objp->{id} engine $_ is not supported, sig check skipped" )
if ( $sig && !$obj->{orig_mail} );
next;
}
if ( ref $sig ) {
for (@$sig) {
$self->log( 8, "mail $objp->{id} e$engine_save sig: $_" );
}
}
else {
$self->log( 8, "mail $objp->{id} e$engine_save sig: $sig" );
}
my $query = $self->make_query(
{
action => 'check',
sig => $sig,
ep4 => $obj->{ep4},
eng => $_
}
);
$valid++ if $query;
if ( ref $query eq 'ARRAY' ) {
push @{ $objp->{sent} }, @$query;
}
else {
push @{ $objp->{sent} }, $query;
}
}
}
}
unless ($valid) {
$self->log( 5, "No queries, no spam" );
return 1;
}
$self->{s}->{list} = $self->{s}->{catalogue};
$self->connect;
# Build query text strings
#
my $queries = $self->obj2queries( $objects, 'check' ) or return $self->errprefix("check 1");
# send to server and store answers in mail obj
#
my $response = $self->_send($queries) or return $self->errprefix("check 2");
$self->queries2obj( $objects, $response, 'check' ) or return $self->errprefix("check 3");
foreach my $obj (@$objects) {
# check_logic will parse response for each object, decide if its spam
#
$self->check_logic($obj);
$self->log( 3, "mail $obj->{id} is " . ( $obj->{spam} ? '' : 'not ' ) . "known spam." );
}
return 1;
}
sub check_logic {
my ( $self, $obj ) = @_;
# default is not spam
$obj->{spam} = 0;
if ( $obj->{skipme} ) {
next;
}
#
# Logic for Spam
#
#
my $logic_method = $self->{conf}->{logic_method} || 4;
my $logic_engines = $self->{conf}->{logic_engines} || 'any';
# cmd-line sig checks default to logic_method 1
$logic_method = 1 unless $obj->{orig_mail};
my $leng;
if ( $logic_engines eq 'any' ) {
$leng = ""; # not a hash ref, implies 'any' logic_engine
}
elsif ( $logic_engines eq 'all' ) {
$leng = $self->{s}->{engines};
}
elsif ( $logic_engines =~ /^(\d\,)+$/ ) {
$leng = {};
foreach ( split /,/, $logic_engines ) {
unless ( $self->{s}->{engines}->{$_} ) {
$self->log( 3, "logic_engine $_ not supported, skipping" );
next;
}
$leng->{$_} = 1;
}
}
else {
$self->log( 3, "invalid logic_engines: $logic_engines, defaulting to 'any'" );
$leng = ""; # not a hash ref, implies 'any' logic_engine
}
# iterate through sent queries and responses,
# perform engine analysis (logic_engines).
#
# engine 1 case
my $sent = $obj->{e1}->{sent};
my $resp = $obj->{e1}->{resp};
if ( $resp && $sent ) {
# if skipme, there would be no resp
my $logmsg = "mail $obj->{id} e=1 sig=$sent->{s}";
$obj->{e1}->{spam} = $self->check_resp( $logmsg, $sent, $resp, $obj->{e1} );
}
# all other engines for all parts
foreach my $objp ( @{ $obj->{p} } ) {
$objp->{spam} = 0;
if ( $objp->{skipme} ) {
$self->log( 8, "doh. $objp->{id} is skipped, yet has sent" ) if $objp->{sent};
next;
}
next unless $objp->{sent};
my $not_spam = 0;
foreach ( 0 .. ( scalar( @{ $objp->{sent} } ) - 1 ) ) {
$sent = $objp->{sent}->[$_];
$resp = $objp->{resp}->[$_];
unless ($resp) {
$self->log( 5, "doh. more sent queries than responses" );
next;
}
my $logmsg = "mail $objp->{id} e=$sent->{e} sig=$sent->{s}";
my $is_spam = $self->check_resp( $logmsg, $sent, $resp, $objp );
if ( ref($leng) ) {
if ( $leng->{ $sent->{e} } && $is_spam ) {
$self->log( 8, "logic_engines requires $sent->{e}, and it is. cool." );
$objp->{spam} = 1;
}
elsif ( $leng->{ $sent->{e} } && !$is_spam ) {
$self->log( 8, "logic_engines requires $sent->{e}, and it is not, part not spam" );
$not_spam = 1;
}
else {
$self->log( 8, "logic_engines doesn't care about $sent->{e}, skipping" );
}
}
else {
# not a hash ref, implies 'any' logic_engine
$objp->{spam} += $is_spam;
}
$objp->{spam} = 0 if $not_spam;
}
}
# mime part analysis (logic_methods)
#
if ( $logic_method == 1 ) {
$obj->{spam} = 0;
if ( $obj->{e1} ) {
$obj->{spam} += $obj->{e1}->{spam} if $obj->{e1}->{spam};
}
foreach my $objp ( @{ $obj->{p} } ) {
$obj->{spam} += $objp->{spam} if $objp->{spam};
}
}
elsif ( $logic_method =~ /^(2|3)$/ ) {
# logic_methods > 1
foreach my $objp ( @{ $obj->{p} } ) {
next if $objp->{skipme};
next unless $objp->{body};
my ( $hdrs, $body ) = split /\n\n/, ${ $objp->{body} }, 2;
$hdrs .= "\n";
#$self->log(8,"$objp->{id} hdrs:\n$hdrs");
my $type = "<type unknown>";
$objp->{is_text} = 0;
$objp->{is_inline} = 0;
$objp->{is_inline} = 1 if $hdrs =~ /Content-Disposition: inline/i;
#$type = $1 if $hdrs =~ /Content-Type:\s([^\;\n]+)/i;
$type = $1 if $hdrs =~ /Content-Type:\s([^\n]+)/i;
$objp->{is_text} = 1 if $type =~ /text\//i;
$objp->{is_text} = 1 if $type =~ /type unknown/; # assume text ?
$self->log( 8, "mail $objp->{id} Type $objp->{is_text},$objp->{is_inline} $type" );
}
}
if ( $logic_method == 2 ) {
# in this method, only 1 dude decides if mail is spam. decider.
# the first part is the default decider. can be overwritten, tho.
my $decider = $obj->{p}->[0];
# basically the first inline text/* becomes the decider.
# however, if no inline, the first text/* is used
my $found = 0;
foreach my $objp ( @{ $obj->{p} } ) {
next if $objp->{skipme};
if ( $objp->{is_inline} && $objp->{is_text} ) {
$decider = $objp;
last;
}
if ( !$found && $objp->{is_text} ) {
$decider = $objp;
$found = 1;
}
}
$self->log( 7, "method 2: $decider->{id} is the spam decider" );
$obj->{spam} = $decider->{spam};
}
elsif ( $logic_method == 3 ) {
# in this method, all text/* parts must be spam for obj to be spam
# non-text parts are ignored
my $found = 0;
foreach my $objp ( @{ $obj->{p} } ) {
next if $objp->{skipme};
next unless $objp->{is_text};
$found = 1;
$obj->{spam} = $objp->{spam};
unless ( $objp->{spam} ) {
$self->log( 7, "method 3: $objp->{id} is_text but not spam, mail not spam" );
last;
}
}
$self->log( 7, "method 3: mail $obj->{id}: all is_text parts spam, mail spam" ) if $obj->{spam};
# if no parts where text, use the first part as spam indicator
unless ($found) {
$self->log( 6, "method 3: mail $obj->{id}: no is_text, using part 1" );
$obj->{spam} = 1 if $obj->{p}->[0]->{spam};
}
}
elsif ( $logic_method == 4 ) {
# in this method, if any non-contention parts is spam, mail obj is spam
# contention parts are ignored.
$obj->{spam} = 0;
foreach my $objp ( @{ $obj->{p} } ) {
next if $objp->{skipme};
if ( $objp->{ct} ) {
$self->log( 7, "method 4: mail $objp->{id}: contention part, skipping" );
}
else {
$self->log( 7, "method 4: mail $objp->{id}: no-contention part, spam=$objp->{spam}" );
$obj->{spam} = 1 if $objp->{spam};
}
}
if ( $obj->{spam} ) {
$self->log( 7, "method 4: mail $obj->{id}: a non-contention part was spam, mail spam" );
}
else {
$self->log( 7, "method 4: mail $obj->{id}: all non-contention parts not spam, mail not spam" );
}
}
elsif ( $logic_method == 5 ) {
# in this method, all non-contention parts must be spam for obj to be spam
# contention parts are ignored.
my $not_spam = 0;
my $is_spam = 0;
foreach my $objp ( @{ $obj->{p} } ) {
next if $objp->{skipme};
if ( $objp->{ct} ) {
$self->log( 7, "method 5: mail $objp->{id}: contention part, skipping" );
next;
}
else {
$self->log( 7, "method 5: mail $objp->{id}: no-contention part, spam=$objp->{spam}" );
}
if ( $objp->{spam} ) {
$is_spam = 1;
}
else {
$not_spam = 1;
}
}
if ( $is_spam && !$not_spam ) {
$obj->{spam} = 1;
$self->log( 7, "method 5: mail $obj->{id}: all non-contention parts spam, mail spam" );
}
else {
$self->log( 7, "method 5: mail $obj->{id}: a non-contention part not spam, mail not spam" );
$obj->{spam} = 0;
}
}
return 1;
}
# returns hash ref if successfully registered
# returns 0 if not
sub register {
my ( $self, $p, ) = @_;
my @queries;
my $registrar = $self->{name_version};
my %qr = ( a => 'reg', registrar => $registrar );
$qr{user} = $p->{user} if $p->{user};
$qr{pass} = $p->{pass} if $p->{pass};
$queries[0] = makesis(%qr);
$self->{s}->{list} = $self->{s}->{nomination};
$self->connect;
my $response = $self->_send( \@queries ) or return $self->errprefix("register");
my %resp = parsesis( $$response[0] );
if ( $resp{err} && $resp{err} eq '210' ) {
if ( $qr{user} && $qr{pass} ) {
my ($creds) = { user => $qr{user}, pass => $qr{pass} };
if ( $self->authenticate($creds) ) {
$self->log( 6, "Successfully registered provided credentials.\n" );
return $creds;
}
}
return $self->error("Error $resp{err}: User exists. Try another name. aborting.\n");
}
return $self->error("Error $resp{err} while performing register, aborting.\n")
if ( $resp{err} );
return $self->error("No success (res=$resp{res}) while performing register, aborting.\n")
if ( $resp{res} ne '1' );
$self->log( 6, "Successfully registered with $self->{s}->{ip} identity: $resp{user}" );
# otherwise return hash containing 'user' and 'pass'
delete $resp{res};
return \%resp;
}
sub authenticate {
my ( $self, $options ) = @_;
my @queries;
unless ( ( $options->{user} =~ /\S/ ) && ( $options->{pass} =~ /\S/ ) ) {
return $self->error("authenticate did not get valid user + pass");
}
my %qr = ( a => 'ai', user => $options->{user}, cn => 'razor-agents', cv => $Razor2::Client::Version::VERSION );
$queries[0] = makesis(%qr);
$self->{s}->{list} = $self->{s}->{nomination};
$self->connect;
my $response = $self->_send( \@queries ) or return $self->errprefix("authenticate 1");
my %resp = parsesis( $$response[0] );
if ( $resp{err} ) {
if ( ( $resp{err} eq '213' ) && !defined( $self->{reregistered} ) ) {
# 213 = unknown user.
# Try to register with current user+pass and continue with authenticate
$self->log( 8, "unknown user, attempting to re-register" );
my $id = $self->register($options);
$self->{reregistered} = 1;
if ( ( $id->{user} eq $options->{user} )
&& ( $id->{pass} eq $options->{pass} ) ) {
$self->log( 5, "re-registered user $id->{user} with $self->{s}->{ip}" );
return $self->authenticate($options);
}
else {
return $self->error("Error 213 while authenticating, aborting.\n");
}
}
else {
return $self->error("Error $resp{err} while authenticating, aborting.\n");
}
}
my ( $iv1, $iv2 ) = xor_key( $options->{pass} );
my ($my_digest) = hmac_sha1( $resp{achal}, $iv1, $iv2 );
%qr = ( a => 'auth', aresp => $my_digest );
$queries[0] = makesis(%qr);
$response = $self->_send( \@queries ) or return $self->errprefix("authenticate 2");
%resp = parsesis( $$response[0] );
return $self->error("Error $resp{err} while authenticating, aborting.\n") if ( $resp{err} );
return $self->error("Authentication failed for user=$options->{user}")
if ( $resp{res} ne '1' );
$self->log( 5, "Authenticated user=$options->{user}" );
$self->{authenticated} = 1;
return 1;
}
#
# handles report and revoke
#
sub report {
my ( $self, $objs ) = @_;
return $self->error("report: Not Authenticated") unless $self->{authenticated};
return $self->error("report/revoke for engine 1 not supported")
if ( $self->{s}->{conf}->{dre} == 1 );
$self->{s}->{list} = $self->{s}->{nomination};
$self->connect;
my @robjs;
my $valid = 0;
if ( $self->{breed} eq 'report' ) {
#
# Before reporting entire email, check to see if server already has it
#
unless ( $self->{s}->{conf}->{dre} ) {
$self->logobj( 8, "server has no default dre, using 4", $self->{s}->{conf} );
$self->{s}->{conf}->{dre} = 4;
}
foreach my $obj (@$objs) {
next if $obj->{skipme};
# handle special case for engine 1
# note: razor 1 does not store emails in its db, just sigs.
# so we should never get a res=230 for e=1 a=r sig=xxx
#
#$obj->{e1}->{sent} = $self->make_query( {
# action => 'rcheck',
# sig => $obj->{e1}->{e1},
# eng => 1, } );
#$valid++ if $obj->{e1}->{sent};
# rest of engines and mime parts
foreach my $objp ( @{ $obj->{p} } ) {
if ( $objp->{skipme} ) {
$self->log( 13, "mail $objp->{id} skipped in report" );
next;
}
my $q = $self->make_query(
{
action => 'rcheck',
sig => $objp->{"e$self->{s}->{conf}->{dre}"},
ep4 => $obj->{ep4},
eng => $self->{s}->{conf}->{dre},
}
);
$objp->{sent} = [$q];
$valid++;
}
}
unless ($valid) {
$self->log( 5, "No report check queries, no spam" );
return 1;
}
$valid = 0;
# Build query text strings - signatures computed already (see reportit)
my $queries = $self->obj2queries( $objs, 'rcheck' ) or return $self->errprefix("report1");
# send to server and store answers in mail obj
my $response = $self->_send($queries) or return $self->errprefix("report2");
$self->queries2obj( $objs, $response, 'rcheck' ) or return $self->errprefix("report3");
#
# If server wants email or certain body parts,
# create new {sent} and add obj to @robjs
#
foreach my $obj (@$objs) {
next if $obj->{skipme};
#$self->log(12,"mail $obj->{id} read ". scalar(@{$obj->{resp}}) ." queries");
# handle engine 1 special case
#if ( !$obj->{e1}->{skipme} && $self->rcheck_resp(
# "mail ". $obj->{id} .", orig_email, special case eng 1",
# $obj->{e1}->{sent},
# $obj->{e1}->{resp}
# ) ) {
# $self->log(5,"doh. Server should not send res=230 for eng=1 report");
#}
#delete $obj->{e1}->{sent};
my $wants_orig_mail = 0;
foreach my $objp ( @{ $obj->{p} } ) {
next if $objp->{skipme};
$self->logobj( 14, "checking response for $objp->{id}", $objp );
unless (
$self->rcheck_resp(
"mail $objp->{id}, eng $self->{s}->{conf}->{dre}",
$objp->{sent}->[0], $objp->{resp}->[0]
)
) {
$objp->{skipme} = 1;
}
else {
$wants_orig_mail++;
}
$objp->{resp} = []; # clear responses from rcheck
$objp->{sent} = [];
}
if ($wants_orig_mail) {
# reports are special, all parts need to be together, so use part 0's sent
my $objp = $obj->{p}->[0];
$objp->{skipme} = 0 if $objp->{skipme};
push @{ $objp->{sent} }, $self->make_query(
{
action => 'report',
obj => $obj,
}
);
push @robjs, $obj;
}
$valid += $wants_orig_mail;
}
}
else { # revoke
foreach my $obj (@$objs) {
# don't revoke eng 1
# engines > 1 we send all the body parts, use part 0 to store sent
my $objp = $obj->{p}->[0];
$objp->{sent} = [];
push @{ $objp->{sent} }, $self->make_query(
{
action => 'revoke',
obj => $obj,
}
);
$valid++ if scalar( @{ $objp->{sent} } );
$self->log( 9, "revoke sent:" . scalar( @{ $objp->{sent} } ) );
push @robjs, $obj;
}
}
unless ( $valid && scalar(@robjs) ) {
$self->log( 3, "Finished $self->{breed}." );
return 1;
}
#$self->logobj(14,"report objs", \@robjs);
#
# send server mails/body parts either
# revoked, or requested if reporting
#
my $queries = $self->obj2queries( \@robjs, $self->{breed} ) or return $self->errprefix("report4");
my $response = $self->_send($queries) or return $self->errprefix("report5");
$self->queries2obj( \@robjs, $response, $self->{breed} ) or return $self->errprefix("report6");
# we just do this to log server's response
#
foreach my $obj (@robjs) {
my $objp = $obj->{p}->[0];
my $cur = -1;
while ( $objp->{sent}->[ ++$cur ] ) {
$self->rcheck_resp(
"$self->{breed}: mail $obj->{id}, $cur",
$objp->{sent}->[$cur],
$objp->{resp}->[$cur]
) unless ( $objp->{skipme} );
}
}
$self->logobj( 14, "report objs", \@robjs );
$self->log( 3, "Sent $self->{breed}." );
return 1;
}
sub _send {
my ( $self, $msg, $closesock, $skipread ) = @_;
$self->log( 16, "entered _send" );
unless ( $self->{connected_to} ) {
$self->connect() or return $self->errprefix("_send");
}
my @response;
my $select = $self->{select};
my $sock = ( $select->handles )[0];
$self->{sent_cnt} = 0 unless $self->{sent_cnt};
foreach my $i ( 0 .. ( ( scalar @$msg ) - 1 ) ) {
my @handles = $select->can_write(15);
if ( $handles[0] ) {
$self->log( 4, "$self->{connected_to} << " . length( $$msg[$i] ) );
if ( $$msg[$i] =~ /message/ ) {
my $line = debugobj( $$msg[$i] );
$self->log( 6, $line );
$self->log2file( 16, \$$msg[$i], "sent_to." . $self->{sent_cnt} );
}
else {
$self->log( 6, $$msg[$i] );
}
local $\;
undef $\;
print $sock $$msg[$i];
$self->{sent_cnt}++;
}
else {
return $self->error("Timed out (15 sec) while writing to $self->{s}->{ip}");
}
next if $skipread;
@handles = $select->can_read(15);
if ( $sock = $handles[0] ) {
local $/;
undef $/;
$response[$i] = $self->_read($sock) or return $self->error("Error reading socket");
$self->log( 4, "$self->{connected_to} >> " . length( $response[$i] ) );
$self->log( 6, "response to sent.$self->{sent_cnt}\n" . $response[$i] );
}
else {
return $self->error("Timed out (15 sec) while reading from $self->{s}->{ip}");
}
}
if ($closesock) {
$select->remove($sock);
close $sock;
}
return \@response;
}
sub _read {
my ( $self, $socket ) = @_;
my ( $query, $read );
# fixme - need to trim this down (copied from server)
#
unless ( $read = sysread( $socket, $query, 1024 ) ) {
# There was an error on sysread(), could be a real error or a
# blocking error.
if ( $! == EWOULDBLOCK ) {
# write would block, so we try again later
$self->debug("_read: EWOULDBLOCK");
return;
}
elsif ( $! == EINTR or $! == EIO ) {
# sysread() got interupted by a signal.
# we will process this socket on next wheelwalk.
$self->debug("_read: EINTR");
return;
}
elsif ( $! == EPIPE or $! == EISDIR or $! == EBADF or $! == EINVAL or $! == EFAULT ) {
$self->debug("_read: EPIPE");
return;
}
else {
# This happens when client breaks the connection.
# Find out why we don't get an EPIPE instead. FIX!
$self->debug("_read: connection_closed");
return;
}
}
if ( $read > 0 ) {
# Now we are absolutely sure there is data on the socket.
return $query;
}
else {
# Otherwise we got an EOF, expire the socket
$self->debug("_read: EOF, connection_closed");
return;
}
}
sub connect {
my ( $self, %params ) = @_;
my $sock;
$self->log( 16, "entered connect" );
if ( $self->{simulate} ) {
return $self->error("Razor Error 4: This is a simulation. Won't connect to $self->{s}->{ip}.");
}
my $server = $params{server} || $self->{s}->{ip};
unless ( $self->{s}->{ip} ) {
$self->{s}->{ip} = $server;
}
if ( $self->{sock} && $self->{connected_to} ) {
unless ($server) {
$self->log( 13, "no server specified, using already connected server $self->{connected_to}" );
return 1;
}
if ( $server eq $self->{connected_to} ) {
$self->log( 15, "already connected to server $self->{connected_to}" );
return 1;
}
return 1 if $self->{disconnecting};
$self->log( 6, "losing old server connection, $self->{connected_to}, for new server, $server" );
$self->disconnect;
}
unless ($server) {
$self->log( 6, "no server specified, not connecting" );
return;
}
my $port = $params{port} || $self->{s}->{port};
unless ( defined($port) && $port =~ /^\d+$/ ) {
my $portlog = defined($port) ? " ($port)" : "";
$self->log( 6, "No port specified$portlog, using 2703" ); # bootstrap_discovery will come here
$port = 2703;
}
$self->log( 5, "Connecting to $server ..." );
if ( my $proxy = $self->{conf}->{proxy} ) {
#
# Proxy stuff never been tested
#
$proxy =~ s!^http://!!;
$proxy =~ s!:(\d+)/?$!!;
my $pport = $1 || 80;
$self->debug("HTTP tunneling through $proxy:$pport.");
$sock = IO::Socket::INET->new(
PeerAddr => $proxy,
PeerPort => $pport,
Proto => 'tcp',
Timeout => 20,
);
unless ($sock) {
$self->debug("Unable to connect to proxy $proxy:$pport; Reason: $!.");
}
else {
$sock->printf( "CONNECT %s:%d HTTP/1.0\r\n\r\n", $server, $port );
if ( $sock->getline =~ m!^HTTP/1\.\d+ 200 ! ) {
# Skip through remaining part of MIME header.
while ( $sock->getline !~ m!^\r! ) { ; }
}
else {
$self->log( 4, "HTTP tunneling is disabled at $proxy." );
$sock = undef;
}
}
}
# if proxy, we already might have a $sock.
# if proxy failed to connect, try without proxy.
#
if ( $self->{conf}->{socks_server} ) {
my $socks_module = "Net::SOCKS";
eval "require $socks_module";
$self->log( 6, "Will try to connect through the SOCKS server on $$self{conf}{socks_server}..." );
my $socks_sock = Net::SOCKS->new(
socks_addr => $$self{conf}{socks_server},
socks_port => 1080,
protocol_version => 4
);
if ($socks_sock) {
$sock = $socks_sock->connect( peer_addr => $server, peer_port => $port );
if ($sock) {
$self->log( 6, "Connected to $server via SOCKS server $$self{conf}{socks_server}." );
}
}
}
unless ($sock) {
$sock = IO::Socket::INET->new(
PeerAddr => $server,
PeerPort => $port,
Proto => 'tcp',
Timeout => 20,
);
unless ($sock) {
$self->log( 3, "Unable to connect to $server:$port; Reason: $!." );
return if $params{discovery_server};
$self->nextserver or do { return $self->errprefix("connect1"); };
return $self->connect;
}
}
my $select = new IO::Select($sock);
my @handles = $select->can_read(15);
if ( $handles[0] ) {
$self->log( 8, "Connection established" );
my $greeting = <$sock>;
# $sock->autoflush; # this is on by default as of IO::Socket 1.18
$self->{sock} = $sock;
$self->{connected_to} = $server;
$self->{select} = $select;
$self->log( 4, "$server >> " . length($greeting) . " server greeting: $greeting" );
return 1 if $params{discovery_server};
unless ( $self->parse_greeting($greeting) ) {
$self->nextserver or return $self->errprefix("connect2");
return $self->connect;
}
return 1;
}
else {
$self->log( 3, "Timed out (15 sec) while reading from $self->{s}->{ip}." );
$select->remove($sock);
$sock->close();
return $self->errprefix("connect3") if $params{skip_greeting};
$self->nextserver or return $self->errprefix("connect4");
return $self->connect;
}
}
sub disconnect {
my $self = shift;
unless ( $self->{sock} ) {
$self->log( 5, "already disconnected from server " . $self->{connected_to} );
return 1;
}
$self->log( 5, "disconnecting from server " . $self->{connected_to} );
$self->{disconnecting} = 1;
$self->_send( ["a=q\r\n"], 0, 1 );
delete $self->{disconnecting};
delete $self->{sock}; # _send closes socket
return 1;
}
sub parse_greeting {
my ( $self, $greeting ) = @_;
$self->log( 16, "entered parse_greeting($greeting)" );
my %server_greeting = parsesis($greeting);
$self->{s}->{greeting} = \%server_greeting;
unless ( $self->{s}->{greeting} && $self->{s}->{greeting}->{sn} ) {
$self->log( 1, "Couldn't parse server greeting\n" );
return;
}
# server greeting must contain: sn, srl
# server greeting may contain: ep4, redirect, a,
#
# fixme - add support for redirect, etc.
#
#
# current server config info is stored in $self->{s}->{conf}
# see nextserver for more info
#
# If server greeting says there are new values
# (which we know if greeting's srl > conf's srl)
# we ask server for new values, update conf, then
# put that server on modified list so it gets recorded to disk
#
# fixme - in the future, we could have a key with no value
# in .conf file - forcing client to ask server 'a=g&pm=key'
#
if ( $self->{s}->{greeting}->{a} eq 'cg' ) {
my $version = $Razor2::Client::Version::VERSION;
my @cg = ("cn=razor-agents&cv=$version\r\n");
$self->_send( \@cg, 0, 1 );
}
if ( defined( $self->{s}->{greeting}->{srl} )
&& defined( $self->{s}->{conf}->{srl} )
&& $self->{s}->{greeting}->{srl} <= $self->{s}->{conf}->{srl} ) {
$self->compute_server_conf;
return 1;
}
# srl > our cached srl, request update (a=g&pm=state)
# and rediscover
#
my @queries = ("a=g&pm=state\r\n");
my $response = $self->_send( \@queries ) or return $self->errprefix("parse_greeting");
# should be just one response
# from_batched_query wants "-" in beginning, but not ".\r\n" at end
$response->[0] =~ s/\.\r\n$//sg;
my $h = from_batched_query( $response->[0], {} );
foreach my $href (@$h) {
foreach ( sort keys %$href ) {
$self->{s}->{conf}->{$_} = $href->{$_};
#$self->log(8,"updated: $_=$href->{$_}");
}
}
$self->log( 1, "Bad info while trying to get server state (a=g&pm=state)" )
unless scalar(@$h);
$self->{s}->{conf}->{srl} = $self->{s}->{greeting}->{srl};
push @{ $self->{s}->{modified} }, $self->{s}->{ip};
$self->{s}->{allconfs}->{ $self->{s}->{ip} } = $self->{s}->{conf}; # in case new server
# now we're up to date
$self->log( 5, "Updated to new server state srl " . $self->{s}->{conf}->{srl} . " for server " . $self->{s}->{ip} );
$self->compute_server_conf();
$self->writeservers; # writes to disk servers listed in $self->{s}->{modified}
$self->log( 5, "srl was updated, forcing discovery ..." );
$self->{done_discovery} = 0;
$self->{force_discovery} = 1;
$self->discover();
return 1;
}
# Returns engines supported
#
# can be called with no paramaters or
# with hash of server supported engines
sub compute_supported_engines {
my ( $self, $orig ) = @_;
my %all;
my $se = $self->supported_engines(); # local supported engines
foreach ( @{ $self->{conf}->{use_engines} } ) {
if ($orig) {
$all{$_} = 1 if ( exists $se->{$_} ) && ( exists $orig->{$_} );
}
else {
$all{$_} = 1 if exists $se->{$_};
}
}
if ($orig) {
$self->log( 8, "Computed supported_engines: " . join( ' ', sort( keys %all ) ) );
}
else {
$self->log( 8, "Client supported_engines: " . join( ' ', sort( keys %all ) ) );
}
return \%all;
}
# called when we need to parse server conf
# - after initial parse_greeting
# - if state (srl) changes
# - when we switch to cached server conf info in nextserver
#
sub compute_server_conf {
my ( $self, $cached ) = @_;
#
# compute a confindence (cf) from razor-agent.conf's 'min_cf'
# and server's average confidence (ac)
#
# min_cf can be 'n', 'ac', 'ac + n', or 'ac - n'
# where 'n' can be 1..100
#
my $cf = $self->{s}->{conf}->{ac}; # default is server's ac
my $min_cf = $self->{conf}->{min_cf};
$min_cf =~ s/\s//g;
if ( $min_cf =~ /^ac\+(\d+)$/ ) {
$cf = $self->{s}->{conf}->{ac} + $1;
}
elsif ( $min_cf =~ /^ac-(\d+)$/ ) {
$cf = $self->{s}->{conf}->{ac} - $1;
}
elsif ( $min_cf =~ /^ac$/ ) {
$cf = $self->{s}->{conf}->{ac};
}
elsif ( $min_cf =~ /^(\d+)$/ ) {
$cf = $min_cf;
}
else {
$self->log( 5, "Invalid min_cf $self->{conf}->{min_cf}" );
}
$cf = 100 if $cf > 100;
$cf = 0 if $cf < 0;
$self->{s}->{min_cf} = $cf;
#
# ep4 - special for vr4
#
$self->{s}->{conf}->{ep4} = $self->{s}->{greeting}->{ep4}
if $self->{s}->{greeting}->{ep4};
my $info = $cached ? $self->{s}->{conf} : $self->{s}->{greeting};
my $name = "Unknown-Type: ";
if ( $info->{sn} ) {
$name .= $info->{sn};
$name = "Nomination" if $info->{sn} =~ /N/;
$name = "Catalogue" if $info->{sn} =~ /C/;
$name = "Catalogue" if $info->{sn} =~ /S/;
$name = "Discovery" if $info->{sn} =~ /D/;
}
$self->log( 6, $self->{s}->{ip} . " is a $name Server srl " . $self->{s}->{conf}->{srl} . "; computed min_cf=$cf, Server se: $self->{s}->{conf}->{se}" );
#
# Supported Engines - greeting contains hex of bits
# we turn into a hash so we can just quickly do
# do_eng3_stuff if $self->{s}->{engines}->{3};
#
# if we're just computing hashes locally, ignore what engines server currently supports
# fixme - this prolly should be done somewhere else
if ( $self->{opt}->{printhash} ) {
$self->log( 6, "Ignore what engines server supports for -H" );
$self->{s}->{engines} = $self->compute_supported_engines();
}
else {
my $se = hexbits2hash( $self->{s}->{conf}->{se} );
$self->{s}->{engines} = $self->compute_supported_engines($se);
}
}
# sub log2file moved to Agent.pm
sub debug {
my ( $self, $message ) = @_;
$self->log( 5, $message );
}
sub DESTROY {
my $self = shift;
#$self->debug ("Agent terminated");
}
sub zonename {
my ( $zone, $type ) = @_;
my ( $sub, $dom ) = split /\./, $zone, 2;
return "$sub-$type.$dom";
}
1;