App-HTTP_Proxy_IMP

 view release on metacpan or  search on metacpan

lib/App/HTTP_Proxy_IMP/Request.pm  view on Meta::CPAN


############################################################################
# Request
############################################################################

use strict;
use warnings;

package App::HTTP_Proxy_IMP::Request;
use base 'Net::Inspect::Flow';
use fields (
    'conn',         # App::HTTP_Proxy_IMP::Connection object
    'meta',         # meta data
    'me_proxy',     # defined if I'm proxy, if true will be used for Via:
    'up_proxy',     # address of upstream proxy if any
    'acct',         # some accounting data
    'connected',    # false|CONN_HOST|CONN_INTERNAL

    'imp_analyzer', # App::HTTP_Proxy_IMP::IMP object
    'defer_rqhdr',  # deferred request header (wait until body length known)
    'defer_rqbody', # deferred request body (wait until header can be sent)

    'method',       # request method
    'rqhost',       # hostname from request
    'rq_version',   # version of request
    'rp_encoder',   # sub to encode response body (chunked)
    'keep_alive',   # do we use keep_alive in response
);

use App::HTTP_Proxy_IMP::Debug qw(debug $DEBUG debug_context);
use Scalar::Util 'weaken';
use Net::Inspect::Debug 'trace';
use Net::IMP qw(:DEFAULT :log);
use Net::IMP::HTTP; # constants
use Sys::Hostname 'hostname';

my $HOSTNAME = hostname();

# connected to host or do we fake the response internally
use constant CONN_HOST => 1;
use constant CONN_INTERNAL => 2;

sub DESTROY { 
    $DEBUG && debug("destroy request"); 
    #Devel::TrackObjects->show_tracked;
}
sub new_request {
    my ($factory,$meta,$conn) = @_;
    my $self = $factory->new;
    $DEBUG && $conn->xdebug("new request $self");

    $self->{meta} = $meta;
    weaken($self->{conn} = $conn);
    $self->{defer_rqhdr} = $self->{defer_rqbody} = '';

    $self->{acct} = { %$meta, Id => $self->id };
    if ( my $f = $conn->{imp_factory} ) {
	$self->{imp_analyzer} = $f->new_analyzer($self,$meta);
    }

    $self->{me_proxy} = $HOSTNAME;
    $self->{up_proxy} = $meta->{upstream};

    return $self;
}

sub xdebug {
    my $self = shift;
    my $ctx = debug_context( id => $self->id );
    goto &debug;
}

sub id {
    my $self = shift;
    $self->{conn} or return '';
    return $$.'.'.$self->{conn}{connid}.'.'.$self->{meta}{reqid}
}

sub fatal {
    my ($self,$reason) = @_;
    warn "[fatal] ".$self->id." $reason\n";
    if ( my $conn = $self->{conn} ) {
        my $relay =  $conn->{relay};
        $relay->account('fatal');
        $relay->close;
    }
}

