File Coverage

blib/lib/Net/Google/SafeBrowsing3.pm
Criterion Covered Total %
statement 134 517 25.9
branch 15 154 9.7
condition 4 87 4.6
subroutine 25 45 55.5
pod 24 24 100.0
total 202 827 24.4


line stmt bran cond sub pod time code
1             package Net::Google::SafeBrowsing3;
2              
3 1     1   13547 use strict;
  1         2  
  1         24  
4 1     1   3 use warnings;
  1         1  
  1         22  
5              
6 1     1   3 use Carp;
  1         5  
  1         69  
7 1     1   535 use LWP::UserAgent;
  1         33533  
  1         28  
8 1     1   8 use URI;
  1         1  
  1         19  
9 1     1   715 use Digest::SHA qw(sha256);
  1         2611  
  1         127  
10 1     1   8 use List::Util qw(first);
  1         2  
  1         73  
11 1     1   386 use Text::Trim;
  1         486  
  1         46  
12 1     1   363 use MIME::Base64::URLSafe;
  1         1056  
  1         42  
13 1     1   4 use MIME::Base64;
  1         1  
  1         30  
14 1     1   354 use String::HexConvert;
  1         291  
  1         48  
15 1     1   980 use IO::Socket::SSL 'inet4';
  1         62714  
  1         7  
16 1     1   729 use Google::ProtocolBuffers;
  1         47274  
  1         30  
17 1     1   16 use Data::Dumper;
  1         1  
  1         40  
18              
19 1     1   3 use Exporter 'import';
  1         1  
  1         49  
20             our @EXPORT = qw(DATABASE_RESET INTERNAL_ERROR SERVER_ERROR NO_UPDATE NO_DATA SUCCESSFUL MALWARE PHISHING UNWANTED LANDING DISTRIBUTION);
21              
22              
23             BEGIN {
24 1     1   5 IO::Socket::SSL::set_ctx_defaults(
25             # verify_mode => Net::SSLeay->VERIFY_PEER(),
26             SSL_verify_mode => 0,
27             );
28             }
29              
30             our $VERSION = '0.7';
31              
32             Google::ProtocolBuffers->parse("
33             message ChunkData {
34             required int32 chunk_number = 1;
35              
36             // The chunk type is either an add or sub chunk.
37             enum ChunkType {
38             ADD = 0;
39             SUB = 1;
40             }
41             optional ChunkType chunk_type = 2 [default = ADD];
42              
43             // Prefix type which currently is either 4B or 32B. The default is set
44             // to the prefix length, so it doesn't have to be set at all for most
45             // chunks.
46             enum PrefixType {
47             PREFIX_4B = 0;
48             FULL_32B = 1;
49             }
50             optional PrefixType prefix_type = 3 [default = PREFIX_4B];
51              
52             // Stores all SHA256 add or sub prefixes or full-length hashes. The number
53             // of hashes can be inferred from the length of the hashes string and the
54             // prefix type above.
55             optional bytes hashes = 4;
56              
57             // Sub chunks also encode one add chunk number for every hash stored above.
58             repeated int32 add_numbers = 5 [packed = true];
59              
60             }
61             ",
62             {create_accessors => 0 }
63             );
64              
65             Google::ProtocolBuffers->parse("
66             message MalwarePatternType {
67             enum PATTERN_TYPE {
68             LANDING = 1;
69             DISTRIBUTION = 2;
70             }
71              
72             required PATTERN_TYPE pattern_type = 1;
73             }
74             ",
75             {create_accessors => 0 }
76             );
77              
78             # TODO ###################################################
79             #Todo: request full hashes: seperate 32bytes for 4bytes
80             # Todo: optimize lookup_suffix, 1 search for all lists
81              
82             =head1 NAME
83              
84             Net::Google::SafeBrowsing3 - Perl extension for the Google Safe Browsing v3 API. (Google Safe Browsing v2 has been deprecated by Google.)
85              
86             =head1 SYNOPSIS
87              
88             use Net::Google::SafeBrowsing3;
89             use Net::Google::SafeBrowsing3::Sqlite;
90            
91             my $storage = Net::Google::SafeBrowsing3::Sqlite->new(file => 'google-v3.db');
92             my $gsb = Net::Google::SafeBrowsing3->new(
93             key => "my key",
94             storage => $storage,
95             );
96            
97             $gsb->update();
98             my $match = $gsb->lookup(url => 'http://www.gumblar.cn/');
99            
100             if ($match eq MALWARE) {
101             print "http://www.gumblar.cn/ is flagged as a dangerous site\n";
102             }
103              
104             $storage->close();
105              
106             =head1 DESCRIPTION
107              
108             Net::Google::SafeBrowsing3 implements the Google Safe Browsing v3 API.
109              
110             The library passes most of the unit tests listed in the API documentation. See the documentation (L) for more details about the failed tests.
111              
112             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.
113              
114             The source code is available on github at L.
115              
116             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.
117              
118             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.
119              
120             IMPORTANT: Google Safe Browsing v3 requires a different key than v2.
121              
122              
123             =head1 CONSTANTS
124              
125             Several constants are exported by this module:
126              
127             =over 4
128              
129             =item DATABASE_RESET
130              
131             Google requested to reset (empty) the local database.
132              
133             =item INTERNAL_ERROR
134              
135             An internal error occurred.
136              
137             =item SERVER_ERROR
138              
139             The server sent an error back to the client.
140              
141             =item NO_UPDATE
142              
143             No update was performed, probably because it is too early to make a new request to Google Safe Browsing.
144              
145             =item NO_DATA
146              
147             No data was sent back by Google to the client, probably because the database is up to date.
148              
149             =item SUCCESSFUL
150              
151             The operation was successful.
152              
153             =item MALWARE
154              
155             Name of the Malware list in Google Safe Browsing (shortcut to 'goog-malware-shavar')
156              
157             =item PHISHING
158              
159             Name of the Phishing list in Google Safe Browsing (shortcut to 'googpub-phish-shavar')
160              
161             =item UNWANTED
162              
163             Name of the Unwamted Application list in Google Safe Browsing (shortcut to 'goog-unwanted-shavar')
164              
165             =item LANDING
166              
167             Landing site.
168              
169             =item DISTRIBUTION
170              
171             Distribution site.
172              
173             =back
174              
175             =cut
176              
177             use constant {
178 1         165 DATABASE_RESET => -6,
179             INTERNAL_ERROR => -3, # internal/parsing error
180             SERVER_ERROR => -2, # Server sent an error back
181             NO_UPDATE => -1, # no update (too early)
182             NO_DATA => 0, # no data sent
183             SUCCESSFUL => 1, # data sent
184             MALWARE => 'goog-malware-shavar',
185             PHISHING => 'googpub-phish-shavar',
186             UNWANTED => 'goog-unwanted-shavar',
187             LANDING => 1, # Metadata goog-malware-shavar
188             DISTRIBUTION => 2, # Metadata goog-malware-shavar
189 1     1   111 };
  1         1  
