File Coverage

blib/lib/Net/Google/SafeBrowsing4/URI.pm
Criterion Covered Total %
statement 126 126 100.0
branch 42 42 100.0
condition 12 12 100.0
subroutine 12 12 100.0
pod 4 4 100.0
total 196 196 100.0


line stmt bran cond sub pod time code
1             package Net::Google::SafeBrowsing4::URI;
2              
3 9     9   331112 use strict;
  9         21  
  9         258  
4 9     9   44 use warnings;
  9         18  
  9         237  
5              
6 9     9   2080 use Digest::SHA qw(sha256);
  9         15094  
  9         531  
7 9     9   2770 use Net::IP::Lite qw();
  9         34728  
  9         194  
8 9     9   2240 use URI;
  9         33882  
  9         11001  
9              
10             =encoding utf-8
11              
12             =head1 NAME
13              
14             Net::Google::SafeBrowsing4::URI - URI management Class for the Google SafeBrowsing (version 4) API.
15              
16              
17             =head1 SYNOPSIS
18              
19             use Net::Google::SafeBrowsing4::URI;
20              
21             my $gsb_uri = Net::Google::SafeBrowsing4::URI->new('http://my.example.site:80/path/to/file.html?query=param#fragment');
22             my @uris = $gsb_uri->generate_lookupuris();
23              
24             =head1 DESCRIPTION
25              
26             Net::Google::SafeBrowsing4::URI takes care of normalizing URLs, extracting suffix/prefix expressions, calculating hashes.
27              
28             =head1 METHODS
29              
30             =over
31              
32             =item new
33              
34             =back
35              
36             my $gsb_uri = Net::Google::SafeBrowsing4::URI->new('http://my.example.site:80/path/to/file.html?query=param#fragment');
37              
38             =over
39              
40             Initializes the object.
41              
42             Arguments:
43              
44             =over
45              
46             =item $uri The URL to parse
47              
48             =back
49              
50             =back
51              
52             =cut
53              
54             sub new {
55 262     262 1 104196 my $class = shift;
56 262         559 my @args = @_;
57              
58 262 100 100     1130 if ((scalar(@args) == 0) || !$args[0]) {
59 3         15 return undef;
60             }
61              
62 259         656 my $self = {
63             rawuri => $args[0],
64             };
65              
66 259         472 bless($self, $class);
67 259 100       515 return $self->_normalize() ? $self : undef;
68             }
69              
70             =over
71              
72             =item as_string
73              
74             Returns the normalized URI as string.
75              
76             =back
77              
78             =cut
79              
80             sub as_string {
81 432     432 1 62904 my $self = shift;
82              
83 432         1244 return '' . $self->{uri};
84             }
85              
86             =item generate_lookupuris
87              
88             Generates all partial/full URIs supported by Google SafeBrowsing. See "suffix/prefix expressions" topic in GSBv4 API reference.
89             Returns a list of L objects.
90              
91             =cut
92              
93             sub generate_lookupuris {
94 8     8 1 87 my $self = shift;
95 8         19 my @uris = ();
96              
97 8         19 $self->as_string() =~ /^(https?:\/\/)([^\/]+)(\/.*)$/i;
98 8         105 my ($scheme, $host, $path_query) = ($1, $2, $3);
99              
100             # Collect host suffixes
101 8         18 my @domains = ();
102 8 100       25 if ($host !~ /^\d+\.\d+\.\d+\.\d+$/) {
103 7         29 my @parts = split(/\./, $host);
104 7         15 splice(@parts, 0, -6); # take 5 top most compoments
105              
106 7         20 while (scalar(@parts) > 2) {
107 9         16 shift(@parts);
108 9         32 push(@domains, join(".", @parts) );
109             }
110             }
111 8         16 push(@domains, $host);
112              
113             # Collect path & query prefixes
114 8         14 my @paths = ();
115 8         21 my @parts = split(/\//, $path_query);
116 8         22 my $part_count = scalar(@parts);
117 8 100       21 $part_count = $part_count > 4 ? 4 : $part_count - 1; # limit to 4
118 8         14 my $previous = "";
119 8         28 for (my $i = 0; $i < $part_count; $i++) {
120 8         15 $previous .= "/" . $parts[$i] ."/";
121 8         19 push(@paths, $previous);
122             }
123 8 100       21 if ($path_query =~ /^([^\?]+)\?.*$/) {
124 1         2 push(@paths, $1);
125             }
126 8         15 push(@paths, $path_query);
127              
128             # Assemble the list of Net::Google::SafeBrowsing4::URI objects
129 8         14 foreach my $domain (@domains) {
130 17         29 foreach my $path (@paths) {
131 33         101 my $gsb_uri = Net::Google::SafeBrowsing4::URI->new($scheme . $domain . $path);
132 33         261 push(@uris, $gsb_uri);
133             }
134             }
135              
136 8         36 return @uris;
137             }
138              
139             =item hash
140              
141             Generates the SHA-256 hash of the URI (with scheme removed).
142              
143             =cut
144              
145             sub hash {
146 2     2 1 19 my $self = shift;
147              
148 2         6 my $uri = $self->as_string();
149 2         16 $uri =~ s/^https?:\/\///i;
150              
151 2         32 return sha256($uri);
152             }
153              
154             =head1 PRIVATE METHODS
155              
156             =over
157              
158             =item _normalize
159              
160             Parses and normalizes the URI.
161              
162             =back
163              
164             =cut
165              
166             sub _normalize {
167 259     259   368 my $self = shift;
168 259         429 my $modified_rawuri = $self->{rawuri};
169              
170             # Remove third and more slashes after the scheme
171 259         576 $modified_rawuri =~ s/^(\s*https?:\/\/)\/+/$1/si;
172             # Remove any Tab, CR, LF characters from the URI
173 259         566 $modified_rawuri =~ s/[\r\n\t]+//sgi;
174             # Recursive percent-unescape (everything but '#' not to confuse URI parser)
175 259         610 while ($modified_rawuri =~ s{%(?!23)([[:xdigit:]]{2})}{chr(hex($1))}esg) { }
  98         307  
176              
177             # Parse URI
178 259         692 my $uri_obj = URI->new($modified_rawuri);
179 259 100       55884 if (ref($uri_obj) !~ /^URI::https?$/) {
180 29 100 100     99 if (!$uri_obj->scheme() || (!$uri_obj->has_recognized_scheme() && $modified_rawuri =~ /^[^:]+:\d{1,5}(?:\/|$)/)) {
      100        
181 25         564 $uri_obj = URI->new("http://" . $modified_rawuri);
182             }
183             }
184             # Only http and https URIs are supported
185 259 100       8815 if (ref($uri_obj) !~ /^URI::https?$/) {
186 4         27 return undef;
187             }
188              
189             # Remove userinfo
190 255         772 $uri_obj->userinfo(undef);
191             # Remove port
192 255         12463 $uri_obj->port(undef);
193             # Remove Fragment
194 255         12132 $uri_obj->fragment(undef);
195              
196             # Host modifications
197 255         2353 my $modified_host = $uri_obj->host();
198             # Host part cannot be empty
199 255 100       5968 if ($modified_host =~ /^\s*$/) {
200 2         10 return undef;
201             }
202             # Collapse consecutive dots into one
203 253         468 $modified_host =~ s/\.\.+/\./sg;
204             # Remove leading and trailing dot
205 253         795 $modified_host =~ s/^\.|\.$//sg;
206              
207             # IPv4 canonicalizations
208 253         452 $modified_host = _normalize_ip($modified_host);
209 253 100       9442 if (!defined($modified_host)) {
210 11         51 return undef;
211             }
212 242         625 $uri_obj->host($modified_host);
213              
214             # Empty/separator-only host
215 242 100       15570 if ($uri_obj->host() =~ /^[\s.\/]*$/) {
216 1         31 return undef;
217             }
218              
219             # Numeric TLD (and not IPv4)
220 241 100 100     5489 if ($uri_obj->host() =~ /\.\d[^\.]*$/
221             && $uri_obj->host() !~ /^(?:2(?:5[0-5]|[0-4][0-9])|[01]?[0-9]{1,2})(?:\.(?:2(?:5[0-5]|[0-4][0-9])|[01]?[0-9]{1,2})){3}$/) {
222 3         130 return undef;
223             }
224              
225 238         8076 my $modified_path = $uri_obj->path();
226             # Eliminate current directory /./ parts
227 238         2272 while ($modified_path =~ s/\/\.(?:\/|$)/\//sg) {};
228             # Eliminate parent directory /something/./ parts
229 238         468 while ($modified_path =~ s/\/[^\/]+\/\.\.(?:\/|$)/\//sg) {};
230             # Eliminate double // slashes from path
231 238         350 $modified_path =~ s/\/\/+/\//sg;
232 238         571 $uri_obj->path($modified_path);
233              
234             # Fix some percent encoding
235 238         5689 my $modified_path_query = $uri_obj->path_query();
236             # Fix lone percent signs %
237 238         2007 $modified_path_query =~ s/%(?![[:xdigit:]]{2})/%25/sg;
238 238         565 $uri_obj->path_query($modified_path_query);
239              
240 238         5339 my $canonical = $uri_obj->canonical();
241             # Fix caret escaping
242 238         24965 $canonical=~ s/%5E/\^/sg;
243              
244 238         1247 $self->{uri} = $canonical;
245              
246 238         701 return $self->{uri};
247             }
248              
249             =head1 PRIVATE FUNCTIONS
250              
251             =over
252              
253             =item _normalize_ip
254              
255             Function for recognising various IPv4 formatted addresses and convert them to I format (111.11.1.1)
256              
257             Arguments:
258              
259             =over
260              
261             =item $host
262              
263             Hostname to parse as IP Address
264              
265             =back
266              
267             =back
268              
269             =cut
270              
271             sub _normalize_ip {
272 253     253   357 my $host = shift;
273              
274             # Shortcut: If it doesn't look like an IPv4, then return early
275 253 100       823 if ($host !~ /^(?:0x[[:xdigit:]]+|\d+)(?:\.(?:0x[[:xdigit:]]+|\d+))*$/si) {
276 113         257 return $host;
277             }
278              
279             # Most formats are detected and converted by Net::IP::Lite
280 140         409 my $ip = Net::IP::Lite->new($host);
281 140 100       8474 if ($ip) {
282 93         212 return $ip->transform();
283             }
284              
285             # One and two dots case is missing: xxx.xxxxxxxxxx, xxx.xxx.xxxxxx
286 47         58 my $bits = 32;
287 47         111 my @segments = split(/\./, $host);
288 47         66 my $segment_count = scalar(@segments);
289              
290 47         69 my $decimal = 0;
291 47         98 for (my $i = 0; $i < $segment_count; $i++) {
292 122         166 my $is_last_segment = $i >= $segment_count - 1;
293 122 100       243 my $segment = _parse_ipv4_segment($segments[$i], !$is_last_segment ? 8 : $bits);
294 122 100       196 if (!defined($segment)) {
295 11         23 return undef;
296             }
297 111         129 $bits -= 8;
298 111 100       247 $decimal += $segment << (!$is_last_segment ? $bits : 0);
299             }
300              
301 36         92 $ip = Net::IP::Lite->new($decimal);
302 36         818 return $ip->transform();
303             }
304              
305             =over
306              
307             =item _parse_ipv4_segment
308              
309             =back
310              
311             my $decimal = _parse_ipv4_part($segment, $bits)
312              
313             =over
314              
315             Transforms one IPv4 segment to decimal with range checking.
316              
317             Arguments:
318              
319             =over
320              
321             =item $segment
322              
323             Decimal/octal/hexadecimal value to parse
324              
325             =item $bits
326              
327             Bit length for range checking
328              
329             =back
330              
331             =back
332              
333             =cut
334              
335             sub _parse_ipv4_segment {
336 122     122   162 my $segment = shift;
337 122         154 my $bits = shift;
338 122         136 my $decimal;
339              
340 122 100       405 if ($segment =~ /^0+([0-7]{0,10}|[0-3][0-7]{10})$/) {
    100          
    100          
341 34         63 $decimal = oct($1);
342             }
343             elsif ($segment =~ /^0x0*([[:xdigit:]]{1,8})$/si) {
344 35         72 $decimal = hex($1);
345             }
346             elsif ($segment =~ /^[1-9]\d+$/) {
347 49         82 $decimal = $segment;
348             }
349             else {
350 4         8 return undef;
351             }
352              
353 118 100       221 if ($decimal >= (1 << $bits)) {
354 7         22 return undef;
355             }
356 111         171 return $decimal;
357             }
358              
359             =head1 BUGS
360              
361             Some URI normalizatuion cases are still missing:
362              
363             =over
364              
365             =item Highbit characters in hostname are punycoded, not percent encoded.
366              
367             Google's GO Client percent-escapes based on the hostname is a valid unicode string or not
368              
369             =back
370              
371             =head1 AUTHORS
372              
373             Tamás Fehérvári, Egeever@users.sourceforge.net
374             Julien Sobrier, Ejulien@sobrier.netE,
375              
376             =head1 COPYRIGHT AND LICENSE
377              
378             Copyright (C) 2016 by Julien Sobrier, Tamás Fehérvári
379              
380             This library is free software; you can redistribute it and/or modify
381             it under the same terms as Perl itself, either Perl version 5.8.8 or,
382             at your option, any later version of Perl 5 you may have available.
383              
384             =cut
385              
386             1;