qw(OK FORBIDDEN NOT_FOUND MODE_GETLINE MODE_READBYTES SERVER_ERROR)
;
use
APR::Const
-compile
=>
qw(SUCCESS SO_NONBLOCK BLOCK_READ)
;
use
constant
APACHE24
=> have_min_apache_version(
'2.4.0'
);
our
$spamtest
;
sub
handler {
my
(
$c
) =
@_
;
$c
->client_socket->opt_set(APR::Const::
SO_NONBLOCK
=> 0);
my
$self
= __PACKAGE__->new(
c
=>
$c
,
spamtest
=>
$spamtest
,
pool
=>
$c
->pool);
$self
->log_connection;
if
(
defined
(
my
$ret
=
$self
->read_headers)) {
return
$ret
;
}
$self
->check_headers
or
return
Apache2::Const::FORBIDDEN;
$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;
alarm
0;
}
else
{
$self
->pass_through_sa;
}
};
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 {
my
$class
=
shift
;
my
$self
= {
@_
};
$self
->{start_time} ||=
time
;
bless
$self
, (
ref
$class
||
$class
);
$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 {
my
$self
=
shift
;
if
(
exists
$self
->{parsed}) {
delete
$self
->{parsed};
$self
->{parsed}->finish
if
$self
->{parsed};
}
if
(
exists
$self
->{status}) {
$self
->status->finish
if
$self
->status;
delete
$self
->{status};
}
$self
->in->destroy;
$self
->out->destroy;
}
sub
c {
$_
[0]->{c} }
sub
in {
$_
[0]->{in} }
sub
out {
$_
[0]->{out} }
sub
_server {
$_
[0]->c->base_server }
sub
_remote_host {
$_
[0]->c->get_remote_host }
sub
_remote_ip { APACHE24 ?
$_
[0]->c->client_ip :
$_
[0]->c->remote_ip; }
sub
_remote_port { APACHE24 ?
$_
[0]->c->client_addr->port :
$_
[0]->c->remote_addr->port }
sub
send_buffer {
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'
;
if
(!
defined
(
$ident_username
) ||
$username
ne
$ident_username
) {
info(
"ident username ($dn) does not match "
.
"spamc username ($username)"
);
return
0;
}
1;
}
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 {
my
$self
=
shift
;
my
$line_num
;
while
(
my
$line
=
$self
->getline) {
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;
}
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 /) {
$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;
}
last
unless
length
$line
;
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]/;
if
(
$header
=~ /^(?:Content-[Ll]ength|User|Message-[Cc]lass|Set|Remove)$/) {
$header
=~ y/A-Z-/a-z_/;
$self
->headers_in->{
$header
} =
$value
;
}
else
{
warn
"unknown header: '$header'='$value'"
;
}
}
undef
;
}
sub
read_body {
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;
next
unless
$self
->in->flatten(
my
$chunk
);
$self
->in->cleanup;
my
$chlen
=
length
$chunk
;
$len
+=
$chlen
;
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
;
}
sub
handle_user_local {
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'
}) {
$self
->service_unavailable_error(
$errmsg
);
}
else
{
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
);
my
$user_dir
= $) == (
stat
$cf_dir
)[5] ?
$dir
:
undef
;
$self
->spamtest->signal_user_changed(
{
username
=>
$username
,
user_dir
=>
$user_dir
, });
}
return
1;
}
1;