package Mail::SpamAssassin::Spamd::Apache2; use strict; use Apache2::Const -compile => qw(OK FORBIDDEN NOT_FOUND MODE_GETLINE MODE_READBYTES SERVER_ERROR); use Apache2::Connection (); use Apache2::Filter (); use Apache2::Module (); use Apache2::ServerRec (); use Apache2::ServerUtil (); use APR::Const -compile => qw(SUCCESS SO_NONBLOCK BLOCK_READ); use APR::Brigade (); use APR::Bucket (); use APR::Error (); use APR::Pool (); # cleanup_register use APR::SockAddr (); use APR::Socket (); use APR::Status (); use Apache::Test; use constant APACHE24 => have_min_apache_version('2.4.0'); eval { use Time::HiRes qw(time); }; our $spamtest; use Mail::SpamAssassin (); use Mail::SpamAssassin::Message (); use Mail::SpamAssassin::PerMsgStatus (); use Mail::SpamAssassin::Logger; use base qw(Mail::SpamAssassin::Spamd); =head1 NAME Mail::SpamAssassin::Spamd::Apache2 -- spamd protocol handler for Apache2 =head1 SYNOPSIS SetHandler modperl PerlProcessConnectionHandler Mail::SpamAssassin::Spamd::Apache2 =head1 DESCRIPTION What is this obsession with documentation? Don't you have the source? -- Michael G Schwern on makemaker@perl.org This is a protocol handler, to be run as C. It's different from regular HTTP handlers (C) -- we don't have the C<$r> object (unless we create it) and the only other run-time Apache hook which will run is C. This means you can't use modules which hook themselves in, for example, C. If there is a clean way to enable it, don't hesitate to drop me an e-mail. =head1 INTERNALS handler() runs read_headers(), then check_headers(). If the User header has been provided by the client and user configuration has been enabled, it runs read_user_config(). Then it reads body, passes it through SA and sends reply. =cut sub handler { # -: c my ($c) = @_; # Apache2::Connection $c->client_socket->opt_set(APR::Const::SO_NONBLOCK => 0); # ? my $self = __PACKAGE__->new(c => $c, spamtest => $spamtest, pool => $c->pool); $self->log_connection; # we might be done after this in case of client error or SKIP / PING if (defined(my $ret = $self->read_headers)) { return $ret; } $self->check_headers or return Apache2::Const::FORBIDDEN; # should we complain if returns 0 and --paranoid? $self->read_user_config; if (defined(my $ret = $self->read_body)) { return $ret; } $self->parse_msgids; $self->log_start_work; eval { if ($self->cfg->{satimeout}) { local $SIG{ALRM} = sub { die 'child processing timeout' }; alarm $self->cfg->{satimeout}; $self->pass_through_sa; # do the checking alarm 0; } else { $self->pass_through_sa; # do the checking } }; if ($@) { if ( $@ =~ /child processing timeout/ ) { $self->service_timeout( sprintf '(%d second timeout while trying to %s)', $self->cfg->{satimeout}, $self->{method} ); } else { warn "spamd: $@"; } return Apache2::Const::SERVER_ERROR; } $self->send_status_line('EX_OK'); $self->send_response; $self->log_end_work; $self->log_result; return Apache2::Const::OK; } sub new { # -: A my $class = shift; my $self = {@_}; # requires: c, spamtest $self->{start_time} ||= time; bless $self, (ref $class || $class); ##$self->{c} ||= $self->r->connection if $self->r; $self->{in} ||= APR::Brigade->new($self->c->pool, $self->c->bucket_alloc); $self->{out} ||= APR::Brigade->new($self->c->pool, $self->c->bucket_alloc); $self->{cfg} ||= Apache2::Module::get_config('Mail::SpamAssassin::Spamd::Apache2::Config', $self->_server); $self->{headers_in} ||= {}; $self; } sub DESTROY { # -: a my $self = shift; if (exists $self->{parsed}) { delete $self->{parsed}; $self->{parsed}->finish if $self->{parsed}; # can't do it before status->rewrite_mail } if (exists $self->{status}) { $self->status->finish if $self->status; delete $self->{status}; } $self->in->destroy; $self->out->destroy; } sub c { $_[0]->{c} } # -: A sub in { $_[0]->{in} } # -: a sub out { $_[0]->{out} } # -: a sub _server { $_[0]->c->base_server } # -: a sub _remote_host { $_[0]->c->get_remote_host } # -: a sub _remote_ip { APACHE24 ? $_[0]->c->client_ip : $_[0]->c->remote_ip; } # -: a sub _remote_port { APACHE24 ? $_[0]->c->client_addr->port : $_[0]->c->remote_addr->port } # -: a sub send_buffer { # -: A my $self = shift; for my $buffer (@_) { $self->out->insert_tail(APR::Bucket->new($self->out->bucket_alloc, $buffer)); } $self->c->output_filters->fflush($self->out); } sub auth_ident { # -: my $self = shift; my ($username) = @_; my $ident_username = Mail::SpamAssassin::Spamd::Apache2::AclRFC1413::get_ident($username); my $dn = $ident_username || 'NONE'; # display name # we might also log $c->remote_addr->ip_get(), $c->remote_addr->port() # dbg("ident: ident_username = $dn, spamc_username = $username\n"); if (!defined($ident_username) || $username ne $ident_username) { info( "ident username ($dn) does not match " . "spamc username ($username)"); return 0; } 1; } #sub read_line { # -: A # my $self = shift; #} sub getline { my $self = shift; my $rc = $self->c->input_filters->get_brigade($self->in, Apache2::Const::MODE_GETLINE); last if APR::Status::is_EOF($rc); die APR::Error::strerror($rc) unless $rc == APR::Const::SUCCESS; next unless $self->in->flatten(my $line); $self->in->cleanup; $line =~ y/\r\n//d; return $line; } sub read_headers { # -: A my $self = shift; my $line_num; while (my $line = $self->getline) { # XXX: lower this to 10? if (++$line_num > 255) { $self->protocol_error('(too many headers)'); return Apache2::Const::FORBIDDEN; } if (length $line > 200) { $self->protocol_error('(line too long)' . length $line); return Apache2::Const::FORBIDDEN; } # get method name unless ($self->{method}) { if ($line =~ /^(SKIP|PING|PROCESS|CHECK|SYMBOLS|REPORT|HEADERS|REPORT_IFSPAM|TELL) \ SPAMC\/(\d{1,2}\.\d{1,3})\b/x) { $self->{method} = $1; $self->{client_version} = $2; if ($self->{method} eq 'PING') { $self->send_status_line('EX_OK', 'PONG'); return Apache2::Const::OK; } elsif ($self->{method} eq 'SKIP') { return Apache2::Const::OK; } elsif ($self->{method} eq 'TELL' && !$self->cfg->{allow_tell}) { $self->service_unavailable_error('TELL commands have not been enabled.'); return Apache2::Const::FORBIDDEN; } next; } elsif ($line =~ /^GET /) { # treat this like ping $self->send_buffer( join "\r\n", 'HTTP/1.0 200 SA running', 'Content-Type: text/plain', 'Content-Length: 0', '' ); return Apache2::Const::OK; } $self->protocol_error('method required' . ": '$line'"); return Apache2::Const::NOT_FOUND; # something more reasonable? } last unless length $line; # end of headers # get headers, ignore unknown my ($header, $value) = split /:\s+/, $line, 2; unless (defined $header && length $header && defined $value && length $value) { $self->protocol_error("(header not in 'Name: value' format)"); return Apache2::Const::FORBIDDEN; } return Apache2::Const::FORBIDDEN if $header =~ /[^a-z\d_-]/i || $value =~ /[^\x20-\xFF]/; # naughty if ($header =~ /^(?:Content-[Ll]ength|User|Message-[Cc]lass|Set|Remove)$/) { $header =~ y/A-Z-/a-z_/; $self->headers_in->{$header} = $value; } else { # FIXME: remove warn "unknown header: '$header'='$value'"; } } undef; } sub read_body { # -: A my $self = shift; my ($message, $len) = ('', 0); my $content_length = $self->headers_in->{content_length}; while (1) { my $rc = $self->c->input_filters->get_brigade($self->in, Apache2::Const::MODE_READBYTES, APR::Const::BLOCK_READ, ($content_length ? $content_length - $len : ())); last if APR::Status::is_EOF($rc); die APR::Error::strerror($rc) unless $rc == APR::Const::SUCCESS; # timeout next unless $self->in->flatten(my $chunk); $self->in->cleanup; my $chlen = length $chunk; $len += $chlen; # this is never true, actually... get_brigade ensures we won't get # more bytes... well, at least it's logically correct. ;-) # we could check if $message ends with "\n" to detect weird cases. if ($content_length && $len > $content_length) { $self->protocol_error('(Content-Length mismatch: Expected' . " $content_length bytes, got $len bytes"); return Apache2::Const::FORBIDDEN; } $message .= $chunk; last if $content_length && $len == $content_length; } $self->{actual_length} = $len; $self->{parsed} = $self->spamtest->parse($message , 0); undef; } # # Code to deal with user configuration. # # Run handle_* directly (ie. not from read_user_config) only if you know # what you are doing. # # Change handle_* to return undef if not found and 0 if something's wrong? # sub handle_user_local { # -: a require File::Spec; my $self = shift; my($username) = @_; my ($name, $uid, $gid, $dir) = (getpwnam $username)[0, 2, 3, 7]; unless (defined $uid) { my $errmsg = "handle_user unable to find user: '$username'"; if ($self->spamtest->{'paranoid'}) { # FIXME: return something? die? whatever? $self->service_unavailable_error($errmsg); } else { # if we are given a username, but can't look it up, maybe name # services are down? let's break out here to allow them to get # 'defaults' when we are not running paranoid info($errmsg); } return 0; } my $cf_dir = File::Spec->catdir($dir, '.spamassassin'); my $cf_file = File::Spec->catfile($cf_dir, 'user_prefs'); if (!-l $cf_dir && -d _ && !-d $cf_file && -f _ && -s _) { $self->spamtest->read_scoreonly_config($cf_file); # if the $cf_dir group matches ours, assume we can write there my $user_dir = $) == (stat $cf_dir)[5] ? $dir : undef; $self->spamtest->signal_user_changed( { username => $username, user_dir => $user_dir, }); } return 1; } =head1 TODO Timeout... NetSet =head1 BUGS See Ehttp://bugzilla.spamassassin.org/E. =head1 SEE ALSO C, C, C, C =cut 1; # vim: ts=2 sw=2 et