File: Apache2.pm

package info (click to toggle)
spamassassin 3.4.0-6
  • links: PTS, VCS
  • area: main
  • in suites: jessie, jessie-kfreebsd
  • size: 8,296 kB
  • ctags: 2,968
  • sloc: perl: 54,007; ansic: 3,397; sh: 590; makefile: 195; sql: 176; python: 17
file content (391 lines) | stat: -rw-r--r-- 10,657 bytes parent folder | download | duplicates (5)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
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   ();

eval { use Time::HiRes qw(time); };

use vars qw($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<PerlProcessConnectionHandler>.  It's
different from regular HTTP handlers (C<PerlResponseHandler>) -- 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<PerlPreConnectionHandler>.

This means you can't use modules which hook themselves in, for example,
C<PerlAccessHandler>.  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   { $_[0]->c->remote_ip }            # -: a
sub _remote_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 <http://bugzilla.spamassassin.org/>.

=head1 SEE ALSO

L<httpd(8)>,
L<spamd(1)>,
L<apache-spamd(1)>,
L<Mail::SpamAssassin::Spamd::Apache2::Config(3)>

=cut

1;

# vim: ts=2 sw=2 et