lib/spf/model.rb in spf-0.0.2 vs lib/spf/model.rb in spf-0.0.3
- old
+ new
@@ -84,18 +84,25 @@
# :: | -
::
"
attr_reader :ip_address, :ip_network, :ipv4_prefix_length, :ipv6_prefix_length
+ attr_accessor :errors
- def initialize
+ def initialize(options = {})
@ip_address = nil
@ip_network = nil
@ipv4_prefix_length = nil
@ipv6_prefix_length = nil
+ @errors = []
end
+ def error(exception)
+ @errors << exception
+ raise exception
+ end
+
def self.new_from_string(text, options = {})
#term = SPF::Term.new(options, {:text => text})
options[:text] = text
term = self.new(options)
term.parse
@@ -116,11 +123,11 @@
def parse_ipv4_address(required = false)
if @parse_text.sub!(/^(#{IPV4_ADDRESS_PATTERN})/x, '')
@ip_address = $1
elsif required
raise SPF::TermIPv4AddressExpectedError.new(
- "Missing required IPv4 address in '#{@text}'");
+ "Missing required IPv4 address in '#{@text}'")
end
end
def parse_ipv4_prefix_length(required = false)
if @parse_text.sub!(/^\/(\d+)/, '')
@@ -196,10 +203,12 @@
end
class SPF::Mech < SPF::Term
+ attr_reader :ip_netblocks, :errors
+
DEFAULT_QUALIFIER = SPF::Record::DEFAULT_QUALIFIER
def default_ipv4_prefix_length; 32; end
def default_ipv6_prefix_length; 128; end
QUALIFIER_PATTERN = '[+\\-~\\?]'
@@ -211,11 +220,14 @@
:softfail => "Sender is not authorized to use '%{s}' in '%{_scope}' identity, however domain is not currently prepared for false failures",
:neutral => "Domain does not state whether sender is authorized to use '%{s}' in '%{_scope}' identity"
}
def initialize(options)
- super()
+ super(options)
+
+ @ip_netblocks = []
+
@text = options[:text]
if not self.instance_variable_defined?(:@parse_text)
@parse_text = @text.dup
end
if self.instance_variable_defined?(:@domain_spec) and
@@ -237,20 +249,19 @@
def parse_qualifier
if @parse_text.sub!(/(#{QUALIFIER_PATTERN})?/x, '')
@qualifier = $1 or DEFAULT_QUALIFIER
else
raise SPF::InvalidMechQualifierError.new(
- "Invalid qualifier encountered in '#{@text}'")
+ "Invalid qualifier encountered in '#{@text}'")
end
end
def parse_name
if @parse_text.sub!(/^ (#{NAME_PATTERN}) (?: : (?=.) )? /x, '')
@name = $1
else
- raise SPF::InvalidMech.new(
- "Unexpected mechanism encountered in '#{@text}'")
+ raise SPF::InvalidMech.new("Unexpected mechanism encountered in '#{@text}'")
end
end
def parse_params
# Parse generic string of parameters text (should be overridden in sub-classes):
@@ -300,13 +311,15 @@
server.count_void_dns_lookup(request) unless (rrs = packet.answer)
rrs.each do |rr|
if rr.type == 'A'
network = IP.new("#{rr.address}/#{ipv4_prefix_length}")
+ @ip_netblocks << network
return true if network.contains?(request.ip_address)
elsif rr.type == 'AAAA'
network = IP.new("#{rr.address}/#{ipv6_prefix_length}")
+ @ip_netblocks << network
return true if network.contains?(request.ip_address_v6)
elsif rr.type == 'CNAME'
# Ignore -- we should have gotten the A/AAAA records anyway.
else
# Unexpected RR type.
@@ -337,11 +350,11 @@
end
class SPF::Mech::A < SPF::Mech
- NAME = 'a'
+ NAME = 'a'
def parse_params
self.parse_domain_spec
self.parse_ipv4_ipv6_prefix_lengths
end
@@ -367,11 +380,11 @@
end
class SPF::Mech::All < SPF::Mech
- NAME = 'all'
+ NAME = 'all'
def parse_params
# No parameters.
end
@@ -381,11 +394,11 @@
end
class SPF::Mech::Exists < SPF::Mech
- NAME = 'exists'
+ NAME = 'exists'
def parse_params
self.parse_domain_spec(true)
end
@@ -394,24 +407,27 @@
end
def match(server, request, want_result = true)
server.count_dns_interactive_term(request)
+ # Other method of denoting "potentially ~infinite" netblocks?
+ @ip_netblocks << nil
domain = self.domain(server, request)
packet = server.dns_lookup(domain, 'A')
rrs = (packet.answer or server.count_void_dns_lookup(request))
rrs.each do |rr|
return true if rr.type == 'A'
end
+
return false
end
end
class SPF::Mech::IP4 < SPF::Mech
- NAME = 'ip4'
+ NAME = 'ip4'
def parse_params
self.parse_ipv4_network(true)
end
@@ -425,18 +441,19 @@
def match(server, request, want_result = true)
ip_network_v6 = IP::V4 === @ip_network ?
SPF::Util.ipv4_address_to_ipv6(@ip_network) :
@ip_network
+ @ip_netblocks << @ip_network
return ip_network_v6.contains?(request.ip_address_v6)
end
end
class SPF::Mech::IP6 < SPF::Mech
- NAME = 'ip6'
+ NAME = 'ip6'
def parse_params
self.parse_ipv6_network(true)
end
@@ -446,19 +463,27 @@
@ip_network.masklen != DEFAULT_IPV6_PREFIX_LENGTH
return params
end
def match(server, request, want_result = true)
+ @ip_netblocks << @ip_network
return @ip_network.contains?(request.ip_address_v6)
end
end
class SPF::Mech::Include < SPF::Mech
- NAME = 'include'
+ NAME = 'include'
+ attr_accessor :nested_record
+
+ def intitialize(options = {})
+ super(options)
+ @nested_record = nil
+ end
+
def parse_params
self.parse_domain_spec(true)
end
def params
@@ -474,10 +499,12 @@
sub_request = request.new_sub_request({:authority_domain => authority_domain})
# Process sub-request:
result = server.process(sub_request)
+ @nested_record = sub_request.record
+
# Translate result of sub-request (RFC 4408, 5.9):
return false unless want_result
return true if SPF::Result::Pass === result
@@ -496,11 +523,11 @@
end
end
class SPF::Mech::MX < SPF::Mech
- NAME = 'mx'
+ NAME = 'mx'
def parse_params
self.parse_domain_spec
self.parse_ipv4_ipv6_prefix_lengths
end
@@ -548,11 +575,11 @@
end
end
class SPF::Mech::PTR < SPF::Mech
- NAME = 'ptr'
+ NAME = 'ptr'
def parse_params
self.parse_domain_spec
end
@@ -676,15 +703,20 @@
end
end
class SPF::Mod::Redirect < SPF::GlobalMod
- attr_reader :domain_spec
+ attr_reader :domain_spec, :included_record
- NAME = 'redirect'
- PRECEDENCE = 0.8
+ NAME = 'redirect'
+ PRECEDENCE = 0.8
+ def initialize(options = {})
+ super(options)
+ @nested_record = nil
+ end
+
def parse_params
self.parse_domain_spec(true)
end
def params
@@ -702,10 +734,12 @@
sub_request = request.new_sub_request({:authority_domain => authority_domain})
# Process sub-request:
result = server.process(sub_request)
+ @nested_record = sub_request.record
+
# Translate result of sub-request (RFC 4408, 6.1/4):
if SPF::Result::None === result
server.throw_result(:permerror, request,
"Redirect domain '#{authority_domain}' has no applicable sender policy")
end
@@ -716,11 +750,11 @@
end
end
class SPF::Record
- attr_reader :terms, :text
+ attr_reader :terms, :text, :errors, :ip_netblocks
RESULTS_BY_QUALIFIER = {
'' => :pass,
'+' => :pass,
'-' => :fail,
@@ -728,13 +762,16 @@
'?' => :neutral
}
def initialize(options)
super()
- @parse_text = (@text = options[:text] if not self.instance_variable_defined?(:@parse_text)).dup
- @terms ||= []
- @global_mods ||= {}
+ @parse_text = (@text = options[:text] if not self.instance_variable_defined?(:@parse_text)).dup
+ @terms ||= []
+ @global_mods ||= {}
+ @errors = []
+ @ip_netblocks = []
+ @raise_exceptions = options.has_key?(:raise_exceptions) ? options[:raise_exceptions] : true
end
def self.new_from_string(text, options = {})
options[:text] = text
record = new(options)
@@ -745,11 +782,22 @@
def parse
unless self.instance_variable_defined?(:@parse_text) and @parse_text
raise SPF::NothingToParseError.new('Nothing to parse for record')
end
self.parse_version_tag
- self.parse_term while @parse_text.length > 0
+ while @parse_text.length > 0
+ term = nil
+ begin
+ term = self.parse_term
+ rescue SPF::Error => e
+ term.errors << e if term
+ @errors << e
+ raise if @raise_exceptions
+ end
+ @ip_netblocks << term.ip_netblocks if term
+ end
+ @ip_netblocks.flatten!
#self.parse_end
end
def parse_version_tag
#@parse_text.sub!(self.version_tag_pattern, '')
@@ -770,20 +818,20 @@
[^\x20]*
)
(?: \x20+ | $ )
/x
-
+ term = nil
if @parse_text.sub!(regex, '') and $&
# Looks like a mechanism:
mech_text = $1
mech_name = $2.downcase
mech_class = self.mech_classes[mech_name.to_sym]
unless mech_class
raise SPF::InvalidMech.new("Unknown mechanism type '#{mech_name}' in '#{@version_tag}' record")
end
- mech = mech_class.new_from_string(mech_text)
+ term = mech = mech_class.new_from_string(mech_text)
@terms << mech
elsif (
@parse_text.sub!(/
^
(
@@ -797,11 +845,11 @@
mod_text = $1
mod_name = $2.downcase
mod_class = MOD_CLASSES[mod_name]
if mod_class
# Known modifier.
- mod = mod_class.new_from_string(mod_text)
+ term = mod = mod_class.new_from_string(mod_text)
if SPF::GlobalMod === mod
# Global modifier.
unless @global_mods[mod_name]
raise SPF::DuplicateGlobalMod.new("Duplicate global modifier '#{mod_name}' encountered")
end
@@ -812,10 +860,11 @@
end
end
else
raise SPF::JunkInRecordError.new("Junk encountered in record '#{@text}'")
end
+ return term
end
def global_mods
return @global_mods.values.sort {|a,b| a.precedence <=> b.precedence }
end
@@ -832,10 +881,9 @@
raise SPF::OptionRequiredError.new('SPF server object required for record evaluation') unless server
raise SPF::OptionRequiredError.new('Request object required for record evaluation') unless request
begin
@terms.each do |term|
if SPF::Mech === term
- #if term.is_a?(SPF::Mech)
# Term is a mechanism.
mech = term
if mech.match(server, request, request.ip_address != nil)
result_name = RESULTS_BY_QUALIFIER[mech.qualifier]
result_class = server.result_class(result_name)