File Coverage

blib/lib/Net/Google/SafeBrowsing2.pm
Criterion Covered Total %
statement 140 649 21.5
branch 16 202 7.9
condition 6 140 4.2
subroutine 27 58 46.5
pod 35 36 97.2
total 224 1085 20.6


line stmt bran cond sub pod time code
1             package Net::Google::SafeBrowsing2;
2              
3 1     1   13616 use strict;
  1         2  
  1         23  
4 1     1   3 use warnings;
  1         1  
  1         24  
5              
6 1     1   2 use Carp;
  1         4  
  1         69  
7 1     1   541 use LWP::UserAgent;
  1         33940  
  1         25  
8 1     1   7 use URI;
  1         1  
  1         18  
9 1     1   457 use Digest::SHA qw(sha256);
  1         3160  
  1         70  
10 1     1   5 use List::Util qw(first);
  1         1  
  1         47  
11 1     1   365 use Text::Trim;
  1         410  
  1         46  
12 1     1   334 use Digest::HMAC_SHA1 qw(hmac_sha1 hmac_sha1_hex);
  1         939  
  1         40  
13 1     1   356 use MIME::Base64::URLSafe;
  1         1036  
  1         42  
14 1     1   5 use MIME::Base64;
  1         1  
  1         30  
15 1     1   385 use String::HexConvert;
  1         219  
  1         30  
16 1     1   460 use File::Slurp;
  1         10189  
  1         88  
17 1     1   1017 use IO::Socket::SSL 'inet4' ;
  1         62661  
  1         7  
18              
19              
20 1     1   181 use Exporter 'import';
  1         1  
  1         52  
21             our @EXPORT = qw(DATABASE_RESET MAC_ERROR MAC_KEY_ERROR INTERNAL_ERROR SERVER_ERROR NO_UPDATE NO_DATA SUCCESSFUL MALWARE PHISHING);
22              
23             our $VERSION = '1.13';
24              
25             BEGIN {
26 1     1   4 IO::Socket::SSL::set_ctx_defaults(
27             # verify_mode => Net::SSLeay->VERIFY_PEER(),
28             SSL_verify_mode => 0,
29             );
30             }
31              
32              
33             =head1 NAME
34              
35             DEPRECATED. Please use L for the Google Safe Browsing v3 API.
36              
37             Net::Google::SafeBrowsing2 - Perl library for the Google Safe Browsing v2 API. (Google Safe Browsing v1 has been deprecated by Google.)
38              
39             =head1 SYNOPSIS
40              
41             use Net::Google::SafeBrowsing2;
42             use Net::Google::SafeBrowsing2::Sqlite;
43            
44             my $storage = Net::Google::SafeBrowsing2::Sqlite->new(file => 'google-v2.db');
45             my $gsb = Net::Google::SafeBrowsing2->new(
46             key => "my key",
47             storage => $storage,
48             );
49            
50             $gsb->update();
51             my $match = $gsb->lookup(url => 'http://www.gumblar.cn/');
52            
53             if ($match eq MALWARE) {
54             print "http://www.gumblar.cn/ is flagged as a dangerous site\n";
55             }
56              
57             $storage->close();
58              
59             =head1 DESCRIPTION
60              
61             Net::Google::SafeBrowsing2 implements the Google Safe Browsing v2 API.
62              
63             The library passes most of the unit tests listed in the API documentation. See the documentation (L) for more details about the failed tests.
64              
65             The Google Safe Browsing database must be stored and managed locally. L uses Sqlite as the storage back-end, L uses MySQL. Other storage mechanisms (databases, memory, etc.) can be added and used transparently with this module.
66              
67             You may want to look at "Google Safe Browsing v2: Implementation Notes" (L), a collection of notes and real-world numbers about the API. This is intended for people who want to learn more about the API, whether as a user or to make their own implementation.
68              
69             The source code is available on github at L.
70              
71             If you do not need to inspect more than 10,000 URLs a day, you can use L with the Google Safe Browsing v2 Lookup API which does not require to store and maintain a local database.
72              
73             IMPORTANT: If you start with an empty database, you will need to perform several updates to retrieve all the Google Safe Browsing information. This may require up to 24 hours. This is a limitation of the Google API, not of this module. See "Google Safe Browsing v2: Implementation Notes" at L.
74              
75             =head1 CONSTANTS
76              
77             Several constants are exported by this module:
78              
79             =over 4
80              
81             =item DATABASE_RESET
82              
83             Google requested to reset (empty) the local database.
84              
85             =item MAC_ERROR
86              
87             The replies from Google could not be validated with the MAC keys.
88              
89             =item MAC_KEY_ERROR
90              
91             The request for the MAC keys failed.
92              
93             =item INTERNAL_ERROR
94              
95             An internal error occurred.
96              
97             =item SERVER_ERROR
98              
99             The server sent an error back to the client.
100              
101             =item NO_UPDATE
102              
103             No update was performed, probably because it is too early to make a new request to Google Safe Browsing.
104              
105             =item NO_DATA
106              
107             No data was sent back by Google to the client, probably because the database is up to date.
108              
109             =item SUCCESSFUL
110              
111             The operation was successful.
112              
113             =item MALWARE
114              
115             Name of the Malware list in Google Safe Browsing (shortcut to 'goog-malware-shavar')
116              
117             =item PHISHING
118              
119             Name of the Phishing list in Google Safe Browsing (shortcut to 'googpub-phish-shavar')
120              
121             =back
122              
123             =cut
124              
125             use constant {
126 1         172 DATABASE_RESET => -6,
127             MAC_ERROR => -5,
128             MAC_KEY_ERROR => -4,
129             INTERNAL_ERROR => -3, # internal/parsing error
130             SERVER_ERROR => -2, # Server sent an error back
131             NO_UPDATE => -1, # no update (too early)
132             NO_DATA => 0, # no data sent
133             SUCCESSFUL => 1, # data sent
134             MALWARE => 'goog-malware-shavar',
135             PHISHING => 'googpub-phish-shavar',
136             FULL_HASH_TIME => 45 * 60,
137             INTERVAL_FULL_HASH_TIME => 'INTERVAL 45 MINUTE',
138 1     1   73 };
  1         1  
