File Coverage

blib/lib/CatalystX/DebugFilter.pm
Criterion Covered Total %
statement 78 84 92.8
branch 25 26 96.1
condition 3 6 50.0
subroutine 12 14 85.7
pod n/a
total 118 130 90.7


line stmt bran cond sub pod time code
1             package CatalystX::DebugFilter;
2             $CatalystX::DebugFilter::VERSION = '0.16';
3             # ABSTRACT: Provides configurable filtering of data that is logged to the debug logs (and error screen)
4 3     3   1966140 use Moose::Role;
  3         971134  
  3         20  
5 3     3   19195 use namespace::autoclean;
  3         16392  
  3         15  
6 3     3   225 use Scalar::Util qw(reftype blessed);
  3         6  
  3         3783  
7             requires('dump_these','log_request_headers','log_response_headers');
8             our $CONFIG_KEY = __PACKAGE__;
9             my %filters = (
10             Request => \&_filter_request,
11             Response => \&_filter_response,
12             Stash => \&_filter_stash,
13             Session => \&_filter_session,
14             );
15             around dump_these => sub {
16             my $next = shift;
17             my $c = shift;
18             my @dump = $next->( $c, @_ );
19             if ( my $config = $c->config->{$CONFIG_KEY} ) {
20             foreach my $d (@dump) {
21             my ( $type, $obj ) = @$d;
22             my $callback = $filters{$type} or next;
23             my $filter_config = $config->{$type} or next;
24             my $obj_type = reftype($obj);
25              
26             # poor-man's shallow cloning, none of the Clone
27             # modules were problem-free...
28             my $copy;
29             if ( $obj_type eq 'HASH' ) {
30             $copy = {%$obj};
31             } elsif ( $obj_type eq 'ARRAY' ) {
32             $copy = [@$obj];
33             } else {
34             $copy = "$obj"; # not going to bother with anything else
35             }
36             if(ref $copy and my $obj_ref = blessed $obj){
37             bless $copy, $obj_ref;
38             }
39              
40             if ( $callback->( $filter_config, $copy ) ) {
41             $d->[1] = $copy;
42             }
43             }
44             }
45             return @dump;
46             };
47              
48             sub _normalize_filters {
49 21 100   21   81 my @filters = grep { defined $_ } ( ref( $_[0] ) eq 'ARRAY' ? @{ $_[0] } : @_ );
  28         96  
  7         20  
50 21         48 my @normalized = map { _make_filter_callback($_) } @filters;
  28         56  
51 21         53 return @normalized;
52             }
53              
54             sub _make_filter_callback {
55 28     28   48 my $filter = shift;
56              
57 28         50 my $filter_str = '[FILTERED]';
58 28 100       83 if ( ref($filter) eq 'Regexp' ) {
    100          
59 14 100   49   71 return sub { return $_[0] =~ $filter ? $filter_str : undef };
  49         202  
60             } elsif ( ref($filter) eq 'CODE' ) {
61 7         19 return $filter;
62             } else {
63 7 100   12   52 return sub { return $_[0] eq $filter ? $filter_str : undef };
  12         39  
64             }
65             }
66              
67             sub _filter_request {
68 7     7   20 my ( $config, $req ) = @_;
69              
70 7         33 my $filtered = _filter_request_params( $config->{params}, $req );
71 7 100       183 if ( my $h = _filter_headers( $config, $req->headers ) ) {
72 2         63 $req->headers($h);
73 2         42 $filtered++;
74             }
75 7         79 return $filtered;
76              
77             }
78              
79             sub _filter_request_params {
80 7     7   18 my ( $param_filter, $req ) = @_;
81 7 50       29 return if !$param_filter;
82 7         15 my $is_filtered = 0;
83 7         32 my @types = ( 'query', 'body', '' );
84 7         23 my @filters = _normalize_filters($param_filter);
85 7         21 foreach my $type (@types) {
86 21         87 my $method = join '_', grep { $_ } $type, 'parameters';
  42         114  
87 21         459 my $params = $req->$method;
88 21 100 33     567 next unless defined $params && ref $params && %$params;
      66        
89 12         62 my $copy = { %$params };
90 12         41 $is_filtered += _filter_hash_ref( $copy, @filters );
91 12 100       34 if($is_filtered){
92 10         171 $req->$method($copy);
93             }
94             }
95 7         278 return $is_filtered;
96             }
97              
98             sub _filter_hash_ref {
99 12     12   20 my $hash = shift;
100 12         32 my @filters = @_;
101 12         24 my $is_filtered = 0;
102 12         41 foreach my $k ( keys %$hash ) {
103 12         25 foreach my $f (@filters) {
104 12         25 my $copy = $k;
105 12         33 my $filtered = $f->( $copy => $hash->{$k} );
106 12 100       32 if ( defined $filtered ) {
107 10         22 $hash->{$k} = $filtered;
108 10         14 $is_filtered++;
109 10         24 last;
110             }
111             }
112             }
113 12         30 return $is_filtered;
114             }
115              
116             sub _filter_headers {
117 14     14   856 my ( $config, $headers ) = @_;
118 14         36 my @filters = _normalize_filters( $config->{headers} );
119 14         49 my $filtered_headers = HTTP::Headers->new();
120 14         122 my $filtered = 0;
121 14         45 foreach my $name ( $headers->header_field_names ) {
122 49         1257 my @values = $headers->header($name);
123              
124             # headers can be multi-valued
125 49         1576 foreach my $value (@values) {
126 49         82 foreach my $f (@filters) {
127 60         110 my ( $copy_name, $copy_value ) = ( $name, $value );
128 60         119 my $new_value = $f->( $copy_name, $copy_value );
129              
130             # if a defined value is returned, we use that
131 60 100       198 if ( defined $new_value ) {
132 4         6 $value = $new_value;
133 4         8 $filtered++;
134 4         8 last; # skip the rest of the filters
135             }
136             }
137 49         121 $filtered_headers->push_header( $name, $value );
138             }
139             }
140 14 100       504 return $filtered ? $filtered_headers : undef;
141             }
142              
143             sub _filter_response {
144 7     7   17 my ( $config, $res ) = @_;
145 7         12 my $filtered = 0;
146 7 100       32 if ( my $h = _filter_headers( $config, $res->headers ) ) {
147 1         7 $res->headers($h);
148 1         530 $filtered++;
149             }
150 7         195 return $filtered;
151             }
152              
153             sub _filter_stash {
154 0     0     my ( $config, $stash ) = @_;
155 0           my @filters = _normalize_filters($config);
156 0           return _filter_hash_ref($stash);
157             }
158              
159             sub _filter_session {
160 0     0     my ( $config, $stash ) = @_;
161 0           my @filters = _normalize_filters($config);
162 0           return _filter_hash_ref($stash);
163             }
164              
165             1;
166              
167             __END__
168              
169             =pod
170              
171             =encoding UTF-8
172              
173             =head1 NAME
174              
175             CatalystX::DebugFilter - Provides configurable filtering of data that is logged to the debug logs (and error screen)
176              
177             =head1 VERSION
178              
179             version 0.16
180              
181             =head1 SYNOPSIS
182              
183             package MyApp;
184              
185             use Catalyst;
186             with 'CatalystX::DebugFilter';
187              
188             __PACKAGE__->config(
189             'CatalystX::DebugFilter' => {
190              
191             # filter all "Cookie" headers as well as "password" and "SECRET" parameters
192             Request => { headers => 'Cookie', params => [ 'password', qr/SECRET/ ] },
193              
194             # filter all Set-Cookie values in the response
195             Response => { headers => 'Set-Cookie' },
196              
197             Stash => [
198             sub {
199             my ( $key, $value ) = @_;
200             my $type = ref($value);
201              
202             # ignore any non-ref values
203             return undef if !$type;
204              
205             if ( $type->isa('DBIx::Class::ResultSet') ) { # dump ResultSet objects as SQL
206             return $value->as_query;
207             } elsif ( $type->isa('DBIx::Class::Result') ) { # dump Result objects as simple HASH
208             return { $value->get_columns };
209             } else { # ignore these
210             return undef;
211             }
212             },
213             ],
214             Session => [
215             'secret_session_key'
216             ],
217             }
218             );
219              
220             =head1 DESCRIPTION
221              
222             This module provides a Moose role that will filter certain elements of
223             a request/response/stash/session before they are logged to the debug logs (or
224             the error screen).
225              
226             =head1 METHODS
227              
228             =head2 dump_these
229              
230             This role uses an "around" method modifier on the L<Catalyst/dump_these>
231             method and modifies the elements returned according to the configuration
232             provided by the user as demonstrated in the L<SYNOPSIS> section.
233              
234             =head1 FILTER CONFIGURATION
235              
236             There are few different types of filters that can be defined:
237              
238             =over 4
239              
240             =item * Exact Match
241              
242             The parameter/header/stash key is compared against a literal string.
243             If it matches, the value is replaced with C<[FILTERED]>
244              
245             =item * Regular Expression
246              
247             The parameter/header/stash key is compared against a regular expression.
248             If it matches, the value is replaced with C<[FILTERED]>
249              
250             =item * Callback
251              
252             The parameter/header/stash key and value are passed to a callback
253             function. If the function returns a defined value, that value is used
254             instead of the original value.
255              
256             =back
257              
258             This module supports filtering a few different types of data (naturally,
259             these could all be combined into a single C<config> call):
260              
261             =over 4
262              
263             =item * Request Parameters
264              
265             __PACKAGE__->config( 'CatalystX::DebugFilter' => { Request => { params => $filters } } );
266              
267             =item * Request Headers
268              
269             Useful with L<CatalystX::Debug::RequestHeaders>:
270              
271             __PACKAGE__->config( 'CatalystX::DebugFilter' => { Request => { headers => $filters } } );
272              
273             =item * Response Headers
274              
275             Useful with L<CatalystX::Debug::ResponseHeaders>:
276              
277             __PACKAGE__->config( 'CatalystX::DebugFilter' => { Response => { headers => $filters } } );
278              
279             =item * Stash Data
280              
281             __PACKAGE__->config( 'CatalystX::DebugFilter' => { Stash => $filters } );
282              
283             =back
284              
285             In each of the above examples, C<$filters> can be one of a few things:
286              
287             =over 4
288              
289             =item * A non-ref scalar, implying an exact match
290              
291             =item * A Regexp reference, implying an regular expression match
292              
293             =item * A CODE reference, implying a callback matching function
294              
295             =item * An ARRAY reference of any of the above
296              
297             =back
298              
299             =head1 CAVEATS
300              
301             This module will not magically remove all references to a specific piece
302             of data unless filters are explicitly defined for each place this data
303             is stored. For instance, you may define a request parameter filter to
304             prevent passwords from being logged to the debug logs but if you create
305             an object that contains that password and store it in the stash, the
306             password value may still appear on the error screen.
307              
308             Also, the stash and session are only filtered at the top level. If you
309             would like to filter more extensively, you can use a filter callback to
310             traverse the hash, modifying whatever data you like (a shallow copy is
311             made before passing the value to the callback).
312              
313             =head1 SEE ALSO
314              
315             =over 4
316              
317             =item * L<CatalystX::Debug::RequestHeaders>
318              
319             =item * L<CatalystX::Debug::ResponseHeaders>
320              
321             =back
322              
323             =head1 AUTHOR
324              
325             Brian Phillips <bphillips@cpan.org>
326              
327             =head1 COPYRIGHT AND LICENSE
328              
329             This software is copyright (c) 2018 by Brian Phillips.
330              
331             This is free software; you can redistribute it and/or modify it under
332             the same terms as the Perl 5 programming language system itself.
333              
334             =cut