File Coverage

blib/lib/App/HTTP_Proxy_IMP.pm
Criterion Covered Total %
statement 106 278 38.1
branch 24 144 16.6
condition 7 56 12.5
subroutine 19 42 45.2
pod 4 7 57.1
total 160 527 30.3


line stmt bran cond sub pod time code
1              
2 1     1   622 use strict;
  1         2  
  1         30  
3 1     1   4 use warnings;
  1         2  
  1         59  
4              
5             package App::HTTP_Proxy_IMP;
6             our $VERSION = '0.958';
7             use fields (
8 1         4 'addr', # \@addr to listen on
9             'impns', # \@namespace for IMP plugins
10             'filter', # \@plugins to load
11             'logrx', # regexp for filtering log messages
12             'pcapdir', # dir to store pcap files of requests
13             'mitm_ca', # file containing cert and key of proxy cert
14             'capath', # path to CA to verify server cert
15             'no_check_certificate', # don't check server certificates
16             'childs', # use this number of childs ( 0 = don't fork)
17             'max_connect_per_child', # max number of connections before child exits
18 1     1   665 );
  1         1568  
19              
20 1     1   645 use App::HTTP_Proxy_IMP::IMP;
  1         3  
  1         37  
21 1     1   504 use App::HTTP_Proxy_IMP::Conn;
  1         4  
  1         57  
22 1     1   532 use App::HTTP_Proxy_IMP::Request;
  1         3  
  1         85  
23 1     1   468 use App::HTTP_Proxy_IMP::Relay;
  1         4  
  1         46  
24 1     1   7 use AnyEvent;
  1         3  
  1         23  
25 1     1   738 use Getopt::Long qw(:config posix_default bundling);
  1         10404  
  1         4  
26 1     1   198 use App::HTTP_Proxy_IMP::Debug qw(debug $DEBUG $DEBUG_RX);
  1         2  
  1         101  
27 1     1   7 use Net::Inspect::Debug qw(%TRACE);
  1         2  
  1         8  
28 1     1   1860 use IO::Socket::SSL::Intercept;
  1         5765  
  1         35  
29 1     1   7 use IO::Socket::SSL::Utils;
  1         2  
  1         79  
30 1     1   7 use Carp 'croak';
  1         1  
  1         39  
31 1     1   6 use POSIX '_exit';
  1         2  
  1         7  
