blob: fc7579199397f3879b3fae461971a2a18fa4c2c5 [file] [log] [blame]
/****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one *
* or more contributor license agreements. See the NOTICE file *
* distributed with this work for additional information *
* regarding copyright ownership. The ASF licenses this file *
* to you under the Apache License, Version 2.0 (the *
* "License"); you may not use this file except in compliance *
* with the License. You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, *
* software distributed under the License is distributed on an *
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
* KIND, either express or implied. See the License for the *
* specific language governing permissions and limitations *
* under the License. *
****************************************************************/
package org.apache.james.jspf.parser;
import org.apache.james.jspf.core.SPF1Constants;
import org.apache.james.jspf.core.SPF1Record;
import org.apache.james.jspf.core.SPFRecordParser;
import org.apache.james.jspf.core.exceptions.NeutralException;
import org.apache.james.jspf.core.exceptions.NoneException;
import org.apache.james.jspf.core.exceptions.PermErrorException;
import org.apache.james.jspf.executor.FutureSPFResult;
import org.apache.james.jspf.terms.Configuration;
import org.apache.james.jspf.terms.Directive;
import org.apache.james.jspf.terms.Mechanism;
import org.apache.james.jspf.terms.Modifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* This class is used to parse SPF1-Records from their textual form to an
* SPF1Record object that is composed by 2 collections: directives and
* modifiers.
*
* The parsing is modular and get informations from Mechanism and Modifiers
* classes declared in the org/apache/james/jspf/parser/jspf.default.terms file.
*
* Each term implementation provide its own REGEX in the REGEX static public
* field. This parser simply join all the regexp in a single "alternative"
* pattern and count the number of catch groups (brackets) assigned to each
* regex fragment.
*
* SO it creates a big regex and an array where it store what term is associated
* to each catch group of the big regex.
*
* If the regex matches the input vspf1 record then it start looking for the
* matched group (not null) and lookup the term that created that part of the
* regex.
*
* With this informations it creates a new instance of the term and, if the term
* is ConfigurationEnabled it calls the config() method passing to it only the specific
* subset of the MatchResult (using the MatchResultSubset).
*
* TODO doubts about the specification - redirect or exp with no domain-spec are
* evaluated as an unknown-modifiers according to the current spec (it does not
* make too much sense) - top-label is defined differently in various specs.
* We'll have to review the code. -
* http://data.iana.org/TLD/tlds-alpha-by-domain.txt (we should probably beeter
* use and alpha sequence being at least 2 chars - Somewhere is defined as "."
* TLD [ "." ] - Otherwise defined as ( *alphanum ALPHA *alphanum ) / (
* 1*alphanum "-" *( * alphanum / "-" ) alphanum )
*
* @see org.apache.james.jspf.core.SPF1Record
*
*/
public class RFC4408SPF1Parser implements SPFRecordParser {
private static final Logger LOGGER = LoggerFactory.getLogger(RFC4408SPF1Parser.class);
/**
* Regex based on http://www.ietf.org/rfc/rfc4408.txt.
* This will be the next official SPF-Spec
*/
// Changed this because C, T and R MACRO_LETTERS are not available
// in record parsing and must return a PermError.
// private static final String MACRO_LETTER_PATTERN = "[lsodipvhcrtLSODIPVHCRT]";
/**
* ABNF: qualifier = "+" / "-" / "?" / "~"
*/
private static final String QUALIFIER_PATTERN = "[" + "\\"
+ SPF1Constants.PASS + "\\" + SPF1Constants.FAIL + "\\"
+ SPF1Constants.NEUTRAL + "\\" + SPF1Constants.SOFTFAIL + "]";
private Pattern termsSeparatorPattern = null;
private Pattern termPattern = null;
private int TERM_STEP_REGEX_QUALIFIER_POS;
private int TERM_STEP_REGEX_MECHANISM_POS;
private int TERM_STEP_REGEX_MODIFIER_POS;
private List<TermDefinition> matchResultPositions;
private TermsFactory termsFactory;
/**
* Constructor. Creates all the values needed to run the parsing
*
* @param termsFactory the TermsFactory implementation
*/
public RFC4408SPF1Parser(TermsFactory termsFactory) {
this.termsFactory = termsFactory;
/**
* ABNF: mechanism = ( all / include / A / MX / PTR / IP4 / IP6 / exists )
*/
String MECHANISM_REGEX = createRegex(termsFactory.getMechanismsCollection());
/**
* ABNF: modifier = redirect / explanation / unknown-modifier
*/
String MODIFIER_REGEX = "(" + createRegex(termsFactory.getModifiersCollection()) + ")";
/**
* ABNF: directive = [ qualifier ] mechanism
*/
String DIRECTIVE_REGEX = "(" + QUALIFIER_PATTERN + "?)("
+ MECHANISM_REGEX + ")";
/**
* ABNF: ( directive / modifier )
*/
String TERM_REGEX = "(?:" + MODIFIER_REGEX + "|" + DIRECTIVE_REGEX
+ ")";
/**
* ABNF: 1*SP
*/
String TERMS_SEPARATOR_REGEX = "[ ]+";
termsSeparatorPattern = Pattern.compile(TERMS_SEPARATOR_REGEX);
termPattern = Pattern.compile(TERM_REGEX);
initializePositions();
}
/**
* Fill in the matchResultPositions ArrayList. This array simply map each
* regex matchgroup to the Term class that originated that part of the
* regex.
*/
private void initializePositions() {
ArrayList<TermDefinition> matchResultPositions = new ArrayList<TermDefinition>();
// FULL MATCH
int posIndex = 0;
matchResultPositions.ensureCapacity(posIndex + 1);
matchResultPositions.add(posIndex, null);
Iterator<TermDefinition> i;
TERM_STEP_REGEX_MODIFIER_POS = ++posIndex;
matchResultPositions.ensureCapacity(posIndex + 1);
matchResultPositions.add(TERM_STEP_REGEX_MODIFIER_POS, null);
i = termsFactory.getModifiersCollection().iterator();
while (i.hasNext()) {
TermDefinition td = i.next();
int size = td.getMatchSize() + 1;
for (int k = 0; k < size; k++) {
posIndex++;
matchResultPositions.ensureCapacity(posIndex + 1);
matchResultPositions.add(posIndex, td);
}
}
TERM_STEP_REGEX_QUALIFIER_POS = ++posIndex;
matchResultPositions.ensureCapacity(posIndex + 1);
matchResultPositions.add(posIndex, null);
TERM_STEP_REGEX_MECHANISM_POS = ++posIndex;
matchResultPositions.ensureCapacity(posIndex + 1);
matchResultPositions.add(TERM_STEP_REGEX_MECHANISM_POS, null);
i = termsFactory.getMechanismsCollection().iterator();
while (i.hasNext()) {
TermDefinition td = i.next();
int size = td.getMatchSize() + 1;
for (int k = 0; k < size; k++) {
posIndex++;
matchResultPositions.ensureCapacity(posIndex + 1);
matchResultPositions.add(posIndex, td);
}
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Parsing catch group positions: Modifiers[{}] Qualifier[{}] Mechanism[{}]",
TERM_STEP_REGEX_MODIFIER_POS, TERM_STEP_REGEX_QUALIFIER_POS, TERM_STEP_REGEX_MECHANISM_POS);
for (int k = 0; k < matchResultPositions.size(); k++) {
LOGGER
.debug("{}) {}", k,
(matchResultPositions.get(k) != null ? ((TermDefinition) matchResultPositions
.get(k)).getPattern().pattern()
: null));
}
}
this.matchResultPositions = Collections.synchronizedList(matchResultPositions);
}
/**
* Loop the classes searching for a String static field named
* staticFieldName and create an OR regeex like this:
* (?:FIELD1|FIELD2|FIELD3)
*
* @param classes
* classes to analyze
* @param staticFieldName
* static field to concatenate
* @return regex The regex
*/
private String createRegex(Collection<TermDefinition> commandMap) {
StringBuffer modifierRegex = new StringBuffer();
Iterator<TermDefinition> i = commandMap.iterator();
boolean first = true;
while (i.hasNext()) {
if (first) {
modifierRegex.append("(?:(");
first = false;
} else {
modifierRegex.append(")|(");
}
Pattern pattern = i.next().getPattern();
modifierRegex.append(pattern.pattern());
}
modifierRegex.append("))");
return modifierRegex.toString();
}
/**
* @see org.apache.james.jspf.core.SPFRecordParser#parse(java.lang.String)
*/
public SPF1Record parse(String spfRecord) throws PermErrorException,
NoneException, NeutralException {
LOGGER.debug("Start parsing SPF-Record: " + spfRecord);
SPF1Record result = new SPF1Record();
// check the version "header"
if (spfRecord.toLowerCase().startsWith(SPF1Constants.SPF_VERSION1 + " ") || spfRecord.equalsIgnoreCase(SPF1Constants.SPF_VERSION1)) {
if (!spfRecord.toLowerCase().startsWith(SPF1Constants.SPF_VERSION1 + " ")) throw new NeutralException("Empty SPF Record");
} else {
throw new NoneException("No valid SPF Record: " + spfRecord);
}
// extract terms
String[] terms = termsSeparatorPattern.split(spfRecord.replaceFirst(
SPF1Constants.SPF_VERSION1, ""));
// cycle terms
for (int i = 0; i < terms.length; i++) {
if (terms[i].length() > 0) {
Matcher termMatcher = termPattern.matcher(terms[i]);
if (!termMatcher.matches()) {
throw new PermErrorException("Term [" + terms[i]
+ "] is not syntactically valid: "
+ termPattern.pattern());
}
// true if we matched a modifier, false if we matched a
// directive
String modifierString = termMatcher
.group(TERM_STEP_REGEX_MODIFIER_POS);
if (modifierString != null) {
// MODIFIER
Modifier mod = (Modifier) lookupAndCreateTerm(termMatcher,
TERM_STEP_REGEX_MODIFIER_POS);
if (mod.enforceSingleInstance()) {
Iterator<Modifier> it = result.getModifiers().iterator();
while (it.hasNext()) {
if (it.next().getClass().equals(mod.getClass())) {
throw new PermErrorException("More than one "
+ modifierString
+ " found in SPF-Record");
}
}
}
result.getModifiers().add(mod);
} else {
// DIRECTIVE
String qualifier = termMatcher
.group(TERM_STEP_REGEX_QUALIFIER_POS);
Object mech = lookupAndCreateTerm(termMatcher,
TERM_STEP_REGEX_MECHANISM_POS);
result.getDirectives().add(
new Directive(qualifier, (Mechanism) mech));
}
}
}
return result;
}
/**
* @param res
* the MatchResult
* @param start
* the position where the terms starts
* @return
* @throws PermErrorException
*/
private Object lookupAndCreateTerm(Matcher res, int start)
throws PermErrorException {
for (int k = start + 1; k < res.groupCount(); k++) {
if (res.group(k) != null && k != TERM_STEP_REGEX_QUALIFIER_POS) {
TermDefinition c = (TermDefinition) matchResultPositions.get(k);
Configuration subres = new MatcherBasedConfiguration(res, k, c
.getMatchSize());
try {
return termsFactory.createTerm(c.getTermDef(), subres);
} catch (InstantiationException e) {
e.printStackTrace();
// TODO is it ok to use a Runtime for this? Or should we use a PermError here?
throw new IllegalStateException("Unexpected error creating term: " + e.getMessage());
}
}
}
return null;
}
}