blob: 5380c65f8f2e7207635390ae394c1a9b85678729 [file] [log] [blame]
# please insert nothing before this line: -*- mode: cperl; cperl-indent-level: 4; cperl-continued-statement-offset: 4; indent-tabs-mode: nil -*-
package TestFilter::in_bbs_inject_header;
# this filter demonstrates two things:
# 1. how to write a filter that will work only on HTTP headers
# 2. how to inject extra HTTP headers
#
# the first task is simple for non-keepalive connections -- as soon as
# a bucket which matches /^[\r\n]+$/ is read we can store that event
# in the filter context and simply 'return Apache2::Const::DECLINED on the
# future invocation, so not to slow things.
#
# it becomes much trickier with keepalive connection, since Apache
# provides no API to tell you whether a new request is coming in. We
# use $c->keepalives to figure out when a new request is coming in, by
# comparing the previously stored keepalives count, which gets
# incremented by Apache when the HTTP response headers are generated.
#
#
# the second task is a bit trickier, as the headers_in core httpd
# filter is picky and it wants each header to arrive in a separate
# bucket, and moreover this bucket needs to be in its own brigade.
# so this test arranges for this to happen.
#
# the test shows how to push headers at the end of all headers
# and in the middle, whichever way you prefer.
use strict;
use warnings FATAL => 'all';
use base qw(Apache2::Filter);
use Apache2::RequestRec ();
use Apache2::RequestIO ();
use Apache2::Connection ();
use APR::Brigade ();
use APR::Bucket ();
use APR::Table ();
use Apache::TestTrace;
use TestCommon::Utils ();
use Apache2::Const -compile => qw(OK DECLINED CONN_KEEPALIVE);
use APR::Const -compile => ':common';
my $header1_key = 'X-My-Protocol';
my $header1_val = 'POST-IT';
my %headers = (
'X-Extra-Header2' => 'Value 2',
'X-Extra-Header3' => 'Value 3',
);
my $request_body = "This body shouldn't be seen by the filter";
# returns 1 if a bucket with a header was inserted to the $bb's tail,
# otherwise returns 0 (i.e. if there are no buckets to insert)
sub inject_header_bucket {
my ($bb, $ctx) = @_;
return 0 unless @{ $ctx->{buckets} };
my $b = shift @{ $ctx->{buckets} };
$bb->insert_tail($b);
if (1) {
# extra debug, wasting cycles
$b->read(my $data);
debug "injected header: [$data]";
}
else {
debug "injected header";
}
# next filter invocations will bring the request body if any
if ($ctx->{seen_body_separator} && !@{ $ctx->{buckets} }) {
$ctx->{done_with_headers} = 1;
$ctx->{seen_body_separator} = 0;
}
return 1;
}
sub context {
my $filter = shift;
my $ctx = $filter->ctx;
my $c = $filter->c;
unless ($ctx) {
debug "filter context init";
$ctx = {
buckets => [],
done_with_headers => 0,
seen_body_separator => 0,
keepalives => $c->keepalives,
};
# since we are going to manipulate the reference stored in
# ctx, it's enough to store it only once, we will get the same
# reference in the following invocations of that filter
$filter->ctx($ctx);
return $ctx;
}
if ($c->keepalive == Apache2::Const::CONN_KEEPALIVE &&
$ctx->{done_with_headers} &&
$c->keepalives > $ctx->{keepalives}) {
debug "a new request resetting the input filter state";
$ctx->{buckets} = [];
$ctx->{done_with_headers} = 0;
$ctx->{seen_body_separator} = 0;
$ctx->{keepalives} = $c->keepalives;
}
return $ctx;
}
sub handler : FilterConnectionHandler {
my ($filter, $bb, $mode, $block, $readbytes) = @_;
debug join '', "-" x 20 , " input filter called -", "-" x 20;
my $ctx = context($filter);
my $c = $filter->c;
# reset the filter state, we start a new request
if ($c->keepalive == Apache2::Const::CONN_KEEPALIVE &&
$ctx->{done_with_headers} && $c->notes->get('reset_request')) {
debug "a new request resetting the input filter state";
$c->notes->set('reset_request' => 0);
$ctx->{buckets} = [];
$ctx->{seen_body_separator} = 0;
$ctx->{done_with_headers} = 0;
}
# handling the HTTP request body
if ($ctx->{done_with_headers}) {
# XXX: when the bug in httpd filter will be fixed all the
# code in this branch will be replaced with:
# $filter->remove;
# return Apache2::Const::DECLINED;
# at the moment (2.0.48) it doesn't work
# so meanwhile tell the mod_perl filter core to pass-through
# the brigade unmodified
debug "passing the body through unmodified";
return Apache2::Const::DECLINED;
}
# any custom HTTP header buckets to inject?
return Apache2::Const::OK if inject_header_bucket($bb, $ctx);
# normal HTTP headers processing
my $ctx_bb = APR::Brigade->new($c->pool, $c->bucket_alloc);
my $rv = $filter->next->get_brigade($ctx_bb, $mode, $block, $readbytes);
return $rv unless $rv == APR::Const::SUCCESS;
while (!$ctx_bb->is_empty) {
my $b = $ctx_bb->first;
if ($b->is_eos) {
debug "EOS!!!";
$b->remove;
$bb->insert_tail($b);
last;
}
$b->read(my $data);
# remove must happen after read, since it may cause split and
# some new buckets inserted behind - if remove called too
# early, those buckets will be lost
$b->remove;
debug "filter read:\n[$data]";
# check that we really work only on the headers
die "This filter should not ever receive the request body, " .
"but HTTP headers" if ($data||'') eq $request_body;
if ($data and $data =~ /^POST/) {
# demonstrate how to add a header while processing other headers
my $header = "$header1_key: $header1_val\r\n";
push @{ $ctx->{buckets} }, APR::Bucket->new($c->bucket_alloc, $header);
debug "queued header [$header]";
}
elsif ($data =~ /^[\r\n]+$/) {
# normally the body will start coming in the next call to
# get_brigade, so if your filter only wants to work with
# the headers, it can decline all other invocations if that
# flag is set. However since in this test we need to send
# a few extra bucket brigades, we will turn another flag
# 'done_with_headers' when 'seen_body_separator' is on and
# all headers were sent out
debug "END of original HTTP Headers";
$ctx->{seen_body_separator}++;
# we hit the headers and body separator, which is a good
# time to add extra headers:
for my $key (keys %headers) {
my $header = "$key: $headers{$key}\r\n";
push @{ $ctx->{buckets} }, APR::Bucket->new($c->bucket_alloc, $header);
debug "queued header [$header]";
}
# but at the same time we must ensure that the
# the separator header will be sent as a last header
# so we send one newly added header and push the separator
# to the end of the queue
# XXX: this is broken: the bucket must be set-aside before
# it can be stashed away (missing $b->setaside wrapper)
push @{ $ctx->{buckets} }, $b;
debug "queued header [$data]";
inject_header_bucket($bb, $ctx);
next; # inject_header_bucket already called insert_tail
# notice that if we didn't inject any headers, this will
# still work ok, as inject_header_bucket will send the
# separator header which we just pushed to its queue
}
else {
# fall through
}
$bb->insert_tail($b);
}
return Apache2::Const::OK;
}
sub response {
my $r = shift;
# propogate the input headers and the input back to the client
# as we need to do the validations on the client side
$r->headers_out->set($header1_key =>
$r->headers_in->get($header1_key)||'');
for my $key (sort keys %headers) {
$r->headers_out->set($key => $r->headers_in->get($key)||'');
}
my $data = TestCommon::Utils::read_post($r);
$r->print($data);
Apache2::Const::OK;
}
1;
__END__
<NoAutoConfig>
<VirtualHost TestFilter::in_bbs_inject_header>
PerlModule TestFilter::in_bbs_inject_header
PerlInputFilterHandler TestFilter::in_bbs_inject_header
<Location /TestFilter__in_bbs_inject_header>
SetHandler modperl
PerlResponseHandler TestFilter::in_bbs_inject_header::response
</Location>
</VirtualHost>
</NoAutoConfig>