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)