File Coverage

blib/lib/Net/Google/SafeBrowsing2/Redis.pm
Criterion Covered Total %
statement 18 228 7.8
branch 0 60 0.0
condition 0 102 0.0
subroutine 6 34 17.6
pod 18 28 64.2
total 42 452 9.2


line stmt bran cond sub pod time code
1             package Net::Google::SafeBrowsing2::Redis;
2              
3 1     1   24616 use strict;
  1         3  
  1         39  
4 1     1   6 use warnings;
  1         2  
  1         31  
5              
6 1     1   5 use base 'Net::Google::SafeBrowsing2::Storage';
  1         5  
  1         1027  
7              
8              
9 1     1   1687 use Carp;
  1         3  
  1         58  
10 1     1   798 use Redis::hiredis;
  1         743  
  1         29  
11 1     1   965 use Net::Google::SafeBrowsing2;
  1         195988  
  1         3291  
12              
13              
14             our $VERSION = '0.7';
15              
16              
17             =head1 NAME
18              
19             Net::Google::SafeBrowsing2::Redis - Redis as back-end storage for the Google Safe Browsing v2 database.
20              
21             The source code is available on github at L.
22              
23             =head1 SYNOPSIS
24              
25             use Net::Google::SafeBrowsing2::Redis;
26              
27             my $storage = Net::Google::SafeBrowsing2::Redis->new(host => '127.0.0.1', database => 1);
28             ...
29              
30             =head1 DESCRIPTION
31              
32             This is an implementation of L using Redis.
33              
34             =cut
35              
36              
37             =head1 CONSTRUCTOR
38              
39             =over 4
40              
41             =head2 new()
42              
43             Create a Net::Google::SafeBrowsing2::Redis object
44              
45             my $storage = Net::Google::SafeBrowsing2::Redis->new(
46             host => '127.0.0.1',
47             database => 0,
48             );
49              
50             Arguments
51              
52             =over 4
53              
54             =item host
55              
56             Optional. Redis host name. "127.0.01" by default
57              
58             =item database
59              
60             Optional. Redis database name to connect to. 0 by default.
61              
62             =item port
63              
64             Optional. Redis port number to connect to. 6379 by default.
65              
66             =item backward_compatible
67              
68             Optional. Stay backward compatible with 0.3, but requires a bigger Redis database. 0 (disabled) by default
69              
70             =item keep_all
71              
72             Optional. Keel all full hashes, even after they expire (45 minutes). 0 (disabled) by default
73              
74             =back
75              
76             =back
77              
78             =cut
79              
80             sub new {
81 0     0 1   my ($class, %args) = @_;
82              
83 0           my $self = { # default arguments
84             host => '127.0.0.1',
85             database => 0,
86             port => 6379,
87             backward_compatible => 0,
88             keep_all => 0,
89              
90             %args,
91             };
92              
93 0 0         bless $self, $class or croak "Can't bless $class: $!";
94              
95 0           return $self;
96             }
97              
98             =head1 PUBLIC FUNCTIONS
99              
100             =over 4
101              
102             See L for the list of public functions.
103              
104             =cut
105              
106              
107             sub redis {
108 0     0 0   my ($self, %args) = @_;
109              
110 0 0         if (! exists ($self->{redis})) {
111 0           my $redis = Redis::hiredis->new();
112 0           $redis->connect( $self->{host}, $self->{port} );
113 0           $redis->select( $self->{database} );
114 0           $self->{redis} = $redis;
115             }
116              
117 0           return $self->{redis};
118             }
119              
120             my %mapping = (
121             Net::Google::SafeBrowsing2::MALWARE => 'm',
122             Net::Google::SafeBrowsing2::PHISHING => 'p',
123              
124             'm' => Net::Google::SafeBrowsing2::MALWARE,
125             'p' => Net::Google::SafeBrowsing2::PHISHING,
126             );
127              
128             sub map {
129 0     0 0   my ($self, $key) = @_;
130              
131 0 0         return $key if ($self->{backward_compatible});
132              
133 0   0       return $mapping{$key} || $key;
134             }
135              
136              
137             sub add_chunks {
138 0     0 1   my ($self, %args) = @_;
139 0   0       my $type = $args{type} || 'a';
140 0   0       my $chunknum = $args{chunknum} || 0;
141 0   0       my $chunks = $args{chunks} || [];
142 0   0       my $list = $args{'list'} || '';
143              
144 0 0         $list = $self->map($list) unless ($self->{backward_compatible});
145              
146 0 0         if ($type eq 's') {
    0          
147 0           $self->add_chunks_s(chunknum => $chunknum, chunks => $chunks, list => $list);
148             }
149             elsif ($type eq 'a') {
150 0           $self->add_chunks_a(chunknum => $chunknum, chunks => $chunks, list => $list);
151             }
152              
153 0           my $redis = $self->redis();
154              
155 0           my $key = $type . $list;
156 0           $redis->zadd($key, $chunknum, $chunknum);
157              
158 0 0         if (scalar @$chunks == 0) { # keep empty chunks
159 0           my $key = $type . $chunknum . $list;
160              
161 0           $redis->sadd($type . "l$chunknum$list", $key);
162             }
163             }
164              
165             sub add_chunks_a {
166 0     0 0   my ($self, %args) = @_;
167 0   0       my $chunknum = $args{chunknum} || 0;
168 0   0       my $chunks = $args{chunks} || [];
169 0   0       my $list = $args{'list'} || '';
170              
171             # list already mapped by add_chunks
172 0           my $redis = $self->redis();
173              
174 0           foreach my $chunk (@$chunks) {
175 0           my $key = "a$chunknum" . $chunk->{host} . $chunk->{prefix} . $list;
176 0           $redis->hmset($key, "list", $list, "hostkey", $chunk->{host}, "prefix", $chunk->{prefix}, "chunknum", $chunknum);
177            
178 0           $redis->sadd("al$chunknum$list", $key);
179 0           $redis->sadd("ah" . $chunk->{host}, $key);
180             }
181             }
182              
183             sub add_chunks_s {
184 0     0 0   my ($self, %args) = @_;
185 0   0       my $chunknum = $args{chunknum} || 0;
186 0   0       my $chunks = $args{chunks} || [];
187 0   0       my $list = $args{'list'} || '';
188              
189             # list already mapped by add_chunks
190 0           my $redis = $self->redis();
191              
192 0           foreach my $chunk (@$chunks) {
193 0           my $key = "s$chunknum" . $chunk->{host} . $chunk->{prefix} . $chunk->{add_chunknum} . $list;
194 0           $redis->hmset($key, "list", $list, "hostkey", $chunk->{host}, "prefix", $chunk->{prefix}, "addchunknum", $chunk->{add_chunknum}, "chunknum", $chunknum);
195              
196 0           $redis->sadd("sl$chunknum$list", $key);
197 0           $redis->sadd("sh" . $chunk->{host}, $key);
198             }
199             }
200              
201              
202             # TODO: avoid duplicate code
203             sub get_add_chunks {
204 0     0 1   my ($self, %args) = @_;
205 0   0       my $hostkey = $args{hostkey} || '';
206              
207 0           my @list = ();
208 0           my $redis = $self->redis();
209              
210 0           my $keys = $redis->smembers("ah$hostkey");
211              
212 0           foreach my $key (@$keys) {
213 0 0         if (! $redis->exists($key)) { # clean up
214 0           $redis->srem("ah$hostkey", $key);
215 0           next;
216             }
217 0           my $chunk = to_hash($redis->hgetall($key));
218 0 0         $chunk->{list} = $self->map($chunk->{list}) unless ($self->{backward_compatible});
219 0 0         push(@list, $chunk) if ($chunk->{hostkey} eq $hostkey);
220             }
221              
222 0           return @list;
223             }
224              
225             sub get_sub_chunks {
226 0     0 1   my ($self, %args) = @_;
227 0   0       my $hostkey = $args{hostkey} || '';
228              
229 0           my @list = ();
230 0           my $redis = $self->redis();
231 0           my $keys = $redis->smembers("sh$hostkey");
232              
233 0           foreach my $key (@$keys) {
234 0 0         if (! $redis->exists($key)) { # cleanup
235 0           $redis->srem("sh$hostkey", $key);
236 0           next;
237             }
238              
239 0           my $chunk = to_hash($redis->hgetall($key));
240 0 0         $chunk->{list} = $self->map($chunk->{list}) unless ($self->{backward_compatible});
241 0 0         push(@list, $chunk) if ($chunk->{hostkey} eq $hostkey);
242             }
243              
244 0           return @list;
245             }
246              
247             sub get_add_chunks_nums {
248 0     0 1   my ($self, %args) = @_;
249 0   0       my $list = $args{'list'} || '';
250              
251             # list already mapped by get_chunks_nums
252 0           return $self->get_chunks_nums(type => 'a', list => $list);
253             }
254              
255             sub get_sub_chunks_nums {
256 0     0 1   my ($self, %args) = @_;
257 0   0       my $list = $args{'list'} || '';
258              
259             # list already mapped by get_chunks_nums
260 0           return $self->get_chunks_nums(type => 's', list => $list);
261             }
262              
263             sub get_chunks_nums {
264 0     0 0   my ($self, %args) = @_;
265 0   0       my $list = $args{'list'} || '';
266 0   0       my $type = $args{type} || 'a';
267              
268 0 0         $list = $self->map($list) unless ($self->{backward_compatible});
269 0           my $key = "$type$list";
270 0           my $values = $self->redis()->zrangebyscore($key, "-inf", "+inf");
271 0           return @$values;
272             }
273              
274              
275             sub delete_add_ckunks {
276 0     0 0   my ($self, %args) = @_;
277 0   0       my $chunknums = $args{chunknums} || [];
278 0   0       my $list = $args{'list'} || '';
279              
280 0 0         $list = $self->map($list) unless ($self->{backward_compatible});
281 0           my $redis = $self->redis();
282              
283 0           foreach my $num (@$chunknums) {
284 0           my $list2 = "al$num$list";
285 0           while ($redis->scard($list2) > 0) {
286 0           my $key = $redis->spop($list2);
287              
288 0           my $host = $redis->hget($key, 'hostkey');
289             # Remove key from this list
290 0           $redis->srem("ah$host", $key);
291 0 0         if ($redis->scard("ah$host") == 0) {
292 0           $redis->del("ah$host");
293             }
294              
295 0           $redis->del($key);
296             }
297 0           $redis->del($list); # list is empty now
298              
299 0           $redis->zrem("a$list", $num);
300              
301             # empty chunks
302 0           $redis->del("a$num" . $list);
303             }
304             }
305              
306              
307             sub delete_sub_ckunks {
308 0     0 0   my ($self, %args) = @_;
309 0   0       my $chunknums = $args{chunknums} || [];
310 0   0       my $list = $args{'list'} || '';
311              
312 0 0         $list = $self->map($list) unless ($self->{backward_compatible});
313 0           my $redis = $self->redis();
314              
315 0           foreach my $num (@$chunknums) {
316 0           my $list2 = "sl$num$list";
317 0           while ($redis->scard($list2) > 0) {
318 0           my $key = $redis->spop($list2);
319              
320 0           my $host = $redis->hget($key, 'hostkey');
321             # Remove key from this list
322 0           $redis->srem("sh$host", $key);
323 0 0         if ($redis->scard("sh$host") == 0) {
324 0           $redis->del("sh$host");
325             }
326              
327 0           $redis->del($key);
328             }
329 0           $redis->del($list); # list is empty now
330              
331 0           $redis->zrem("s$list", $num);
332              
333             # empty chunks
334 0           $redis->del("s$num" . $list);
335             }
336             }
337              
338             sub get_full_hashes {
339 0     0 1   my ($self, %args) = @_;
340 0   0       my $chunknum = $args{chunknum} || 0;
341 0   0       my $timestamp = $args{timestamp} || 0;
342 0   0       my $list = $args{list} || '';
343              
344 0 0         $list = $self->map($list) unless ($self->{backward_compatible});
345 0           my @hashes = ();
346 0           my $redis = $self->redis();
347              
348 0           my $keys = $redis->keys("h$chunknum*$list");
349 0           foreach my $key (@$keys) {
350 0           my $chunk = to_hash($redis->hgetall($key));
351 0 0 0       push(@hashes, $chunk->{hash}) if ($chunk->{chunknum} == $chunknum
      0        
      0        
352             && exists($chunk->{timestamp}) && exists($chunk->{hash})
353             && $chunk->{timestamp} >= $timestamp);
354             }
355              
356 0           return @hashes;
357             }
358              
359             sub updated {
360 0     0 1   my ($self, %args) = @_;
361 0   0       my $time = $args{'time'} || time();
362 0   0       my $wait = $args{'wait'} || 1800;
363 0   0       my $list = $args{'list'} || '';
364              
365 0 0         $list = $self->map($list) unless ($self->{backward_compatible});
366 0           $self->redis()->hmset($list, "time", $time, "errors", 0, "wait", $wait);
367             }
368              
369             sub update_error {
370 0     0 1   my ($self, %args) = @_;
371 0   0       my $time = $args{'time'} || time();
372 0   0       my $list = $args{'list'} || '';
373 0   0       my $wait = $args{'wait'} || 60;
374 0   0       my $errors = $args{errors} || 1;
375              
376 0 0         $list = $self->map($list) unless ($self->{backward_compatible});
377 0           $self->redis()->hmset($list, "time", $time, "errors", $errors, "wait", $wait);
378             }
379              
380             sub last_update {
381 0     0 1   my ($self, %args) = @_;
382 0   0       my $list = $args{'list'} || '';
383              
384 0 0         $list = $self->map($list) unless ($self->{backward_compatible});
385 0           my $keys = $self->redis()->keys($list);
386 0 0         if (scalar @$keys > 0) {
387 0           return to_hash($self->redis()->hgetall($keys->[0]));
388             }
389             else {
390 0           return {'time' => 0, 'wait' => 0, errors => 0};
391             }
392             }
393              
394             sub add_full_hashes {
395 0     0 1   my ($self, %args) = @_;
396 0   0       my $timestamp = $args{timestamp} || time();
397 0   0       my $full_hashes = $args{full_hashes} || [];
398              
399 0           my $redis = $self->redis();
400              
401 0           foreach my $hash (@$full_hashes) {
402 0           my $key = "h" . $hash->{chunknum} . $hash->{hash} . $self->map( $hash->{list} );
403 0           $redis->hmset($key, "chunknum", $hash->{chunknum}, "hash", $hash->{hash}, "timestamp", $timestamp);
404 0 0         $redis->expire($key, 45 * 60) unless ($self->{keep_all});
405             }
406             }
407              
408              
409             sub delete_full_hashes {
410 0     0 1   my ($self, %args) = @_;
411 0   0       my $chunknums = $args{chunknums} || [];
412 0   0       my $list = $args{list} || croak "Missing list name\n";
413              
414 0 0         $list = $self->map($list) unless ($self->{backward_compatible});
415 0           my $redis = $self->redis();
416              
417 0           my @keys = $redis->keys("h*$list");
418 0           foreach my $key (@keys) {
419 0           foreach my $num (@$chunknums) {
420 0 0         $redis->del($key) if ($key =~ /^h$num/);
421             }
422             }
423             }
424              
425              
426             sub full_hash_error {
427 0     0 1   my ($self, %args) = @_;
428 0   0       my $timestamp = $args{timestamp} || time();
429 0   0       my $prefix = $args{prefix} || '';
430              
431 0           my $key = "eh$prefix";
432              
433 0           my $keys = $self->redis()->keys($key);
434 0 0         if (scalar(@$keys) == 0) {
435 0           $self->redis()->hmset($key, "prefix", $prefix, "errors", 0, "timestamp", $timestamp);
436             }
437             else {
438 0           $self->redis()->hincrby($key, "errors", 1);
439 0           $self->redis()->hset($key, "timestamp", $timestamp);
440             }
441             }
442              
443             sub full_hash_ok {
444 0     0 1   my ($self, %args) = @_;
445 0   0       my $timestamp = $args{timestamp} || time();
446 0   0       my $prefix = $args{prefix} || '';
447              
448 0           $self->redis()->del("eh$prefix");
449             }
450              
451             sub get_full_hash_error {
452 0     0 1   my ($self, %args) = @_;
453 0   0       my $prefix = $args{prefix} || '';
454              
455 0           my $key = "eh$prefix";
456 0           my $keys = $self->redis()->keys($key);
457              
458 0 0         if (scalar(@$keys) > 0 ) {
459 0           return to_hash( $self->redis()->hgetall($key) );
460             }
461             else {
462             # no error
463 0           return undef;
464             }
465             }
466              
467             # TODO: init() to set empty mac keys
468             sub get_mac_keys {
469 0     0 1   my ($self, %args) = @_;
470              
471 0 0         if (scalar($self->redis()->keys("mac")) == 0) {
472 0           return { client_key => '', wrapped_key => '' };
473             }
474             else {
475 0           return to_hash($self->redis()->hgetall("mac"));
476             }
477             }
478              
479             sub add_mac_keys {
480 0     0 0   my ($self, %args) = @_;
481 0   0       my $client_key = $args{client_key} || '';
482 0   0       my $wrapped_key = $args{wrapped_key} || '';
483              
484 0           $self->redis()->hmset("mac", "client_key", $client_key, "wrapped_key", $wrapped_key);
485             }
486              
487             sub delete_mac_keys {
488 0     0 1   my ($self, %args) = @_;
489              
490 0           $self->redis()->hmset("mac", "client_key", '', "wrapped_key", '');
491             }
492              
493              
494             sub reset {
495 0     0 1   my ($self, %args) = @_;
496              
497 0           $self->redis()->flushdb();
498             }
499              
500             sub close {
501 0     0 0   my ($self, %args) = @_;
502             }
503              
504              
505             sub to_hash {
506 0     0 0   my ($data) = @_;
507              
508 0           my $result = { };
509              
510 0           my @elements = @$data;
511 0           while(my ($key, $value) = splice(@elements,0,2)) {
512 0           $result->{$key} = $value
513             }
514              
515 0           return $result;
516             }
517              
518             =back
519              
520             =head1 BENCHMARK
521              
522             =over 4
523              
524             Here are some numbers comparing the MySQL 0.6 back-end and Redis 0.4 back-end:
525              
526             Database update, from empty to full update:
527             MySQL: 1330s
528             Redis 2.4: 351s
529              
530             10,000 URLs lookup
531             MySQL: 6s
532             Redis 2.4: 5s
533              
534             Storage:
535             MySQL: 154MB
536             Redis 2.4: 780MB
537              
538             =back
539              
540              
541             =head1 CHANGELOG
542              
543             =over 4
544              
545             =item 0.7
546              
547             FIX: chunks were not deleted correctly.
548              
549             =item 0.6
550              
551             FIX: some keys were never deleted from Redis.
552              
553             =item 0.4
554              
555             New options backward_compatible and keep_all.
556              
557             Save 140MB in Redis (as of 08/01/2012)
558              
559             =item 0.3
560              
561             Break backward compatibility with previous versions. Make sure you start from a fresh database (reset your existing database if needed).
562              
563             Improve performances, fixes lookup. Requires 920MB for a full database (as of 07/31/2012)
564              
565             =back
566              
567             =head1 SEE ALSO
568              
569             See L for handling Google Safe Browsing v2.
570              
571             See L for the list of public functions.
572              
573             See L for a back-end using Sqlite.
574              
575             Google Safe Browsing v2 API: L
576              
577              
578             =head1 AUTHOR
579              
580             Julien Sobrier, Ejsobrier@zscaler.comE or Ejulien@sobrier.netE
581              
582             =head1 COPYRIGHT AND LICENSE
583              
584             Copyright (C) 2012 by Julien Sobrier
585              
586             This library is free software; you can redistribute it and/or modify
587             it under the same terms as Perl itself, either Perl version 5.8.8 or,
588             at your option, any later version of Perl 5 you may have available.
589              
590              
591             =cut
592              
593             1;