File Coverage

lib/Browser/FingerPrint.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package Browser::FingerPrint;
2 1     1   1189 use strict;
  1         2  
  1         37  
3 1     1   5 use warnings;
  1         2  
  1         34  
4              
5 1     1   771 use version; our $VERSION = qv('0.1');
  1         2208  
  1         6  
6 1     1   481 use Params::Validate qw/ :all /;
  0            
  0            
7             use HTML::Entities;
8              
9             use Apache2::RequestUtil;
10             use Apache2::RequestRec;
11              
12             =pod
13              
14             =head1 NAME
15            
16             Browser::FingerPrint - Web Browser Fingerprinting
17            
18            
19             =head1 VERSION
20            
21             This documentation refers to Browser::FingerPrint version 0.1
22            
23            
24             =head1 SYNOPSIS
25            
26             use Browser::FingerPrint;
27             use CGI;
28              
29             my $q = CGI->new;
30              
31             my $fp = Browser::FingerPrint->new({
32             q => $q,
33             database_path => 'db',
34             });
35              
36             my $best_hit = $fp->browser_recon();
37            
38            
39            
40             =head1 DESCRIPTION
41            
42             A full description of the module and its features.
43            
44            
45             =head1 SUBROUTINES/METHODS
46            
47             =cut
48              
49             ##########################################################################
50              
51             =pod
52              
53             =head2 new()
54              
55             Constructor
56              
57             my $fp = Browser::FingerPrint->new({
58             q => $q,
59             database_path => '/path/to/db',
60             });
61              
62             q - The CGI object
63             database_path - path to the fingerprint database directory
64              
65             =cut
66              
67             #############################################################################
68             sub new
69             {
70             my $proto = shift;
71              
72             my %params = validate(
73             @_,
74             {
75             q => { ias => 'CGI', },
76             database_path => {
77             callbacks => {
78             is_a_dir => sub {
79             my $p = shift;
80             return -d $p;
81             },
82             },
83             },
84             }
85             );
86              
87             my $class = ref $proto || $proto;
88             my $self = {};
89             bless $self, $class;
90              
91             $self->{_q} = $params{q};
92             $self->{_db_path} = $params{database_path};
93             $self->{_headers} = $self->get_http_headers;
94              
95             return $self;
96             } # end sub new
97              
98             sub get_http_headers
99             {
100             my $self = shift;
101             my %ret = map { lc($_) => $self->{_q}->http($_) } $self->{_q}->http;
102             return \%ret;
103             }
104              
105             ##########################################################################
106              
107             =pod
108              
109             =head2 SUB/METHOD NAME GOES HERE
110              
111             Description of the sub here
112              
113             =cut
114              
115             #############################################################################
116             sub count_hit_possibilities
117             {
118             my $self = shift;
119             my $count = 0;
120              
121             if ( exists $self->{_headers}->{http_user_agent} )
122             {
123             ++$count;
124             }
125             if ( exists $self->{_headers}->{http_accept} )
126             {
127             ++$count;
128             }
129             if ( exists $self->{_headers}->{http_accept_language} )
130             {
131             ++$count;
132             }
133             if ( exists $self->{_headers}->{http_accept_encoding} )
134             {
135             ++$count;
136             }
137             if ( exists $self->{_headers}->{http_accept_charset} )
138             {
139             ++$count;
140             }
141             if ( exists $self->{_headers}->{http_keep_alive} )
142             {
143             ++$count;
144             }
145             if ( exists $self->{_headers}->{http_connection} )
146             {
147             ++$count;
148             }
149             if ( exists $self->{_headers}->{http_cache_control} )
150             {
151             ++$count;
152             }
153             if (get_header_order() ne q{}) {
154             ++$count;
155             }
156              
157             #(getheadervalue($rawheader, 'UA-Pixels') != '' ? ++$count : '');
158             #(getheadervalue($rawheader, 'UA-Color') != '' ? ++$count : '');
159             #(getheadervalue($rawheader, 'UA-OS') != '' ? ++$count : '');
160             #(getheadervalue($rawheader, 'UA-CPU') != '' ? ++$count : '');
161             #(getheadervalue($rawheader, 'TE') != '' ? ++$count : '');
162              
163             return $count;
164             } # end sub count_hit_possibilities
165              
166             ##########################################################################
167              
168             =pod
169              
170             =head2 SUB/METHOD NAME GOES HERE
171              
172             Description of the sub here
173              
174             =cut
175              
176             #############################################################################
177             sub identify_global_fingerprint
178             {
179             my $self = shift;
180             my $ml = q{};
181              
182             $ml .= $self->find_match_in_database( 'user-agent.fdb',
183             $self->{_headers}->{http_user_agent} );
184             $ml .= $self->find_match_in_database( 'accept.fdb',
185             $self->{_headers}->{http_accept} );
186             $ml .= $self->find_match_in_database( 'accept-language.fdb',
187             $self->{_headers}->{http_accept_language} );
188             $ml .= $self->find_match_in_database( 'accept-encoding.fdb',
189             $self->{_headers}->{http_accept_encoding} );
190             $ml .= $self->find_match_in_database( 'accept-charset.fdb',
191             $self->{_headers}->{http_accept_charset} );
192             $ml .= $self->find_match_in_database( 'keep-alive.fdb',
193             $self->{_headers}->{http_keep_alive} );
194             $ml .= $self->find_match_in_database( 'connection.fdb',
195             $self->{_headers}->{http_connection} );
196             $ml .= $self->find_match_in_database( 'cache-control.fdb',
197             $self->{_headers}->{http_cache_control} );
198             #$ml .= $self->find_match_in_database( 'header-order.fdb',
199             #get_header_order() );
200              
201             #$matchlist.= findmatchindatabase($database.'ua-pixels.fdb', getheadervalue($rawheader, 'UA-Pixels'));
202             #$matchlist.= findmatchindatabase($database.'ua-color.fdb', getheadervalue($rawheader, 'UA-Color'));
203             #$matchlist.= findmatchindatabase($database.'ua-os.fdb', getheadervalue($rawheader, 'UA-OS'));
204             #$matchlist.= findmatchindatabase($database.'ua-cpu.fdb', getheadervalue($rawheader, 'UA-CPU'));
205             #$matchlist.= findmatchindatabase($database.'te.fdb', getheadervalue($rawheader, 'TE'));
206             #$matchlist.= findmatchindatabase($database.'header-order.fdb', getheaderorder($rawheader));
207              
208             return $ml;
209             } # end sub identify_global_fingerprint
210              
211             ##########################################################################
212              
213             =pod
214              
215             =head2 SUB/METHOD NAME GOES HERE
216              
217             Description of the sub here
218              
219             =cut
220              
221             #############################################################################
222             sub find_match_in_database
223             {
224             my ( $self, $db_file, $fp ) = @_;
225             my $matches = q{};
226              
227             open my $RFH, "<", $self->db_file_path($db_file)
228             or die "Can't open $db_file for reading";
229              
230             while (<$RFH>)
231             {
232             chomp;
233             my ( $k, $v ) = split /;/, $_, 2;
234             if ( $fp eq trim($v) )
235             {
236             $matches .= $k . ';';
237             }
238             }
239              
240             return $matches;
241             } # end sub find_match_in_database
242              
243             sub db_file_path
244             {
245             my ( $self, $f ) = @_;
246              
247             return $self->{_db_path} . '/' . $f;
248             }
249              
250             sub trim
251             {
252             my $s = shift;
253             $s =~ s{^\s*}{};
254             $s =~ s{\s*$}{};
255             return $s;
256             }
257              
258             ##########################################################################
259              
260             =pod
261              
262             =head2 SUB/METHOD NAME GOES HERE
263              
264             Description of the sub here
265              
266             =cut
267              
268             #############################################################################
269             sub generate_match_statistics
270             {
271             my ( $self, $ml ) = @_;
272             my $ms = q{};
273              
274             my @orig_matches = split ';', $ml;
275             my %matches = map { $_ => 1 } @orig_matches;
276             my @matches = keys %matches;
277              
278             for (@matches)
279             {
280             $ms .= $_ . '=' . count_if( \@orig_matches, $_ ) . "\n";
281             }
282              
283             return $ms;
284             }
285              
286             sub count_if
287             {
288             my ( $input, $search ) = @_;
289             my $sum = 0;
290              
291             for (@$input)
292             {
293             if ( $_ eq $search )
294             {
295             ++$sum;
296             }
297             }
298              
299             return $sum;
300             }
301              
302             ##########################################################################
303              
304             =pod
305              
306             =head2 SUB/METHOD NAME GOES HERE
307              
308             Description of the sub here
309              
310             =cut
311              
312             #############################################################################
313             sub announce_fingerprint_matches
314             {
315             my $self = shift;
316              
317             my %params = validate(
318             @_,
319             {
320             full_match_list => { type => SCALAR, },
321             mode => {
322             default => 'best_hit',
323             regex => qr/^best_hit|list|best_hit_list|best_hit_detail$/,
324             },
325             hit_possibilities => { default => 0, },
326             }
327             );
328              
329             my @res = split /\n/, $params{full_match_list};
330              
331             my $scan_besthitcount = 0;
332             my $scan_besthitname = q{};
333             my $scan_resultlist = q{};
334             my @scan_resultarray;
335              
336             for (@res)
337             {
338             my @entry = split /=/, $_, 2;
339              
340             if ( length $entry[0] )
341             {
342             if ( $scan_besthitcount < $entry[1] )
343             {
344             $scan_besthitname = $entry[0];
345             $scan_besthitcount = $entry[1];
346             }
347             $scan_resultlist .= $entry[0] . ': ' . $entry[1] . "\n";
348             push @scan_resultarray,
349             $entry[1] . ';' . encode_entities( $entry[0] );
350             }
351             } # end for (@res)
352              
353             if ( $params{mode} eq 'list' )
354             {
355             return $scan_resultlist;
356             }
357             elsif ( $params{mode} eq 'best_hit_list' )
358             {
359             my $scan_hitaccuracy;
360             my $scan_hitlist;
361              
362             @scan_resultarray = reverse sort @scan_resultarray;
363             for ( 0 .. 9 )
364             {
365             my @scan_resultitem = split /;/, $scan_resultarray[$_], 2;
366             if ( $scan_resultitem[0] > 0 )
367             {
368             if ( $params{hit_possibilities} > 0 )
369             {
370             $scan_hitaccuracy = sprintf "%0.*f", 2,
371             ( 100 / $params{hit_possibilities} )
372             * $scan_resultitem[0];
373             }
374             else
375             {
376             $scan_hitaccuracy = sprintf "%0.*f", 2,
377             ( 100 / $scan_besthitcount ) * $scan_resultitem[0];
378             }
379              
380             $scan_hitlist .=
381             ( $_ + 1 ) . '. '
382             . $scan_resultitem[1] . ' ('
383             . $scan_hitaccuracy
384             . '% with '
385             . $scan_resultitem[0]
386             . ' hits)';
387              
388             if ( $_ < 9 )
389             {
390             $scan_hitlist .= "\n";
391             }
392             } # end if ( $scan_resultitem[...])
393             } # end for ( 0 .. 9 )
394              
395             return $scan_hitlist;
396             }
397             elsif ( $params{mode} eq 'best_hit_detail' )
398             {
399             my $scan_hitaccuracy;
400              
401             if ( $params{hit_possibilities} > 0 )
402             {
403             $scan_hitaccuracy = sprintf "%0.*f", 2,
404             ( 100 / $params{hit_possibilities} ) * $scan_besthitcount;
405             }
406             else
407             {
408             $scan_hitaccuracy = 100;
409             }
410             return
411             $scan_besthitname . ' ('
412             . $scan_hitaccuracy
413             . '% with '
414             . $scan_besthitcount
415             . ' hits)';
416             }
417             else
418             {
419             return $scan_besthitname;
420             }
421             } # end sub announce_fingerprint_matches
422              
423             sub round
424             {
425             my $number = shift;
426             return int( $number + .5 );
427             }
428              
429              
430             ##########################################################################
431             =pod
432              
433             =head2 browser_recon()
434              
435             Description of the sub here
436              
437             =cut
438             #############################################################################
439             sub browser_recon
440             {
441             my $self = shift;
442              
443             my %params = validate(
444             @_,
445             {
446             mode => {
447             default => 'best_hit',
448             regex => qr/^best_hit|list|best_hit_list|best_hit_detail$/,
449             },
450             }
451             );
452              
453             my $ms = $self->generate_match_statistics(
454             $self->identify_global_fingerprint );
455             my $hit_possibilities = $self->count_hit_possibilities;
456              
457             return $self->announce_fingerprint_matches(
458             {
459             full_match_list => $ms,
460             mode => $params{mode},
461             hit_possibilities => $hit_possibilities,
462             }
463             );
464             } # end sub browser_recon
465              
466              
467             ##########################################################################
468             =pod
469              
470             =head2 get_header_order()
471              
472             Returns a string containing the order in which the HTTP request headers
473             were sent
474              
475             NOTE: This only works under mod_perl
476              
477             =cut
478             #############################################################################
479             sub get_header_order
480             {
481             my $r = Apache2::RequestUtil->request;
482              
483             return join ", ", map { m{^(.+?):} } grep { m{^.+: .+$} } split "\n",
484             $r->as_string;
485             }
486              
487             1;
488              
489             =head1 AUTHOR
490            
491             Rohan Almeida
492            
493            
494             =head1 LICENCE AND COPYRIGHT
495            
496             Copyright (c) 2010 Rohan Almeida . All rights
497             reserved.
498              
499             This module is free software; you can redistribute it and/or
500             modify it under the same terms as Perl itself.
501              
502             This program is distributed in the hope that it will be useful,
503             but WITHOUT ANY WARRANTY; without even the implied warranty of
504             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
505