package Mail::SpamAssassin::Spamd::Apache2::AclRFC1413;
use strict;

use Apache2::Const -compile => qw(OK FORBIDDEN SERVER_ERROR);
use Apache2::Connection  ();
use Apache2::RequestUtil ();    # RequestRec->new
use Apache2::RequestRec  ();
use Apache2::Access      ();    # $r->get_remote_logname

use Apache::Test;
use constant APACHE24   => have_min_apache_version('2.4.0');

use APR::SockAddr ();           # $c->remote_addr->...
use APR::Table    ();           # $c->notes

=head1 NAME

Mail::SpamAssassin::Spamd::Apache2::AclRFC1413 - check spamd's client ident

=head1 SYNOPSIS

 ##### in httpd.conf:
 # engine; module has been separated in Apache 2.1
 LoadModule ident_module modules/mod_ident.so
 IdentityCheck   on
 IdentityTimeout 4

 # enable check
 PerlLoadModule Mail::SpamAssassin::Spamd::Apache2::Config
 SAident on

 ##### in PerlProcessConnectionHandler:
 Mail::SpamAssassin::Spamd::Apache2::AclRFC1413::check_ident($c, "user")
   or return Apache2::Const::FORBIDDEN;

 # or like this:
 my $remote_logname =
   Mail::SpamAssassin::Spamd::Apache2::AclRFC1413::get_ident($c)

=head1 DESCRIPTION

Queries remote ident server using mod_ident.so, saves result in
C<$c-E<gt>notes()>.

Returns C<Apache2::Const::FORBIDDEN> on failure.

The C<SAident On> directive actually does this:
 PerlPreConnectionHandler Mail::SpamAssassin::Spamd::Apache2::AclRFC1413

=head1 NOTE

Doing ident for non-localhost users is rather pointless.  Unless you
know what you're doing, listen only on C<127.0.0.1> and/or C<::1>, if
you want to prevent users from lying about their identity.  Or use SSL
with client certificates (refer to C<mod_ssl> documentation for details).

=head1 FUNCTIONS

=cut

sub handler {
  my ($c) = @_;

  # is there a point in doing ident for remote users?
  #$c->remote_ip eq $c->local_ip
  #  or return Apache2::Const::FORBIDDEN;

  my $r = Apache2::RequestRec->new($c)
    or die 'Apache2::RequestRec->new($c) failed';

  my $remote_user = $r->get_remote_logname;

  unless (defined $remote_user && length $remote_user) {
    warn 'rfc1413 check: failed to obtain info for '
      . APACHE24 ? $c->client_addr->ip_get() : $c->remote_addr->ip_get() . ':'
      . APACHE24 ? $c->client_addr->port() : $c->remote_addr->port() . "\n";
    return Apache2::Const::FORBIDDEN;
  }

  my $notes = $c->notes    # APR::Table
    or die '$c->notes failed';
  $notes->{remote_user} = $remote_user;
  $c->notes($notes);

  return Apache2::Const::OK;
}

=head2 check_ident($c, $username)

Returns remote username (might be "0"), as returned by the ident server, if it
matches supplied $username; undef otherwise.

=cut

sub check_ident {
  my ($c, $user) = @_;
  my $remote_user = $c->notes->{remote_user};
  die "rfc1413 check: no query result for user=$user ip="
    . APACHE24 ? $c->client_addr->ip_get() : $c->remote_addr->ip_get()
    . ' port='
    . APACHE24 ? $c->client_addr->port() : $c->remote_addr->port()
    unless defined $remote_user && length $remote_user;
  return $remote_user if $user eq $remote_user;
  warn "ident mismatch for [$user] from "
    . APACHE24 ? $c->client_addr->ip_get() : $c->remote_addr->ip_get() . ':'
    . APACHE24 ? $c->client_addr->port() : $c->remote_addr->port()
    . "; remote identd returned [$remote_user]\n";
  0;
}

=head2 get_ident($c)

Returns remote username (might be "0"), as returned by the ident server.

=cut

sub get_ident {
  my ($c) = @_;
  $c->notes->{remote_user};
}

=head1 EXPORTS

Nothing.

=head1 BUGS

See E<lt>http://bugzilla.spamassassin.org/E<gt>

=head1 SEE ALSO

C<Mail::SpamAssassin::Spamd::Apache2::Config(3)>

=cut

1;

# vim: ts=8 sw=2 et
