View Javadoc

1   /*****************************************************************
2    * Licensed to the Apache Software Foundation (ASF) under one   *
3    * or more contributor license agreements.  See the NOTICE file *
4    * distributed with this work for additional information        *
5    * regarding copyright ownership.  The ASF licenses this file   *
6    * to you under the Apache License, Version 2.0 (the            *
7    * "License"); you may not use this file except in compliance   *
8    * with the License.  You may obtain a copy of the License at   *
9    *                                                              *
10   *   http://www.apache.org/licenses/LICENSE-2.0                 *
11   *                                                              *
12   * Unless required by applicable law or agreed to in writing,   *
13   * software distributed under the License is distributed on an  *
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
15   * KIND, either express or implied.  See the License for the    *
16   * specific language governing permissions and limitations      *
17   * under the License.                                           *
18   ****************************************************************/
19  
20  
21  package org.apache.james.jspf.parser;
22  
23  import org.apache.james.jspf.core.Configuration;
24  import org.apache.james.jspf.core.Directive;
25  import org.apache.james.jspf.core.Logger;
26  import org.apache.james.jspf.core.Mechanism;
27  import org.apache.james.jspf.core.Modifier;
28  import org.apache.james.jspf.core.SPF1Constants;
29  import org.apache.james.jspf.core.SPF1Record;
30  import org.apache.james.jspf.core.SPFRecordParser;
31  import org.apache.james.jspf.exceptions.NeutralException;
32  import org.apache.james.jspf.exceptions.NoneException;
33  import org.apache.james.jspf.exceptions.PermErrorException;
34  
35  import java.util.ArrayList;
36  import java.util.Collection;
37  import java.util.Collections;
38  import java.util.Iterator;
39  import java.util.List;
40  import java.util.regex.Matcher;
41  import java.util.regex.Pattern;
42  
43  /***
44   * This class is used to parse SPF1-Records from their textual form to an
45   * SPF1Record object that is composed by 2 collections: directives and
46   * modifiers.
47   * 
48   * The parsing is modular and get informations from Mechanism and Modifiers
49   * classes declared in the org/apache/james/jspf/parser/jspf.default.terms file.
50   * 
51   * Each term implementation provide its own REGEX in the REGEX static public
52   * field. This parser simply join all the regexp in a single "alternative"
53   * pattern and count the number of catch groups (brackets) assigned to each
54   * regex fragment.
55   * 
56   * SO it creates a big regex and an array where it store what term is associated
57   * to each catch group of the big regex.
58   * 
59   * If the regex matches the input vspf1 record then it start looking for the
60   * matched group (not null) and lookup the term that created that part of the
61   * regex.
62   * 
63   * With this informations it creates a new instance of the term and, if the term
64   * is Configurable it calls the config() method passing to it only the specific
65   * subset of the MatchResult (using the MatchResultSubset).
66   * 
67   * TODO doubts about the specification - redirect or exp with no domain-spec are
68   * evaluated as an unknown-modifiers according to the current spec (it does not
69   * make too much sense) - top-label is defined differently in various specs.
70   * We'll have to review the code. -
71   * http://data.iana.org/TLD/tlds-alpha-by-domain.txt (we should probably beeter
72   * use and alpha sequence being at least 2 chars - Somewhere is defined as "."
73   * TLD [ "." ] - Otherwise defined as ( *alphanum ALPHA *alphanum ) / (
74   * 1*alphanum "-" *( * alphanum / "-" ) alphanum )
75   * 
76   * @see org.apache.james.jspf.core.SPF1Record
77   * 
78   */
79  public class DefaultSPF1Parser implements SPFRecordParser {
80  
81      /***
82       * Regex based on http://www.ietf.org/rfc/rfc4408.txt.
83       * This will be the next official SPF-Spec
84       */
85  
86      // Changed this because C, T and R MACRO_LETTERS are not available 
87      // in record parsing and must return a PermError.
88     
89      // private static final String MACRO_LETTER_PATTERN = "[lsodipvhcrtLSODIPVHCRT]";
90  
91      /***
92       * ABNF: qualifier = "+" / "-" / "?" / "~"
93       */
94      private static final String QUALIFIER_PATTERN = "[" + "//"
95              + SPF1Constants.PASS + "//" + SPF1Constants.FAIL + "//"
96              + SPF1Constants.NEUTRAL + "//" + SPF1Constants.SOFTFAIL + "]";
97  
98      private Pattern termsSeparatorPattern = null;
99  
100     private Pattern termPattern = null;
101 
102     private int TERM_STEP_REGEX_QUALIFIER_POS;
103 
104     private int TERM_STEP_REGEX_MECHANISM_POS;
105 
106     private int TERM_STEP_REGEX_MODIFIER_POS;
107 
108     private List matchResultPositions;
109 
110     private Logger log;
111 
112     private TermsFactory termsFactory;
113 
114     /***
115      * Constructor. Creates all the values needed to run the parsing
116      * 
117      * @param loggerThe logger to use
118      */
119     public DefaultSPF1Parser(Logger logger, TermsFactory termsFactory) {
120         this.log = logger;
121         this.termsFactory = termsFactory;
122         
123         /***
124          * ABNF: mechanism = ( all / include / A / MX / PTR / IP4 / IP6 / exists )
125          */
126         String MECHANISM_REGEX = createRegex(termsFactory.getMechanismsCollection());
127 
128         /***
129          * ABNF: modifier = redirect / explanation / unknown-modifier
130          */
131         String MODIFIER_REGEX = "(" + createRegex(termsFactory.getModifiersCollection()) + ")";
132 
133         /***
134          * ABNF: directive = [ qualifier ] mechanism
135          */
136         String DIRECTIVE_REGEX = "(" + QUALIFIER_PATTERN + "?)("
137                 + MECHANISM_REGEX + ")";
138 
139         /***
140          * ABNF: ( directive / modifier )
141          */
142         String TERM_REGEX = "(?:" + MODIFIER_REGEX + "|" + DIRECTIVE_REGEX
143                 + ")";
144 
145         /***
146          * ABNF: 1*SP
147          */
148         String TERMS_SEPARATOR_REGEX = "[ ]+";
149 
150         termsSeparatorPattern = Pattern.compile(TERMS_SEPARATOR_REGEX);
151         termPattern = Pattern.compile(TERM_REGEX);
152 
153         initializePositions();
154     }
155 
156     /***
157      * Fill in the matchResultPositions ArrayList. This array simply map each
158      * regex matchgroup to the Term class that originated that part of the
159      * regex.
160      */
161     private void initializePositions() {
162         ArrayList matchResultPositions = new ArrayList();
163 
164         // FULL MATCH
165         int posIndex = 0;
166         matchResultPositions.ensureCapacity(posIndex + 1);
167         matchResultPositions.add(posIndex, null);
168 
169         Iterator i;
170 
171         TERM_STEP_REGEX_MODIFIER_POS = ++posIndex;
172         matchResultPositions.ensureCapacity(posIndex + 1);
173         matchResultPositions.add(TERM_STEP_REGEX_MODIFIER_POS, null);
174         i = termsFactory.getModifiersCollection().iterator();
175         while (i.hasNext()) {
176             TermDefinition td = (TermDefinition) i.next();
177             int size = td.getMatchSize() + 1;
178             for (int k = 0; k < size; k++) {
179                 posIndex++;
180                 matchResultPositions.ensureCapacity(posIndex + 1);
181                 matchResultPositions.add(posIndex, td);
182             }
183         }
184 
185         TERM_STEP_REGEX_QUALIFIER_POS = ++posIndex;
186         matchResultPositions.ensureCapacity(posIndex + 1);
187         matchResultPositions.add(posIndex, null);
188 
189         TERM_STEP_REGEX_MECHANISM_POS = ++posIndex;
190         matchResultPositions.ensureCapacity(posIndex + 1);
191         matchResultPositions.add(TERM_STEP_REGEX_MECHANISM_POS, null);
192         i = termsFactory.getMechanismsCollection().iterator();
193         while (i.hasNext()) {
194             TermDefinition td = (TermDefinition) i.next();
195             int size = td.getMatchSize() + 1;
196             for (int k = 0; k < size; k++) {
197                 posIndex++;
198                 matchResultPositions.ensureCapacity(posIndex + 1);
199                 matchResultPositions.add(posIndex, td);
200             }
201         }
202 
203         if (log.isDebugEnabled()) {
204             log.debug("Parsing catch group positions: Modifiers["
205                     + TERM_STEP_REGEX_MODIFIER_POS + "] Qualifier["
206                     + TERM_STEP_REGEX_QUALIFIER_POS + "] Mechanism["
207                     + TERM_STEP_REGEX_MECHANISM_POS + "]");
208             for (int k = 0; k < matchResultPositions.size(); k++) {
209                 log
210                         .debug(k
211                                 + ") "
212                                 + (matchResultPositions.get(k) != null ? ((TermDefinition) matchResultPositions
213                                         .get(k)).getPattern().pattern()
214                                         : null));
215             }
216         }
217         
218         this.matchResultPositions = Collections.synchronizedList(matchResultPositions);
219     }
220 
221     /***
222      * Loop the classes searching for a String static field named
223      * staticFieldName and create an OR regeex like this:
224      * (?:FIELD1|FIELD2|FIELD3)
225      * 
226      * @param classes
227      *            classes to analyze
228      * @param staticFieldName
229      *            static field to concatenate
230      * @return regex The regex
231      */
232     private String createRegex(Collection commandMap) {
233         StringBuffer modifierRegex = new StringBuffer();
234         Iterator i = commandMap.iterator();
235         boolean first = true;
236         while (i.hasNext()) {
237             if (first) {
238                 modifierRegex.append("(?:(");
239                 first = false;
240             } else {
241                 modifierRegex.append(")|(");
242             }
243             Pattern pattern = ((TermDefinition) i.next()).getPattern();
244             modifierRegex.append(pattern.pattern());
245         }
246         modifierRegex.append("))");
247         return modifierRegex.toString();
248     }
249 
250     /***
251      * @see org.apache.james.jspf.parser.SPFRecordParser#parse(java.lang.String)
252      */
253     public SPF1Record parse(String spfRecord) throws PermErrorException,
254             NoneException, NeutralException {
255 
256         log.debug("Start parsing SPF-Record: " + spfRecord);
257 
258         SPF1Record result = new SPF1Record();
259 
260         // check the version "header"
261         if (spfRecord.toLowerCase().startsWith(SPF1Constants.SPF_VERSION + " ") || spfRecord.equalsIgnoreCase(SPF1Constants.SPF_VERSION)) {
262             if (!spfRecord.toLowerCase().startsWith(SPF1Constants.SPF_VERSION + " ")) throw new NeutralException("Empty SPF Record");
263         } else {
264             throw new NoneException("No valid SPF Record: " + spfRecord);
265         }
266 
267         // extract terms
268         String[] terms = termsSeparatorPattern.split(spfRecord.replaceFirst(
269                 SPF1Constants.SPF_VERSION, ""));
270 
271         // cycle terms
272         for (int i = 0; i < terms.length; i++) {
273             if (terms[i].length() > 0) {
274                 Matcher termMatcher = termPattern.matcher(terms[i]);
275                 if (!termMatcher.matches()) {
276                     throw new PermErrorException("Term [" + terms[i]
277                             + "] is not syntactically valid: "
278                             + termPattern.pattern());
279                 }
280 
281                 // true if we matched a modifier, false if we matched a
282                 // directive
283                 String modifierString = termMatcher
284                         .group(TERM_STEP_REGEX_MODIFIER_POS);
285 
286                 if (modifierString != null) {
287                     // MODIFIER
288                     Modifier mod = (Modifier) lookupAndCreateTerm(termMatcher,
289                             TERM_STEP_REGEX_MODIFIER_POS);
290 
291                     if (mod.enforceSingleInstance()) {
292                         Iterator it = result.getModifiers().iterator();
293                         while (it.hasNext()) {
294                             if (it.next().getClass().equals(mod.getClass())) {
295                                 throw new PermErrorException("More than one "
296                                         + modifierString
297                                         + " found in SPF-Record");
298                             }
299                         }
300                     }
301 
302                     result.getModifiers().add(mod);
303 
304                 } else {
305                     // DIRECTIVE
306                     String qualifier = termMatcher
307                             .group(TERM_STEP_REGEX_QUALIFIER_POS);
308 
309                     Object mech = lookupAndCreateTerm(termMatcher,
310                             TERM_STEP_REGEX_MECHANISM_POS);
311 
312                     result.getDirectives().add(
313                             new Directive(qualifier, (Mechanism) mech, log.getChildLogger(qualifier+"directive")));
314 
315                 }
316 
317             }
318         }
319 
320         return result;
321     }
322 
323     /***
324      * @param res
325      *            the MatchResult
326      * @param start
327      *            the position where the terms starts
328      * @return
329      * @throws PermErrorException
330      */
331     private Object lookupAndCreateTerm(Matcher res, int start)
332             throws PermErrorException {
333         for (int k = start + 1; k < res.groupCount(); k++) {
334             if (res.group(k) != null && k != TERM_STEP_REGEX_QUALIFIER_POS) {
335                 TermDefinition c = (TermDefinition) matchResultPositions.get(k);
336                 Configuration subres = new MatcherBasedConfiguration(res, k, c
337                         .getMatchSize());
338                 try {
339                     return termsFactory.createTerm(c.getTermDef(), subres);
340                 } catch (InstantiationException e) {
341                     e.printStackTrace();
342                     // TODO is it ok to use a Runtime for this? Or should we use a PermError here?
343                     throw new IllegalStateException("Unexpected error creating term: " + e.getMessage());
344                 }
345 
346             }
347         }
348         return null;
349     }
350 
351 }