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