File Coverage

blib/lib/Net/IMP/HTTP/LogFormData.pm
Criterion Covered Total %
statement 18 97 18.5
branch 0 40 0.0
condition 0 16 0.0
subroutine 6 16 37.5
pod 6 9 66.6
total 30 178 16.8


line stmt bran cond sub pod time code
1             # sample IMP plugin to log formulare data
2             # e.q query_string and POST data
3              
4 1     1   24595 use strict;
  1         3  
  1         41  
5 1     1   5 use warnings;
  1         2  
  1         35  
6             package Net::IMP::HTTP::LogFormData;
7 1     1   5 use base 'Net::IMP::HTTP::Connection';
  1         2  
  1         157  
8             use fields (
9 1         9 'rqbuf', # buffered data from request
10             'req', # HTTP::Request object for request header
11             'info', # collection of infos for logging after request end
12             'btype', # content type from request body, eg.
13             # application/x-www-form-urlencoded or multipart/form-data
14 1     1   4 );
  1         2  
15              
16 1     1   60 use Net::IMP qw(:DEFAULT :log); # constants
  1         2  
  1         217  
17             require HTTP::Request;
18 1     1   6 use Net::IMP::Debug;
  1         1  
  1         11  
19              
20             sub RTYPES {
21             # we don't change anything but need to analyze, so we can PREPASS
22             # everything initially until Inf and later upgrade it to PASS
23             # because we are only interested in request header and body, data
24             # from server can be passed from the beginning
25             return (
26 0     0 0   IMP_PREPASS,
27             IMP_PASS,
28             IMP_DENY, # on parsing errors
29             IMP_LOG, # somewhere to log the info about form data
30             );
31             }
32              
33             sub new_analyzer {
34 0     0 1   my ($class,%args) = @_;
35 0           my $self = $class->SUPER::new_analyzer(%args);
36 0           $self->run_callback(
37             # prepass all from request
38             [ IMP_PREPASS,0,IMP_MAXOFFSET ],
39             # we don't even need to look at response
40             [ IMP_PASS,1,IMP_MAXOFFSET ],
41             );
42 0           return $self;
43             }
44              
45              
46             sub request_hdr {
47 0     0 1   my ($self,$hdr) = @_;
48 0 0         my $req = $self->{req} = HTTP::Request->parse($hdr) or do {
49             # failed to parse
50 0           $self->run_callback(
51             [ IMP_DENY,0,"failed to parse request header" ]);
52 0           return;
53             };
54              
55 0           $self->{rqbuf} = '';
56 0           $self->{info} = undef;
57 0           $self->{btype} = undef;
58              
59 0 0         if ( my @qp = $req->uri->query_form ) {
60             #debug("got query_string @qp");
61 0           my @param;
62 0           for(my $i=0;$i<@qp;$i+=2 ) {
63 0           push @param,[ $qp[$i], $qp[$i+1] ];
64             }
65 0           $self->{info}{'header.query_string'} = \@param
66             }
67              
68 0           my $ct = $req->content_type;
69 0 0 0       if ( $ct && $req->method eq 'POST' and
      0        
70             $ct ~~ ['application/x-www-form-urlencoded','multipart/form-data']
71             ){
72             #debug("got content-type $ct");
73 0           $self->{btype} = $ct;
74             } else {
75             # no need to analyze further
76 0   0       my $len = $req->content_length // 0;
77             #debug("no or no interesting body");
78 0 0         $self->_log_formdata() if $self->{info};
79 0           $self->{rqbuf} = ''; # throw away
80 0           $self->run_callback( [ IMP_PASS,0, $self->offset(0) + $len ]);
81             }
82             }
83              
84             sub request_body {
85 0     0 1   my ($self,$data,$offset) = @_;
86 0 0         $offset and die "gaps should not happen";
87              
88 0 0 0       if (( $data//'') eq '' ) {
    0          
89             # eof
90             # parse body if necessary
91             #debug("eof on $dir");
92 0 0         if ( ! $self->{btype} ) {
    0          
    0          
93             } elsif ( $self->{btype} eq 'application/x-www-form-urlencoded' ) {
94 0           my @param;
95 0           for( split( /\&/,$self->{rqbuf}) ) {
96 0           my ($k,$v) = split('=',$_,2);
97 0           for($k,$v) {
98 0 0         defined($_) or next;
99 0           s{\+}{ }g;
100 0           s{%([\da-fA-F]{2})}{ chr(hex($1)) }esg;
  0            
101             }
102 0           push @param,[$k,$v];
103             }
104 0           $self->{info}{'body.urlencoded'} = \@param;
105            
106             } elsif ( $self->{btype} eq 'multipart/form-data' ) {
107 0           my (undef,$boundary) = $self->{req}->header('content-type')
108             =~m{;\s*boundary=(\"?)([^";,]+)\1}i;
109 0 0         if ( ! $boundary ) {
110 0           $self->run_callback([
111             IMP_DENY,0,
112             "missing boundary for multipart/form-data"
113             ]);
114             }
115             # we might use MIME:: heere, but this would be yet another non-CORE
116             # dependency :(
117             # this is quick and dirty and we just skip param on errors, but
118             # this is just a demo!
119 0           my @param;
120 0           for my $part ( split(
121             m{^--\Q$boundary\E(?:--)?\r?\n}m,
122             $self->{rqbuf} )) {
123 0 0         $part =~m{\A(.*?(\r?\n))\2(.*)}s or next;
124 0           my ($hdr,$v) = ($1,$3);
125             my ($cd) = $hdr =~m{^Content-Disposition:[ \t]*(.*(?:\r?\n[ \t].*)*)}mi
126 0 0         or do {
127 0           debug("no content-disposition in multipart header: $hdr");
128 0           next;
129             };
130 0           $cd =~s{\r?\n}{}g;
131 0   0       my $name = $cd =~m{;\s*name=(?:\"([^\"]+)\"|([^\s\";]+))} && ($1||$2);
132 0 0         $name or do {
133 0           debug("no name in content-disposition in multipart header: $hdr");
134 0           next;
135             };
136 0   0       my $fname = $cd =~m{;\s*filename=(?:\"([^\"]+)\"|([^\s\";]+))} && ($1||$2);
137 0           $v =~s{\r?\n\Z}{};
138 0 0         $v = "UPLOAD:$fname (".length($v)." bytes)" if $fname; # don't display content of file
139 0           push @param, [$name,$v];
140             }
141 0           $self->{info}{'body.multipart'} = \@param;
142             } else {
143             # should not happen, we set btype only if we can handle the type
144 0           die "unhandled POST content-type $self->{btype}"
145             }
146 0           $self->_log_formdata();
147              
148             } elsif ( $self->{btype} ) {
149             # add to buf to analyze later
150 0           $self->{rqbuf} .= $data;
151             }
152             }
153              
154             # these should not be reached
155 0     0 1   sub response_hdr {}
156 0     0 1   sub response_body {}
157 0     0 1   sub any_data {}
158 0     0 0   sub chunk_header {}
159 0     0 0   sub chunk_trailer {}
160              
161             sub _log_formdata {
162 0     0     my $self = shift;
163 0 0         my $info = $self->{info} or return;
164             # report form information if any, preferable as YAML, but fall back to
165             # Data::Dumper, which is in core
166 0           my $text;
167 0 0         if ( eval { require YAML } ) {
  0 0          
    0          
168 0           $text = YAML::Dump($info)
169 0           } elsif ( eval { require YAML::Tiny } ) {
170 0           $text = YAML::Tiny::Dump($info)
171 0           } elsif ( eval { require Data::Dumper }) {
172 0           $text = Data::Dumper->new([$info])->Terse(1)->Dump;
173             } else {
174             # Data::Dumper is perl core!
175 0           die "WTF, not even Data::Dumper is installed?";
176             }
177 0           $self->run_callback([ IMP_LOG,0,0,0,IMP_LOG_INFO,$text ]);
178 0           $self->{info} = undef;
179             }
180              
181             __END__