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  package org.apache.james.smtpserver.core.filter.fastfail;
21  
22  import java.io.InputStream;
23  import java.sql.Connection;
24  import java.sql.DatabaseMetaData;
25  import java.sql.PreparedStatement;
26  import java.sql.ResultSet;
27  import java.sql.SQLException;
28  import java.util.ArrayList;
29  import java.util.Collection;
30  import java.util.HashMap;
31  import java.util.Iterator;
32  import java.util.Map;
33  import java.util.StringTokenizer;
34  import java.sql.Timestamp;
35  
36  import org.apache.avalon.cornerstone.services.datasources.DataSourceSelector;
37  import org.apache.avalon.excalibur.datasource.DataSourceComponent;
38  import org.apache.avalon.framework.activity.Initializable;
39  import org.apache.avalon.framework.configuration.Configurable;
40  import org.apache.avalon.framework.configuration.Configuration;
41  import org.apache.avalon.framework.configuration.ConfigurationException;
42  import org.apache.avalon.framework.logger.AbstractLogEnabled;
43  import org.apache.avalon.framework.service.ServiceException;
44  import org.apache.avalon.framework.service.ServiceManager;
45  import org.apache.avalon.framework.service.Serviceable;
46  
47  import org.apache.james.api.dnsservice.DNSService;
48  import org.apache.james.api.dnsservice.util.NetMatcher;
49  import org.apache.james.dsn.DSNStatus;
50  import org.apache.james.services.FileSystem;
51  import org.apache.james.smtpserver.CommandHandler;
52  import org.apache.james.smtpserver.SMTPSession;
53  import org.apache.james.util.TimeConverter;
54  import org.apache.james.util.sql.JDBCUtil;
55  import org.apache.james.util.sql.SqlResources;
56  import org.apache.mailet.MailAddress;
57  
58  /**
59   * GreylistHandler which can be used to activate Greylisting
60   */
61  public class GreylistHandler extends AbstractLogEnabled implements
62      CommandHandler, Configurable, Serviceable, Initializable {
63  
64      private DataSourceSelector datasources = null;
65  
66      private DataSourceComponent datasource = null;
67  
68      private FileSystem fileSystem = null;
69  
70      // 1 hour
71      private long tempBlockTime = 3600000;
72  
73      // 36 days
74      private long autoWhiteListLifeTime = 3110400000L;
75  
76      // 4 hours
77      private long unseenLifeTime = 14400000;
78  
79      private String selectQuery;
80  
81      private String insertQuery;
82  
83      private String deleteQuery;
84  
85      private String deleteAutoWhiteListQuery;
86  
87      private String updateQuery;
88  
89      /**
90       * Contains all of the sql strings for this component.
91       */
92      private SqlResources sqlQueries = new SqlResources();
93  
94      /**
95       * The sqlFileUrl
96       */
97      private String sqlFileUrl;
98  
99      /**
100      * Holds value of property sqlParameters.
101      */
102     private Map sqlParameters = new HashMap();
103 
104     /**
105      * The repositoryPath
106      */
107     private String repositoryPath;
108 
109     private DNSService dnsServer;
110 
111     private NetMatcher wNetworks;
112 
113     /**
114      * @see org.apache.avalon.framework.configuration.Configurable#configure(Configuration)
115      */
116     public void configure(Configuration handlerConfiguration) throws ConfigurationException {
117         Configuration configTemp = handlerConfiguration.getChild("tempBlockTime", false);
118         if (configTemp != null) {
119             try {
120                 setTempBlockTime(configTemp.getValue());
121 
122             } catch (NumberFormatException e) {
123                throw new ConfigurationException(e.getMessage());
124             }
125         }
126     
127         Configuration configAutoWhiteList = handlerConfiguration.getChild("autoWhiteListLifeTime", false);
128         if (configAutoWhiteList != null) {
129             try {
130                 setAutoWhiteListLifeTime(configAutoWhiteList.getValue());
131             } catch (NumberFormatException e) {
132                 throw new ConfigurationException(e.getMessage());
133             }
134         }
135        
136         Configuration configUnseen = handlerConfiguration.getChild("unseenLifeTime", false);
137         if (configUnseen != null) {
138             try {
139                 setUnseenLifeTime(configUnseen.getValue());
140             } catch (NumberFormatException e) {
141                 throw new ConfigurationException(e.getMessage());
142             }
143         }
144 
145         Configuration configRepositoryPath = handlerConfiguration.getChild("repositoryPath", false);
146         if (configRepositoryPath != null) {
147             setRepositoryPath(configRepositoryPath.getValue());
148         } else {
149             throw new ConfigurationException("repositoryPath is not configured");
150         }
151 
152         // Get the SQL file location
153         Configuration sFile = handlerConfiguration.getChild("sqlFile", false);
154         if (sFile != null) {
155             setSqlFileUrl(sFile.getValue());
156             if (!sqlFileUrl.startsWith("file://")) {
157                 throw new ConfigurationException(
158                     "Malformed sqlFile - Must be of the format \"file://<filename>\".");
159             }
160         } else {
161             throw new ConfigurationException("sqlFile is not configured");
162         }
163 
164         Configuration whitelistedNetworks = handlerConfiguration.getChild("whitelistedNetworks", false);
165         if (whitelistedNetworks != null) {
166             Collection nets = whitelistedNetworks(whitelistedNetworks.getValue());
167 
168             if (nets != null) {
169                 wNetworks = new NetMatcher(nets,dnsServer);
170                 getLogger().info("Whitelisted addresses: " + wNetworks.toString());
171             }
172         }
173     }
174 
175     /**
176      * @see org.apache.avalon.framework.activity.Initializable#initialize()
177      */
178     public void initialize() throws Exception {
179         setDataSource(initDataSource(repositoryPath));
180         initSqlQueries(datasource.getConnection(), sqlFileUrl);
181         
182         // create table if not exist
183         createTable(datasource.getConnection(), "greyListTableName", "createGreyListTable");
184     }
185 
186     /**
187      * @see org.apache.avalon.framework.service.Serviceable#service(org.apache.avalon.framework.service.ServiceManager)
188      */
189     public void service(ServiceManager serviceMan) throws ServiceException {
190         setDataSources((DataSourceSelector) serviceMan.lookup(DataSourceSelector.ROLE));
191         setDnsServer((DNSService) serviceMan.lookup(DNSService.ROLE));
192         setFileSystem((FileSystem) serviceMan.lookup(FileSystem.ROLE));
193     }
194 
195     /**
196      * Set the DNSService
197      * 
198      * @param dnsServer
199      *            The DNSService
200      */
201     public void setDnsServer(DNSService dnsServer) {
202         this.dnsServer = dnsServer;
203     }
204 
205     /**
206      * Set the sqlFileUrl to use for getting the sqlRessource.xml file
207      * 
208      * @param sqlFileUrl
209      *            The fileUrl
210      */
211     public void setSqlFileUrl(String sqlFileUrl) {
212         this.sqlFileUrl = sqlFileUrl;
213     }
214 
215     /**
216      * Set the repositoryPath to use
217      * 
218      * @param repositoryPath
219      *            The repositoryPath
220      */
221     public void setRepositoryPath(String repositoryPath) {
222         this.repositoryPath = repositoryPath;
223     }
224 
225     /**
226      * Set the datasources
227      * 
228      * @param datasources
229      *            The datasources
230      */
231     public void setDataSources(DataSourceSelector datasources) {
232         this.datasources = datasources;
233     }
234 
235     /**
236      * Sets the filesystem service
237      * 
238      * @param system The filesystem service
239      */
240     private void setFileSystem(FileSystem system) {
241         this.fileSystem = system;
242     }
243 
244 
245     /**
246      * Set the datasource
247      * 
248      * @param datasource
249      *            the datasource
250      */
251     public void setDataSource(DataSourceComponent datasource) {
252         this.datasource = datasource;
253     }
254 
255     /**
256      * Setup the temporary blocking time
257      * 
258      * @param tempBlockTime
259      *            The temporary blocking time 
260      */
261     public void setTempBlockTime(String tempBlockTime) {
262         this.tempBlockTime = TimeConverter.getMilliSeconds(tempBlockTime);
263     }
264 
265     /**
266      * Setup the autowhitelist lifetime for which we should whitelist a triplet.
267      * After this lifetime the record will be deleted
268      * 
269      * @param autoWhiteListLifeTime
270      *            The lifeTime 
271      */
272     public void setAutoWhiteListLifeTime(String autoWhiteListLifeTime) {
273         this.autoWhiteListLifeTime = TimeConverter.getMilliSeconds(autoWhiteListLifeTime);
274     }
275 
276     /**
277      * Set up the liftime of only once seen triplet. After this liftime the
278      * record will be deleted
279      * 
280      * @param unseenLifeTime
281      *            The lifetime 
282      */
283     public void setUnseenLifeTime(String unseenLifeTime) {
284         this.unseenLifeTime = TimeConverter.getMilliSeconds(unseenLifeTime);
285     }
286 
287     /**
288      * @see org.apache.james.smtpserver.CommandHandler#onCommand(SMTPSession)
289      */
290     public void onCommand(SMTPSession session) {
291         if (!session.isRelayingAllowed() && !(session.isAuthRequired() && session.getUser() != null)) {
292 
293             if ((wNetworks == null) || (!wNetworks.matchInetNetwork(session.getRemoteIPAddress()))) {
294                 doGreyListCheck(session, session.getCommandArgument());
295             } else {
296                 getLogger().info("IpAddress " + session.getRemoteIPAddress() + " is whitelisted. Skip greylisting.");
297             }
298         } else {
299             getLogger().info("IpAddress " + session.getRemoteIPAddress() + " is allowed to send. Skip greylisting.");
300         }
301     }
302 
303     /**
304      * Handler method called upon receipt of a RCPT command. Calls a greylist
305      * check
306      * 
307      * 
308      * @param session
309      *            SMTP session object
310      * @param argument
311      */
312     private void doGreyListCheck(SMTPSession session, String argument) {
313         String recip = "";
314         String sender = "";
315         MailAddress recipAddress = (MailAddress) session.getState().get(SMTPSession.CURRENT_RECIPIENT);
316         MailAddress senderAddress = (MailAddress) session.getState().get(SMTPSession.SENDER);
317 
318         if (recipAddress != null) recip = recipAddress.toString();
319         if (senderAddress != null) sender = senderAddress.toString();
320     
321         long time = System.currentTimeMillis();
322         String ipAddress = session.getRemoteIPAddress();
323     
324         try {
325             long createTimeStamp = 0;
326             int count = 0;
327             
328             // get the timestamp when he triplet was last seen
329             Iterator data = getGreyListData(datasource.getConnection(), ipAddress, sender, recip);
330             
331             if (data.hasNext()) {
332                 createTimeStamp = Long.parseLong(data.next().toString());
333                 count = Integer.parseInt(data.next().toString());
334             }
335             
336             getLogger().debug("Triplet " + ipAddress + " | " + sender + " | " + recip  +" -> TimeStamp: " + createTimeStamp);
337 
338 
339             // if the timestamp is bigger as 0 we have allready a triplet stored
340             if (createTimeStamp > 0) {
341                 long acceptTime = createTimeStamp + tempBlockTime;
342         
343                 if ((time < acceptTime) && (count == 0)) {
344                     String responseString = "451 " + DSNStatus.getStatus(DSNStatus.TRANSIENT, DSNStatus.NETWORK_DIR_SERVER) 
345                         + " Temporary rejected: Reconnect to fast. Please try again later";
346 
347                     // reconnect to fast block it again
348                     session.writeResponse(responseString);
349                     session.setStopHandlerProcessing(true);
350 
351                 } else {
352                     
353                     getLogger().debug("Update triplet " + ipAddress + " | " + sender + " | " + recip + " -> timestamp: " + time);
354                     
355                     // update the triplet..
356                     updateTriplet(datasource.getConnection(), ipAddress, sender, recip, count, time);
357 
358                 }
359             } else {
360                 getLogger().debug("New triplet " + ipAddress + " | " + sender + " | " + recip );
361            
362                 // insert a new triplet
363                 insertTriplet(datasource.getConnection(), ipAddress, sender, recip, count, time);
364       
365                 // Tempory block on new triplet!
366                 String responseString = "451 " + DSNStatus.getStatus(DSNStatus.TRANSIENT, DSNStatus.NETWORK_DIR_SERVER) 
367                     + " Temporary rejected: Please try again later";
368 
369                 session.writeResponse(responseString);
370                 session.setStopHandlerProcessing(true);
371             }
372 
373             // some kind of random cleanup process
374             if (Math.random() > 0.99) {
375                 // cleanup old entries
376             
377                 getLogger().debug("Delete old entries");
378             
379                 cleanupAutoWhiteListGreyList(datasource.getConnection(),(time - autoWhiteListLifeTime));
380                 cleanupGreyList(datasource.getConnection(), (time - unseenLifeTime));
381             }
382 
383         } catch (SQLException e) {
384             // just log the exception
385             getLogger().error("Error on SQLquery: " + e.getMessage());
386         }
387     }
388 
389     /**
390      * Get all necessary data for greylisting based on provided triplet
391      * 
392      * @param conn
393      *            The Connection
394      * @param ipAddress
395      *            The ipAddress of the client
396      * @param sender
397      *            The mailFrom
398      * @param recip
399      *            The rcptTo
400      * @return data
401      *            The data
402      * @throws SQLException
403      */
404     private Iterator getGreyListData(Connection conn, String ipAddress,
405         String sender, String recip) throws SQLException {
406         Collection data = new ArrayList(2);
407         PreparedStatement mappingStmt = null;
408         try {
409             mappingStmt = conn.prepareStatement(selectQuery);
410             ResultSet mappingRS = null;
411             try {
412                 mappingStmt.setString(1, ipAddress);
413                 mappingStmt.setString(2, sender);
414                 mappingStmt.setString(3, recip);
415                 mappingRS = mappingStmt.executeQuery();
416 
417                 if (mappingRS.next()) {
418                     data.add(String.valueOf(mappingRS.getTimestamp(1).getTime()));
419                     data.add(String.valueOf(mappingRS.getInt(2)));
420                 }
421             } finally {
422                 theJDBCUtil.closeJDBCResultSet(mappingRS);
423             }
424         } finally {
425             theJDBCUtil.closeJDBCStatement(mappingStmt);
426             theJDBCUtil.closeJDBCConnection(conn);
427         }
428         return data.iterator();
429     }
430 
431     /**
432      * Insert new triplet in the store
433      * 
434      * @param conn
435      *            The Connection
436      * @param ipAddress
437      *            The ipAddress of the client
438      * @param sender
439      *            The mailFrom
440      * @param recip
441      *            The rcptTo
442      * @param count
443      *            The count
444      * @param createTime
445      *            The createTime
446      * @throws SQLException
447      */
448     private void insertTriplet(Connection conn, String ipAddress,
449         String sender, String recip, int count, long createTime)
450         throws SQLException {
451 
452         PreparedStatement mappingStmt = null;
453 
454         try {
455             mappingStmt = conn.prepareStatement(insertQuery);
456 
457             mappingStmt.setString(1, ipAddress);
458             mappingStmt.setString(2, sender);
459             mappingStmt.setString(3, recip);
460             mappingStmt.setInt(4, count);
461             mappingStmt.setTimestamp(5, new Timestamp(createTime));
462             mappingStmt.executeUpdate();
463         } finally {
464             theJDBCUtil.closeJDBCStatement(mappingStmt);
465             theJDBCUtil.closeJDBCConnection(conn);
466         }
467     }
468 
469     /**
470      * Update the triplet
471      * 
472      * @param conn
473      *            The Connection
474      * 
475      * @param ipAddress
476      *            The ipAddress of the client
477      * @param sender
478      *            The mailFrom
479      * @param recip
480      *            The rcptTo
481      * @param count
482      *            The count
483      * @param time
484      *            the current time in ms
485      * @throws SQLException
486      */
487     private void updateTriplet(Connection conn, String ipAddress,
488         String sender, String recip, int count, long time)
489         throws SQLException {
490 
491         PreparedStatement mappingStmt = null;
492 
493         try {
494             mappingStmt = conn.prepareStatement(updateQuery);
495             mappingStmt.setTimestamp(1, new Timestamp(time));
496             mappingStmt.setInt(2, (count + 1));
497             mappingStmt.setString(3, ipAddress);
498             mappingStmt.setString(4, sender);
499             mappingStmt.setString(5, recip);
500             mappingStmt.executeUpdate();
501         } finally {
502             theJDBCUtil.closeJDBCStatement(mappingStmt);
503             theJDBCUtil.closeJDBCConnection(conn);
504         }
505     }
506 
507     /**
508      * Init the dataSource
509      * 
510      * @param repositoryPath
511      *            The repositoryPath
512      * @return dataSource The DataSourceComponent
513      * @throws ServiceException
514      * @throws SQLException
515      */
516     private DataSourceComponent initDataSource(String repositoryPath)
517         throws ServiceException, SQLException {
518 
519         int stindex = repositoryPath.indexOf("://") + 3;
520         String datasourceName = repositoryPath.substring(stindex);
521 
522         return (DataSourceComponent) datasources.select(datasourceName);
523     }
524 
525     /**
526      * Cleanup the autowhitelist
527      * 
528      * @param conn
529      *            The Connection
530      * @param time
531      *            The time which must be reached before delete the records
532      * @throws SQLException
533      */
534     private void cleanupAutoWhiteListGreyList(Connection conn, long time)
535         throws SQLException {
536         PreparedStatement mappingStmt = null;
537 
538         try {
539             mappingStmt = conn.prepareStatement(deleteAutoWhiteListQuery);
540 
541             mappingStmt.setTimestamp(1, new Timestamp(time));
542 
543             mappingStmt.executeUpdate();
544         } finally {
545             theJDBCUtil.closeJDBCStatement(mappingStmt);
546             theJDBCUtil.closeJDBCConnection(conn);
547         }
548     }
549 
550     /**
551      * Cleanup the autowhitelist
552      * 
553      * @param conn
554      *            The Connection
555      * @param time
556      *            The time which must be reached before delete the records
557      * @throws SQLException
558      */
559     private void cleanupGreyList(Connection conn, long time)
560         throws SQLException {
561         PreparedStatement mappingStmt = null;
562 
563         try {
564             mappingStmt = conn.prepareStatement(deleteQuery);
565 
566             mappingStmt.setTimestamp(1, new Timestamp(time));
567 
568             mappingStmt.executeUpdate();
569         } finally {
570             theJDBCUtil.closeJDBCStatement(mappingStmt);
571             theJDBCUtil.closeJDBCConnection(conn);
572         }
573     }
574 
575     /**
576      * The JDBCUtil helper class
577      */
578     private final JDBCUtil theJDBCUtil = new JDBCUtil() {
579         protected void delegatedLog(String logString) {
580             getLogger().debug("JDBCVirtualUserTable: " + logString);
581         }
582     };
583 
584     /**
585      * Initializes the sql query environment from the SqlResources file. Will
586      * look for conf/sqlResources.xml.
587      * 
588      * @param conn
589      *            The connection for accessing the database
590      * @param sqlFileUrl
591      *            The url which we use to get the sql file
592      * @throws Exception
593      *             If any error occurs
594      */
595     public void initSqlQueries(Connection conn, String sqlFileUrl)
596         throws Exception {
597         try {
598 
599             InputStream sqlFile = null;
600     
601             try {
602                 sqlFile = fileSystem.getResource(sqlFileUrl);
603                 sqlFileUrl = null;
604             } catch (Exception e) {
605                 getLogger().fatalError(e.getMessage(), e);
606                 throw e;
607             }
608 
609             sqlQueries.init(sqlFile, "GreyList", conn, sqlParameters);
610 
611             selectQuery = sqlQueries.getSqlString("selectQuery", true);
612             insertQuery = sqlQueries.getSqlString("insertQuery", true);
613             deleteQuery = sqlQueries.getSqlString("deleteQuery", true);
614             deleteAutoWhiteListQuery = sqlQueries.getSqlString("deleteAutoWhitelistQuery", true);
615             updateQuery = sqlQueries.getSqlString("updateQuery", true);
616 
617         } finally {
618             theJDBCUtil.closeJDBCConnection(conn);
619         }
620     }
621 
622     /**
623      * Create the table if not exists.
624      * 
625      * @param conn
626      *            The connection
627      * @param tableNameSqlStringName
628      *            The tableSqlname
629      * @param createSqlStringName
630      *            The createSqlname
631      * @return true or false
632      * @throws SQLException
633      */
634     private boolean createTable(Connection conn, String tableNameSqlStringName,
635     String createSqlStringName) throws SQLException {
636         String tableName = sqlQueries.getSqlString(tableNameSqlStringName, true);
637 
638         DatabaseMetaData dbMetaData = conn.getMetaData();
639 
640         // Try UPPER, lower, and MixedCase, to see if the table is there.
641         if (theJDBCUtil.tableExists(dbMetaData, tableName)) {
642             return false;
643         }
644 
645         PreparedStatement createStatement = null;
646 
647         try {
648             createStatement = conn.prepareStatement(sqlQueries.getSqlString(createSqlStringName, true));
649             createStatement.execute();
650 
651             StringBuffer logBuffer = null;
652             logBuffer = new StringBuffer(64).append("Created table '").append(tableName)
653             .append("' using sqlResources string '")
654             .append(createSqlStringName).append("'.");
655         getLogger().info(logBuffer.toString());
656 
657         } finally {
658             theJDBCUtil.closeJDBCStatement(createStatement);
659         }
660         return true;
661     }
662 
663     /**
664      * Return a Collection which holds the values of the given string splitted
665      * on ","
666      * 
667      * @param networks
668      *            The commaseperated list of values
669      * @return wNetworks The Collection which holds the whitelistNetworks
670      */
671     private Collection whitelistedNetworks(String networks) {
672         Collection wNetworks = null;
673         StringTokenizer st = new StringTokenizer(networks, ", ", false);
674         wNetworks = new ArrayList();
675         
676         while (st.hasMoreTokens())
677             wNetworks.add(st.nextToken());
678         return wNetworks;
679     }
680 
681     public Collection getImplCommands() {
682         Collection c = new ArrayList();
683         c.add("RCPT");
684         return c;
685     }
686 }