This proxy tries to be HTTP/1.0 vs HTTP/1.1 compliant with respect to keep-alives and supports transparent proxying using the "Host:" header. There is plenty of documentation on transparent proxies, but for now review this one-liner:
iptables -t nat -I OUTPUT -p tcp --dport 80 -j REDIRECT --to-ports 8088 -m owner ! --uid-owner the_proxy_user
Important Note: This proxy does not support HTTP/1.1 pipelining. Requests are sent to the PoCoCl::HTTP user agent as they come in but handle_http_response does not differentiate between receipt sources. If you decide to fix this, remember that pipelined requests must be sent in-order. I will update this when I get a moment to upload a clean cookbook example with pipeline support; if you beat me to it, please update this! -ewheeler
#!/usr/bin/perl use warnings; use strict; $SIG{PIPE} = 'IGNORE'; # otherwise, SIGPIPE terminates the proxy. use POE; use POE::Component::Server::TCP; use POE::Component::Client::HTTP; use POE::Filter::HTTPD; use HTTP::Response; sub DUMP_REQUESTS () { 1 } sub DUMP_RESPONSES () { 1 } sub LISTEN_PORT () { 8088 } ### Spawn a web client to fetch requests through. POE::Component::Client::HTTP->spawn( Alias => 'ua', Streaming => 32768, ConnectionManager => POE::Component::Client::Keepalive->new( keep_alive => 10, # seconds to keep connections alive max_open => 100, # max concurrent connections - total max_per_host => 20, # max concurrent connections - per host timeout => 30, # max time (seconds) to establish a new connection ), ); ### Spawn a web server. # The ClientInput function is called to deal with client input. # ClientInput's callback function will receive entire HTTP requests # because this server uses POE::Filter::HTTPD to parse its input. # # InlineStates let us attach our own events and handlers to a TCP # server. Here we attach a handler for the got_response event, which # will be sent to us by Client::HTTP when it has fetched something. POE::Component::Server::TCP->new( Alias => "web_server", Address => '127.0.0.1', Port => LISTEN_PORT, ClientFilter => 'POE::Filter::HTTPD', ClientInput => \&handle_http_request, ClientDisconnected => sub { Debug("client disconnected") }, InlineStates => {got_response => \&handle_http_response,}, ); ### Run the proxy until it is done, then exit. POE::Kernel->run(); exit 0; ### Handle HTTP requests from the client. Pass them to the HTTP ### client component for further processing. Optionally dump the ### request as text to STDOUT. sub handle_http_request { my ($kernel, $heap, $request) = @_[KERNEL, HEAP, ARG0]; # If the request is really a HTTP::Response, then it indicates a # problem parsing the client's request. Send the response back so # the client knows what's happened. if ($request->isa("HTTP::Response")) { $heap->{client}->put($request); $kernel->yield("shutdown"); return; } # Client::HTTP doesn't support keep-alives yet. #$request->header("Connection", "close"); #$request->header("Proxy-Connection", "close"); #display_thing(scalar(localtime)."[BEFORE Req Id ". $_[SESSION]->ID."]: ", $request->as_string()) if DUMP_REQUESTS; # default keep-alive for HTTP/1.0 vs HTTP/1.1 $heap->{keepalive} = 1 if (!$request->header('Connection') && $request->protocol eq 'HTTP/1.1'); $heap->{keepalive} = 0 if (!$request->header('Connection') && $request->protocol eq 'HTTP/1.0'); $heap->{keepalive} = 1 if (lc($request->header('Connection')) eq 'keep-alive'); $heap->{keepalive} = 1 if (lc($request->header('Proxy-Connection')) eq 'keep-alive'); # Remove RFC2616 hop-to-hop headers $request->remove_header($_) foreach ( qw/Connection Keep-Alive Proxy-Connection Proxy-Authenticate Proxy-Authorization TE Trailers Upgrade/ #Transfer-Encoding # Technically this is included, but pass it through ); if ($request->uri !~ /^http:/) { $request->uri("http://" . $request->header("Host") . $request->uri); $heap->{transparent_proxy} = 1; } display_thing(scalar(localtime) . "[AFTER Req Id " . $_[SESSION]->ID . "]: ", $request->as_string()) if DUMP_REQUESTS; $kernel->post( "ua" => "request", "got_response", $request ); } ## end sub handle_http_request ### Handle HTTP responses from the POE::Component::Client::HTTP we've ### spawned at the beginning of the program. Send each response back ### to the client that requested it. Optionally display the response ### as text. sub handle_http_response { my ($kernel, $heap) = @_[KERNEL, HEAP]; my $http_request = $_[ARG0]->[0]; my $http_response = $_[ARG1]->[0]; my $chunk = $_[ARG1]->[1]; # data chunk streaming in. $heap->{bytes_sent} += length $chunk if (defined($chunk)); # Skip dead clients return if (!defined $heap->{client}); if (!$heap->{sent_header}) { # We cannot keep-alive for systems that fail to return content-length if ($heap->{keepalive} && !$http_response->header('Content-Length')) { $http_response->header("Connection", 'close'); $heap->{keepalive} = 0; } $heap->{client}->put($http_response); # Send the header $heap->{client}->set_output_filter(POE::Filter::Stream->new); # raw output $heap->{client}->put($chunk) if (defined $chunk); # send the data chunk $heap->{sent_header}++; display_thing(scalar(localtime) . "[Req Id " . $_[SESSION]->ID . "]: ", $http_response->as_string()) if DUMP_RESPONSES; } else { if (defined($chunk)) { $heap->{client}->put($chunk); } else # end of stream { if ($heap->{keepalive}) { Debug( "End of stream, resetting output state. bytes_sent=$heap->{bytes_sent} [keepalive=1]" ); $heap->{client}->set_output_filter(POE::Filter::HTTPD->new); $heap->{sent_header} = 0; $heap->{bytes_sent} = 0; } else { # No Keep-Alive, disconnect. Debug( "End of stream, disconnecting client. bytes_sent=$heap->{bytes_sent} [keepalive=1]" ); $kernel->yield("shutdown"); } } } ## end else [ if (!$heap->{sent_header...})] } ## end sub handle_http_response ### Display requests and responses with brackets around them so they ### stand apart. sub display_thing { my $note = shift; my $thing = shift; $thing =~ s/^/| /mg; print ",", '-' x (78 - length($note)), "" . scalar($note) . "\n"; print $thing; print "`", '-' x 78, "\n"; } sub Debug { print STDERR join(", ", @_) . "\n"; }