139              
140              
141             =head1 CONSTRUCTOR
142              
143             =over 4
144              
145             =back
146              
147             =head2 new()
148              
149             Create a Net::Google::SafeBrowsing2 object
150              
151             my $gsb = Net::Google::SafeBrowsing2->new(
152             key => "my key",
153             storage => Net::Google::SafeBrowsing2::Sqlite->new(file => 'google-v2.db'),
154             debug => 0,
155             mac => 0,
156             list => MALWARE,
157             );
158              
159             Arguments
160              
161             =over 4
162              
163             =item server
164              
165             Safe Browsing Server. https://safebrowsing.clients.google.com/safebrowsing/ by default
166              
167             =item mac_server
168              
169             Safe Browsing MAC Server. https://sb-ssl.google.com/safebrowsing/ by default
170              
171             =item key
172              
173             Required. Your Google Safe browsing API key
174              
175             =item storage
176              
177             Required. Object which handle the storage for the Google Safe Browsing database. See L for more details.
178              
179             =item list
180              
181             Optional. The Google Safe Browsing list to handle. By default, handles both MALWARE and PHISHING.
182              
183             =item mac
184              
185             Optional. Set to 1 to enable Message Authentication Code (MAC). 0 (disabled) by default.
186              
187             =item debug
188              
189             Optional. Set to 1 to enable debugging. 0 (disabled) by default.
190              
191             The debug output maybe quite large and can slow down significantly the update and lookup functions.
192              
193             =item errors
194              
195             Optional. Set to 1 to show errors to STDOUT. 0 (disabled by default).
196              
197             =item perf
198              
199             Optional. Set to 1 to show performance information.
200              
201             =item version
202              
203             Optional. Google Safe Browsing version. 2.2 by default
204              
205             =back
206              
207             =cut
208              
209             sub new {
210 1     1 1 357 my ($class, %args) = @_;
211              
212 1         12 my $self = { # default arguments
213             server => 'https://safebrowsing.clients.google.com/safebrowsing/',
214             mac_server => 'https://sb-ssl.google.com/safebrowsing/',
215             list => ['googpub-phish-shavar', 'goog-malware-shavar'],
216             key => '',
217             version => '2.2',
218             debug => 0,
219             errors => 0,
220             last_error => '',
221             mac => 0,
222             perf => 0,
223              
224             %args,
225             };
226              
227 1 50       4 if (! exists $self->{storage}) {
228 1     1   385 use Net::Google::SafeBrowsing2::Storage;
  1         1  
  1         5973  
229 1         8 $self->{storage} = Net::Google::SafeBrowsing2::Storage->new();
230             }
231 1 50       4 if (ref $self->{list} ne 'ARRAY') {
232 0         0 $self->{list} = [$self->{list}];
233             }
234              
235 1 50       6 bless $self, $class or croak "Can't bless $class: $!";
236 1         2 return $self;
237             }
238              
239              
240             =head1 PUBLIC FUNCTIONS
241              
242             =over 4
243              
244             =back
245              
246             =head2 update()
247              
248             Perform a database update.
249              
250             $gsb->update();
251              
252             Return the status of the update (see the list of constants above): INTERNAL_ERROR, SERVER_ERROR, NO_UPDATE, NO_DATA or SUCCESSFUL
253              
254             This function can handle two lists at the same time. If one of the list should not be updated, it will automatically skip it and update the other one. It is faster to update two lists at once rather than doing them one by one.
255              
256             NOTE: If you start with an empty database, you will need to perform several updates to retrieve all the Google Safe Browsing information. This may require up to 24 hours. This is a limitation of the Google API, not of this module. See "Google Safe Browsing v2: Implementation Notes" at L.
257              
258              
259             Arguments
260              
261             =over 4
262              
263             =item list
264              
265             Optional. Update a specific list. Use the list(s) from new() by default.
266              
267             =item mac
268              
269             Optional. Set to 1 to enable Message Authentication Code (MAC). Use the value from new() by default.
270              
271             =item force
272              
273             Optional. Force the update (1). Disabled by default (0).
274              
275             Be careful if you set this option to 1 as too frequent updates might result in the blacklisting of your API key.
276              
277             =back
278              
279             =cut
280              
281             sub update {
282 0     0 1 0 my ($self, %args) = @_;
283             # my @lists = @{[$args{list}]} || @{$self->{list}} || croak "Missing list name\n";
284 0         0 my $list = $args{list};
285 0   0     0 my $force = $args{force} || 0;
286 0   0     0 my $mac = $args{mac} || $self->{mac} || 0;
287              
288              
289 0         0 my @lists = @{$self->{list}};
  0         0  
290 0 0       0 @lists = @{[$args{list}]} if (defined $list);
  0         0  
291              
292 0         0 my $result = 0;
293              
294             # Too early to update?
295 0         0 my $start = time();
296 0         0 my $i = 0;
297 0         0 while ($i < scalar @lists) {
298 0         0 my $list = $lists[$i];
299 0         0 my $info = $self->{storage}->last_update(list => $list);
300            
301 0 0 0     0 if ($info->{'time'} + $info->{'wait'} > time && $force == 0) {
302 0         0 $self->debug("Too early to update $list\n");
303 0         0 splice(@lists, $i, 1);
304             }
305             else {
306 0         0 $self->debug("OK to update $list: " . time() . "/" . ($info->{'time'} + $info->{'wait'}) . "\n");
307 0         0 $i++;
308             }
309             }
310              
311 0 0       0 if (scalar @lists == 0) {
312 0         0 $self->debug("Too early to update any list\n");
313 0         0 return NO_UPDATE;
314             }
315 0         0 $self->perf("OK to update check: " . (time() - $start) . "s\n");
316            
317             # MAC?
318 0         0 my $client_key = '';
319 0         0 my $wrapped_key = '';
320              
321 0 0       0 if ($mac) {
322 0         0 ($client_key, $wrapped_key) = $self->get_mac_keys();
323              
324 0 0 0     0 if ($client_key eq '' || $wrapped_key eq '') {
325 0         0 return MAC_KEY_ERROR;
326             }
327             }
328            
329              
330              
331 0         0 my $ua = $self->ua;
332              
333 0         0 my $url = $self->{server} . "downloads?client=api&apikey=" . $self->{key} . "&appver=$VERSION&pver=" . $self->{version};
334 0 0       0 $url .= "&wrkey=$wrapped_key" if ($mac);
335              
336 0         0 my $body = '';
337 0         0 foreach my $list (@lists) {
338             # Report existng chunks
339 0         0 $start = time();
340 0         0 my $a_range = $self->create_range(numbers => [$self->{storage}->get_add_chunks_nums(list => $list)]);
341 0         0 my $s_range = $self->create_range(numbers => [$self->{storage}->get_sub_chunks_nums(list => $list)]);
342 0         0 $self->perf("Create add and sub ranges: " . (time() - $start) . "s\n");
343            
344 0         0 my $chunks_list = '';
345 0 0       0 if ($a_range ne '') {
346 0         0 $chunks_list .= "a:$a_range";
347             }
348 0 0       0 if ($s_range ne '') {
349 0 0       0 $chunks_list .= ":" if ($a_range ne '');
350 0         0 $chunks_list .= "s:$s_range";
351             }
352              
353 0         0 $body .= "$list;$chunks_list";
354 0 0       0 $body .= ":mac" if ($mac);
355 0         0 $body .= "\n";
356             }
357              
358 0         0 my $start_req = time();
359 0         0 my $res = $ua->post($url, Content => $body);
360 0         0 $self->perf("$body\n");
361              
362             # $self->debug($res->request->as_string . "\n" . $res->as_string . "\n");
363 0 0       0 $self->debug($res->request->as_string . "\n") if ($self->{debug});
364 0 0       0 $self->debug($res->as_string . "\n") if ($self->{debug});
365 0         0 my $duration_req = time() - $start_req;
366              
367 0 0       0 if (! $res->is_success) {
368 0         0 $self->error("Request failed\n");
369              
370 0         0 foreach my $list (@lists) {
371 0         0 $self->update_error('time' => time(), list => $list);
372             }
373              
374 0         0 return SERVER_ERROR;
375             }
376              
377 0         0 my $last_update = time;
378 0         0 my $wait = 0;
379              
380 0         0 my @redirections = ();
381 0         0 my $del_add_duration = 0;
382 0         0 my $del_sub_duration = 0;
383 0         0 my $add_range_info = '';
384              
385 0         0 my @lines = split/\s/, $res->decoded_content;
386 0         0 $list = '';
387 0         0 foreach my $line (@lines) {
388 0 0 0     0 if ($line =~ /n:\s*(\d+)\s*$/) {
    0 0        
    0          
    0          
    0          
    0          
    0          
    0          
    0          
389 0         0 $self->debug("Next poll: $1 seconds\n");
390 0         0 $wait = $1;
391             }
392             elsif ($line =~ /i:\s*(\S+)\s*$/) {
393 0         0 $self->debug("List: $1\n");
394 0         0 $list = $1;
395             }
396             elsif ($line =~ /u:\s*(\S+),(\S+)\s*$/) {
397 0         0 $self->debug("Redirection: $1\n");
398 0         0 $self->debug("MAC: $2\n");
399 0         0 push(@redirections, [$1, $list, $2]);
400             }
401             elsif ($line =~ /u:\s*(\S+)\s*$/) {
402 0         0 $self->debug("Redirection: $1\n");
403 0         0 push(@redirections, [$1, $list, '']);
404             }
405             elsif ($line =~ /ad:(\S+)$/) {
406 0         0 $self->debug("Delete Add Chunks: $1\n");
407              
408 0         0 my $del_add_start = time();
409 0         0 $add_range_info = $1 . " $list";
410 0         0 my @nums = $self->expand_range(range => $1);
411 0         0 $self->{storage}->delete_add_ckunks(chunknums => [@nums], list => $list);
412              
413             # Delete full hash as well
414 0         0 $self->{storage}->delete_full_hashes(chunknums => [@nums], list => $list);
415 0         0 $del_add_duration = time() - $del_add_start;
416              
417 0         0 $result = 1;
418             }
419             elsif ($line =~ /sd:(\S+)$/) {
420 0         0 $self->debug("Delete Sub Chunks: $1\n");
421              
422 0         0 my $del_sub_start = time();
423 0         0 my @nums = $self->expand_range(range => $1);
424 0         0 $self->{storage}->delete_sub_ckunks(chunknums => [@nums], list => $list);
425 0         0 $del_add_duration = time() - $del_sub_start;
426              
427 0         0 $result = 1;
428             }
429             elsif ($line =~ /m:(\S+)$/ && $mac) {
430 0         0 my $hmac = $1;
431 0         0 $self->debug("MAC of request: $hmac\n");
432              
433             # Remove this line for data
434 0         0 my $data = $res->decoded_content;
435 0         0 $data =~ s/^m:(\S+)\n//g;
436              
437 0 0       0 if (! $self->validate_data_mac(data => $data, key => $client_key, digest => $hmac) ) {
438 0         0 $self->error("MAC error on main request\n");
439              
440 0         0 return MAC_ERROR;
441             }
442             }
443             elsif ($line =~ /e:pleaserekey/ && $mac) {
444 0         0 $self->debug("MAC key has been expired\n");
445              
446 0         0 $self->{storage}->delete_mac_keys();
447 0         0 return $self->update(list => $list, force => $force, mac => $mac);
448             }
449             elsif ($line =~ /r:pleasereset/) {
450 0         0 $self->debug("Database must be reset\n");
451              
452 0         0 $self->{storage}->reset(list => $list);
453              
454 0         0 return DATABASE_RESET;
455             }
456             }
457 0         0 $self->debug("\n");
458 0         0 $self->perf("Handle first request: " . (time() - $last_update) . "s (POST: ${duration_req}s, DEL add: ${del_add_duration}s, DEL sub: ${del_sub_duration}s, ADD range: ${add_range_info})\n");
459              
460 0 0       0 $result = 1 if (scalar @redirections > 0);
461              
462 0         0 $self->perf("Parse redirections: ");
463 0         0 foreach my $data (@redirections) {
464 0         0 $start = time();
465 0         0 my $redirection = $data->[0];
466 0         0 $list = $data->[1];
467 0         0 my $hmac = $data->[2];
468              
469 0         0 $self->debug("Checking redirection https://$redirection ($list)\n");
470 0         0 $res = $ua->get("https://$redirection");
471 0 0       0 if (! $res->is_success) {
472 0         0 $self->error("Request to $redirection failed\n");
473              
474 0         0 foreach my $list (@lists) {
475 0         0 $self->update_error('time' => $last_update, list => $list);
476             }
477              
478 0         0 return SERVER_ERROR;
479             }
480            
481 0 0       0 $self->debug(substr($res->as_string, 0, 250) . "\n\n") if ($self->{debug});
482 0 0       0 $self->debug(substr($res->content, 0, 250) . "\n\n") if ($self->{debug});
483            
484 0         0 my $data = $res->content;
485 0 0 0     0 if ($mac && ! $self->validate_data_mac(data => $data, key => $client_key, digest => $hmac) ) {
486 0         0 $self->error("MAC error on redirection\n");
487 0         0 $self->debug("Length of data: " . length($data) . "\n");
488              
489 0         0 return MAC_ERROR;
490             }
491              
492 0         0 my $result = $self->parse_data(data => $data, list => $list);
493 0 0       0 if ($result != SUCCESSFUL) {
494 0         0 foreach my $list (@lists) {
495 0         0 $self->update_error('time' => $last_update, list => $list);
496             }
497              
498 0         0 return $result;
499             }
500 0         0 $self->perf((time() - $start) . "s ");
501             }
502 0         0 $self->perf("\n");
503              
504 0         0 foreach my $list (@lists) {
505 0         0 $self->debug("List update: $last_update $wait $list\n");
506 0         0 $self->{storage}->updated('time' => $last_update, 'wait' => $wait, list => $list);
507             }
508              
509 0         0 return $result; # ok
510             }
511              
512             =head2 import_chunks()
513              
514             Import add and sub chunks from a file.
515              
516             my $result = $gsb->import_chunks(list => MALWARE, file => 'malware.dat');
517              
518             Return the status of the import: INTERNAL_ERROR or SUCCESSFUL.
519              
520             This function should be used to initialize an empty back-end storage.
521              
522              
523             Arguments
524              
525             =over 4
526              
527             =item list
528              
529             Required. Google list to use.
530              
531             =item file
532              
533             Required. File that contains the list of chunks. This file can be created with the C function inherited from C.
534              
535             =back
536              
537             =cut
538              
539             sub import_chunks {
540 0     0 1 0 my ($self, %args) = @_;
541 0   0     0 my $list = $args{list} || '';
542 0   0     0 my $file = $args{file} || "$list.dat";
543              
544 0         0 my $data = read_file($file, { binmode => ':raw' });
545              
546 0         0 return $self->parse_data(data => $data, list => $list);
547              
548             }
549              
550             =head2 lookup()
551              
552             Lookup a URL against the Google Safe Browsing database.
553              
554             my $match = $gsb->lookup(url => 'http://www.gumblar.cn');
555              
556             Returns the name of the list if there is any match, returns an empty string otherwise.
557              
558             Arguments
559              
560             =over 4
561              
562             =item list
563              
564             Optional. Lookup against a specific list. Use the list(s) from new() by default.
565              
566             =item url
567              
568             Required. URL to lookup.
569              
570             =back
571              
572             =cut
573              
574             sub lookup {
575 0     0 1 0 my ($self, %args) = @_;
576 0   0     0 my $list = $args{list} || '';
577 0   0     0 my $url = $args{url} || return '';
578              
579 0         0 my @lists = @{$self->{list}};
  0         0  
580 0 0       0 @lists = @{[$args{list}]} if ($list ne '');
  0         0  
581              
582              
583             # TODO: create our own URI management for canonicalization
584             # fix for http:///foo.com (3 ///)
585 0         0 $url =~ s/^(https?:\/\/)\/+/$1/;
586              
587              
588              
589 0         0 my $uri = URI->new($url)->canonical;
590              
591 0         0 my $domain = $uri->host;
592            
593 0         0 my @hosts = $self->canonical_domain_suffixes($domain); # only top-3 in this case
594              
595 0         0 foreach my $host (@hosts) {
596 0         0 $self->debug("Domain for key: $domain => $host\n");
597 0         0 my $suffix = $self->prefix("$host/"); # Don't forget trailing hash
598 0         0 $self->debug("Host key: " . $self->hex_to_ascii($suffix) . "\n");
599              
600 0         0 my $match = $self->lookup_suffix(lists => [@lists], url => $url, suffix => $suffix);
601 0 0       0 return $match if ($match ne '');
602             }
603              
604 0         0 return '';
605             }
606              
607              
608              
609             =head2 get_lists()
610              
611             Returns the name of all the Google Safe Browsing lists
612              
613             my $@lists = $gsb->get_lists();
614              
615             NOTE: this function is useless in practice because Google includes some lists which cannot be used by the Google Safe Browsing API, like lists used by the Google toolbar.
616              
617             =cut
618              
619             sub get_lists {
620 0     0 1 0 my ($self, %args) = @_;
621              
622 0         0 my $url = $self->{server} . "list?client=api&apikey=" . $self->{key} . "&appver=$VERSION&pver=" . $self->{version};
623              
624 0         0 my $res = $self->ua->get($url);
625              
626 0         0 return split/\s/, $res->decoded_content; # 1 list per line
627             }
628              
629              
630             =head2 last_error()
631              
632             Get/Set the last error message.
633              
634             print "Last error: ", $gsb->last_error(), "\n";
635             $gsb->last_error(''); # Reset last error
636              
637             NOTE: the last error message might not come from the last call. Returns an empty string if no errors.
638              
639             =cut
640              
641             sub last_error {
642 0     0 1 0 my ($self, $message) = @_;
643              
644 0 0       0 if (defined $message) {
645 0         0 $self->{last_error} = $message;
646             }
647             else {
648 0         0 return $self->{last_error};
649             }
650             }
651              
652              
653             =pod
654              
655             =head1 PRIVATE FUNCTIONS
656              
657             These functions are not intended to be used externally.
658              
659             =over 4
660              
661             =back
662              
663             =head2 lookup_suffix()
664              
665             Lookup a host prefix.
666              
667             =cut
668              
669             sub lookup_suffix {
670 0     0 1 0 my ($self, %args) = @_;
671 0   0     0 my $lists = $args{lists} || croak "Missing lists\n";
672 0   0     0 my $url = $args{url} || return '';
673 0   0     0 my $suffix = $args{suffix} || return '';
674              
675             # Calculate prefixes
676 0         0 my @full_hashes = $self->full_hashes($url); # Get the prefixes from the first 4 bytes
677 0         0 my @full_hashes_prefix = map (substr($_, 0, 4), @full_hashes);
678              
679             # Local lookup
680 0         0 my @add_chunks = $self->local_lookup_suffix(lists => $lists, url => $url, suffix => $suffix, full_hashes_prefix => [@full_hashes_prefix]);
681 0 0       0 if (scalar @add_chunks == 0) {
682 0         0 $self->debug("No hit in local lookup\n");
683 0         0 return '';
684             }
685              
686              
687             # Check against full hashes
688 0         0 my $found = '';
689              
690             # get stored full hashes
691 0         0 foreach my $add_chunk (@add_chunks) {
692            
693 0         0 my @hashes = $self->{storage}->get_full_hashes( chunknum => $add_chunk->{chunknum}, timestamp => time() - FULL_HASH_TIME, list => $add_chunk->{list});
694              
695 0         0 $self->debug("Full hashes already stored for chunk " . $add_chunk->{chunknum} . ": " . scalar @hashes . "\n");
696 0         0 foreach my $full_hash (@full_hashes) {
697 0         0 foreach my $hash (@hashes) {
698 0 0 0 0   0 if ($hash eq $full_hash && defined first { $add_chunk->{list} eq $_ } @$lists) {
  0         0  
699 0         0 $self->debug("Full hash was found in storage: " . $self->hex_to_ascii($hash) . "\n");
700 0         0 $found = $add_chunk->{list};
701 0         0 last;
702             }
703             # elsif ($hash ne $full_hash) {
704             # $self->debug($self->hex_to_ascii($hash) . " ne " . $self->hex_to_ascii($full_hash) . "\n\n");
705             # }
706             }
707 0 0       0 last if ($found ne '');
708             }
709 0 0       0 last if ($found ne '');
710             }
711              
712 0 0       0 return $found if ($found ne '');
713              
714              
715             # ask for new hashes
716             # TODO: make sure we don't keep asking for the same over and over
717 0   0     0 my @hashes = $self->request_full_hash(prefixes => [ map($_->{prefix} || $_->{hostkey}, @add_chunks) ]);
718 0         0 $self->{storage}->add_full_hashes(full_hashes => [@hashes], timestamp => time());
719              
720 0         0 foreach my $full_hash (@full_hashes) {
721 0     0   0 my $hash = first { $_->{hash} eq $full_hash} @hashes;
  0         0  
722 0 0       0 next if (! defined $hash);
723              
724 0     0   0 my $list = first { $hash->{list} eq $_ } @$lists;
  0         0  
725              
726 0 0 0     0 if (defined $hash && defined $list) {
727             # $self->debug($self->hex_to_ascii($hash->{hash}) . " eq " . $self->hex_to_ascii($full_hash) . "\n\n");
728              
729 0         0 $self->debug("Match: " . $self->hex_to_ascii($full_hash) . "\n");
730              
731 0         0 return $hash->{list};
732             }
733             # elsif (defined $hash) {
734             # $self->debug("hash: " . $self->hex_to_ascii($hash->{hash}) . "\n");
735             # $self->debug("list: " . $hash->{list} . "\n");
736             # }
737             }
738            
739 0         0 $self->debug("No match\n");
740 0         0 return '';
741             }
742              
743             =head2 local_lookup_suffix()
744              
745             Lookup a host prefix in the local database only.
746              
747             =cut
748             sub local_lookup_suffix {
749 0     0 1 0 my ($self, %args) = @_;
750 0   0     0 my $lists = $args{lists} || croak "Missing lists\n";
751 0   0     0 my $url = $args{url} || return ();
752 0   0     0 my $suffix = $args{suffix} || return ();
753 0   0     0 my $full_hashe_list = $args{full_hashes} || [];
754 0   0     0 my $full_hashes_prefix_list = $args{full_hashes_prefix} || [];
755              
756              
757             # Step 1: get all add chunks for this host key
758             # Do it for all lists
759 0         0 my @add_chunks = $self->{storage}->get_add_chunks(hostkey => $suffix);
760             # return scalar @add_chunks;
761 0 0       0 if (scalar @add_chunks == 0) { # no match
762 0         0 $self->debug("No host key\n");
763 0         0 return @add_chunks;
764             }
765              
766             # Step 2: calculate prefixes if not provided
767             # Get the prefixes from the first 4 bytes
768 0         0 my @full_hashes_prefix = @{$full_hashes_prefix_list};
  0         0  
769 0 0       0 if (scalar @full_hashes_prefix == 0) {
770 0         0 my @full_hashes = @{$full_hashe_list};
  0         0  
771 0 0       0 @full_hashes = $self->full_hashes($url) if (scalar @full_hashes == 0);
772              
773 0         0 @full_hashes_prefix = map (substr($_, 0, 4), @full_hashes);
774             }
775              
776             # Step 3: filter out add_chunks with prefix
777 0         0 my $i = 0;
778 0         0 while ($i < scalar @add_chunks) {
779 0 0       0 if ($add_chunks[$i]->{prefix} ne '') {
780 0         0 my $found = 0;
781 0         0 foreach my $hash_prefix (@full_hashes_prefix) {
782 0 0       0 if ( $add_chunks[$i]->{prefix} eq $hash_prefix) {
783 0         0 $found = 1;
784 0         0 last;
785             }
786             # else {
787             # $self->debug( $self->hex_to_ascii($add_chunks[$i]->{prefix}) . " ne " . $self->hex_to_ascii($hash_prefix) . "\n" );
788             # }
789             }
790              
791 0 0       0 if ($found == 0) {
792 0         0 $self->debug("No prefix found\n");
793 0         0 splice(@add_chunks, $i, 1);
794             }
795             else {
796 0         0 $i++;
797             }
798             }
799             else {
800 0         0 $i++;
801             }
802             }
803 0 0       0 if (scalar @add_chunks == 0) {
804 0         0 $self->debug("No prefix match for any host key\n");
805 0         0 return @add_chunks;
806             }
807              
808              
809             # Step 4: get all sub chunks for this host key
810 0         0 my @sub_chunks = $self->{storage}->get_sub_chunks(hostkey => $suffix);
811              
812 0         0 foreach my $sub_chunk (@sub_chunks) {
813 0         0 my $i = 0;
814 0         0 while ($i < scalar @add_chunks) {
815 0         0 my $add_chunk = $add_chunks[$i];
816              
817 0 0 0     0 if ($add_chunk->{chunknum} != $sub_chunk->{addchunknum} || $add_chunk->{list} ne $sub_chunk->{list}) {
818 0         0 $i++;
819 0         0 next;
820             }
821              
822 0 0       0 if ($sub_chunk->{prefix} eq $add_chunk->{prefix}) {
823 0         0 splice(@add_chunks, $i, 1);
824             }
825             else {
826 0         0 $i++;
827             }
828             }
829             }
830              
831 0 0       0 if (scalar @add_chunks == 0) {
832 0         0 $self->debug("All add_chunks have been removed by sub_chunks\n");
833             }
834              
835 0         0 return @add_chunks;
836             }
837              
838             =head2 local_lookup()
839              
840             Lookup a URL against the local Google Safe Browsing database URL. This should be used for debugging purpose only. See the lookup for normal use.
841              
842             my $match = $gsb->local_lookup(url => 'http://www.gumblar.cn');
843              
844             Returns the name of the list if there is any match, returns an empty string otherwise.
845              
846             Arguments
847              
848             =over 4
849              
850             =item list
851              
852             Optional. Lookup against a specific list. Use the list(s) from new() by default.
853              
854             =item url
855              
856             Required. URL to lookup.
857              
858             =back
859              
860             =cut
861             sub local_lookup {
862 0     0 1 0 my ($self, %args) = @_;
863 0   0     0 my $list = $args{list} || '';
864 0   0     0 my $url = $args{url} || return '';
865              
866 0         0 my @lists = @{$self->{list}};
  0         0  
867 0 0       0 @lists = @{[$args{list}]} if ($list ne '');
  0         0  
868              
869              
870             # TODO: create our own URI management for canonicalization
871             # fix for http:///foo.com (3 ///)
872 0         0 $url =~ s/^(https?:\/\/)\/+/$1/;
873              
874              
875              
876 0         0 my $uri = URI->new($url)->canonical;
877              
878 0         0 my $domain = $uri->host;
879            
880 0         0 my @hosts = $self->canonical_domain_suffixes($domain); # only top-3 in this case
881              
882 0         0 foreach my $host (@hosts) {
883 0         0 $self->debug("Domain for key: $domain => $host\n");
884 0         0 my $suffix = $self->prefix("$host/"); # Don't forget trailing hash
885 0         0 $self->debug("Host key: " . $self->hex_to_ascii($suffix) . "\n");
886              
887 0         0 my @matches = $self->local_lookup_suffix(lists => [@lists], url => $url, suffix => $suffix);
888             # return $matches[0]->{list} if (scalar @matches > 0);
889 0 0       0 return $matches[0]->{list} . " " . $matches[0]->{chunknum} if (scalar @matches > 0);
890             }
891              
892 0         0 return '';
893              
894             }
895              
896             =head2 request_key()
897              
898             Request the Message Authentication Code (MAC) keys
899              
900             =cut
901              
902             sub get_mac_keys {
903 0     0 0 0 my ($self, %args) = @_;
904              
905 0         0 my $keys = $self->{storage}->get_mac_keys();
906              
907 0 0 0     0 if ($keys->{client_key} eq '' || $keys->{wrapped_key} eq '') {
908 0         0 my ($client_key, $wrapped_key) = $self->request_mac_keys();
909              
910             # $self->debug("Client key: $client_key\n");
911 0         0 $self->{storage}->add_mac_keys(client_key => $client_key, wrapped_key => $wrapped_key);
912              
913 0         0 return ($client_key, $wrapped_key);
914             }
915              
916 0         0 return ($keys->{client_key}, $keys->{wrapped_key});
917             }
918              
919              
920             =head2 request_mac_keys()
921              
922             Request the Message Authentication Code (MAC) keys from Google.
923              
924             =cut
925              
926             sub request_mac_keys {
927 0     0 1 0 my ($self, %args) = @_;
928              
929 0         0 my $client_key = '';
930 0         0 my $wrapped_key = '';
931              
932 0         0 my $url = $self->{mac_server} . "newkey?client=api&apikey=" . $self->{key} . "&appver=$VERSION&pver=" . $self->{version};
933              
934 0         0 my $res = $self->ua->get($url);
935              
936 0 0       0 if (! $res->is_success) {
937 0         0 $self->error("Key request failed: " . $res->code . "\n");
938 0         0 return ($client_key, $wrapped_key);
939             }
940              
941            
942              
943 0         0 my $data = $res->decoded_content;
944 0 0       0 if ($data =~ s/^clientkey:(\d+)://mi) {
945 0         0 my $length = $1;
946 0         0 $self->debug("MAC client key length: $length\n");
947 0         0 $client_key = substr($data, 0, $length, '');
948 0         0 $self->debug("MAC client key: $client_key\n");
949              
950 0         0 substr($data, 0, 1, ''); # remove \n
951              
952 0 0       0 if ($data =~ s/^wrappedkey:(\d+)://mi) {
953 0         0 $length = $1;
954 0         0 $self->debug("MAC wrapped key length: $length\n");
955 0         0 $wrapped_key = substr($data, 0, $length, '');
956 0         0 $self->debug("MAC wrapped key: $wrapped_key\n");
957              
958 0         0 return (decode_base64($client_key), $wrapped_key);
959             }
960             else {
961 0         0 return ('', '');
962             }
963             }
964              
965 0         0 return ($client_key, $wrapped_key);
966             }
967              
968             =head2 validate_data_mac()
969              
970             Validate data against the MAC keys.
971              
972             =cut
973              
974             sub validate_data_mac {
975 0     0 1 0 my ($self, %args) = @_;
976 0   0     0 my $data = $args{data} || '';
977 0   0     0 my $key = $args{key} || '';
978 0   0     0 my $digest = $args{digest} || '';
979              
980              
981             # my $hash = urlsafe_b64encode trim hmac_sha1($data, decode_base64($key));
982             # my $hash = urlsafe_b64encode (trim (hmac_sha1($data, decode_base64($key))));
983 0         0 my $hash = urlsafe_b64encode(hmac_sha1($data, $key));
984 0         0 $hash .= '=';
985              
986 0         0 $self->debug("$hash / $digest\n");
987             # $self->debug(urlsafe_b64encode(hmac_sha1($data, decode_base64($key))) . "\n");
988             # $self->debug(urlsafe_b64encode(trim(hmac_sha1($data, decode_base64($key)))) . "\n");
989              
990 0         0 return ($hash eq $digest);
991             }
992              
993             =head2 update_error()
994              
995             Handle server errors during a database update.
996              
997             =cut
998              
999             sub update_error {
1000 0     0 1 0 my ($self, %args) = @_;
1001 0   0     0 my $time = $args{'time'} || time;
1002 0   0     0 my $list = $args{'list'} || '';
1003              
1004 0         0 my $info = $self->{storage}->last_update(list => $list);
1005 0 0       0 $info->{errors} = 0 if (! exists $info->{errors});
1006 0         0 my $errors = $info->{errors} + 1;
1007 0         0 my $wait = 0;
1008              
1009 0 0       0 $wait = $errors == 1 ? 60
    0          
    0          
    0          
    0          
    0          
1010             : $errors == 2 ? int(30 * 60 * (rand(1) + 1)) # 30-60 mins
1011             : $errors == 3 ? int(60 * 60 * (rand(1) + 1)) # 60-120 mins
1012             : $errors == 4 ? int(2 * 60 * 60 * (rand(1) + 1)) # 120-240 mins
1013             : $errors == 5 ? int(4 * 60 * 60 * (rand(1) + 1)) # 240-480 mins
1014             : $errors > 5 ? 480 * 60
1015             : 0;
1016              
1017 0         0 $self->{storage}->update_error('time' => $time, list => $list, 'wait' => $wait, errors => $errors);
1018              
1019             }
1020              
1021              
1022             =head2 lookup_whitelist()
1023              
1024             Lookup a host prefix and suffix in the whitelist (s chunks)
1025              
1026             =cut
1027              
1028             sub lookup_whitelist {
1029 0     0 1 0 my ($self, %args) = @_;
1030 0   0     0 my $suffix = $args{suffix} || return 0;
1031 0   0     0 my $prefix = $args{prefix} || '';
1032 0   0     0 my $chuknum = $args{chunknum} || return 0;
1033              
1034              
1035 0         0 foreach my $schunknum (keys %{ $self->{s_chunks} }) {
  0         0  
1036 0         0 foreach my $chunk ( @{ $self->{s_chunks}->{$schunknum} }) {
  0         0  
1037 0 0 0     0 if ($chunk->{host} eq $suffix && ($chunk->{prefix} eq $prefix || $chunk->{prefix} eq '') && $chunk->{add_chunknum} == $chuknum) {
      0        
      0        
1038 0         0 return 1;
1039             }
1040             }
1041             }
1042              
1043 0         0 return 0;
1044             }
1045              
1046              
1047             =head2 ua()
1048              
1049             Create LWP::UserAgent to make HTTP requests to Google.
1050              
1051             =cut
1052              
1053             sub ua {
1054 0     0 1 0 my ($self, %args) = @_;
1055              
1056 0 0       0 if (! exists $self->{ua}) {
1057 0         0 my $ua = LWP::UserAgent->new;
1058 0         0 $ua->timeout(60);
1059              
1060 0         0 $self->{ua} = $ua;
1061             }
1062              
1063 0         0 return $self->{ua};
1064             }
1065              
1066              
1067             =head2 parse_data()
1068              
1069             Parse data from a rediration (add and sub chunk information).
1070              
1071             =cut
1072              
1073             sub parse_data {
1074 0     0 1 0 my ($self, %args) = @_;
1075 0   0     0 my $data = $args{data} || '';
1076 0   0     0 my $list = $args{list} || '';
1077              
1078 0         0 my $chunk_num = 0;
1079 0         0 my $hash_length = 0;
1080 0         0 my $chunk_length = 0;
1081              
1082 0         0 while (length $data > 0) {
1083             # print "Length 1: ", length $data, "\n"; # 58748
1084            
1085 0         0 my $type = substr($data, 0, 2, ''); # s:34321:4:137
1086             # print "Length 1.5: ", length $data, "\n"; # 58746 -2
1087            
1088 0 0       0 if ($data =~ /^(\d+):(\d+):(\d+)\n/sgi) {
1089 0         0 $chunk_num = $1;
1090 0         0 $hash_length = $2;
1091 0         0 $chunk_length = $3;
1092            
1093             # shorten data
1094 0         0 substr($data, 0, length($chunk_num) + length($hash_length) + length($chunk_length) + 3, '');
1095             # print "Remove ", length($chunk_num) + length($hash_length) + length($chunk_length) + 3, "\n";
1096             # print "Length 2: ", length $data, "\n"; # 58741 -5
1097            
1098 0         0 my $encoded = substr($data, 0, $chunk_length, '');
1099             # print "Length 3: ", length $data, "\n"; # 58604 -137
1100            
1101 0 0       0 if ($type eq 's:') {
    0          
1102 0         0 my @chunks = $self->parse_s(value => $encoded, hash_length => $hash_length);
1103              
1104 0         0 $self->{storage}->add_chunks(type => 's', chunknum => $chunk_num, chunks => [@chunks], list => $list); # Must happen all at once => not 100% sure
1105             }
1106             elsif ($type eq 'a:') {
1107 0         0 my @chunks = $self->parse_a(value => $encoded, hash_length => $hash_length);
1108 0         0 $self->{storage}->add_chunks(type => 'a', chunknum => $chunk_num, chunks => [@chunks], list => $list); # Must happen all at once => not 100% sure
1109             }
1110             else {
1111 0         0 $self->error("Incorrect chunk type: $type, should be a: or s:\n");
1112 0         0 return INTERNAL_ERROR;# failed
1113             }
1114            
1115 0         0 $self->debug("$type$chunk_num:$hash_length:$chunk_length OK\n");
1116            
1117             }
1118             else {
1119 0         0 $self->error("could not parse header\n");
1120 0         0 return INTERNAL_ERROR;# failed
1121             }
1122             }
1123              
1124 0         0 return SUCCESSFUL;
1125             }
1126              
1127              
1128             =head2 parse_s()
1129              
1130             Parse s chunks information for a database update.
1131              
1132             =cut
1133              
1134             sub parse_s {
1135 0     0 1 0 my ($self, %args) = @_;
1136 0   0     0 my $value = $args{value} || return ();
1137 0   0     0 my $hash_length = $args{hash_length} || 4;
1138              
1139 0         0 my @data = ();
1140              
1141              
1142 0         0 while (length $value > 0) {
1143             # my $host = $self->hex_to_ascii( substr($value, 0, 4, '') ); # Host hash
1144 0         0 my $host = substr($value, 0, 4, ''); # HEX
1145             # print "\t Host key: $host\n";
1146              
1147 0         0 my $count = substr($value, 0, 1, ''); # hex value
1148 0         0 $count = ord($count);
1149              
1150             # my $add_chunk_num_hex;
1151              
1152 0 0       0 if ($count == 0) { # ADDCHUNKNUM only
1153             # $self->debug("\nadd_chuknum: " . substr($value, 0, 4) . " => ");
1154 0         0 my $add_chunknum = hex($self->hex_to_ascii( substr($value, 0, 4, '') ) ); #chunk num
1155             # $self->debug("$add_chunknum\n");
1156              
1157 0         0 push(@data, { host => $host, add_chunknum => $add_chunknum, prefix => '' });
1158              
1159 0 0       0 if ($self->{debug}) {
1160 0         0 $self->debug("\t" . $self->hex_to_ascii($host) . " $add_chunknum\n");
1161             }
1162             }
1163             else { # ADDCHUNKNUM + PREFIX
1164 0         0 for(my $i = 0; $i < $count; $i++) {
1165             # my $add_chunknum = $self->hex_to_ascii( substr($value, 0, 4, '') ); #chunk num - ACII
1166             # $self->debug("\nadd_chuknum: " . substr($value, 0, 4) . " => ");
1167 0         0 my $add_chunknum = hex($self->hex_to_ascii( substr($value, 0, 4, '') )); # DEC
1168             # $self->debug("$add_chunknum\n");
1169              
1170             # my $prefix = $self->hex_to_ascii( substr($value, 0, $hash_length, '') ); # ASCII
1171 0         0 my $prefix = substr($value, 0, $hash_length, ''); # HEX
1172              
1173 0         0 push(@data, { host => $host, add_chunknum => $add_chunknum, prefix => $prefix });
1174              
1175 0 0       0 if ($self->{debug}) {
1176 0         0 $self->debug("\t" . $self->hex_to_ascii($host) . " $add_chunknum " . $self->hex_to_ascii($prefix) . "\n");
1177             }
1178             }
1179             }
1180             }
1181              
1182 0         0 return @data;
1183             }
1184              
1185              
1186             =head2 parse_a()
1187              
1188             Parse a chunks information for a database update.
1189              
1190             =cut
1191              
1192             sub parse_a {
1193 0     0 1 0 my ($self, %args) = @_;
1194 0   0     0 my $value = $args{value} || return ();
1195 0   0     0 my $hash_length = $args{hash_length} || 4;
1196              
1197 0         0 my @data = ();
1198              
1199              
1200 0         0 while (length $value > 0) {
1201             # my $host = $self->hex_to_ascii( substr($value, 0, 4, '') ); # Host hash
1202 0         0 my $host = substr($value, 0, 4, ''); # HEX
1203             # print "\t Host key: $host\n";
1204              
1205 0         0 my $count = substr($value, 0, 1, ''); # hex value
1206 0         0 $count = ord($count);
1207              
1208              
1209 0 0       0 if ($count > 0) { # ADDCHUNKNUM only
1210 0         0 for(my $i = 0; $i < $count; $i++) {
1211             # my $prefix = $self->hex_to_ascii( substr($value, 0, $hash_length, '') ); # ASCII
1212 0         0 my $prefix = substr($value, 0, $hash_length, ''); # HEX
1213              
1214 0         0 push(@data, { host => $host, prefix => $prefix });
1215              
1216 0 0       0 if ($self->{debug}) {
1217 0         0 $self->debug("\t" . $self->hex_to_ascii($host) . " " . $self->hex_to_ascii($prefix) . "\n");
1218             }
1219             }
1220             }
1221             else {
1222 0         0 push(@data, { host => $host, prefix => '' });
1223              
1224 0 0       0 if ($self->{debug}) {
1225 0         0 $self->debug("\t" . $self->hex_to_ascii($host) . "\n");
1226             }
1227             }
1228             }
1229              
1230 0         0 return @data;
1231             }
1232              
1233              
1234             =head2 hex_to_ascii()
1235              
1236             Transform hexadecimal strings to printable ASCII strings. Used mainly for debugging.
1237              
1238             print $gsb->hex_to_ascii('hex value');
1239              
1240             =cut
1241              
1242             sub hex_to_ascii {
1243 2     2 1 6 my ($self, $hex) = @_;
1244              
1245 2         5 return String::HexConvert::ascii_to_hex($hex);
1246             # my $ascii = '';
1247             #
1248             # while (length $hex > 0) {
1249             # $ascii .= sprintf("%02x", ord( substr($hex, 0, 1, '') ) );
1250             # }
1251             #
1252             # return $ascii;
1253             }
1254              
1255              
1256             =head2 ascii_to_hex()
1257              
1258             Transform ASCII strings to hexadecimal strings.
1259              
1260             =cut
1261              
1262             sub ascii_to_hex {
1263 4     4 1 315 my ($self, $ascii) = @_;
1264              
1265 4         4 my $hex = '';
1266 4         17 for (my $i = 0; $i < int(length($ascii) / 2); $i++) {
1267 16         37 $hex .= chr hex( substr($ascii, $i * 2, 2) );
1268             }
1269              
1270 4         14 return $hex;
1271             }
1272              
1273             =head2 debug()
1274              
1275             Print debug output.
1276              
1277             =cut
1278              
1279             sub debug {
1280 0     0 1 0 my ($self, $message) = @_;
1281              
1282 0 0       0 print $message if ($self->{debug} > 0);
1283             }
1284              
1285              
1286             =head2 error()
1287              
1288             Print error message.
1289              
1290             =cut
1291              
1292             sub error {
1293 0     0 1 0 my ($self, $message) = @_;
1294              
1295 0 0 0     0 print "ERROR - ", $message if ($self->{debug} > 0 || $self->{errors} > 0);
1296 0         0 $self->{last_error} = $message;
1297             }
1298              
1299              
1300             =head2 error()
1301              
1302             Print performance message.
1303              
1304             =cut
1305              
1306             sub perf {
1307 0     0 1 0 my ($self, $message) = @_;
1308              
1309 0 0       0 print $message if ($self->{perf} > 0);
1310             }
1311              
1312             =head2 canonical_domain_suffixes()
1313              
1314             Find all suffixes for a domain.
1315              
1316             =cut
1317              
1318             sub canonical_domain_suffixes {
1319 3     3 1 4246 my ($self, $domain) = @_;
1320              
1321 3         8 my @domains = ();
1322              
1323 3 50       12 if ($domain =~ /^\d+\.\d+\.\d+\.\d+$/) { # loose check for IP address, should be enough
1324 0         0 return ($domain);
1325             }
1326              
1327 3         10 my @parts = split/\./, $domain; # take 3 components
1328 3 100       11 if (scalar @parts >= 3) {
1329 2         7 @parts = splice (@parts, -3, 3);
1330              
1331 2         8 push(@domains, join('.', @parts));
1332              
1333 2         5 splice(@parts, 0, 1);
1334             }
1335              
1336 3         9 push(@domains, join('.', @parts));
1337              
1338 3         12 return @domains;
1339             }
1340              
1341              
1342             =head2 canonical_domain()
1343              
1344             Find all canonical domains a domain.
1345              
1346             =cut
1347              
1348             sub canonical_domain {
1349 3     3 1 58 my ($self, $domain) = @_;
1350              
1351 3         8 my @domains = ($domain);
1352              
1353              
1354 3 50       12 if ($domain =~ /^\d+\.\d+\.\d+\.\d+$/) { # loose check for IP address, should be enough
1355 0         0 return @domains;
1356             }
1357              
1358 3         11 my @parts = split/\./, $domain;
1359 3         6 splice(@parts, 0, -6); # take 5 top most compoments
1360              
1361              
1362 3         9 while (scalar @parts > 2) {
1363 6         7 shift @parts;
1364 6         21 push(@domains, join(".", @parts) );
1365             }
1366              
1367 3         12 return @domains;
1368             }
1369              
1370             =head2 canonical_path()
1371              
1372             Find all canonical paths for a URL.
1373              
1374             =cut
1375              
1376             sub canonical_path {
1377 3     3 1 35 my ($self, $path) = @_;
1378              
1379 3         6 my @paths = ($path); # return full path
1380            
1381 3 100       8 if ($path =~ /\?/) {
1382 1         3 $path =~ s/\?.*$//;
1383              
1384 1         2 push(@paths, $path);
1385             }
1386              
1387 3         8 my @parts = split /\//, $path;
1388 3         5 my $previous = '';
1389 3   66     16 while (scalar @parts > 1 && scalar @paths < 6) {
1390 3         3 my $val = shift(@parts);
1391 3         6 $previous .= "$val/";
1392              
1393 3         7 push(@paths, $previous);
1394             }
1395            
1396 3         9 return @paths;
1397             }
1398              
1399             =head2 canonical()
1400              
1401             Find all canonical URLs for a URL.
1402              
1403             =cut
1404              
1405             sub canonical {
1406 3     3 1 9570 my ($self, $url) = @_;
1407              
1408 3         7 my @urls = ();
1409              
1410             # my $uri = URI->new($url)->canonical;
1411 3         8 my $uri = $self->canonical_uri($url);
1412 3         143 my @domains = $self->canonical_domain($uri->host);
1413 3         14 my @paths = $self->canonical_path($uri->path_query);
1414              
1415 3         6 foreach my $domain (@domains) {
1416 9         10 foreach my $path (@paths) {
1417 20         34 push(@urls, "$domain$path");
1418             }
1419             }
1420              
1421 3         18 return @urls;
1422             }
1423              
1424              
1425             =head2 canonical_uri()
1426              
1427             Create a canonical URI.
1428              
1429             NOTE: URI cannot handle all the test cases provided by Google. This method is a hack to pass most of the test. A few tests are still failing. The proper way to handle URL canonicalization according to Google would be to create a new module to handle URLs. However, I believe most real-life cases are handled correctly by this function.
1430              
1431             =cut
1432              
1433             sub canonical_uri {
1434 39     39 1 13327 my ($self, $url) = @_;
1435              
1436 39         82 $url = trim $url;
1437              
1438             # Special case for \t \r \n
1439 39         702 while ($url =~ s/^([^?]+)[\r\t\n]/$1/sgi) { }
1440              
1441 39         108 my $uri = URI->new($url)->canonical; # does not deal with directory traversing
1442              
1443             # $self->debug("0. $url => " . $uri->as_string . "\n");
1444              
1445            
1446 39 100 66     10992 if (! $uri->scheme() || $uri->scheme() eq '') {
1447 3         24 $uri = URI->new("http://$url")->canonical;
1448             }
1449              
1450 39         979 $uri->fragment('');
1451              
1452 39         372 my $escape = $uri->as_string;
1453              
1454             # Reduce double // to single / in path
1455 39         203 while ($escape =~ s/^([a-z]+:\/\/[^?]+)\/\//$1\//sgi) { }
1456              
1457              
1458             # Remove empty fragment
1459 39         89 $escape =~ s/#$//;
1460              
1461             # canonial does not handle ../
1462             # $self->debug("\t$escape\n");
1463 39         120 while($escape =~ s/([^\/])\/([^\/]+)\/\.\.([\/?].*)$/$1$3/gi) { }
1464 39         60 while($escape =~ s/([^\/])\/([^\/]+)\/\.\.$/$1/gi) { }
1465              
1466             # May have removed ending /
1467             # $self->debug("\t$escape\n");
1468 39 100       108 $escape .= "/" if ($escape =~ /^[a-z]+:\/\/[^\/\?]+$/);
1469 39         86 $escape =~ s/^([a-z]+:\/\/[^\/]+)(\?.*)$/$1\/$2/gi;
1470             # $self->debug("\t$escape\n");
1471              
1472             # other weird case if domain = digits only, try to translate it to IP address
1473 39 100       69 if ((my $domain = URI->new($escape)->host) =~/^\d+$/) {
1474 3         152 my $ip = Socket::inet_ntoa(Socket::inet_aton($domain));
1475              
1476 3         8 $uri = URI->new($escape);
1477 3         90 $uri->host($ip);
1478              
1479 3         146 $escape = $uri->as_string;
1480             }
1481              
1482             # $self->debug("1. $url => $escape\n");
1483              
1484             # Try to escape the path again
1485 39         1686 $url = $escape;
1486 39         60 while (($escape = URI::Escape::uri_unescape($url)) ne $escape) { # wrong for %23 -> #
1487 0         0 $url = $escape;
1488             }
1489             # while (($escape = URI->new($url)->canonical->as_string) ne $escape) { # breask more unit tests than previous
1490             # $url = $escape;
1491             # }
1492              
1493             # Fix for %23 -> #
1494 39         261 while($escape =~ s/#/%23/sgi) { }
1495              
1496             # $self->debug("2. $url => $escape\n");
1497              
1498             # Fix over escaping
1499 39         77 while($escape =~ s/^([^?]+)%%(%.*)$/$1%25%25$2/sgi) { }
1500 39         59 while($escape =~ s/^([^?]+)%%/$1%25%25/sgi) { }
1501              
1502             # URI has issues with % in domains, it gets the host wrong
1503              
1504             # 1. fix the host
1505             # $self->debug("Domain: " . URI->new($escape)->host . "\n");
1506 39         34 my $exception = 0;
1507 39         120 while ($escape =~ /^[a-z]+:\/\/[^\/]*([^a-z0-9%_.-\/:])[^\/]*(\/.*)$/) {
1508 3         4 my $source = $1;
1509 3         17 my $target = sprintf("%02x", ord($source));
1510              
1511 3         45 $escape =~ s/^([a-z]+:\/\/[^\/]*)\Q$source\E/$1%\Q$target\E/;
1512              
1513 3         12 $exception = 1;
1514             }
1515              
1516             # 2. need to parse the path again
1517 39 50 66     77 if ($exception && $escape =~ /^[a-z]+:\/\/[^\/]+\/(.+)/) {
1518 0         0 my $source = $1;
1519 0         0 my $target = URI::Escape::uri_unescape($source);
1520              
1521             # print "Source: $source\n";
1522 0         0 while ($target ne URI::Escape::uri_unescape($target)) {
1523 0         0 $target = URI::Escape::uri_unescape($target);
1524             }
1525              
1526            
1527 0         0 $escape =~ s/\/\Q$source\E/\/$target/;
1528              
1529 0         0 while ($escape =~ s/#/%23/sgi) { } # fragement has been removed earlier
1530 0         0 while ($escape =~ s/^([a-z]+:\/\/[^\/]+\/.*)%5e/$1\&/sgi) { } # not in the host name
1531             # while ($escape =~ s/%5e/&/sgi) { }
1532              
1533 0         0 while ($escape =~ s/%([^0-9a-f]|.[^0-9a-f])/%25$1/sgi) { }
1534             }
1535              
1536             # $self->debug("$url => $escape\n");
1537             # $self->debug(URI->new($escape)->as_string . "\n");
1538              
1539 39         76 return URI->new($escape);
1540             }
1541              
1542             =head2 full_hashes()
1543              
1544             Return all possible full hashes for a URL.
1545              
1546             =cut
1547              
1548             sub full_hashes {
1549 0     0 1 0 my ($self, $url) = @_;
1550              
1551 0         0 my @urls = $self->canonical($url);
1552 0         0 my @hashes = ();
1553              
1554 0         0 foreach my $url (@urls) {
1555             # $self->debug("$url\n");
1556 0         0 push(@hashes, sha256($url));
1557             # $self->debug("$url " . $self->hex_to_ascii(sha256($url)) . "\n");
1558             }
1559              
1560 0         0 return @hashes;
1561             }
1562              
1563             =head2 prefix()
1564              
1565             Return a hash prefix. The size of the prefix is set to 4 bytes.
1566              
1567             =cut
1568              
1569             sub prefix {
1570 4     4 1 238 my ($self, $string) = @_;
1571              
1572 4         8212 return substr(sha256($string), 0, 4);
1573             }
1574              
1575             =head2 request_full_hash()
1576              
1577             Request full full hashes for specific prefixes from Google.
1578              
1579             =cut
1580              
1581             sub request_full_hash {
1582 0     0 1   my ($self, %args) = @_;
1583 0   0       my $prefixes = $args{prefixes} || return ();
1584 0   0       my $size = $args{size} || length $prefixes->[0];
1585              
1586             # # Handle errors
1587 0           my $i = 0;
1588 0           my $errors;
1589             my $delay = sub {
1590 0     0     my $time = shift;
1591 0 0         if ((time() - $errors->{timestamp}) < $time) {
1592 0           splice(@$prefixes, $i, 1);
1593             }
1594             else {
1595 0           $i++;
1596             }
1597 0           };
1598              
1599 0           while ($i < scalar @$prefixes) {
1600 0           my $prefix = $prefixes->[$i];
1601              
1602 0           $errors = $self->{storage}->get_full_hash_error(prefix => $prefix);
1603 0 0 0       if (defined $errors && $errors->{errors} > 2) { # 2 errors is OK
1604             $errors->{errors} == 3 ? $delay->(30 * 60) # 30 minutes
1605 0 0         : $errors->{errors} == 4 ? $delay->(60 * 60) # 1 hour
    0          
1606             : $delay->(2 * 60 * 60); # 2 hours
1607             }
1608             else {
1609 0           $i++;
1610             }
1611             }
1612              
1613 0           my $url = $self->{server} . "gethash?client=api&apikey=" . $self->{key} . "&appver=$VERSION&pver=" . $self->{version};
1614              
1615 0           my $prefix_list = join('', @$prefixes);
1616 0           my $header = "$size:" . scalar @$prefixes * $size;
1617              
1618             # print @{$args{prefixes}}, "\n";
1619             # print $$prefixes[0], "\n"; return;
1620              
1621              
1622 0           my $res = $self->ua->post($url, Content => "$header\n$prefix_list");
1623              
1624 0 0         if (! $res->is_success) {
1625 0           $self->error("Full hash request failed\n");
1626 0           $self->debug($res->as_string . "\n");
1627              
1628 0           foreach my $prefix (@$prefixes) {
1629 0           my $errors = $self->{storage}->get_full_hash_error(prefix => $prefix);
1630 0 0 0       if (defined $errors && (
      0        
1631             $errors->{errors} >=2 # backoff mode
1632             || $errors->{errors} == 1 && (time() - $errors->{timestamp}) > 5 * 60)) { # 5 minutes
1633 0           $self->{storage}->full_hash_error(prefix => $prefix, timestamp => time()); # more complicate than this, need to check time between 2 errors
1634             }
1635             }
1636              
1637 0           return ();
1638             }
1639             else {
1640 0           $self->debug("Full hash request OK\n");
1641              
1642 0           foreach my $prefix (@$prefixes) {
1643 0           $self->{storage}->full_hash_ok(prefix => $prefix, timestamp => time());
1644             }
1645             }
1646              
1647 0           $self->debug($res->request->as_string . "\n");
1648 0           $self->debug($res->as_string . "\n");
1649             # $self->debug(substr($res->content, 0, 250), "\n\n");
1650              
1651 0           return $self->parse_full_hashes($res->content);
1652             }
1653              
1654             =head2 parse_full_hashes()
1655              
1656             Process the request for full hashes from Google.
1657              
1658             =cut
1659              
1660             sub parse_full_hashes {
1661 0     0 1   my ($self, $data) = @_;
1662              
1663 0           my @hashes = ();
1664              
1665             # goog-malware-shavar:22428:32\nHEX
1666 0           while (length $data > 0) {
1667 0 0         if ($data !~ /^[a-z-]+:\d+:\d+\n/) {
1668 0           $self->error("list not found\n");
1669 0           return ();
1670             }
1671 0           $data =~ s/^([a-z-]+)://;
1672 0           my $list = $1;
1673            
1674 0           $data =~ s/^(\d+)://;
1675 0           my $chunknum = $1;
1676              
1677 0           $data =~ s/^(\d+)\n//;
1678 0           my $length = $1;
1679              
1680 0           my $current = 0;
1681 0           while ($current < $length) {
1682 0           my $hash = substr($data, 0, 32, '');
1683 0           push(@hashes, { hash => $hash, chunknum => $chunknum, list => $list });
1684              
1685 0           $current += 32;
1686             }
1687             }
1688              
1689 0           return @hashes;
1690             }
1691              
1692             =head2 get_a_range()
1693              
1694             Get the list of a chunks ranges for a list update.
1695              
1696             =cut
1697              
1698             sub get_a_range {
1699 0     0 1   my ($self, %args) = @_;
1700 0   0       my $list = $args{'list'} || '';
1701              
1702 0           my @nums = $self->{storage}->get_add_chunks_nums(); # trust storage to torder list, or reorder it here?
1703              
1704 0           return $self->create_range(numbers => [@nums]);
1705             }
1706              
1707             =head2 get_s_range()
1708              
1709             Get the list of s chunks ranges for a list update.
1710              
1711             =cut
1712              
1713             sub get_s_range {
1714 0     0 1   my ($self, %args) = @_;
1715 0   0       my $list = $args{'list'} || '';
1716              
1717 0           my @nums = $self->{storage}->get_sub_chunks_nums(); # trust storage to torder list, or reorder it here?
1718              
1719 0           return $self->create_range(numbers => [@nums]);
1720             }
1721              
1722             =head2 create_range()
1723              
1724             Create a list of ranges (1-3, 5, 7-11) from a list of numbers.
1725              
1726             =cut
1727              
1728             sub create_range {
1729 0     0 1   my ($self, %args) = @_;
1730 0   0       my $numbers = $args{numbers} || []; # should already be ordered
1731              
1732 0 0         return '' if (scalar @$numbers == 0);
1733              
1734 0           my $range = $$numbers[0];
1735 0           my $new_range = 0;
1736 0           for(my $i = 1; $i < scalar @$numbers; $i++) {
1737             # next if ($$numbers[$i] == $$numbers[$i-1]); # should not happen
1738              
1739 0 0         if ($$numbers[$i] != $$numbers[$i-1] + 1) {
    0          
1740 0 0 0       $range .= $$numbers[$i-1] if ($i > 1 && $new_range == 1);
1741 0           $range .= ',' . $$numbers[$i];
1742              
1743 0           $new_range = 0
1744             }
1745             elsif ($new_range == 0) {
1746 0           $range .= "-";
1747 0           $new_range = 1;
1748             }
1749             }
1750 0 0         $range .= $$numbers[scalar @$numbers - 1] if ($new_range == 1);
1751              
1752 0           return $range;
1753             }
1754              
1755             =head2 expand_range()
1756              
1757             Explode list of ranges (1-3, 5, 7-11) into a list of numbers (1,2,3,5,7,8,9,10,11).
1758              
1759             =cut
1760              
1761             sub expand_range {
1762 0     0 1   my ($self, %args) = @_;
1763 0   0       my $range = $args{range} || return ();
1764              
1765 0           my @list = ();
1766 0           my @elements = split /,/, $range;
1767              
1768 0           foreach my $data (@elements) {
1769 0 0         if ($data =~ /^\d+$/) { # single number
    0          
1770 0           push(@list, $data);
1771             }
1772             elsif ($data =~ /^(\d+)-(\d+)$/) {
1773 0           my $start = $1;
1774 0           my $end = $2;
1775              
1776 0           for(my $i = $start; $i <= $end; $i++) {
1777 0           push(@list, $i);
1778             }
1779             }
1780             }
1781              
1782 0           return @list;
1783             }
1784              
1785              
1786             =head1 CHANGELOG
1787              
1788             =over 4
1789              
1790             =item 1.11
1791              
1792             Add dependency on IO::Socket::SSL.
1793             Remove dependency on Net::IPAddress.
1794              
1795             =item 1.10
1796              
1797             Force IPv4 to solve bug on CentOS.
1798              
1799             =item 1.09
1800              
1801             Use HTTPS to access safebrowsing.clients.google.com/.
1802              
1803             =item 1.07
1804              
1805             Add C feature to import add chunks and sub chunks from a file.
1806              
1807             =item 1.05
1808              
1809             No code change. Move C to PRIVATE FUNCTIONS to avoid confusions.
1810              
1811             =item 1.04
1812              
1813             Introduce L. Remind people that Google Safe Browsing v1 has been deprecated by Google.
1814              
1815             =item 1.03
1816              
1817             The source code is available on github at L.
1818              
1819             =item 1.02
1820              
1821             Fix uninitialized $self->{errors} variable
1822              
1823             =item 1.01
1824              
1825             Use String::HexConvert for faster hex_to_ascii.
1826              
1827             =item 1.0
1828              
1829             Separate the error output from the debug output.
1830              
1831             =item 0.9
1832              
1833             Fix bug with local whitelisting (sub chunks). Fix the parsing of full hashes.
1834              
1835             =item 0.8
1836              
1837             Reduce the number of full hash requests.
1838              
1839             =item 0.7
1840              
1841             Add local_lookup to perform a lookup against the local database only. This function should be used for debugging purpose only.
1842             Small code optimizations.
1843              
1844             =item 0.6
1845              
1846             Handle local database reset.
1847              
1848             =item 0.5
1849              
1850             Update documentation.
1851              
1852             =item 0.4
1853              
1854             Speed update the database update. The first update went down from 20 minutes to 20 minutes.
1855              
1856             =item 0.3
1857              
1858             Fix typos in the documentation.
1859              
1860             Remove dependency on Switch (thanks to Curtis Jewel).
1861              
1862             Fix value of FULL_HASH_TIME.
1863              
1864             =item 0.2
1865              
1866             Add support for Message Authentication Code (MAC)
1867              
1868             =back
1869              
1870             =head1 SEE ALSO
1871              
1872             Source code available at L.
1873              
1874             See L, L and L for information on storing and managing the Google Safe Browsing database.
1875              
1876             Google Safe Browsing v2 API: L
1877              
1878             L (Google Safe Browsing v1) is deprecated by Google since 12/01/2011.
1879              
1880             L (Google Safe Browsing v2) will deprecated by Google on 12/01/2014.
1881              
1882             =head1 AUTHOR
1883              
1884             Julien Sobrier, Ejsobrier@zscaler.comE or Ejulien@sobrier.netE
1885              
1886             =head1 COPYRIGHT AND LICENSE
1887              
1888             Copyright (C) 2012 by Julien Sobrier
1889              
1890             This library is free software; you can redistribute it and/or modify
1891             it under the same terms as Perl itself, either Perl version 5.8.8 or,
1892             at your option, any later version of Perl 5 you may have available.
1893              
1894              
1895             =cut
1896              
1897             1;