32              
33              
34             # try IPv6 using IO::Socket::IP or IO::Socket::INET6
35             # fallback to IPv4 only
36             my $sockclass;
37             BEGIN {
38 1     1   126 for(qw( IO::Socket::IP IO::Socket::INET6 IO::Socket::INET )) {
39 1 50       52 if ( eval "require $_" ) {
40 1         2 $sockclass = $_;
41 1         3 last;
42             }
43             }
44 1 50       3528 $sockclass or die "cannot find usable socket class";
45             }
46              
47              
48             sub new {
49 2     2 1 6 my ($class,@args) = @_;
50 2         7 my $self = fields::new($class);
51 2         212 $self->{impns} = [qw(App::HTTP_Proxy_IMP::IMP Net::IMP::HTTP Net::IMP)];
52 2 50 33     15 %$self = ( %$self, %{ shift(@args) }) if @args && ref($args[0]);
  2         11  
53 2 50       7 $self->getoptions(@args) if @args;
54 2         7 return $self;
55             }
56              
57             sub start {
58 2     2 1 2000 my $self = shift;
59 2 50 50     10 $self = $self->new(@_) or return if ! ref($self); # package->start
60              
61 2         4 my $pcapdir = $self->{pcapdir};
62 2 50       5 if ( $pcapdir ) {
63 0 0 0     0 croak("pcap directory not writeable") unless -d $pcapdir && -w _;
64 0 0       0 eval { require Net::PcapWriter } or croak(
  0         0  
65             "cannot load Net::PcapWriter, which is needed with --pcapdir option");
66             }
67              
68 2         3 my $mitm;
69 2 50       6 if ( my $f = $self->{mitm_ca} ) {
70 0         0 my $serial = 1;
71 0         0 my $cache = {};
72 0         0 my $cachedir = "$f.cache";
73 0 0 0     0 if ( -d $cachedir || mkdir($cachedir,0700)) {
74 0         0 for my $f (glob("$cachedir/*.pem")) {
75 0 0 0     0 -f $f && -r _ && -s _ or next;
      0        
76 0         0 my $time = (stat(_))[9];
77 0 0       0 my $key = PEM_file2key($f) or next;
78 0 0       0 my $cert = PEM_file2cert($f) or next;
79 0         0 my $sn = CERT_asHash($cert)->{serial};
80 0 0       0 $serial = $sn+1 if $sn>=$serial;
81 0         0 my ($id) = $f=~m{/([^/]+)\.pem$};
82 0         0 $cache->{$id} = {
83             cert => $cert,
84             key => $key,
85             atime => $time,
86             };
87 0         0 debug("loaded certificate id=$id from cache");
88             }
89              
90 0         0 my $cache_hash = $cache;
91             $cache = sub {
92 0     0   0 my $id = shift;
93 0         0 my $e;
94 0 0       0 if ( ! @_ ){ # get
95 0 0       0 $e = $cache_hash->{$id} or return;
96             } else {
97 0         0 my ($cert,$key) = @_;
98 0         0 $e = $cache_hash->{$id} = {
99             cert => $cert,
100             key => $key,
101             };
102             }
103 0         0 my $f = "$cachedir/$id.pem";
104 0 0 0     0 if ( @_ || ! -f $f and open( my $fh,">",$f )) {
      0        
105 0         0 debug("save mitm certificate and key to $cachedir/$id.pem");
106             print $fh PEM_cert2string($e->{cert}),
107             PEM_key2string($e->{key})
108 0         0 } else {
109 0         0 utime(undef,undef,$f);
110             }
111 0         0 $e->{atime} = time();
112 0         0 return ($e->{cert},$e->{key});
113 0         0 };
114             }
115              
116 0         0 $mitm = IO::Socket::SSL::Intercept->new(
117             proxy_cert_file => $f,
118             proxy_key_file => $f,
119             cache => $cache,
120             serial => $serial,
121             );
122             }
123              
124 2         4 my $imp_factory;
125 2         3 my $filter = $self->{filter};
126 2 50 33     9 if ($filter && @$filter ) {
127 2         4 my $ns = $self->{impns};
128 2         3 my @mod;
129 2         8 my $ev = App::HTTP_Proxy_IMP::EventLoop->new;
130 2         4 for my $f (@$filter) {
131 2 50       5 if ( ref($f) ) {
132             # already factory object
133 0         0 push @mod,$f;
134 0         0 next;
135             }
136              
137 2         3 my $f = $f; # copy
138 2   66     15 my $args = $f =~s{=(.*)}{} && $1;
139              
140 2         4 my $found;
141 2         4 for my $prefix ('', map { "${_}::" } @$ns) {
  2         7  
142 4         14 my $mod = $prefix.$f;
143 4 100       251 if ( eval "require $mod" ) {
144 2         5 $found = $mod;
145 2         5 last;
146             }
147             }
148 2 50       8 croak("IMP module $f could not be loaded: $@") if ! $found;
149 2 100       19 my %args = $args ? $found->str2cfg($args) :();
150 2         47 my @err = $found->validate_cfg(%args);
151 2 50       13 die "bad config for $found: @err" if @err;
152 2         10 push @mod, $found->new_factory(%args, eventlib => $ev )
153             }
154              
155 2   33     187 my $logsub = $self->{logrx} && do {
156             my $rx = $self->{logrx};
157             sub {
158 0     0   0 my ($level,$msg,$dir,$off,$len) = @_;
159 0         0 $level =~ $rx or return;
160 0         0 print STDERR "[$level]($dir:$off,$len) $msg\n";
161             };
162             };
163 2         15 $imp_factory = App::HTTP_Proxy_IMP::IMP->new_factory(
164             mod => \@mod,
165             logsub => $logsub,
166             );
167             }
168              
169 2 50       7 if ( $self->{childs} ) {
170 0         0 $self->{childs} = [ map { undef } (1..$self->{childs}) ];
  0         0  
171             }
172              
173 2         4 my $capath;
174 2 50       5 if ( ! $mitm ) {
    0          
    0          
175             # no interception = no certificate checking
176             } elsif ( $self->{no_check_certificate} ) {
177             # no certificate checking
178             } elsif ( $capath = $self->{capath} ) {
179             # use this capath
180             } else {
181             # try to guess capath
182 0 0 0     0 if ( eval { require Mozilla::CA } ) {
  0 0 0     0  
    0          
183 0         0 $capath = Mozilla::CA::SSL_ca_file();
184             } elsif ( glob("/etc/ssl/certs/*.pem") ) {
185 0         0 $capath = "/etc/ssl/certs";
186             } elsif ( -f "/etc/ssl/certs.pem" && -r _ && -s _ ) {
187 0         0 $capath = "/etc/ssl/certs.pem";
188             } else {
189 0         0 croak "cannot determine CA path, needed for SSL interception"
190             }
191             }
192              
193             # create connection fabric, attach request handling
194 2         20 my $req = App::HTTP_Proxy_IMP::Request->new;
195 2         276 my $conn = App::HTTP_Proxy_IMP::Conn->new($req,
196             pcapdir => $pcapdir,
197             mitm => $mitm,
198             capath => $capath,
199             imp_factory => $imp_factory
200             );
201              
202             # create listeners
203 2         4 my @listen;
204              
205             $self->{addr} = [ $self->{addr} ]
206 2 50 33     13 if $self->{addr} && ref($self->{addr}) ne 'ARRAY';
207 2         5 for my $spec (@{$self->{addr}}) {
  2         5  
208 2 50       12 my ($addr,$upstream) =
    50          
209             ref($spec) eq 'ARRAY' ? @$spec:
210             ref($spec) ? ( $spec,undef ):
211             split('=',$spec,2);
212 2         3 my $srv;
213 2 50       4 if ( ref($addr)) {
214             # listing socket already
215 0         0 $srv = $addr;
216 0         0 (my $port,$addr) = AnyEvent::Socket::unpack_sockaddr( getsockname($srv));
217 0         0 $addr = AnyEvent::Socket::format_address($addr);
218 0 0       0 $addr = $addr =~m{:} ? "[$addr]:$port" : "$addr:$port";
219             } else {
220 2 50       32 $srv = $sockclass->new(
221             LocalAddr => $addr,
222             Listen => 10,
223             ReuseAddr => 1,
224             ) or croak("cannot listen to $addr: $!");
225             }
226 2         1538 $spec = [ $addr,$upstream,$srv ];
227             push @listen, AnyEvent->io(
228             fh => $srv,
229             poll => 'r',
230             cb => sub {
231 0 0   0   0 my $cl = $srv->accept or return;
232 0         0 debug("new request from %s:%s on %s",$cl->peerhost,$cl->peerport,$addr);
233 0 0 0     0 if ( $self->{max_connect_per_child}>0
234             and 0 == --$self->{max_connect_per_child} ) {
235             # last connection for child
236             # fork-away and handle outstanding connections, parent will
237             # in the meantime fork a replacement child
238 0 0       0 defined( my $pid = fork()) or die "failed to fork: $!";
239 0 0       0 if ( $pid ) {
240 0 0       0 $DEBUG && debug("forked away child $$ as $pid");
241 0         0 _exit(0);
242             } else {
243 0         0 $0 =~s{\Q[worker]}{[death-trip]};
244 0         0 undef @listen; # only handle outstanding connections
245 0         0 App::HTTP_Proxy_IMP::Relay->exit_if_no_relays(1);
246 0 0       0 $DEBUG && debug(
247             "forked away child $$ to handle last connections");
248             }
249             }
250 0         0 App::HTTP_Proxy_IMP::Relay->new($cl,$upstream,$conn);
251             }
252 2         24 );
253 2         36 debug("listening on $addr");
254             }
255              
256 2 50       14 $self->{max_connect_per_child} = 0 if ! $self->{childs};
257              
258 2 50       9 return 1 if defined wantarray;
259 0         0 $self->loop;
260             }
261              
262             sub DESTROY {
263 0     0   0 my $self = shift;
264 0 0       0 ref(my $ch = delete $self->{childs}) or return;
265 0         0 kill 9, grep { $_ } @$ch;
  0         0  
266             }
267              
268             {
269             my $loop;
270             my @once;
271             sub once {
272 0     0 1 0 shift;
273 0         0 push @once, shift;
274 0 0       0 $loop->send if $loop;
275             }
276             sub loop {
277 0     0 1 0 my $self = shift;
278 0 0       0 return $self->parent_loop if $self->{childs};
279              
280             my $usr2 = AnyEvent->signal( signal => 'USR2', cb => sub {
281 0     0   0 my $was_debug = $DEBUG;
282 0         0 $DEBUG = 1;
283 0 0       0 debug("($$) ".( $was_debug ? 'disable':'enable' ) ." debugging");
284 0         0 $DEBUG = ! $was_debug;
285 0         0 });
286              
287             # on SIGUSR1 dump state of all relays
288             my $usr1 = AnyEvent->signal( signal => 'USR1', cb => sub {
289             # temporaly enable debugging, even if off
290 0     0   0 my $msg = "-------- active relays ------------------\n";
291 0         0 my @relays = App::HTTP_Proxy_IMP::Relay->relays;
292 0 0       0 if ( ! @relays ) {
293 0         0 $msg .= " * NO RELAYS\n"
294             } else {
295 0         0 $msg .= $_->dump_state."\n" for(@relays);
296             }
297 0         0 $msg .= "-------- active relays ------------------\n";
298 0         0 my $od = $DEBUG;
299 0         0 $DEBUG = 1;
300 0         0 debug($msg);
301 0         0 $DEBUG = $od;
302 0         0 });
303              
304 0         0 while (1) {
305 0         0 shift(@once)->() while (@once);
306 0         0 $loop = AnyEvent->condvar;
307 0         0 $loop->recv;
308             }
309             }
310              
311             # parent mainloop: keep children running
312             sub parent_loop {
313 0     0 0 0 my $self = shift;
314 0 0       0 $DEBUG && debug("parent $$");
315              
316             $SIG{USR1} = sub {
317 0 0   0   0 my @pid = grep { $_ } @{$self->{childs}} or return;
  0         0  
  0         0  
318 0         0 debug("propagating USR1 to @pid");
319 0         0 kill 'USR1', @pid;
320 0         0 };
321              
322             $SIG{USR2} = sub {
323 0 0   0   0 my @pid = grep { $_ } @{$self->{childs}} or return;
  0         0  
  0         0  
324 0         0 my $was_debug = $DEBUG;
325 0         0 $DEBUG = 1;
326 0         0 debug("propagating USR2 to @pid");
327 0         0 kill 'USR2', @pid;
328 0         0 $DEBUG = ! $was_debug;
329 0         0 };
330              
331 0         0 while ( my $ch = $self->{childs} ) {
332             # check if anything needs to be started
333 0         0 for(@$ch) {
334 0 0       0 $_ and next; # child is up
335             # start new child
336 0 0       0 defined( my $pid = fork()) or do {
337 0         0 warn "fork failed: $!";
338 0         0 sleep(1);
339 0         0 next;
340             };
341 0 0       0 if ( $pid == 0 ) {
342             # child
343 0         0 $0 = "[worker] $0";
344 0         0 $self->{childs} = undef;
345 0         0 return $self->loop;
346             }
347 0         0 $_ = $pid;
348 0 0       0 $DEBUG && debug("(re)starting child, pid=$pid");
349             }
350             # wait for child exit
351 0 0       0 my $pid = waitpid(-1,0) or next;
352 0 0       0 $DEBUG && debug("child $pid exit with code ".($?>>8));
353 0 0       0 my $ch = $self->{childs} or return;
354 0         0 for(@$ch) {
355 0 0       0 $_ = undef,last if $_ == $pid
356             }
357             }
358             }
359             }
360              
361             sub getoptions {
362 0     0 0 0 my $self = shift;
363 0         0 local @ARGV = @_;
364             GetOptions(
365 0     0   0 'h|help' => sub { usage() },
366             'P|pcapdir=s' => \$self->{pcapdir},
367             'mitm-ca=s' => \$self->{mitm_ca},
368             'capath=s' => \$self->{capath},
369             'no-check-certificate=s' => \$self->{no_check_certificate},
370             'C|childs=i' => \$self->{childs},
371             'M|maxconn=i' => \$self->{max_connect_per_child},
372             'F|filter=s' => sub {
373 0 0   0   0 if ($_[1] eq '-') {
374             # discard all previously defined
375 0         0 @{$self->{filter}} = ();
  0         0  
376             } else {
377 0         0 push @{$self->{filter}}, $_[1]
  0         0  
378             }
379             },
380             'imp-ns=s' => sub {
381 0 0   0   0 if ($_[1] eq '-') {
382             # discard all previously defined
383 0         0 @{$self->{impns}} = ();
  0         0  
384             } else {
385 0         0 push @{$self->{impns}}, $_[1]
  0         0  
386             }
387             },
388             'l|log:s' => sub {
389             $self->{logrx} = $_[1]
390 0 0 0 0   0 ? eval { qr/$_[1]/ } || "bad rx $_[1]"
391             : qr/./;
392             },
393             'd|debug:s' => sub {
394 0     0   0 $DEBUG = 1;
395 0 0       0 if ($_[1]) {
396 0         0 my $rx = eval { qr{$_[1]} };
  0         0  
397 0 0       0 croak("invalid regex '$_[1]' for debugging: $@") if ! $rx;
398 0         0 $DEBUG_RX = $rx;
399             }
400             },
401             'T|trace=s' => sub {
402 0     0   0 $TRACE{$_} = 1 for split(m/,/,$_[1])
403             },
404 0         0 );
405              
406 0         0 my @addr = @ARGV;
407 0   0     0 $self->{logrx} //= qr/./;
408 0 0 0     0 $self->{addr} or @addr or usage("no listener given");
409 0         0 $self->{addr} = \@addr;
410 0         0 1;
411             }
412              
413              
414             sub usage {
415 0     0 0 0 my ($msg,$cmd) = @_;
416 0   0     0 $cmd ||= $0;
417 0 0       0 print STDERR "ERROR: $msg\n" if $msg;
418 0         0 print STDERR <
419              
420             HTTP proxy, which can inspect and modify requests and responses before
421             forwarding using Net::IMP plugins.
422              
423             $cmd Options* [ip:port|ip:port=upstream_ip:port]+
424             ip:port - listen address(es) for the proxy
425             ip:port=upstream_ip:port - listen adress and upstream proxy
426              
427             Options:
428             -h|--help show usage
429              
430             --mitm-ca ca.pem use given file in PEM format as a Proxy-CA for intercepting
431             SSL connections (e.g. man in the middle). Should include key
432             and cert.
433             --capath P path to file or dir containing CAs, which are used to verify
434             server certificates when intercepting SSL.
435             Tries to use builtin default if not given.
436             --no-check-certificate do not check server certificates when intercepting
437             SSL connections
438              
439             -C|--childs N fork N childs an keep them running, e.g. if one child dies
440             immediatly fork another one. This way one can spread the load
441             over multiple processors (N>1) or just make sure, that child
442             gets restarted on errors (N=1)
443             -M|--maxconn N child will exit (and gets restarted) after N connections
444              
445             -F|--filter F add named IMP plugin as filter, can be used multiple times
446             with --filter mod=args arguments can be given to the filter
447             --imp-ns N perl module namespace, were it will look for IMP plugins.
448             Can be given multiple times.
449             Plugins outside these namespace need to be given with
450             full name.
451             Defaults to App::HTTP_Proxy_IMP, Net::IMP
452              
453             -l|--log [rx] print log messages where category matches rx (default all)
454              
455             # options intended for development and debugging:
456             -P|--pcapdir D save connections as pcap files into D, needs Net::PcapWriter
457             -d|--debug [RX] debug mode, if RX is given restricts debugging to packages
458             matching RX
459             -T|--trace T enable Net::Inspect traces
460              
461             Examples:
462             start proxy at 127.0.0.1:8888 and log all requests to /tmp as pcap files
463             $cmd --filter Net::IMP::SessionLog=dir=/tmp/&format=pcap 127.0.0.1:8888
464             start proxy at 127.0.0.1:8888 and log all form fields
465             $cmd --filter LogFormData 127.0.0.1:8888
466             start proxy at 127.0.0.1:8888 with CSRF protection plugin
467             $cmd --filter CSRFprotect 127.0.0.1:8888
468             start proxy at 127.0.0.1:8888 with CSRF protection plugin, using upstream
469             proxy proxy:8888
470             $cmd --filter CSRFprotect 127.0.0.1:8888=proxy:8888
471              
472             USAGE
473 0         0 exit(2);
474             }
475              
476             ############################################################################
477             # AnyEvent wrapper to privide Net::IMP::Remote etc with acccess to
478             # IO events
479             ############################################################################
480             package App::HTTP_Proxy_IMP::EventLoop;
481 2     2   5 sub new { bless {},shift }
482             {
483             my %watchr;
484             sub onread {
485 0     0     my ($self,$fh,$cb) = @_;
486 0 0         defined( my $fn = fileno($fh)) or die "invalid filehandle";
487 0 0         if ( $cb ) {
488 0           $watchr{$fn} = AnyEvent->io(
489             fh => $fh,
490             cb => $cb,
491             poll => 'r'
492             );
493             } else {
494 0           undef $watchr{$fn};
495             }
496             }
497             }
498              
499             {
500             my %watchw;
501             sub onwrite {
502 0     0     my ($self,$fh,$cb) = @_;
503 0 0         defined( my $fn = fileno($fh)) or die "invalid filehandle";
504 0 0         if ( $cb ) {
505 0           $watchw{$fn} = AnyEvent->io(
506             fh => $fh,
507             cb => $cb,
508             poll => 'w'
509             );
510             } else {
511 0           undef $watchw{$fn};
512             }
513             }
514             }
515              
516 0     0     sub now { return AnyEvent->now }
517             sub timer {
518 0     0     my ($self,$after,$cb,$interval) = @_;
519 0 0         return AnyEvent->timer(
520             after => $after,
521             cb => $cb,
522             $interval ? ( interval => $interval ):()
523             );
524             }
525              
526              
527              
528              
529             1;
530             __END__