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