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  package org.apache.james.jspf;
21  
22  import org.apache.james.jspf.core.DNSRequest;
23  import org.apache.james.jspf.core.DNSService;
24  import org.apache.james.jspf.core.DNSServiceEnabled;
25  import org.apache.james.jspf.core.LogEnabled;
26  import org.apache.james.jspf.core.Logger;
27  import org.apache.james.jspf.core.MacroExpand;
28  import org.apache.james.jspf.core.MacroExpandEnabled;
29  import org.apache.james.jspf.core.SPFCheckEnabled;
30  import org.apache.james.jspf.core.SPFRecordParser;
31  import org.apache.james.jspf.core.exceptions.TimeoutException;
32  import org.apache.james.jspf.tester.DNSTestingServer;
33  import org.apache.james.jspf.executor.SPFExecutor;
34  import org.apache.james.jspf.executor.SPFResult;
35  import org.apache.james.jspf.executor.StagedMultipleSPFExecutor;
36  import org.apache.james.jspf.executor.SynchronousSPFExecutor;
37  import org.apache.james.jspf.impl.DNSJnioAsynchService;
38  import org.apache.james.jspf.impl.DNSServiceAsynchSimulator;
39  import org.apache.james.jspf.impl.DNSServiceXBillImpl;
40  import org.apache.james.jspf.impl.DefaultTermsFactory;
41  import org.apache.james.jspf.impl.SPF;
42  import org.apache.james.jspf.parser.RFC4408SPF1Parser;
43  import org.apache.james.jspf.wiring.WiringService;
44  import org.apache.james.jspf.wiring.WiringServiceException;
45  import org.apache.james.jspf.tester.SPFYamlTestDescriptor;
46  import org.xbill.DNS.Cache;
47  import org.xbill.DNS.DClass;
48  import org.xbill.DNS.ExtendedNonblockingResolver;
49  import org.xbill.DNS.Lookup;
50  import org.xbill.DNS.LookupAsynch;
51  import org.xbill.DNS.Name;
52  import org.xbill.DNS.NonblockingResolver;
53  import org.xbill.DNS.Resolver;
54  import org.xbill.DNS.SimpleResolver;
55  import org.xbill.DNS.TextParseException;
56  
57  import java.io.IOException;
58  import java.net.UnknownHostException;
59  import java.util.ArrayList;
60  import java.util.HashMap;
61  import java.util.Iterator;
62  import java.util.List;
63  import java.util.Locale;
64  import java.util.Map;
65  
66  import junit.framework.AssertionFailedError;
67  import junit.framework.TestCase;
68  
69  public abstract class AbstractYamlTest extends TestCase {
70  
71      private static final int FAKE_SERVER_PORT = 31348;
72      protected static final int TIMEOUT = 10;
73      protected static final int MOCK_SERVICE = 2;
74      protected static final int FAKE_SERVER = 1;
75      protected static final int REAL_SERVER = 3;
76      private int dnsServiceMockStyle = MOCK_SERVICE;
77  
78      protected static final int SYNCHRONOUS_EXECUTOR = 1;
79      protected static final int STAGED_EXECUTOR = 2;
80      protected static final int STAGED_EXECUTOR_MULTITHREADED = 3;
81      protected static final int STAGED_EXECUTOR_DNSJNIO = 4;
82      private int spfExecutorType = SYNCHRONOUS_EXECUTOR;
83  
84      SPFYamlTestDescriptor data;
85      String test;
86      protected Logger log;
87      private SPFExecutor executor;
88      protected static MacroExpand macroExpand;
89      protected static SPF spf;
90      protected static SPFYamlTestDescriptor prevData;
91      protected static SPFRecordParser parser;
92      private static DNSService dns;
93      protected static DNSTestingServer dnsTestServer;
94  
95      protected AbstractYamlTest(SPFYamlTestDescriptor def, String test) {
96          super(def.getComment()+" #"+test);
97          this.data = def;
98          this.test = test;
99      }
100 
101     protected AbstractYamlTest(SPFYamlTestDescriptor def) {
102         super(def.getComment()+" #COMPLETE!");
103         this.data = def;
104         this.test = null;
105     }
106 
107     protected abstract String getFilename();
108 
109     protected AbstractYamlTest(String name) throws IOException {
110         super(name);
111         List tests = SPFYamlTestDescriptor.loadTests(getFilename());
112         Iterator i = tests.iterator();
113         while (i.hasNext() && data == null) {
114             SPFYamlTestDescriptor def = (SPFYamlTestDescriptor) i.next();
115             if (name.equals(def.getComment()+" #COMPLETE!")) {
116                 data = def;
117                 this.test = null;
118             } else {
119                 Iterator j = def.getTests().keySet().iterator();
120                 while (j.hasNext() && data == null) {
121                     String test = (String) j.next();
122                     if (name.equals(def.getComment()+ " #"+test)) {
123                         data = def;
124                         this.test = test;
125                     }
126                 }
127             }
128         }
129         assertNotNull(data);
130         // assertNotNull(test);
131     }
132 
133     protected void runTest() throws Throwable {
134 
135         if (log == null) {
136                 log = new ConsoleLogger(ConsoleLogger.LEVEL_DEBUG, "root");
137         }
138         
139         log.info("Running test: "+getName()+" ...");
140 
141         if (parser == null) {
142             /* PREVIOUS SLOW WAY 
143             enabledServices = new WiringServiceTable();
144             enabledServices.put(LogEnabled.class, log);
145             */
146             parser = new RFC4408SPF1Parser(log.getChildLogger("parser"), new DefaultTermsFactory(log.getChildLogger("termsfactory"), new WiringService() {
147 
148                 public void wire(Object component) throws WiringServiceException {
149                     if (component instanceof LogEnabled) {
150                         String[] path = component.getClass().toString().split("\\.");
151                         ((LogEnabled) component).enableLogging(log.getChildLogger("dep").getChildLogger(path[path.length-1].toLowerCase()));
152                     }
153                     if (component instanceof MacroExpandEnabled) {
154                         ((MacroExpandEnabled) component).enableMacroExpand(macroExpand);
155                     }
156                     if (component instanceof DNSServiceEnabled) {
157                         ((DNSServiceEnabled) component).enableDNSService(dns);
158                     }
159                     if (component instanceof SPFCheckEnabled) {
160                         ((SPFCheckEnabled) component).enableSPFChecking(spf);
161                     }
162                 }
163                 
164             }));
165         }
166         if (this.data != AbstractYamlTest.prevData) {
167             dns = new LoggingDNSService(getDNSService(), log.getChildLogger("dns"));
168             AbstractYamlTest.prevData = this.data;
169         }
170         macroExpand = new MacroExpand(log.getChildLogger("macroExpand"), dns);
171         if (getSpfExecutorType() == SYNCHRONOUS_EXECUTOR) {  // synchronous
172             executor = new SynchronousSPFExecutor(log, dns);
173         } else if (getSpfExecutorType() == STAGED_EXECUTOR || getSpfExecutorType() == STAGED_EXECUTOR_MULTITHREADED){
174             executor = new StagedMultipleSPFExecutor(log, new DNSServiceAsynchSimulator(dns, getSpfExecutorType() == STAGED_EXECUTOR_MULTITHREADED));
175         } else if (getSpfExecutorType() == STAGED_EXECUTOR_DNSJNIO) {
176             
177             // reset cache between usages of the asynchronous lookuper
178             LookupAsynch.setDefaultCache(new Cache(), DClass.IN);
179             // reset cache between usages of the asynchronous lookuper
180             LookupAsynch.getDefaultCache(DClass.IN).clearCache();
181 
182             try {
183                 ExtendedNonblockingResolver resolver;
184                 
185                 if (getDnsServiceMockStyle() == FAKE_SERVER) {
186                     NonblockingResolver nonblockingResolver = new NonblockingResolver("127.0.0.1");
187                     resolver = ExtendedNonblockingResolver.newInstance(new NonblockingResolver[] {nonblockingResolver});
188                     nonblockingResolver.setPort(FAKE_SERVER_PORT);
189                     nonblockingResolver.setTCP(false);
190                 } else if (getDnsServiceMockStyle() == REAL_SERVER) {
191                     resolver = ExtendedNonblockingResolver.newInstance();
192                     Resolver[] resolvers = resolver.getResolvers();
193                     for (int i = 0; i < resolvers.length; i++) {
194                         resolvers[i].setTCP(false);
195                     }
196                 } else {
197                     throw new IllegalStateException("DnsServiceMockStyle "+getDnsServiceMockStyle()+" is not supported when STAGED_EXECUTOR_DNSJNIO executor style is used");
198                 }
199                 
200                 DNSJnioAsynchService jnioAsynchService = new DNSJnioAsynchService(resolver);
201                 jnioAsynchService.setTimeout(TIMEOUT);
202                 executor = new StagedMultipleSPFExecutor(log, jnioAsynchService);
203 
204             } catch (UnknownHostException e) {
205                 // TODO Auto-generated catch block
206                 e.printStackTrace();
207             }
208 
209         } else {
210             throw new UnsupportedOperationException("Unknown executor type");
211         }
212         spf = new SPF(dns, parser, log.getChildLogger("spf"), macroExpand, executor);
213 
214         if (test != null) {
215             String next = test;
216             SPFResult res = runSingleTest(next);
217             verifyResult(next, res);
218         } else {
219             Map queries = new HashMap();
220             for (Iterator i = data.getTests().keySet().iterator(); i.hasNext(); ) {
221                 String next = (String) i.next();
222                 SPFResult res = runSingleTest(next);
223                 queries.put(next, res);
224             }
225             AssertionFailedError firstError = null; 
226             for (Iterator i = queries.keySet().iterator(); i.hasNext(); ) {
227                 String next = (String) i.next();
228                 try {
229                     verifyResult(next, (SPFResult) queries.get(next));
230                 } catch (AssertionFailedError e) {
231                     log.getChildLogger(next).info("FAILED. "+e.getMessage()+" ("+getName()+")", e.getMessage()==null ? e : null);
232                     if (firstError == null) firstError = e;
233                 }
234             }
235             if (firstError != null) throw firstError;
236         }
237         
238     }
239 
240     private SPFResult runSingleTest(String testName) {
241         HashMap currentTest = (HashMap) data.getTests().get(testName);
242         Logger testLogger = log.getChildLogger(testName);
243         testLogger.info("TESTING "+testName+": "+currentTest.get("description"));
244 
245         String ip = null;
246         String sender = null;
247         String helo = null;
248     
249         if (currentTest.get("helo") != null) {
250             helo = (String) currentTest.get("helo");
251         }
252         if (currentTest.get("host") != null) {
253             ip = (String) currentTest.get("host");
254         }
255         if (currentTest.get("mailfrom") != null) {
256             sender = (String) currentTest.get("mailfrom");
257         } else {
258             sender = "";
259         }
260     
261         SPFResult res = spf.checkSPF(ip, sender, helo);
262         return res;
263     }
264 
265     private void verifyResult(String testName, SPFResult res) {
266         String resultSPF = res.getResult();
267         HashMap currentTest = (HashMap) data.getTests().get(testName);
268         Logger testLogger = log.getChildLogger(testName+"-verify");
269         if (currentTest.get("result") instanceof String) {
270             assertEquals("Test "+testName+" ("+currentTest.get("description")+") failed. Returned: "+resultSPF+" Expected: "+currentTest.get("result")+" [["+resultSPF+"||"+res.getHeaderText()+"]]", currentTest.get("result"), resultSPF);
271         } else {
272             ArrayList results = (ArrayList) currentTest.get("result");
273             boolean match = false;
274             for (int i = 0; i < results.size(); i++) {
275                 if (results.get(i).equals(resultSPF)) match = true;
276                 // testLogger.debug("checking "+resultSPF+" against allowed result "+results.get(i));
277             }
278             assertTrue("Test "+testName+" ("+currentTest.get("description")+") failed. Returned: "+resultSPF+" Expected: "+results, match);
279         }
280         
281         if (currentTest.get("explanation") != null) {
282             
283             // Check for our default explanation!
284             if (currentTest.get("explanation").equals("DEFAULT")) {
285                 assertTrue(res.getExplanation().startsWith("http://www.openspf.org/why.html?sender="));
286             } else if (currentTest.get("explanation").equals("cafe:babe::1 is queried as 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.E.B.A.B.E.F.A.C.ip6.arpa")) {
287                 // See http://java.sun.com/j2se/1.4.2/docs/api/java/net/Inet6Address.html    
288                 // For methods that return a textual representation as output value, the full form is used. 
289                 // Inet6Address will return the full form because it is unambiguous when used in combination with other textual data.
290                 assertTrue(res.getExplanation().equals("cafe:babe:0:0:0:0:0:1 is queried as 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.E.B.A.B.E.F.A.C.ip6.arpa"));
291             } else {
292                 assertEquals(currentTest.get("explanation"),res.getExplanation());
293             }
294     
295         }
296     
297         testLogger.info("PASSED. Result="+resultSPF+" Explanation="+res.getExplanation()+" Header="+res.getHeaderText());
298     }
299 
300     /**
301      * @return a Mocked DNSService
302      */
303     protected DNSService getDNSServiceMockedDNSService() {
304         SPFYamlDNSService yamlDNSService = new SPFYamlDNSService(data.getZonedata());
305         return yamlDNSService;
306     }
307 
308     /**
309      * @return the right dnsservice according to what the test specialization declares
310      */
311     protected DNSService getDNSService() {
312         switch (getDnsServiceMockStyle()) {
313             case MOCK_SERVICE: return getDNSServiceMockedDNSService();
314             case FAKE_SERVER: return getDNSServiceFakeServer();
315             case REAL_SERVER: return getDNSServiceReal();
316             default: 
317                 throw new UnsupportedOperationException("Unsupported mock style");
318         }
319     }
320 
321     protected int getDnsServiceMockStyle() {
322         return dnsServiceMockStyle;
323     }
324 
325     /**
326      * @return a dns resolver pointing to the local fake server
327      */
328     protected DNSService getDNSServiceFakeServer() {
329         Resolver resolver = null;
330         try {
331             resolver = new SimpleResolver("127.0.0.1");
332         } catch (UnknownHostException e) {
333             // TODO Auto-generated catch block
334             e.printStackTrace();
335         }
336         resolver.setPort(FAKE_SERVER_PORT);
337         Lookup.setDefaultResolver(resolver);
338         Lookup.setDefaultCache(null, DClass.IN);
339         Lookup.setDefaultSearchPath(new Name[] {});
340 
341         if (dnsTestServer == null) {
342             try {
343                 dnsTestServer = new DNSTestingServer("0.0.0.0", ""+FAKE_SERVER_PORT);
344             } catch (TextParseException e) {
345                 throw new RuntimeException("Error trying to instantiate the testing dns server.", e);
346             } catch (IOException e) {
347                 throw new RuntimeException("Error trying to instantiate the testing dns server.", e);
348             }
349         }
350         
351         dnsTestServer.setData(data.getZonedata());
352         
353         DNSServiceXBillImpl serviceXBillImpl = new DNSServiceXBillImpl(log) {
354 
355             public List getLocalDomainNames() {
356                 List l = new ArrayList();
357                 l.add("localdomain.foo.bar");
358                 return l; 
359             }
360 
361         };
362         // TIMEOUT 2 seconds
363         serviceXBillImpl.setTimeOut(TIMEOUT);
364         return serviceXBillImpl;
365     }
366     
367     /**
368      * @return a real dns resolver
369      */
370     protected DNSService getDNSServiceReal() {
371         DNSServiceXBillImpl serviceXBillImpl = new DNSServiceXBillImpl(log);
372         // TIMEOUT 2 seconds
373         serviceXBillImpl.setTimeOut(TIMEOUT);
374         return serviceXBillImpl;
375     }
376 
377     public AbstractYamlTest() {
378         super();
379     }
380 
381     final class SPFYamlDNSService implements DNSService {
382 
383         private HashMap zonedata;
384         private int recordLimit;
385 
386         public SPFYamlDNSService(HashMap zonedata) {
387             this.zonedata = zonedata;
388             this.recordLimit = 10;
389         }
390 
391         public List getLocalDomainNames() {
392             List l = new ArrayList();
393             l.add("localdomain.foo.bar");
394             return l; 
395         }
396 
397         public void setTimeOut(int timeOut) {
398             try {
399                 throw new UnsupportedOperationException("setTimeOut()");
400             } catch (UnsupportedOperationException e) {
401                 e.printStackTrace();
402                 throw e;
403             }
404         }
405 
406         public int getRecordLimit() {
407             return recordLimit;
408         }
409 
410         public void setRecordLimit(int recordLimit) {
411             this.recordLimit = recordLimit;
412         }
413 
414         public List getRecords(DNSRequest request) throws TimeoutException {
415             return getRecords(request.getHostname(), request.getRecordType(), 6);
416         }
417 
418         public List getRecords(String hostname, int recordType, int depth) throws TimeoutException {
419             String type = getRecordTypeDescription(recordType);
420 
421             List res;
422             
423             // remove trailing dot before running the search.
424             if (hostname.endsWith(".")) hostname = hostname.substring(0, hostname.length()-1);
425             
426             // dns search lowercases:
427             hostname = hostname.toLowerCase(Locale.US);
428             
429             if (zonedata.get(hostname) != null) {
430                 List l = (List) zonedata.get(hostname);
431                 Iterator i = l.iterator();
432                 res = new ArrayList();
433                 while (i.hasNext()) {
434                     Object o = i.next();
435                     if (o instanceof HashMap) {
436                         HashMap hm = (HashMap) o;
437                         if (hm.get(type) != null) {
438                             if (recordType == DNSRequest.MX) {
439                                 List mxList = (List) hm.get(type);
440     
441                                 // For MX records we overwrite the result ignoring the priority.
442                                 Iterator mxs = mxList.iterator();
443                                 while (mxs.hasNext()) {
444                                     // skip the MX priority
445                                     mxs.next();
446                                     String cname = (String) mxs.next();
447                                     res.add(cname);
448                                 }
449                             } else {
450                                 Object obj = hm.get(type);
451                                 
452                                 if (obj instanceof String) {
453                                     res.add(obj);
454                                 } else if (obj instanceof ArrayList) {
455                                     ArrayList a = (ArrayList) obj;
456                                     StringBuffer sb = new StringBuffer();
457                                     
458                                     for (int i2 = 0; i2 < a.size(); i2++) {
459                                         sb.append(a.get(i2));
460                                     }
461                                     res.add(sb.toString());
462                                 }
463                             }
464                         }
465                         if (hm.get("CNAME") != null && depth > 0) {
466                             return getRecords((String) hm.get("CNAME"), recordType, depth - 1);
467                         }
468                     } else if ("TIMEOUT".equals(o)) {
469                         throw new TimeoutException("TIMEOUT");
470                     } else {
471                         throw new IllegalStateException("getRecord found an unexpected data");
472                     }
473                 }
474                 return res.size() > 0 ? res : null;
475             }
476             return null;
477         }
478         
479     }
480 
481     
482     /**
483      * Return a string representation of a DNSService record type.
484      * 
485      * @param recordType the DNSService.CONSTANT type to convert
486      * @return a string representation of the given record type
487      */
488     public static String getRecordTypeDescription(int recordType) {
489         switch (recordType) {
490             case DNSRequest.A: return "A";
491             case DNSRequest.AAAA: return "AAAA";
492             case DNSRequest.MX: return "MX";
493             case DNSRequest.PTR: return "PTR";
494             case DNSRequest.TXT: return "TXT";
495             case DNSRequest.SPF: return "SPF";
496             default: return null;
497         }
498     }
499 
500     protected int getSpfExecutorType() {
501         return spfExecutorType;
502     }
503 
504 }