File Coverage

blib/lib/Net/Google/SafeBrowsing4/Storage/File.pm
Criterion Covered Total %
statement 36 237 15.1
branch 6 130 4.6
condition 0 75 0.0
subroutine 10 24 41.6
pod 12 14 85.7
total 64 480 13.3


line stmt bran cond sub pod time code
1             package Net::Google::SafeBrowsing4::Storage::File;
2              
3 3     3   131831 use strict;
  3         7  
  3         85  
4 3     3   17 use warnings;
  3         7  
  3         90  
5              
6 3     3   14 use base qw(Net::Google::SafeBrowsing4::Storage);
  3         7  
  3         903  
7              
8 3     3   21 use Carp;
  3         7  
  3         221  
9 3     3   18 use List::Util qw(first);
  3         8  
  3         219  
10 3     3   1640 use Path::Tiny;
  3         27842  
  3         177  
11 3     3   417 use Storable qw(nstore retrieve);
  3         2361  
  3         167  
12 3     3   1002 use List::BinarySearch qw(binsearch);
  3         4125  
  3         6753  
13              
14              
15             our $VERSION = '0.2';
16              
17             =head1 NAME
18              
19             Net::Google::SafeBrowsing4::Storage::File - File storage for the Google Safe Browsing v4 database
20              
21             =head1 SYNOPSIS
22              
23             use Net::Google::SafeBrowsing4::Storage::File;
24              
25             my $storage = Net::Google::SafeBrowsing4::Storage::File->new(path => '.');
26             $storage->save(...);
27              
28             =head1 DESCRIPTION
29              
30             This is the a file-based implementation of L to manage the Google Safe Browsing v4 local database.
31              
32             =cut
33              
34              
35             =head1 CONSTRUCTOR
36              
37             =over 4
38              
39             =back
40              
41             =head2 new()
42              
43             Create a Net::Google::SafeBrowsing4::Storage::File object
44              
45             my $storage => Net::Google::SafeBrowsing4::Storage::File->new(path => '.');
46              
47             Arguments
48              
49             =over 4
50              
51             =item path
52              
53             Optional. Path to store the database files Use current directory by default.
54              
55             =item keep_all
56              
57             Optional. Set to 1 to keep old information (such as expiring full hashes) in the database. 0 (delete) by default.
58              
59             =item sticky
60              
61             Optional. Set to 1 to if you are going to do multiple lookup. More memory will be used but lookup will be sped up. 0 by default.
62              
63             =item files
64              
65             Optional. Hash reference to map file types to file names. Default:
66              
67             {
68             updates => "updates.gdb4",
69             full_hashes => "full_hashes.gsb4"
70             }
71              
72             =item logger
73              
74             Optional. Log4Perl compatible object reference. By default this option is unset, making Net::Google::SafeBrowsing4::Storage::File silent.
75              
76             =back
77              
78             =cut
79              
80             sub new {
81 11     11 1 11289 my ($class, %args) = @_;
82              
83 11         83 my $self = { # default arguments
84             keep_all => 0,
85             path => '.',
86             sticky => 0,
87             files => {
88             updates => "updates.gdb4",
89             full_hashes => "full_hashes.gsb4"
90             },
91             data => { },
92             %args,
93             };
94              
95 11 50       51 bless($self, $class) or croak("Can't bless $class: $!");
96              
97 11         39 $self->init();
98              
99 11         373 return $self;
100             }
101              
102             =head1 PUBLIC FUNCTIONS
103              
104             =over 4
105              
106             See L for a complete list of public functions.
107              
108             =back
109              
110             =head2 close()
111              
112             Cleanup old full hashes, and close the connection to the database.
113              
114             $storage->close();
115              
116             =cut
117              
118              
119             sub init {
120 11     11 0 22 my ($self, %args) = @_;
121              
122             # make sure path exists
123 11 50       189 if (! -d $self->{path}) {
124 0 0       0 mkdir($self->{path}) or croak("Cannot create directory " . $self->{path} . ": $!\n");
125             }
126              
127             # file to hold all updates
128 11         60 my $file = path(join("/", $self->{path}, $self->{files}->{updates}));
129 11 100       434 if (! -e $file) {
130 1         33 my %update = (last_update => 0, next_update => 0, errors => 0);
131 1 50       4 if ($self->{sticky}) {
132 0         0 $self->{data}->{ $self->{files}->{updates} } = { %update };
133             }
134              
135 1 50       5 nstore(\%update, $file) or croak("Cannot store information into $file: $!\n");
136             }
137             }
138              
139             sub save {
140 0     0 1   my ($self, %args) = @_;
141 0   0       my $list = $args{list} || croak("Missing list information");
142 0   0       my $override = $args{override} || 0;
143 0 0         my @hashes = @{ $args{add} || [] };
  0            
144 0 0         my @remove = @{ $args{remove} || [] };
  0            
145 0   0       my $state = $args{'state'} || '';
146              
147             # save the information somewhere
148 0           my $file = path(join("/", $self->{path}, $self->list_to_file($list)));
149 0 0         $self->{logger} && $self->{logger}->debug("Save hashes to $file");
150              
151 0           my %data = ('state' => $state, hashes => [@hashes]); # hashes are already stored
152 0 0 0       if (-e $file && !$override) {
153 0           my $db = retrieve($file);
154 0 0         $self->{logger} && $self->{logger}->debug("Load $file (save)");
155              
156 0 0         $self->{logger} && $self->{logger}->debug("hashes to remove: ", scalar(@remove));
157 0 0         $self->{logger} && $self->{logger}->debug("hashes to add: ", scalar(@hashes));
158              
159 0 0         $self->{logger} && $self->{logger}->debug("Number of hashes before removal: ", scalar(@{ $db->{hashes} }));
  0            
160 0           foreach my $index (@remove) {
161 0 0         $self->{logger} && $self->{logger}->debug("Remove index $index");
162 0           $db->{hashes}->[$index] = '';
163             }
164 0           $db->{hashes} = [ grep { $_ ne '' } @{ $db->{hashes} } ];
  0            
  0            
165 0 0         $self->{logger} && $self->{logger}->debug("Number of hashes after removal: ", scalar(@{ $db->{hashes} }));
  0            
166              
167 0           $data{hashes} = [sort { $a cmp $b } (@hashes, @{ $db->{hashes} })];
  0            
  0            
168             }
169              
170 0 0         nstore(\%data, $file) or croak("Cannot save data to $file: $!\n");
171 0 0         if ($self->{sticky}) {
172 0           $self->{data}->{ $self->list_to_file($list) } = { %data };
173             }
174              
175             # return the list of hashes, sorted, from the new storage
176 0 0         $self->{logger} && $self->{logger}->debug("Number of hashes at end: ", scalar(@{ $data{hashes} }));
  0            
177 0           return @{ $data{hashes} };
  0            
178             }
179              
180              
181              
182             sub reset {
183 0     0 1   my ($self, %args) = @_;
184 0   0       my $list = $args{list} || croak("Missing list information");
185              
186 0           my $file = path(join("/", $self->{path}, $self->list_to_file($list)));
187 0           unlink($file);
188              
189 0 0         if ($self->{sticky}) {
190 0           $self->{data}->{ $self->list_to_file($list) } = { };
191             }
192             }
193              
194              
195             sub next_update {
196 0     0 1   my ($self, %args) = @_;
197              
198             # make sure the file exists
199 0           $self->init();
200              
201 0           my $update = { };
202 0 0 0       if ($self->{sticky} && exists($self->{data}->{ $self->{files}->{updates} })) {
203 0           $update = $self->{data}->{ $self->{files}->{updates} };
204             }
205             else {
206             # retrieve information from storage
207 0           my $file = path(join("/", $self->{path}, $self->{files}->{updates}));
208 0           $update = retrieve($file);
209 0 0         $self->{logger} && $self->{logger}->debug("Load $file (reset)");
210              
211 0 0         if ($self->{sticky}) {
212 0           $self->{data}->{ $self->{files}->{updates} } = $update;;
213             }
214             }
215              
216 0   0       return $update->{next_update} || 0;
217             }
218              
219             sub last_update {
220 0     0 1   my ($self, %args) = @_;
221              
222             # make sure the file exists
223 0           $self->init();
224              
225 0           my $update = { };
226 0 0 0       if ($self->{sticky} && exists($self->{data}->{ $self->{files}->{updates} })) {
227 0           $update = $self->{data}->{ $self->{files}->{updates} };
228             }
229             else {
230             # retrieve information from storage
231 0           my $file = path(join("/", $self->{path}, $self->{files}->{updates}));
232 0           $update = retrieve($file);
233 0 0         $self->{logger} && $self->{logger}->debug("Load $file (last_udpate)");
234              
235 0 0         if ($self->{sticky}) {
236 0           $self->{data}->{ $self->{files}->{updates} } = $update;
237             }
238             }
239              
240 0   0       return { last_update => $update->{last_update} || 0, errors => $update->{errors} || 0 };
      0        
241             }
242              
243              
244             sub get_state {
245 0     0 1   my ($self, %args) = @_;
246 0   0       my $list = $args{list} || croak("Missing list information\n");
247              
248 0           my $update = { };
249 0 0 0       if ($self->{sticky} && exists($self->{data}->{ $self->list_to_file($list) })) {
250 0           $update = $self->{data}->{ $self->list_to_file($list) };
251             }
252             else {
253 0           my $file = path(join("/", $self->{path}, $self->list_to_file($list)));
254 0 0         if (! -e $file) {
255 0           return "";
256             }
257             else {
258 0 0         $self->{logger} && $self->{logger}->debug("Load $file (get_state)");
259 0           $update = retrieve($file);
260              
261 0 0         if ($self->{sticky}) {
262 0           $self->{data}->{ $self->list_to_file($list) } = $update;
263             }
264             }
265             }
266              
267 0   0       return $update->{'state'} || '';
268             }
269              
270             sub updated {
271 0     0 1   my ($self, %args) = @_;
272 0   0       my $time = $args{'time'} || time();
273 0   0       my $next = $args{'next'} || time() + 1800;
274              
275             # next update applies to all lists, save it
276             # make sure the file exists
277 0           $self->init();
278              
279 0           my $file = path(join("/", $self->{path}, $self->{files}->{updates}));
280 0           my $update = { };
281 0 0 0       if ($self->{sticky} && exists($self->{data}->{ $self->{files}->{updates} })) {
282 0           $update = $self->{data}->{ $self->{files}->{updates} };
283             }
284             else {
285             # retrieve information from storage
286 0 0         $self->{logger} && $self->{logger}->debug("Load $file (updated)");
287 0           $update = retrieve($file);
288             }
289              
290 0           $update->{next_update} = $next;
291 0           $update->{last_udpate} = $time;
292 0           $update->{errors} = 0;
293              
294 0 0         nstore($update, $file) or croak("Cannot save data to $file: $!\n");
295              
296 0 0         if ($self->{sticky}) {
297 0           $self->{data}->{ $self->{files}->{updates} } = $update;
298             }
299             }
300              
301              
302             sub update_error {
303 0     0 1   my ($self, %args) = @_;
304 0   0       my $time = $args{'time'} || time();
305 0   0       my $wait = $args{'wait'} || 1800;
306 0   0       my $errors = $args{errors} || 0;
307              
308             # make sure the file exists
309 0           $self->init();
310              
311 0           my $file = path(join("/", $self->{path}, $self->{files}->{updates}));
312 0           my $update = { };
313 0 0 0       if ($self->{sticky} && exists($self->{data}->{ $self->{files}->{updates} })) {
314 0           $update = $self->{data}->{ $self->{files}->{updates} };
315             }
316             else {
317             # retrieve information from storage
318 0 0         $self->{logger} && $self->{logger}->debug("Load $file (update_error)");
319 0           $update = retrieve($file);
320             }
321              
322 0           $update->{next_update} = $time + $wait;
323 0           $update->{last_udpate} = $time;
324 0           $update->{errors} = $errors;
325              
326 0 0         nstore($update, $file) or croak("Cannot save data to $file: $!\n");
327 0 0         if ($self->{sticky}) {
328 0           $self->{data}->{ $self->{files}->{updates} } = $update;
329             }
330             }
331              
332              
333             sub get_prefixes {
334 0     0 1   my ($self, %args) = @_;
335 0 0         my @lists = @{ $args{lists} || [] };
  0            
336 0 0         my @hashes = @{ $args{hashes} || [] };
  0            
337 0           my @data = ();
338              
339 0 0         $self->{logger} && $self->{logger}->debug("Number of lists: ", scalar(@lists));
340 0           foreach my $list (@lists) {
341 0           my $db = { };
342 0 0 0       if ($self->{sticky} && exists($self->{data}->{ $self->list_to_file($list) })) {
343 0           $db = $self->{data}->{ $self->list_to_file($list) };
344             }
345             else {
346 0           my $file = path(join("/", $self->{path}, $self->list_to_file($list)));
347 0 0         if (! -e $file) {
348 0 0         $self->{logger} && $self->{logger}->debug("File $file does not exist");
349 0           next;
350             }
351              
352 0 0         $self->{logger} && $self->{logger}->debug("Load $file (get_prefixes)");
353 0           $db = retrieve($file);
354              
355 0 0         if ($self->{sticky}) {
356 0           $self->{data}->{ $self->list_to_file($list) } = $db;
357             }
358             }
359              
360 0           foreach my $hash (@hashes) {
361 0           my $prefix = undef;
362 0     0     my $index = binsearch {substr($a,0,length($b)) cmp $b} $hash, @{ $db->{hashes} };
  0            
  0            
363 0 0         if (defined $index) {
364 0           $prefix = $db->{hashes}->[$index];
365 0           push(@data, { prefix => $prefix, list => $list });
366             }
367             }
368             }
369              
370 0           return @data;
371             }
372              
373              
374             sub add_full_hashes {
375 0     0 1   my ($self, %args) = @_;
376 0 0         my @hashes = @{ $args{hashes} || [] };
  0            
377 0   0       my $timestamp = $args{timestamp} || time();
378              
379              
380 0           my $file = path(join("/", $self->{path}, $self->{files}->{full_hashes}));
381 0           my $db = { hashes => [] };
382 0 0 0       if ($self->{sticky} && exists($self->{data}->{ $self->{files}->{full_hashes} })) {
    0          
383 0           $db = $self->{data}->{ $self->{files}->{full_hashes} };
384             }
385             elsif (-e $file) {
386 0           $db = retrieve($file);
387             }
388              
389 0           foreach my $hash (@hashes) {
390 0           my $cache = $hash->{cache};
391 0           $cache =~ s/s//;
392 0 0         $self->{logger} && $self->{logger}->debug("cache: $cache");
393              
394 0           $hash->{expire} = $cache + $timestamp;
395 0           push(@{ $db->{hashes} }, $hash);
  0            
396             }
397              
398 0 0         $self->{logger} && $self->{logger}->debug("Save ", scalar(@{ $db->{hashes} }), " full hashes to $file");
  0            
399 0 0         nstore($db, $file) or croak("Cannot save data to $file: $!\n");
400              
401 0 0         if ($self->{sticky}) {
402 0           $self->{data}->{ $self->{files}->{full_hashes} } = $db;
403             }
404              
405 0           return (@{ $db->{hashes} });
  0            
406             }
407              
408              
409             sub get_full_hashes {
410 0     0 1   my ($self, %args) = @_;
411 0 0         my @lists = @{ $args{lists} || [] };
  0            
412 0   0       my $hash = $args{hash} || return ();
413              
414 0           my $db = { };
415 0 0 0       if ($self->{sticky} && exists($self->{data}->{ $self->{files}->{full_hashes} })) {
416 0           $db = $self->{data}->{ $self->{files}->{full_hashes} };
417             }
418             else {
419 0           my $file = path(join("/", $self->{path}, $self->{files}->{full_hashes}));
420 0 0         if (! -e $file) {
421 0           return ();
422             }
423              
424 0 0         $self->{logger} && $self->{logger}->debug("Load $file");
425 0           $db = retrieve($file);
426             }
427              
428 0           my @hashes = ();
429 0 0         $self->{logger} && $self->{logger}->debug("Number of full hashes on file: ", scalar(@{ $db->{hashes} }));
  0            
430 0           foreach my $list (@lists) {
431             my $result = first {
432             $_->{hash} eq $hash &&
433             $_->{list}->{threatEntryType} eq $list->{threatEntryType} &&
434             $_->{list}->{threatType} eq $list->{threatType} &&
435             $_->{list}->{platformType} eq $list->{platformType} &&
436             $_->{expire} > time()
437 0 0 0 0     } @{ $db->{hashes} };
  0   0        
  0   0        
438              
439 0 0         push(@hashes, $result) if (defined($result));
440             }
441              
442 0           return @hashes;
443             }
444              
445              
446             sub list_to_file {
447 0     0 0   my ($self, $list) = @_;
448              
449 0           return join("_", $list->{threatType}, $list->{platformType}, $list->{threatEntryType}) . ".gsb4";
450             }
451              
452              
453             sub close {
454 0     0 1   my ($self, %args) = @_;
455              
456 0 0         if ($self->{keep_all} == 0) {
457 0           return;
458             }
459              
460 0           my $file = path(join("/", $self->{path}, $self->{files}->{full_hashes}));
461 0 0         if (! -e $file) {
462 0           return;
463             }
464              
465 0           my $db = retrieve($file);
466              
467 0           my @results = grep { $_->{expire} > time() } @{ $db->{hashes} };
  0            
  0            
468 0 0         if (scalar(@results) < scalar(@{ $db->{hashes} })) {
  0            
469 0           $db->{hashes} = [@results];
470 0 0         nstore($db, $file) or croak("Cannot save data to $file: $!\n");
471             }
472              
473 0           $self->{data} = { };
474             }
475              
476             =head1 CHANGELOG
477              
478             =over 4
479              
480              
481             =item 0.1
482              
483             Initial release
484              
485             =back
486              
487             =head1 SEE ALSO
488              
489             See L for handling Google Safe Browsing v4.
490              
491             See L for the list of public functions.
492              
493             Google Safe Browsing v4 API: L
494              
495              
496             =head1 AUTHOR
497              
498             Julien Sobrier, Ejulien@sobrier.netE
499              
500             =head1 COPYRIGHT AND LICENSE
501              
502             Copyright (C) 2016 by Julien Sobrier
503              
504             This library is free software; you can redistribute it and/or modify
505             it under the same terms as Perl itself, either Perl version 5.8.8 or,
506             at your option, any later version of Perl 5 you may have available.
507              
508              
509             =cut
510              
511             1;