File Coverage

blib/lib/Mail/SPF/Util.pm
Criterion Covered Total %
statement 67 108 62.0
branch 9 48 18.7
condition 17 33 51.5
subroutine 22 27 81.4
pod 7 8 87.5
total 122 224 54.4


line stmt bran cond sub pod time code
1             #
2             # Mail::SPF::Util
3             # Mail::SPF utility class.
4             #
5             # (C) 2005-2012 Julian Mehnle
6             # 2005 Shevek
7             # $Id: Util.pm 57 2012-01-30 08:15:31Z julian $
8             #
9             ##############################################################################
10              
11             package Mail::SPF::Util;
12              
13             =head1 NAME
14              
15             Mail::SPF::Util - Mail::SPF utility class
16              
17             =cut
18              
19 6     6   32464 use warnings;
  6         15  
  6         251  
20 6     6   34 use strict;
  6         14  
  6         194  
21              
22 6     6   3713 use utf8; # Hack to keep Perl 5.6 from whining about /[\p{}]/.
  6         77  
  6         38  
23              
24 6     6   204 use base 'Mail::SPF::Base';
  6         13  
  6         1098  
25              
26 6     6   34 use Mail::SPF::Exception;
  6         9  
  6         67  
27              
28 6     6   353 use Error ':try';
  6         11  
  6         28  
29 6     6   7352 use Sys::Hostname ();
  6         12733  
  6         169  
30 6     6   3708 use NetAddr::IP;
  6         149413  
  6         38  
31              
32 6     6   752 use constant TRUE => (0 == 0);
  6         12  
  6         463  
33 6     6   29 use constant FALSE => not TRUE;
  6         13  
  6         448  
34              
35 5         8084 use constant ipv4_mapped_ipv6_address_pattern =>
36 6     6   218 qr/^::ffff:(\p{IsXDigit}{1,4}):(\p{IsXDigit}{1,4})/i;
  6     5   8  
  6         721  
  5         155817  
  5         12  
