File Coverage

blib/lib/WebService/AbuseIPDB.pm
Criterion Covered Total %
statement 100 100 100.0
branch 38 38 100.0
condition 12 17 70.5
subroutine 13 13 100.0
pod 4 4 100.0
total 167 172 97.0


line stmt bran cond sub pod time code
1             package WebService::AbuseIPDB;
2              
3 9     9   581746 use 5.010;
  9         115  
4 9     9   49 use strict;
  9         17  
  9         195  
5 9     9   40 use warnings;
  9         19  
  9         220  
6              
7             # use other modules
8 9     9   6900 use IO::Socket::SSL;
  9         724495  
  9         78  
9 9     9   6346 use REST::Client;
  9         399643  
  9         336  
10 9     9   80 use Carp;
  9         18  
  9         465  
11 9     9   2870 use JSON::XS;
  9         19788  
  9         441  
12 9     9   56 use URI; # The GET requests need URI-escaping
  9         19  
  9         9555  
13              
14             our $VERSION = '0.03_01';
15              
16             sub new {
17 9     9 1 8305 my ($class, %opts) = @_;
18             croak "Only version 2 is supported."
19 9 100 100     175 if exists $opts{ver} && 2 != $opts{ver};
20 8 100       185 croak "No key provided." unless exists $opts{key};
21              
22             # This is weird. If you don't set the outer timeout it goes to 300.
23             # If you do set the outer timeout, it sets the inner one too so it's
24             # effectively doubled. Setting the inner timeout has no effect.
25             my $self = {
26             ua => REST::Client->new (
27             { host => 'https://api.abuseipdb.com',
28             timeout => $opts{timeout} // 20,
29             agent => "WebService::AbuseIPDB/$VERSION",
30              
31             # useragent => LWP::UserAgent->new (ssl_opts => {
32             # verify_hostname => 1,
33             # },
34             # #timeout => $opts{timeout} // 20,
35             # )
36             }
37             ),
38             retry => $opts{retry} // 0,
39             api_ver => $opts{ver} // 2,
40             key => $opts{key},
41 7   100     101 };
      100        
      100        
42 7         27968 bless ($self, $class);
43 7         44 return $self;
44             }
45              
46             sub _send_receive {
47 13     13   162 my ($self, $meth, $path, $data) = @_;
48              
49 13         58 $path = "/api/v$self->{api_ver}/$path";
50 13         54 my $ct = {'Content-type' => 'application/json'};
51             my $headers = {
52             Accept => 'application/json',
53             Key => $self->{key}
54 13         56 };
55 13 100       48 if ($meth eq 'GET') {
56 9         64 my $u = URI->new ($path);
57 9         19189 $u->query_form (%$data);
58 9         967 $path = $u->as_string;
59             }
60 13         89 my $tries_left = $self->{retry} + 1;
61 13         45 while ($tries_left) {
62              
63 14 100       277 if ($meth eq 'GET') {
    100          
64 10         56 $self->{ua}->GET ($path, $headers);
65             } elsif ($meth eq 'POST') {
66 3         7 $headers->{'Content-type'} = 'application/json';
67 3         26 $self->{ua}->POST ($path, encode_json ($data), $headers);
68             } else {
69 1         180 croak "Unrecognised method '$meth'";
70             }
71              
72 13 100       13340641 if ($self->{ua}->responseCode !~ /400|50./) {
73             return decode_json $self->{ua}->responseContent
74 8 100       98 if $self->{ua}->responseHeader ('Content-type') eq
75             'application/json';
76 1         69 return undef;
77             }
78 5         137 $tries_left--;
79 5         22 warn "REST error " . $self->{ua}->responseCode;
80             }
81              
82 4 100       534 if ($self->{ua}->responseCode > 499) {
83             # Not our problem, so don't carp
84             return {
85             errors => [{
86             status => $self->{ua}->responseCode,
87             detail => $self->{ua}->responseContent //
88 3   50     49 'Server Problem'
89             }]
90             };
91             }
92              
93             carp "Problem with $meth $path\nData was " . encode_json ($data) .
94             "\nClient warning: " .
95             ( $self->{ua}->responseHeader ('Client-Warning') //
96             $self->{ua}->responseContent //
97 1   33     16 $self->{ua}->responseCode);
      33        
98 1         319 return undef;
99             }
100              
101             sub check {
102 8     8 1 3034 my ($self, %args) = @_;
103 8 100       41 unless (exists $args{ip}) {
104 1         169 carp "No IP in argument hash";
105 1         87 return;
106             }
107              
108             # Validate this here TODO
109 7         26 my $data = {ipAddress => $args{ip}};
110 7 100       26 $data->{maxAgeInDays} = $args{max_age} if exists $args{max_age};
111             # TODO $data->{verbose} = 1 if $args{verbose};
112 7         1787 require WebService::AbuseIPDB::CheckResponse;
113 7         35 return WebService::AbuseIPDB::CheckResponse->new (
114             $self->_send_receive ('GET', 'check', $data));
115              
116             }
117              
118             sub report {
119 5     5 1 4105 my ($self, %args) = @_;
120 5         14 for my $mand (qw/ip categories/) {
121 9 100       29 unless (exists $args{$mand}) {
122 2         250 carp "No '$mand' key in argument hash";
123 2         337 return;
124             }
125             }
126              
127             # More validation here
128 3         24 my $data = {ip => $args{ip}};
129              
130             # Form the category string
131             # More validation here too
132 3         518 require WebService::AbuseIPDB::Category;
133             my @categories =
134 3         8 map { WebService::AbuseIPDB::Category->new ($_) } @{$args{categories}};
  6         25  
  3         7  
135 3         10 $data->{categories} = join (',', map { $_->id } @categories);
  4         12  
136              
137             # Trim the comment
138             $data->{comment} = substr ($args{comment}, 0, 1024)
139 3 100       13 if defined $args{comment};
140              
141             # Run it
142 3         496 require WebService::AbuseIPDB::ReportResponse;
143 3         13 return WebService::AbuseIPDB::ReportResponse->new (
144             $self->_send_receive ('POST', 'report', $data));
145             }
146              
147             sub blacklist {
148 7     7 1 3658 my ($self, %args) = @_;
149 7         18 my $data = {
150             limit => 1000,
151             confidenceMinimum => 75
152             };
153              
154 7 100       20 if (exists $args{limit}) {
155 4 100       21 unless ($args{limit} =~ /^[0-9]+$/) {
156 1         146 carp "limit must be a whole number";
157 1         81 return;
158             }
159 3 100       9 if ($args{limit} < 1) {
160 1         64 carp "limit must be greater than zero";
161 1         65 return;
162             }
163 2         4 $data->{limit} = $args{limit};
164             }
165              
166 5 100       10 if (exists $args{min_abuse}) {
167 4 100       18 unless ($args{min_abuse} =~ /^[0-9]+$/) {
168 1         61 carp "min_abuse must be a whole number";
169 1         60 return;
170             }
171 3 100       21 if ($args{min_abuse} < 25) {
172 1         61 carp "min_abuse is $args{min_abuse} but must be greater than 24";
173 1         59 return;
174             }
175 2 100       6 if ($args{min_abuse} > 100) {
176 1         61 carp "min_abuse is $args{min_abuse} but must be less than 100";
177 1         58 return;
178             }
179 1         3 $data->{confidenceMinimum} = $args{min_abuse};
180             }
181              
182 2         459 require WebService::AbuseIPDB::BlacklistResponse;
183 2         8 require WebService::AbuseIPDB::BlacklistMember;
184 2         8 return WebService::AbuseIPDB::BlacklistResponse->new (
185             $self->_send_receive ('GET', 'blacklist', $data));
186              
187             }
188              
189             1;
190              
191             __END__