| blib/lib/Mail/SPF/Server.pm | |||
|---|---|---|---|
| Criterion | Covered | Total | % |
| statement | 82 | 164 | 50.0 |
| branch | 15 | 48 | 31.2 |
| condition | 4 | 30 | 13.3 |
| subroutine | 25 | 47 | 53.1 |
| pod | 10 | 13 | 76.9 |
| total | 136 | 302 | 45.0 |
| line | stmt | bran | cond | sub | pod | time | code |
|---|---|---|---|---|---|---|---|
| 1 | # | ||||||
| 2 | # Mail::SPF::Server | ||||||
| 3 | # Server class for processing SPF requests. | ||||||
| 4 | # | ||||||
| 5 | # (C) 2005-2012 Julian Mehnle |
||||||
| 6 | # 2005 Shevek |
||||||
| 7 | # $Id: Server.pm 61 2013-07-22 03:45:15Z julian $ | ||||||
| 8 | # | ||||||
| 9 | ############################################################################## | ||||||
| 10 | |||||||
| 11 | package Mail::SPF::Server; | ||||||
| 12 | |||||||
| 13 | =head1 NAME | ||||||
| 14 | |||||||
| 15 | Mail::SPF::Server - Server class for processing SPF requests | ||||||
| 16 | |||||||
| 17 | =head1 VERSION | ||||||
| 18 | |||||||
| 19 | version 3.20250505 | ||||||
| 20 | |||||||
| 21 | =cut | ||||||
| 22 | |||||||
| 23 | 3 | 3 | 502771 | use warnings; | |||
| 3 | 8 | ||||||
| 3 | 280 | ||||||
| 24 | 3 | 3 | 22 | use strict; | |||
| 3 | 7 | ||||||
| 3 | 115 | ||||||
| 25 | |||||||
| 26 | 3 | 3 | 18 | use base 'Mail::SPF::Base'; | |||
| 3 | 7 | ||||||
| 3 | 1577 | ||||||
| 27 | |||||||
| 28 | 3 | 3 | 24 | use Error ':try'; | |||
| 3 | 7 | ||||||
| 3 | 24 | ||||||
| 29 | 3 | 3 | 1373 | use Net::DNS::Resolver; | |||
| 3 | 109758 | ||||||
| 3 | 207 | ||||||
| 30 | |||||||
| 31 | 3 | 3 | 1338 | use Mail::SPF::MacroString; | |||
| 3 | 15 | ||||||
| 3 | 135 | ||||||
| 32 | 3 | 3 | 1652 | use Mail::SPF::Record; | |||
| 3 | 12 | ||||||
| 3 | 106 | ||||||
| 33 | 3 | 3 | 1944 | use Mail::SPF::Result; | |||
| 3 | 13 | ||||||
| 3 | 21 | ||||||
| 34 | |||||||
| 35 | 3 | 3 | 247 | use constant TRUE => (0 == 0); | |||
| 3 | 6 | ||||||
| 3 | 188 | ||||||
| 36 | 3 | 3 | 19 | use constant FALSE => not TRUE; | |||
| 3 | 7 | ||||||
| 3 | 158 | ||||||
| 37 | |||||||
| 38 | 3 | 242 | use constant record_classes_by_version => { | ||||
| 39 | 1 => 'Mail::SPF::v1::Record', | ||||||
| 40 | 2 => 'Mail::SPF::v2::Record' | ||||||
| 41 | 3 | 3 | 18 | }; | |||
| 3 | 6 | ||||||
| 42 | |||||||
| 43 | 3 | 3 | 21 | use constant result_base_class => 'Mail::SPF::Result'; | |||
| 3 | 7 | ||||||
| 3 | 241 | ||||||
| 44 | |||||||
| 45 | 3 | 3 | 21 | use constant query_rr_type_all => 0; | |||
| 3 | 5 | ||||||
| 3 | 138 | ||||||
| 46 | 3 | 3 | 15 | use constant query_rr_type_txt => 1; | |||
| 3 | 7 | ||||||
| 3 | 158 | ||||||
| 47 | 3 | 3 | 16 | use constant query_rr_type_spf => 2; | |||
| 3 | 5 | ||||||
| 3 | 171 | ||||||
| 48 | |||||||
| 49 | 3 | 218 | use constant default_default_authority_explanation => | ||||
| 50 | 3 | 3 | 20 | 'Rejected by SPF record.'; | |||
| 3 | 5 | ||||||
| 51 | |||||||
| 52 | 7 | 7 | 0 | 34 | sub default_query_rr_types { shift->query_rr_type_txt }; | ||
| 53 | |||||||
| 54 | 3 | 3 | 20 | use constant default_max_dns_interactive_terms => 10; # RFC 4408, 10.1/6 | |||
| 3 | 5 | ||||||
| 3 | 266 | ||||||
| 55 | 3 | 3 | 17 | use constant default_max_name_lookups_per_term => 10; # RFC 4408, 10.1/7 | |||
| 3 | 154 | ||||||
| 3 | 269 | ||||||
| 56 | 6 | 6 | 0 | 23 | sub default_max_name_lookups_per_mx_mech { shift->max_name_lookups_per_term }; | ||
| 57 | 7 | 7 | 0 | 25 | sub default_max_name_lookups_per_ptr_mech { shift->max_name_lookups_per_term }; | ||
| 58 | |||||||
| 59 | 3 | 3 | 18 | use constant default_max_void_dns_lookups => 2; | |||
| 3 | 7 | ||||||
| 3 | 12111 | ||||||
| 60 | |||||||
| 61 | # Interface: | ||||||
| 62 | ############################################################################## | ||||||
| 63 | |||||||
| 64 | =head1 SYNOPSIS | ||||||
| 65 | |||||||
| 66 | use Mail::SPF; | ||||||
| 67 | |||||||
| 68 | my $spf_server = Mail::SPF::Server->new( | ||||||
| 69 | # Optional custom default for authority explanation: | ||||||
| 70 | default_authority_explanation => | ||||||
| 71 | 'See http://www.%{d}/why/id=%{S};ip=%{I};r=%{R}' | ||||||
| 72 | ); | ||||||
| 73 | |||||||
| 74 | my $result = $spf_server->process($request); | ||||||
| 75 | |||||||
| 76 | =cut | ||||||
| 77 | |||||||
| 78 | # Implementation: | ||||||
| 79 | ############################################################################## | ||||||
| 80 | |||||||
| 81 | =head1 DESCRIPTION | ||||||
| 82 | |||||||
| 83 | B |
||||||
| 84 | server instance can be configured with specific processing parameters. Also, | ||||||
| 85 | the default I |
||||||
| 86 | be overridden with a custom resolver object. | ||||||
| 87 | |||||||
| 88 | =head2 Constructor | ||||||
| 89 | |||||||
| 90 | The following constructor is provided: | ||||||
| 91 | |||||||
| 92 | =over | ||||||
| 93 | |||||||
| 94 | =item B |
||||||
| 95 | |||||||
| 96 | Creates a new server object for processing SPF requests. | ||||||
| 97 | |||||||
| 98 | %options is a list of key/value pairs representing any of the following | ||||||
| 99 | options: | ||||||
| 100 | |||||||
| 101 | =over | ||||||
| 102 | |||||||
| 103 | =item B |
||||||
| 104 | |||||||
| 105 | A I |
||||||
| 106 | string to use if the authority domain does not specify an explanation string of | ||||||
| 107 | its own. Defaults to: | ||||||
| 108 | |||||||
| 109 | 'Rejected by SPF record.' | ||||||
| 110 | |||||||
| 111 | As can be seen from the default, a non-standard C<_scope> pseudo macro is | ||||||
| 112 | supported that expands to the name of the identity's scope. (Note: Do I |
||||||
| 113 | use any non-standard macros in explanation strings published in DNS.) | ||||||
| 114 | |||||||
| 115 | =item B |
||||||
| 116 | |||||||
| 117 | A I |
||||||
| 118 | be used for expanding the C |
||||||
| 119 | system's configured host name. | ||||||
| 120 | |||||||
| 121 | =item B |
||||||
| 122 | |||||||
| 123 | An optional DNS resolver object. If none is specified, a new I |
||||||
| 124 | object is used. The resolver object may be of a different class, but it must | ||||||
| 125 | provide an interface similar to I |
||||||
| 126 | and C |
||||||
| 127 | return either an object of class I |
||||||
| 128 | error, B |
||||||
| 129 | |||||||
| 130 | =item B |
||||||
| 131 | |||||||
| 132 | For which RR types to query when looking up and selecting SPF records. The | ||||||
| 133 | following values are supported: | ||||||
| 134 | |||||||
| 135 | =over | ||||||
| 136 | |||||||
| 137 | =item B<< Mail::SPF::Server->query_rr_type_all >> | ||||||
| 138 | |||||||
| 139 | Both C |
||||||
| 140 | |||||||
| 141 | =item B<< Mail::SPF::Server->query_rr_type_txt >> (default) | ||||||
| 142 | |||||||
| 143 | C |
||||||
| 144 | |||||||
| 145 | =item B<< Mail::SPF::Server->query_rr_type_spf >> | ||||||
| 146 | |||||||
| 147 | C |
||||||
| 148 | |||||||
| 149 | =back | ||||||
| 150 | |||||||
| 151 | For years B |
||||||
| 152 | RRs as recommended by RFC 4408. Experience has shown, however, that a | ||||||
| 153 | significant portion of name servers suffer from serious brain damage with | ||||||
| 154 | regard to the handling of queries for RR types that are unknown to them, such | ||||||
| 155 | as the C |
||||||
| 156 | only C |
||||||
| 157 | option. | ||||||
| 158 | |||||||
| 159 | See RFC 4408, 3.1.1, for a discussion of the topic, as well as the description | ||||||
| 160 | of the L method. | ||||||
| 161 | |||||||
| 162 | =item B |
||||||
| 163 | |||||||
| 164 | An I |
||||||
| 165 | per SPF check that perform DNS look-ups, as defined in RFC 4408, 10.1, | ||||||
| 166 | paragraph 6. If B |
||||||
| 167 | terms. Defaults to B<10>, which is the value defined in RFC 4408. | ||||||
| 168 | |||||||
| 169 | A value above the default is I |
||||||
| 170 | value below the default has implications with regard to the predictability of | ||||||
| 171 | SPF results. Only deviate from the default if you know what you are doing! | ||||||
| 172 | |||||||
| 173 | =item B |
||||||
| 174 | |||||||
| 175 | An I |
||||||
| 176 | (mechanism or modifier), as defined in RFC 4408, 10.1, paragraph 7. If | ||||||
| 177 | B |
||||||
| 178 | Defaults to B<10>, which is the value defined in RFC 4408. | ||||||
| 179 | |||||||
| 180 | A value above the default is I |
||||||
| 181 | value below the default has implications with regard to the predictability of | ||||||
| 182 | SPF results. Only deviate from the default if you know what you are doing! | ||||||
| 183 | |||||||
| 184 | =item B |
||||||
| 185 | |||||||
| 186 | =item B |
||||||
| 187 | |||||||
| 188 | An I |
||||||
| 189 | mechanism, respectively. Defaults to the value of the C |
||||||
| 190 | option. See there for additional information and security notes. | ||||||
| 191 | |||||||
| 192 | =item B |
||||||
| 193 | |||||||
| 194 | An I |
||||||
| 195 | i.e. the number of DNS look-ups that were caused by DNS-interactive terms and | ||||||
| 196 | macros (as defined in RFC 4408, 10.1, paragraphs 6 and 7) and that are allowed | ||||||
| 197 | to return an empty answer with RCODE 0 or RCODE 3 (C |
||||||
| 198 | processing is aborted with a C |
||||||
| 199 | there is no stricter limit on the number of void DNS look-ups beyond the usual | ||||||
| 200 | processing limits. Defaults to B<2>. | ||||||
| 201 | |||||||
| 202 | Specifically, the DNS look-ups that are subject to this limit are those caused | ||||||
| 203 | by the C, C macro. |
||||||
| 204 | |||||||
| 205 | A value of B<2> is likely to prevent effective DoS attacks against third-party | ||||||
| 206 | victim domains. However, a definite limit may cause C |
||||||
| 207 | with certain (overly complex) innocent sender policies where useful results | ||||||
| 208 | would normally be returned. | ||||||
| 209 | |||||||
| 210 | =back | ||||||
| 211 | |||||||
| 212 | =cut | ||||||
| 213 | |||||||
| 214 | sub new { | ||||||
| 215 | 7 | 7 | 1 | 653706 | my ($self, %options) = @_; | ||
| 216 | 7 | 61 | $self = $self->SUPER::new(%options); | ||||
| 217 | |||||||
| 218 | # Do not cache a Mail::SPF::MacroString instance with | ||||||
| 219 | # context provided, this causes a circular reference that leaks | ||||||
| 220 | # both objects. | ||||||
| 221 | $self->{default_authority_explanation} = $self->default_default_authority_explanation | ||||||
| 222 | 7 | 50 | 57 | if not defined($self->{default_authority_explanation}); | |||
| 223 | |||||||
| 224 | 7 | 33 | 74 | $self->{hostname} ||= Mail::SPF::Util->hostname; | |||
| 225 | |||||||
| 226 | 7 | 66 | 524 | $self->{dns_resolver} ||= Net::DNS::Resolver->new(); | |||
| 227 | |||||||
| 228 | $self->{query_rr_types} = $self->default_query_rr_types | ||||||
| 229 | 7 | 50 | 136 | if not defined($self->{query_rr_types}); | |||
| 230 | |||||||
| 231 | $self->{max_dns_interactive_terms} = $self->default_max_dns_interactive_terms | ||||||
| 232 | 7 | 100 | 38 | if not exists($self->{max_dns_interactive_terms}); | |||
| 233 | $self->{max_name_lookups_per_term} = $self->default_max_name_lookups_per_term | ||||||
| 234 | 7 | 100 | 44 | if not exists($self->{max_name_lookups_per_term}); | |||
| 235 | $self->{max_name_lookups_per_mx_mech} = $self->default_max_name_lookups_per_mx_mech | ||||||
| 236 | 7 | 100 | 31 | if not exists($self->{max_name_lookups_per_mx_mech}); | |||
| 237 | $self->{max_name_lookups_per_ptr_mech} = $self->default_max_name_lookups_per_ptr_mech | ||||||
| 238 | 7 | 50 | 33 | if not exists($self->{max_name_lookups_per_ptr_mech}); | |||
| 239 | |||||||
| 240 | $self->{max_void_dns_lookups} = $self->default_max_void_dns_lookups | ||||||
| 241 | 7 | 50 | 42 | if not exists($self->{max_void_dns_lookups}); | |||
| 242 | |||||||
| 243 | 7 | 32 | return $self; | ||||
| 244 | } | ||||||
| 245 | |||||||
| 246 | =back | ||||||
| 247 | |||||||
| 248 | =head2 Class methods | ||||||
| 249 | |||||||
| 250 | The following class methods are provided: | ||||||
| 251 | |||||||
| 252 | =over | ||||||
| 253 | |||||||
| 254 | =item B |
||||||
| 255 | |||||||
| 256 | =item B |
||||||
| 257 | |||||||
| 258 | Returns a I |
||||||
| 259 | result name via the server's inherent result base class, or returns the | ||||||
| 260 | server's inherent result base class if no result name is given. This method | ||||||
| 261 | may also be used as an instance method. | ||||||
| 262 | |||||||
| 263 | I |
||||||
| 264 | names as this would ignore any derivative result classes provided by | ||||||
| 265 | B |
||||||
| 266 | |||||||
| 267 | =cut | ||||||
| 268 | |||||||
| 269 | sub result_class { | ||||||
| 270 | 0 | 0 | 1 | 0 | my ($self, $name) = @_; | ||
| 271 | return | ||||||
| 272 | defined($name) ? | ||||||
| 273 | 0 | 0 | 0 | $self->result_base_class->result_classes->{$name} | |||
| 274 | : $self->result_base_class; | ||||||
| 275 | } | ||||||
| 276 | |||||||
| 277 | =item B |
||||||
| 278 | |||||||
| 279 | =item B |
||||||
| 280 | |||||||
| 281 | Throws a I |
||||||
| 282 | via the server's inherent result base class, passing an optional result text | ||||||
| 283 | and associating the given I |
||||||
| 284 | This method may also be used as an instance method. | ||||||
| 285 | |||||||
| 286 | I |
||||||
| 287 | as this would ignore any derivative result classes provided by B |
||||||
| 288 | extension modules. | ||||||
| 289 | |||||||
| 290 | =cut | ||||||
| 291 | |||||||
| 292 | sub throw_result { | ||||||
| 293 | 0 | 0 | 1 | 0 | my ($self, $name, $request, @text) = @_; | ||
| 294 | 0 | 0 | $self->result_class($name)->throw($self, $request, @text); | ||||
| 295 | } | ||||||
| 296 | |||||||
| 297 | =back | ||||||
| 298 | |||||||
| 299 | =head2 Instance methods | ||||||
| 300 | |||||||
| 301 | The following instance methods are provided: | ||||||
| 302 | |||||||
| 303 | =over | ||||||
| 304 | |||||||
| 305 | =item B |
||||||
| 306 | |||||||
| 307 | Processes the given I |
||||||
| 308 | domain for an SPF sender policy (see the description of the L | ||||||
| 309 | method), evaluates the policy with regard to the given identity and other | ||||||
| 310 | request parameters, and returns a I |
||||||
| 311 | result of the policy evaluation. See RFC 4408, 4, and RFC 4406, 4, for | ||||||
| 312 | details. | ||||||
| 313 | |||||||
| 314 | =cut | ||||||
| 315 | |||||||
| 316 | sub process { | ||||||
| 317 | 0 | 0 | 1 | 0 | my ($self, $request) = @_; | ||
| 318 | |||||||
| 319 | 0 | 0 | $request->state('authority_explanation', undef); | ||||
| 320 | 0 | 0 | $request->state('dns_interactive_terms_count', 0); | ||||
| 321 | 0 | 0 | $request->state('void_dns_lookups_count', 0); | ||||
| 322 | |||||||
| 323 | 0 | 0 | my $result; | ||||
| 324 | try { | ||||||
| 325 | 0 | 0 | 0 | my $record = $self->select_record($request); | |||
| 326 | 0 | 0 | $request->record($record); | ||||
| 327 | 0 | 0 | $record->eval($self, $request); | ||||
| 328 | } | ||||||
| 329 | catch Mail::SPF::Result with { | ||||||
| 330 | 0 | 0 | 0 | $result = shift; | |||
| 331 | } | ||||||
| 332 | catch Mail::SPF::EDNSError with { | ||||||
| 333 | 0 | 0 | 0 | $result = $self->result_class('temperror')->new($self, $request, shift->text); | |||
| 334 | } | ||||||
| 335 | catch Mail::SPF::ENoAcceptableRecord with { | ||||||
| 336 | 0 | 0 | 0 | 0 | 0 | if((not defined $request->{record}->{terms}[0]->{domain_spec}->{text}) or ($request->{record}->{terms}[0]->{domain_spec}->{text} !~ /\.\./)) { | |
| 337 | # remove empty record structure in order to stringify it correctly | ||||||
| 338 | 0 | 0 | undef $request->{record}; | ||||
| 339 | 0 | 0 | $result = $self->result_class('none' )->new($self, $request, shift->text); | ||||
| 340 | } else { | ||||||
| 341 | 0 | 0 | $result = $self->result_class('permerror')->new($self, $request, shift->text); | ||||
| 342 | } | ||||||
| 343 | } | ||||||
| 344 | catch Mail::SPF::ERedundantAcceptableRecords with { | ||||||
| 345 | 0 | 0 | 0 | $result = $self->result_class('permerror')->new($self, $request, shift->text); | |||
| 346 | } | ||||||
| 347 | catch Mail::SPF::ESyntaxError with { | ||||||
| 348 | 0 | 0 | 0 | $result = $self->result_class('permerror')->new($self, $request, shift->text); | |||
| 349 | } | ||||||
| 350 | catch Mail::SPF::EProcessingLimitExceeded with { | ||||||
| 351 | 0 | 0 | 0 | $result = $self->result_class('permerror')->new($self, $request, shift->text); | |||
| 352 | 0 | 0 | }; | ||||
| 353 | # Propagate other, unknown errors. | ||||||
| 354 | # This should not happen, but if it does, it helps exposing the bug! | ||||||
| 355 | |||||||
| 356 | 0 | 0 | return $result; | ||||
| 357 | } | ||||||
| 358 | |||||||
| 359 | =item B |
||||||
| 360 | throws I |
||||||
| 361 | I |
||||||
| 362 | I |
||||||
| 363 | |||||||
| 364 | Queries the authority domain of the given I |
||||||
| 365 | sender policy records and, if multiple records are available, selects the | ||||||
| 366 | record of the highest acceptable record version that covers the requested | ||||||
| 367 | scope. | ||||||
| 368 | |||||||
| 369 | More precisely, the following algorithm is performed (assuming that both C |
||||||
| 370 | and C |
||||||
| 371 | |||||||
| 372 | =over | ||||||
| 373 | |||||||
| 374 | =item 1. | ||||||
| 375 | |||||||
| 376 | Determine the authority domain, the set of acceptable SPF record versions, and | ||||||
| 377 | the identity scope from the given request object. | ||||||
| 378 | |||||||
| 379 | =item 2. | ||||||
| 380 | |||||||
| 381 | Query the authority domain for SPF records of the C |
||||||
| 382 | discarding any records that are of an inacceptable version or do not cover the | ||||||
| 383 | desired scope. | ||||||
| 384 | |||||||
| 385 | If this yields no SPF records, query the authority domain for SPF records of | ||||||
| 386 | the C |
||||||
| 387 | version or do not cover the desired scope. | ||||||
| 388 | |||||||
| 389 | If still no acceptable SPF records could be found, throw a | ||||||
| 390 | I |
||||||
| 391 | |||||||
| 392 | =item 3. | ||||||
| 393 | |||||||
| 394 | Discard all records but those of the highest acceptable version found. | ||||||
| 395 | |||||||
| 396 | If exactly one record remains, return it. Otherwise, throw a | ||||||
| 397 | I |
||||||
| 398 | |||||||
| 399 | =back | ||||||
| 400 | |||||||
| 401 | If the querying of either RR type has been disabled via the L | ||||||
| 402 | constructor's C |
||||||
| 403 | be skipped. | ||||||
| 404 | |||||||
| 405 | I |
||||||
| 406 | I |
||||||
| 407 | also be thrown. | ||||||
| 408 | |||||||
| 409 | =cut | ||||||
| 410 | |||||||
| 411 | sub select_record { | ||||||
| 412 | 0 | 0 | 1 | 0 | my ($self, $request) = @_; | ||
| 413 | |||||||
| 414 | 0 | 0 | my $domain = $request->authority_domain; | ||||
| 415 | 0 | 0 | my @versions = $request->versions; | ||||
| 416 | 0 | 0 | my $scope = $request->scope; | ||||
| 417 | |||||||
| 418 | # Employ identical behavior for 'v=spf1' and 'spf2.0' records, both of | ||||||
| 419 | # which support SPF (code 99) and TXT type records (this may be different | ||||||
| 420 | # in future revisions of SPF): | ||||||
| 421 | # Query for SPF type records first, then fall back to TXT type records. | ||||||
| 422 | |||||||
| 423 | 0 | 0 | my @records; | ||||
| 424 | 0 | 0 | my $query_count = 0; | ||||
| 425 | 0 | 0 | my @dns_errors; | ||||
| 426 | |||||||
| 427 | # Query for SPF-type RRs first: | ||||||
| 428 | 0 | 0 | 0 | 0 | if ( | ||
| 429 | $self->query_rr_types == $self->query_rr_type_all or | ||||||
| 430 | $self->query_rr_types & $self->query_rr_type_spf | ||||||
| 431 | ) { | ||||||
| 432 | try { | ||||||
| 433 | 0 | 0 | 0 | $query_count++; | |||
| 434 | 0 | 0 | my $packet = $self->dns_lookup($domain, 'SPF'); | ||||
| 435 | 0 | 0 | push( | ||||
| 436 | @records, | ||||||
| 437 | $self->get_acceptable_records_from_packet( | ||||||
| 438 | $packet, 'SPF', \@versions, $scope, $domain) | ||||||
| 439 | ); | ||||||
| 440 | } | ||||||
| 441 | catch Mail::SPF::EDNSError with { | ||||||
| 442 | 0 | 0 | 0 | push(@dns_errors, shift); | |||
| 443 | 0 | 0 | }; | ||||
| 444 | #catch Mail::SPF::EDNSTimeout with { | ||||||
| 445 | # # FIXME Ignore DNS time-outs on SPF type lookups? | ||||||
| 446 | # # Apparrently some brain-dead DNS servers time out on SPF-type queries. | ||||||
| 447 | #}; | ||||||
| 448 | } | ||||||
| 449 | |||||||
| 450 | # If no usable SPF-type RRs, try TXT-type RRs: | ||||||
| 451 | 0 | 0 | 0 | 0 | if ( | ||
| 0 | |||||||
| 452 | not @records and | ||||||
| 453 | ( | ||||||
| 454 | $self->query_rr_types == $self->query_rr_type_all or | ||||||
| 455 | $self->query_rr_types & $self->query_rr_type_txt | ||||||
| 456 | ) | ||||||
| 457 | ) { | ||||||
| 458 | # NOTE: | ||||||
| 459 | # This deliberately violates RFC 4406 (Sender ID), 4.4/3 (4.4.1): | ||||||
| 460 | # TXT-type RRs are still tried if there _are_ SPF-type RRs but all of | ||||||
| 461 | # them are inapplicable (i.e. "Hi!", or even "spf2.0/pra" for an | ||||||
| 462 | # 'mfrom' scope request). This conforms to the spirit of the more | ||||||
| 463 | # sensible algorithm in RFC 4408 (SPF), 4.5. | ||||||
| 464 | # Implication: Sender ID processing may make use of existing TXT- | ||||||
| 465 | # type records where a result of "None" would normally be returned | ||||||
| 466 | # under a strict interpretation of RFC 4406. | ||||||
| 467 | |||||||
| 468 | try { | ||||||
| 469 | 0 | 0 | 0 | $query_count++; | |||
| 470 | 0 | 0 | my $packet = $self->dns_lookup($domain, 'TXT'); | ||||
| 471 | 0 | 0 | push( | ||||
| 472 | @records, | ||||||
| 473 | $self->get_acceptable_records_from_packet( | ||||||
| 474 | $packet, 'TXT', \@versions, $scope, $domain) | ||||||
| 475 | ); | ||||||
| 476 | } | ||||||
| 477 | catch Mail::SPF::EDNSError with { | ||||||
| 478 | 0 | 0 | 0 | push(@dns_errors, shift); | |||
| 479 | 0 | 0 | }; | ||||
| 480 | } | ||||||
| 481 | |||||||
| 482 | 0 | 0 | 0 | @dns_errors < $query_count | |||
| 483 | or $dns_errors[0]->throw; | ||||||
| 484 | # Unless at least one query succeeded, re-throw the first DNS error that occurred. | ||||||
| 485 | |||||||
| 486 | @records | ||||||
| 487 | 0 | 0 | 0 | or throw Mail::SPF::ENoAcceptableRecord( | |||
| 488 | "No applicable sender policy available"); # RFC 4408, 4.5/7 | ||||||
| 489 | |||||||
| 490 | # Discard all records but the highest acceptable version: | ||||||
| 491 | 0 | 0 | my $preferred_record_class = $records[0]->class; | ||||
| 492 | 0 | 0 | @records = grep($_->isa($preferred_record_class), @records); | ||||
| 493 | |||||||
| 494 | 0 | 0 | 0 | @records == 1 | |||
| 495 | or throw Mail::SPF::ERedundantAcceptableRecords( | ||||||
| 496 | "Redundant applicable '" . $preferred_record_class->version_tag . "' " . | ||||||
| 497 | "sender policies found"); # RFC 4408, 4.5/6 | ||||||
| 498 | |||||||
| 499 | 0 | 0 | return $records[0]; | ||||
| 500 | } | ||||||
| 501 | |||||||
| 502 | =item B |
||||||
| 503 | returns I
|
||||||
| 504 | |||||||
| 505 | Filters from the given I |
||||||
| 506 | given RR type and for the given domain name, discarding any records that are | ||||||
| 507 | not SPF records at all, that are of an inacceptable SPF record version, or that | ||||||
| 508 | do not cover the given scope. Returns a list of acceptable records. | ||||||
| 509 | |||||||
| 510 | =cut | ||||||
| 511 | |||||||
| 512 | sub get_acceptable_records_from_packet { | ||||||
| 513 | 0 | 0 | 1 | 0 | my ($self, $packet, $rr_type, $versions, $scope, $domain) = @_; | ||
| 514 | |||||||
| 515 | 0 | 0 | my @versions = sort { $b <=> $a } @$versions; | ||||
| 0 | 0 | ||||||
| 516 | # Try higher record versions first. | ||||||
| 517 | # (This may be too simplistic for future revisions of SPF.) | ||||||
| 518 | |||||||
| 519 | 0 | 0 | my @records; | ||||
| 520 | |||||||
| 521 | 0 | 0 | foreach my $rr ($packet->answer) { | ||||
| 522 | 0 | 0 | 0 | next if $rr->type ne $rr_type; # Ignore RRs of unexpected type. | |||
| 523 | |||||||
| 524 | # char_str_list method is 'historical', use as a fallback for Net::DNS prior to 0.69 | ||||||
| 525 | # where txtdata is not available. | ||||||
| 526 | # join with no intervening spaces, RFC 6376 | ||||||
| 527 | # must call txtdata() in a list context | ||||||
| 528 | 0 | 0 | 0 | my $text = $rr->can('txtdata') | |||
| 529 | ? join('', $rr->txtdata) | ||||||
| 530 | : join('', $rr->char_str_list); | ||||||
| 531 | 0 | 0 | my $record; | ||||
| 532 | |||||||
| 533 | # Try to parse RR as each of the requested record versions, | ||||||
| 534 | # starting from the highest version: | ||||||
| 535 | VERSION: | ||||||
| 536 | 0 | 0 | foreach my $version (@versions) { | ||||
| 537 | 0 | 0 | my $class = $self->record_classes_by_version->{$version}; | ||||
| 538 | 0 | 0 | eval("require $class"); | ||||
| 539 | try { | ||||||
| 540 | 0 | 0 | 0 | $record = $class->new_from_string($text); | |||
| 541 | } | ||||||
| 542 | 0 | 0 | 0 | catch Mail::SPF::EInvalidRecordVersion with {}; | |||
| 543 | # Ignore non-SPF and unknown-version records. | ||||||
| 544 | # Propagate other errors (including syntax errors), though. | ||||||
| 545 | 0 | 0 | 0 | last VERSION if defined($record); | |||
| 546 | } | ||||||
| 547 | |||||||
| 548 | 0 | 0 | 0 | 0 | push(@records, $record) | ||
| 549 | if defined($record) | ||||||
| 550 | and grep($scope eq $_, $record->scopes); # record covers requested scope? | ||||||
| 551 | } | ||||||
| 552 | 0 | 0 | return @records; | ||||
| 553 | } | ||||||
| 554 | |||||||
| 555 | =item B |
||||||
| 556 | throws I |
||||||
| 557 | |||||||
| 558 | Queries the DNS using the configured resolver for resource records of the | ||||||
| 559 | desired type at the specified domain and returns a I |
||||||
| 560 | if an answer packet was received. Throws a I |
||||||
| 561 | if a DNS time-out occurred. Throws a I |
||||||
| 562 | error (other than RCODE 3 AKA C |
||||||
| 563 | |||||||
| 564 | =cut | ||||||
| 565 | |||||||
| 566 | sub dns_lookup { | ||||||
| 567 | 4 | 4 | 1 | 30 | my ($self, $domain, $rr_type) = @_; | ||
| 568 | |||||||
| 569 | 4 | 9 | my $packet; | ||||
| 570 | |||||||
| 571 | 4 | 50 | 29 | if (UNIVERSAL::isa($domain, 'Mail::SPF::MacroString')) { | |||
| 572 | 0 | 0 | $domain = $domain->expand; | ||||
| 573 | # Truncate overlong labels at 63 bytes (RFC 4408, 8.1/27): | ||||||
| 574 | 0 | 0 | $domain =~ s/([^.]{63})[^.]+/$1/g; | ||||
| 575 | # Drop labels from the head of domain if longer than 253 bytes (RFC 4408, 8.1/25): | ||||||
| 576 | 0 | 0 | $domain =~ s/^[^.]+\.(.*)$/$1/ | ||||
| 577 | while length($domain) > 253; | ||||||
| 578 | } | ||||||
| 579 | |||||||
| 580 | 4 | 1291 | $domain =~ s/^(.*?)\.?$/\L$1/; # Normalize domain. | ||||
| 581 | |||||||
| 582 | try { | ||||||
| 583 | 4 | 4 | 155 | $packet = $self->dns_resolver->send($domain, $rr_type); | |||
| 584 | } | ||||||
| 585 | otherwise { | ||||||
| 586 | 0 | 0 | 0 | throw Mail::SPF::ENoAcceptableRecord($self->dns_resolver->errorstring); | |||
| 587 | 4 | 63 | }; | ||||
| 588 | |||||||
| 589 | # Throw DNS exception unless an answer packet with RCODE 0 or 3 (NXDOMAIN) | ||||||
| 590 | # was received (thereby treating NXDOMAIN as an acceptable but empty answer packet): | ||||||
| 591 | 4 | 50 | 33 | 27991 | throw Mail::SPF::EDNSTimeout("Time-out on DNS '$rr_type' lookup of '$domain'") | ||
| 592 | if defined $self->dns_resolver->errorstring && $self->dns_resolver->errorstring =~ /^(timeout|query timed out)$/; | ||||||
| 593 | 4 | 50 | 65 | throw Mail::SPF::EDNSError("Unknown error on DNS '$rr_type' lookup of '$domain'") | |||
| 594 | unless defined $packet; | ||||||
| 595 | 4 | 100 | 17 | throw Mail::SPF::EDNSError("'" . $packet->header->rcode . "' error on DNS '$rr_type' lookup of '$domain'") | |||
| 596 | unless $packet->header->rcode =~ /^(NOERROR|NXDOMAIN)$/; | ||||||
| 597 | |||||||
| 598 | 3 | 234 | return $packet; | ||||
| 599 | } | ||||||
| 600 | |||||||
| 601 | =item B |
||||||
| 602 | |||||||
| 603 | Increments by one the count of DNS-interactive mechanisms and modifiers that | ||||||
| 604 | have been processed so far during the evaluation of the given | ||||||
| 605 | I |
||||||
| 606 | L constructor's C |
||||||
| 607 | I |
||||||
| 608 | |||||||
| 609 | This method is supposed to be called by the C |
||||||
| 610 | I |
||||||
| 611 | do any DNS look-ups. | ||||||
| 612 | |||||||
| 613 | =cut | ||||||
| 614 | |||||||
| 615 | sub count_dns_interactive_term { | ||||||
| 616 | 0 | 0 | 1 | my ($self, $request) = @_; | |||
| 617 | 0 | my $dns_interactive_terms_count = ++$request->root_request->state('dns_interactive_terms_count'); | |||||
| 618 | 0 | my $max_dns_interactive_terms = $self->max_dns_interactive_terms; | |||||
| 619 | 0 | 0 | 0 | if ( | |||
| 620 | defined($max_dns_interactive_terms) and | ||||||
| 621 | $dns_interactive_terms_count > $max_dns_interactive_terms | ||||||
| 622 | ) { | ||||||
| 623 | 0 | throw Mail::SPF::EProcessingLimitExceeded( | |||||
| 624 | "Maximum DNS-interactive terms limit ($max_dns_interactive_terms) exceeded"); | ||||||
| 625 | } | ||||||
| 626 | 0 | return; | |||||
| 627 | } | ||||||
| 628 | |||||||
| 629 | =item B |
||||||
| 630 | |||||||
| 631 | Increments by one the count of "void" DNS look-ups that have occurred so far | ||||||
| 632 | during the evaluation of the given I |
||||||
| 633 | exceeds the configured limit (see the L constructor's C |
||||||
| 634 | option), throws a I |
||||||
| 635 | |||||||
| 636 | This method is supposed to be called by any code after any calls to the | ||||||
| 637 | L method whenever (i) no answer records were returned, and (ii) | ||||||
| 638 | this fact is a possible indication of a DoS attack against a third-party victim | ||||||
| 639 | domain, and (iii) the number of "void" look-ups is not already constrained | ||||||
| 640 | otherwise (as for example is the case with the C |
||||||
| 641 | C |
||||||
| 642 | C, C macro. |
||||||
| 643 | |||||||
| 644 | =cut | ||||||
| 645 | |||||||
| 646 | sub count_void_dns_lookup { | ||||||
| 647 | 0 | 0 | 1 | my ($self, $request) = @_; | |||
| 648 | 0 | my $void_dns_lookups_count = ++$request->root_request->state('void_dns_lookups_count'); | |||||
| 649 | 0 | my $max_void_dns_lookups = $self->max_void_dns_lookups; | |||||
| 650 | 0 | 0 | 0 | if ( | |||
| 651 | defined($max_void_dns_lookups) and | ||||||
| 652 | $void_dns_lookups_count > $max_void_dns_lookups | ||||||
| 653 | ) { | ||||||
| 654 | 0 | throw Mail::SPF::EProcessingLimitExceeded( | |||||
| 655 | "Maximum void DNS look-ups limit ($max_void_dns_lookups) exceeded"); | ||||||
| 656 | } | ||||||
| 657 | 0 | return; | |||||
| 658 | } | ||||||
| 659 | |||||||
| 660 | =item B |
||||||
| 661 | |||||||
| 662 | Returns the default authority explanation as a I |
||||||
| 663 | description of the L constructor's C |
||||||
| 664 | option. | ||||||
| 665 | |||||||
| 666 | =cut | ||||||
| 667 | |||||||
| 668 | sub default_authority_explanation { | ||||||
| 669 | 0 | 0 | 1 | my ($self) = @_; | |||
| 670 | |||||||
| 671 | return Mail::SPF::MacroString->new( | ||||||
| 672 | text => $self->{default_authority_explanation}, | ||||||
| 673 | 0 | server => $self, | |||||
| 674 | is_explanation => TRUE | ||||||
| 675 | ); | ||||||
| 676 | } | ||||||
| 677 | |||||||
| 678 | =item B |
||||||
| 679 | |||||||
| 680 | Returns the local system's host name. See the description of the L | ||||||
| 681 | constructor's C |
||||||
| 682 | |||||||
| 683 | =item B |
||||||
| 684 | |||||||
| 685 | Returns the DNS resolver object of the server object. See the description of | ||||||
| 686 | the L constructor's C |
||||||
| 687 | |||||||
| 688 | =item B |
||||||
| 689 | |||||||
| 690 | Returns a value denoting the RR types for which to query when looking up and | ||||||
| 691 | selecting SPF records. See the description of the L constructor's | ||||||
| 692 | C |
||||||
| 693 | |||||||
| 694 | =item B |
||||||
| 695 | |||||||
| 696 | =item B |
||||||
| 697 | |||||||
| 698 | =item B |
||||||
| 699 | |||||||
| 700 | =item B |
||||||
| 701 | |||||||
| 702 | =item B |
||||||
| 703 | |||||||
| 704 | Return the limit values of the server object. See the description of the | ||||||
| 705 | L constructor's corresponding options. | ||||||
| 706 | |||||||
| 707 | =cut | ||||||
| 708 | |||||||
| 709 | # Make read-only accessors: | ||||||
| 710 | __PACKAGE__->make_accessor($_, TRUE) | ||||||
| 711 | foreach qw( | ||||||
| 712 | hostname | ||||||
| 713 | |||||||
| 714 | dns_resolver | ||||||
| 715 | query_rr_types | ||||||
| 716 | |||||||
| 717 | max_dns_interactive_terms | ||||||
| 718 | max_name_lookups_per_term | ||||||
| 719 | max_name_lookups_per_mx_mech | ||||||
| 720 | max_name_lookups_per_ptr_mech | ||||||
| 721 | |||||||
| 722 | max_void_dns_lookups | ||||||
| 723 | ); | ||||||
| 724 | |||||||
| 725 | =back | ||||||
| 726 | |||||||
| 727 | =head1 SEE ALSO | ||||||
| 728 | |||||||
| 729 | L |
||||||
| 730 | |||||||
| 731 | L |
||||||
| 732 | |||||||
| 733 | For availability, support, and license information, see the README file | ||||||
| 734 | included with Mail::SPF. | ||||||
| 735 | |||||||
| 736 | =head1 AUTHORS | ||||||
| 737 | |||||||
| 738 | Julian Mehnle |
||||||
| 739 | |||||||
| 740 | =cut | ||||||
| 741 | |||||||
| 742 | TRUE; |