37              
38             # Interface:
39             ##############################################################################
40              
41             =head1 SYNOPSIS
42              
43             use Mail::SPF::Util;
44              
45             $hostname = Mail::SPF::Util->hostname;
46              
47             $ipv6_address_v4mapped =
48             Mail::SPF::Util->ipv4_address_to_ipv6($ipv4_address);
49              
50             $ipv4_address =
51             Mail::SPF::Util->ipv6_address_to_ipv4($ipv6_address_v4mapped);
52              
53             $is_v4mapped =
54             Mail::SPF::Util->ipv6_address_is_ipv4_mapped($ipv6_address);
55              
56             $ip_address_string = Mail::SPF::Util->ip_address_to_string($ip_address);
57             $reverse_name = Mail::SPF::Util->ip_address_reverse($ip_address);
58              
59             $validated_domain = Mail::SPF::Util->valid_domain_for_ip_address(
60             $spf_server, $request,
61             $ip_address, $domain,
62             $find_best_match, # defaults to false
63             $accept_any_domain # defaults to false
64             );
65              
66             $sanitized_string = Mail::SPF::Util->sanitize_string($string);
67              
68             =cut
69              
70             # Implementation:
71             ##############################################################################
72              
73             =head1 DESCRIPTION
74              
75             B is Mail::SPF's utility class.
76              
77             =head2 Class methods
78              
79             The following class methods are provided:
80              
81             =over
82              
83             =item B: returns I
84              
85             Returns the fully qualified domain name (FQDN) of the local host.
86              
87             =cut
88              
89             my $hostname;
90              
91             sub hostname {
92 6     6 0 13 my ($self) = @_;
93 6   66     37 return $hostname ||= (gethostbyname(Sys::Hostname::hostname))[0];
94             # Thanks to Sys::Hostname::FQDN for that trick!
95             }
96              
97             =item B: returns I; throws
98             I
99              
100             Converts the specified I IPv4 address into an IPv4-mapped IPv6
101             address. Throws a I exception if the specified
102             IP address is not an IPv4 address.
103              
104             =cut
105              
106             sub ipv4_address_to_ipv6 {
107 14     14 1 2799 my ($self, $ipv4_address) = @_;
108 14 100 100     129 UNIVERSAL::isa($ipv4_address, 'NetAddr::IP') and
109             $ipv4_address->version == 4
110             or throw Mail::SPF::EInvalidOptionValue('NetAddr::IP IPv4 address expected');
111 12         317 return NetAddr::IP->new(
112             '::ffff:' . $ipv4_address->addr, # address
113             $ipv4_address->masklen - 32 + 128 # netmask length
114             );
115             }
116              
117             =item B: returns I; throws
118             I
119              
120             Converts the specified I IPv4-mapped IPv6 address into a proper
121             IPv4 address. Throws a I exception if the
122             specified IP address is not an IPv4-mapped IPv6 address.
123              
124             =cut
125              
126             sub ipv6_address_to_ipv4 {
127 7     7 1 1785 my ($self, $ipv6_address) = @_;
128 7 100 100     62 UNIVERSAL::isa($ipv6_address, 'NetAddr::IP') and
      100        
129             $ipv6_address->version == 6 and
130             $ipv6_address->short =~ $self->ipv4_mapped_ipv6_address_pattern
131             or throw Mail::SPF::EInvalidOptionValue('NetAddr::IP IPv4-mapped IPv6 address expected');
132 3 50       1656 return NetAddr::IP->new(
133             join('.', unpack('C4', pack('H8', sprintf('%04s%04s', $1, $2)))), # address
134             $ipv6_address->masklen >= 128 - 32 ? $ipv6_address->masklen - 128 + 32 : 0 # netmask length
135             );
136             }
137              
138             =item B: returns I
139              
140             Returns B if the specified I IPv6 address is an IPv4-mapped
141             address, B otherwise.
142              
143             =cut
144              
145             sub ipv6_address_is_ipv4_mapped {
146 18     18 1 1148 my ($self, $ipv6_address) = @_;
147             return (
148 18   100     966 UNIVERSAL::isa($ipv6_address, 'NetAddr::IP') and
149             $ipv6_address->version == 6 and
150             $ipv6_address->short =~ $self->ipv4_mapped_ipv6_address_pattern
151             );
152             }
153              
154             =item B: returns I;
155             throws I
156              
157             Returns the given I IPv4 or IPv6 address compactly formatted as a
158             I. For IPv4 addresses, this is equivalent to calling L< NetAddr::IP's
159             C |NetAddr::IP/addr> method. For IPv6 addresses, this is equivalent to
160             calling L< NetAddr::IP's C |NedAddr::IP/short> method. Throws a
161             I exception if the specified object is not a
162             I IPv4 or IPv6 address object.
163              
164             =cut
165              
166             sub ip_address_to_string {
167 0     0 1 0 my ($self, $ip_address) = @_;
168 0 0 0     0 UNIVERSAL::isa($ip_address, 'NetAddr::IP') and
      0        
169             ($ip_address->version == 4 or $ip_address->version == 6)
170             or throw Mail::SPF::EInvalidOptionValue('NetAddr::IP IPv4 or IPv6 address expected');
171 0 0       0 return $ip_address->version == 4 ? $ip_address->addr : lc($ip_address->short);
172             }
173              
174             =item B: returns I;
175             throws I
176              
177             Returns the C/C reverse notation of the given
178             I IPv4 or IPv6 address. Throws a I
179             exception if the specified object is not a I IPv4 or IPv6 address
180             object.
181              
182             =cut
183              
184             sub ip_address_reverse {
185 3     3 1 1121 my ($self, $ip_address) = @_;
186 3 50 66     24 UNIVERSAL::isa($ip_address, 'NetAddr::IP') and
      33        
187             ($ip_address->version == 4 or $ip_address->version == 6)
188             or throw Mail::SPF::EInvalidOptionValue('NetAddr::IP IPv4 or IPv6 address expected');
189             try {
190             # Treat IPv4-mapped IPv6 addresses as IPv4 addresses:
191 3     3   83 $ip_address = $self->ipv6_address_to_ipv4($ip_address);
192             }
193 3     2   54 catch Mail::SPF::EInvalidOptionValue with {};
  2         105  
194             # ...deliberately ignoring conversion errors.
195 3 100       177 if ($ip_address->version == 4) {
    50          
196 2         15 my @octets = split(/\./, $ip_address->addr);
197 2         269 @octets = @octets[0 .. int($ip_address->masklen / 8) - 1];
198 2         49 return join('.', reverse(@octets)) . '.in-addr.arpa.';
199             }
200             elsif ($ip_address->version == 6) {
201 1         26 my @nibbles = split(//, unpack("H32", $ip_address->aton));
202 1         26 @nibbles = @nibbles[0 .. int($ip_address->masklen / 4) - 1];
203 1         36 return join('.', reverse(@nibbles)) . '.ip6.arpa.';
204             }
205             }
206              
207             =item B
208             $find_best_match = false, $accept_any_domain = false)>:
209             returns I or B
210              
211             Finds a valid domain name for the given I IP address that matches
212             the given domain or a sub-domain thereof. A domain name is valid for the given
213             IP address if the IP address reverse-maps to that domain name in DNS, and the
214             domain name in turn forward-maps to the IP address. Uses the given
215             I and I objects to perform DNS look-ups.
216             Returns the validated domain name.
217              
218             If C<$find_best_match> is B, the one domain name is selected that best
219             matches the given domain name, preferring direct matches over sub-domain
220             matches. Defaults to B.
221              
222             If C<$accept_any_domain> is B, I domain names are considered
223             acceptable, even if they differ completely from the given domain name (which
224             is then effectively unused unless a best match is requested). Defaults to
225             B.
226              
227             =cut
228              
229 6     6   47 use constant valid_domain_match_none => 0;
  6         12  
  6         311  
230 6     6   30 use constant valid_domain_match_subdomain => 1;
  6         11  
  6         362  
231 6     6   29 use constant valid_domain_match_identical => 2;
  6         9  
  6         4417  
