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  
22  package org.apache.james.socket;
23  
24  import org.apache.avalon.cornerstone.services.connection.ConnectionHandler;
25  import org.apache.avalon.excalibur.pool.Poolable;
26  import org.apache.avalon.framework.container.ContainerUtil;
27  import org.apache.avalon.framework.logger.AbstractLogEnabled;
28  import org.apache.avalon.framework.logger.Logger;
29  import org.apache.avalon.framework.service.ServiceException;
30  import org.apache.avalon.framework.service.ServiceManager;
31  import org.apache.avalon.framework.service.Serviceable;
32  import org.apache.james.api.dnsservice.DNSService;
33  import org.apache.james.util.InternetPrintWriter;
34  import org.apache.james.util.watchdog.Watchdog;
35  import org.apache.james.util.watchdog.WatchdogTarget;
36  
37  import java.io.BufferedInputStream;
38  import java.io.BufferedOutputStream;
39  import java.io.File;
40  import java.io.FileOutputStream;
41  import java.io.IOException;
42  import java.io.InputStream;
43  import java.io.InterruptedIOException;
44  import java.io.OutputStream;
45  import java.io.PrintWriter;
46  import java.net.Socket;
47  import java.net.SocketException;
48  
49  /**
50   * Common Handler code
51   */
52  public abstract class AbstractJamesHandler extends AbstractLogEnabled implements ConnectionHandler, Poolable,Serviceable {
53  
54  
55      private static final int DEFAULT_OUTPUT_BUFFER_SIZE = 1024;
56  
57      private static final int DEFAULT_INPUT_BUFFER_SIZE = 1024;
58  
59      /** Name used by default */
60      private static final String DEFAULT_NAME = "Handler-ANON";
61  
62      /**
63       * The thread executing this handler
64       */
65      private Thread handlerThread;
66  
67      /**
68       * The TCP/IP socket over which the service interaction
69       * is occurring
70       */
71      protected Socket socket;
72  
73      /**
74       * The writer to which outgoing messages are written.
75       */
76      protected PrintWriter out;
77      
78      /**
79       * The incoming stream of bytes coming from the socket.
80       */
81      protected InputStream in;
82  
83      /**
84       * The reader associated with incoming characters.
85       */
86      protected CRLFTerminatedReader inReader;
87  
88      /**
89       * The socket's output stream
90       */
91      protected OutputStream outs;
92      
93      /**
94       * The watchdog being used by this handler to deal with idle timeouts.
95       */
96      protected Watchdog theWatchdog;
97  
98      /**
99       * The watchdog target that idles out this handler.
100      */
101     private WatchdogTarget theWatchdogTarget = new JamesWatchdogTarget();
102 
103     /**
104      * This method will be implemented checking for the correct class
105      * type.
106      * 
107      * @param theData Configuration Bean.
108      */
109     public abstract void setConfigurationData(Object theData);
110     
111 
112     /**
113      * The remote host name obtained by lookup on the socket.
114      */
115     protected String remoteHost = null;
116 
117     /**
118      * The remote IP address of the socket.
119      */
120     protected String remoteIP = null;
121 
122     /**
123      * The DNSService
124      */
125     protected DNSService dnsServer = null;
126 
127     /**
128      * Used for debug: if not null enable tcp stream dump.
129      */
130     private String tcplogprefix = null;
131 
132     /**
133      * Names the handler.
134      * This name is used for contextual logging.
135      */
136     private String name = DEFAULT_NAME;
137     
138 
139     /**
140      * @see org.apache.avalon.framework.service.Serviceable#service(org.apache.avalon.framework.service.ServiceManager)
141      */
142     public void service(ServiceManager arg0) throws ServiceException {
143         setDnsServer((DNSService) arg0.lookup(DNSService.ROLE));
144     }
145 
146     /**
147      * Helper method for accepting connections.
148      * This MUST be called in the specializations.
149      *
150      * @param connection The Socket which belongs to the connection 
151      * @throws IOException get thrown if an IO error is detected
152      */
153     protected void initHandler( Socket connection ) throws IOException {
154         this.socket = connection;
155         remoteIP = socket.getInetAddress().getHostAddress();
156         remoteHost = dnsServer.getHostName(socket.getInetAddress());
157 
158         try {
159             synchronized (this) {
160                 handlerThread = Thread.currentThread();
161             }
162             in = new BufferedInputStream(socket.getInputStream(), DEFAULT_INPUT_BUFFER_SIZE);
163             outs = new BufferedOutputStream(socket.getOutputStream(), DEFAULT_OUTPUT_BUFFER_SIZE);
164             // enable tcp dump for debug
165             if (tcplogprefix != null) {
166                 outs = new SplitOutputStream(outs, new FileOutputStream(tcplogprefix+"out"));
167                 in = new CopyInputStream(in, new FileOutputStream(tcplogprefix+"in"));
168             }
169             
170             // An ASCII encoding can be used because all transmissions other
171             // that those in the message body command are guaranteed
172             // to be ASCII
173             inReader = new CRLFTerminatedReader(in, "ASCII");
174             
175             out = new InternetPrintWriter(outs, true);
176         } catch (RuntimeException e) {
177             StringBuffer exceptionBuffer = 
178                 new StringBuffer(256)
179                     .append("[" + toString() + "] Unexpected exception opening from ")
180                     .append(remoteHost)
181                     .append(" (")
182                     .append(remoteIP)
183                     .append("): ")
184                     .append(e.getMessage());
185             String exceptionString = exceptionBuffer.toString();
186             getLogger().error(exceptionString, e);
187             throw e;
188         } catch (IOException e) {
189             StringBuffer exceptionBuffer = 
190                 new StringBuffer(256)
191                     .append("[" + toString() + "] Cannot open connection from ")
192                     .append(remoteHost)
193                     .append(" (")
194                     .append(remoteIP)
195                     .append("): ")
196                     .append(e.getMessage());
197             String exceptionString = exceptionBuffer.toString();
198             getLogger().error(exceptionString, e);
199             throw e;
200         }
201         
202         if (getLogger().isInfoEnabled()) {
203             StringBuffer infoBuffer =
204                 new StringBuffer(128)
205                         .append("[" + toString() + "]Connection from ")
206                         .append(remoteHost)
207                         .append(" (")
208                         .append(remoteIP)
209                         .append(")");
210             getLogger().info(infoBuffer.toString());
211         }
212     }
213 
214     /**
215      * The method clean up and close the allocated resources
216      */
217     private void cleanHandler() {
218         // Clear the Watchdog
219         if (theWatchdog != null) {
220             ContainerUtil.dispose(theWatchdog);
221             theWatchdog = null;
222         }
223 
224         // Clear the streams
225         try {
226             if (inReader != null) {
227                 inReader.close();
228             }
229         } catch (IOException ioe) {
230             getLogger().warn("[" + toString() + "] Unexpected exception occurred while closing reader: " + ioe);
231         } finally {
232             inReader = null;
233         }
234 
235         in = null;
236 
237         if (out != null) {
238             out.close();
239             out = null;
240         }
241         outs = null;
242 
243         try {
244             if (socket != null) {
245                 socket.close();
246             }
247         } catch (IOException ioe) {
248             getLogger().warn("[" + toString() + "] Unexpected exception occurred while closing socket: " + ioe);
249         } finally {
250             socket = null;
251         }
252         
253         remoteIP = null;
254         remoteHost = null;
255 
256         synchronized (this) {
257             handlerThread = null;
258         }
259     }
260 
261     /**
262      * @see org.apache.avalon.cornerstone.services.connection.ConnectionHandler#handleConnection(java.net.Socket)
263      */
264     public void handleConnection(Socket connection) throws IOException {
265         initHandler(connection);
266 
267         final Logger logger = getLogger();
268         try {
269             
270             // Do something:
271             handleProtocol();
272             
273             logger.debug("[" + toString() + "] Closing socket");
274         } catch (SocketException se) {
275             // Indicates a problem at the underlying protocol level
276             if (logger.isWarnEnabled()) {
277                 String message =
278                     new StringBuffer(64)
279                         .append("[" + toString() + "]Socket to ")
280                         .append(remoteHost)
281                         .append(" (")
282                         .append(remoteIP)
283                         .append("): ")
284                         .append(se.getMessage()).toString();
285                 logger.warn(message);
286                 logger.debug(se.getMessage(), se);
287             }
288         } catch ( InterruptedIOException iioe ) {
289             if (logger.isErrorEnabled()) {
290                 StringBuffer errorBuffer =
291                     new StringBuffer(64)
292                         .append("[" + toString() + "] Socket to ")
293                         .append(remoteHost)
294                         .append(" (")
295                         .append(remoteIP)
296                         .append(") timeout.");
297                 logger.error( errorBuffer.toString(), iioe );
298             }
299         } catch ( IOException ioe ) {
300             if (logger.isWarnEnabled()) {
301                 String message =
302                     new StringBuffer(256)
303                             .append("[" + toString() + "] Exception handling socket to ")
304                             .append(remoteHost)
305                             .append(" (")
306                             .append(remoteIP)
307                             .append(") : ")
308                             .append(ioe.getMessage()).toString();
309                 logger.warn(message);
310                 logger.debug( ioe.getMessage(), ioe );
311             }
312         } catch (RuntimeException e) {
313             errorHandler(e);
314         } finally {
315             //Clear all the session state variables
316             cleanHandler();
317             resetHandler();
318         }
319     }
320 
321     /**
322      * Method which will be colled on error
323      *  
324      * @param e the RuntimeException
325      */
326     protected void errorHandler(RuntimeException e) {
327         if (getLogger().isErrorEnabled()) {
328             getLogger().error( "[" + toString() + "] Unexpected runtime exception: "
329                                + e.getMessage(), e );
330         }
331     }
332 
333 
334     /**
335      * Handle the protocol
336      * 
337      * @throws IOException get thrown if an IO error is detected
338      */
339     protected abstract void handleProtocol() throws IOException;
340 
341    /**
342     * Resets the handler data to a basic state.
343     */
344     protected abstract void resetHandler();
345 
346 
347     /**
348      * Set the Watchdog for use by this handler.
349      *
350      * @param theWatchdog the watchdog
351      */
352     public void setWatchdog(Watchdog theWatchdog) {
353         this.theWatchdog = theWatchdog;
354     }
355 
356     /**
357      * Gets the Watchdog Target that should be used by Watchdogs managing
358      * this connection.
359      *
360      * @return the WatchdogTarget
361      */
362     WatchdogTarget getWatchdogTarget() {
363         return theWatchdogTarget;
364     }
365 
366     /**
367      * Idle out this connection
368      */
369     void idleClose() {
370         if (getLogger() != null) {
371             getLogger().error("[" + toString() + "] Service Connection has idled out.");
372         }
373         try {
374             if (socket != null) {
375                 socket.close();
376             }
377         } catch (Exception e) {
378             // ignored
379         } finally {
380             socket = null;
381         }
382 
383         synchronized (this) {
384             // Interrupt the thread to recover from internal hangs
385             if (handlerThread != null) {
386                 handlerThread.interrupt();
387                 handlerThread = null;
388             }
389         }
390 
391     }
392 
393     /**
394      * This method logs at a "DEBUG" level the response string that
395      * was sent to the service client.  The method is provided largely
396      * as syntactic sugar to neaten up the code base.  It is declared
397      * private and final to encourage compiler inlining.
398      *
399      * @param responseString the response string sent to the client
400      */
401     private final void logResponseString(String responseString) {
402         if (getLogger().isDebugEnabled()) {
403             getLogger().debug("[" + toString() + "] Sent: " + responseString);
404         }
405     }
406 
407     /**
408      * Write and flush a response string.  The response is also logged.
409      * Should be used for the last line of a multi-line response or
410      * for a single line response.
411      *
412      * @param responseString the response string sent to the client
413      */
414     public final void writeLoggedFlushedResponse(String responseString) {
415         out.println(responseString);
416         out.flush();
417         logResponseString(responseString);
418     }
419 
420     /**
421      * Write a response string.  The response is also logged.
422      * Used for multi-line responses.
423      *
424      * @param responseString the response string sent to the client
425      */
426     public final void writeLoggedResponse(String responseString) {
427         out.println(responseString);
428         logResponseString(responseString);
429     }
430 
431     /**
432      * A private inner class which serves as an adaptor
433      * between the WatchdogTarget interface and this
434      * handler class.
435      */
436     private class JamesWatchdogTarget
437         implements WatchdogTarget {
438 
439         /**
440          * @see org.apache.james.util.watchdog.WatchdogTarget#execute()
441          */
442         public void execute() {
443             AbstractJamesHandler.this.idleClose();
444         }
445 
446         /**
447          * Used for context sensitive logging
448          */
449         @Override
450         public String toString() {
451             return AbstractJamesHandler.this.toString();
452         }
453     }
454 
455     /**
456      * If not null, this will enable dump to file for tcp connections
457      * 
458      * @param streamDumpDir the dir
459      */
460     public void setStreamDumpDir(String streamDumpDir) {
461         if (streamDumpDir != null) {
462             String streamdumpDir=streamDumpDir;
463             this.tcplogprefix = streamdumpDir+"/" + getName() + "_TCP-DUMP."+System.currentTimeMillis()+".";
464             File logdir = new File(streamdumpDir);
465             if (!logdir.exists()) {
466                 logdir.mkdir();
467             }
468         } else {
469             this.tcplogprefix = null;
470         }
471     }
472 
473     public void setDnsServer(DNSService dnsServer) {
474         this.dnsServer = dnsServer;
475     }
476 
477     /**
478      * The name of this handler.
479      * Used for context sensitive logging.
480      * @return the name, not null
481      */
482     public final String getName() {
483         return name;
484     }
485 
486     /**
487      * The name of this handler.
488      * Note that this name should be file-system safe.
489      * Used for context sensitive logging.
490      * @param name the name to set
491      */
492     public final void setName(final String name) {
493         if (name == null) {
494             this.name = DEFAULT_NAME;
495         } else {
496             this.name = name;
497         }
498     }
499 
500     /**
501      * Use for context sensitive logging.
502      * @return the name of the handler
503      */
504     @Override
505     public String toString() {
506         return name;
507     }
508 }