File Coverage

blib/lib/Apache2/Proxy.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Apache2::Proxy;
2              
3 1     1   22160 use strict;
  1         3  
  1         40  
4 1     1   6 use warnings;
  1         3  
  1         62  
5              
6             our $VERSION = 0.04;
7              
8             =head1 NAME
9              
10             Apache2::Proxy - a mod_perl based http proxy base class
11              
12             =head1 SYNOPSIS
13              
14             A mod_perl based HTTP proxy base class
15              
16             =cut
17              
18              
19 0           use Apache2::Const -compile => qw( OK SERVER_ERROR NOT_FOUND DECLINED
20             REDIRECT LOG_DEBUG LOG_ERR LOG_INFO CONN_KEEPALIVE HTTP_BAD_REQUEST
21             HTTP_UNAUTHORIZED HTTP_SEE_OTHER HTTP_MOVED_PERMANENTLY DONE
22 1     1   464 HTTP_NO_CONTENT HTTP_PARTIAL_CONTENT HTTP_NOT_MODIFIED );
  0            
23             use Apache2::Connection ();
24             use Apache2::Log ();
25             use Apache2::RequestRec ();
26             use Apache2::RequestUtil ();
27             use Apache2::RequestIO ();
28             use Apache2::Response ();
29             use Apache2::ServerRec ();
30             use Apache2::ServerUtil ();
31             use Apache2::URI ();
32             use Apache2::Filter ();
33             use APR::Table ();
34              
35             use Compress::Zlib ();
36             use Compress::Bzip2 ();
37             use Encode ();
38              
39             use URI;
40             use Net::HTTP;
41             use HTTP::Response;
42             use HTTP::Headers;
43             use HTTP::Headers::Util ();
44             use Data::Dumper;
45             use Net::DNS;
46             our $Resolver = Net::DNS::Resolver->new;
47              
48             use constant DEBUG => 1;
49             use constant VERBOSE_DEBUG => 0;
50              
51             use constant MAX_CONTENT_LENGTH => 131072; # 128k
52              
53             # firefox default headers
54             our %Headers = (
55             'Accept-Encoding' => 'gzip,deflate',
56             'Accept-Charset' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
57             'Accept-Lang' => 'en-us,en;q=0.5',
58             'Accept' =>
59             'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
60             'User-Agent' =>
61             'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.0.10) Gecko/2009042315 Firefox/3.0.10',
62             );
63              
64             our %Response = (
65             200 => 'twohundred',
66             204 => 'twoohfour',
67             206 => 'twoohsix',
68             301 => 'threeohone',
69             302 => 'redirect',
70             303 => 'redirect',
71             304 => 'threeohfour',
72             307 => 'redirect',
73             400 => 'bsod',
74             401 => 'bsod',
75             403 => 'bsod',
76             404 => 'bsod',
77             410 => 'bsod',
78             500 => 'bsod',
79             502 => 'bsod',
80             503 => 'bsod',
81             504 => 'bsod',
82             );
83              
84             # handles common proxy functions
85              
86             # takes $r and returns the http headers
87              
88             sub get_request_headers {
89             my ( $class, $r ) = @_;
90              
91             my %headers;
92             $r->headers_in->do(
93             sub {
94             my $k = shift;
95             my $v = shift;
96              
97             if ( $k =~ m/^connection/i ) {
98             $headers{$k} = 'keep-alive';
99             return 1;
100             }
101              
102             # pass this header onto the remote request
103             $headers{$k} = $v;
104              
105             return 1; # don't remove me or you will burn in hell baby
106             }
107             );
108              
109             # work around clients which don't support compression
110             if ( !exists $headers{'Accept-Encoding'} ) {
111             $r->log->debug(
112             "$$ client DOES NOT support compression " . Dumper( \%headers ) )
113             if VERBOSE_DEBUG;
114              
115             # set default outgoing compression headers
116             $headers{'Accept-Encoding'} = 'gzip, deflate';
117             }
118             else {
119             $r->log->debug(
120             "$$ client supports compression " . $headers{'Accept-Encoding'} )
121             if VERBOSE_DEBUG;
122             $r->pnotes(
123             client_supports_compression => $headers{'Accept-Encoding'} );
124             }
125              
126             $r->log->debug( "$$ proxy request headers " . Dumper( \%headers ) )
127             if DEBUG;
128              
129             return \%headers;
130             }
131              
132             # Takes an HTTP::Response object, clears the response headers,
133             # adds cookie and auth headers, and additional headers
134             # Sets the Server header to sl if it is not defined.
135              
136             sub set_response_headers {
137             my ( $class, $r, $res ) = @_;
138              
139             #############################
140             # clear the current headers
141             $r->headers_out->clear();
142              
143             $class->translate_cookie_and_auth_headers( $r, $res );
144              
145             #########################
146             # Create a hash with the remaining HTTP::Response HTTP::Headers attributes
147             my %headers;
148             $res->scan( sub { $headers{ $_[0] } = $_[1]; } );
149              
150             ##########################################
151             # this is for any additional headers, usually site specific
152             $class->translate_remaining_headers( $r, \%headers );
153              
154             # set the server header
155             $headers{Server} ||= __PACKAGE__;
156             $r->log->debug( "$$ server header is " . $headers{Server} )
157             if VERBOSE_DEBUG;
158             $r->server->add_version_component( $headers{Server} );
159              
160             return 1;
161             }
162              
163             sub translate_remaining_headers {
164             my ( $class, $r, $headers ) = @_;
165              
166             foreach my $key ( keys %{$headers} ) {
167              
168             # we set this manually
169             next if lc($key) eq 'server';
170              
171             # skip HTTP::Response inserted headers
172             next if substr( lc($key), 0, 6 ) eq 'client';
173              
174             # let apache set these
175             next if substr( lc($key), 0, 10 ) eq 'connection';
176             next if substr( lc($key), 0, 10 ) eq 'keep-alive';
177              
178             # some headers have an unecessary newline appended so chomp the value
179             chomp( $headers->{$key} );
180             if ( $headers->{$key} =~ m/\n/ ) {
181             $headers->{$key} =~ s/\n/ /g;
182             }
183              
184             $r->log->debug(
185             "$$ Setting header key $key, value " . $headers->{$key} )
186             if VERBOSE_DEBUG;
187             $r->headers_out->set( $key => $headers->{$key} );
188             }
189              
190             return 1;
191             }
192              
193             sub translate_cookie_and_auth_headers {
194             my ( $class, $r, $res ) = @_;
195              
196             ################################################
197             # process the www-auth and set-cookie headers
198             no strict 'refs';
199             foreach my $header_type qw( set-cookie www-authenticate ) {
200             next unless defined $res->header($header_type);
201              
202             my @headers = $res->header($header_type);
203             foreach my $header (@headers) {
204             $r->log->debug("$$ setting header $header_type value $header")
205             if VERBOSE_DEBUG;
206             $r->err_headers_out->add( $header_type => $header );
207             }
208              
209             # and remove it from the response headers
210             my $removed = $res->headers->remove_header($header_type);
211             $r->log->debug("$$ translated $removed $header_type headers")
212             if VERBOSE_DEBUG;
213             }
214              
215             return 1;
216             }
217              
218             sub set_twohundred_response_headers {
219             my ( $class, $r, $res, $response_content_ref ) = @_;
220              
221              
222             $r->log->debug("setting response headers " . Dumper($res->headers)) if DEBUG;
223              
224             # This loops over the response headers and adds them to headers_out.
225             # Override any headers with our own here
226             my %headers;
227             $r->headers_out->clear();
228              
229             $class->translate_cookie_and_auth_headers( $r, $res );
230              
231             # Create a hash with the HTTP::Response HTTP::Headers attributes
232             $res->scan( sub { $headers{ $_[0] } = $_[1]; } );
233             $r->log->debug(
234             sprintf( "$$ not cookie/auth headers: %s", Dumper( \%headers ) ) )
235             if VERBOSE_DEBUG;
236              
237             ## Set the response content type from the request, preserving charset
238             $r->content_type( $headers{'Content-Type'} );
239             delete $headers{'Content-Type'};
240              
241             # need to encode content if utf-8
242             my $charset = $class->response_charset($r, $res);
243             $r->log->debug("$$ charset is $charset") if DEBUG;
244             if (($charset ne 'ISO-8859-1') && ($r->content_type !~ m/image|video/)) {
245             $$response_content_ref = Encode::encode($charset,
246             $$response_content_ref);
247             }
248              
249             #############################
250             ## Content languages
251             if ( defined $headers{'content-language'} ) {
252             $r->content_languages( [ $res->header('content-language') ] );
253             $r->log->debug(
254             "$$ content languages set to " . $res->header('content_language') )
255             if DEBUG;
256             delete $headers{'Content-Language'};
257             }
258              
259             ##################
260             # content_encoding
261             # do not mess with this next section unless you like pain
262             my $encoding;
263             if (($r->content_type !~ m/image|video/) &&
264             ((length($$response_content_ref) != 0) or
265             ($headers{'Content-Length'} != 0)) &&
266             ( $r->pnotes('client_supports_compression') )) {
267              
268             $r->log->debug( "$$ client supports compression: "
269             . $r->pnotes('client_supports_compression') )
270             if DEBUG;
271              
272             my @h =
273             map { $_->[0] }
274             HTTP::Headers::Util::split_header_words(
275             $r->pnotes('client_supports_compression') );
276             $r->log->debug( "$$ header words are " . join( ',', @h ) )
277             if VERBOSE_DEBUG;
278              
279             # use the first acceptable compression, ordered by
280             if ( grep { $_ eq 'x-bzip2' } @h ) {
281              
282             $response_content_ref =
283             Compress::Bzip2::compress($$response_content_ref);
284             $encoding = 'x-bzip2';
285              
286             }
287             elsif (( grep { $_ eq 'gzip' } @h )
288             || ( grep { $_ eq 'x-gzip' } @h ) )
289             { # some parts lifted from HTTP::Message
290              
291             # need a copy for memgzip, see HTTP::Message notes
292             my $gzipped =
293             eval { Compress::Zlib::memGzip($response_content_ref); };
294              
295             my $err = $@;
296             if ($err) {
297             $r->log->error("compression error: $err");
298             } else {
299              
300             $r->log->debug("$$ compressed response from " . length($$response_content_ref) . " to " . length($gzipped)) if DEBUG;
301             $$response_content_ref = $gzipped;
302             $encoding = 'gzip';
303             }
304              
305             }
306             elsif ( grep { $_ eq 'deflate' } @h ) {
307              
308             my $copy = $$response_content_ref;
309             $$response_content_ref = Compress::Zlib::compress($copy);
310             $encoding = 'deflate';
311              
312             }
313             else {
314             $r->log->error( "$$ unknown content-encoding encountered: "
315             . join( ',', @h ) );
316             }
317             }
318              
319             if ($encoding) {
320             $r->log->debug("$$ setting content encoding to $encoding") if DEBUG;
321             $r->content_encoding($encoding);
322             delete $headers{'Transfer-Encoding'}; # don't want to be chunked here
323             }
324             delete $headers{'Content-Encoding'};
325              
326             ###########################
327             # set the content length to the uncompressed content length
328             $r->set_content_length( length($$response_content_ref) );
329             delete $headers{'Content-Length'};
330              
331             ##########################################
332             # this is for any additional headers, usually site specific
333             $class->translate_remaining_headers( $r, \%headers );
334              
335             ###############################
336             # possible through a nasty hack, set the server version
337             $r->server->add_version_component( $headers{Server} || 'sl' );
338              
339             ###############################
340             # maybe someday but not today, do not cache this response
341             $r->no_cache(1);
342              
343             return 1;
344             }
345              
346             # figure out what charset a response was made in, code adapted from
347             # HTTP::Message::decoded_content
348             sub response_charset {
349             my ( $class, $r, $response ) = @_;
350              
351             # pull apart Content-Type header and extract charset
352             my $charset;
353             my @ct = HTTP::Headers::Util::split_header_words(
354             $response->header("Content-Type") );
355             if (@ct) {
356             my ( undef, undef, %ct_param ) = @{ $ct[-1] };
357             $charset = $ct_param{charset};
358             }
359              
360             # if the charset wasn't in the http header look for meta-equiv
361             unless ($charset) {
362              
363             # default charset for HTTP::Message - if it couldn't guess it will
364             # have decoded as 8859-1, so we need to match that when
365             # re-encoding
366             return $charset || "ISO-8859-1";
367             }
368             }
369              
370             sub resolve {
371             my ( $class, $hostname ) = @_;
372              
373             # run the dns query
374             my $query = $Resolver->query($hostname);
375             unless ($query) {
376              
377             die "dns resolution failed: " . $Resolver->errorstring;
378              
379             }
380             else {
381              
382             foreach my $rr ( $query->answer ) {
383              
384             next unless $rr->type eq "A";
385              
386             # return the A record
387             return $rr->address;
388             }
389             }
390              
391             die "could not resolve A record for $hostname";
392             }
393              
394             sub handler {
395             my ( $class, $r ) = @_;
396              
397             unless ($r->hostname) {
398             $r->log->error("$$ no hostname for req " . $r->as_string);
399             return Apache2::Const::HTTP_BAD_REQUEST;
400             }
401              
402             # Build the request headers
403             my $headers = $class->get_request_headers($r);
404              
405             my $url = $r->construct_url($r->unparsed_uri);
406             $r->pnotes('url' => $url);
407              
408             my %get = (
409             headers => $headers,
410             url => $url,
411             );
412              
413             my $ip;
414             if ($r->hostname !~ m/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) {
415              
416             $r->log->debug( "$$ resolving host " . $r->hostname ) if DEBUG;
417             $ip = eval { $class->resolve( $r->hostname ) };
418             if ($@) {
419              
420             # dns error
421             $r->log->error( "$$ unable to resolve host " . $r->hostname );
422             return &crazypage($r); # haha this page is kwazy!
423              
424             }
425             } else {
426             $ip = $r->hostname
427             }
428              
429             $get{host} = $ip;
430             $r->log->debug( "$$ making proxy request " . Dumper( \%get ) ) if DEBUG;
431              
432             # Make the request to the remote server
433             my $response = eval { $class->get( \%get ); };
434              
435             # socket timeout, give em the crazy page
436             if ($@) {
437             $r->log->error("$$ error fetching $url : $@") if DEBUG;
438             return &crazypage($r); # haha this page is kwazy!
439             }
440              
441             $r->log->debug("$$ request to $url complete") if DEBUG;
442              
443             # no response means html too big
444             # send it to perlbal to reproxy
445             unless ($response) {
446              
447             $r->log->error("$$ no response") if DEBUG;
448             return Apache2::Const::NOT_FOUND;
449             }
450              
451             $r->log->debug( "$$ Response headers from url $url proxy request code\n"
452             . "code: "
453             . $response->code . "\n"
454             . Dumper( $response->headers ) )
455             if VERBOSE_DEBUG;
456              
457             # Dispatch the response
458             my $sub = $Response{ $response->code };
459             unless ( defined $sub ) {
460             $r->log->error(
461             sprintf(
462             "No handler for response code %d, url %s, ua %s",
463             $response->code, $url, $r->pnotes('ua')
464             )
465             );
466             $sub = $Response{'404'};
467             }
468              
469             $r->log->debug(
470             sprintf(
471             "$$ Request returned %d response: %s",
472             $response->code, Dumper( $response->decoded_content ),
473             )
474             ) if VERBOSE_DEBUG;
475              
476             no strict 'refs';
477             return $class->$sub( $r, $response );
478             }
479              
480             # this page handles invalid urls, we run ads there
481              
482             sub crazypage {
483             my $r = shift;
484              
485             $r->content_type('text/html');
486             $r->print( "

Sorry the url "

487             . $r->construct_url($r->unparsed_uri)
488             . ' is not a valid hostname, please try again.' );
489             return Apache2::Const::OK;
490             }
491              
492             sub twoohfour {
493             my ( $class, $r, $res ) = @_;
494              
495             # status line 204 response
496             $r->status( $res->code );
497              
498             # translate the headers from the remote response to the proxy response
499             my $translated = $class->set_response_headers( $r, $res );
500              
501             # rflush() flushes the headers to the client
502             # thanks to gozer's mod_perl for speed presentation
503             $r->rflush();
504              
505             # no content sent for a 204
506             return Apache2::Const::OK;
507             }
508              
509             sub twoohsix {
510             my ( $class, $r, $res ) = @_;
511              
512             # set the status line here and I will beat you with a stick
513              
514             my $content_type = $res->content_type;
515             $r->content_type($content_type) if $content_type;
516              
517             # translate the headers from the remote response to the proxy response
518             my $translated = $class->set_response_headers( $r, $res );
519              
520             # rflush() flushes the headers to the client
521             # thanks to gozer's mod_perl for speed presentation
522             $r->rflush();
523              
524             $r->print( $res->content );
525              
526             # we send a 200 here so don't change this or mess with the status line!
527             return Apache2::Const::OK;
528             }
529              
530             sub bsod {
531             my ( $class, $r, $res ) = @_;
532              
533             # setup response
534             $r->status( $res->code );
535              
536             my $content_type = $res->content_type;
537             $r->content_type($content_type) if $content_type;
538              
539             # translate the headers from the remote response to the proxy response
540             my $translated = $class->set_response_headers( $r, $res );
541              
542             # rflush() flushes the headers to the client
543             # thanks to gozer's mod_perl for speed presentation
544             $r->rflush();
545              
546             $r->print( $res->content );
547              
548             return Apache2::Const::OK;
549             }
550              
551             sub threeohone {
552             my ( $class, $r, $res ) = @_;
553              
554             my $content_type = $res->content_type;
555             $r->content_type($content_type) if $content_type;
556              
557             # translate the headers from the remote response to the proxy response
558             my $translated = $class->set_response_headers( $r, $res );
559              
560             # do not change this line
561             return Apache2::Const::HTTP_MOVED_PERMANENTLY;
562             }
563              
564             # 302, 303, 307
565             sub redirect {
566             my ( $class, $r, $res ) = @_;
567              
568             # translate the headers from the remote response to the proxy response
569             my $translated = $class->set_response_headers( $r, $res );
570              
571             # do not change this line
572             return Apache2::Const::REDIRECT;
573             }
574              
575             sub threeohfour {
576             my ( $class, $r, $res ) = @_;
577              
578             # set the status line
579             $r->status( $res->code );
580              
581             # translate the headers from the remote response to the proxy response
582             my $translated = $class->set_response_headers( $r, $res );
583              
584             # do not change this line
585             return Apache2::Const::OK;
586             }
587              
588             # the big dog
589             sub twohundred {
590             my ( $class, $r, $response, $subref ) = @_;
591              
592             my $url = $r->pnotes('url');
593              
594             if ( $response->is_html ) {
595              
596             #$Cache->add_known_html( $url => $response->content_type );
597              
598             }
599             else {
600              
601             #$Cache->add_known_not_html( $url => $response->content_type );
602             }
603              
604             $r->log->debug( "$$ 200 for $url, length "
605             . length( $response->decoded_content )
606             . " bytes" )
607             if DEBUG;
608              
609             my $response_content_ref = \$response->decoded_content;
610              
611             # set the status line
612             $r->status_line( $response->status_line );
613             $r->log->debug( "$$ status line is " . $response->status_line )
614             if DEBUG;
615              
616             # set the response headers
617             my $set_ok =
618             $class->set_twohundred_response_headers( $r, $response,
619             $response_content_ref );
620              
621             if (VERBOSE_DEBUG) {
622             $r->log->debug( "$$ Response content: " . $$response_content_ref );
623             }
624              
625             # rflush() flushes the headers to the client
626             # thanks to gozer's mod_perl for speed presentation
627             $r->rflush();
628              
629             my $bytes_sent = $r->print($$response_content_ref);
630             $r->log->debug("$$ bytes sent: $bytes_sent") if DEBUG;
631              
632             return Apache2::Const::DONE;
633             }
634              
635             sub get {
636             my ( $class, $args_ref ) = @_;
637             unless ( $args_ref->{url} ) {
638             warn("$$ no url passed, returning");
639             return;
640             }
641             my $url = $args_ref->{url};
642             my $host = $args_ref->{host} || $args_ref->{headers}->{Host} || 'localhost';
643             my $port = $args_ref->{port} || 80;
644              
645             $url = URI->new($url) or die("Unable to parse url '$url'.");
646              
647             my $headers = $args_ref->{headers} || \%Headers;
648              
649             # convert headers to array-ref if a hash-ref is passed
650             $headers = [%$headers] if ( ref $headers eq 'HASH' );
651              
652             my $http = Net::HTTP->new(
653             Host => $url->host,
654             PeerAddr => $host,
655             PeerPort => $port
656             ) || die $@;
657              
658             # set keep alive
659             $http->keep_alive(1);
660              
661             # reinforce the point (Net::HTTP adds PeerPort to host during
662             # new())
663             $http->host( $url->host );
664              
665             # make the request
666             my $req = $url->path_query || "/";
667             my $ok = $http->write_request( GET => $req, @$headers );
668              
669             # get the result code, message and response headers
670             my ( $code, $mess, @headers_out ) = $http->read_response_headers;
671              
672             # read response body
673             my $body = "";
674             my $response = _build_response( $code, $mess, \@headers_out, \$body );
675              
676             # is this response too big?
677             my $content_length = $response->headers->header('Content-Length') || 0;
678             return if ( $content_length > MAX_CONTENT_LENGTH );
679              
680             while (1) {
681              
682             my $buf;
683             my $n = $http->read_entity_body( $buf, 10240 );
684             die "read failed: $!" unless defined $n;
685             last unless $n;
686             $body .= $buf;
687              
688             return if ( length($body) > MAX_CONTENT_LENGTH );
689             }
690              
691             $response->content_ref( \$body );
692             return $response;
693             }
694              
695             # turns data returned by Net::HTTP into a HTTP::Response object
696             sub _build_response {
697             my ( $code, $mess, $header_list, $body_ref ) = @_;
698              
699             my $header = HTTP::Headers->new(@$header_list);
700              
701             my $response = HTTP::Response->new( $code, $mess, $header, $$body_ref );
702             return $response;
703             }
704              
705             # adds a convenient extra method for inspection
706             {
707             no warnings;
708             *HTTP::Response::is_html = sub {
709             return 1 if ( shift->content_type =~ m/text\/html/ );
710             return;
711             };
712              
713             *HTTP::Response::should_compress = sub {
714             $" = '|';
715             my @compressibles
716             ; # = qw( text/html text/xml text/plain application/pdf );
717             return 1 if ( shift->content_type =~ m/(?:@compressibles)/ );
718             return;
719             };
720             }
721              
722             =head1 COPYRIGHT
723              
724             Copyright 2012 Red Hot Penguin Consulting LLC
725              
726             =head1 LICENSE
727              
728             This software is licensed under the same terms as Perl itself.
729              
730             =head1 SEE ALSO
731              
732             mod_perl
733              
734             =cut
735              
736              
737             1;
738