lib/App/HTTP_Proxy_IMP/Request.pm  view on Meta::CPAN

	$url =~m{^(?:\[([\w\-.:]+)\]|([\w\-.]+))(?::(\d+))$} or
	    return $self->fatal("invalid host[:port] in connect: $url");
	$proto = 'https';
	$host = lc($1||$2);
	$port = $3 || $default_port{$proto};
	$path = '';
	$url = ( $host =~m{:} ? "[$host]":$host ) . ":$port";

    } else {
	if ( $url =~m{^(\w+)://(?:\[([\w\-.:]+)\]|([\w\-.]+))(?::(\d+))?(.+)?} ) {
	    # absolute url, valid for HTTP/1.1 or proxy requests
	    $proto = lc($1);
	    $host = lc($2||$3);
	    $port = $4;
	    $path = $5 // '/';

	} else {
	    # relativ url, needs Host header if we want to get target
	    # from request
	    $proto = 'http';
	    $path = $url;
	    if ( my $h = $head->{host} ) {
		$relay->error("Ignoring multiple host headers") if @$h>1;
		$h->[0] =~m{^(?:\[([\w\-.:]+)\]|([\w\-.]+))(?::(\d+))?$} or 
		    return $self->fatal("bad host line '$h->[0]'");
		$host = $1||$2;
		$port = $3;
	    } else {
		return $self->fatal("cannot determine target host");
	    }
	}

	$port //= $default_port{$proto};
	return $self->fatal("invalid port $port") 
	    if ! $port or $port > 2**16-1;

	$path !~m{^/} and return $self->fatal("invalid path $path ($url)");

	# set/replace host header with target from URL and normalize URL
	$host =~s{\.\.+}{.}g;
	my $hp = $host =~m{:} ? "[$host]":$host;
	$hp .= ":$port" if $default_port{$proto} != $port;
	$head->{host} = [ $hp ];
	$url = "$proto://$hp$path";
    }

    $self->{acct}{url} = $url;
    $self->{acct}{url} =~s{://}{s://} if $conn->{intunnel};
    $self->{acct}{method} = $met;
    $self->{acct}{reqid} = $self->{meta}{reqid};
    $self->{rqhost} = $host;

    if ( $met eq 'CONNECT' and ! $self->{up_proxy} ) {
	# just skip all the header manipulation and normalization, we don't
	# need your stinkin header!
	$hdr = '';
	goto SRVCON;
    }

    # do we want/support persistence?
    my %conn = map { lc($_) => 1 } grep { m{\b(close|keep-alive)\b}i } (
	@{ delete $head->{connection} || [] },
	defined($self->{me_proxy}) 
	    ? @{ delete $head->{'proxy-connection'} || [] } : ()
    );
    if ( keys %conn > 1 ) {
	# fall back to close
	$self->{keep_alive} = 0;
	$head->{connection} = [ 'close' ];
    } elsif ( $conn{close} ) {
	$self->{keep_alive} = 0;
	# default in 1.1 is keep-alive
	$head->{connection} = [ 'close' ] if $version eq '1.1';
    } elsif ( $conn{'keep-alive'} ) {
	$self->{keep_alive} = 1;
	# default in 1.0 is close
	$head->{connection} = [ 'keep-alive' ] if $version eq '1.0';
    } else {
	# use default of version
	$self->{keep_alive} = $version eq '1.1';
    }

    # if we are a proxy set a via tag
    if ( my $via = $self->{me_proxy} ) {
	push @{$head->{via}}, "$version $via";
    }

    # normalize header before forwarding it
    # sort keys, normalize case of keys etc
    $hdr = "$met ".( $self->{up_proxy} ? $url : $path )." HTTP/$version\r\n";
    for my $k ( sort keys %$head) {
	$hdr .= "\u$k: $_\r\n" for @{$head->{$k}};
    }
    $hdr .= "\r\n";

    SRVCON:

    if ( $xhdr->{internal_url} ) {
	# the IMP plugin rewrote the url to internal://smthg,
	# meaning, that the plugin will provide us with the real response
	$self->{acct}{internal} = 1;
	$self->{connected} = CONN_INTERNAL;
	$self->{keep_alive} = 0;

	# accept more body data
	_call_spooled_this($conn);
	$relay->mask(0,r=>1);

	# inject minimal response into Net::Inspect, which than can modify
	# it at will
	# IMP let us not change nothing (e.g. empty body) into something, so
	# we need to provide minimal content where content is expected
	$conn->in(1, 
	    $met eq 'HEAD' 
		? "HTTP/$version 200 Ok\r\n\r\n" 
		: "HTTP/$version 200 Ok\r\nContent-length: 1\r\n\r\n%",
	    1, # eof
	    0, # time
	);
	return;
    }

    if ( my $imp = $self->{imp_analyzer} ) {
	if ( defined( my $len = $xhdr->{content_length} )) {
	    # length is given, fix header
	    my $debug = $DEBUG && debug_context( id => $self->id);
	    $imp->fixup_request_header(\$hdr, content => $len);
	} else {
	    $self->{defer_rqhdr} = $hdr;
	}
    }

    if ( $conn->{intunnel} ) {
	_fwd_request_after_connect($self,$hdr);
    } else {
	$relay->connect( 1,
	    @{ $self->{up_proxy} || [ $host,$port ] },
	    sub { _fwd_request_after_connect($self,$hdr) }
	);
    }
}

sub _fwd_request_after_connect {
    my ($self,$hdr) = @_;
    $self->{connected} = CONN_HOST;

    if ($hdr eq '') {
	# no header, e.g we have a CONNECT to a non-proxy
	# put a fake response into Net::Inspect to keep state
	$self->{conn}->in(1,"HTTP/1.0 200 Connection established\r\n\r\n");
	return _call_spooled_this($self->{conn});
    }

    if ( my $imp = $self->{imp_analyzer} ) {
	my $debug = $DEBUG && debug_context( id => $self->id);
	if ( $imp->fixup_request_header(\$hdr, defered => 0) ) {
	    $self->{defer_rqhdr} = '';
	} else {
	    # keep deferring sending header, length not known
	    _call_spooled_this($self->{conn}); # any body already ?
	    return;
	}
    }

lib/App/HTTP_Proxy_IMP/Request.pm  view on Meta::CPAN

    if ( my $imp = $self->{imp_analyzer} ) {
	my $debug = $DEBUG && debug_context( id => $self->id);
	$imp->response_header($hdr,$xhdr,
	    \&_response_header_after_imp,$self);
    } else {
	_response_header_after_imp($self,$hdr,$xhdr);
    }
}


############################################################################
# process response header, maybe it got manipulated by IMP
############################################################################
sub _response_header_after_imp {
    my ($self,$hdr,$xhdr) = @_;
    my $relay = $self->{conn}{relay} or return;

    my $version = $xhdr->{version};
    my $code    = $self->{acct}{code} = $xhdr->{code};
    my $clen    = $xhdr->{content_length};

    $DEBUG && $self->xdebug("input header: $hdr");
    my $status_line = "HTTP/$version $code $xhdr->{reason}\r\n"; # normalized

    my $head = $xhdr->{fields};
    #warn Dumper($head); use Data::Dumper;
    $xhdr->{junk} and $relay->error(
	"Bad response header lines: $xhdr->{junk}");

    # check if the response is chunked and strip any transfer-encoding header
    # it will be added, when we know, how we talk to the client
    if ( $xhdr->{chunked} ) {
	delete $head->{'transfer-encoding'};
	# if chunked is given content-length should be ignored
	# better strip, so that client will parse it correctly
	delete $head->{'content-length'};
    }

    # if we don't know the content_length we try chunked, but only if client
    # and server used version 1.1. Otherwise we will close connection
    # at request end.
    # if only client supports chunking we better don't change response header
    # to 1.1, because in the 1.0 response might contain 1.0 specific headers 
    # (Pragma...) which we don't know how to translate
    if ( defined $clen ) {
	$DEBUG && $self->xdebug("have content-length $clen");
    } elsif ( $self->{method} eq 'CONNECT' ) {
	$DEBUG && $self->xdebug("have connect request");
    } else {
	if  ( $version eq '1.1' and $self->{rq_version} eq '1.1' ) {
	    $head->{'transfer-encoding'} = [ 'chunked' ];
	    delete $head->{'content-length'};
	    $DEBUG && $self->xdebug("no clen known - use chunked encoding");
	    $self->{rp_encoder} = sub { 
		my $data = shift;
		sprintf("%x\r\n%s\r\n", length($data),$data) 
	    };
	} else {
	    # disable persistance, we will end with EOF
	    $DEBUG && $self->xdebug("no clen known - use eof to end response");
	    $self->{keep_alive} = 0;
	}
    }

    # set connection header if behavior is not default
    if ( $version eq '1.1' and ! $self->{keep_alive} ) {
	$head->{connection} = [ 'close' ];
    } elsif ( $version eq '1.0' and $self->{keep_alive} ) {
	$head->{connection} = [ 'keep-alive' ];
    } else {
	delete $head->{connection}
    }


    # create normalized header
    $hdr = $status_line;
    for my $k ( sort keys %$head) {
	$hdr .= "\u$k: $_\r\n" for @{$head->{$k}};
    }
    $hdr .= "\r\n";

    # forward header
    $DEBUG && $self->xdebug("output hdr: $hdr");
    $relay->forward(1,0,$hdr);

    if ( $self->{method} eq 'CONNECT' ) {
	# upgrade server side and client side with SSL, but intercept traffic.
	# need to be called outside the current event handler, because $hdr
	# will only be removed from rbuf after the current handler is done
	App::HTTP_Proxy_IMP->once( sub {
	    $relay->sslify(1,0,$self->{rqhost});
	});
    }
}


############################################################################
# handle response body data
# will be forwarded to _response_body_after_imp with data or '' (eof)
# maybe it will forwarded before to IMP analyzer
############################################################################
sub in_response_body {
    my ($self,$data,$eof) = @_;

    $self->xdebug("len=".length($data)." eof=$eof");
    if ( my $imp = $self->{imp_analyzer} ) {
	my $debug = $DEBUG && debug_context( id => $self->id);
	$data ne '' && $imp->response_body($data,
	    \&_response_body_after_imp,$self);
	$eof && $imp->response_body('',
	    \&_response_body_after_imp,$self); 
    } else {
	_response_body_after_imp($self,$data,$eof);
    }
}

sub _response_body_after_imp {
    my ($self,$data,$eof) = @_;
    $self->xdebug("len=".length($data)." eof=$eof");
    my $relay = $self->{conn}{relay} or return;

    # chunking, compression ...
    if ( my $encode = $self->{rp_encoder} ) {
	$data = $encode->($data) if $data ne '';
	$data.= $encode->('') if $eof;
    }
    if ( $data ne '' ) {
	$DEBUG && $self->xdebug("send ".length($data)." bytes to c");
	$relay->forward(1,0,$data);
    } 

    if ($eof) {
        $relay->account('request');
	if ( ! $self->{keep_alive} ) {
	    # close connection
	    $DEBUG && $self->xdebug("end of request: close");
	    return $relay->close;
	}

	# keep connection open
	# and continue with next request if we have one
	$DEBUG && $self->xdebug("end of request: keep-alive");
	_call_spooled_next( $self->{conn} );
    }
}

############################################################################
# Websockets, TLS upgrades etc
# if not IMP the forwarding will be done inside this function, otherwise it
# will be done in _in_data_imp, which gets called by IMP callback
############################################################################
sub in_data {
    my ($self,$dir,$data,$eof) = @_;

    if ( my $imp = $self->{imp_analyzer} ) {
	my $debug = $DEBUG && debug_context( id => $self->id);
	$data ne '' and $imp->data($dir,$data,\&_in_data_imp,$self);
	$eof and $imp->data($dir,'',\&_in_data_imp,$self);
    } else {
	my $relay = $self->{conn}{relay} or return;
	$DEBUG && $self->xdebug("got %d bytes from %d, eof=%d",length($data),$dir,$eof);
	if ( $data ne '' ) {
	    if ( $dir == 1 ) {
		$relay->forward(1,0,$data)
	    } else {
		$relay->forward(0,1,$data) if $self->{connected} == CONN_HOST;
	    }
	}
	$relay->account('upgrade') if $eof;
    }
}
sub _in_data_imp {
    my ($self,$dir,$data,$eof) = @_;
    my $relay = $self->{conn}{relay} or return;
    $DEBUG && $self->xdebug("imp got %d bytes from %d, eof=%d",length($data),$dir,$eof);
    if ( $data ne '' ) {
	if ( $dir == 1 ) {
	    $relay->forward(1,0,$data)
	} else {
	    $relay->forward(0,1,$data) if $self->{connected} == CONN_HOST;
	}
    }

    $relay->account('upgrade') if $eof;
}

############################################################################
# chunks and junk gets ignored
# - we decide ourself, when we will forward data chunked and do the
#   chunking ourself
# - junk data will not be forwarded
############################################################################

sub in_chunk_header {}
sub in_chunk_trailer {}
sub in_junk {}


1;



( run in 0.855 second using v1.01-cache-2.11-cpan-df04353d9ac )