190              
191              
192             =head1 CONSTRUCTOR
193              
194             =over 4
195              
196             =back
197              
198             =head2 new()
199              
200             Create a Net::Google::SafeBrowsing3 object
201              
202             my $gsb = Net::Google::SafeBrowsing3->new(
203             key => "my key",
204             storage => Net::Google::SafeBrowsing3::Sqlite->new(file => 'google-v3.db'),
205             debug => 0,
206             list => MALWARE,
207             );
208              
209             Arguments
210              
211             =over 4
212              
213             =item server
214              
215             Safe Browsing Server. https://safebrowsing.google.com/safebrowsing/ by default
216              
217             =item key
218              
219             Required. Your Google Safe browsing API key
220              
221             =item storage
222              
223             Required. Object which handle the storage for the Google Safe Browsing database. See L for more details.
224              
225             =item list
226              
227             Optional. The Google Safe Browsing list to handle. By default, handles both MALWARE and PHISHING.
228              
229             =item debug
230              
231             Optional. Set to 1 to enable debugging. 0 (disabled) by default.
232              
233             The debug output maybe quite large and can slow down significantly the update and lookup functions.
234              
235             =item errors
236              
237             Optional. Set to 1 to show errors to STDOUT. 0 (disabled by default).
238              
239             =item perf
240              
241             Optional. Set to 1 to show performance information.
242              
243             =item version
244              
245             Optional. Google Safe Browsing version. 3.0 by default
246              
247             =back
248              
249             =cut
250              
251             sub new {
252 1     1 1 611 my ($class, %args) = @_;
253              
254 1         11 my $self = { # default arguments
255             server => 'https://safebrowsing.google.com/safebrowsing/',
256             list => [PHISHING, MALWARE, UNWANTED],
257             key => '',
258             version => '3.0',
259             debug => 0,
260             errors => 0,
261             last_error => '',
262             perf => 0,
263              
264             %args,
265             };
266              
267 1 50       18 if (! exists $self->{storage}) {
268 1     1   567 use Net::Google::SafeBrowsing3::Storage;
  1         1  
  1         4527  
269 1         8 $self->{storage} = Net::Google::SafeBrowsing3::Storage->new();
270             }
271 1 50       3 if (ref $self->{list} ne 'ARRAY') {
272 0         0 $self->{list} = [$self->{list}];
273             }
274              
275 1 50       5 bless $self, $class or croak "Can't bless $class: $!";
276 1         2 return $self;
277             }
278              
279             =head1 PUBLIC FUNCTIONS
280              
281             =over 4
282              
283             =back
284              
285             =head2 update()
286              
287             Perform a database update.
288              
289             $gsb->update();
290              
291             Return the status of the update (see the list of constants above): INTERNAL_ERROR, SERVER_ERROR, NO_UPDATE, NO_DATA or SUCCESSFUL
292              
293             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.
294              
295              
296             Arguments
297              
298             =over 4
299              
300             =item list
301              
302             Optional. Update a specific list. Use the list(s) from new() by default.
303              
304              
305             =item force
306              
307             Optional. Force the update (1). Disabled by default (0).
308              
309             Be careful if you set this option to 1 as too frequent updates might result in the blacklisting of your API key.
310              
311             =back
312              
313             =cut
314              
315             sub update {
316 0     0 1 0 my ($self, %args) = @_;
317 0         0 my $list = $args{list};
318 0   0     0 my $force = $args{force} || 0;
319              
320 0         0 my @lists = @{$self->{list}};
  0         0  
321 0 0       0 @lists = @{[$args{list}]} if (defined $list);
  0         0  
322              
323 0         0 my $result = 0;
324              
325             # Too early to update?
326 0         0 my $start = time();
327 0         0 my $i = 0;
328 0         0 while ($i < scalar @lists) {
329 0         0 my $list = $lists[$i];
330 0         0 my $info = $self->{storage}->last_update(list => $list);
331            
332 0 0 0     0 if ($info->{'time'} + $info->{'wait'} > time && $force == 0) {
333 0         0 $self->debug("Too early to update $list\n");
334 0         0 splice(@lists, $i, 1);
335             }
336             else {
337 0         0 $self->debug("OK to update $list: " . time() . "/" . ($info->{'time'} + $info->{'wait'}) . "\n");
338 0         0 $i++;
339             }
340             }
341              
342 0 0       0 if (scalar @lists == 0) {
343 0         0 $self->debug("Too early to update any list\n");
344 0         0 return NO_UPDATE;
345             }
346 0         0 $self->perf("OK to update check: " . (time() - $start) . "s\n");
347            
348              
349 0         0 my $ua = $self->ua;
350              
351 0         0 my $url = $self->{server} . "downloads?client=api&key=" . $self->{key} . "&appver=$VERSION&pver=" . $self->{version};
352              
353 0         0 my $body = '';
354 0         0 foreach my $list (@lists) {
355             # Report existng chunks
356 0         0 $start = time();
357 0         0 my $a_range = $self->create_range(numbers => [$self->{storage}->get_add_chunks_nums(list => $list)]);
358 0         0 my $s_range = $self->create_range(numbers => [$self->{storage}->get_sub_chunks_nums(list => $list)]);
359 0         0 $self->perf("Create add and sub ranges: " . (time() - $start) . "s\n");
360            
361 0         0 my $chunks_list = '';
362 0 0       0 if ($a_range ne '') {
363 0         0 $chunks_list .= "a:$a_range";
364             }
365 0 0       0 if ($s_range ne '') {
366 0 0       0 $chunks_list .= ":" if ($a_range ne '');
367 0         0 $chunks_list .= "s:$s_range";
368             }
369              
370 0         0 $body .= "$list;$chunks_list";
371 0         0 $body .= "\n";
372             }
373              
374 0         0 my $start_req = time();
375 0         0 my $res = $ua->post($url, Content => $body);
376 0         0 $self->perf("$body\n");
377              
378 0         0 $self->debug($res->request->as_string, "\n", $res->as_string . "\n");
379 0         0 my $duration_req = time() - $start_req;
380              
381 0 0       0 if (! $res->is_success) {
382 0         0 $self->error("Request failed\n");
383              
384 0         0 foreach my $list (@lists) {
385 0         0 $self->update_error('time' => time(), list => $list);
386             }
387              
388 0         0 return SERVER_ERROR;
389             }
390              
391 0         0 my $last_update = time;
392 0         0 my $wait = 0;
393              
394 0         0 my @redirections = ();
395 0         0 my $del_add_duration = 0;
396 0         0 my $del_sub_duration = 0;
397 0         0 my $add_range_info = '';
398              
399             # API doc: Clients must clear cached full-length hashes each time they send an update request.
400 0         0 foreach my $list (@lists) {
401 0         0 $self->{storage}->reset_full_hashes(list => $list);
402             }
403              
404 0         0 my @lines = split/\s/, $res->decoded_content;
405 0         0 $list = '';
406 0         0 foreach my $line (@lines) {
407 0 0       0 if ($line =~ /n:\s*(\d+)\s*$/) {
    0          
    0          
    0          
    0          
    0          
    0          
408 0         0 $self->debug("Next poll: $1 seconds\n");
409 0         0 $wait = $1;
410             }
411             elsif ($line =~ /i:\s*(\S+)\s*$/) {
412 0         0 $self->debug("List: $1\n");
413 0         0 $list = $1;
414             }
415             elsif ($line =~ /u:\s*(\S+),(\S+)\s*$/) {
416 0         0 $self->debug("Redirection: $1\n");
417 0         0 $self->debug("MAC: $2\n");
418 0         0 push(@redirections, [$1, $list, $2]);
419             }
420             elsif ($line =~ /u:\s*(\S+)\s*$/) {
421 0         0 $self->debug("Redirection: $1\n");
422 0         0 push(@redirections, [$1, $list, '']);
423             }
424             elsif ($line =~ /ad:(\S+)$/) {
425 0         0 $self->debug("Delete Add Chunks: $1\n");
426              
427 0         0 my $del_add_start = time();
428 0         0 $add_range_info = $1 . " $list";
429 0         0 my @nums = $self->expand_range(range => $1);
430 0         0 $self->{storage}->delete_add_ckunks(chunknums => [@nums], list => $list);
431 0         0 $del_add_duration = time() - $del_add_start;
432              
433 0         0 $result = 1;
434             }
435             elsif ($line =~ /sd:(\S+)$/) {
436 0         0 $self->debug("Delete Sub Chunks: $1\n");
437              
438 0         0 my $del_sub_start = time();
439 0         0 my @nums = $self->expand_range(range => $1);
440 0         0 $self->{storage}->delete_sub_ckunks(chunknums => [@nums], list => $list);
441 0         0 $del_sub_duration = time() - $del_sub_start;
442              
443 0         0 $result = 1;
444             }
445             elsif ($line =~ /r:pleasereset/) {
446 0         0 $self->debug("Database must be reset\n");
447              
448 0         0 $self->{storage}->reset(list => $list);
449              
450 0         0 return DATABASE_RESET;
451             }
452             }
453 0         0 $self->debug("\n");
454 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");
455              
456 0 0       0 $result = 1 if (scalar @redirections > 0);
457              
458 0         0 $self->perf("Parse redirections: ");
459 0         0 foreach my $data (@redirections) {
460 0         0 $start = time();
461 0         0 my $redirection = $data->[0];
462 0         0 $list = $data->[1];
463              
464 0         0 $self->debug("Checking redirection https://$redirection ($list)\n");
465 0         0 $res = $ua->get("https://$redirection");
466 0 0       0 if (! $res->is_success) {
467 0         0 $self->error("Request to $redirection failed\n");
468              
469 0         0 foreach my $list (@lists) {
470 0         0 $self->update_error('time' => $last_update, list => $list);
471             }
472              
473 0         0 return SERVER_ERROR;
474             }
475            
476 0 0       0 $self->debug(substr($res->as_string, 0, 250) . "\n\n") if ($self->{debug});
477 0 0       0 $self->debug(substr($res->content, 0, 250) . "\n\n") if ($self->{debug});
478            
479 0         0 my $data = $res->content;
480              
481 0         0 my $result = $self->parse_data(data => $data, list => $list);
482 0 0       0 if ($result != SUCCESSFUL) {
483 0         0 foreach my $list (@lists) {
484 0         0 $self->update_error('time' => $last_update, list => $list);
485             }
486              
487 0         0 return $result;
488             }
489 0         0 $self->perf((time() - $start) . "s ");
490             }
491 0         0 $self->perf("\n");
492              
493 0         0 foreach my $list (@lists) {
494 0         0 $self->debug("List update: $last_update $wait $list\n");
495 0         0 $self->{storage}->updated('time' => $last_update, 'wait' => $wait, list => $list);
496             }
497              
498 0         0 return $result; # ok
499             }
500              
501             =head2 lookup()
502              
503             Lookup a URL against the Google Safe Browsing database.
504              
505             my $match = $gsb->lookup(url => 'http://www.gumblar.cn');
506             my ($match, $type) = $gsb->lookup(url => 'http://www.gumblar.cn');
507              
508             In scalar context, returns the name of the list if there is any match, returns an empty string otherwise.
509             In array context, return the name of the list (empty if no match) and the type of malware site (0 if no type specified)
510              
511             Arguments
512              
513             =over 4
514              
515             =item list
516              
517             Optional. Lookup against a specific list. Use the list(s) from new() by default.
518              
519             =item url
520              
521             Required. URL to lookup.
522              
523             =back
524              
525             =cut
526              
527             sub lookup {
528 0     0 1 0 my ($self, %args) = @_;
529 0   0     0 my $list = $args{list} || '';
530 0   0     0 my $url = $args{url} || return '';
531              
532 0         0 my @lists = @{$self->{list}};
  0         0  
533 0 0       0 @lists = @{[$args{list}]} if ($list ne '');
  0         0  
534              
535              
536             # TODO: create our own URI management for canonicalization
537             # fix for http:///foo.com (3 ///)
538 0         0 $url =~ s/^(https?:\/\/)\/+/$1/;
539              
540              
541 0         0 my $uri = URI->new($url)->canonical;
542 0         0 my ($match, $type) = $self->lookup_suffix(lists => [@lists], url => $url);
543 0 0       0 return ($match, $type) if (wantarray);
544 0         0 return $match
545             }
546              
547              
548              
549              
550              
551              
552              
553              
554             =pod
555              
556             =head1 PRIVATE FUNCTIONS
557              
558             These functions are not intended to be used externally.
559              
560             =over 4
561              
562             =back
563              
564             =head2 lookup_suffix()
565              
566             Lookup a host prefix.
567              
568             =cut
569              
570             sub lookup_suffix {
571 0     0 1 0 my ($self, %args) = @_;
572 0   0     0 my $lists = $args{lists} || croak "Missing lists\n";
573 0   0     0 my $url = $args{url} || return '';
574              
575             # Calculate prefixes
576 0         0 my @full_hashes = $self->full_hashes($url);
577 0         0 my @full_hashes_prefix = map (substr($_, 0, 4), @full_hashes); # Get the prefixes from the first 4 bytes
578              
579             # Local lookup
580 0         0 my @add_chunks = $self->local_lookup_suffix(lists => $lists, url => $url, full_hashes => [@full_hashes], full_hashes_prefix => [@full_hashes_prefix]);
581 0 0       0 if (scalar @add_chunks == 0) {
582 0         0 $self->debug("No hit in local lookup\n");
583 0 0       0 return ('', 0) if (wantarray);
584 0         0 return '';
585             }
586              
587 0         0 $self->debug("Found ", scalar(@add_chunks), " add chunk(s) in local database\n");
588 0         0 foreach my $add (@add_chunks) {
589 0         0 $self->debug("Chunk: ", $self->hex_to_ascii($add->{prefix}), " - ", $add->{list}, "\n");
590             }
591              
592             # get stored full hashes
593 0         0 foreach my $hash (@full_hashes) {
594 0         0 foreach my $list (@$lists) {
595 0         0 my @hashes = $self->{storage}->get_full_hashes(hash => $hash, list => $list);
596            
597 0 0       0 if (scalar @hashes > 0) {
598 0         0 $self->debug("Full hashes found: ", scalar(@hashes), "\n");
599 0         0 my $result = pop(@hashes);
600              
601 0 0 0     0 return ($list, $result->{type} || 0) if (wantarray);
602 0         0 return $list;
603             }
604             }
605             }
606              
607              
608             # ask for new hashes
609             # TODO: make sure we don't keep asking for the same over and over
610 0         0 my @hashes = $self->request_full_hash(prefixes => [ map($_->{prefix}, @add_chunks) ]);
611 0         0 $self->{storage}->add_full_hashes(full_hashes => [@hashes], timestamp => time());
612              
613 0         0 foreach my $full_hash (@full_hashes) {
614 0     0   0 my $hash = first { $_->{hash} eq $full_hash} @hashes;
  0         0  
615 0 0       0 next if (! defined $hash);
616              
617 0     0   0 my $list = first { $hash->{list} eq $_ } @$lists;
  0         0  
618              
619 0 0 0     0 if (defined $hash && defined $list) {
620             # $self->debug($self->hex_to_ascii($hash->{hash}) . " eq " . $self->hex_to_ascii($full_hash) . "\n\n");
621              
622 0         0 $self->debug("Match: " . $self->hex_to_ascii($full_hash) . "\n");
623              
624 0 0 0     0 return ($hash->{list}, $hash->{type} || 0) if (wantarray);
625 0         0 return $hash->{list};
626             }
627             # elsif (defined $hash) {
628             # $self->debug("hash: " . $self->hex_to_ascii($hash->{hash}) . "\n");
629             # $self->debug("list: " . $hash->{list} . "\n");
630             # }
631             }
632            
633 0         0 $self->debug("No match\n");
634 0 0       0 return ('', 0) if (wantarray);
635 0         0 return '';
636             }
637              
638             =head2 local_lookup_suffix()
639              
640             Lookup a host prefix in the local database only.
641              
642             =cut
643             sub local_lookup_suffix {
644 0     0 1 0 my ($self, %args) = @_;
645 0   0     0 my $lists = $args{lists} || croak "Missing lists\n";
646 0   0     0 my $url = $args{url} || return ();
647 0   0     0 my $full_hashe_list = $args{full_hashes} || [];
648 0   0     0 my $full_hashes_prefix_list = $args{full_hashes_prefix} || [];
649              
650              
651              
652             # Step 1: calculate prefixes if not provided
653             # Get the prefixes from the first 4 bytes
654 0         0 my @full_hashes = @{$full_hashe_list};
  0         0  
655 0         0 my @full_hashes_prefix = @{$full_hashes_prefix_list};
  0         0  
656 0 0       0 if (scalar @full_hashes_prefix == 0) {
657 0 0       0 @full_hashes = $self->full_hashes($url) if (scalar @full_hashes == 0);
658              
659 0         0 @full_hashes_prefix = map (substr($_, 0, 4), @full_hashes);
660             }
661              
662             # Step 2: get all add chunks for these suffixes
663             # Do it for all lists
664 0         0 my @add_chunks = ();
665 0         0 foreach my $prefix (@full_hashes_prefix, @full_hashes) {
666 0         0 push(@add_chunks, $self->{storage}->get_add_chunks(prefix => $prefix));
667             }
668              
669 0 0       0 if (scalar @add_chunks == 0) { # no match
670 0         0 $self->debug("No prefix found\n");
671 0         0 return @add_chunks;
672             }
673              
674              
675             # Step 3: get all sub chunks for this host key
676 0         0 my @sub_chunks = ();
677 0         0 foreach my $prefix (@full_hashes_prefix, @full_hashes) {
678 0         0 push(@sub_chunks, $self->{storage}->get_sub_chunks(hostkey => $prefix));
679             }
680              
681 0         0 foreach my $sub_chunk (@sub_chunks) {
682 0         0 my $i = 0;
683 0         0 while ($i < scalar @add_chunks) {
684 0         0 my $add_chunk = $add_chunks[$i];
685              
686 0 0 0     0 if ($add_chunk->{chunknum} != $sub_chunk->{addchunknum} || $add_chunk->{list} ne $sub_chunk->{list}) {
687 0         0 $i++;
688 0         0 next;
689             }
690              
691 0 0       0 if ($sub_chunk->{prefix} eq $add_chunk->{prefix}) {
692 0         0 splice(@add_chunks, $i, 1);
693             }
694             else {
695 0         0 $i++;
696             }
697             }
698             }
699              
700 0 0       0 if (scalar @add_chunks == 0) {
701 0         0 $self->debug("All add_chunks have been removed by sub_chunks\n");
702             }
703              
704 0         0 return @add_chunks;
705             }
706              
707              
708             =head2 update_error()
709              
710             Handle server errors during a database update.
711              
712             =cut
713              
714             sub update_error {
715 0     0 1 0 my ($self, %args) = @_;
716 0   0     0 my $time = $args{'time'} || time;
717 0   0     0 my $list = $args{'list'} || '';
718              
719 0         0 my $info = $self->{storage}->last_update(list => $list);
720 0 0       0 $info->{errors} = 0 if (! exists $info->{errors});
721 0         0 my $errors = $info->{errors} + 1;
722 0         0 my $wait = 0;
723              
724 0 0       0 $wait = $errors == 1 ? 60
    0          
    0          
    0          
    0          
    0          
725             : $errors == 2 ? int(30 * 60 * (rand(1) + 1)) # 30-60 mins
726             : $errors == 3 ? int(60 * 60 * (rand(1) + 1)) # 60-120 mins
727             : $errors == 4 ? int(2 * 60 * 60 * (rand(1) + 1)) # 120-240 mins
728             : $errors == 5 ? int(4 * 60 * 60 * (rand(1) + 1)) # 240-480 mins
729             : $errors > 5 ? 480 * 60
730             : 0;
731              
732 0         0 $self->{storage}->update_error('time' => $time, list => $list, 'wait' => $wait, errors => $errors);
733              
734             }
735              
736              
737             =head2 ua()
738              
739             Create LWP::UserAgent to make HTTP requests to Google.
740              
741             =cut
742              
743             sub ua {
744 0     0 1 0 my ($self, %args) = @_;
745              
746 0 0       0 if (! exists $self->{ua}) {
747 0         0 my $ua = LWP::UserAgent->new;
748 0         0 $ua->timeout(60);
749              
750 0         0 $self->{ua} = $ua;
751             }
752              
753 0         0 return $self->{ua};
754             }
755              
756              
757             =head2 parse_data()
758              
759             Parse data from a redirection (add and sub chunk information).
760              
761             =cut
762              
763             sub parse_data {
764 0     0 1 0 my ($self, %args) = @_;
765 0   0     0 my $data = $args{data} || '';
766 0   0     0 my $list = $args{list} || '';
767              
768 0         0 my $chunk_num = 0;
769 0         0 my $hash_length = 0;
770 0         0 my $chunk_length = 0;
771              
772 0         0 while (length $data > 0) {
773             # my $length = substr($data, 0, 4); # HEX
774 0         0 my $length = hex $self->hex_to_ascii( substr($data, 0, 4, '') );
775 0         0 $self->debug("Length: $length\n");
776 0         0 my $chunk = substr($data, 0, $length, '');
777 0         0 my $data = ChunkData->decode($chunk);
778 0         0 $self->debug(Dumper($data), "\n");
779              
780 0 0 0     0 if (! exists($data->{chunk_type}) || $data->{chunk_type} == 0) {
781 0         0 my @chunks = $self->parse_a(chunk => $data);
782 0         0 $self->{storage}->add_chunks(type => 'a', chunknum => $data->{chunk_number}, chunks => [@chunks], list => $list);
783             }
784             else {
785 0         0 my @chunks = $self->parse_s(chunk => $data);
786 0         0 $self->{storage}->add_chunks(type => 's', chunknum => $data->{chunk_number}, chunks => [@chunks], list => $list);
787             }
788             }
789              
790 0         0 return SUCCESSFUL;
791             }
792              
793             =head2 parse_s()
794              
795             Parse s chunks information for a database update.
796              
797             =cut
798              
799             sub parse_s {
800 0     0 1 0 my ($self, %args) = @_;
801 0   0     0 my $chunk = $args{chunk} || return ();
802              
803             # {
804             # 'add_numbers' => [
805             # 161383,
806             # 156609,
807             # 161686,
808             # 159174,
809             # 166040,
810             # 164187
811             # ],
812             # 'chunk_type' => 1,
813             # 'chunk_number' => 158095,
814             # 'hashes' => ' _*���F�E����A��;;v����i'
815             # }
816              
817 0         0 my @data = ();
818 0   0     0 my $prefix_type = $chunk->{prefix_type} || 0;
819 0   0     0 my $prefix = $chunk->{hashes} || ''; # HEX
820 0         0 $self->debug("Hashes length: ", length($prefix), "\n");
821 0 0       0 $self->debug("Hashes: ", $self->hex_to_ascii($prefix), "\n") if ($self->{debug});
822              
823 0         0 my $hash_length = 4;
824 0 0       0 $hash_length = 32 if ($prefix_type == 1);
825 0         0 my @hashes = ();
826 0         0 while(length($prefix) > 0) {
827 0         0 push(@hashes, substr($prefix, 0, $hash_length, ''));
828             }
829              
830 0         0 for(my $i = 0; $i < scalar @{ $chunk->{add_numbers} }; $i++) {
  0         0  
831 0         0 push(@data, { add_chunknum => ${ $chunk->{add_numbers} }[$i], prefix => $hashes[$i] });
  0         0  
832             }
833              
834 0         0 return @data;
835             }
836              
837              
838             =head2 parse_a()
839              
840             Parse a chunks information for a database update.
841              
842             =cut
843              
844             sub parse_a {
845 0     0 1 0 my ($self, %args) = @_;
846 0   0     0 my $chunk = $args{chunk} || return ();
847              
848             # {
849             # 'chunk_number' => 166146,
850             # 'hashes' => 'Z[�$~�����w5���B�;0����z;�E&�ʳY�H$`-'
851             # }
852              
853 0         0 my @data = ();
854 0   0     0 my $prefix_type = $chunk->{prefix_type} || 0;
855 0   0     0 my $prefix = $chunk->{hashes} || ''; # HEX
856              
857 0         0 my $hash_length = 4;
858 0 0       0 $hash_length = 32 if ($prefix_type == 1);
859              
860 0         0 while(length($prefix) > 0) {
861 0         0 push(@data, { prefix => substr($prefix, 0, $hash_length, '') });
862             }
863              
864 0         0 return @data;
865             }
866              
867             =head2 hex_to_ascii()
868              
869             Transform hexadecimal strings to printable ASCII strings. Used mainly for debugging.
870              
871             print $gsb->hex_to_ascii('hex value');
872              
873             =cut
874              
875             sub hex_to_ascii {
876 2     2 1 6 my ($self, $hex) = @_;
877              
878 2         6 return String::HexConvert::ascii_to_hex($hex);
879             }
880              
881              
882             =head2 ascii_to_hex()
883              
884             Transform ASCII strings to hexadecimal strings.
885              
886             =cut
887              
888             sub ascii_to_hex {
889 1     1 1 379 my ($self, $ascii) = @_;
890              
891 1         2 my $hex = '';
892 1         6 for (my $i = 0; $i < int(length($ascii) / 2); $i++) {
893 4         13 $hex .= chr hex( substr($ascii, $i * 2, 2) );
894             }
895              
896 1         3 return $hex;
897             }
898              
899             =head2 debug()
900              
901             Print debug output.
902              
903             =cut
904              
905             sub debug {
906 0     0 1 0 my ($self, @messages) = @_;
907              
908 0 0       0 print join('', @messages) if ($self->{debug} > 0);
909             }
910              
911              
912             =head2 error()
913              
914             Print error message.
915              
916             =cut
917              
918             sub error {
919 0     0 1 0 my ($self, $message) = @_;
920              
921 0 0 0     0 print "ERROR - ", $message if ($self->{debug} > 0 || $self->{errors} > 0);
922 0         0 $self->{last_error} = $message;
923             }
924              
925              
926             =head2 perf()
927              
928             Print performance message.
929              
930             =cut
931              
932             sub perf {
933 0     0 1 0 my ($self, @messages) = @_;
934              
935 0 0       0 print join('', @messages)if ($self->{perf} > 0);
936             }
937              
938              
939             =head2 canonical_domain()
940              
941             Find all canonical domains a domain.
942              
943             =cut
944              
945             sub canonical_domain {
946 11     11 1 4133 my ($self, $domain) = @_;
947              
948             # Remove all leading and trailing dots.
949 11         17 $domain =~ s/^\.+//;
950 11         24 $domain =~ s/\.+$//;
951              
952             # Replace consecutive dots with a single dot.
953 11         31 while ($domain =~ s/\.\.+/\./g) { }
954              
955             # Lowercase the whole string.
956 11         13 $domain = lc $domain;
957              
958 11         13 my @domains = ($domain);
959              
960              
961 11 100       24 if ($domain =~ /^\d+\.\d+\.\d+\.\d+$/) { # loose check for IP address, should be enough
962 1         3 return @domains;
963             }
964              
965 10         24 my @parts = split/\./, $domain;
966 10         12 splice(@parts, 0, -6); # take 5 top most compoments
967              
968              
969 10         17 while (scalar @parts > 2) {
970 9         7 shift @parts;
971 9         22 push(@domains, join(".", @parts) );
972             }
973              
974 10         27 return @domains;
975             }
976              
977             =head2 canonical_path()
978              
979             Find all canonical paths for a URL.
980              
981             =cut
982              
983             sub canonical_path {
984 4     4 1 32 my ($self, $path) = @_;
985              
986 4         6 my @paths = ($path); # return full path
987            
988             # without query string
989 4 100       11 if ($path =~ /\?/) {
990 1         3 $path =~ s/\?.*$//;
991              
992 1         2 push(@paths, $path);
993             }
994              
995 4         8 my @parts = split /\//, $path;
996 4 50       7 if (scalar @parts > 4) {
997 0         0 @parts = splice(@parts, -4, 4);
998             }
999              
1000             # if (scalar @parts == 0) {
1001             # push(@paths, "/");
1002             # }
1003              
1004              
1005 4         6 my $previous = '';
1006 4         6 while (scalar @parts > 1) {
1007 4         6 my $val = shift(@parts);
1008 4         5 $previous .= "$val/";
1009              
1010 4         6 push(@paths, $previous);
1011             }
1012            
1013 4         10 return @paths;
1014             }
1015              
1016             =head2 canonical()
1017              
1018             Find all canonical URLs for a URL.
1019              
1020             =cut
1021              
1022             sub canonical {
1023 4     4 1 8220 my ($self, $url) = @_;
1024              
1025 4         8 my @urls = ();
1026              
1027             # my $uri = URI->new($url)->canonical;
1028 4         6 my $uri = $self->canonical_uri($url);
1029 4         129 my @domains = $self->canonical_domain($uri->host);
1030 4         14 my @paths = $self->canonical_path($uri->path_query);
1031              
1032 4         7 foreach my $domain (@domains) {
1033 10         8 foreach my $path (@paths) {
1034 22         28 push(@urls, "$domain$path");
1035             }
1036             }
1037              
1038 4         18 return @urls;
1039             }
1040              
1041              
1042             =head2 canonical_uri()
1043              
1044             Create a canonical URI.
1045              
1046             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.
1047              
1048             =cut
1049              
1050             sub canonical_uri {
1051 40     40 1 13769 my ($self, $url) = @_;
1052              
1053 40         88 $url = trim $url;
1054              
1055             # Special case for \t \r \n
1056 40         733 while ($url =~ s/^([^?]+)[\r\t\n]/$1/sgi) { }
1057              
1058 40         114 my $uri = URI->new($url)->canonical; # does not deal with directory traversing
1059              
1060             # $self->debug("0. $url => " . $uri->as_string . "\n");
1061              
1062            
1063 40 100 66     10068 if (! $uri->scheme() || $uri->scheme() eq '') {
1064 3         24 $uri = URI->new("http://$url")->canonical;
1065             }
1066              
1067 40         962 $uri->fragment('');
1068              
1069 40         339 my $escape = $uri->as_string;
1070              
1071             # Reduce double // to single / in path
1072 40         217 while ($escape =~ s/^([a-z]+:\/\/[^?]+)\/\//$1\//sgi) { }
1073              
1074              
1075             # Remove empty fragment
1076 40         94 $escape =~ s/#$//;
1077              
1078             # canonial does not handle ../
1079             # $self->debug("\t$escape\n");
1080 40         104 while($escape =~ s/([^\/])\/([^\/]+)\/\.\.([\/?].*)$/$1$3/gi) { }
1081 40         68 while($escape =~ s/([^\/])\/([^\/]+)\/\.\.$/$1/gi) { }
1082              
1083             # May have removed ending /
1084             # $self->debug("\t$escape\n");
1085 40 100       97 $escape .= "/" if ($escape =~ /^[a-z]+:\/\/[^\/\?]+$/);
1086 40         67 $escape =~ s/^([a-z]+:\/\/[^\/]+)(\?.*)$/$1\/$2/gi;
1087             # $self->debug("\t$escape\n");
1088              
1089             # other weird case if domain = digits only, try to translate it to IP address
1090 40 100       65 if ((my $domain = URI->new($escape)->host) =~/^\d+$/) {
1091 3         157 my $ip = Socket::inet_ntoa(Socket::inet_aton($domain));
1092              
1093 3         7 $uri = URI->new($escape);
1094 3         92 $uri->host($ip);
1095              
1096 3         169 $escape = $uri->as_string;
1097             }
1098              
1099             # $self->debug("1. $url => $escape\n");
1100              
1101             # Try to escape the path again
1102 40         1717 $url = $escape;
1103 40         62 while (($escape = URI::Escape::uri_unescape($url)) ne $escape) { # wrong for %23 -> #
1104 0         0 $url = $escape;
1105             }
1106             # while (($escape = URI->new($url)->canonical->as_string) ne $escape) { # breask more unit tests than previous
1107             # $url = $escape;
1108             # }
1109              
1110             # Fix for %23 -> #
1111 40         254 while($escape =~ s/#/%23/sgi) { }
1112              
1113             # $self->debug("2. $url => $escape\n");
1114              
1115             # Fix over escaping
1116 40         72 while($escape =~ s/^([^?]+)%%(%.*)$/$1%25%25$2/sgi) { }
1117 40         64 while($escape =~ s/^([^?]+)%%/$1%25%25/sgi) { }
1118              
1119             # URI has issues with % in domains, it gets the host wrong
1120              
1121             # 1. fix the host
1122             # $self->debug("Domain: " . URI->new($escape)->host . "\n");
1123 40         31 my $exception = 0;
1124 40         126 while ($escape =~ /^[a-z]+:\/\/[^\/]*([^a-z0-9%_.-\/:])[^\/]*(\/.*)$/) {
1125 3         4 my $source = $1;
1126 3         9 my $target = sprintf("%02x", ord($source));
1127              
1128 3         37 $escape =~ s/^([a-z]+:\/\/[^\/]*)\Q$source\E/$1%\Q$target\E/;
1129              
1130 3         13 $exception = 1;
1131             }
1132              
1133             # 2. need to parse the path again
1134 40 50 66     69 if ($exception && $escape =~ /^[a-z]+:\/\/[^\/]+\/(.+)/) {
1135 0         0 my $source = $1;
1136 0         0 my $target = URI::Escape::uri_unescape($source);
1137              
1138             # print "Source: $source\n";
1139 0         0 while ($target ne URI::Escape::uri_unescape($target)) {
1140 0         0 $target = URI::Escape::uri_unescape($target);
1141             }
1142              
1143            
1144 0         0 $escape =~ s/\/\Q$source\E/\/$target/;
1145              
1146 0         0 while ($escape =~ s/#/%23/sgi) { } # fragement has been removed earlier
1147 0         0 while ($escape =~ s/^([a-z]+:\/\/[^\/]+\/.*)%5e/$1\&/sgi) { } # not in the host name
1148             # while ($escape =~ s/%5e/&/sgi) { }
1149              
1150 0         0 while ($escape =~ s/%([^0-9a-f]|.[^0-9a-f])/%25$1/sgi) { }
1151             }
1152              
1153             # $self->debug("$url => $escape\n");
1154             # $self->debug(URI->new($escape)->as_string . "\n");
1155              
1156 40         70 return URI->new($escape);
1157             }
1158              
1159             =head2 full_hashes()
1160              
1161             Return all possible full hashes for a URL.
1162              
1163             =cut
1164              
1165             sub full_hashes {
1166 0     0 1   my ($self, $url) = @_;
1167              
1168 0           my @urls = $self->canonical($url);
1169 0           my @hashes = ();
1170              
1171 0           foreach my $url (@urls) {
1172             # $self->debug("$url\n");
1173 0           push(@hashes, sha256($url));
1174 0           $self->debug("$url " . $self->hex_to_ascii(sha256($url)) . "\n");
1175             }
1176              
1177 0           return @hashes;
1178             }
1179              
1180             =head2 request_full_hash()
1181              
1182             Request full full hashes for specific prefixes from Google.
1183              
1184             =cut
1185              
1186             sub request_full_hash {
1187 0     0 1   my ($self, %args) = @_;
1188 0   0       my $prefixes = $args{prefixes} || return ();
1189 0   0       my $size = $args{size} || length $prefixes->[0];
1190              
1191             # # Handle errors
1192 0           my $i = 0;
1193 0           my $errors;
1194             my $delay = sub {
1195 0     0     my $time = shift;
1196 0 0         if ((time() - $errors->{timestamp}) < $time) {
1197 0           splice(@$prefixes, $i, 1);
1198             }
1199             else {
1200 0           $i++;
1201             }
1202 0           };
1203              
1204 0           while ($i < scalar @$prefixes) {
1205 0           my $prefix = $prefixes->[$i];
1206              
1207 0           $errors = $self->{storage}->get_full_hash_error(prefix => $prefix);
1208 0 0 0       if (defined $errors && $errors->{errors} > 2) { # 2 errors is OK
1209             $errors->{errors} == 3 ? $delay->(30 * 60) # 30 minutes
1210 0 0         : $errors->{errors} == 4 ? $delay->(60 * 60) # 1 hour
    0          
1211             : $delay->(2 * 60 * 60); # 2 hours
1212             }
1213             else {
1214 0           $i++;
1215             }
1216             }
1217              
1218 0           my $url = $self->{server} . "gethash?client=api&key=" . $self->{key} . "&appver=$VERSION&pver=" . $self->{version};
1219              
1220 0           my $prefix_list = join('', @$prefixes);
1221 0           my $header = "$size:" . scalar @$prefixes * $size;
1222              
1223             # print @{$args{prefixes}}, "\n";
1224             # print $$prefixes[0], "\n"; return;
1225              
1226              
1227 0           my $res = $self->ua->post($url, Content => "$header\n$prefix_list");
1228              
1229 0 0         if (! $res->is_success) {
1230 0           $self->error("Full hash request failed\n");
1231 0           $self->debug($res->as_string . "\n");
1232              
1233 0           foreach my $prefix (@$prefixes) {
1234 0           my $errors = $self->{storage}->get_full_hash_error(prefix => $prefix);
1235 0 0 0       if (defined $errors && (
      0        
1236             $errors->{errors} >=2 # backoff mode
1237             || $errors->{errors} == 1 && (time() - $errors->{timestamp}) > 5 * 60)) { # 5 minutes
1238 0           $self->{storage}->full_hash_error(prefix => $prefix, timestamp => time()); # more complicate than this, need to check time between 2 errors
1239             }
1240             }
1241              
1242 0           return ();
1243             }
1244             else {
1245 0           $self->debug("Full hash request OK\n");
1246              
1247 0           foreach my $prefix (@$prefixes) {
1248 0           $self->{storage}->full_hash_ok(prefix => $prefix, timestamp => time());
1249             }
1250             }
1251              
1252 0           $self->debug($res->request->as_string . "\n");
1253 0           $self->debug($res->as_string . "\n");
1254             # $self->debug(substr($res->content, 0, 250), "\n\n");
1255              
1256 0           return $self->parse_full_hashes($res->content);
1257             }
1258              
1259             =head2 parse_full_hashes()
1260              
1261             Process the request for full hashes from Google.
1262              
1263             =cut
1264              
1265             sub parse_full_hashes {
1266 0     0 1   my ($self, $data) = @_;
1267              
1268 0           my @hashes = ();
1269              
1270             # 900
1271             # goog-malware-shavar:32:2:m
1272             # 01234567890123456789012345678901987654321098765432109876543210982
1273             # AA3
1274              
1275             # cache life time
1276 0           my $life = 0;
1277 0 0         if ($data =~ s/^(\d+)\n//) {
1278 0           $life = $1;
1279 0           $self->debug("Full hash life time: ", $life, "\n");
1280             }
1281             else {
1282 0           $self->error("Life time not found\n");
1283             }
1284              
1285 0           while (length $data > 0) {
1286 0 0         if ($data !~ /^[a-z-]+:\d+:\d+(:m)?\n/gi) { # goog-malware-shavar:32:1:m
1287 0           $self->error("list not found\n");
1288 0           return ();
1289             }
1290 0           $data =~ s/^([a-z-]+)://;
1291 0           my $list = $1;
1292            
1293 0           $data =~ s/^(\d+)://;
1294 0           my $length = $1;
1295 0           $self->debug("Full hash length: ", $length, "\n");
1296              
1297 0           $data =~ s/^(\d+)//;
1298 0           my $num = $1;
1299            
1300 0           $self->debug("Number of full hashes returned: ", $num, "\n");
1301              
1302 0           my $metadata = 0;
1303 0 0         if ($data =~ s/:m[\r\n]//) {
1304 0           $metadata = 1;
1305             }
1306              
1307 0           my $current = 0;
1308 0           my @local_hashes = ();
1309 0           while ($current < $num) {
1310 0           my $hash = substr($data, 0, $length, '');
1311 0           push(@local_hashes, { hash => $hash, list => $list, life => $life, type => 0 });
1312              
1313 0           $current ++;
1314             }
1315              
1316 0 0         if ($metadata) {
1317 0           my $count = 0;
1318 0           while ($data =~ s/(\d+)[\r\n]//) {
1319 0           my $meta_length = $1;
1320              
1321 0           my $info = substr($data, 0, $meta_length, '');
1322 0           $self->debug("Metadata: $info\n");
1323 0           my $extra = MalwarePatternType->decode($info);
1324              
1325             # update the type
1326 0           my $hash = $local_hashes[$count];
1327 0           $hash->{type} = $extra->{pattern_type};
1328 0           $local_hashes[$count] = $hash;
1329              
1330 0           $count++;
1331             }
1332             }
1333              
1334              
1335 0           push(@hashes, @local_hashes);
1336             }
1337              
1338 0           $self->debug("Number of hashes: ", scalar(@hashes), "\n");
1339 0           return @hashes;
1340             }
1341              
1342              
1343             =head2 create_range()
1344              
1345             Create a list of ranges (1-3, 5, 7-11) from a list of numbers.
1346              
1347             =cut
1348              
1349             sub create_range {
1350 0     0 1   my ($self, %args) = @_;
1351 0   0       my $numbers = $args{numbers} || []; # should already be ordered
1352              
1353 0 0         return '' if (scalar @$numbers == 0);
1354              
1355 0           my $range = $$numbers[0];
1356 0           my $new_range = 0;
1357 0           for(my $i = 1; $i < scalar @$numbers; $i++) {
1358             # next if ($$numbers[$i] == $$numbers[$i-1]); # should not happen
1359              
1360 0 0         if ($$numbers[$i] != $$numbers[$i-1] + 1) {
    0          
1361 0 0 0       $range .= $$numbers[$i-1] if ($i > 1 && $new_range == 1);
1362 0           $range .= ',' . $$numbers[$i];
1363              
1364 0           $new_range = 0
1365             }
1366             elsif ($new_range == 0) {
1367 0           $range .= "-";
1368 0           $new_range = 1;
1369             }
1370             }
1371 0 0         $range .= $$numbers[scalar @$numbers - 1] if ($new_range == 1);
1372              
1373 0           return $range;
1374             }
1375              
1376             =head2 expand_range()
1377              
1378             Explode list of ranges (1-3, 5, 7-11) into a list of numbers (1,2,3,5,7,8,9,10,11).
1379              
1380             =cut
1381              
1382             sub expand_range {
1383 0     0 1   my ($self, %args) = @_;
1384 0   0       my $range = $args{range} || return ();
1385              
1386 0           my @list = ();
1387 0           my @elements = split /,/, $range;
1388              
1389 0           foreach my $data (@elements) {
1390 0 0         if ($data =~ /^\d+$/) { # single number
    0          
1391 0           push(@list, $data);
1392             }
1393             elsif ($data =~ /^(\d+)-(\d+)$/) {
1394 0           my $start = $1;
1395 0           my $end = $2;
1396              
1397 0           for(my $i = $start; $i <= $end; $i++) {
1398 0           push(@list, $i);
1399             }
1400             }
1401             }
1402              
1403 0           return @list;
1404             }
1405              
1406             =head1 CHANGELOG
1407              
1408             =over 4
1409              
1410             =item 0.7
1411              
1412             Remove \r from metata data
1413              
1414             =item 0.6
1415              
1416             Many fixes: local database update, lcoal database lookup, full hash response parsing, etc.
1417              
1418             =back
1419              
1420             =head1 SEE ALSO
1421              
1422             See L for handling Google Safe Browsing v3.
1423              
1424             See L for the list of public functions.
1425              
1426             See L for a back-end using Sqlite.
1427              
1428             Google Safe Browsing v3 API: L
1429              
1430              
1431             =head1 AUTHOR
1432              
1433             Julien Sobrier, Ejulien@sobrier.netE
1434              
1435             =head1 COPYRIGHT AND LICENSE
1436              
1437             Copyright (C) 2015 by Julien Sobrier
1438              
1439             This library is free software; you can redistribute it and/or modify
1440             it under the same terms as Perl itself, either Perl version 5.8.8 or,
1441             at your option, any later version of Perl 5 you may have available.
1442              
1443              
1444             =cut
1445              
1446             1;
1447             __END__