232              
233             sub valid_domain_for_ip_address {
234 0     0 1   my ($self, $server, $request, $ip_address, $domain, $find_best_match, $accept_any_domain) = @_;
235              
236 0 0         my $addr_rr_type = $ip_address->version == 4 ? 'A' : 'AAAA';
237              
238 0           my $reverse_ip_name = $self->ip_address_reverse($ip_address);
239 0           my $ptr_packet = $server->dns_lookup($reverse_ip_name, 'PTR');
240 0 0         my @ptr_rrs = $ptr_packet->answer
241             or $server->count_void_dns_lookup($request);
242              
243             # Respect the PTR mechanism lookups limit (RFC 4408, 5.5/3/4):
244 0 0         @ptr_rrs = splice(@ptr_rrs, 0, $server->max_name_lookups_per_ptr_mech)
245             if defined($server->max_name_lookups_per_ptr_mech);
246              
247 0           my $best_match_type;
248             my $valid_domain;
249              
250             # Check PTR records:
251 0           foreach my $ptr_rr (@ptr_rrs) {
252 0 0         if ($ptr_rr->type eq 'PTR') {
253 0           my $ptr_domain = $ptr_rr->ptrdname;
254              
255 0           my $match_type;
256 0 0         if ($ptr_domain =~ /^\Q$domain\E$/i) {
    0          
257 0           $match_type = valid_domain_match_identical;
258             }
259             elsif ($ptr_domain =~ /\.\Q$domain\E$/i) {
260 0           $match_type = valid_domain_match_subdomain;
261             }
262             else {
263 0           $match_type = valid_domain_match_none;
264             }
265              
266             # If we're not accepting _any_ domain, and the PTR domain does not match
267             # the requested domain at all, ignore this PTR domain (RFC 4408, 5.5/5):
268 0 0 0       next if not $accept_any_domain and $match_type == valid_domain_match_none;
269              
270 0           my $is_valid_domain = FALSE;
271              
272             try {
273 0     0     my $addr_packet = $server->dns_lookup($ptr_domain, $addr_rr_type);
274 0 0         my @addr_rrs = $addr_packet->answer
275             or $server->count_void_dns_lookup($request);
276 0           foreach my $addr_rr (@addr_rrs) {
277 0 0         if ($addr_rr->type eq $addr_rr_type) {
    0          
278 0 0         $is_valid_domain = TRUE, last
279             if $ip_address == NetAddr::IP->new($addr_rr->address);
280             # IP address reverse and forward mapping match,
281             # PTR domain validated!
282             }
283             elsif ($addr_rr->type =~ /^(CNAME|A|AAAA)$/) {
284             # A CNAME (which has hopefully been resolved by the server
285             # for us already), or an address RR of an unrequested type.
286             # Silently ignore any of those.
287             # FIXME Silently ignoring address RRs of an "unrequested"
288             # FIXME type poses a disparity with how the "ip{4,6}", "a",
289             # FIXME and "mx" mechanisms tolerantly handle alien but
290             # FIXME convertible IP address types.
291             }
292             else {
293             # Unexpected RR type.
294             # TODO Generate debug info or ignore silently.
295             }
296             }
297             }
298 0     0     catch Mail::SPF::EDNSError with {};
  0            
299             # Ignore DNS errors on doing A/AAAA RR lookups (RFC 4408, 5.5/5/5).
300              
301 0 0         if ($is_valid_domain) {
302             # If we're not looking for the _best_ match, any acceptable validated
303             # domain will do (RFC 4408, 5.5/5):
304 0 0         return $ptr_domain if not $find_best_match;
305              
306             # Otherwise, is this PTR domain the best possible match?
307 0 0         return $ptr_domain if $match_type == valid_domain_match_identical;
308              
309             # Lastly, record this match as the best one as of yet:
310 0 0 0       if (
311             not defined($best_match_type) or
312             $match_type > $best_match_type
313             ) {
314 0           $valid_domain = $ptr_domain;
315 0           $best_match_type = $match_type;
316             }
317             }
318             }
319             else {
320             # Unexpected RR type.
321             # TODO Generate debug info or ignore silently.
322             }
323             }
324              
325             # Return best match, possibly none (undef):
326 0           return $valid_domain;
327             }
328              
329             =item B: returns I or B
330              
331             Replaces all non-printable or non-ascii characters in a string with their
332             hex-escaped representation (e.g., C<\x00>).
333              
334             =cut
335              
336             sub sanitize_string {
337 0     0 1   my ($self, $string) = @_;
338              
339 0 0         return undef if not defined($string);
340              
341 0           $string =~ s/([\x00-\x1f\x7f-\xff])/sprintf("\\x%02x", ord($1))/gex;
  0            
342 0           $string =~ s/([\x{0100}-\x{ffff}]) /sprintf("\\x{%04x}", ord($1))/gex;
  0            
343              
344 0           return $string;
345             }
346              
347             =back
348              
349             =head1 SEE ALSO
350              
351             L
352              
353             For availability, support, and license information, see the README file
354             included with Mail::SPF.
355              
356             =head1 AUTHORS
357              
358             Julian Mehnle , Shevek
359              
360             =cut
361              
362             TRUE;