File Coverage

blib/lib/Robots/Validate.pm
Criterion Covered Total %
statement 49 52 94.2
branch 14 18 77.7
condition 4 7 57.1
subroutine 11 13 84.6
pod 1 1 100.0
total 79 91 86.8


line stmt bran cond sub pod time code
1             package Robots::Validate;
2              
3             # ABSTRACT: Validate that IP addresses are associated with known robots
4              
5 1     1   633240 use v5.14;
  1         4  
6              
7 1     1   825 use Moo 1;
  1         12055  
  1         7  
8              
9 1     1   3200 use MooX::Const v0.4.0;
  1         325104  
  1         6  
10 1     1   215 use List::Util 1.33 qw/ first none /;
  1         23  
  1         87  
11 1     1   9 use Net::DNS::Resolver;
  1         2  
  1         45  
12 1     1   593 use Ref::Util qw/ is_plain_hashref /;
  1         794  
  1         96  
13 1     1   7 use Types::Standard -types;
  1         2  
  1         8  
14              
15             # RECOMMEND PREREQ: Type::Tiny::XS
16             # RECOMMEND PREREQ: Ref::Util::XS
17              
18 1     1   9786 use namespace::autoclean;
  1         2  
  1         11  
19              
20             our $VERSION = 'v0.2.9';
21              
22              
23             has resolver => (
24             is => 'lazy',
25             isa => InstanceOf ['Net::DNS::Resolver'],
26             builder => 1,
27             );
28              
29             sub _build_resolver {
30 0     0   0 return Net::DNS::Resolver->new;
31             }
32              
33              
34             has robots => (
35             is => 'const',
36             isa => ArrayRef [
37             Dict [
38             name => Str,
39             agent => Optional [RegexpRef],
40             domain => RegexpRef,
41             ]
42             ],
43             lazy => 1,
44             strict => 0,
45             builder => 1,
46             );
47              
48             sub _build_robots {
49             return [
50              
51             {
52 0     0   0 name => 'Amazonbot',
53             agent => qr/Amazonbot\b/,
54             domain => qr/\.crawl\.amazonbot\.amazon$/,
55             },
56              
57             {
58             name => 'Applebot',
59             agent => qr/Applebot\b/,
60             domain => qr/\.applebot\.apple\.com$/,
61             },
62              
63             {
64             name => 'Arquivo.pt',
65             agent => qr/arquivo-web-crawler/,
66             domain => qr/\.arquivo\.pt$/,
67             },
68              
69             {
70             name => 'Baidu',
71             agent => qr/Baiduspider\b/,
72             domain => qr/\.crawl\.baidu\.com$/,
73              
74             },
75              
76             {
77             name => 'Bing',
78             agent => qr/(?:Bingbot|MSNBot|AdIdxBot|BingPreview)\b/i,
79             domain => qr/\.search\.msn\.com$/,
80              
81             },
82              
83             {
84             name => 'CocCoc',
85             agent => qr/coccocbot-web\b/,
86             domain => qr/\.coccoc\.com$/,
87             },
88              
89             {
90             name => 'DataProvider',
91             agent => qr/Dataprovider\.com/,
92             domain => qr/\.dataproviderbot\.com$/,
93             },
94              
95             {
96             name => 'Embedly',
97             agent => qr/Embedly\b/,
98             domain => qr/\.embed\.ly$/,
99             },
100              
101             {
102             name => 'Headline',
103             agent => qr/ev-crawler\b/,
104             domain => qr/\.headline\.com$/,
105             },
106              
107             {
108             name => 'Exabot',
109             agent => qr/Exabot\b/i,
110             domain => qr/\.exabot\.com$/,
111             },
112              
113             {
114             name => 'Google',
115             agent => qr/Google(?:bot?)\b/i,
116             domain => qr/\.google(?:bot)?\.com$/,
117             },
118              
119             {
120             name => 'InfoTiger',
121             agent => qr/InfoTigerBot\b/,
122             domain => qr/\.infotiger\.com$/,
123             },
124              
125             {
126             name => 'IONOS',
127             agent => qr/IonCrawl\b/,
128             domain => qr/\.1and1\.org$/,
129             },
130              
131             {
132             name => 'LinkedIn',
133             agent => qr/LinkedInBot\b/,
134             domain => qr/\.linkedin\.com$/,
135             },
136              
137             {
138             name => 'Mojeek',
139             agent => qr/MojeekBot\b/,
140             domain => qr/\.mojeek\.com$/,
141             },
142              
143             {
144             name => 'Monsido',
145             agent => qr{Monsidobot\b}ao,
146             domain => qr{\.monsido\.com$}ao,
147             },
148              
149             {
150             name => 'PetalBot',
151             agent => qr/PetalBot\b/,
152             domain => qr/\.petalsearch\.com$/,
153             },
154              
155             {
156             name => 'Pinterest',
157             agent => qr/Pinterest\b/,
158             domain => qr/\.pinterest\.com$/,
159             },
160              
161             {
162             name => 'Qwant',
163             agent => qr/Qwantify\b/,
164             domain => qr/\.qwant\.com$/,
165             },
166              
167             {
168             name => 'SeznamBot',
169             agent => qr/Seznam\b/,
170             domain => qr/\.seznam\.cz$/,
171             },
172              
173             {
174             name => 'Sogou',
175             agent => qr/Sogou\b/,
176             domain => qr/\.sogou\.com$/,
177             },
178              
179             {
180             name => 'Yahoo',
181             agent => qr/Slurp/,
182             domain => qr/\.crawl\.yahoo\.net$/,
183              
184             },
185              
186             {
187             name => "Yandex",
188             agent => qr/Yandex/,
189             domain => qr/\.yandex\.(?:com|ru|net)$/,
190             },
191              
192             {
193             name => 'Yeti',
194             agent => qr/naver\.me\b/,
195             domain => qr/\.naver\.com$/,
196             },
197              
198             ];
199             }
200              
201              
202             has die_on_error => (
203             is => 'lazy',
204             isa => Bool,
205             default => 0,
206             );
207              
208              
209             sub validate {
210 5     5 1 15200 my ( $self, $ip, $args ) = @_;
211              
212 5 100 66     28 if (is_plain_hashref($ip) && !$args) {
213 1         4 $args = { agent => $ip->{HTTP_USER_AGENT} };
214 1         3 $ip = $ip->{REMOTE_ADDR};
215             }
216              
217 5         184 my $res = $self->resolver;
218              
219             # Reverse DNS
220              
221 5         50 my $hostname;
222              
223 5 100       41 if ( my $reply = $res->query($ip, 'PTR') ) {
224 4         1006 ($hostname) = map { $_->ptrdname } $reply->answer;
  4         38  
225             }
226             else {
227 1 50       350 die $res->errorstring if $self->die_on_error;
228             }
229              
230 5 100       142 return unless $hostname;
231              
232 4   100     17 $args //= {};
233              
234 4         9 my $agent = $args->{agent};
235             my @matches =
236 4 100       8 grep { !$agent || $agent =~ $_->{agent} } @{ $self->robots };
  4         77  
  4         189  
237              
238 4 50 0     23 my $reply = $res->search( $hostname, "A" )
239             or $self->die_on_error && die $res->errorstring;
240              
241 4 50       788 return unless $reply;
242              
243 4 50       31 if (
244 4     4   68 none { $_ eq $ip } (
245 4         13 map { $_->address }
246 4         52 grep { $_->can('address') } $reply->answer
247             )
248             )
249             {
250 0         0 return;
251             }
252              
253 4 100   3   30 if ( my $match = first { $hostname =~ $_->{domain} } @matches ) {
  3         42  
254              
255             return {
256 3         75 %$match,
257             hostname => $hostname,
258             ip_address => $ip,
259             };
260              
261             }
262              
263 1         14 return;
264             }
265              
266              
267             1;
268              
269             __END__
270              
271             =pod
272              
273             =encoding UTF-8
274              
275             =head1 NAME
276              
277             Robots::Validate - Validate that IP addresses are associated with known robots
278              
279             =head1 VERSION
280              
281             version v0.2.9
282              
283             =head1 SYNOPSIS
284              
285             use Robots::Validate;
286              
287             my $rv = Robots::Validate->new;
288              
289             ...
290              
291             if ( $rs->validate( $ip, \%opts ) ) { ... }
292              
293             =head1 DESCRIPTION
294              
295             =head1 ATTRIBUTES
296              
297             =head2 C<resolver>
298              
299             This is the L<Net::DNS::Resolver> used for DNS lookups.
300              
301             =head2 C<robots>
302              
303             This is an array reference of rules with information about
304             robots. Each item is a hash reference with the following keys:
305              
306             =over
307              
308             =item C<name>
309              
310             The name of the robot.
311              
312             =item C<agent>
313              
314             A regular expression for matching against user agent names.
315              
316             =item C<domain>
317              
318             A regular expression for matching against the hostname.
319              
320             =back
321              
322             =head2 C<die_on_error>
323              
324             When true, L</validate> will die on a L</resolver> failure.
325              
326             By default it is false.
327              
328             =head1 METHODS
329              
330             =head2 C<validate>
331              
332             my $result = $rv->validate( $ip, \%opts );
333              
334             This method attempts to validate that an IP address belongs to a known
335             robot by first looking up the hostname that corresponds to the IP address,
336             and then validating that the hostname resolves to that IP address.
337              
338             If this succeeds, it then checks if the hostname is associated with a
339             known web robot.
340              
341             If that succeeds, it returns a copy of the matched rule from L</robots>.
342              
343             You can specify the following C<%opts>:
344              
345             =over
346              
347             =item C<agent>
348              
349             This is the user-agent string. If it does not match, then the DNS lookups
350             will not be performed.
351              
352             It is optional.
353              
354             =back
355              
356             Alternatively, you can pass in a Plack environment:
357              
358             my $result = $rv->validate($env);
359              
360             =head1 KNOWN ISSUES
361              
362             =head2 Undocumented Rules
363              
364             Many of these rules are not documented, but have been guessed from web
365             traffic.
366              
367             =head2 Limitations
368              
369             The current module can only be used for systems that consistently
370             support reverse DNS lookups. This means that it cannot be used to
371             validate some robots from
372             L<Facebook|https://developers.facebook.com/docs/sharing/webmasters/crawler>
373             or Twitter.
374              
375             =head1 SUPPORT FOR OLDER PERL VERSIONS
376              
377             This module requires Perl v5.14 or later.
378              
379             Future releases may only support Perl versions released in the last ten years.
380              
381             =head1 SEE ALSO
382              
383             =over
384              
385             =item L<Verifying Bingbot|https://www.bing.com/webmaster/help/how-to-verify-bingbot-3905dc26>
386              
387             =item L<Verifying Googlebot|https://support.google.com/webmasters/answer/80553>
388              
389             =item L<How to check that a robot belongs to Yandex|https://yandex.com/support/webmaster/robot-workings/check-yandex-robots.html>
390              
391             =back
392              
393             =head1 SOURCE
394              
395             The development version is on github at L<https://github.com/robrwo/Robots-Validate>
396             and may be cloned from L<git://github.com/robrwo/Robots-Validate.git>
397              
398             =head1 BUGS
399              
400             Please report any bugs or feature requests on the bugtracker website
401             L<https://github.com/robrwo/Robots-Validate/issues>
402              
403             When submitting a bug or request, please include a test-file or a
404             patch to an existing test-file that illustrates the bug or desired
405             feature.
406              
407             =head1 AUTHOR
408              
409             Robert Rothenberg <rrwo@cpan.org>
410              
411             =head1 COPYRIGHT AND LICENSE
412              
413             This software is Copyright (c) 2018-2024 by Robert Rothenberg.
414              
415             This is free software, licensed under:
416              
417             The Artistic License 2.0 (GPL Compatible)
418              
419             =cut