1 /*
2  * Copyright (C) 2005-2008 Jive Software. All rights reserved.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package org.jivesoftware.openfire.nio;
18 
19 import org.apache.mina.core.service.IoHandlerAdapter;
20 import org.apache.mina.core.session.IdleStatus;
21 import org.apache.mina.core.session.IoSession;
22 import org.apache.mina.core.write.WriteException;
23 import org.dom4j.io.XMPPPacketReader;
24 import org.jivesoftware.openfire.Connection;
25 import org.jivesoftware.openfire.net.MXParser;
26 import org.jivesoftware.openfire.net.ServerTrafficCounter;
27 import org.jivesoftware.openfire.net.StanzaHandler;
28 import org.jivesoftware.openfire.spi.ConnectionConfiguration;
29 import org.jivesoftware.util.JiveGlobals;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
32 import org.xmlpull.v1.XmlPullParserException;
33 import org.xmlpull.v1.XmlPullParserFactory;
34 import org.xmpp.packet.StreamError;
35 
36 import java.nio.charset.StandardCharsets;
37 
38 /**
39  * A ConnectionHandler is responsible for creating new sessions, destroying sessions and delivering
40  * received XML stanzas to the proper StanzaHandler.
41  *
42  * @author Gaston Dombiak
43  */
44 public abstract class ConnectionHandler extends IoHandlerAdapter {
45 
46     private static final Logger Log = LoggerFactory.getLogger(ConnectionHandler.class);
47 
48     static final String XML_PARSER = "XML-PARSER";
49     static final String HANDLER = "HANDLER";
50     static final String CONNECTION = "CONNECTION";
51 
52     private static final ThreadLocal<XMPPPacketReader> PARSER_CACHE = new ThreadLocal<XMPPPacketReader>()
53             {
54                @Override
55                protected XMPPPacketReader initialValue()
56                {
57                   final XMPPPacketReader parser = new XMPPPacketReader();
58                   parser.setXPPFactory( factory );
59                   return parser;
60                }
61             };
62     /**
63      * Reuse the same factory for all the connections.
64      */
65     private static XmlPullParserFactory factory = null;
66 
67     static {
68         try {
69             factory = XmlPullParserFactory.newInstance(MXParser.class.getName(), null);
70             factory.setNamespaceAware(true);
71         }
72         catch (XmlPullParserException e) {
73             Log.error("Error creating a parser factory", e);
74         }
75     }
76 
77     /**
78      * The configuration for new connections.
79      */
80     protected final ConnectionConfiguration configuration;
81 
ConnectionHandler( ConnectionConfiguration configuration )82     protected ConnectionHandler( ConnectionConfiguration configuration ) {
83         this.configuration = configuration;
84     }
85 
86     @Override
sessionOpened(IoSession session)87     public void sessionOpened(IoSession session) throws Exception {
88         // Create a new XML parser for the new connection. The parser will be used by the XMPPDecoder filter.
89         final XMLLightweightParser parser = new XMLLightweightParser(StandardCharsets.UTF_8);
90         session.setAttribute(XML_PARSER, parser);
91         // Create a new NIOConnection for the new session
92         final NIOConnection connection = createNIOConnection(session);
93         session.setAttribute(CONNECTION, connection);
94         session.setAttribute(HANDLER, createStanzaHandler(connection));
95         // Set the max time a connection can be idle before closing it. This amount of seconds
96         // is divided in two, as Openfire will ping idle clients first (at 50% of the max idle time)
97         // before disconnecting them (at 100% of the max idle time). This prevents Openfire from
98         // removing connections without warning.
99         final int idleTime = getMaxIdleTime() / 2;
100         if (idleTime > 0) {
101             session.getConfig().setIdleTime(IdleStatus.READER_IDLE, idleTime);
102         }
103     }
104 
105     @Override
sessionClosed(IoSession session)106     public void sessionClosed(IoSession session) throws Exception {
107         final Connection connection = (Connection) session.getAttribute(CONNECTION);
108         if ( connection != null ) {
109             connection.close();
110         }
111     }
112 
113     /**
114      * Invoked when a MINA session has been idle for half of the allowed XMPP
115      * session idle time as specified by {@link #getMaxIdleTime()}. This method
116      * will be invoked each time that such a period passes (even if no IO has
117      * occurred in between).
118      *
119      * Openfire will disconnect a session the second time this method is
120      * invoked, if no IO has occurred between the first and second invocation.
121      * This allows extensions of this class to use the first invocation to check
122      * for livelyness of the MINA session (e.g by polling the remote entity, as
123      * {@link ClientConnectionHandler} does).
124      *
125      * @see IoHandlerAdapter#sessionIdle(IoSession, IdleStatus)
126      */
127     @Override
sessionIdle(IoSession session, IdleStatus status)128     public void sessionIdle(IoSession session, IdleStatus status) throws Exception {
129         if (session.getIdleCount(status) > 1) {
130             // Get the connection for this session
131             final Connection connection = (Connection) session.getAttribute(CONNECTION);
132             if (connection != null) {
133                 // Close idle connection
134                 if (Log.isDebugEnabled()) {
135                     Log.debug("ConnectionHandler: Closing connection that has been idle: " + connection);
136                 }
137                 connection.close();
138             }
139         }
140     }
141 
142     @Override
exceptionCaught(IoSession session, Throwable cause)143     public void exceptionCaught(IoSession session, Throwable cause) throws Exception {
144         Log.warn("Closing connection due to exception in session: " + session, cause);
145 
146         try {
147             // OF-524: Determine stream:error message.
148             final StreamError error;
149             if ( cause != null && (cause instanceof XMLNotWellFormedException || (cause.getCause() != null && cause.getCause() instanceof XMLNotWellFormedException) ) ) {
150                 error = new StreamError( StreamError.Condition.not_well_formed );
151             } else {
152                 error = new StreamError( StreamError.Condition.internal_server_error );
153             }
154 
155             final Connection connection = (Connection) session.getAttribute( CONNECTION );
156 
157             // OF-1784: Don't write an error when the source problem is an issue with writing data.
158             if ( JiveGlobals.getBooleanProperty( "xmpp.skip-error-delivery-on-write-error.disable", false ) || !(cause instanceof WriteException) ) {
159                 connection.deliverRawText( error.toXML() );
160             }
161         } finally {
162             final Connection connection = (Connection) session.getAttribute( CONNECTION );
163             if (connection != null) {
164                 connection.close();
165             }
166         }
167     }
168 
169     @Override
messageReceived(IoSession session, Object message)170     public void messageReceived(IoSession session, Object message) throws Exception {
171         // Get the stanza handler for this session
172         StanzaHandler handler = (StanzaHandler) session.getAttribute(HANDLER);
173         // Get the parser to use to process stanza. For optimization there is going
174         // to be a parser for each running thread. Each Filter will be executed
175         // by the Executor placed as the first Filter. So we can have a parser associated
176         // to each Thread
177         final XMPPPacketReader parser = PARSER_CACHE.get();
178         // Update counter of read btyes
179         updateReadBytesCounter(session);
180         //System.out.println("RCVD: " + message);
181         // Let the stanza handler process the received stanza
182         try {
183             handler.process((String) message, parser);
184         } catch (Throwable e) { // Make sure to catch Throwable, not (only) Exception! See OF-2367
185             Log.error("Closing connection due to error while processing message: {}", message, e);
186             final Connection connection = (Connection) session.getAttribute(CONNECTION);
187             if ( connection != null ) {
188                 connection.close();
189             }
190         }
191     }
192 
193     @Override
messageSent(IoSession session, Object message)194     public void messageSent(IoSession session, Object message) throws Exception {
195         super.messageSent(session, message);
196         // Update counter of written btyes
197         updateWrittenBytesCounter(session);
198         //System.out.println("SENT: " + Charset.forName("UTF-8").decode(((ByteBuffer)message).buf()));
199     }
200 
createNIOConnection(IoSession session)201     abstract NIOConnection createNIOConnection(IoSession session);
202 
createStanzaHandler(NIOConnection connection)203     abstract StanzaHandler createStanzaHandler(NIOConnection connection);
204 
205     /**
206      * Returns the max number of seconds a connection can be idle (both ways) before
207      * being closed.<p>
208      *
209      * @return the max number of seconds a connection can be idle.
210      */
getMaxIdleTime()211     abstract int getMaxIdleTime();
212 
213     /**
214      * Updates the system counter of read bytes. This information is used by the incoming
215      * bytes statistic.
216      *
217      * @param session the session that read more bytes from the socket.
218      */
updateReadBytesCounter(IoSession session)219     private void updateReadBytesCounter(IoSession session) {
220         long currentBytes = session.getReadBytes();
221         Long prevBytes = (Long) session.getAttribute("_read_bytes");
222         long delta;
223         if (prevBytes == null) {
224             delta = currentBytes;
225         }
226         else {
227             delta = currentBytes - prevBytes;
228         }
229         session.setAttribute("_read_bytes", currentBytes);
230         ServerTrafficCounter.incrementIncomingCounter(delta);
231     }
232 
233     /**
234      * Updates the system counter of written bytes. This information is used by the outgoing
235      * bytes statistic.
236      *
237      * @param session the session that wrote more bytes to the socket.
238      */
updateWrittenBytesCounter(IoSession session)239     private void updateWrittenBytesCounter(IoSession session) {
240         long currentBytes = session.getWrittenBytes();
241         Long prevBytes = (Long) session.getAttribute("_written_bytes");
242         long delta;
243         if (prevBytes == null) {
244             delta = currentBytes;
245         }
246         else {
247             delta = currentBytes - prevBytes;
248         }
249         session.setAttribute("_written_bytes", currentBytes);
250         ServerTrafficCounter.incrementOutgoingCounter(delta);
251     }
252 }
253